From 516a5f1b63a71deb0d43d4d5dab12b806623a13a Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 3 Jul 2026 19:10:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(member):=20=E4=BF=AE=E5=A4=8D=E6=88=90?= =?UTF-8?q?=E5=91=98=E5=AF=86=E7=A0=81=E4=BF=9D=E5=AD=98=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E8=AF=AF=E6=8A=A5=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/profile/page.tsx | 48 ++++++++++++++----- ...member-password-persistence-source.test.ts | 19 ++++++++ apps/web/stores/useMemberStore.ts | 36 ++++++++------ 3 files changed, 75 insertions(+), 28 deletions(-) create mode 100644 apps/web/lib/member-password-persistence-source.test.ts diff --git a/apps/web/app/profile/page.tsx b/apps/web/app/profile/page.tsx index b1024bd..37b31c4 100644 --- a/apps/web/app/profile/page.tsx +++ b/apps/web/app/profile/page.tsx @@ -113,6 +113,7 @@ function InfoForm() { const [name, setName] = useState(member?.name ?? user.name); const [email, setEmail] = useState(member?.email ?? user.email); const [errors, setErrors] = useState<{ name?: string; email?: string }>({}); + const [saveError, setSaveError] = useState(''); const [submitting, setSubmitting] = useState(false); const [savedAt, setSavedAt] = useState(0); @@ -134,8 +135,10 @@ function InfoForm() { setErrors({}); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setSaveError(''); + setSavedAt(0); const errs: typeof errors = {}; if (!name.trim()) errs.name = '请输入姓名'; else if (name.trim().length > 20) errs.name = '姓名最长 20 字符'; @@ -144,14 +147,17 @@ function InfoForm() { setErrors(errs); if (Object.keys(errs).length > 0) return; - setSubmitting(true); - setTimeout(() => { - updateMember(user.id, { name: name.trim(), email: email.trim() }); - refreshUser(); - setSubmitting(false); + try { + setSubmitting(true); + await updateMember(user.id, { name: name.trim(), email: email.trim() }); + await refreshUser(); setSavedAt(Date.now()); setTimeout(() => setSavedAt(0), 3000); - }, 200); + } catch { + setSaveError('保存失败,请确认网络/API 服务正常后重试'); + } finally { + setSubmitting(false); + } }; return ( @@ -177,6 +183,11 @@ function InfoForm() {
+ {saveError && ( + + + + )} {savedAt > 0 && ( 已保存 @@ -203,6 +214,7 @@ function PasswordForm() { const [showNew, setShowNew] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const [errors, setErrors] = useState<{ oldPwd?: string; newPwd?: string; confirmPwd?: string }>({}); + const [saveError, setSaveError] = useState(''); const [submitting, setSubmitting] = useState(false); const [savedAt, setSavedAt] = useState(0); @@ -211,8 +223,10 @@ function PasswordForm() { setErrors({}); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setSaveError(''); + setSavedAt(0); const member = members.find((m) => m.id === user.id); const errs: typeof errors = {}; if (!oldPwd) errs.oldPwd = '请输入当前密码'; @@ -225,14 +239,17 @@ function PasswordForm() { setErrors(errs); if (Object.keys(errs).length > 0) return; - setSubmitting(true); - setTimeout(() => { - updateMember(user.id, { password: newPwd }); + try { + setSubmitting(true); + await updateMember(user.id, { password: newPwd }); handleReset(); - setSubmitting(false); setSavedAt(Date.now()); setTimeout(() => setSavedAt(0), 3000); - }, 200); + } catch { + setSaveError('密码保存失败,请确认网络/API 服务正常后重试'); + } finally { + setSubmitting(false); + } }; return ( @@ -253,6 +270,11 @@ function PasswordForm() {
+ {saveError && ( + + + + )} {savedAt > 0 && ( 密码已修改,下次登录生效 diff --git a/apps/web/lib/member-password-persistence-source.test.ts b/apps/web/lib/member-password-persistence-source.test.ts new file mode 100644 index 0000000..8cbf3a2 --- /dev/null +++ b/apps/web/lib/member-password-persistence-source.test.ts @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import test from 'node:test'; + +test('member store save helper does not swallow AppData save failures', () => { + const source = readFileSync(join(process.cwd(), 'stores/useMemberStore.ts'), 'utf8'); + + assert.match( + source, + /async function saveStored\(state: \{ departments: Department\[\]; members: Member\[\]; roles: RoleItem\[\]; passwordRule: PasswordRule \}\) \{\s+await saveServerData\('members', state\);\s+\}/, + ); +}); + +test('profile password form waits for member password save before showing success', () => { + const source = readFileSync(join(process.cwd(), 'app/profile/page.tsx'), 'utf8'); + + assert.match(source, /await updateMember\(user\.id, \{ password: newPwd \}\);/); +}); diff --git a/apps/web/stores/useMemberStore.ts b/apps/web/stores/useMemberStore.ts index 84f2a0e..ab9407f 100644 --- a/apps/web/stores/useMemberStore.ts +++ b/apps/web/stores/useMemberStore.ts @@ -43,15 +43,15 @@ interface MemberState { updateDepartment: (id: string, data: Partial) => void; deleteDepartment: (id: string) => void; createMember: (data: Omit) => void; - updateMember: (id: string, data: Partial) => void; + updateMember: (id: string, data: Partial) => Promise; deleteMember: (id: string) => void; createRole: (data: Omit) => void; updateRole: (id: string, data: Partial) => void; deleteRole: (id: string) => void; } -function saveStored(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) { - saveServerData('members', state).catch(() => {}); +async function saveStored(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) { + await saveServerData('members', state); } async function loadStored(): Promise<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null> { @@ -101,7 +101,7 @@ export const useMemberStore = create((set, get) => ({ passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE, }; set({ ...next, loaded: true }); - if (ensured.changed || usernameEnsured.changed || rolesChanged) saveStored(next); + if (ensured.changed || usernameEnsured.changed || rolesChanged) void saveStored(next).catch(() => {}); } else { set({ loaded: true }); } @@ -109,24 +109,24 @@ export const useMemberStore = create((set, get) => ({ updatePasswordRule: (rule) => { set({ passwordRule: rule }); - saveStored({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule }); + void saveStored({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule }).catch(() => {}); }, createDepartment: (data) => { const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) }; const departments = [...get().departments, dept]; set({ departments }); - saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }); + void saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {}); }, updateDepartment: (id, data) => { const departments = get().departments.map((d) => d.id === id ? { ...d, ...data } : d); set({ departments }); - saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }); + void saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {}); }, deleteDepartment: (id) => { const departments = get().departments.filter((d) => d.id !== id && d.parentId !== id); set({ departments }); - saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }); + void saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {}); }, createMember: (data) => { @@ -139,20 +139,26 @@ export const useMemberStore = create((set, get) => ({ }; const members = [...get().members, member]; set({ members }); - saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }); + void saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {}); }, - updateMember: (id, data) => { + updateMember: async (id, data) => { + const previousMembers = get().members; const safeData = sanitizeSystemAdminPatch(id, data); const updatedMembers = get().members.map((m) => m.id === id ? { ...m, ...safeData } : m); const ensured = ensureSystemAdminMember(updatedMembers); set({ members: ensured.members }); - saveStored({ departments: get().departments, members: ensured.members, roles: get().roles, passwordRule: get().passwordRule }); + try { + await saveStored({ departments: get().departments, members: ensured.members, roles: get().roles, passwordRule: get().passwordRule }); + } catch (error) { + set({ members: previousMembers }); + throw error; + } }, deleteMember: (id) => { if (!canDeleteSystemMember(id)) return; const members = get().members.filter((m) => m.id !== id); set({ members }); - saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }); + void saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {}); }, createRole: (data) => { @@ -164,7 +170,7 @@ export const useMemberStore = create((set, get) => ({ }; const roles = [...get().roles, role]; set({ roles }); - saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }); + void saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }).catch(() => {}); }, updateRole: (id, data) => { const roles = get().roles.map((r) => { @@ -177,12 +183,12 @@ export const useMemberStore = create((set, get) => ({ return { ...r, ...data }; }); set({ roles }); - saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }); + void saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }).catch(() => {}); }, deleteRole: (id) => { if (id === SYSTEM_ADMIN_ROLE_ID) return; const roles = get().roles.filter((r) => r.id !== id); set({ roles }); - saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }); + void saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }).catch(() => {}); }, }));