feat(web): upgrade settings modal with dynamic roles CRUD and permission matrix
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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: 清空工作台缓存,恢复系统默认体验状态
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user