'use client'; import { useEffect, useMemo, useState } from 'react'; import { Plus, Pencil, Trash2, X, ChevronRight, FolderOpen, Settings } from 'lucide-react'; import { useMemberStore } from '@/stores/useMemberStore'; import { maskPhone, generatePassword } from '@/lib/members'; import type { Member, Department, PasswordRule } from '@/lib/members'; import { RouteGuard } from '@/components/auth/Guard'; export default function MembersPage() { return ( ); } function MembersPageContent() { const { departments, members, roles, passwordRule, fetchMembers, updatePasswordRule, createDepartment, updateDepartment, deleteDepartment, createMember, updateMember, deleteMember } = useMemberStore(); const [activeDeptId, setActiveDeptId] = useState(null); const [showMemberModal, setShowMemberModal] = useState(false); const [editingMember, setEditingMember] = useState(null); const [showDeptModal, setShowDeptModal] = useState(false); const [editingDept, setEditingDept] = useState(null); const [expandedDepts, setExpandedDepts] = useState>(new Set(['dept-2'])); const [showPwdRuleModal, setShowPwdRuleModal] = useState(false); useEffect(() => { fetchMembers(); }, [fetchMembers]); const topDepts = useMemo(() => departments.filter((d) => !d.parentId).sort((a, b) => a.order - b.order), [departments]); 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]); const activeDeptName = activeDeptId ? departments.find((d) => d.id === activeDeptId)?.name ?? '全部' : '全部成员'; const roleName = (roleId: string) => roles.find((r) => r.id === roleId)?.name ?? '-'; const toggleExpand = (id: string) => { setExpandedDepts((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }; return ( {/* Left: Departments */} 部门 { setEditingDept(null); setShowDeptModal(true); }} className="p-1 rounded-md hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)] hover:text-[var(--accent)]"> {/* All */} setActiveDeptId(null)} className={`w-full flex items-center gap-2 px-4 py-2 text-[12px] transition-colors ${!activeDeptId ? 'bg-[var(--accent-soft)] text-[var(--accent)] font-medium' : 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]'}`} > 全部成员 {members.length} {/* Tree */} {topDepts.map((dept) => { const children = childDepts(dept.id); const expanded = expandedDepts.has(dept.id); const count = members.filter((m) => m.departmentId === dept.id || children.some((c) => c.id === m.departmentId)).length; return ( {children.length > 0 && ( toggleExpand(dept.id)} className="p-0.5 mr-1 rounded hover:bg-[var(--bg-card)]"> )} {children.length === 0 && } setActiveDeptId(dept.id)} className="flex-1">{dept.name} {count} { e.stopPropagation(); setEditingDept(dept); setShowDeptModal(true); }} className="p-0.5 rounded hover:bg-[var(--bg-card)] text-[var(--ink-muted)]"> { e.stopPropagation(); deleteDepartment(dept.id); if (activeDeptId === dept.id) setActiveDeptId(null); }} className="p-0.5 rounded hover:bg-[var(--bg-card)] text-[var(--ink-muted)] hover:text-red-500"> {expanded && children.map((child) => { const childCount = members.filter((m) => m.departmentId === child.id).length; return ( setActiveDeptId(child.id)} className={`group flex items-center pl-9 pr-4 py-2 text-[12px] cursor-pointer transition-colors ${activeDeptId === child.id ? 'bg-[var(--accent-soft)] text-[var(--accent)] font-medium' : 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]'}`} > {child.name} {childCount} { e.stopPropagation(); setEditingDept(child); setShowDeptModal(true); }} className="p-0.5 rounded hover:bg-[var(--bg-card)] text-[var(--ink-muted)]"> { e.stopPropagation(); deleteDepartment(child.id); if (activeDeptId === child.id) setActiveDeptId(null); }} className="p-0.5 rounded hover:bg-[var(--bg-card)] text-[var(--ink-muted)] hover:text-red-500"> ); })} ); })} {/* Right: Member list */} {activeDeptName} {filteredMembers.length} 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"> 初始密码设置 { 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"> 新建成员 {filteredMembers.length === 0 ? ( 暂无成员 ) : ( 姓名 角色 部门 手机号 企业邮箱 操作 {filteredMembers.map((m) => ( {m.name} {roleName(m.roleId)} {departments.find((d) => d.id === m.departmentId)?.name ?? '-'} {maskPhone(m.phone)} {m.email} { setEditingMember(m); setShowMemberModal(true); }} className="h-6 px-2 rounded text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]">编辑 deleteMember(m.id)} className="h-6 px-2 rounded text-[11px] font-medium text-red-500 hover:bg-red-50">删除 ))} )} {/* Member Modal */} {showMemberModal && ( setShowMemberModal(false)} onSubmit={(data) => { if (editingMember) updateMember(editingMember.id, data); else createMember(data as any); setShowMemberModal(false); }} /> )} {/* Department Modal */} {showDeptModal && ( setShowDeptModal(false)} onSubmit={(data) => { if (editingDept) updateDepartment(editingDept.id, data); else createDepartment(data as any); setShowDeptModal(false); }} /> )} {/* Password Rule Modal */} {showPwdRuleModal && ( setShowPwdRuleModal(false)} onSave={(rule) => { updatePasswordRule(rule); setShowPwdRuleModal(false); }} /> )} ); } function MemberModal({ initial, departments, roles, defaultDeptId, passwordRule, onClose, onSubmit }: { initial: Member | null; departments: Department[]; roles: { id: string; name: string }[]; defaultDeptId: string | null; passwordRule: PasswordRule; onClose: () => void; onSubmit: (data: any) => void; }) { 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 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 }); }; return ( e.stopPropagation()}> {initial ? '编辑成员' : '新建成员'} 姓名 * setName(e.target.value)} required 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" /> 部门 * setDepartmentId(e.target.value)} required 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"> 选择部门 {departments.map((d) => {d.parentId ? ' ' : ''}{d.name})} 角色 * setRoleId(e.target.value)} required 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"> 选择角色 {roles.map((r) => {r.name})} 手机号 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" /> 企业邮箱 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" /> 初始密码 {password} {!initial && ( 自动生成 )} 取消 {initial ? '保存' : '创建'} ); } function DeptModal({ initial, departments, onClose, onSubmit }: { initial: Department | null; departments: Department[]; onClose: () => void; onSubmit: (data: any) => void; }) { const [name, setName] = useState(initial?.name ?? ''); const [parentId, setParentId] = useState(initial?.parentId ?? ''); const topDepts = departments.filter((d) => !d.parentId); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!name.trim()) return; onSubmit({ name: name.trim(), parentId: parentId || undefined, order: departments.length + 1 }); }; return ( e.stopPropagation()}> {initial ? '编辑部门' : '新建部门'} 部门名称 * setName(e.target.value)} required 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" /> 上级部门 setParentId(e.target.value)} 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"> 无(顶级部门) {topDepts.filter((d) => d.id !== initial?.id).map((d) => {d.name})} 取消 {initial ? '保存' : '创建'} ); } function PasswordRuleModal({ rule, onClose, onSave }: { rule: PasswordRule; onClose: () => void; onSave: (rule: PasswordRule) => void; }) { const [prefix, setPrefix] = useState(rule.prefix); const [length, setLength] = useState(rule.length); const [includeUppercase, setIncludeUppercase] = useState(rule.includeUppercase); const [includeLowercase, setIncludeLowercase] = useState(rule.includeLowercase); const [includeNumbers, setIncludeNumbers] = useState(rule.includeNumbers); const [includeSpecial, setIncludeSpecial] = useState(rule.includeSpecial); const currentRule: PasswordRule = { prefix, length, includeUppercase, includeLowercase, includeNumbers, includeSpecial }; const preview = generatePassword(currentRule); return ( e.stopPropagation()}> 初始密码规则设置 固定前缀 setPrefix(e.target.value)} placeholder="如 Ftb" 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" /> 总长度 setLength(Number(e.target.value))} 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" /> 包含字符类型 setIncludeUppercase(e.target.checked)} className="rounded" /> 大写字母 setIncludeLowercase(e.target.checked)} className="rounded" /> 小写字母 setIncludeNumbers(e.target.checked)} className="rounded" /> 数字 setIncludeSpecial(e.target.checked)} className="rounded" /> 特殊字符 预览效果 {preview} 取消 onSave(currentRule)} className="h-8 px-4 rounded-lg text-[12px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]">保存 ); }
暂无成员