Files
Agent-System/web/src/components/admin/AdminSettingsModal.tsx
T
weichenandCursor ec8a45a5fe fix(web): sync parent user directory after admin account changes
AdminSettingsModal previously left onRefreshUsers as a dead prop, so newly
created users never reached App allUsers/localStorage and Create Project
member candidates stayed stale (UI-01). Wire syncAdminUserDirectories,
surface refresh errors, and document the residual cross-device directory gap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 11:58:06 +00:00

1678 lines
72 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, { useState, useEffect } from 'react';
import {
ShieldAlert,
Users,
UserCheck,
Plus,
Trash2,
Check,
X,
AlertCircle,
CheckCircle2,
Sliders,
Activity,
Shield,
Search,
UserPlus,
GitBranch,
} from 'lucide-react';
import { UserProfile } from '../../types/docgov';
import { RoleDefinition, PermissionKey } from '../../types/rbac';
import { SystemAuthConfig, UserAccount } from '../../types/auth';
import { authClient } from '../../api/auth';
import {
listAdminWorkflows,
getAdminWorkflow,
validateWorkflowVersion,
publishWorkflowVersion,
listAdminTemplates,
WorkflowDefinitionSummary,
WorkflowVersionSummary,
} from '../../api/workflowAdmin';
import { syncAdminUserDirectories } from '../../lib/adminUserDirectorySync';
export interface AdminSettingsModalProps {
isOpen: boolean;
onClose: () => void;
currentUser: UserProfile;
roles: RoleDefinition[];
onSaveRoles: (roles: RoleDefinition[]) => void;
allUsers: UserProfile[];
onUpdateUserRole: (userId: number, roleId: string) => void;
onRefreshUsers: () => void | Promise<void>;
systemConfig: SystemAuthConfig;
onUpdateSystemConfig: (config: Partial<SystemAuthConfig>) => void;
env?: string;
}
const ALL_PERMISSION_ITEMS: {
key: PermissionKey;
label: string;
desc: string;
category: '项目与流程' | '权限与安全' | '文档与导出';
}[] = [
{
key: 'project:create',
label: '创建新项目/立项',
desc: '允许在协同工作台左侧栏发起新医疗器械立项项目并克隆模板',
category: '项目与流程',
},
{
key: 'project:assign_lead',
label: '设置/变更文档责任人',
desc: '允许在文档治理抽屉为交付物指派编制人、审核人与批准人',
category: '项目与流程',
},
{
key: 'perm:manage',
label: '系统权限与角色管理',
desc: '管理员超级特权,可维护角色库、审批账号与配置准入策略',
category: '权限与安全',
},
{
key: 'workflow:config_visibility',
label: '配置工作流默认可见性',
desc: '调整整个业务阶段中各受控交付物的基线查阅准入边界',
category: '权限与安全',
},
{
key: 'doc:override_visibility',
label: '配置单文档可见性覆盖',
desc: '对特定涉密或专精文档设置独立白名单或责任人限制',
category: '权限与安全',
},
{
key: 'workflow:view_all',
label: '查阅常规工作流受控文档',
desc: '是否可浏览项目内的受控工作流交付物大盘',
category: '文档与导出',
},
{
key: 'doc:export',
label: '导出受控成果物 (DOCX/XLSX)',
desc: '允许将交付物编译导出为合规 DOCX 报告或计算 XLSX 表格',
category: '文档与导出',
},
];
export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
isOpen,
onClose,
currentUser,
roles,
onSaveRoles,
allUsers: _allUsers,
onUpdateUserRole,
onRefreshUsers,
systemConfig,
onUpdateSystemConfig,
env = 'dev',
}) => {
const [activeTab, setActiveTab] = useState<
'users' | 'approvals' | 'policy' | 'roles' | 'workflows' | 'status'
>('users');
// 全量账号列表(含待审核及禁用状态)
const [adminUsers, setAdminUsers] = useState<UserAccount[]>([]);
const [userSearchText, setUserSearchText] = useState('');
const [actionTip, setActionTip] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
// 角色库与编辑态
const [editRoles, setEditRoles] = useState<RoleDefinition[]>(roles);
const [selectedRoleId, setSelectedRoleId] = useState<string>(roles[0]?.id || 'role-admin');
const [isCreatingRole, setIsCreatingRole] = useState(false);
const [newRoleName, setNewRoleName] = useState('');
const [newRoleCode, setNewRoleCode] = useState('');
const [newRoleDesc, setNewRoleDesc] = useState('');
// 管理员新建用户表单
const [showCreateUserModal, setShowCreateUserModal] = useState(false);
const [newUname, setNewUname] = useState('');
const [newRealName, setNewRealName] = useState('');
const [newInitPass, setNewInitPass] = useState('InitPass123');
const [newRoleId, setNewRoleId] = useState('role-engineer');
const [newPosition, setNewPosition] = useState('方案工程师');
const [newDept, setNewDept] = useState('研发中心');
// 准入策略本地状态
const [allowReg, setAllowReg] = useState(systemConfig.allowRegistration);
const [requireAppr, setRequireAppr] = useState(systemConfig.requireApproval);
const [adminWorkflows, setAdminWorkflows] = useState<WorkflowDefinitionSummary[]>([]);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<number | null>(null);
const [workflowVersions, setWorkflowVersions] = useState<WorkflowVersionSummary[]>([]);
const [adminTemplates, setAdminTemplates] = useState<Array<{ asset_key: string; name: string }>>([]);
const [validationMsg, setValidationMsg] = useState<string>('');
// 刷新全量用户列表,并同步父级 App allUsers(立项成员候选依赖此目录)
const refreshUsers = async () => {
try {
await syncAdminUserDirectories({
reloadLocalUsers: async () => {
const list = await authClient.listAllUsers();
setAdminUsers(list);
},
onRefreshUsers,
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : '刷新/同步用户目录失败';
setActionTip({ type: 'error', msg });
throw err;
}
};
const refreshWorkflows = async () => {
try {
const list = await listAdminWorkflows();
setAdminWorkflows(list);
const templates = await listAdminTemplates();
setAdminTemplates(templates.map((t) => ({ asset_key: t.asset_key, name: t.name })));
} catch {
setAdminWorkflows([]);
}
};
const loadWorkflowDetail = async (id: number) => {
setSelectedWorkflowId(id);
try {
const detail = await getAdminWorkflow(id);
setWorkflowVersions(detail.versions ?? []);
} catch {
setWorkflowVersions([]);
}
};
const handleValidateVersion = async (workflowId: number, version: number) => {
try {
const res = await validateWorkflowVersion(workflowId, version);
if (res.valid) {
setValidationMsg(`v${version} 校验通过`);
} else {
setValidationMsg(res.issues.map((i) => i.message).join(';'));
}
} catch (err: any) {
setValidationMsg(err.message || '校验失败');
}
};
const handlePublishVersion = async (workflowId: number, version: number) => {
if (!window.confirm(`确认发布工作流版本 v${version}?发布后不可原地修改。`)) return;
try {
await publishWorkflowVersion(workflowId, version);
showNotification('success', `已发布 v${version}`);
refreshWorkflows();
loadWorkflowDetail(workflowId);
} catch (err: any) {
showNotification('error', err.message || '发布失败');
}
};
const refreshRoles = async () => {
try {
const list = await authClient.listRoles();
setEditRoles(list);
} catch {
setEditRoles(roles);
}
};
useEffect(() => {
if (isOpen) {
void refreshUsers().catch(() => {
// 错误已写入 actionTip;打开时失败不阻断模态
});
refreshRoles();
refreshWorkflows();
setAllowReg(systemConfig.allowRegistration);
setRequireAppr(systemConfig.requireApproval);
}
}, [isOpen, roles, systemConfig]);
if (!isOpen) return null;
const showNotification = (type: 'success' | 'error', msg: string) => {
setActionTip({ type, msg });
setTimeout(() => setActionTip(null), 3000);
};
// 1. 人员职位与角色分配
const handleUserRoleChange = async (userId: number, roleId: string) => {
const role = editRoles.find((r) => r.id === roleId);
try {
await authClient.updateUserRole(userId, roleId, role?.name || '方案工程师');
onUpdateUserRole(userId, roleId);
} catch (err: any) {
showNotification('error', err.message || '更新角色失败');
return;
}
try {
await refreshUsers();
showNotification('success', '已成功变更该成员的所属职位与系统角色');
} catch {
showNotification('error', '角色已更新,但全局用户目录同步失败,请刷新后重试');
}
};
// 2. 账号状态(激活/停用)
const handleToggleUserStatus = async (user: UserAccount) => {
const nextStatus = user.status === 'active' ? 'disabled' : 'active';
try {
await authClient.updateUserStatus(user.id, nextStatus);
} catch (err: any) {
showNotification('error', err.message || '切换状态失败');
return;
}
try {
await refreshUsers();
showNotification('success', `已将账号【${user.name}】状态切换为:${nextStatus === 'active' ? '启用' : '停用'}`);
} catch {
showNotification('error', '账号状态已更新,但全局用户目录同步失败,请刷新后重试');
}
};
// 3. 审批通过或驳回
const handleApproveRegistration = async (user: UserAccount, targetRoleId: string) => {
const role = editRoles.find((r) => r.id === targetRoleId);
try {
await authClient.updateUserStatus(user.id, 'active', targetRoleId, role?.name || '方案工程师');
} catch (err: any) {
showNotification('error', err.message || '审批操作失败');
return;
}
try {
await refreshUsers();
showNotification('success', `已批准【${user.name}】注册申请,已正式授予【${role?.name}】职位`);
} catch {
showNotification('error', '审批已生效,但全局用户目录同步失败,请刷新后重试');
}
};
const handleRejectRegistration = async (user: UserAccount) => {
if (!window.confirm(`确定要驳回【${user.name}】的注册申请吗?该账号将被置为禁用。`)) return;
try {
await authClient.updateUserStatus(user.id, 'disabled');
} catch (err: any) {
showNotification('error', err.message || '驳回操作失败');
return;
}
try {
await refreshUsers();
showNotification('success', `已驳回【${user.name}】的注册申请`);
} catch {
showNotification('error', '驳回已生效,但全局用户目录同步失败,请刷新后重试');
}
};
// 4. 重置用户密码为初始并强制改密
const handleResetUserPassword = async (user: UserAccount) => {
const tempPass = 'ResetPass123';
if (!window.confirm(`确定要重置【${user.name}】的密码吗?重置后初始密码为 ${tempPass},该用户下次登录将被强制改密。`)) return;
try {
await authClient.adminResetPassword(user.id, tempPass);
} catch (err: any) {
showNotification('error', err.message || '重置密码失败');
return;
}
try {
await refreshUsers();
showNotification('success', `已重置【${user.name}】密码为 ${tempPass},已标记首次登录强制改密`);
} catch {
showNotification('error', '密码已重置,但全局用户目录同步失败,请刷新后重试');
}
};
// 5. 管理员新建账号
const handleCreateUserSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newUname.trim() || !newRealName.trim() || !newInitPass) {
showNotification('error', '请填写完整账号、姓名与初始密码');
return;
}
try {
await authClient.createAdminUser({
username: newUname.trim(),
name: newRealName.trim(),
initialPassword: newInitPass,
roleId: newRoleId,
position: newPosition,
department: newDept,
});
setShowCreateUserModal(false);
setNewUname('');
setNewRealName('');
try {
await refreshUsers();
showNotification('success', `已成功开通受控账号【${newRealName}】,已自动标记首次登录强制改密`);
} catch {
showNotification(
'error',
`账号【${newRealName}】已开通,但全局用户目录同步失败,立项成员候选可能暂不可见该账号`,
);
}
} catch (err: any) {
showNotification('error', err.message || '新建员工账号失败');
}
};
// 6. 保存准入策略
const handleSavePolicy = async () => {
try {
await authClient.updateSystemAuthConfig({
allowRegistration: allowReg,
requireApproval: requireAppr,
});
onUpdateSystemConfig({
allowRegistration: allowReg,
requireApproval: requireAppr,
});
showNotification('success', '系统准入策略已即刻全局生效');
} catch (err: any) {
showNotification('error', err.message || '更新策略失败');
}
};
// 7. 角色权限矩阵操作
const currentSelectedRole = editRoles.find((r) => r.id === selectedRoleId) || editRoles[0];
const handleTogglePermission = async (permKey: PermissionKey) => {
if (!currentSelectedRole || currentSelectedRole.isSystem) return;
const exists = currentSelectedRole.permissions.includes(permKey);
const updatedPermissions = exists
? currentSelectedRole.permissions.filter((k) => k !== permKey)
: [...currentSelectedRole.permissions, permKey];
try {
const updated = await authClient.updateRole(currentSelectedRole.id, {
name: currentSelectedRole.name,
description: currentSelectedRole.description,
permissions: updatedPermissions,
});
const updatedRoles = editRoles.map((r) => (r.id === updated.id ? updated : r));
setEditRoles(updatedRoles);
onSaveRoles(updatedRoles);
showNotification('success', `已更新【${currentSelectedRole.name}】权限矩阵`);
} catch (err: any) {
showNotification('error', err.message || '更新权限失败');
}
};
const handleCreateRole = async () => {
if (!newRoleName.trim() || !newRoleCode.trim()) return;
try {
const newRole = await authClient.createRole({
code: newRoleCode.trim().toUpperCase(),
name: newRoleName.trim(),
description: newRoleDesc.trim() || '自定义受控职位角色',
permissions: ['workflow:view_all', 'doc:export'],
});
const nextRoles = [...editRoles, newRole];
setEditRoles(nextRoles);
onSaveRoles(nextRoles);
setSelectedRoleId(newRole.id);
setIsCreatingRole(false);
setNewRoleName('');
setNewRoleCode('');
setNewRoleDesc('');
showNotification('success', `已创建新业务角色【${newRole.name}】`);
} catch (err: any) {
showNotification('error', err.message || '创建角色失败');
}
};
const handleDeleteRole = async (roleId: string) => {
const roleToDelete = editRoles.find((r) => r.id === roleId);
if (!roleToDelete || roleToDelete.isSystem) return;
if (!window.confirm(`确定要删除角色【${roleToDelete.name}】吗?删除后该角色人员将自动退回普通工程师。`)) {
return;
}
try {
await authClient.deleteRole(roleId);
const nextRoles = editRoles.filter((r) => r.id !== roleId);
setEditRoles(nextRoles);
onSaveRoles(nextRoles);
setSelectedRoleId(nextRoles[0]?.id || 'role-admin');
showNotification('success', `已删除角色【${roleToDelete.name}】`);
} catch (err: any) {
showNotification('error', err.message || '删除角色失败');
}
};
// 待审批列表
const pendingUsers = adminUsers.filter((u) => u.status === 'pending_approval');
// 过滤后的用户列表
const filteredUsers = adminUsers.filter(
(u) =>
u.name.toLowerCase().includes(userSearchText.toLowerCase()) ||
(u.username && u.username.toLowerCase().includes(userSearchText.toLowerCase())) ||
(u.department && u.department.toLowerCase().includes(userSearchText.toLowerCase()))
);
return (
<div
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(11, 14, 20, 0.75)',
backdropFilter: 'blur(8px)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
fontFamily: 'var(--font-sans)',
}}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
style={{
width: '980px',
maxWidth: '96vw',
height: '660px',
maxHeight: '92vh',
backgroundColor: 'var(--bg-panel, #131820)',
borderRadius: '14px',
border: '1px solid var(--border-strong)',
boxShadow: 'var(--shadow-lg)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* 顶部标题栏 */}
<div
style={{
padding: '14px 22px',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: 'var(--bg-subtle, #1A212C)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div
style={{
width: '36px',
height: '36px',
borderRadius: '8px',
backgroundColor: 'rgba(242, 169, 59, 0.15)',
color: 'var(--amber-500)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<ShieldAlert size={20} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-strong)' }}>
系统管理控制台
</span>
<span
style={{
fontSize: '11px',
padding: '2px 8px',
borderRadius: '999px',
backgroundColor: 'rgba(242, 169, 59, 0.12)',
color: 'var(--amber-500)',
border: '1px solid rgba(242, 169, 59, 0.3)',
fontWeight: 600,
}}
>
ADMIN 独占
</span>
<span
style={{
fontSize: '11px',
padding: '2px 8px',
borderRadius: '999px',
backgroundColor: 'var(--bg-panel)',
color: 'var(--text-muted)',
border: '1px solid var(--border)',
}}
>
运行环境: {env.toUpperCase()}
</span>
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
人员职位治理 · 注册审批中心 · 系统准入策略 · RBAC 角色权限矩阵
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{actionTip && (
<span
style={{
fontSize: '12px',
color: actionTip.type === 'success' ? 'var(--success)' : 'var(--danger)',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
{actionTip.type === 'success' ? <CheckCircle2 size={14} /> : <AlertCircle size={14} />}
<span>{actionTip.msg}</span>
</span>
)}
<button
type="button"
onClick={onClose}
style={{
padding: '6px',
borderRadius: '6px',
color: 'var(--text-muted)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<X size={18} />
</button>
</div>
</div>
{/* 主体左右分栏 */}
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{/* 左侧垂直导航 Tab */}
<div
style={{
width: '210px',
backgroundColor: 'var(--bg-subtle, #1A212C)',
borderRight: '1px solid var(--border)',
padding: '16px 12px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<button
type="button"
onClick={() => setActiveTab('users')}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
backgroundColor: activeTab === 'users' ? 'var(--bg-panel)' : 'transparent',
color: activeTab === 'users' ? 'var(--brand-500)' : 'var(--text)',
border: activeTab === 'users' ? '1px solid var(--border)' : '1px solid transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<Users size={16} />
<span>人员与职位治理</span>
</button>
<button
type="button"
onClick={() => setActiveTab('approvals')}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
backgroundColor: activeTab === 'approvals' ? 'var(--bg-panel)' : 'transparent',
color: activeTab === 'approvals' ? 'var(--brand-500)' : 'var(--text)',
border: activeTab === 'approvals' ? '1px solid var(--border)' : '1px solid transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<UserCheck size={16} />
<span>注册审批中心</span>
</div>
{pendingUsers.length > 0 && (
<span
style={{
fontSize: '10px',
fontWeight: 700,
padding: '1px 6px',
borderRadius: '999px',
backgroundColor: 'var(--amber-500)',
color: '#000000',
}}
>
{pendingUsers.length}
</span>
)}
</button>
<button
type="button"
onClick={() => setActiveTab('policy')}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
backgroundColor: activeTab === 'policy' ? 'var(--bg-panel)' : 'transparent',
color: activeTab === 'policy' ? 'var(--brand-500)' : 'var(--text)',
border: activeTab === 'policy' ? '1px solid var(--border)' : '1px solid transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<Sliders size={16} />
<span>系统准入策略</span>
</button>
<button
type="button"
onClick={() => setActiveTab('roles')}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
backgroundColor: activeTab === 'roles' ? 'var(--bg-panel)' : 'transparent',
color: activeTab === 'roles' ? 'var(--brand-500)' : 'var(--text)',
border: activeTab === 'roles' ? '1px solid var(--border)' : '1px solid transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<Shield size={16} />
<span>RBAC 角色权限矩阵</span>
</button>
<button
type="button"
onClick={() => setActiveTab('workflows')}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
backgroundColor: activeTab === 'workflows' ? 'var(--bg-panel)' : 'transparent',
color: activeTab === 'workflows' ? 'var(--brand-500)' : 'var(--text)',
border: activeTab === 'workflows' ? '1px solid var(--border)' : '1px solid transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<GitBranch size={16} />
<span>工作流与模板库</span>
</button>
<button
type="button"
onClick={() => setActiveTab('status')}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
backgroundColor: activeTab === 'status' ? 'var(--bg-panel)' : 'transparent',
color: activeTab === 'status' ? 'var(--brand-500)' : 'var(--text)',
border: activeTab === 'status' ? '1px solid var(--border)' : '1px solid transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<Activity size={16} />
<span>系统状态与审计</span>
</button>
</div>
{/* 底部系统信息 */}
<div
style={{
padding: '10px',
borderRadius: '8px',
backgroundColor: 'var(--bg-card)',
border: '1px solid var(--border)',
fontSize: '11px',
color: 'var(--text-muted)',
}}
>
<div>水木东方医疗器械 AI 工作台</div>
<div style={{ color: 'var(--text-faint)', marginTop: '2px' }}>Auth & RBAC v2.4</div>
</div>
</div>
{/* 右侧主视口 */}
<div
style={{
flex: 1,
padding: '20px 24px',
overflowY: 'auto',
backgroundColor: 'var(--bg-panel, #131820)',
}}
>
{/* Tab 1: 人员与职位治理 */}
{activeTab === 'users' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ position: 'relative', width: '280px' }}>
<Search
size={15}
style={{
position: 'absolute',
left: '10px',
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--text-muted)',
}}
/>
<input
type="text"
value={userSearchText}
onChange={(e) => setUserSearchText(e.target.value)}
placeholder="搜索姓名、用户名或部门..."
style={{
width: '100%',
padding: '7px 10px 7px 32px',
borderRadius: '8px',
backgroundColor: 'var(--bg-app)',
border: '1px solid var(--border)',
color: 'var(--text-strong)',
fontSize: '12px',
boxSizing: 'border-box',
}}
/>
</div>
<button
type="button"
onClick={() => setShowCreateUserModal(true)}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '7px 14px',
borderRadius: '8px',
background: 'var(--brand-grad)',
color: '#FFFFFF',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(79, 107, 255, 0.3)',
}}
>
<UserPlus size={15} />
<span>开通受控员工账号</span>
</button>
</div>
{/* 用户列表表格 */}
<div
style={{
borderRadius: '8px',
border: '1px solid var(--border)',
overflow: 'hidden',
}}
>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', fontSize: '12px' }}>
<thead>
<tr style={{ backgroundColor: 'var(--bg-subtle)', borderBottom: '1px solid var(--border)', color: 'var(--text-muted)' }}>
<th style={{ padding: '10px 14px', fontWeight: 600 }}>成员姓名</th>
<th style={{ padding: '10px 14px', fontWeight: 600 }}>登录账号 / 工号</th>
<th style={{ padding: '10px 14px', fontWeight: 600 }}>所属部门</th>
<th style={{ padding: '10px 14px', fontWeight: 600 }}>系统职位与角色</th>
<th style={{ padding: '10px 14px', fontWeight: 600 }}>账号状态</th>
<th style={{ padding: '10px 14px', fontWeight: 600, textAlign: 'right' }}>操作治理</th>
</tr>
</thead>
<tbody>
{filteredUsers.map((u) => {
const isSelf = u.id === currentUser.id;
return (
<tr
key={u.id}
style={{
borderBottom: '1px solid var(--border-light)',
backgroundColor: 'transparent',
}}
>
<td style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{
width: '28px',
height: '28px',
borderRadius: '50%',
backgroundColor: 'rgba(79, 107, 255, 0.15)',
color: 'var(--brand-500)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '12px',
}}
>
{u.name.slice(0, 1)}
</div>
<span style={{ fontWeight: 600, color: 'var(--text-strong)' }}>{u.name}</span>
{isSelf && (
<span style={{ fontSize: '10px', color: 'var(--amber-500)', padding: '1px 4px', borderRadius: '4px', backgroundColor: 'rgba(242, 169, 59, 0.1)' }}>
当前账号
</span>
)}
</div>
</td>
<td style={{ padding: '10px 14px', color: 'var(--text-muted)' }}>
<div>{u.username || '系统预设'}</div>
<div style={{ fontSize: '11px', color: 'var(--text-faint)' }}>{u.employeeId || 'EMP-未设定'}</div>
</td>
<td style={{ padding: '10px 14px', color: 'var(--text)' }}>
{u.department || '未分配部门'}
</td>
<td style={{ padding: '10px 14px' }}>
<select
value={u.roleId || 'role-engineer'}
disabled={isSelf}
onChange={(e) => handleUserRoleChange(u.id, e.target.value)}
style={{
padding: '4px 8px',
borderRadius: '6px',
backgroundColor: 'var(--bg-app)',
border: '1px solid var(--border)',
color: 'var(--brand-500)',
fontSize: '12px',
fontWeight: 500,
cursor: isSelf ? 'not-allowed' : 'pointer',
}}
>
{editRoles.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
</select>
</td>
<td style={{ padding: '10px 14px' }}>
<span
style={{
fontSize: '11px',
padding: '2px 8px',
borderRadius: '999px',
fontWeight: 600,
backgroundColor:
u.status === 'active'
? 'rgba(53, 199, 126, 0.12)'
: u.status === 'pending_approval'
? 'rgba(242, 169, 59, 0.15)'
: 'rgba(228, 87, 75, 0.12)',
color:
u.status === 'active'
? 'var(--success)'
: u.status === 'pending_approval'
? 'var(--amber-500)'
: 'var(--danger)',
}}
>
{u.status === 'active' ? '正常在职' : u.status === 'pending_approval' ? '待审核' : '已停用'}
</span>
</td>
<td style={{ padding: '10px 14px', textAlign: 'right' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '8px' }}>
<button
type="button"
onClick={() => handleResetUserPassword(u)}
style={{
fontSize: '11px',
color: 'var(--text-muted)',
padding: '3px 6px',
borderRadius: '4px',
border: '1px solid var(--border)',
cursor: 'pointer',
}}
title="重置初始密码并强制下次改密"
>
重置密码
</button>
{!isSelf && (
<button
type="button"
onClick={() => handleToggleUserStatus(u)}
style={{
fontSize: '11px',
color: u.status === 'active' ? 'var(--danger)' : 'var(--success)',
padding: '3px 6px',
borderRadius: '4px',
border: '1px solid var(--border)',
cursor: 'pointer',
}}
>
{u.status === 'active' ? '停用账号' : '恢复激活'}
</button>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Tab 2: 注册审批中心 */}
{activeTab === 'approvals' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ paddingBottom: '12px', borderBottom: '1px solid var(--border)' }}>
<div style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-strong)' }}>
新员工注册审批与职位指派中心
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
依据 NMPA 体系规范,自主注册申请需经系统管理员人工核准并正式授予业务职位。
</div>
</div>
{pendingUsers.length === 0 ? (
<div
style={{
padding: '48px 20px',
textAlign: 'center',
borderRadius: '10px',
backgroundColor: 'var(--bg-subtle)',
border: '1px dashed var(--border)',
}}
>
<CheckCircle2 size={36} style={{ color: 'var(--success)', margin: '0 auto 12px' }} />
<div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-strong)' }}>
当前没有待审批的注册申请
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
所有新注册员工均已完成合规审批并完成业务职位委派。
</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{pendingUsers.map((u) => {
return (
<div
key={u.id}
style={{
padding: '16px 20px',
borderRadius: '10px',
backgroundColor: 'var(--bg-subtle)',
border: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style={{ fontSize: '14px', fontWeight: 700, color: 'var(--text-strong)' }}>
{u.name}
</span>
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
账号: {u.username}
</span>
<span
style={{
fontSize: '11px',
padding: '2px 8px',
borderRadius: '999px',
backgroundColor: 'rgba(242, 169, 59, 0.15)',
color: 'var(--amber-500)',
fontWeight: 600,
}}
>
待审核
</span>
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '6px', display: 'flex', gap: '16px' }}>
<span>部门: {u.department || '未填写'}</span>
<span>工号: {u.employeeId || '未填写'}</span>
<span>申请时间: {u.createdAt || '刚刚'}</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<select
id={`role-select-${u.id}`}
defaultValue="role-engineer"
style={{
padding: '6px 10px',
borderRadius: '6px',
backgroundColor: 'var(--bg-app)',
border: '1px solid var(--border)',
color: 'var(--brand-500)',
fontSize: '12px',
fontWeight: 500,
}}
>
{editRoles.map((r) => (
<option key={r.id} value={r.id}>
授予: {r.name}
</option>
))}
</select>
<button
type="button"
onClick={() => {
const sel = document.getElementById(`role-select-${u.id}`) as HTMLSelectElement;
handleApproveRegistration(u, sel?.value || 'role-engineer');
}}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '6px 12px',
borderRadius: '6px',
backgroundColor: 'var(--success)',
color: '#FFFFFF',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
}}
>
<Check size={14} />
<span>通过并指派</span>
</button>
<button
type="button"
onClick={() => handleRejectRegistration(u)}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '6px 12px',
borderRadius: '6px',
backgroundColor: 'rgba(228, 87, 75, 0.1)',
border: '1px solid rgba(228, 87, 75, 0.3)',
color: 'var(--danger)',
fontSize: '12px',
cursor: 'pointer',
}}
>
<X size={14} />
<span>驳回</span>
</button>
</div>
</div>
);
})}
</div>
)}
</div>
)}
{/* Tab 3: 系统准入策略 */}
{activeTab === 'policy' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '22px' }}>
<div style={{ paddingBottom: '12px', borderBottom: '1px solid var(--border)' }}>
<div style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-strong)' }}>
系统全局准入与注册审核策略
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
控制系统外部开放度,关闭自主注册或开启强制审核均实时阻断越权账号。
</div>
</div>
{/* 开关 1: 开放自主注册 */}
<div
style={{
padding: '16px 20px',
borderRadius: '10px',
backgroundColor: 'var(--bg-subtle)',
border: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>
<div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-strong)' }}>
允许新用户自主注册账号
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
开启后,登录页将呈现「用户自主注册」选项卡;关闭后,仅管理员可在控制台开通账号。
</div>
</div>
<input
type="checkbox"
checked={allowReg}
onChange={(e) => setAllowReg(e.target.checked)}
style={{ width: '18px', height: '18px', cursor: 'pointer', accentColor: 'var(--brand-500)' }}
/>
</div>
{/* 开关 2: 注册需审核 */}
<div
style={{
padding: '16px 20px',
borderRadius: '10px',
backgroundColor: 'var(--bg-subtle)',
border: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>
<div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-strong)' }}>
新注册账号必须由管理员审核通过
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
开启后,新注册账号处于「待审核」状态禁止登录,直至管理员在审批中心核准并正式分配职位。
</div>
</div>
<input
type="checkbox"
checked={requireAppr}
onChange={(e) => setRequireAppr(e.target.checked)}
style={{ width: '18px', height: '18px', cursor: 'pointer', accentColor: 'var(--brand-500)' }}
/>
</div>
<div>
<button
type="button"
onClick={handleSavePolicy}
style={{
padding: '9px 24px',
borderRadius: '8px',
background: 'var(--brand-grad)',
color: '#FFFFFF',
fontSize: '13px',
fontWeight: 600,
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(79, 107, 255, 0.3)',
}}
>
保存并立即应用准入策略
</button>
</div>
</div>
)}
{/* Tab 4: RBAC 角色权限矩阵 */}
{activeTab === 'roles' && (
<div style={{ display: 'flex', height: '100%', gap: '16px' }}>
{/* 角色列表 */}
<div
style={{
width: '240px',
backgroundColor: 'var(--bg-subtle)',
borderRadius: '8px',
border: '1px solid var(--border)',
padding: '12px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<div style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text-muted)', marginBottom: '4px' }}>
全局受控角色库 ({editRoles.length})
</div>
{editRoles.map((r) => {
const isSel = r.id === selectedRoleId;
return (
<div
key={r.id}
onClick={() => setSelectedRoleId(r.id)}
style={{
padding: '8px 10px',
borderRadius: '6px',
backgroundColor: isSel ? 'var(--bg-panel)' : 'transparent',
border: isSel ? '1px solid var(--brand-500)' : '1px solid transparent',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>
<div style={{ fontSize: '13px', fontWeight: 600, color: isSel ? 'var(--brand-500)' : 'var(--text-strong)' }}>
{r.name}
</div>
<div style={{ fontSize: '10px', color: 'var(--text-faint)' }}>
{r.code} · {r.permissions.length} 项权限
</div>
</div>
{!r.isSystem && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleDeleteRole(r.id);
}}
style={{ color: 'var(--text-faint)', cursor: 'pointer', padding: '2px' }}
title="删除自定义角色"
>
<Trash2 size={13} />
</button>
)}
</div>
);
})}
</div>
{/* 新增自定义角色 */}
<div style={{ paddingTop: '10px', borderTop: '1px solid var(--border)' }}>
{isCreatingRole ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<input
type="text"
value={newRoleName}
onChange={(e) => setNewRoleName(e.target.value)}
placeholder="角色名称 (如 临床协调员)"
style={{ width: '100%', padding: '6px 8px', borderRadius: '4px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-strong)', fontSize: '11px', boxSizing: 'border-box' }}
/>
<input
type="text"
value={newRoleCode}
onChange={(e) => setNewRoleCode(e.target.value)}
placeholder="标识代号 (如 CRA)"
style={{ width: '100%', padding: '6px 8px', borderRadius: '4px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-strong)', fontSize: '11px', boxSizing: 'border-box' }}
/>
<div style={{ display: 'flex', gap: '6px' }}>
<button
type="button"
onClick={handleCreateRole}
style={{ flex: 1, padding: '4px', borderRadius: '4px', background: 'var(--brand-grad)', color: '#FFFFFF', fontSize: '11px', fontWeight: 600, cursor: 'pointer' }}
>
保存
</button>
<button
type="button"
onClick={() => setIsCreatingRole(false)}
style={{ padding: '4px 8px', borderRadius: '4px', backgroundColor: 'var(--bg-app)', color: 'var(--text-muted)', fontSize: '11px', cursor: 'pointer' }}
>
取消
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setIsCreatingRole(true)}
style={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
padding: '6px',
borderRadius: '6px',
backgroundColor: 'var(--bg-panel)',
border: '1px dashed var(--border)',
color: 'var(--brand-500)',
fontSize: '12px',
cursor: 'pointer',
}}
>
<Plus size={14} />
<span>新增受控角色</span>
</button>
)}
</div>
</div>
{/* 对应角色的权限复选矩阵 */}
<div style={{ flex: 1, overflowY: 'auto' }}>
<div style={{ paddingBottom: '12px', borderBottom: '1px solid var(--border)', marginBottom: '14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '15px', fontWeight: 700, color: 'var(--text-strong)' }}>
【{currentSelectedRole?.name}】权限配置矩阵
</span>
{currentSelectedRole?.isSystem && (
<span style={{ fontSize: '11px', padding: '1px 6px', borderRadius: '4px', backgroundColor: 'rgba(79, 107, 255, 0.1)', color: 'var(--brand-500)' }}>
系统内置受控角色 (只读)
</span>
)}
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
{currentSelectedRole?.description}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{ALL_PERMISSION_ITEMS.map((item) => {
const isChecked =
currentSelectedRole?.code === 'ADMIN' ||
currentSelectedRole?.permissions.includes(item.key);
const isLocked = currentSelectedRole?.isSystem;
return (
<div
key={item.key}
onClick={() => !isLocked && handleTogglePermission(item.key)}
style={{
padding: '12px 14px',
borderRadius: '8px',
backgroundColor: isChecked ? 'var(--bg-subtle)' : 'var(--bg-app)',
border: isChecked ? '1px solid var(--border)' : '1px dashed var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
cursor: isLocked ? 'not-allowed' : 'pointer',
}}
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', fontWeight: 600, color: isChecked ? 'var(--text-strong)' : 'var(--text-faint)' }}>
{item.label}
</span>
<span style={{ fontSize: '10px', padding: '1px 6px', borderRadius: '4px', backgroundColor: 'var(--bg-panel)', color: 'var(--text-muted)' }}>
{item.category}
</span>
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '3px' }}>
{item.desc}
</div>
</div>
<input
type="checkbox"
checked={isChecked}
disabled={isLocked}
onChange={() => {}}
style={{ width: '16px', height: '16px', accentColor: 'var(--brand-500)', cursor: isLocked ? 'not-allowed' : 'pointer' }}
/>
</div>
);
})}
</div>
</div>
</div>
)}
{/* Tab 5: 系统状态与审计 */}
{activeTab === 'workflows' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<h3 style={{ margin: 0, fontSize: '15px', color: 'var(--text-strong)' }}>工作流定义(DB 权威)</h3>
<p style={{ margin: 0, fontSize: '12px', color: 'var(--text-muted)' }}>
已发布版本不可变;新项目选用固定版本快照(项目激活冻结见 Phase 3)。
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
{adminWorkflows.map((wf) => (
<button
key={wf.id}
type="button"
onClick={() => loadWorkflowDetail(wf.id)}
style={{
display: 'block',
width: '100%',
textAlign: 'left',
padding: '10px 12px',
marginBottom: '8px',
borderRadius: '8px',
border:
selectedWorkflowId === wf.id
? '1px solid var(--brand-500)'
: '1px solid var(--border)',
background: 'var(--bg-card)',
cursor: 'pointer',
color: 'var(--text)',
}}
>
<div style={{ fontWeight: 600 }}>{wf.name}</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
{wf.key} · 最新发布 v{wf.latest_published_version || '-'}
</div>
</button>
))}
</div>
<div>
{selectedWorkflowId && (
<>
<div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px' }}>版本列表</div>
{workflowVersions.map((v) => (
<div
key={v.id}
style={{
padding: '10px',
border: '1px solid var(--border)',
borderRadius: '8px',
marginBottom: '8px',
fontSize: '12px',
}}
>
<div>
v{v.version} · {v.status}
</div>
<div style={{ color: 'var(--text-muted)', wordBreak: 'break-all' }}>
hash: {v.definition_hash.slice(0, 16)}…
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button type="button" onClick={() => handleValidateVersion(selectedWorkflowId, v.version)}>
校验
</button>
{v.status === 'draft' && (
<button type="button" onClick={() => handlePublishVersion(selectedWorkflowId, v.version)}>
发布
</button>
)}
</div>
</div>
))}
{validationMsg && (
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>{validationMsg}</div>
)}
</>
)}
</div>
</div>
<div>
<div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px' }}>模板库 ({adminTemplates.length})</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
{adminTemplates.map((t) => t.asset_key).join(', ') || '暂无模板(需 PG 环境 bootstrap)'}
</div>
</div>
</div>
)}
{activeTab === 'status' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}>
<div style={{ paddingBottom: '12px', borderBottom: '1px solid var(--border)' }}>
<div style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-strong)' }}>
系统安全与运行状态
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
环境拓扑隔离与安全凭据审计。
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}>
<div style={{ padding: '16px', borderRadius: '10px', backgroundColor: 'var(--bg-subtle)', border: '1px solid var(--border)' }}>
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>当前运行环境态</div>
<div style={{ fontSize: '18px', fontWeight: 700, color: 'var(--brand-500)', marginTop: '4px' }}>
{env.toUpperCase()}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-faint)', marginTop: '4px' }}>
{env === 'dev' ? '开发态 (允许测试快速身份切换)' : '正式受控态 (强制真实登出/登录)'}
</div>
</div>
<div style={{ padding: '16px', borderRadius: '10px', backgroundColor: 'var(--bg-subtle)', border: '1px solid var(--border)' }}>
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>密码加密算法</div>
<div style={{ fontSize: '18px', fontWeight: 700, color: 'var(--success)', marginTop: '4px' }}>
Bcrypt (加盐哈希)
</div>
<div style={{ fontSize: '11px', color: 'var(--text-faint)', marginTop: '4px' }}>
电子签名与审计事件由服务端记录并防篡改校验
</div>
</div>
<div style={{ padding: '16px', borderRadius: '10px', backgroundColor: 'var(--bg-subtle)', border: '1px solid var(--border)' }}>
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>当前系统人员总数</div>
<div style={{ fontSize: '18px', fontWeight: 700, color: 'var(--text-strong)', marginTop: '4px' }}>
{adminUsers.length} 人
</div>
<div style={{ fontSize: '11px', color: 'var(--text-faint)', marginTop: '4px' }}>
在职正常: {adminUsers.filter((u) => u.status === 'active').length} 人 · 待审: {pendingUsers.length} 人
</div>
</div>
<div style={{ padding: '16px', borderRadius: '10px', backgroundColor: 'var(--bg-subtle)', border: '1px solid var(--border)' }}>
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>超管凭据配置通道</div>
<div style={{ fontSize: '18px', fontWeight: 700, color: 'var(--amber-500)', marginTop: '4px' }}>
.env 动态环境变量
</div>
<div style={{ fontSize: '11px', color: 'var(--text-faint)', marginTop: '4px' }}>
ADMIN_USERNAME 与 ADMIN_PASSWORD 受控注入
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* 管理员直接开通员工账号的内层弹窗 */}
{showCreateUserModal && (
<div
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.6)',
zIndex: 1100,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
}}
onClick={(e) => {
if (e.target === e.currentTarget) setShowCreateUserModal(false);
}}
>
<div
style={{
width: '460px',
backgroundColor: 'var(--bg-panel)',
borderRadius: '12px',
border: '1px solid var(--border-strong)',
boxShadow: 'var(--shadow-lg)',
padding: '24px',
}}
>
<div style={{ fontSize: '16px', fontWeight: 700, color: 'var(--text-strong)', marginBottom: '4px' }}>
开通受控员工账号
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginBottom: '18px' }}>
由管理员开通的员工,首次登录系统将被强制修改初始密码。
</div>
<form onSubmit={handleCreateUserSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
<div>
<label style={{ display: 'block', fontSize: '12px', fontWeight: 600, color: 'var(--text-strong)', marginBottom: '4px' }}>
用户名 (系统账号) *
</label>
<input
type="text"
value={newUname}
onChange={(e) => setNewUname(e.target.value)}
placeholder="如 li_si"
style={{ width: '100%', padding: '8px 10px', borderRadius: '6px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-strong)', fontSize: '12px', boxSizing: 'border-box' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '12px', fontWeight: 600, color: 'var(--text-strong)', marginBottom: '4px' }}>
真实姓名 *
</label>
<input
type="text"
value={newRealName}
onChange={(e) => setNewRealName(e.target.value)}
placeholder="如 李四"
style={{ width: '100%', padding: '8px 10px', borderRadius: '6px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-strong)', fontSize: '12px', boxSizing: 'border-box' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '12px', fontWeight: 600, color: 'var(--text-strong)', marginBottom: '4px' }}>
分配职位与角色
</label>
<select
value={newRoleId}
onChange={(e) => {
setNewRoleId(e.target.value);
const r = editRoles.find((role) => role.id === e.target.value);
if (r) setNewPosition(r.name);
}}
style={{ width: '100%', padding: '8px 10px', borderRadius: '6px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-strong)', fontSize: '12px', boxSizing: 'border-box' }}
>
{editRoles.map((r) => (
<option key={r.id} value={r.id}>
{r.name} ({r.code})
</option>
))}
</select>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div>
<label style={{ display: 'block', fontSize: '12px', color: 'var(--text-muted)', marginBottom: '4px' }}>
部门
</label>
<input
type="text"
value={newDept}
onChange={(e) => setNewDept(e.target.value)}
style={{ width: '100%', padding: '8px 10px', borderRadius: '6px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-strong)', fontSize: '12px', boxSizing: 'border-box' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '12px', color: 'var(--text-muted)', marginBottom: '4px' }}>
临时初始密码
</label>
<input
type="text"
value={newInitPass}
onChange={(e) => setNewInitPass(e.target.value)}
style={{ width: '100%', padding: '8px 10px', borderRadius: '6px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-strong)', fontSize: '12px', boxSizing: 'border-box' }}
/>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '10px' }}>
<button
type="button"
onClick={() => setShowCreateUserModal(false)}
style={{ padding: '7px 14px', borderRadius: '6px', backgroundColor: 'var(--bg-app)', border: '1px solid var(--border)', color: 'var(--text-muted)', fontSize: '12px', cursor: 'pointer' }}
>
取消
</button>
<button
type="submit"
style={{ padding: '7px 18px', borderRadius: '6px', background: 'var(--brand-grad)', color: '#FFFFFF', fontSize: '12px', fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px rgba(79, 107, 255, 0.3)' }}
>
确认开通账号
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};