1067 lines
50 KiB
TypeScript
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>
|
|
);
|
|
};
|