修复:将页面异步副作用绑定到当前归属

This commit is contained in:
cieanyau committed 2026-11-07 04:00:10 +08:00
1 parent b3a66d97c8
commit eca3aa8fe5
6 files changed
+623 -90

No files matched your search

+21 -12
View File
@@ -11,6 +11,7 @@ definePageMeta({
alias: [`/forgot_password`],
});
import { toDisplayString } from "vue";
import { onScopeDispose } from "vue";
import { normalizeClass } from "vue";
import { createBlock } from "vue";
import { ref } from "vue";
@@ -30,7 +31,7 @@ import { useRoute } from "#app";
import { useRouter } from "#app";
import { useHead } from "#app";
import { NuxtLink } from "#components";
import { useApi } from "~/lib/auth.js";
import { useApi, useAuth } from "~/lib/auth.js";
import button from "openvue/button";
import message from "openvue/message";
import { n as auth_redirect_n } from "~/lib/auth-redirect.js";
@@ -79,6 +80,12 @@ var D = {
z = defineComponent({
__name: `forgot-password`,
setup(f) {
const session = useAuth().prepareLogin();
let disposed = false;
const isCurrent = () => !disposed && session.isCurrent();
onScopeDispose(() => {
disposed = true;
});
(useHead({
title: `找回密码 - 破碎工坊网络`,
}),
@@ -133,7 +140,7 @@ var D = {
}
// 校验账号、区号、验证码和两次密码后提交重置;失败交由验证码实例处理挑战刷新。
async function pe() {
if (G.value || Q.value) return;
if (!isCurrent() || G.value || Q.value) return;
W.value = ``;
let e = U.account.trim();
if (!e)
@@ -145,26 +152,28 @@ var D = {
return (W.value = `两次输入的密码不一致`);
G.value = !0;
try {
(await z.post(`/account/password_reset`, {
await z.post(`/account/password_reset`, {
type: H.value,
account: e,
phone_code: `86`,
code: U.code.trim(),
password: U.password,
re_password: U.re_password,
}),
await B.push({
path: `/login`,
query: {
reset: `1`,
...auth_redirect_n(V.query.redirect),
},
}));
});
if (!isCurrent()) return;
await B.push({
path: `/login`,
query: {
reset: `1`,
...auth_redirect_n(V.query.redirect),
},
});
} catch (e) {
if (!isCurrent()) return;
((W.value = e instanceof Error ? e.message : `重置失败`),
await ue(e));
} finally {
G.value = !1;
if (isCurrent()) G.value = !1;
}
}
// 手机号/邮箱输入、验证码和密码表单共享提交锁;成功导航由 pe 负责,渲染层不直接建立会话。
+52 -33
View File
@@ -10,6 +10,7 @@ definePageMeta({
});
import { toDisplayString } from "vue";
import { onMounted } from "vue";
import { onScopeDispose } from "vue";
import { normalizeClass } from "vue";
import { createBlock } from "vue";
import { ref } from "vue";
@@ -27,7 +28,7 @@ import { createVNode } from "vue";
import { openBlock } from "vue";
import { withModifiers } from "vue";
import { useRoute } from "#app";
import { useApi } from "~/lib/auth.js";
import { useApi, useAuth } from "~/lib/auth.js";
import dialog from "openvue/dialog";
import button from "openvue/button";
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
@@ -111,6 +112,12 @@ var ge = {
defineComponent({
__name: `index`,
setup(p) {
const session = useAuth().prepareLogin();
let disposed = false;
const isCurrent = () => !disposed && session.isCurrent();
onScopeDispose(() => {
disposed = true;
});
composables_t({
key: (e) => e.fullPath,
});
@@ -185,37 +192,39 @@ var ge = {
}
// 将指定 ID 标为已读,完成后清除选择并刷新列表与全局未读计数。
async function ze(e) {
if (!(!e.length || B.value || G.value)) {
if (isCurrent() && !(!e.length || B.value || G.value)) {
((B.value = !0), (U.value = ``));
try {
(await y.put(`/client_care/mail/read`, {
await y.put(`/client_care/mail/read`, {
id: e,
}),
(z.value = []),
await R(),
await D());
});
if (!isCurrent()) return;
z.value = [];
await refreshMessages();
} catch (e) {
U.value = e instanceof Error ? e.message : `操作失败`;
if (isCurrent())
U.value = e instanceof Error ? e.message : `操作失败`;
} finally {
B.value = !1;
if (isCurrent()) B.value = !1;
}
}
}
// 使用 all 标记请求服务端全部已读,不局限于当前可见页。
async function Be() {
if (!(B.value || G.value)) {
if (isCurrent() && !(B.value || G.value)) {
((B.value = !0), (U.value = ``));
try {
(await y.put(`/client_care/mail/read`, {
await y.put(`/client_care/mail/read`, {
all: 1,
}),
(z.value = []),
await R(),
await D());
});
if (!isCurrent()) return;
z.value = [];
await refreshMessages();
} catch (e) {
U.value = e instanceof Error ? e.message : `操作失败`;
if (isCurrent())
U.value = e instanceof Error ? e.message : `操作失败`;
} finally {
B.value = !1;
if (isCurrent()) B.value = !1;
}
}
}
@@ -231,24 +240,30 @@ var ge = {
}
// 仅提交选中消息的 ID;成功后同步列表与未读计数。
async function He() {
if (!z.value.length || G.value || B.value) return;
if (!isCurrent() || !z.value.length || G.value || B.value) return;
let e = z.value.map((e) => e.id);
((G.value = !0), (U.value = ``));
try {
(await y.del(`/client_care/mail`, {
await y.del(`/client_care/mail`, {
id: e,
}),
(W.value = !1),
(z.value = []),
await R(),
await D());
});
if (!isCurrent()) return;
W.value = !1;
z.value = [];
await refreshMessages();
} catch (e) {
if (!isCurrent()) return;
((W.value = !1),
(U.value = e instanceof Error ? e.message : `删除失败`));
} finally {
G.value = !1;
if (isCurrent()) G.value = !1;
}
}
async function refreshMessages() {
if (!isCurrent()) return;
await R();
if (isCurrent()) await D();
}
let K = ref(!1),
q = ref(null),
J = ref(!1),
@@ -257,7 +272,9 @@ var ge = {
Z = 0;
// 详情打开时清除旧正文并递增请求序号;加载成功后尝试标读,标读失败不覆盖已加载的正文。
async function Q(e) {
if (!isCurrent()) return;
let t = ++Z;
const ownsDetail = () => isCurrent() && t === Z;
((X.value = e),
(K.value = !0),
(J.value = !0),
@@ -265,7 +282,7 @@ var ge = {
(q.value = null));
try {
let n = await y.get(`/client_care/mail/${e}`);
if (t !== Z) return;
if (!ownsDetail()) return;
if (!n?.mail?.id) {
Y.value = `消息不存在或已删除`;
return;
@@ -274,23 +291,25 @@ var ge = {
let r = M.value.find((t) => t.id === e);
if (!r || r.read === 0)
try {
(await y.put(`/client_care/mail/read`, {
await y.put(`/client_care/mail/read`, {
id: [e],
}),
r && (r.read = 1),
await D());
});
if (!ownsDetail()) return;
if (r) r.read = 1;
await D();
} catch {}
} catch (e) {
t === Z && (Y.value = e instanceof Error ? e.message : `加载失败`);
ownsDetail() &&
(Y.value = e instanceof Error ? e.message : `加载失败`);
} finally {
t === Z && (J.value = !1);
ownsDetail() && (J.value = !1);
}
}
return (
onMounted(async () => {
await D();
let e = Number(T.query.id);
e && Q(e);
isCurrent() && e && Q(e);
}),
// 批量操作基于当前选择集,详情正文经公共清理后展示;详情 ID 与加载代次防止快速切换串信。
(t, i) => {
+26 -12
View File
@@ -9,6 +9,7 @@ import {
reactive,
ref,
useId,
watch,
} from "vue";
import Button from "openvue/button";
import Column from "openvue/column";
@@ -19,11 +20,12 @@ import Message from "openvue/message";
import Paginator from "openvue/paginator";
import Textarea from "openvue/textarea";
import ListEmpty from "~/components/ListEmpty.vue";
import { useApi } from "~/lib/auth.js";
import { useApi, useAuth } from "~/lib/auth.js";
import { n as loadCompleteList } from "~/lib/complete-list.js";
import "~/assets/styles/ssh-keys.WWTOf2zG.css";
const api = useApi();
const session = useAuth().prepareLogin();
const fieldId = useId();
const draftSearch = ref("");
const appliedSearch = ref("");
@@ -43,6 +45,16 @@ const copiedKeyId = ref(null);
let copyResetTimer;
let requestGeneration = 0;
let disposed = false;
let editorGeneration = 0;
const isCurrent = () => !disposed && session.isCurrent();
watch(
editorVisible,
() => {
editorGeneration++;
saving.value = false;
},
{ flush: "sync" },
);
const firstRecord = computed(() => (currentPage.value - 1) * pageSize.value);
const visibleRows = computed(() =>
@@ -53,7 +65,7 @@ const visibleRows = computed(() =>
// 仅允许当前加载代次且组件尚未卸载时修改列表反馈。
function isCurrentLoad(generation) {
return !disposed && generation === requestGeneration;
return isCurrent() && generation === requestGeneration;
}
// 删除或搜索后夹紧页码;返回是否变化,供服务端分页决定是否重读修正后的页。
@@ -67,7 +79,7 @@ function clampPage() {
// 非搜索模式请求当前服务端页,删除末页最后一条时先修正页码再重读。
async function loadKeys() {
if (disposed) return;
if (!isCurrent()) return;
if (appliedSearch.value) return loadSearchResults();
const generation = ++requestGeneration;
searchLoading.value = false;
@@ -174,7 +186,7 @@ function openEditDialog(key) {
// 校验名称非空、字符数上限和公钥非空;按编辑对象选择创建/更新,成功后刷新当前模式列表。
async function saveKey() {
if (saving.value || disposed) return;
if (saving.value || !isCurrent()) return;
actionError.value = "";
if (!keyForm.name.trim()) {
actionError.value = "请输入密钥名称";
@@ -189,6 +201,8 @@ async function saveKey() {
return;
}
saving.value = true;
const generation = editorGeneration;
const ownsDraft = () => isCurrent() && generation === editorGeneration;
try {
if (editingKey.value) {
await api.put("/ssh_key/" + editingKey.value.id, {
@@ -198,25 +212,25 @@ async function saveKey() {
} else {
await api.post("/ssh_key", { ...keyForm });
}
if (disposed) return;
editorVisible.value = false;
if (!isCurrent()) return;
if (ownsDraft()) editorVisible.value = false;
await loadKeys();
} catch (error) {
if (!disposed)
if (ownsDraft())
actionError.value = error instanceof Error ? error.message : "保存失败";
} finally {
if (!disposed) saving.value = false;
if (ownsDraft()) saving.value = false;
}
}
// 删除后复用加载逻辑;末页恢复由 loadKeys/clampPage 统一处理。
async function deleteKey(key) {
if (disposed) return;
if (!isCurrent()) return;
try {
await api.del("/ssh_key/" + key.id);
await loadKeys();
if (isCurrent()) await loadKeys();
} catch (error) {
if (!disposed)
if (isCurrent())
actionError.value = error instanceof Error ? error.message : "删除失败";
}
}
@@ -225,7 +239,7 @@ async function deleteKey(key) {
async function copyPublicKey(key) {
try {
await navigator.clipboard.writeText(key.public_key);
if (disposed) return;
if (!isCurrent()) return;
clearTimeout(copyResetTimer);
copiedKeyId.value = key.id;
copyResetTimer = setTimeout(() => (copiedKeyId.value = null), 2000);
+45 -24
View File
@@ -12,6 +12,7 @@ definePageMeta({
import { toDisplayString } from "vue";
import { renderList } from "vue";
import { onMounted } from "vue";
import { onScopeDispose } from "vue";
import { normalizeClass } from "vue";
import { createBlock } from "vue";
import { ref } from "vue";
@@ -30,7 +31,7 @@ import { openBlock } from "vue";
import { normalizeStyle } from "vue";
import { useRoute } from "#app";
import { NuxtLink } from "#components";
import { useApi } from "~/lib/auth.js";
import { useApi, useAuth } from "~/lib/auth.js";
import button from "openvue/button";
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
import message from "openvue/message";
@@ -124,6 +125,12 @@ var le = {
defineComponent({
__name: `[id]`,
setup(m) {
const session = useAuth().prepareLogin();
let disposed = false;
const isCurrent = () => !disposed && session.isCurrent();
onScopeDispose(() => {
disposed = true;
});
let y = useRoute(),
z = useApi(),
B = String(y.params.id ?? y.query.id ?? ``);
@@ -143,17 +150,20 @@ var le = {
const canOperate = computed(() => V.value?.can_operate === 1);
// 读取工单并确认实际存在;后续回复/催单/关闭完成也复用此入口同步服务端状态。
async function X() {
if (!isCurrent()) return;
try {
let e = await z.get(`/ticket/${B}`);
if (!isCurrent()) return;
if (!e.ticket || !e.ticket.id) {
U.value = `工单不存在或已删除`;
return;
}
V.value = e.ticket;
} catch (e) {
U.value = e instanceof Error ? e.message : `加载失败`;
if (isCurrent())
U.value = e instanceof Error ? e.message : `加载失败`;
} finally {
H.value = !1;
if (isCurrent()) H.value = !1;
}
}
let Z = computed(() => {
@@ -192,15 +202,16 @@ var le = {
);
// 工单允许操作时逐个上传并检查 save_name,成功部分保留在当前附件草稿。
async function xe(e) {
if (J.value || G.value || !canOperate.value) return;
if (!isCurrent() || J.value || G.value || !canOperate.value) return;
let t = e.target,
n = Array.from(t.files ?? []);
if (n.length) {
((J.value = !0), (U.value = ``));
try {
for (let e of n) {
if (!isCurrent() || !canOperate.value) return;
let t = await z.upload(e);
if (!canOperate.value) return;
if (!isCurrent() || !canOperate.value) return;
if (!t?.save_name)
throw Error(`附件「${e.name}」上传失败,请重新选择文件`);
q.value.push({
@@ -209,9 +220,10 @@ var le = {
});
}
} catch (e) {
U.value = e instanceof Error ? e.message : `附件上传失败`;
if (isCurrent())
U.value = e instanceof Error ? e.message : `附件上传失败`;
} finally {
((J.value = !1), (t.value = ``));
if (isCurrent()) ((J.value = !1), (t.value = ``));
}
}
}
@@ -254,47 +266,56 @@ var le = {
}
// 回复要求内容非空、附件已上传且服务端允许操作;成功后清空草稿与引用并重读工单。
async function De() {
if (!(G.value || !W.value.trim() || J.value || !canOperate.value)) {
if (
isCurrent() &&
!(G.value || !W.value.trim() || J.value || !canOperate.value)
) {
G.value = !0;
try {
(await z.post(`/ticket/${B}/reply`, {
await z.post(`/ticket/${B}/reply`, {
content: W.value.trim(),
attachment: q.value.map((e) => e.save_name),
quote_reply_id: Y.value,
}),
(W.value = ``),
(q.value = []),
(Y.value = 0),
await X());
});
if (!isCurrent()) return;
W.value = ``;
q.value = [];
Y.value = 0;
await X();
} catch (e) {
U.value = e instanceof Error ? e.message : `回复失败`;
if (isCurrent())
U.value = e instanceof Error ? e.message : `回复失败`;
} finally {
G.value = !1;
if (isCurrent()) G.value = !1;
}
}
}
// 催单后重新读取工单,结果与可继续操作状态以服务端为准。
async function Oe() {
if (!canOperate.value) return;
if (!isCurrent() || !canOperate.value) return;
K.value = !0;
try {
(await z.put(`/ticket/${B}/urge`), await X());
await z.put(`/ticket/${B}/urge`);
if (isCurrent()) await X();
} catch (e) {
U.value = e instanceof Error ? e.message : `催单失败`;
if (isCurrent())
U.value = e instanceof Error ? e.message : `催单失败`;
} finally {
K.value = !1;
if (isCurrent()) K.value = !1;
}
}
// 关闭工单完成后重读详情,由服务端权限控制回复与上传入口。
async function ke() {
if (!canOperate.value) return;
if (!isCurrent() || !canOperate.value) return;
K.value = !0;
try {
(await z.put(`/ticket/${B}/close`), await X());
await z.put(`/ticket/${B}/close`);
if (isCurrent()) await X();
} catch (e) {
U.value = e instanceof Error ? e.message : `关闭失败`;
if (isCurrent())
U.value = e instanceof Error ? e.message : `关闭失败`;
} finally {
K.value = !1;
if (isCurrent()) K.value = !1;
}
}
return (
+26 -9
View File
@@ -7,6 +7,7 @@ import "~/assets/styles/create.gX6gpD00.css";
import { toDisplayString } from "vue";
import { renderList } from "vue";
import { onMounted } from "vue";
import { onScopeDispose } from "vue";
import { createBlock } from "vue";
import { ref } from "vue";
import { withCtx } from "vue";
@@ -24,7 +25,7 @@ import { openBlock } from "vue";
import { withModifiers } from "vue";
import { useRouter } from "#app";
import { NuxtLink } from "#components";
import { useApi } from "~/lib/auth.js";
import { useApi, useAuth } from "~/lib/auth.js";
import button from "openvue/button";
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
import message from "openvue/message";
@@ -100,6 +101,12 @@ var le = {
defineComponent({
__name: `create`,
setup(m) {
const session = useAuth().prepareLogin();
let disposed = false;
const isCurrent = () => !disposed && session.isCurrent();
onScopeDispose(() => {
disposed = true;
});
let b = useApi(),
z = useRouter(),
B = ref([]),
@@ -136,14 +143,16 @@ var le = {
Z = ref(!1);
// 逐个上传并要求 save_name,上传部分成功的附件仍保留,失败显示具体文件问题。
async function he(e) {
if (Z.value || W.value) return;
if (!isCurrent() || Z.value || W.value) return;
let t = e.target,
n = Array.from(t.files ?? []);
if (n.length) {
((Z.value = !0), (G.value = ``));
try {
for (let e of n) {
if (!isCurrent()) return;
let t = await b.upload(e);
if (!isCurrent()) return;
if (!t?.save_name)
throw Error(`附件「${e.name}」上传失败,请重新选择文件`);
X.value.push({
@@ -152,9 +161,10 @@ var le = {
});
}
} catch (e) {
G.value = e instanceof Error ? e.message : `上传失败`;
if (isCurrent())
G.value = e instanceof Error ? e.message : `上传失败`;
} finally {
((Z.value = !1), (t.value = ``));
if (isCurrent()) ((Z.value = !1), (t.value = ``));
}
}
}
@@ -164,45 +174,50 @@ var le = {
}
// 类型/部门加载成功后再开放提交;实例可选列表分页读取,最多 100 页,实例查询失败不阻止无关联工单创建。
async function Q() {
if (!K.value) {
if (isCurrent() && !K.value) {
((K.value = !0), (q.value = !1), (G.value = ``));
try {
let [e, t] = await Promise.all([
b.get(`/ticket/type`),
b.get(`/ticket/department`),
]);
if (!isCurrent()) return;
((B.value = e.list ?? []), (V.value = t.list ?? []));
try {
let e = [],
t = 1,
n = 0;
do {
if (!isCurrent()) return;
let r = await b.get(`/host`, {
page: t,
limit: 200,
});
if (!isCurrent()) return;
(e.push(...(r.list ?? [])),
(n = Number(r.count ?? e.length)),
(t += 1));
} while (e.length < n && t <= 100);
H.value = e;
} catch {
if (!isCurrent()) return;
H.value = [];
}
((U.ticket_type_id = B.value[0]?.id ?? null),
(U.admin_role_id = V.value[0]?.id ?? null),
(q.value = !0));
} catch (e) {
if (!isCurrent()) return;
G.value =
e instanceof Error ? e.message : `加载工单选项失败,请重试`;
} finally {
K.value = !1;
if (isCurrent()) K.value = !1;
}
}
}
// 标题和内容非空且选项就绪后创建工单,只提交附件保存标识,成功进入返回 ID 的详情。
async function $() {
if (!(W.value || Z.value || K.value || !q.value)) {
if (isCurrent() && !(W.value || Z.value || K.value || !q.value)) {
if (((G.value = ``), !U.title.trim())) {
G.value = `请输入工单标题`;
return;
@@ -221,11 +236,13 @@ var le = {
host_ids: U.host_ids,
attachment: X.value.map((e) => e.save_name),
});
if (!isCurrent()) return;
z.push(`/tickets/${e.id}`);
} catch (e) {
G.value = e instanceof Error ? e.message : `提交失败`;
if (isCurrent())
G.value = e instanceof Error ? e.message : `提交失败`;
} finally {
W.value = !1;
if (isCurrent()) W.value = !1;
}
}
}