fix(member): 修复成员密码保存失败误报成功
This commit is contained in:
@@ -113,6 +113,7 @@ function InfoForm() {
|
|||||||
const [name, setName] = useState(member?.name ?? user.name);
|
const [name, setName] = useState(member?.name ?? user.name);
|
||||||
const [email, setEmail] = useState(member?.email ?? user.email);
|
const [email, setEmail] = useState(member?.email ?? user.email);
|
||||||
const [errors, setErrors] = useState<{ name?: string; email?: string }>({});
|
const [errors, setErrors] = useState<{ name?: string; email?: string }>({});
|
||||||
|
const [saveError, setSaveError] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [savedAt, setSavedAt] = useState(0);
|
const [savedAt, setSavedAt] = useState(0);
|
||||||
|
|
||||||
@@ -134,8 +135,10 @@ function InfoForm() {
|
|||||||
setErrors({});
|
setErrors({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setSaveError('');
|
||||||
|
setSavedAt(0);
|
||||||
const errs: typeof errors = {};
|
const errs: typeof errors = {};
|
||||||
if (!name.trim()) errs.name = '请输入姓名';
|
if (!name.trim()) errs.name = '请输入姓名';
|
||||||
else if (name.trim().length > 20) errs.name = '姓名最长 20 字符';
|
else if (name.trim().length > 20) errs.name = '姓名最长 20 字符';
|
||||||
@@ -144,14 +147,17 @@ function InfoForm() {
|
|||||||
setErrors(errs);
|
setErrors(errs);
|
||||||
if (Object.keys(errs).length > 0) return;
|
if (Object.keys(errs).length > 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setTimeout(() => {
|
await updateMember(user.id, { name: name.trim(), email: email.trim() });
|
||||||
updateMember(user.id, { name: name.trim(), email: email.trim() });
|
await refreshUser();
|
||||||
refreshUser();
|
|
||||||
setSubmitting(false);
|
|
||||||
setSavedAt(Date.now());
|
setSavedAt(Date.now());
|
||||||
setTimeout(() => setSavedAt(0), 3000);
|
setTimeout(() => setSavedAt(0), 3000);
|
||||||
}, 200);
|
} catch {
|
||||||
|
setSaveError('保存失败,请确认网络/API 服务正常后重试');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,6 +183,11 @@ function InfoForm() {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-2 pt-5 mt-5 border-t border-[var(--line)]">
|
<div className="flex items-center justify-end gap-2 pt-5 mt-5 border-t border-[var(--line)]">
|
||||||
|
{saveError && (
|
||||||
|
<span className="mr-auto">
|
||||||
|
<FieldError message={saveError} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{savedAt > 0 && (
|
{savedAt > 0 && (
|
||||||
<span className="flex items-center gap-1 text-[12px] text-emerald-600 mr-auto transition-opacity">
|
<span className="flex items-center gap-1 text-[12px] text-emerald-600 mr-auto transition-opacity">
|
||||||
<Check className="h-3.5 w-3.5" />已保存
|
<Check className="h-3.5 w-3.5" />已保存
|
||||||
@@ -203,6 +214,7 @@ function PasswordForm() {
|
|||||||
const [showNew, setShowNew] = useState(false);
|
const [showNew, setShowNew] = useState(false);
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
const [errors, setErrors] = useState<{ oldPwd?: string; newPwd?: string; confirmPwd?: string }>({});
|
const [errors, setErrors] = useState<{ oldPwd?: string; newPwd?: string; confirmPwd?: string }>({});
|
||||||
|
const [saveError, setSaveError] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [savedAt, setSavedAt] = useState(0);
|
const [savedAt, setSavedAt] = useState(0);
|
||||||
|
|
||||||
@@ -211,8 +223,10 @@ function PasswordForm() {
|
|||||||
setErrors({});
|
setErrors({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setSaveError('');
|
||||||
|
setSavedAt(0);
|
||||||
const member = members.find((m) => m.id === user.id);
|
const member = members.find((m) => m.id === user.id);
|
||||||
const errs: typeof errors = {};
|
const errs: typeof errors = {};
|
||||||
if (!oldPwd) errs.oldPwd = '请输入当前密码';
|
if (!oldPwd) errs.oldPwd = '请输入当前密码';
|
||||||
@@ -225,14 +239,17 @@ function PasswordForm() {
|
|||||||
setErrors(errs);
|
setErrors(errs);
|
||||||
if (Object.keys(errs).length > 0) return;
|
if (Object.keys(errs).length > 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setTimeout(() => {
|
await updateMember(user.id, { password: newPwd });
|
||||||
updateMember(user.id, { password: newPwd });
|
|
||||||
handleReset();
|
handleReset();
|
||||||
setSubmitting(false);
|
|
||||||
setSavedAt(Date.now());
|
setSavedAt(Date.now());
|
||||||
setTimeout(() => setSavedAt(0), 3000);
|
setTimeout(() => setSavedAt(0), 3000);
|
||||||
}, 200);
|
} catch {
|
||||||
|
setSaveError('密码保存失败,请确认网络/API 服务正常后重试');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -253,6 +270,11 @@ function PasswordForm() {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-2 pt-5 mt-5 border-t border-[var(--line)]">
|
<div className="flex items-center justify-end gap-2 pt-5 mt-5 border-t border-[var(--line)]">
|
||||||
|
{saveError && (
|
||||||
|
<span className="mr-auto">
|
||||||
|
<FieldError message={saveError} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{savedAt > 0 && (
|
{savedAt > 0 && (
|
||||||
<span className="flex items-center gap-1 text-[12px] text-emerald-600 mr-auto transition-opacity">
|
<span className="flex items-center gap-1 text-[12px] text-emerald-600 mr-auto transition-opacity">
|
||||||
<Check className="h-3.5 w-3.5" />密码已修改,下次登录生效
|
<Check className="h-3.5 w-3.5" />密码已修改,下次登录生效
|
||||||
|
|||||||
19
apps/web/lib/member-password-persistence-source.test.ts
Normal file
19
apps/web/lib/member-password-persistence-source.test.ts
Normal file
@@ -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 \}\);/);
|
||||||
|
});
|
||||||
@@ -43,15 +43,15 @@ interface MemberState {
|
|||||||
updateDepartment: (id: string, data: Partial<Department>) => void;
|
updateDepartment: (id: string, data: Partial<Department>) => void;
|
||||||
deleteDepartment: (id: string) => void;
|
deleteDepartment: (id: string) => void;
|
||||||
createMember: (data: Omit<Member, 'id' | 'createdAt'>) => void;
|
createMember: (data: Omit<Member, 'id' | 'createdAt'>) => void;
|
||||||
updateMember: (id: string, data: Partial<Member>) => void;
|
updateMember: (id: string, data: Partial<Member>) => Promise<void>;
|
||||||
deleteMember: (id: string) => void;
|
deleteMember: (id: string) => void;
|
||||||
createRole: (data: Omit<RoleItem, 'id' | 'createdAt'>) => void;
|
createRole: (data: Omit<RoleItem, 'id' | 'createdAt'>) => void;
|
||||||
updateRole: (id: string, data: Partial<RoleItem>) => void;
|
updateRole: (id: string, data: Partial<RoleItem>) => void;
|
||||||
deleteRole: (id: string) => void;
|
deleteRole: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveStored(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
|
async function saveStored(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
|
||||||
saveServerData('members', state).catch(() => {});
|
await saveServerData('members', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStored(): Promise<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null> {
|
async function loadStored(): Promise<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null> {
|
||||||
@@ -101,7 +101,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
|||||||
passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE,
|
passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE,
|
||||||
};
|
};
|
||||||
set({ ...next, loaded: true });
|
set({ ...next, loaded: true });
|
||||||
if (ensured.changed || usernameEnsured.changed || rolesChanged) saveStored(next);
|
if (ensured.changed || usernameEnsured.changed || rolesChanged) void saveStored(next).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
set({ loaded: true });
|
set({ loaded: true });
|
||||||
}
|
}
|
||||||
@@ -109,24 +109,24 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
|||||||
|
|
||||||
updatePasswordRule: (rule) => {
|
updatePasswordRule: (rule) => {
|
||||||
set({ passwordRule: 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) => {
|
createDepartment: (data) => {
|
||||||
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||||
const departments = [...get().departments, dept];
|
const departments = [...get().departments, dept];
|
||||||
set({ departments });
|
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) => {
|
updateDepartment: (id, data) => {
|
||||||
const departments = get().departments.map((d) => d.id === id ? { ...d, ...data } : d);
|
const departments = get().departments.map((d) => d.id === id ? { ...d, ...data } : d);
|
||||||
set({ departments });
|
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) => {
|
deleteDepartment: (id) => {
|
||||||
const departments = get().departments.filter((d) => d.id !== id && d.parentId !== id);
|
const departments = get().departments.filter((d) => d.id !== id && d.parentId !== id);
|
||||||
set({ departments });
|
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) => {
|
createMember: (data) => {
|
||||||
@@ -139,20 +139,26 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
const members = [...get().members, member];
|
const members = [...get().members, member];
|
||||||
set({ members });
|
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 safeData = sanitizeSystemAdminPatch(id, data);
|
||||||
const updatedMembers = get().members.map((m) => m.id === id ? { ...m, ...safeData } : m);
|
const updatedMembers = get().members.map((m) => m.id === id ? { ...m, ...safeData } : m);
|
||||||
const ensured = ensureSystemAdminMember(updatedMembers);
|
const ensured = ensureSystemAdminMember(updatedMembers);
|
||||||
set({ members: ensured.members });
|
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) => {
|
deleteMember: (id) => {
|
||||||
if (!canDeleteSystemMember(id)) return;
|
if (!canDeleteSystemMember(id)) return;
|
||||||
const members = get().members.filter((m) => m.id !== id);
|
const members = get().members.filter((m) => m.id !== id);
|
||||||
set({ members });
|
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) => {
|
createRole: (data) => {
|
||||||
@@ -164,7 +170,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
const roles = [...get().roles, role];
|
const roles = [...get().roles, role];
|
||||||
set({ roles });
|
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) => {
|
updateRole: (id, data) => {
|
||||||
const roles = get().roles.map((r) => {
|
const roles = get().roles.map((r) => {
|
||||||
@@ -177,12 +183,12 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
|||||||
return { ...r, ...data };
|
return { ...r, ...data };
|
||||||
});
|
});
|
||||||
set({ roles });
|
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) => {
|
deleteRole: (id) => {
|
||||||
if (id === SYSTEM_ADMIN_ROLE_ID) return;
|
if (id === SYSTEM_ADMIN_ROLE_ID) return;
|
||||||
const roles = get().roles.filter((r) => r.id !== id);
|
const roles = get().roles.filter((r) => r.id !== id);
|
||||||
set({ roles });
|
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(() => {});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user