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(() => {});
},
}));