Files
Agent-System/web/src/components/admin/AdminSettingsModal.tsx
T

1067 lines
50 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
ShieldAlert,
Users,
UserCheck,
ToggleLeft,
ToggleRight,
Shield,
Server,
Plus,
Lock,
X,
Check,
Ban,
CheckCircle2,
AlertCircle,
Settings2,
Info,
} from 'lucide-react';
import { UserProfile } from '../../types/docgov';
import { RoleDefinition, PermissionKey } from '../../types/rbac';
import { UserAccount, SystemAuthConfig } from '../../types/auth';
import { authClient } from '../../api/auth';
export interface AdminSettingsModalProps {
isOpen: boolean;
onClose: () => void;
currentUser: UserProfile;
roles: RoleDefinition[];
onSaveRoles: (roles: RoleDefinition[]) => void;
allUsers: UserProfile[];
onRefreshUsers?: () => void;
env?: string;
}
const ALL_PERMISSION_ITEMS: { key: PermissionKey; name: string; desc: string }[] = [
{ key: 'project:create', name: '创建新项目/立项', desc: '具备在协同工作台开启全新研发项目并指派阶段的权限' },
{ key: 'project:assign_lead', name: '设置文档责任人', desc: '在文档治理面板为交付物指派编制人、审核人与批准人' },
{ key: 'perm:manage', name: '系统权限与角色管理', desc: '增删改全局职位角色定义与权限矩阵' },
{ key: 'workflow:config_visibility', name: '配置工作流默认可见性', desc: '调整业务阶段中各受控交付物的基线查阅准入边界' },
{ key: 'doc:override_visibility', name: '配置单文档可见性覆盖', desc: '针对特定核心机密交付物设定独占白名单或责任人收紧' },
{ key: 'doc:export', name: '受控交付物导出', desc: '允许将医疗受控文档编译导出为受保护的 DOCX 或 XLSX' },
{ key: 'workflow:view_all', name: '默认查阅常规工作流文档', desc: '允许在全景大盘中查阅已就绪的常规项目协作文档' },
];
export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
isOpen,
onClose,
currentUser,
roles,
onSaveRoles,
allUsers: _allUsers,
onRefreshUsers,
env = 'dev',
}) => {
const [activeTab, setActiveTab] = useState<'users' | 'approvals' | 'policy' | 'rbac' | 'system'>('users');
// Accounts state
const [userAccounts, setUserAccounts] = useState<UserAccount[]>([]);
const [loadingUsers, setLoadingUsers] = useState(false);
const [feedbackTip, setFeedbackTip] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
// System auth config state
const [systemConfig, setSystemConfig] = useState<SystemAuthConfig>({
allowRegistration: true,
requireApproval: false,
});
// Create User modal
const [showCreateModal, setShowCreateModal] = useState(false);
const [newUsername, setNewUsername] = useState('');
const [newName, setNewName] = useState('');
const [newInitialPassword, setNewInitialPassword] = useState('123456');
const [newRoleId, setNewRoleId] = useState('role-engineer');
const [newDepartment, setNewDepartment] = useState('研发中心');
const [newEmployeeId, setNewEmployeeId] = useState('');
// RBAC dynamic role editing state
const [editRoles, setEditRoles] = useState<RoleDefinition[]>(roles);
const [selectedRoleId, setSelectedRoleId] = useState<string>(roles[0]?.id || 'role-admin');
const [showNewRoleDialog, setShowNewRoleDialog] = useState(false);
const [newRoleName, setNewRoleName] = useState('');
const [newRoleCode, setNewRoleCode] = useState('');
const [newRoleDesc, setNewRoleDesc] = useState('');
// 1. 数据装载
const loadAccounts = async () => {
setLoadingUsers(true);
try {
const list = await authClient.getAdminUsers();
setUserAccounts(list);
const cfg = await authClient.getSystemAuthConfig();
setSystemConfig(cfg);
} catch (e: any) {
console.warn('Failed to load admin accounts:', e);
} finally {
setLoadingUsers(false);
}
};
useEffect(() => {
if (isOpen) {
loadAccounts();
setEditRoles(roles);
}
}, [isOpen, roles]);
if (!isOpen) return null;
// 严格入口防线:仅系统管理员独占访问
const isAdmin = currentUser.roleId === 'role-admin' || currentUser.name.includes('管理员');
if (!isAdmin) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4">
<div className="w-full max-w-md bg-slate-900 border border-rose-500/40 rounded-2xl shadow-2xl p-6 text-slate-100 text-center">
<div className="w-12 h-12 rounded-xl bg-rose-500/10 text-rose-400 mx-auto flex items-center justify-center mb-3">
<ShieldAlert className="w-7 h-7" />
</div>
<h3 className="text-lg font-bold text-white">403 访问被拒绝</h3>
<p className="text-xs text-rose-300 mt-2">
当前功能为系统管理控制台,仅系统管理员 (role-admin) 具备访问权限。
</p>
<button
type="button"
onClick={onClose}
className="mt-5 px-5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 transition"
>
返回工作台
</button>
</div>
</div>
);
}
const showTip = (type: 'success' | 'error', msg: string) => {
setFeedbackTip({ type, msg });
setTimeout(() => setFeedbackTip(null), 3000);
};
// 职位分配
const handleAssignRole = async (targetUser: UserAccount, targetRoleId: string) => {
const roleDef = roles.find((r) => r.id === targetRoleId);
const positionName = roleDef ? roleDef.name : '方案工程师';
try {
await authClient.updateUserRole(targetUser.id, targetRoleId, positionName);
await loadAccounts();
onRefreshUsers?.();
showTip('success', `已将 ${targetUser.name} 的职位更新为【${positionName}】`);
} catch (e: any) {
showTip('error', e.message || '更新职位失败');
}
};
// 审批通过并指派职位
const handleApproveUser = async (targetUser: UserAccount, targetRoleId: string) => {
const roleDef = roles.find((r) => r.id === targetRoleId);
const positionName = roleDef ? roleDef.name : '方案工程师';
try {
await authClient.updateUserStatus(targetUser.id, 'active', targetRoleId, positionName);
await loadAccounts();
onRefreshUsers?.();
showTip('success', `已批准 ${targetUser.name} 入职并指派职位【${positionName}】`);
} catch (e: any) {
showTip('error', e.message || '审批处理失败');
}
};
// 停用/启用账号
const handleToggleUserStatus = async (targetUser: UserAccount) => {
const nextStatus = targetUser.status === 'active' ? 'disabled' : 'active';
try {
await authClient.updateUserStatus(targetUser.id, nextStatus);
await loadAccounts();
onRefreshUsers?.();
showTip('success', `已将账号 ${targetUser.name} 设置为 ${nextStatus === 'active' ? '正常' : '已停用'}`);
} catch (e: any) {
showTip('error', e.message || '切换状态失败');
}
};
// 管理员新建账号
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault();
if (!newUsername.trim() || !newName.trim() || !newInitialPassword.trim()) {
showTip('error', '请填写必填字段');
return;
}
const roleDef = roles.find((r) => r.id === newRoleId);
const positionName = roleDef ? roleDef.name : '方案工程师';
try {
await authClient.createAdminUser({
username: newUsername.trim(),
name: newName.trim(),
initialPassword: newInitialPassword.trim(),
roleId: newRoleId,
position: positionName,
department: newDepartment.trim() || undefined,
employeeId: newEmployeeId.trim() || undefined,
});
setShowCreateModal(false);
setNewUsername('');
setNewName('');
setNewInitialPassword('123456');
setNewEmployeeId('');
await loadAccounts();
onRefreshUsers?.();
showTip('success', `新员工账号创建成功!已标记为【首次登录必须改密】。`);
} catch (e: any) {
showTip('error', e.message || '创建账号失败');
}
};
// 准入策略切换
const handleTogglePolicy = async (key: keyof SystemAuthConfig) => {
const updated = { ...systemConfig, [key]: !systemConfig[key] };
try {
await authClient.updateSystemAuthConfig(updated);
setSystemConfig(updated);
showTip('success', '准入治理策略已实时生效');
} catch (e: any) {
showTip('error', e.message || '更新策略失败');
}
};
// RBAC 权限勾选
const handleTogglePermission = (roleId: string, permKey: PermissionKey) => {
setEditRoles((prev) =>
prev.map((r) => {
if (r.id !== roleId) return r;
const exists = r.permissions.includes(permKey);
const nextPerms = exists
? r.permissions.filter((p) => p !== permKey)
: [...r.permissions, permKey];
return { ...r, permissions: nextPerms, updatedAt: new Date().toISOString() };
})
);
};
// 保存 RBAC 角色
const handleSaveRBAC = () => {
onSaveRoles(editRoles);
showTip('success', '全局角色与权限矩阵已保存并实时广播!');
};
// 待审批列表
const pendingUsers = userAccounts.filter((u) => u.status === 'pending_approval');
const selectedRole = editRoles.find((r) => r.id === selectedRoleId) || editRoles[0];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="w-full max-w-5xl bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl flex flex-col h-[88vh] overflow-hidden text-slate-100">
{/* 顶部标题栏 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/80">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-amber-600 to-indigo-600 flex items-center justify-center text-white shadow-md">
<Settings2 className="w-5 h-5" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-base font-bold text-white">系统管理与治理控制台</h2>
<span className="text-[11px] font-mono px-2 py-0.5 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-400 font-semibold">
管理员特权独占
</span>
<span className="text-[11px] font-mono px-2 py-0.5 rounded bg-slate-800 text-cyan-400">
ENV: {env.toUpperCase()}
</span>
</div>
<p className="text-xs text-slate-400">
人员账号审核、受控职位指派、准入开关及全局 RBAC 权限矩阵
</p>
</div>
</div>
<div className="flex items-center gap-3">
{feedbackTip && (
<div
className={`px-3 py-1 rounded-lg text-xs flex items-center gap-1.5 border animate-in fade-in ${
feedbackTip.type === 'success'
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-300'
: 'bg-rose-500/10 border-rose-500/30 text-rose-300'
}`}
>
{feedbackTip.type === 'success' ? (
<CheckCircle2 className="w-3.5 h-3.5" />
) : (
<AlertCircle className="w-3.5 h-3.5" />
)}
<span>{feedbackTip.msg}</span>
</div>
)}
<button
type="button"
onClick={onClose}
className="p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* 主体布局 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧导航 Tab */}
<div className="w-56 border-r border-slate-800 bg-slate-950/40 p-3 space-y-1">
<button
type="button"
onClick={() => setActiveTab('users')}
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-xs font-medium transition ${
activeTab === 'users'
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
}`}
>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>人员与受控职位</span>
</div>
<span className="text-[11px] font-mono px-1.5 py-0.2 rounded bg-slate-800 text-slate-300">
{userAccounts.length}
</span>
</button>
<button
type="button"
onClick={() => setActiveTab('approvals')}
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-xs font-medium transition ${
activeTab === 'approvals'
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
}`}
>
<div className="flex items-center gap-2">
<UserCheck className="w-4 h-4" />
<span>新用户注册审核</span>
</div>
{pendingUsers.length > 0 && (
<span className="text-[10px] font-bold px-1.5 py-0.2 rounded-full bg-rose-500 text-white animate-pulse">
{pendingUsers.length}
</span>
)}
</button>
<button
type="button"
onClick={() => setActiveTab('policy')}
className={`w-full flex items-center gap-2 px-3 py-2.5 rounded-xl text-xs font-medium transition ${
activeTab === 'policy'
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
}`}
>
<Lock className="w-4 h-4" />
<span>系统准入策略</span>
</button>
<button
type="button"
onClick={() => setActiveTab('rbac')}
className={`w-full flex items-center gap-2 px-3 py-2.5 rounded-xl text-xs font-medium transition ${
activeTab === 'rbac'
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
}`}
>
<Shield className="w-4 h-4" />
<span>角色与权限矩阵</span>
</button>
<button
type="button"
onClick={() => setActiveTab('system')}
className={`w-full flex items-center gap-2 px-3 py-2.5 rounded-xl text-xs font-medium transition ${
activeTab === 'system'
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
}`}
>
<Server className="w-4 h-4" />
<span>环境与运行状态</span>
</button>
</div>
{/* 右侧主工作区 */}
<div className="flex-1 overflow-y-auto p-6">
{/* 1. 人员与职位治理 */}
{activeTab === 'users' && (
<div className="space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<div>
<h3 className="text-sm font-semibold text-white">研发中心全员账号与职位指派</h3>
<p className="text-xs text-slate-400">
职位对应系统角色,决定其在研发协同工作台中的立项、文档责任人分配与受控操作权限
</p>
</div>
<button
type="button"
onClick={() => setShowCreateModal(true)}
className="px-3.5 py-1.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white text-xs font-medium flex items-center gap-1.5 shadow-md shadow-amber-600/20 transition"
>
<Plus className="w-3.5 h-3.5" />
<span>手动开通新员工账号</span>
</button>
</div>
{loadingUsers ? (
<div className="py-12 text-center text-xs text-slate-500">正在同步系统用户列表...</div>
) : (
<div className="border border-slate-800 rounded-xl overflow-hidden">
<table className="w-full text-left text-xs border-collapse">
<thead className="bg-slate-950/80 text-slate-400 font-medium border-b border-slate-800">
<tr>
<th className="py-2.5 px-3">姓名 / 用户名</th>
<th className="py-2.5 px-3">部门 / 工号</th>
<th className="py-2.5 px-3">受控职位 (指派角色)</th>
<th className="py-2.5 px-3">账号状态</th>
<th className="py-2.5 px-3 text-right">管理操作</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/70">
{userAccounts.map((u) => {
const isSuperAdmin = u.username === 'admin' || u.roleId === 'role-admin';
return (
<tr key={u.id} className="hover:bg-slate-800/30 transition">
<td className="py-2.5 px-3">
<div className="font-medium text-slate-100 flex items-center gap-1.5">
<span>{u.name}</span>
{u.mustChangePassword && (
<span className="text-[10px] px-1.5 py-0.2 rounded bg-amber-500/10 text-amber-400 border border-amber-500/30">
待首次改密
</span>
)}
</div>
<div className="text-[11px] font-mono text-slate-500">{u.username}</div>
</td>
<td className="py-2.5 px-3">
<div className="text-slate-300">{u.department || '未录入'}</div>
<div className="text-[11px] font-mono text-slate-500">{u.employeeId || '-'}</div>
</td>
<td className="py-2.5 px-3">
{isSuperAdmin ? (
<span className="inline-flex items-center gap-1 text-[11px] font-semibold text-amber-400 bg-amber-950/80 px-2.5 py-1 rounded-lg border border-amber-800">
<Shield className="w-3 h-3" /> 系统管理员
</span>
) : (
<select
value={u.roleId || 'role-engineer'}
onChange={(e) => handleAssignRole(u, e.target.value)}
className="bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1 text-xs text-cyan-300 focus:outline-none focus:border-cyan-500"
>
{roles.map((r) => (
<option key={r.id} value={r.id}>
{r.name} ({r.code})
</option>
))}
</select>
)}
</td>
<td className="py-2.5 px-3">
{u.status === 'active' && (
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-emerald-400 bg-emerald-950/80 px-2 py-0.5 rounded-full border border-emerald-800">
正常
</span>
)}
{u.status === 'pending_approval' && (
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-amber-400 bg-amber-950/80 px-2 py-0.5 rounded-full border border-amber-800">
待审核
</span>
)}
{u.status === 'disabled' && (
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-rose-400 bg-rose-950/80 px-2 py-0.5 rounded-full border border-rose-800">
已停用
</span>
)}
</td>
<td className="py-2.5 px-3 text-right">
{!isSuperAdmin && (
<div className="flex items-center justify-end gap-2">
<button
type="button"
onClick={() => handleToggleUserStatus(u)}
className={`px-2 py-1 rounded text-[11px] font-medium transition ${
u.status === 'active'
? 'bg-rose-500/10 text-rose-400 hover:bg-rose-500/20'
: 'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20'
}`}
>
{u.status === 'active' ? '停用' : '启用'}
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
)}
{/* 2. 注册审批中心 */}
{activeTab === 'approvals' && (
<div className="space-y-4">
<div className="pb-3 border-b border-slate-800">
<h3 className="text-sm font-semibold text-white">新注册员工审核与职位指派</h3>
<p className="text-xs text-slate-400">
当系统准入策略开启“新员工需管理员审核”时,自主注册的申请人将在此汇聚,待指派正式职位后激活
</p>
</div>
{pendingUsers.length === 0 ? (
<div className="py-16 text-center border border-dashed border-slate-800 rounded-2xl">
<UserCheck className="w-10 h-10 text-slate-600 mx-auto mb-2" />
<p className="text-xs text-slate-400">当前没有待审核的注册申请</p>
<p className="text-[11px] text-slate-500 mt-1">
所有注册员工均已审核激活,或当前准入策略已设置为自动激活
</p>
</div>
) : (
<div className="space-y-3">
{pendingUsers.map((pu) => {
return (
<div
key={pu.id}
className="p-4 rounded-xl bg-slate-950 border border-amber-500/30 flex items-center justify-between gap-4"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-white">{pu.name}</span>
<span className="text-xs font-mono text-slate-400">({pu.username})</span>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-300">
待审申请
</span>
</div>
<div className="text-xs text-slate-400 flex items-center gap-4">
<span>部门: {pu.department || '未填写'}</span>
<span>工号: {pu.employeeId || '未填写'}</span>
<span>申请时间: {pu.createdAt || '近期'}</span>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5 text-xs">
<span className="text-slate-400">指派职位:</span>
<select
id={`assign-role-${pu.id}`}
defaultValue="role-engineer"
className="bg-slate-900 border border-slate-700 rounded-lg px-2.5 py-1 text-xs text-cyan-300 focus:outline-none focus:border-cyan-500"
>
{roles.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
</select>
</div>
<button
type="button"
onClick={() => {
const selectEl = document.getElementById(
`assign-role-${pu.id}`
) as HTMLSelectElement | null;
const chosenRoleId = selectEl?.value || 'role-engineer';
handleApproveUser(pu, chosenRoleId);
}}
className="px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-medium flex items-center gap-1 transition shadow-sm"
>
<Check className="w-3.5 h-3.5" />
<span>批准并激活</span>
</button>
<button
type="button"
onClick={() => handleToggleUserStatus(pu)}
className="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-rose-900/40 text-slate-300 hover:text-rose-300 text-xs font-medium transition"
>
<Ban className="w-3.5 h-3.5" />
<span>拒绝申请</span>
</button>
</div>
</div>
);
})}
</div>
)}
</div>
)}
{/* 3. 系统准入策略 */}
{activeTab === 'policy' && (
<div className="space-y-6">
<div className="pb-3 border-b border-slate-800">
<h3 className="text-sm font-semibold text-white">系统账号准入与注册安全策略</h3>
<p className="text-xs text-slate-400">
控制外部人员能否自行在登录界面注册,以及新用户是否必须经过管理员审批与受控职位授权
</p>
</div>
<div className="space-y-4">
{/* 开关 1: 开放自主注册 */}
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800 flex items-center justify-between">
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-white">允许员工自主注册账号</span>
<span
className={`text-[10px] font-mono px-2 py-0.5 rounded-full ${
systemConfig.allowRegistration
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
: 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
}`}
>
{systemConfig.allowRegistration ? '已开启' : '已暂停'}
</span>
</div>
<p className="text-xs text-slate-400">
关闭后,登录主页将彻底隐藏注册入口,仅允许系统管理员在后台手动添加开通账号。
</p>
</div>
<button
type="button"
onClick={() => handleTogglePolicy('allowRegistration')}
className={`p-1.5 rounded-xl transition ${
systemConfig.allowRegistration
? 'text-cyan-400 hover:text-cyan-300'
: 'text-slate-600 hover:text-slate-500'
}`}
>
{systemConfig.allowRegistration ? (
<ToggleRight className="w-9 h-9" />
) : (
<ToggleLeft className="w-9 h-9" />
)}
</button>
</div>
{/* 开关 2: 注册需审批 */}
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800 flex items-center justify-between">
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-white">新注册账号必须经过管理员审批</span>
<span
className={`text-[10px] font-mono px-2 py-0.5 rounded-full ${
systemConfig.requireApproval
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
: 'bg-slate-800 text-slate-400 border border-slate-700'
}`}
>
{systemConfig.requireApproval ? '强制审批' : '免审激活'}
</span>
</div>
<p className="text-xs text-slate-400">
开启后,自主注册的账号处于待审核状态无法登录,必须由管理员在“新用户注册审核”中心指派职位并激活。
</p>
</div>
<button
type="button"
onClick={() => handleTogglePolicy('requireApproval')}
className={`p-1.5 rounded-xl transition ${
systemConfig.requireApproval
? 'text-cyan-400 hover:text-cyan-300'
: 'text-slate-600 hover:text-slate-500'
}`}
>
{systemConfig.requireApproval ? (
<ToggleRight className="w-9 h-9" />
) : (
<ToggleLeft className="w-9 h-9" />
)}
</button>
</div>
</div>
</div>
)}
{/* 4. 全局角色与权限矩阵 (RBAC) */}
{activeTab === 'rbac' && (
<div className="space-y-5">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<div>
<h3 className="text-sm font-semibold text-white">全局动态角色与 7 项原子权限矩阵</h3>
<p className="text-xs text-slate-400">
支持动态增删角色,定制各职位默认拥有的全局业务权限能力
</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setShowNewRoleDialog(true)}
className="px-3 py-1.5 rounded-xl border border-slate-700 hover:bg-slate-800 text-xs font-medium text-slate-300 flex items-center gap-1 transition"
>
<Plus className="w-3.5 h-3.5" />
<span>新增自定义角色</span>
</button>
<button
type="button"
onClick={handleSaveRBAC}
className="px-4 py-1.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white text-xs font-medium flex items-center gap-1 transition shadow-md shadow-amber-600/20"
>
<Check className="w-3.5 h-3.5" />
<span>保存权限矩阵配置</span>
</button>
</div>
</div>
{/* 角色卡片选择 */}
<div className="grid grid-cols-4 gap-2.5">
{editRoles.map((r) => {
const isSelected = r.id === selectedRoleId;
return (
<div
key={r.id}
onClick={() => setSelectedRoleId(r.id)}
className={`p-3 rounded-xl border cursor-pointer transition ${
isSelected
? 'bg-amber-500/10 border-amber-500/50 text-white shadow-sm'
: 'bg-slate-950/60 border-slate-800 text-slate-400 hover:border-slate-700'
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="font-bold text-xs">{r.name}</span>
{r.isSystem && (
<span className="text-[10px] font-mono px-1 rounded bg-slate-800 text-slate-400">
内置
</span>
)}
</div>
<div className="text-[11px] font-mono text-cyan-400">{r.code}</div>
<p className="text-[10px] text-slate-500 line-clamp-1 mt-1">{r.description}</p>
</div>
);
})}
</div>
{/* 选定角色的权限勾选矩阵 */}
{selectedRole && (
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800 space-y-3">
<div className="flex items-center justify-between pb-2 border-b border-slate-800/80">
<div>
<span className="text-xs font-bold text-white">
配置【{selectedRole.name}】的授权项
</span>
<span className="text-xs text-slate-400 ml-2">
(已选 {selectedRole.permissions.length} 项)
</span>
</div>
{selectedRole.isSystem && selectedRole.code === 'ADMIN' && (
<span className="text-[11px] text-amber-400 flex items-center gap-1">
<Info className="w-3.5 h-3.5" /> 系统管理员拥有全部底层通配权限
</span>
)}
</div>
<div className="grid grid-cols-1 gap-2">
{ALL_PERMISSION_ITEMS.map((perm) => {
const checked =
selectedRole.code === 'ADMIN' || selectedRole.permissions.includes(perm.key);
const disabled = selectedRole.code === 'ADMIN';
return (
<label
key={perm.key}
className={`p-2.5 rounded-lg border flex items-center justify-between transition ${
checked
? 'bg-amber-950/20 border-amber-800/40'
: 'bg-slate-900/40 border-slate-800/60 opacity-60'
} ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
>
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<span className={`text-xs font-semibold ${checked ? 'text-white' : 'text-slate-400'}`}>
{perm.name}
</span>
<span className="text-[10px] font-mono text-slate-500 px-1 py-0.2 rounded bg-slate-900 border border-slate-800">
{perm.key}
</span>
</div>
<p className="text-[11px] text-slate-400">{perm.desc}</p>
</div>
<input
type="checkbox"
checked={checked}
disabled={disabled}
onChange={() => handleTogglePermission(selectedRole.id, perm.key)}
className="w-4 h-4 rounded text-amber-500 focus:ring-amber-500 bg-slate-950 border-slate-700 cursor-pointer"
/>
</label>
);
})}
</div>
</div>
)}
</div>
)}
{/* 5. 环境与运行状态 */}
{activeTab === 'system' && (
<div className="space-y-5">
<div className="pb-3 border-b border-slate-800">
<h3 className="text-sm font-semibold text-white">系统运行架构与环境隔离状态</h3>
<p className="text-xs text-slate-400">
监控开发、测试与生产环境三套隔离状态,以及知识库共享与大模型后端运行健康度
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800 space-y-2">
<span className="text-xs font-bold text-white flex items-center gap-1.5">
<Server className="w-4 h-4 text-cyan-400" />
<span>运行环境 (ENV)</span>
</span>
<div className="text-lg font-mono font-bold text-cyan-400">{env.toUpperCase()}</div>
<p className="text-[11px] text-slate-400">
{env === 'dev'
? '开发环境:支持测试人员调试快捷身份切换,数据隔离在 data/dev'
: '测试/生产环境:严格禁用身份切换,仅允许真实登出与登录'}
</p>
</div>
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800 space-y-2">
<span className="text-xs font-bold text-white flex items-center gap-1.5">
<Lock className="w-4 h-4 text-amber-400" />
<span>超级管理员凭据 (.env)</span>
</span>
<div className="text-xs font-mono text-slate-300">
ADMIN_USERNAME: <span className="text-amber-400">admin</span>
</div>
<div className="text-xs font-mono text-slate-300">
ADMIN_PASSWORD: <span className="text-slate-500">••••••••••••••</span>
</div>
<p className="text-[11px] text-slate-400">
系统管理员凭据严格从运行环境环境变量动态注入,具备防死锁兜底
</p>
</div>
</div>
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800 space-y-3">
<span className="text-xs font-bold text-white">跨环境隔离合规准则摘要</span>
<div className="text-xs text-slate-400 space-y-1.5 leading-relaxed">
<p>• <b>知识库共享</b>:共享合规法规数据库(端口 5432,数据库 regulations)。</p>
<p>• <b>对话与业务库隔离(红线)</b>:各环境对话历史、草稿与受控文档库物理隔离在各自目录,互不可见。</p>
<p>• <b>身份切换安全隔离(红线)</b>:真实测试(test)与发布(main)环境禁止一切快捷身份切换,必须凭密码登录。</p>
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* 新建员工账号弹窗 */}
{showCreateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in">
<div className="w-full max-w-md bg-slate-900 border border-amber-500/40 rounded-2xl shadow-2xl p-6 text-slate-100">
<div className="flex items-center justify-between pb-3 border-b border-slate-800 mb-4">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Plus className="w-4 h-4 text-amber-400" />
<span>管理员开通新员工账号</span>
</h3>
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="text-slate-400 hover:text-white"
>
<X className="w-4 h-4" />
</button>
</div>
<form onSubmit={handleCreateUser} className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-300 mb-1">
真实姓名 <span className="text-rose-400">*</span>
</label>
<input
type="text"
required
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="例如: 赵工"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-300 mb-1">
登录用户名 <span className="text-rose-400">*</span>
</label>
<input
type="text"
required
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
placeholder="如: zhaogong"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 focus:outline-none focus:border-amber-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-slate-300 mb-1">
初始密码 <span className="text-rose-400">*</span>
</label>
<input
type="text"
required
value={newInitialPassword}
onChange={(e) => setNewInitialPassword(e.target.value)}
placeholder="默认 123456"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 focus:outline-none focus:border-amber-500 font-mono"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-300 mb-1">
指派受控职位与角色 <span className="text-rose-400">*</span>
</label>
<select
value={newRoleId}
onChange={(e) => setNewRoleId(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-cyan-300 focus:outline-none focus:border-amber-500"
>
{roles.map((r) => (
<option key={r.id} value={r.id}>
{r.name} ({r.code})
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-400 mb-1">部门 (选填)</label>
<input
type="text"
value={newDepartment}
onChange={(e) => setNewDepartment(e.target.value)}
placeholder="如: 研发中心"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-200"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1">工号 (选填)</label>
<input
type="text"
value={newEmployeeId}
onChange={(e) => setNewEmployeeId(e.target.value)}
placeholder="如: RD-099"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-200"
/>
</div>
</div>
<div className="p-2.5 rounded-xl bg-amber-500/10 border border-amber-500/30 text-[11px] text-amber-200 flex items-center gap-2">
<AlertCircle className="w-4 h-4 flex-shrink-0" />
<span>核心约束:由管理员创建的账号,首次登录系统时将被强制要求修改密码。</span>
</div>
<div className="pt-2 flex justify-end gap-2">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-4 py-1.5 rounded-xl border border-slate-700 text-xs text-slate-300 hover:bg-slate-800"
>
取消
</button>
<button
type="submit"
className="px-4 py-1.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white text-xs font-medium"
>
确认开通账号
</button>
</div>
</form>
</div>
</div>
)}
{/* 新增自定义角色对话框 */}
{showNewRoleDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in">
<div className="w-full max-w-sm bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl p-5 text-slate-100">
<h3 className="text-sm font-bold text-white mb-3">新增自定义受控角色</h3>
<div className="space-y-3">
<div>
<label className="block text-xs text-slate-400 mb-1">显示名称</label>
<input
type="text"
value={newRoleName}
onChange={(e) => setNewRoleName(e.target.value)}
placeholder="如: 合规审计专家"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100"
/>
</div>
<div>
<label className="block text-xs text-slate-400 mb-1">唯一代码</label>
<input
type="text"
value={newRoleCode}
onChange={(e) => setNewRoleCode(e.target.value.toUpperCase())}
placeholder="如: AUDITOR"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 font-mono"
/>
</div>
<div>
<label className="block text-xs text-slate-400 mb-1">职责描述</label>
<textarea
value={newRoleDesc}
onChange={(e) => setNewRoleDesc(e.target.value)}
placeholder="描述该角色的权限范畴..."
rows={2}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100"
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<button
type="button"
onClick={() => setShowNewRoleDialog(false)}
className="px-3 py-1.5 rounded-xl border border-slate-700 text-xs text-slate-300"
>
取消
</button>
<button
type="button"
onClick={() => {
if (!newRoleName.trim() || !newRoleCode.trim()) return;
const newId = `role-${newRoleCode.toLowerCase()}-${Date.now()}`;
const createdRole: RoleDefinition = {
id: newId,
name: newRoleName.trim(),
code: newRoleCode.trim(),
description: newRoleDesc.trim(),
isSystem: false,
permissions: ['workflow:view_all'],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
setEditRoles([...editRoles, createdRole]);
setSelectedRoleId(newId);
setShowNewRoleDialog(false);
setNewRoleName('');
setNewRoleCode('');
setNewRoleDesc('');
}}
className="px-3 py-1.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white text-xs font-medium"
>
添加角色
</button>
</div>
</div>
</div>
)}
</div>
);
};