fix(data): 清理旧数据并保护服务端写入

This commit is contained in:
Script Generator
2026-07-02 09:30:50 +08:00
parent 28e0c6de19
commit 916bd17a48
38 changed files with 1346 additions and 194 deletions

View File

@@ -1,12 +1,13 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { Plus, Pencil, Trash2, X, ChevronRight, FolderOpen, Settings } from 'lucide-react';
import { Plus, Pencil, Trash2, X, ChevronRight, FolderOpen, Settings, Search } from 'lucide-react';
import { useMemberStore } from '@/stores/useMemberStore';
import { useAuthStore } from '@/stores/useAuthStore';
import { maskPhone, generatePassword } from '@/lib/members';
import { generatePassword, generateUsernameFromName } from '@/lib/members';
import type { Member, Department, PasswordRule } from '@/lib/members';
import { isSystemAdminMember } from '@/lib/member-system';
import { filterMembersForList, getDisplayedAccountPassword, resolveLatestEditingMember } from '@/lib/member-list';
import { RouteGuard } from '@/components/auth/Guard';
export default function MembersPage() {
@@ -22,6 +23,7 @@ function MembersPageContent() {
const currentUser = useAuthStore((s) => s.user);
const refreshUser = useAuthStore((s) => s.refreshUser);
const [activeDeptId, setActiveDeptId] = useState<string | null>(null);
const [nameKeyword, setNameKeyword] = useState('');
const [showMemberModal, setShowMemberModal] = useState(false);
const [editingMember, setEditingMember] = useState<Member | null>(null);
const [showDeptModal, setShowDeptModal] = useState(false);
@@ -35,10 +37,12 @@ function MembersPageContent() {
const childDepts = (parentId: string) => departments.filter((d) => d.parentId === parentId).sort((a, b) => a.order - b.order);
const filteredMembers = useMemo(() => {
if (!activeDeptId) return members;
const deptIds = [activeDeptId, ...departments.filter((d) => d.parentId === activeDeptId).map((d) => d.id)];
return members.filter((m) => deptIds.includes(m.departmentId));
}, [members, activeDeptId, departments]);
return filterMembersForList({ members, departments, activeDeptId, nameKeyword });
}, [members, departments, activeDeptId, nameKeyword]);
const latestEditingMember = useMemo(
() => resolveLatestEditingMember(editingMember, members),
[editingMember, members],
);
const activeDeptName = activeDeptId ? departments.find((d) => d.id === activeDeptId)?.name ?? '全部' : '全部成员';
const roleName = (roleId: string) => roles.find((r) => r.id === roleId)?.name ?? '-';
@@ -131,9 +135,18 @@ function MembersPageContent() {
<span className="rounded-md bg-[var(--bg-subtle)] px-1.5 py-0.5 text-[11px] font-medium tabular-nums text-[var(--ink-soft)]">{filteredMembers.length}</span>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--ink-muted)]" />
<input
value={nameKeyword}
onChange={(e) => setNameKeyword(e.target.value)}
placeholder="搜索姓名"
className="h-8 w-44 rounded-lg border border-[var(--line)] bg-[var(--bg)] pl-8 pr-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none"
/>
</div>
<button onClick={() => setShowPwdRuleModal(true)} className="flex h-8 items-center gap-1.5 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] font-medium text-[var(--ink-soft)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors">
<Settings className="h-3.5 w-3.5" strokeWidth={2} />
</button>
<button onClick={() => { setEditingMember(null); setShowMemberModal(true); }} className="flex h-8 items-center gap-1.5 rounded-lg bg-[var(--accent)] px-3 text-[13px] font-medium text-white shadow-[var(--shadow-sm)] hover:bg-[var(--accent-hover)] transition-colors">
<Plus className="h-3.5 w-3.5" strokeWidth={2} />
@@ -153,9 +166,9 @@ function MembersPageContent() {
<thead className="sticky top-0 z-10 bg-[var(--bg-subtle)]">
<tr className="border-b border-[var(--line)] bg-[var(--bg-subtle)]">
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-right text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
</tr>
@@ -164,11 +177,11 @@ function MembersPageContent() {
{filteredMembers.map((m) => (
<tr key={m.id} className="border-b border-[var(--line-soft)] last:border-0 hover:bg-[var(--bg-subtle)] transition-colors">
<td className="px-4 py-3 font-medium text-[var(--ink)]">{m.name}</td>
<td className="px-4 py-3 font-mono text-[var(--ink-soft)]">{m.username ?? '-'}</td>
<td className="px-4 py-3">
<span className="inline-flex items-center rounded-md bg-[var(--bg-subtle)] px-2 py-0.5 text-[11px] font-medium text-[var(--ink-soft)]">{roleName(m.roleId)}</span>
</td>
<td className="px-4 py-3 text-[var(--ink-soft)]">{departments.find((d) => d.id === m.departmentId)?.name ?? '-'}</td>
<td className="px-4 py-3 tabular-nums text-[var(--ink-soft)]">{maskPhone(m.phone)}</td>
<td className="px-4 py-3 text-[var(--ink-soft)]">{m.email}</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
@@ -193,9 +206,10 @@ function MembersPageContent() {
{/* Member Modal */}
{showMemberModal && (
<MemberModal
initial={editingMember}
initial={latestEditingMember}
departments={departments}
roles={roles}
existingUsernames={members.map((m) => m.username ?? '')}
defaultDeptId={activeDeptId}
passwordRule={passwordRule}
onClose={() => setShowMemberModal(false)}
@@ -237,10 +251,11 @@ function MembersPageContent() {
);
}
function MemberModal({ initial, departments, roles, defaultDeptId, passwordRule, onClose, onSubmit }: {
function MemberModal({ initial, departments, roles, existingUsernames, defaultDeptId, passwordRule, onClose, onSubmit }: {
initial: Member | null;
departments: Department[];
roles: { id: string; name: string }[];
existingUsernames: string[];
defaultDeptId: string | null;
passwordRule: PasswordRule;
onClose: () => void;
@@ -249,15 +264,23 @@ function MemberModal({ initial, departments, roles, defaultDeptId, passwordRule,
const [name, setName] = useState(initial?.name ?? '');
const [departmentId, setDepartmentId] = useState(initial?.departmentId ?? defaultDeptId ?? '');
const [roleId, setRoleId] = useState(initial?.roleId ?? '');
const [phone, setPhone] = useState(initial?.phone ?? '');
const [email, setEmail] = useState(initial?.email ?? '');
const [password] = useState(() => initial?.password ?? generatePassword(passwordRule));
const [generatedPassword] = useState(() => generatePassword(passwordRule));
const password = getDisplayedAccountPassword(initial, generatedPassword);
const isSystemMember = initial ? isSystemAdminMember(initial) : false;
const usernamePreview = initial
? (initial.username || generateUsernameFromName(initial.name, existingUsernames))
: (name.trim() ? generateUsernameFromName(name, existingUsernames) : '');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !departmentId || !roleId) return;
onSubmit({ name: name.trim(), departmentId, roleId, phone: phone.trim(), email: email.trim(), password });
const data: Partial<Member> = { name: name.trim(), departmentId, roleId, email: email.trim(), password };
if (!initial) {
data.username = usernamePreview;
data.phone = '';
}
onSubmit(data);
};
return (
@@ -277,6 +300,15 @@ function MemberModal({ initial, departments, roles, defaultDeptId, passwordRule,
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"> *</label>
<input value={name} onChange={(e) => setName(e.target.value)} required disabled={isSystemMember} placeholder="请输入姓名" className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none disabled:bg-zinc-50 disabled:text-[var(--ink-soft)]" />
</div>
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<div className="flex h-9 w-full items-center rounded-lg border border-[var(--line)] bg-[var(--bg-subtle)] px-3">
<span className="flex-1 font-mono text-[13px] text-[var(--ink)]">{usernamePreview || '输入姓名后自动生成'}</span>
{!initial && (
<span className="text-[10px] text-[var(--ink-muted)]"></span>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"> *</label>
@@ -293,16 +325,12 @@ function MemberModal({ initial, departments, roles, defaultDeptId, passwordRule,
</select>
</div>
</div>
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<input value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="11位手机号" className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
</div>
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="name@company.com" className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
</div>
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<div className="flex items-center h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-subtle)] px-3">
<span className="flex-1 text-[13px] font-mono text-[var(--ink)]">{password}</span>
{!initial && (
@@ -385,7 +413,7 @@ function PasswordRuleModal({ rule, onClose, onSave }: {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
<div className="w-full max-w-sm rounded-2xl bg-[var(--bg-card)] border border-[var(--line)] p-5 shadow-[var(--shadow-md)]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-[13px] font-semibold text-[var(--ink)]"></h3>
<h3 className="text-[13px] font-semibold text-[var(--ink)]"></h3>
<button onClick={onClose} className="p-1 rounded hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)]"><X className="h-4 w-4" /></button>
</div>
<div className="space-y-3">