Files
Mdeical_Sur_Report/src/pages/UserManage.tsx
admin 014aca8619 Initialize backendized SurClaw report system
- Add React/Vite frontend for login, dashboard, reports, templates, users, settings, AI, speech, and media workflows.

- Add NestJS/Prisma/PostgreSQL backend with auth, dashboard stats, reports, templates, users, departments, settings, files, AI, speech, audit logs, and HTML sanitization.

- Add Prisma schema, migrations, seed data, persistent app sessions, Docker/Nginx deployment files, and upload volume configuration.

- Add Vitest, Playwright, backend integration tests, and project documentation for requirements, design, permissions, API contracts, testing, deployment, security, and progress.

- Configure production local fallback switch and remove unused Gemini direct dependency/env wiring.
2026-05-02 01:41:57 +08:00

860 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<User[]>([]);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState<Partial<User>>({
username: '',
name: '',
phone: '',
email: '',
password: '',
role: 'user',
department: '',
status: 'active',
visibleTemplates: [],
manageableTemplates: []
});
const [confirmPassword, setConfirmPassword] = useState('');
const [authKey, setAuthKey] = useState('');
const [allTemplates, setAllTemplates] = useState<Template[]>([]);
const [departments, setDepartments] = useState<Department[]>([]);
useEffect(() => {
const user = storage.get<User | null>('currentUser', null);
if (!user || (user.role !== 'super' && user.role !== 'admin')) {
navigate('/dashboard');
return;
}
setCurrentUser(user);
const savedUsers = storage.get<User[]>('users', []).filter(Boolean);
setUsers(savedUsers);
const savedTemplates = storage.get<Template[]>('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<User[]>('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<string> => {
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<HTMLInputElement>) => {
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<User> = {
...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 (
<div className="flex min-h-screen bg-bg">
<Sidebar />
<main className="flex-1 p-10 overflow-y-auto">
<header className="flex justify-between items-center mb-8">
<div>
<h1 className="text-2xl font-bold tracking-tight text-text-main"></h1>
<p className="text-text-muted text-sm mt-1">
{isAdminLogin ? '管理同部门的医生用户' : '管理系统用户,仅超级管理员可赋予管理员权限'}
</p>
</div>
<button
onClick={handleAdd}
className="btn-accent inline-flex items-center gap-2"
>
<UserPlus size={18} />
</button>
</header>
<div className="card-minimal p-0 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-slate-50">
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border">ID</th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{displayUsers.map((user) => (
<tr key={user.username} className="hover:bg-slate-50 transition-colors group">
<td className="px-6 py-4 text-sm text-text-main font-mono">{user.username}</td>
<td className="px-6 py-4 text-sm text-text-main font-semibold">{user.name}</td>
<td className="px-6 py-4 text-sm text-text-main">{user.email || '-'}</td>
<td className="px-6 py-4 text-sm text-text-main">{user.phone || '-'}</td>
<td className="px-6 py-4">
<span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${
user.role === 'super' ? 'bg-amber-100 text-amber-700' :
user.role === 'admin' ? 'bg-blue-100 text-blue-700' :
'bg-green-100 text-green-700'
}`}>
{roleNames[user.role]}
</span>
</td>
<td className="px-6 py-4 text-sm text-text-main">{user.department || '-'}</td>
<td className="px-6 py-4">
<span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${
user.signature ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'
}`}>
{user.signature ? '已上传' : '未上传'}
</span>
</td>
<td className="px-6 py-4">
<span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${
user.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{user.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td className="px-6 py-4">
<div className="flex gap-2 transition-opacity">
<button
onClick={() => handleEdit(user)}
className="p-2 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 transition-colors"
title="编辑"
>
<Edit size={16} />
</button>
{user.username !== 'admin' && user.username !== currentUser.username && (
<button
onClick={() => handleDelete(user)}
className="p-2 rounded-lg bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
title="删除"
>
<Trash2 size={16} />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</main>
{isModalOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-2xl p-10 w-full max-w-[500px] max-h-[90vh] overflow-y-auto shadow-2xl border border-border">
<h3 className="text-xl font-bold text-text-main mb-2">{isEditing ? '编辑用户' : '新增用户'}</h3>
<p className="text-sm text-text-muted mb-8"></p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">ID *</label>
<input
type="text"
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
disabled={isEditing}
required
className="input-minimal disabled:bg-slate-50 disabled:text-text-muted"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
className="input-minimal"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"></label>
<input
type="tel"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="input-minimal"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"></label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="input-minimal"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">
{isEditing ? '修改密码 (留空则不修改)' : '密码 *'}
</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required={!isEditing}
className="input-minimal"
/>
</div>
{isEditing && formData.password && (
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> *</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
className="input-minimal"
/>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> *</label>
<select
value={formData.role}
onChange={(e) => {
const newRole = e.target.value as any;
const allTplIds = allTemplates.map(t => t.id);
setFormData({
...formData,
role: newRole,
manageableTemplates: newRole === 'user' ? [] : allTplIds,
visibleTemplates: newRole === 'user' ? [] : allTplIds
});
}}
required
disabled={isAdminLogin || (isEditing && formData.username === 'admin')}
className="input-minimal bg-white disabled:bg-slate-50 disabled:text-text-muted"
>
<option value="user"></option>
{!isAdminLogin && <option value="admin"></option>}
</select>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> *</label>
<input
type="text"
value={formData.department}
onChange={(e) => setFormData({ ...formData, department: e.target.value })}
disabled={isAdminLogin}
required
className="input-minimal disabled:bg-slate-50 disabled:text-text-muted"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"></label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
className="input-minimal bg-white"
>
<option value="active"></option>
<option value="inactive"></option>
</select>
</div>
{needAuthKey && (
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> *</label>
<input
type="text"
value={authKey}
onChange={(e) => setAuthKey(e.target.value)}
required
placeholder="请输入授权密钥以禁用管理员"
className="input-minimal"
/>
<p className="text-[10px] text-text-muted"></p>
</div>
)}
<div className="space-y-2">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"></label>
{formData.signature ? (
<div className="flex items-center gap-3">
<img
src={formData.signature}
alt="电子签名预览"
className="h-16 border border-border rounded bg-white object-contain"
/>
<div className="flex flex-col gap-2">
<label className="px-3 py-1.5 text-xs font-medium rounded-lg bg-slate-100 hover:bg-slate-200 transition-colors cursor-pointer inline-flex items-center gap-1">
<Upload size={12} />
<input type="file" accept="image/*" className="hidden" onChange={handleSignatureUpload} />
</label>
<button
type="button"
onClick={() => setFormData(prev => ({ ...prev, signature: undefined }))}
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-red-50 text-red-600 hover:bg-red-100 transition-colors inline-flex items-center gap-1"
>
<X size={12} />
</button>
</div>
</div>
) : (
<div className="flex items-center gap-3">
<label className="px-4 py-2 text-sm font-medium rounded-lg bg-slate-100 hover:bg-slate-200 transition-colors cursor-pointer inline-flex items-center gap-2">
<Upload size={14} />
<input type="file" accept="image/*" className="hidden" onChange={handleSignatureUpload} />
</label>
<span className="text-xs text-text-muted"> JPGPNG 500px </span>
</div>
)}
</div>
{showManageableTemplates && (
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">
{formData.role === 'super' ? '可管理模板' : '可管理模板'}
</label>
<div className="max-h-[150px] overflow-y-auto border border-border rounded-lg p-3 space-y-2 bg-slate-50">
{allTemplates.map(tpl => (
<label key={tpl.id} className={`flex items-center gap-2 p-1 rounded-md transition-colors ${isManageableReadonly ? '' : 'cursor-pointer hover:bg-white'}`}>
<input
type="checkbox"
checked={(formData.manageableTemplates || []).includes(tpl.id)}
onChange={() => toggleTemplate(tpl.id, 'manageableTemplates')}
disabled={isManageableReadonly}
className="w-4 h-4 rounded-sm border-border text-accent focus:ring-accent disabled:opacity-50"
/>
<span className={`text-sm ${isManageableReadonly ? 'text-text-muted' : 'text-text-main'}`}>{tpl.name}</span>
</label>
))}
{allTemplates.length === 0 && <p className="text-xs text-text-muted italic"></p>}
</div>
{isManageableReadonly && (
<p className="text-[10px] text-text-muted">
{formData.role === 'super' ? '超级管理员默认可管理所有模板,不可更改。' : '管理员的模板管理权限由超级管理员设定,不可自行更改。'}
</p>
)}
</div>
)}
{showVisibleTemplates && (
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">
{formData.role === 'super' ? '可视模板 (全部)' : '可视模板'}
</label>
<div className="max-h-[150px] overflow-y-auto border border-border rounded-lg p-3 space-y-2 bg-slate-50">
{visibleCandidates.map(tpl => (
<label key={tpl.id} className="flex items-center gap-2 cursor-pointer hover:bg-white p-1 rounded-md transition-colors">
<input
type="checkbox"
checked={(formData.visibleTemplates || []).includes(tpl.id)}
onChange={() => toggleTemplate(tpl.id, 'visibleTemplates')}
disabled={formData.role === 'super'}
className="w-4 h-4 rounded-sm border-border text-accent focus:ring-accent disabled:opacity-50"
/>
<span className={`text-sm ${formData.role === 'super' ? 'text-text-muted' : 'text-text-main'}`}>{tpl.name}</span>
</label>
))}
{visibleCandidates.length === 0 && <p className="text-xs text-text-muted italic"></p>}
</div>
{formData.role === 'super' && (
<p className="text-[10px] text-text-muted"></p>
)}
{(isSuperEditingAdmin || isAdminEditingSelf) && (
<p className="text-[10px] text-text-muted"></p>
)}
{(isSuperEditingUser || isAdminEditingUser) && (
<p className="text-[10px] text-text-muted"></p>
)}
</div>
)}
<div className="flex justify-end gap-3 pt-4 border-t border-border">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-6 py-2.5 bg-slate-100 text-text-muted rounded-lg text-sm font-semibold hover:bg-slate-200 transition-colors"
>
</button>
<button
type="submit"
className="btn-accent"
>
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}