feat(web): upgrade settings modal with dynamic roles CRUD and permission matrix

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-08 22:33:41 +08:00
co-authored by Cursor
parent d1202038d8
commit fa2d0e9837
2 changed files with 747 additions and 109 deletions
+727 -109
View File
@@ -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<SettingsModalProps> = ({ 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<SettingsModalProps> = ({
isOpen,
onClose,
roles,
onSaveRoles,
users,
currentUser,
onUpdateUserRole,
onSwitchUser,
}) => {
const [activeTab, setActiveTab] = useState<'roles' | 'users'>('roles');
const [selectedRoleId, setSelectedRoleId] = useState<string>(roles[0]?.id || 'role-admin');
const [editRoles, setEditRoles] = useState<RoleDefinition[]>(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<SettingsModalProps> = ({ 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 (
<div
style={{
@@ -38,8 +200,10 @@ export const SettingsModal: FC<SettingsModalProps> = ({ isOpen, onClose }) => {
>
<div
style={{
width: '560px',
width: '840px',
maxWidth: '95vw',
height: '620px',
maxHeight: '90vh',
backgroundColor: 'var(--bg-panel, #131820)',
borderRadius: '12px',
border: '1px solid var(--border)',
@@ -50,9 +214,10 @@ export const SettingsModal: FC<SettingsModalProps> = ({ isOpen, onClose }) => {
animation: 'scaleIn 0.18s ease-out',
}}
>
{/* 顶部标题栏 */}
<div
style={{
padding: '16px 20px',
padding: '14px 20px',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
@@ -66,168 +231,621 @@ export const SettingsModal: FC<SettingsModalProps> = ({ 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)',
}}
>
<Settings size={18} />
</div>
<div>
<div style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-strong)' }}>
系统设置与管理控制台 (规划预留)
系统权限与角色管理控制台
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
水木东方医疗器械智能协同工作台 · 系统配置管理
全局动态角色 CRUD · 权限配置矩阵 · 人员角色委派与实测切换
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{saveSuccessTip && (
<span
style={{
fontSize: '12px',
color: 'var(--success)',
display: 'flex',
alignItems: 'center',
gap: '4px',
animation: 'fadeIn 0.2s',
}}
>
<CheckCircle2 size={14} /> 已实时自动保存
</span>
)}
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
color: 'var(--text-muted)',
padding: '6px',
borderRadius: '6px',
}}
>
<X size={18} />
</button>
</div>
</div>
{/* 标签栏导航 */}
<div
style={{
display: 'flex',
borderBottom: '1px solid var(--border)',
backgroundColor: 'var(--bg-card, #161D27)',
padding: '0 16px',
gap: '8px',
}}
>
<button
onClick={onClose}
onClick={() => setActiveTab('roles')}
style={{
padding: '10px 16px',
fontSize: '13px',
fontWeight: 600,
background: 'none',
border: 'none',
borderBottom: activeTab === 'roles' ? '2px solid var(--brand-500)' : '2px solid transparent',
color: activeTab === 'roles' ? 'var(--brand-500)' : 'var(--text-muted)',
cursor: 'pointer',
color: 'var(--text-muted)',
padding: '6px',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<X size={18} />
<Shield size={15} />
<span>全局角色与默认权限矩阵</span>
</button>
<button
onClick={() => setActiveTab('users')}
style={{
padding: '10px 16px',
fontSize: '13px',
fontWeight: 600,
background: 'none',
border: 'none',
borderBottom: activeTab === 'users' ? '2px solid var(--brand-500)' : '2px solid transparent',
color: activeTab === 'users' ? 'var(--brand-500)' : 'var(--text-muted)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<Users size={15} />
<span>人员角色委派与当前登录人切换</span>
</button>
</div>
<div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '16px', backgroundColor: 'var(--bg-panel, #131820)' }}>
<div
style={{
fontSize: '12px',
color: 'var(--text)',
lineHeight: 1.6,
backgroundColor: 'var(--bg-subtle, #1A212C)',
padding: '12px 14px',
borderRadius: '6px',
border: '1px solid var(--border)',
}}
>
本入口已为您保留,后续迭代将在此接入管理员全局管理界面与跨系统配置服务:
</div>
{/* 内容区 */}
<div style={{ flex: 1, overflow: 'hidden', display: 'flex' }}>
{activeTab === 'roles' ? (
<div style={{ display: 'flex', width: '100%', height: '100%' }}>
{/* 左侧角色列表 */}
<div
style={{
width: '260px',
borderRight: '1px solid var(--border)',
backgroundColor: 'var(--bg-subtle, #1A212C)',
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
padding: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: '1px solid var(--border-light)',
}}
>
<span style={{ fontSize: '11.5px', fontWeight: 600, color: 'var(--text-muted)' }}>
系统全局角色 ({editRoles.length})
</span>
<button
onClick={() => setIsCreatingRole(true)}
style={{
padding: '4px 8px',
borderRadius: 'var(--r-sm)',
background: 'var(--brand-500)',
border: 'none',
color: '#fff',
fontSize: '11px',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '3px',
}}
>
<Plus size={12} />
<span>新增角色</span>
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
<div
style={{
padding: '12px',
borderRadius: '8px',
border: '1px solid var(--border)',
backgroundColor: 'var(--bg-subtle, #1A212C)',
display: 'flex',
flexDirection: 'column',
gap: '6px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--brand-500)' }}>
<Users size={16} />
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
组织与人员权限
</span>
{/* 新增角色输入面板 */}
{isCreatingRole && (
<div
style={{
padding: '10px',
backgroundColor: 'var(--bg-card)',
borderBottom: '1px solid var(--border)',
display: 'flex',
flexDirection: 'column',
gap: '6px',
}}
>
<input
type="text"
placeholder="角色名称 (如: 质量总监)"
value={newRoleName}
onChange={(e) => 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)',
}}
/>
<input
type="text"
placeholder="角色代号 (如: QA_DIR)"
value={newRoleCode}
onChange={(e) => 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)',
}}
/>
<div style={{ display: 'flex', gap: '6px', marginTop: '4px' }}>
<button
onClick={handleCreateRole}
style={{
flex: 1,
padding: '4px',
fontSize: '11.5px',
fontWeight: 600,
background: 'var(--brand-500)',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
确认创建
</button>
<button
onClick={() => setIsCreatingRole(false)}
style={{
flex: 1,
padding: '4px',
fontSize: '11.5px',
background: 'var(--bg-subtle)',
color: 'var(--text-muted)',
border: '1px solid var(--border)',
borderRadius: '4px',
cursor: 'pointer',
}}
>
取消
</button>
</div>
</div>
)}
{/* 角色卡片列表 */}
<div style={{ flex: 1, overflowY: 'auto', padding: '8px' }}>
{editRoles.map((role) => {
const isSelected = role.id === selectedRoleId;
return (
<div
key={role.id}
onClick={() => 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',
}}
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span
style={{
fontSize: '13px',
fontWeight: 600,
color: isSelected ? 'var(--brand-400, #4F6BFF)' : 'var(--text-strong)',
}}
>
{role.name}
</span>
{role.isSystem && (
<span
style={{
fontSize: '9.5px',
padding: '1px 4px',
borderRadius: '3px',
backgroundColor: 'rgba(239, 68, 68, 0.15)',
color: 'var(--danger)',
fontWeight: 600,
}}
>
SYSTEM
</span>
)}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
代码: {role.code} · {role.permissions.length} 项权限
</div>
</div>
{!role.isSystem && (
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteRole(role.id);
}}
title="删除该自定义角色"
style={{
background: 'none',
border: 'none',
color: 'var(--text-faint)',
cursor: 'pointer',
padding: '4px',
}}
onMouseEnter={(e) => (e.currentTarget.style.color = 'var(--danger)')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--text-faint)')}
>
<Trash2 size={13} />
</button>
)}
</div>
);
})}
</div>
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', lineHeight: 1.4 }}>
配置全公司研发与质量人员库、固定职位及各项目阶段团队角色授权。
{/* 右侧权限配置矩阵 */}
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
padding: '20px',
backgroundColor: 'var(--bg-panel)',
}}
>
{currentSelectedRole && (
<>
<div style={{ borderBottom: '1px solid var(--border)', paddingBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ fontSize: '16px', fontWeight: 700, color: 'var(--text-strong)' }}>
{currentSelectedRole.name} ({currentSelectedRole.code})
</div>
{currentSelectedRole.isSystem && (
<span
style={{
fontSize: '11px',
color: 'var(--danger)',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
padding: '2px 8px',
borderRadius: '4px',
}}
>
🛡️ 系统内置超级管理员,默认具备所有权限且防误删
</span>
)}
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
{currentSelectedRole.description}
</div>
</div>
<div style={{ marginTop: '16px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
<div
style={{
fontSize: '12.5px',
fontWeight: 600,
color: 'var(--text-muted)',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<Key size={14} />
<span>默认赋予的原子功能权限项 (勾选即刻生效):</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '8px' }}>
{ALL_PERMISSION_ITEMS.map((item) => {
const isChecked =
currentSelectedRole.isSystem ||
currentSelectedRole.permissions.includes(item.key);
const isDisabled = currentSelectedRole.isSystem;
return (
<div
key={item.key}
onClick={() => {
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',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
{item.label}
</span>
<span
style={{
fontSize: '10px',
padding: '1px 6px',
borderRadius: '4px',
backgroundColor: 'var(--border-light)',
color: 'var(--text-muted)',
}}
>
{item.category}
</span>
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
<code>{item.key}</code> · {item.desc}
</div>
</div>
<div
style={{
width: '20px',
height: '20px',
borderRadius: '4px',
border: isChecked ? '1px solid var(--brand-500)' : '1px solid var(--border)',
backgroundColor: isChecked ? 'var(--brand-500)' : 'transparent',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
flexShrink: 0,
}}
>
{isChecked && <Check size={14} />}
</div>
</div>
);
})}
</div>
</div>
</>
)}
</div>
</div>
) : (
/* Tab 2: 人员角色委派与当前登录人切换 */
<div
style={{
padding: '12px',
borderRadius: '8px',
border: '1px solid var(--border)',
backgroundColor: 'var(--bg-subtle, #1A212C)',
width: '100%',
height: '100%',
padding: '20px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '6px',
gap: '14px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--success)' }}>
<Database size={16} />
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
法规知识库索引
<div
style={{
fontSize: '12px',
color: 'var(--text)',
lineHeight: 1.6,
backgroundColor: 'var(--bg-subtle)',
padding: '12px 16px',
borderRadius: '6px',
border: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<Info size={16} color="var(--brand-500)" style={{ flexShrink: 0 }} />
<span>
在此为研发中心人员委派全局角色。支持一键将当前会话登录人切换为不同角色身份,快速实测“新建项目”、“指派负责人”及“单文档可见性覆盖”的受控拦截效果。
</span>
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', lineHeight: 1.4 }}>
管理 NMPA、FDA、CE-MDR 监管法规库与 pgvector 语义嵌入向量同步更新。
</div>
</div>
<div
style={{
padding: '12px',
borderRadius: '8px',
border: '1px solid var(--border)',
backgroundColor: 'var(--bg-subtle, #1A212C)',
display: 'flex',
flexDirection: 'column',
gap: '6px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--amber-500)' }}>
<Cpu size={16} />
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
多智能体编排与模型
</span>
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', lineHeight: 1.4 }}>
配置法规专家、方案总师、CDO报价师与评审参谋的提示词与大模型网关。
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{users.map((u) => {
const isCurrent = u.id === currentUser.id;
const userRoleId = u.roleId || 'role-engineer';
const userRole = editRoles.find((r) => r.id === userRoleId);
<div
style={{
padding: '12px',
borderRadius: '8px',
border: '1px solid var(--border)',
backgroundColor: 'var(--bg-subtle, #1A212C)',
display: 'flex',
flexDirection: 'column',
gap: '6px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#A855F7' }}>
<FileCheck size={16} />
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
合规审计与电子签名
</span>
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', lineHeight: 1.4 }}>
遵循 21 CFR Part 11 与 GMP 电子签名审计记录、哈希防篡改检验。
return (
<div
key={u.id}
style={{
padding: '14px 16px',
borderRadius: '8px',
border: isCurrent ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
backgroundColor: isCurrent ? 'rgba(79, 107, 255, 0.08)' : 'var(--bg-subtle)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div
style={{
width: '38px',
height: '38px',
borderRadius: '50%',
backgroundColor: isCurrent ? 'var(--brand-500)' : 'var(--border)',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '14px',
}}
>
{u.name.slice(0, 1)}
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
{u.name}
</span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
工号: {u.employeeId}
</span>
{isCurrent && (
<span
style={{
fontSize: '10px',
fontWeight: 600,
color: 'var(--brand-500)',
backgroundColor: 'rgba(79, 107, 255, 0.15)',
padding: '1px 6px',
borderRadius: 'var(--r-pill)',
}}
>
当前登录身份
</span>
)}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
{u.department} · {u.position}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* 角色委派下拉框 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontSize: '11.5px', color: 'var(--text-muted)' }}>委派全局角色:</span>
<select
value={userRoleId}
onChange={(e) => onUpdateUserRole(u.id, e.target.value)}
style={{
padding: '6px 10px',
fontSize: '12px',
borderRadius: '6px',
border: '1px solid var(--border)',
backgroundColor: 'var(--bg-panel)',
color: 'var(--text-strong)',
cursor: 'pointer',
}}
>
{editRoles.map((r) => (
<option key={r.id} value={r.id}>
{r.name} ({r.code})
</option>
))}
</select>
</div>
{/* 切换登录人按钮 */}
{!isCurrent ? (
<button
onClick={() => {
onSwitchUser(u);
showSavedNotification();
}}
style={{
padding: '6px 14px',
borderRadius: '6px',
border: '1px solid var(--brand-500)',
backgroundColor: 'var(--brand-500)',
color: '#fff',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<UserCheck size={13} />
<span>切换为此人登录</span>
</button>
) : (
<span style={{ fontSize: '12px', color: 'var(--brand-500)', fontWeight: 600, width: '120px', textAlign: 'center' }}>
✓ 正在以此身份交互
</span>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
)}
</div>
{/* 底部按钮栏 */}
<div
style={{
padding: '12px 20px',
borderTop: '1px solid var(--border)',
backgroundColor: 'var(--bg-subtle, #1A212C)',
display: 'flex',
justifyContent: 'flex-end',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
水木东方医疗器械智能协同工作台 · 全生命周期权限治理
</span>
<button
onClick={onClose}
style={{
padding: '6px 16px',
padding: '6px 18px',
borderRadius: '6px',
backgroundColor: 'var(--brand-500)',
border: 'none',
background: 'var(--brand-grad, linear-gradient(135deg, #4F6BFF 0%, #3B50D9 100%))',
color: '#fff',
fontSize: '12px',
fontWeight: 500,
fontSize: '12.5px',
fontWeight: 600,
cursor: 'pointer',
}}
>
知道了
完成并关闭
</button>
</div>
</div>
+20
View File
@@ -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<string, string>): void {
safeSetItem(STORAGE_KEYS.DOC_CACHE, cache);
}
export function loadRoles(fallback: RoleDefinition[]): RoleDefinition[] {
return safeGetItem<RoleDefinition[]>(STORAGE_KEYS.ROLES, fallback);
}
export function saveRoles(roles: RoleDefinition[]): void {
safeSetItem(STORAGE_KEYS.ROLES, roles);
}
export function loadUsers(fallback: UserProfile[]): UserProfile[] {
return safeGetItem<UserProfile[]>(STORAGE_KEYS.USERS, fallback);
}
export function saveUsers(users: UserProfile[]): void {
safeSetItem(STORAGE_KEYS.USERS, users);
}
/**
* resetWorkbenchStorage: 清空工作台缓存,恢复系统默认体验状态
*/