import React, { useEffect, useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import Sidebar from '../components/Sidebar'; import { UserPlus, Edit, Trash2, Upload, X } from 'lucide-react'; import { User, Template } from '../types'; import { storage } from '../utils/storage'; import { createUser, deleteUserFromApi, listDepartments, listUsers, updateUser, type Department, } from '../api/users'; import { listTemplates } from '../api/templates'; import { deleteUserSignature, uploadUserSignature } from '../api/files'; import { isLocalFallbackEnabled } from '../config/runtime'; const ADMIN_DISABLE_AUTH_KEY = 'DISABLE_ADMIN_2024'; export default function UserManage() { const navigate = useNavigate(); const [users, setUsers] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isEditing, setIsEditing] = useState(false); const [formData, setFormData] = useState>({ username: '', name: '', phone: '', email: '', password: '', role: 'user', department: '', status: 'active', visibleTemplates: [], manageableTemplates: [] }); const [confirmPassword, setConfirmPassword] = useState(''); const [authKey, setAuthKey] = useState(''); const [allTemplates, setAllTemplates] = useState([]); const [departments, setDepartments] = useState([]); useEffect(() => { const user = storage.get('currentUser', null); if (!user || (user.role !== 'super' && user.role !== 'admin')) { navigate('/dashboard'); return; } setCurrentUser(user); const savedUsers = storage.get('users', []).filter(Boolean); setUsers(savedUsers); const savedTemplates = storage.get('templates', []).filter(Boolean); setAllTemplates(savedTemplates); Promise.allSettled([ listUsers(), listDepartments(), listTemplates('use'), ]).then(([usersResult, departmentsResult, templatesResult]) => { if (usersResult.status === 'fulfilled') { const apiUsers = mergeUsersWithLocalCache(usersResult.value.items, storage.get('users', [])); setUsers(apiUsers); storage.set('users', apiUsers); } if (departmentsResult.status === 'fulfilled') { setDepartments(departmentsResult.value.items); } if (templatesResult.status === 'fulfilled') { setAllTemplates(templatesResult.value.items); storage.set('templates', templatesResult.value.items); } }); }, [navigate]); const mergeUsersWithLocalCache = (apiUsers: User[], localUsers: User[]) => apiUsers.map((apiUser) => { const local = localUsers.find((item) => item.username === apiUser.username); return { ...local, ...apiUser, signature: local?.signature ?? apiUser.signature, password: local?.password, }; }); const displayUsers = useMemo(() => { if (!currentUser) return []; const safeUsers = (Array.isArray(users) ? users : []).filter(Boolean); if (currentUser.role === 'super') return safeUsers; return safeUsers.filter(u => u.department === currentUser.department && (u.role === 'user' || u.username === currentUser.username)); }, [users, currentUser]); const saveToLocalStorage = (updatedUsers: User[]) => { setUsers(updatedUsers); storage.set('users', updatedUsers); }; const compressImage = (file: File, maxSize: number = 500): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (e) => { const img = new Image(); img.src = e.target?.result as string; img.onload = () => { const canvas = document.createElement('canvas'); let { width, height } = img; if (width > height && width > maxSize) { height = Math.round((height * maxSize) / width); width = maxSize; } else if (height > maxSize) { width = Math.round((width * maxSize) / height); height = maxSize; } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (ctx) { ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); } resolve(canvas.toDataURL('image/jpeg', 0.8)); }; img.onerror = reject; }; reader.onerror = reject; }); }; const handleSignatureUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { const compressed = await compressImage(file); setFormData(prev => ({ ...prev, signature: compressed })); } catch { alert('图片压缩失败,请重试'); } }; const handleDelete = async (user: User) => { if (user.username === 'admin') { alert('不能删除默认超级管理员'); return; } if (user.username === currentUser?.username) { alert('不能删除当前登录账号'); return; } if (window.confirm(`确定要删除用户 "${user.username}" 吗?`)) { try { await deleteUserFromApi(user.id || user.username); } catch (error) { if (!isLocalFallbackEnabled()) { alert(`删除失败: ${(error as Error).message}`); return; } console.warn('Delete user API failed, keeping local compatibility path.', error); } const updated = users.filter(u => u.username !== user.username); saveToLocalStorage(updated); } }; const handleEdit = (user: User) => { if (currentUser?.role === 'admin') { if ((user.role !== 'user' && user.username !== currentUser.username) || user.department !== currentUser.department) { alert('您只能管理同部门的医生或您自己'); return; } } setIsEditing(true); const allTplIds = allTemplates.map(t => t.id); let manageable: string[] = []; let visible: string[] = []; if (user.role === 'super') { manageable = allTplIds; visible = allTplIds; } else if (user.role === 'admin') { manageable = Array.isArray(user.manageableTemplates) ? user.manageableTemplates : allTplIds; visible = (Array.isArray(user.visibleTemplates) ? user.visibleTemplates : manageable) .filter(id => manageable.includes(id)); } else { manageable = []; const deptAdmin = users.find(u => u.role === 'admin' && u.department === user.department); const adminManageable = deptAdmin?.manageableTemplates || []; visible = (Array.isArray(user.visibleTemplates) ? user.visibleTemplates : []) .filter(id => adminManageable.includes(id)); } setFormData({ ...user, password: '', visibleTemplates: visible, manageableTemplates: manageable }); setConfirmPassword(''); setAuthKey(''); setIsModalOpen(true); }; const handleAdd = () => { setIsEditing(false); const defaultDept = currentUser?.role === 'admin' ? currentUser.department || '' : ''; const defaultRole = 'user'; let defaultVisible: string[] = []; let defaultManageable: string[] = []; if (currentUser?.role === 'admin') { defaultManageable = []; defaultVisible = currentUser.manageableTemplates || []; } setFormData({ username: '', name: '', phone: '', email: '', password: '', role: defaultRole, department: defaultDept, status: 'active', visibleTemplates: defaultVisible, manageableTemplates: defaultManageable }); setConfirmPassword(''); setAuthKey(''); setIsModalOpen(true); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (!formData.username) { alert('用户ID不能为空'); return; } if (!isEditing && formData.role === 'super') { alert('系统中只能存在一个超级管理员,无法新增'); return; } if (!isEditing && users.find(u => u.username === formData.username)) { alert('用户ID已存在'); return; } if (isEditing && formData.password && formData.password !== confirmPassword) { alert('两次输入的密码不一致'); return; } let finalRole = formData.role as any; if (currentUser?.role === 'admin') { if (!isEditing) finalRole = 'user'; else if (formData.username !== currentUser.username) finalRole = 'user'; } const finalDepartment = currentUser?.role === 'admin' ? currentUser.department || '' : (formData.department || ''); if (finalRole === 'admin') { const existingAdmin = users.find(u => u.role === 'admin' && u.department === finalDepartment && u.username !== formData.username); if (existingAdmin) { alert('该部门已存在管理员,一个部门只能有一个管理员'); return; } } if (finalRole === 'user') { const hasAdminInDept = users.some(u => u.role === 'admin' && u.department === finalDepartment); if (!hasAdminInDept) { alert('该部门暂无管理员,请先建立一个部门管理员再创建医生'); return; } } if (finalRole !== 'user' && formData.status === 'inactive') { if (authKey.trim() !== ADMIN_DISABLE_AUTH_KEY) { alert('禁用管理员账号需要输入正确的授权密钥'); return; } } const allTplIds = allTemplates.map(t => t.id); let manageableTemplates: string[] = []; let visibleTemplates: string[] = []; if (finalRole === 'super') { manageableTemplates = allTplIds; visibleTemplates = allTplIds; } else if (finalRole === 'admin') { manageableTemplates = (formData.manageableTemplates || []).filter(id => allTplIds.includes(id)); visibleTemplates = (formData.visibleTemplates || []) .filter(id => manageableTemplates.includes(id)); } else if (finalRole === 'user') { manageableTemplates = []; const deptAdmin = users.find(u => u.role === 'admin' && u.department === finalDepartment); const adminManageable = deptAdmin?.manageableTemplates || []; visibleTemplates = (formData.visibleTemplates || []) .filter(id => adminManageable.includes(id)); } const oldUser = isEditing ? users.find(u => u.username === formData.username) : undefined; let updatedUsers: User[]; if (isEditing && finalRole === 'admin' && oldUser && oldUser.role === 'admin' && currentUser && currentUser.role === 'super') { const oldManageable = Array.isArray(oldUser.manageableTemplates) ? oldUser.manageableTemplates : allTplIds; const removed = oldManageable.filter(id => !manageableTemplates.includes(id)); const added = manageableTemplates.filter(id => !oldManageable.includes(id)); // Ensure admin's own visible gets new templates too let adminVisible = [...visibleTemplates]; added.forEach(id => { if (!adminVisible.includes(id)) adminVisible.push(id); }); updatedUsers = users.map(u => { if (u.username === formData.username) { return { ...u, role: finalRole, department: finalDepartment, manageableTemplates, visibleTemplates: adminVisible, password: formData.password || u.password, signature: formData.signature } as User; } if (u.role === 'user' && u.department === (oldUser.department || finalDepartment)) { const currentVisible = Array.isArray(u.visibleTemplates) ? u.visibleTemplates : []; const nextVisible = currentVisible.filter(id => !removed.includes(id)); added.forEach(id => { if (!nextVisible.includes(id)) nextVisible.push(id); }); return { ...u, visibleTemplates: nextVisible }; } return u; }); } else { const payload: Partial = { ...formData, role: finalRole, department: finalDepartment, manageableTemplates, visibleTemplates }; if (!formData.password) { delete payload.password; } if (isEditing) { updatedUsers = users.map(u => { if (u.username === formData.username) { return { ...u, ...payload } as User; } return u; }); } else { const newUser: User = { ...(payload as User), createdAt: new Date().toISOString().split('T')[0] }; updatedUsers = [...users, newUser]; } } let savedApiUser: User | null = null; const payload = { ...(updatedUsers.find(u => u.username === formData.username) || formData), departmentId: departments.find(dept => dept.name === finalDepartment)?.id || formData.departmentId, } as User; try { savedApiUser = isEditing ? await updateUser(oldUser?.id || formData.id || formData.username || '', payload) : await createUser(payload); } catch (error: any) { if (!isLocalFallbackEnabled()) { alert(`后端保存失败:${error?.message || String(error)}`); return; } console.warn('Save user API failed, keeping local compatibility path.', error); if (error?.message) { alert(`后端保存失败,已保留本地修改:${error.message}`); } } if (savedApiUser) { if (formData.signature?.startsWith('data:')) { try { const signatureFile = await uploadUserSignature(savedApiUser.id || savedApiUser.username, formData.signature); savedApiUser = { ...savedApiUser, signatureFileId: signatureFile.id, signature: signatureFile.url, }; } catch (error: any) { if (!isLocalFallbackEnabled()) { alert(`签名上传失败:${error?.message || String(error)}`); return; } console.warn('Upload signature API failed, keeping local signature cache.', error); alert(`签名上传到后端失败,已保留本地签名:${error?.message || String(error)}`); } } else if (oldUser?.signature && !formData.signature) { try { await deleteUserSignature(savedApiUser.id || savedApiUser.username); savedApiUser = { ...savedApiUser, signatureFileId: undefined, signature: undefined, }; } catch (error) { if (!isLocalFallbackEnabled()) { alert(`签名删除失败:${(error as Error).message}`); return; } console.warn('Delete signature API failed, keeping local compatibility path.', error); } } const localSignature = formData.signature; const mergedSavedUser: User = { ...oldUser, ...savedApiUser, signature: savedApiUser.signature ?? localSignature, password: formData.password || oldUser?.password, }; updatedUsers = isEditing ? users.map(u => u.username === mergedSavedUser.username ? mergedSavedUser : u) : [...users, mergedSavedUser]; } saveToLocalStorage(updatedUsers); // 如果编辑的是当前登录用户,同步更新 currentUser const currentCached = updatedUsers.find(u => u.username === currentUser?.username); if (currentCached) { storage.set('currentUser', currentCached); setCurrentUser(currentCached); } setIsModalOpen(false); } catch (err: any) { alert('保存失败: ' + (err?.message || String(err))); console.error(err); } }; const toggleTemplate = (templateId: string, field: 'visibleTemplates' | 'manageableTemplates') => { const current = (formData[field] || []) as string[]; if (current.includes(templateId)) { setFormData({ ...formData, [field]: current.filter(id => id !== templateId) }); } else { setFormData({ ...formData, [field]: [...current, templateId] }); } }; // Real-time sync: when manageableTemplates changes for admin, ensure visibleTemplates stay within it useEffect(() => { if (!isModalOpen || !currentUser) return; const isAdminEditingSelf = currentUser.role === 'admin' && formData.username === currentUser.username; const isManageableReadonly = formData.role === 'super' || isAdminEditingSelf; if (formData.role === 'admin' && !isManageableReadonly) { const m = formData.manageableTemplates || []; const prevVisible = formData.visibleTemplates || []; const nextVisible = prevVisible.filter(id => m.includes(id)); if (nextVisible.length !== prevVisible.length) { setFormData(prev => ({ ...prev, visibleTemplates: nextVisible })); } } }, [formData.manageableTemplates, formData.role, isModalOpen, currentUser, formData.username]); if (!currentUser) return null; const roleNames = { 'super': '超级管理员', 'admin': '管理员', 'user': '医生' }; const isAdminLogin = currentUser.role === 'admin'; const needAuthKey = formData.role !== 'user' && formData.status === 'inactive'; // 模板权限显示规则 const isSuperEditingAdmin = currentUser.role === 'super' && formData.role === 'admin'; const isSuperEditingUser = currentUser.role === 'super' && formData.role === 'user'; const isAdminEditingSelf = currentUser.role === 'admin' && formData.username === currentUser.username; const isAdminEditingUser = currentUser.role === 'admin' && formData.role === 'user'; const showManageableTemplates = isSuperEditingAdmin || isAdminEditingSelf || formData.role === 'super'; const isManageableReadonly = formData.role === 'super' || isAdminEditingSelf; const showVisibleTemplates = true; // 可视模板候选 let visibleCandidates = allTemplates; if (isSuperEditingAdmin) { visibleCandidates = allTemplates.filter(t => (formData.manageableTemplates || []).includes(t.id)); } else if (isSuperEditingUser) { const deptAdmin = users.find(u => u.role === 'admin' && u.department === formData.department); const adminManageable = deptAdmin?.manageableTemplates || []; visibleCandidates = allTemplates.filter(t => adminManageable.includes(t.id)); } else if (isAdminEditingSelf) { const adminManageable = currentUser.manageableTemplates || []; visibleCandidates = allTemplates.filter(t => adminManageable.includes(t.id)); } else if (isAdminEditingUser) { const adminManageable = currentUser.manageableTemplates || []; visibleCandidates = allTemplates.filter(t => adminManageable.includes(t.id)); } return (

用户管理

{isAdminLogin ? '管理同部门的医生用户' : '管理系统用户,仅超级管理员可赋予管理员权限'}

{displayUsers.map((user) => ( ))}
用户ID 姓名 邮箱 联系电话 角色 部门 签名状态 状态 操作
{user.username} {user.name} {user.email || '-'} {user.phone || '-'} {roleNames[user.role]} {user.department || '-'} {user.signature ? '已上传' : '未上传'} {user.status === 'active' ? '启用' : '禁用'}
{user.username !== 'admin' && user.username !== currentUser.username && ( )}
{isModalOpen && (

{isEditing ? '编辑用户' : '新增用户'}

填写用户信息并分配系统权限。

setFormData({ ...formData, username: e.target.value })} disabled={isEditing} required className="input-minimal disabled:bg-slate-50 disabled:text-text-muted" />
setFormData({ ...formData, name: e.target.value })} required className="input-minimal" />
setFormData({ ...formData, phone: e.target.value })} className="input-minimal" />
setFormData({ ...formData, email: e.target.value })} className="input-minimal" />
setFormData({ ...formData, password: e.target.value })} required={!isEditing} className="input-minimal" />
{isEditing && formData.password && (
setConfirmPassword(e.target.value)} required className="input-minimal" />
)}
setFormData({ ...formData, department: e.target.value })} disabled={isAdminLogin} required className="input-minimal disabled:bg-slate-50 disabled:text-text-muted" />
{needAuthKey && (
setAuthKey(e.target.value)} required placeholder="请输入授权密钥以禁用管理员" className="input-minimal" />

禁用管理员账号需要输入系统授权密钥进行验证。

)}
{formData.signature ? (
电子签名预览
) : (
支持 JPG、PNG,自动压缩至 500px 以内
)}
{showManageableTemplates && (
{allTemplates.map(tpl => ( ))} {allTemplates.length === 0 &&

暂无模板

}
{isManageableReadonly && (

{formData.role === 'super' ? '超级管理员默认可管理所有模板,不可更改。' : '管理员的模板管理权限由超级管理员设定,不可自行更改。'}

)}
)} {showVisibleTemplates && (
{visibleCandidates.map(tpl => ( ))} {visibleCandidates.length === 0 &&

暂无可视模板

}
{formData.role === 'super' && (

超级管理员默认可视所有模板。

)} {(isSuperEditingAdmin || isAdminEditingSelf) && (

管理员的可视模板只能从其可管理模板中选择。

)} {(isSuperEditingUser || isAdminEditingUser) && (

医生的可视模板只能从其部门管理员的可管理模板中选择。

)}
)}
)}
); }