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:
weichen
2026-09-18 04:02:18 +00:00
co-authored by Cursor
parent da62c49952
commit 2e14b3563c
7 changed files with 0 additions and 995 deletions
-7
View File
@@ -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 }),
});
}
-25
View File
@@ -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>
);
}
-826
View File
@@ -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>
);
};
-5
View File
@@ -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 持久化)
// ==================================================================================
-14
View File
@@ -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;
}