diff --git a/web/src/components/user/SettingsModal.tsx b/web/src/components/user/SettingsModal.tsx index d239da84..fa2a97dd 100644 --- a/web/src/components/user/SettingsModal.tsx +++ b/web/src/components/user/SettingsModal.tsx @@ -1,12 +1,108 @@ -import { FC, useEffect } from 'react'; -import { X, Settings, Database, Users, Cpu, FileCheck } from 'lucide-react'; +import { FC, useState, useEffect } from 'react'; +import { + X, + Settings, + Shield, + Plus, + Trash2, + Check, + UserCheck, + Users, + Key, + Info, + CheckCircle2, +} from 'lucide-react'; +import { RoleDefinition, PermissionKey } from '../../types/rbac'; +import { UserProfile } from '../../types/docgov'; -interface SettingsModalProps { +export interface SettingsModalProps { isOpen: boolean; onClose: () => void; + roles: RoleDefinition[]; + onSaveRoles: (roles: RoleDefinition[]) => void; + users: UserProfile[]; + currentUser: UserProfile; + onUpdateUserRole: (userId: number, roleId: string) => void; + onSwitchUser: (user: UserProfile) => void; } -export const SettingsModal: FC = ({ isOpen, onClose }) => { +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 SettingsModal: FC = ({ + isOpen, + onClose, + roles, + onSaveRoles, + users, + currentUser, + onUpdateUserRole, + onSwitchUser, +}) => { + const [activeTab, setActiveTab] = useState<'roles' | 'users'>('roles'); + const [selectedRoleId, setSelectedRoleId] = useState(roles[0]?.id || 'role-admin'); + const [editRoles, setEditRoles] = useState(roles); + const [isCreatingRole, setIsCreatingRole] = useState(false); + const [newRoleName, setNewRoleName] = useState(''); + const [newRoleCode, setNewRoleCode] = useState(''); + const [newRoleDesc, setNewRoleDesc] = useState(''); + const [saveSuccessTip, setSaveSuccessTip] = useState(false); + + // 同步外部 roles + useEffect(() => { + setEditRoles(roles); + if (!roles.some((r) => r.id === selectedRoleId) && roles.length > 0) { + setSelectedRoleId(roles[0].id); + } + }, [roles]); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { @@ -19,6 +115,72 @@ export const SettingsModal: FC = ({ isOpen, onClose }) => { if (!isOpen) return null; + const currentSelectedRole = + editRoles.find((r) => r.id === selectedRoleId) || editRoles[0]; + + const handleTogglePermission = (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]; + + const updatedRoles = editRoles.map((r) => + r.id === currentSelectedRole.id + ? { ...r, permissions: updatedPermissions, updatedAt: new Date().toLocaleString() } + : r + ); + + setEditRoles(updatedRoles); + onSaveRoles(updatedRoles); + showSavedNotification(); + }; + + const handleCreateRole = () => { + if (!newRoleName.trim() || !newRoleCode.trim()) return; + + const newId = `role-custom-${Date.now()}`; + const newRole: RoleDefinition = { + id: newId, + name: newRoleName.trim(), + code: newRoleCode.trim().toUpperCase(), + description: newRoleDesc.trim() || '自定义业务角色', + isSystem: false, + permissions: ['workflow:view_all', 'doc:export'], + createdAt: new Date().toLocaleString(), + updatedAt: new Date().toLocaleString(), + }; + + const nextRoles = [...editRoles, newRole]; + setEditRoles(nextRoles); + onSaveRoles(nextRoles); + setSelectedRoleId(newId); + setIsCreatingRole(false); + setNewRoleName(''); + setNewRoleCode(''); + setNewRoleDesc(''); + showSavedNotification(); + }; + + const handleDeleteRole = (roleId: string) => { + const roleToDelete = editRoles.find((r) => r.id === roleId); + if (!roleToDelete || roleToDelete.isSystem) return; + + if (window.confirm(`确定要删除角色【${roleToDelete.name}】吗?删除后该角色用户将自动退回普通工程师。`)) { + const nextRoles = editRoles.filter((r) => r.id !== roleId); + setEditRoles(nextRoles); + onSaveRoles(nextRoles); + setSelectedRoleId(nextRoles[0]?.id || 'role-admin'); + showSavedNotification(); + } + }; + + const showSavedNotification = () => { + setSaveSuccessTip(true); + setTimeout(() => setSaveSuccessTip(false), 2000); + }; + return (
= ({ isOpen, onClose }) => { >
= ({ isOpen, onClose }) => { animation: 'scaleIn 0.18s ease-out', }} > + {/* 顶部标题栏 */}
= ({ isOpen, onClose }) => { width: '34px', height: '34px', borderRadius: '8px', - backgroundColor: 'rgba(148, 163, 184, 0.15)', + backgroundColor: 'rgba(79, 107, 255, 0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', - color: 'var(--text-strong)', + color: 'var(--brand-500)', }} >
- 系统设置与管理控制台 (规划预留) + 系统权限与角色管理控制台
- 水木东方医疗器械智能协同工作台 · 系统配置管理 + 全局动态角色 CRUD · 权限配置矩阵 · 人员角色委派与实测切换
+ +
+ {saveSuccessTip && ( + + 已实时自动保存 + + )} + +
+
+ + {/* 标签栏导航 */} +
+ +
-
-
- 本入口已为您保留,后续迭代将在此接入管理员全局管理界面与跨系统配置服务: -
+ {/* 内容区 */} +
+ {activeTab === 'roles' ? ( +
+ {/* 左侧角色列表 */} +
+
+ + 系统全局角色 ({editRoles.length}) + + +
-
-
-
- - - 组织与人员权限 - + {/* 新增角色输入面板 */} + {isCreatingRole && ( +
+ setNewRoleName(e.target.value)} + style={{ + padding: '6px 8px', + fontSize: '12px', + borderRadius: '4px', + border: '1px solid var(--border)', + background: 'var(--bg-panel)', + color: 'var(--text-strong)', + }} + /> + setNewRoleCode(e.target.value)} + style={{ + padding: '6px 8px', + fontSize: '12px', + borderRadius: '4px', + border: '1px solid var(--border)', + background: 'var(--bg-panel)', + color: 'var(--text-strong)', + }} + /> +
+ + +
+
+ )} + + {/* 角色卡片列表 */} +
+ {editRoles.map((role) => { + const isSelected = role.id === selectedRoleId; + return ( +
setSelectedRoleId(role.id)} + style={{ + padding: '10px 12px', + borderRadius: '6px', + marginBottom: '6px', + cursor: 'pointer', + backgroundColor: isSelected ? 'var(--bg-active, #1F2837)' : 'transparent', + border: isSelected ? '1px solid var(--brand-500)' : '1px solid transparent', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + transition: 'all 0.15s ease', + }} + > +
+
+ + {role.name} + + {role.isSystem && ( + + SYSTEM + + )} +
+
+ 代码: {role.code} · {role.permissions.length} 项权限 +
+
+ + {!role.isSystem && ( + + )} +
+ ); + })} +
-
- 配置全公司研发与质量人员库、固定职位及各项目阶段团队角色授权。 + + {/* 右侧权限配置矩阵 */} +
+ {currentSelectedRole && ( + <> +
+
+
+ {currentSelectedRole.name} ({currentSelectedRole.code}) +
+ {currentSelectedRole.isSystem && ( + + 🛡️ 系统内置超级管理员,默认具备所有权限且防误删 + + )} +
+
+ {currentSelectedRole.description} +
+
+ +
+
+ + 默认赋予的原子功能权限项 (勾选即刻生效): +
+ +
+ {ALL_PERMISSION_ITEMS.map((item) => { + const isChecked = + currentSelectedRole.isSystem || + currentSelectedRole.permissions.includes(item.key); + const isDisabled = currentSelectedRole.isSystem; + + return ( +
{ + if (!isDisabled) handleTogglePermission(item.key); + }} + style={{ + padding: '10px 14px', + borderRadius: '8px', + border: '1px solid var(--border)', + backgroundColor: isChecked ? 'rgba(79, 107, 255, 0.05)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + cursor: isDisabled ? 'default' : 'pointer', + transition: 'all 0.15s ease', + }} + > +
+
+ + {item.label} + + + {item.category} + +
+
+ {item.key} · {item.desc} +
+
+ +
+ {isChecked && } +
+
+ ); + })} +
+
+ + )}
- + ) : ( + /* Tab 2: 人员角色委派与当前登录人切换 */
-
- - - 法规知识库索引 +
+ + + 在此为研发中心人员委派全局角色。支持一键将当前会话登录人切换为不同角色身份,快速实测“新建项目”、“指派负责人”及“单文档可见性覆盖”的受控拦截效果。
-
- 管理 NMPA、FDA、CE-MDR 监管法规库与 pgvector 语义嵌入向量同步更新。 -
-
-
-
- - - 多智能体编排与模型 - -
-
- 配置法规专家、方案总师、CDO报价师与评审参谋的提示词与大模型网关。 -
-
+
+ {users.map((u) => { + const isCurrent = u.id === currentUser.id; + const userRoleId = u.roleId || 'role-engineer'; + const userRole = editRoles.find((r) => r.id === userRoleId); -
-
- - - 合规审计与电子签名 - -
-
- 遵循 21 CFR Part 11 与 GMP 电子签名审计记录、哈希防篡改检验。 + return ( +
+
+
+ {u.name.slice(0, 1)} +
+
+
+ + {u.name} + + + 工号: {u.employeeId} + + {isCurrent && ( + + 当前登录身份 + + )} +
+
+ {u.department} · {u.position} +
+
+
+ +
+ {/* 角色委派下拉框 */} +
+ 委派全局角色: + +
+ + {/* 切换登录人按钮 */} + {!isCurrent ? ( + + ) : ( + + ✓ 正在以此身份交互 + + )} +
+
+ ); + })}
-
+ )}
+ {/* 底部按钮栏 */}
+ + 水木东方医疗器械智能协同工作台 · 全生命周期权限治理 +
diff --git a/web/src/data/storage.ts b/web/src/data/storage.ts index ae778ae2..47658e2f 100644 --- a/web/src/data/storage.ts +++ b/web/src/data/storage.ts @@ -1,4 +1,6 @@ import { ProjectSession, ChatMessage, DeliverableItem } from '../types/workbench'; +import { RoleDefinition } from '../types/rbac'; +import { UserProfile } from '../types/docgov'; const STORAGE_PREFIX = 'smdf_agent_workbench_'; @@ -8,6 +10,8 @@ export const STORAGE_KEYS = { MESSAGES: `${STORAGE_PREFIX}messages`, DELIVERABLES: `${STORAGE_PREFIX}deliverables`, DOC_CACHE: `${STORAGE_PREFIX}doc_cache`, + ROLES: `${STORAGE_PREFIX}roles`, + USERS: `${STORAGE_PREFIX}users`, }; /** @@ -168,6 +172,22 @@ export function saveDocCache(cache: Record): void { safeSetItem(STORAGE_KEYS.DOC_CACHE, cache); } +export function loadRoles(fallback: RoleDefinition[]): RoleDefinition[] { + return safeGetItem(STORAGE_KEYS.ROLES, fallback); +} + +export function saveRoles(roles: RoleDefinition[]): void { + safeSetItem(STORAGE_KEYS.ROLES, roles); +} + +export function loadUsers(fallback: UserProfile[]): UserProfile[] { + return safeGetItem(STORAGE_KEYS.USERS, fallback); +} + +export function saveUsers(users: UserProfile[]): void { + safeSetItem(STORAGE_KEYS.USERS, users); +} + /** * resetWorkbenchStorage: 清空工作台缓存,恢复系统默认体验状态 */