fix(web): remove dead notification/workflow/settings components and orphaned APIs (X-5)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -99,10 +99,3 @@ export function decideAgentCandidate(
|
||||
{ method: 'POST', body: JSON.stringify(body) },
|
||||
);
|
||||
}
|
||||
|
||||
export function patchNotification(id: number, status: 'read' | 'acted' | 'dismissed') {
|
||||
return api<{ ok: boolean }>(`/api/v1/notifications/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
WorkflowDefinition,
|
||||
DocumentLineage,
|
||||
CreateProjectInput,
|
||||
ActivateWorkflowInput,
|
||||
ActivateWorkflowResult,
|
||||
} from '../types/projectWorkflow';
|
||||
import { getBearerAuthHeaders } from './authHeaders';
|
||||
|
||||
@@ -68,29 +66,6 @@ export async function fetchProjectWorkflows(projectId: number): Promise<ProjectW
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function activateWorkflow(
|
||||
projectId: number,
|
||||
input: ActivateWorkflowInput
|
||||
): Promise<ActivateWorkflowResult> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/projects/${projectId}/active-workflow`, {
|
||||
method: 'PUT',
|
||||
headers: getBearerAuthHeaders('application/json'),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
let msg = text;
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed.message) msg = parsed.message;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw new Error(msg || `切换激活工作流失败: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchWorkflowCatalog(): Promise<WorkflowDefinition[]> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/workflows`, {
|
||||
headers: getBearerAuthHeaders(),
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { patchNotification } from '../../api/documentRevision';
|
||||
|
||||
export type DocNotificationItem = {
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
body: string;
|
||||
status: string;
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
export function DocumentNotificationList(props: {
|
||||
items: DocNotificationItem[];
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ul aria-label="文档变更通知" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||
{props.items.map((n) => (
|
||||
<li key={n.id} style={{ borderBottom: '1px solid #eee', padding: '8px 0' }}>
|
||||
<div><strong>{n.title}</strong></div>
|
||||
<div style={{ fontSize: 13 }}>{n.body}</div>
|
||||
<div style={{ display: 'flex', gap: 6, marginTop: 4 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await patchNotification(n.id, 'read');
|
||||
props.onChanged?.();
|
||||
}}
|
||||
>
|
||||
已读
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await patchNotification(n.id, 'acted');
|
||||
props.onChanged?.();
|
||||
}}
|
||||
>
|
||||
已处理
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await patchNotification(n.id, 'dismissed');
|
||||
props.onChanged?.();
|
||||
}}
|
||||
>
|
||||
忽略
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -1,826 +0,0 @@
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import {
|
||||
X,
|
||||
Settings,
|
||||
Shield,
|
||||
Plus,
|
||||
Trash2,
|
||||
Check,
|
||||
Users,
|
||||
Key,
|
||||
Info,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import { RoleDefinition, PermissionKey } from '../../types/rbac';
|
||||
import { UserProfile } from '../../types/docgov';
|
||||
|
||||
export interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
roles: RoleDefinition[];
|
||||
onSaveRoles: (roles: RoleDefinition[]) => void;
|
||||
users: UserProfile[];
|
||||
currentUser: UserProfile;
|
||||
onUpdateUserRole: (userId: number, roleId: string) => void;
|
||||
}
|
||||
|
||||
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,
|
||||
}) => {
|
||||
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) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [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={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(11, 14, 20, 0.75)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '840px',
|
||||
maxWidth: '95vw',
|
||||
height: '620px',
|
||||
maxHeight: '90vh',
|
||||
backgroundColor: 'var(--bg-panel, #131820)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid var(--border)',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
animation: 'scaleIn 0.18s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* 顶部标题栏 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '14px 20px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '34px',
|
||||
height: '34px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'rgba(79, 107, 255, 0.15)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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={() => 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',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
<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={{ 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>
|
||||
|
||||
{/* 新增角色输入面板 */}
|
||||
{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={{
|
||||
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={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '20px',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '14px',
|
||||
}}
|
||||
>
|
||||
<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={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{users.map((u) => {
|
||||
const isCurrent = u.id === currentUser.id;
|
||||
const userRoleId = u.roleId || 'role-engineer';
|
||||
|
||||
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 ? (
|
||||
<span style={{ fontSize: '12px', color: 'var(--brand-500)', fontWeight: 600, width: '120px', textAlign: 'center' }}>
|
||||
✓ 当前登录身份
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部按钮栏 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
||||
水木东方医疗器械智能协同工作台 · 全生命周期权限治理
|
||||
</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '6px 18px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'var(--brand-grad, linear-gradient(135deg, #4F6BFF 0%, #3B50D9 100%))',
|
||||
color: '#fff',
|
||||
fontSize: '12.5px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
完成并关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, ArrowRight } from 'lucide-react';
|
||||
|
||||
interface WorkflowPrerequisiteBannerProps {
|
||||
workflowKey: string;
|
||||
hasApprovedSystemReqs: boolean;
|
||||
onSwitchToProductManager: () => void;
|
||||
}
|
||||
|
||||
export const WorkflowPrerequisiteBanner: React.FC<WorkflowPrerequisiteBannerProps> = ({
|
||||
workflowKey,
|
||||
hasApprovedSystemReqs,
|
||||
onSwitchToProductManager,
|
||||
}) => {
|
||||
if (workflowKey !== 'test_manager' || hasApprovedSystemReqs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: '8px 16px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(245, 158, 11, 0.1)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.3)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: '#b45309' }}>
|
||||
<AlertTriangle size={16} style={{ color: '#d97706', flexShrink: 0 }} />
|
||||
<span>
|
||||
<strong>前置依赖未就绪:</strong>
|
||||
智能测试经理工作流强依赖同项目已批准的《系统需求规格书 (RD-SR)》。请先由产品经理完成编制并经三级审批批准。
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onSwitchToProductManager}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '6px',
|
||||
background: '#d97706',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span>前往产品经理编制需求</span>
|
||||
<ArrowRight size={12} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -137,11 +137,6 @@ export function loadMessages(): ChatMessage[] {
|
||||
return enrichMessagesWithCategories(result);
|
||||
}
|
||||
|
||||
/** @deprecated 全量 MESSAGES 持久化已废弃,请使用 saveSessionWindowCache */
|
||||
export function saveMessages(_messages: ChatMessage[]): void {
|
||||
// no-op: 权威历史来自服务端窗口接口
|
||||
}
|
||||
|
||||
// ==================================================================================
|
||||
// P0: 会话窗口 LRU 缓存与输入草稿持久化 (替代原全量 MESSAGES 持久化)
|
||||
// ==================================================================================
|
||||
|
||||
@@ -75,17 +75,3 @@ export interface CreateProjectInput {
|
||||
/** @deprecated Accepted by API for catalog checks only; create dialog must not send this. */
|
||||
initial_workflow_key?: string;
|
||||
}
|
||||
|
||||
export interface ActivateWorkflowInput {
|
||||
workflow_key: string;
|
||||
expected_version: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ActivateWorkflowResult {
|
||||
project_id: number;
|
||||
active_workflow_key: string;
|
||||
version: number;
|
||||
changed_at: string;
|
||||
changed_by: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user