feat(ui): 实现三分类Tab分段切换、全景大盘分区与一键共享/取消共享闭环
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+580
-235
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { AppShell } from './components/layout/AppShell';
|
||||
import { MessageStream } from './components/chat/MessageStream';
|
||||
import { InputArea } from './components/chat/InputArea';
|
||||
@@ -9,43 +9,62 @@ import { BlockEditorModal } from './components/doc/BlockEditorModal';
|
||||
import { CommentSidePanel } from './components/doc/CommentSidePanel';
|
||||
import { DocGovActionBar } from './components/doc/DocGovActionBar';
|
||||
import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard';
|
||||
import { UserProjectRightsModal } from './components/user/UserProjectRightsModal';
|
||||
import { SettingsModal } from './components/user/SettingsModal';
|
||||
import { markdownToBlockAST } from './engine/markdownToBlock';
|
||||
import { blockASTToMarkdown } from './engine/blockToMarkdown';
|
||||
import { recomputeNumbering } from './engine/autoNumber';
|
||||
import { DocumentAST, DocumentBlock, BlockComment } from './types/block';
|
||||
import { ProjectSession, ChatMessage, DeliverableItem } from './types/workbench';
|
||||
import { UserRole } from './types/docgov';
|
||||
import { ProjectSession, ChatMessage, DeliverableItem, DeliverableCategory } from './types/workbench';
|
||||
import { UserRole, UserProfile, DocPermissionRole } from './types/docgov';
|
||||
import { MOCK_DOCUMENTS } from './data/mockDocuments';
|
||||
|
||||
const INITIAL_MD = `# 4K超高清荧光内窥镜系统立项与可行性报告
|
||||
|
||||
本产品主要用于微创腹腔手术中的组织成像与荧光导航,通过将近红外荧光技术与 4K 超高清摄像系统结合,实时显影胆道与血管解剖结构。
|
||||
|
||||
> **[合规依据]** 本项目需严格遵守 GB 9706.1-2020 医用电气安全通用要求与 YY 0569-2011 临床医用内窥镜专用要求。
|
||||
|
||||
## 关键技术指标与法规符合性
|
||||
|
||||
| 参数项 | 指标要求 | 检验依据 | 合规判定 |
|
||||
| 光学中心分辨率 | 4K (3840x2160) @ 60fps | YY 0068.1-2008 | 符合 |
|
||||
| 荧光激发波长 | 785nm / 805nm 近红外双波长 | 行业标准预研 | 待检 |
|
||||
| 电气绝缘阻抗 | ≥ 100 MΩ (强化绝缘) | GB 9706.1-2020 | 符合 |
|
||||
|
||||
### 电气安全与电磁兼容风险评价
|
||||
|
||||
根据 IEC 60601-1-2 标准,系统需在高频电刀同时工作时保持抗扰度,抗静电放电需达到接触 8kV / 空气 15kV。
|
||||
|
||||
\`\`\`mermaid
|
||||
graph TD
|
||||
A[4K荧光内窥镜头端] --> B[双CMOS光学分光模组]
|
||||
B --> C[FPGA实时图像融合算法]
|
||||
C --> D[4K医用级超低延迟监视器]
|
||||
\`\`\`
|
||||
|
||||
$$
|
||||
SNR_{fluorescence} = \\frac{S_{emission}}{\\sqrt{\\sigma_{ambient}^2 + \\sigma_{dark}^2}}
|
||||
$$
|
||||
|
||||
---
|
||||
`;
|
||||
export const MOCK_USERS: UserProfile[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '张工',
|
||||
employeeId: 'SMDF-0821',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
position: '资深医疗器械方案工程师',
|
||||
projectPermissions: {
|
||||
'doc-001': 'author', // 编制人
|
||||
'doc-002': 'reviewer', // 审核人
|
||||
'doc-003': 'approver', // 批准人
|
||||
'doc-004': 'viewer', // 查阅者
|
||||
'doc-shared-001': 'author',
|
||||
'doc-private-001': 'author',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李主管',
|
||||
employeeId: 'SMDF-0205',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
position: '研发项目技术主管',
|
||||
projectPermissions: {
|
||||
'doc-001': 'reviewer',
|
||||
'doc-002': 'author',
|
||||
'doc-003': 'reviewer',
|
||||
'doc-004': 'viewer',
|
||||
'doc-shared-001': 'viewer',
|
||||
'doc-private-002': 'author',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '王总监',
|
||||
employeeId: 'SMDF-0018',
|
||||
department: '医疗器械研发事业部',
|
||||
position: '微创器械研发总监',
|
||||
projectPermissions: {
|
||||
'doc-001': 'approver',
|
||||
'doc-002': 'approver',
|
||||
'doc-003': 'author',
|
||||
'doc-004': 'approver',
|
||||
'doc-shared-001': 'approver',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
// 会话与用户状态
|
||||
@@ -73,16 +92,20 @@ export default function App() {
|
||||
]);
|
||||
|
||||
const [activeSessionId, setActiveSessionId] = useState('sess-001');
|
||||
const [currentRole, setCurrentRole] = useState<UserRole>('author');
|
||||
const [currentUserName, setCurrentUserName] = useState('张工');
|
||||
|
||||
// 交付物状态大盘
|
||||
// 当前用户身份 (职位在全公司唯一不变,但在不同文档有不同法定职责)
|
||||
const [currentUser, setCurrentUser] = useState<UserProfile>(MOCK_USERS[0]);
|
||||
|
||||
// 全生命周期完整交付物大盘清单 (含工作流受控、项目协作共享、个人私密生成三大分类)
|
||||
const [deliverables, setDeliverables] = useState<DeliverableItem[]>([
|
||||
{
|
||||
docId: 'doc-001',
|
||||
taskKey: 'feasibility_reg',
|
||||
title: '注册路径可行性评估报告',
|
||||
title: '4K荧光内窥镜立项与可行性报告',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
@@ -95,8 +118,11 @@ export default function App() {
|
||||
{
|
||||
docId: 'doc-002',
|
||||
taskKey: 'research',
|
||||
title: '微创腹腔镜全球竞品深度调研',
|
||||
title: '微创外科全球竞品深度调研',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
lifecycleState: 'reviewing',
|
||||
isClaimed: true,
|
||||
@@ -109,10 +135,13 @@ export default function App() {
|
||||
{
|
||||
docId: 'doc-003',
|
||||
taskKey: 'prelim_design',
|
||||
title: '光学系统初步设计规格书',
|
||||
title: '光学分光与电路初步设计规格书',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
lifecycleState: 'approved',
|
||||
lifecycleState: 'approving',
|
||||
isClaimed: true,
|
||||
author: '张工',
|
||||
reviewer: '李主管',
|
||||
@@ -120,25 +149,247 @@ export default function App() {
|
||||
updatedAt: '2026-09-07 17:00',
|
||||
summary: '4K荧光双路分光棱镜光路设计与近红外消色差物镜参数设计。',
|
||||
},
|
||||
]);
|
||||
|
||||
const [activeDocId, setActiveDocId] = useState('doc-001');
|
||||
const [deckDrawerOpen, setDeckDrawerOpen] = useState(false);
|
||||
|
||||
// 文档 AST
|
||||
const [docAST, setDocAST] = useState<DocumentAST>(() =>
|
||||
markdownToBlockAST(INITIAL_MD, {
|
||||
docId: 'doc-001',
|
||||
title: '4K超高清荧光内窥镜系统立项与可行性报告',
|
||||
taskKey: 'feasibility_reg',
|
||||
version: 'V1.0',
|
||||
{
|
||||
docId: 'doc-004',
|
||||
taskKey: 'cdo_quote',
|
||||
title: 'CDO商业报价单与工时清单',
|
||||
stage: 'V1.0',
|
||||
category: 'workflow',
|
||||
creatorId: 4,
|
||||
creatorName: '王商务',
|
||||
isFixedWorkflow: true,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: '王商务',
|
||||
reviewer: '张工',
|
||||
approver: '李总',
|
||||
updatedAt: '2026-09-08 15:30',
|
||||
summary: '基于WBS千工时分解与CDO成本模型的真实商业报价核算。',
|
||||
},
|
||||
{
|
||||
docId: 'doc-shared-001',
|
||||
taskKey: 'supply_chain_analysis',
|
||||
title: '4K双路CMOS供应链与成本灵敏度分析',
|
||||
stage: 'V0.5',
|
||||
category: 'shared',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
sharedAt: '2026-09-08 15:20',
|
||||
isFixedWorkflow: false,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: '张工',
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
})
|
||||
);
|
||||
updatedAt: '2026-09-08 15:20',
|
||||
summary: '张工在与助手对话中推演的 Sony IMX485 与国产长光辰芯双路 CMOS 传感器成本灵敏度测算。',
|
||||
},
|
||||
{
|
||||
docId: 'doc-private-001',
|
||||
taskKey: 'negotiation_memo',
|
||||
title: '微创医疗战略商务谈判底线与预付款让利推演备忘',
|
||||
stage: 'V1.0',
|
||||
category: 'private',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: false,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: '张工',
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
updatedAt: '2026-09-08 16:10',
|
||||
summary: '张工个人与助手对话进行的商务谈判底线、毛利率红线测算草案,含个人思考,仅本人可见。',
|
||||
},
|
||||
{
|
||||
docId: 'doc-private-002',
|
||||
taskKey: 'lead_capacity_memo',
|
||||
title: '团队绩效核算与阶段资源饱和度评估',
|
||||
stage: 'V0.5',
|
||||
category: 'private',
|
||||
creatorId: 2,
|
||||
creatorName: '李主管',
|
||||
isFixedWorkflow: false,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: '李主管',
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
updatedAt: '2026-09-08 15:50',
|
||||
summary: '李主管个人对话生成的团队工时饱和度与绩效预算底稿,仅李主管可见。',
|
||||
},
|
||||
]);
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<DeliverableCategory>('workflow');
|
||||
const [activeDocId, setActiveDocId] = useState('doc-001');
|
||||
const [deckDrawerOpen, setDeckDrawerOpen] = useState(false);
|
||||
|
||||
// 模态浮层控制
|
||||
const [rightsModalOpen, setRightsModalOpen] = useState(false);
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
// 基于当前登录用户过滤可见交付物 (私密文档严格按 creatorId 隔离,流程与共享文档全员可见)
|
||||
const visibleDeliverables = useMemo(() => {
|
||||
return deliverables.filter((doc) => {
|
||||
if (doc.category === 'workflow' || doc.category === 'shared') {
|
||||
return true;
|
||||
}
|
||||
return doc.creatorId === currentUser.id;
|
||||
});
|
||||
}, [deliverables, currentUser.id]);
|
||||
|
||||
// 当用户切换或可见列表变动时,自动校准激活文档
|
||||
useEffect(() => {
|
||||
const isCurrentVisible = visibleDeliverables.some((d) => d.docId === activeDocId);
|
||||
if (!isCurrentVisible) {
|
||||
const fallback =
|
||||
visibleDeliverables.find((d) => d.category === activeCategory) ||
|
||||
visibleDeliverables[0];
|
||||
if (fallback) {
|
||||
setActiveDocId(fallback.docId);
|
||||
setActiveCategory(fallback.category);
|
||||
}
|
||||
}
|
||||
}, [currentUser.id, visibleDeliverables, activeDocId, activeCategory]);
|
||||
|
||||
// 文档缓存与当前 AST
|
||||
const [docCache, setDocCache] = useState<Record<string, DocumentAST>>(() => {
|
||||
const initialCache: Record<string, DocumentAST> = {};
|
||||
Object.entries(MOCK_DOCUMENTS).forEach(([id, meta]) => {
|
||||
initialCache[id] = markdownToBlockAST(meta.markdown, {
|
||||
docId: meta.docId,
|
||||
title: meta.title,
|
||||
taskKey: meta.taskKey,
|
||||
version: meta.version,
|
||||
category: meta.category,
|
||||
creatorId: meta.creatorId,
|
||||
creatorName: meta.creatorName,
|
||||
sharedAt: meta.sharedAt,
|
||||
lifecycleState: meta.lifecycleState,
|
||||
isClaimed: meta.isClaimed,
|
||||
author: meta.author,
|
||||
reviewer: meta.reviewer,
|
||||
approver: meta.approver,
|
||||
});
|
||||
});
|
||||
return initialCache;
|
||||
});
|
||||
|
||||
const docAST = docCache[activeDocId] || docCache['doc-001'];
|
||||
|
||||
// 根据当前激活文档与用户权限矩阵,计算当前生效角色
|
||||
const currentDocRole: UserRole = (() => {
|
||||
const perm = currentUser.projectPermissions?.[activeDocId];
|
||||
if (perm === 'author' || perm === 'reviewer' || perm === 'approver') {
|
||||
return perm;
|
||||
}
|
||||
// 降级回退
|
||||
if (docAST.author === currentUser.name) return 'author';
|
||||
if (docAST.reviewer === currentUser.name) return 'reviewer';
|
||||
if (docAST.approver === currentUser.name) return 'approver';
|
||||
return 'author'; // 默认
|
||||
})();
|
||||
|
||||
const setDocAST = (newAst: DocumentAST) => {
|
||||
setDocCache((prev) => ({
|
||||
...prev,
|
||||
[activeDocId]: newAst,
|
||||
}));
|
||||
};
|
||||
|
||||
// 切换文档
|
||||
const handleSelectDoc = (id: string) => {
|
||||
setActiveDocId(id);
|
||||
const targetDoc = deliverables.find((d) => d.docId === id);
|
||||
if (targetDoc) {
|
||||
setActiveCategory(targetDoc.category);
|
||||
}
|
||||
if (!docCache[id] && MOCK_DOCUMENTS[id]) {
|
||||
const meta = MOCK_DOCUMENTS[id];
|
||||
const parsed = markdownToBlockAST(meta.markdown, {
|
||||
docId: meta.docId,
|
||||
title: meta.title,
|
||||
taskKey: meta.taskKey,
|
||||
version: meta.version,
|
||||
category: meta.category,
|
||||
creatorId: meta.creatorId,
|
||||
creatorName: meta.creatorName,
|
||||
sharedAt: meta.sharedAt,
|
||||
lifecycleState: meta.lifecycleState,
|
||||
isClaimed: meta.isClaimed,
|
||||
author: meta.author,
|
||||
reviewer: meta.reviewer,
|
||||
approver: meta.approver,
|
||||
});
|
||||
setDocCache((prev) => ({ ...prev, [id]: parsed }));
|
||||
}
|
||||
};
|
||||
|
||||
// 一键将个人私密文档共享至项目协作参考库
|
||||
const handleShareToProject = (docId: string) => {
|
||||
const nowStr = new Date().toISOString().replace('T', ' ').substring(0, 16);
|
||||
setDeliverables((prev) =>
|
||||
prev.map((d) => (d.docId === docId ? { ...d, category: 'shared', sharedAt: nowStr } : d))
|
||||
);
|
||||
setDocCache((prev) => {
|
||||
if (!prev[docId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[docId]: { ...prev[docId], category: 'shared', sharedAt: nowStr },
|
||||
};
|
||||
});
|
||||
setActiveCategory('shared');
|
||||
const targetDoc = deliverables.find((d) => d.docId === docId);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'ast-share-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'assistant',
|
||||
authorName: '水木东方协同参谋',
|
||||
intentTag: '🌐 一键共享至项目参考库',
|
||||
content: `已为您将私密推演文档《${targetDoc?.title || '文档'}》一键共享至【项目协作参考库】!项目全员均可作为方案参考进行查阅与批注。您作为创建者,可随时点击「取消共享」恢复为个人私密状态。`,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// 取消共享 (恢复为个人私有草稿)
|
||||
const handleRevokeShare = (docId: string) => {
|
||||
setDeliverables((prev) =>
|
||||
prev.map((d) => (d.docId === docId ? { ...d, category: 'private', sharedAt: undefined } : d))
|
||||
);
|
||||
setDocCache((prev) => {
|
||||
if (!prev[docId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[docId]: { ...prev[docId], category: 'private', sharedAt: undefined },
|
||||
};
|
||||
});
|
||||
setActiveCategory('private');
|
||||
const targetDoc = deliverables.find((d) => d.docId === docId);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'ast-revoke-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'assistant',
|
||||
authorName: '水木东方协同参谋',
|
||||
intentTag: '🔒 撤回共享至个人私密',
|
||||
content: `已撤回共享!《${targetDoc?.title || '文档'}》已从项目协作库移除并恢复为您的【个人私密文档】,其他团队成员在界面与大盘中已不可见。`,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// 删除私密文档
|
||||
const handleDeleteDoc = (docId: string) => {
|
||||
setDeliverables((prev) => prev.filter((d) => d.docId !== docId));
|
||||
if (activeDocId === docId) {
|
||||
setActiveDocId('doc-001');
|
||||
setActiveCategory('workflow');
|
||||
}
|
||||
};
|
||||
|
||||
// 模态弹窗与侧栏
|
||||
const [editingBlock, setEditingBlock] = useState<DocumentBlock | null>(null);
|
||||
@@ -196,86 +447,103 @@ export default function App() {
|
||||
id: 'msg-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'user',
|
||||
authorName: currentUserName,
|
||||
authorName: currentUser.name,
|
||||
content: text,
|
||||
attachments,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
attachments,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
setIsThinking(true);
|
||||
|
||||
// 智能体推演回复逻辑
|
||||
setTimeout(() => {
|
||||
setIsThinking(false);
|
||||
|
||||
// 对话驱动 1:审核人对话提出批注
|
||||
if (currentRole === 'reviewer' && (text.includes('批注') || text.includes('意见') || text.includes('补充'))) {
|
||||
const targetBlock = docAST.rootBlock.children?.[3] || docAST.rootBlock.children?.[1];
|
||||
if (targetBlock) {
|
||||
const newComment: BlockComment = {
|
||||
id: 'cmt-' + Date.now(),
|
||||
authorName: currentUserName,
|
||||
authorRole: 'reviewer',
|
||||
createdAt: new Date().toTimeString().substring(0, 5),
|
||||
type: 'issue',
|
||||
content: text,
|
||||
resolved: false,
|
||||
};
|
||||
targetBlock.comments = [...(targetBlock.comments || []), newComment];
|
||||
setDocAST({ ...docAST });
|
||||
}
|
||||
// 1. 用户要求助手生成新文档/测算/推演/备忘 (默认自动归档至【我的私密生成】,保护隐私)
|
||||
if (
|
||||
text.includes('生成文档') ||
|
||||
text.includes('起草文档') ||
|
||||
text.includes('测算') ||
|
||||
text.includes('推演') ||
|
||||
text.includes('备忘') ||
|
||||
text.includes('生成报告')
|
||||
) {
|
||||
const newDocId = `doc-private-${Date.now()}`;
|
||||
const docTitle = text.includes('备忘')
|
||||
? `${currentUser.name}专属:临床与商业推演备忘录`
|
||||
: text.includes('测算')
|
||||
? `${currentUser.name}专属:BOM器件成本与毛利灵敏度测算`
|
||||
: `${currentUser.name}专属:AI协同专项推演草案`;
|
||||
|
||||
const newMeta: DeliverableItem = {
|
||||
docId: newDocId,
|
||||
taskKey: 'custom_draft',
|
||||
title: docTitle,
|
||||
stage: 'V0.5',
|
||||
category: 'private',
|
||||
creatorId: currentUser.id,
|
||||
creatorName: currentUser.name,
|
||||
isFixedWorkflow: false,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: currentUser.name,
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
updatedAt: new Date().toISOString().replace('T', ' ').substring(0, 16),
|
||||
summary: `根据对话中您的需求“${text.slice(0, 60)}...”即时生成的个人私密草稿。`,
|
||||
};
|
||||
|
||||
const sampleMd = `# ${docTitle}\n\n## 1. 产生背景与对话诉求\n根据用户【${currentUser.name}】于 ${new Date().toLocaleTimeString()} 提出的协同要求:“${text}”,参谋小队即时推演生成本专属文档。\n\n> 🔒 [隐私受控标记] 本文档为用户个人私密生成物,仅 ${currentUser.name} 本人可见,不进入项目固定受控审签体系。若推演成熟,可随时点击下方「一键共享给项目组」转为协作参考资料。\n\n## 2. 核心分析与推演结论\n依据系统知识库与工程模型,对关键指标与风险进行分解:\n- 技术可行性评级:A 级(具备清晰工程落地路径)\n- 建议跟进措施:优化元器件批量采购配额,规避长周期交货风险。\n`;
|
||||
|
||||
const parsedAst = markdownToBlockAST(sampleMd, {
|
||||
docId: newDocId,
|
||||
title: docTitle,
|
||||
taskKey: 'custom_draft',
|
||||
version: 'V0.1',
|
||||
category: 'private',
|
||||
creatorId: currentUser.id,
|
||||
creatorName: currentUser.name,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: currentUser.name,
|
||||
});
|
||||
|
||||
setDeliverables((prev) => [newMeta, ...prev]);
|
||||
setDocCache((prev) => ({ ...prev, [newDocId]: parsedAst }));
|
||||
setActiveCategory('private');
|
||||
setActiveDocId(newDocId);
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'reply-' + Date.now(),
|
||||
id: 'ast-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'assistant',
|
||||
intentTag: '💡 评审批注挂载',
|
||||
content: `李主管,已为您将审核意见记录并挂载在右侧受控文档【${targetBlock?.numbering || ''} ${targetBlock?.content.substring(0, 16)}...】章节旁,编制人随时可见。`,
|
||||
authorName: '水木东方协同参谋',
|
||||
intentTag: '🔒 个人私密文档已生成',
|
||||
content: `收到您的要求,已为您即时生成专属推演文档《${docTitle}》!\n\n根据系统隐私保护机制,该文档已自动存入【我的私密】库,**仅您本人可见**。右侧工作台已为您自动切至该文档,您可以自由查看与编辑。推演成熟后,可随时点击底部「一键共享给项目组」作为团队协作参考资料。`,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 对话驱动 2:审核人对话批准推进
|
||||
if (currentRole === 'reviewer' && (text.includes('通过') || text.includes('同意'))) {
|
||||
handlePassReview();
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'reply-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'assistant',
|
||||
intentTag: '🛡️ 审核通过流转',
|
||||
content: '李主管,已为您执行【审核通过】指令,文档已锁定并正式流转至王总监(批准人)进行终审。',
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 对话驱动 3:编制人认领保护拦截 (Change Proposal)
|
||||
if (text.includes('修改') || text.includes('更新') || text.includes('改写') || text.includes('补充')) {
|
||||
if (docAST.isClaimed) {
|
||||
if (text.includes('修改') || text.includes('更新') || text.includes('调整')) {
|
||||
if (docAST.isClaimed && currentDocRole === 'author') {
|
||||
setPendingProposal({
|
||||
changes: `根据您的指令【${text}】,拟将第二章关键技术指标补充 4K 荧光抗扰度测试参数,并在表格中增加检验依据条款。`,
|
||||
impact: '将直接影响后续型检大纲覆盖率,增加 IEC 60601-1-2 第 8 节要求。',
|
||||
risk: '低风险合规强化项,无需重跑临床验证。',
|
||||
patchBlockId: docAST.rootBlock.children?.[4]?.id,
|
||||
changes: '拟在第2节「监管分类与强制性合规标准矩阵」中,强化补充高频电刀共模抑制比设计指标。',
|
||||
impact: '将直接影响电路原理图物料选型与型式检验 EMC 滤波网络参数。',
|
||||
risk: '低风险。符合 GB 9706.1-2020 强标,提升系统抗扰度,无侵权风险。',
|
||||
});
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'reply-' + Date.now(),
|
||||
id: 'ast-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'assistant',
|
||||
intentTag: '🛡️ 认领变更拦截',
|
||||
content:
|
||||
'检测到本文档已被张工认领受控。依据认领保护机制,已为您生成《变更提案》。请在右侧下方或对话卡片中确认是否采纳本次变更。',
|
||||
authorName: '水木东方协同参谋',
|
||||
intentTag: '🛡️ 文档认领保护机制拦截',
|
||||
content: `检测到《${docAST.title}》已处于【编制人已认领】状态。根据医疗器械质量体系规范,智能体小队不能擅自修改,已为您生成【变更方案提议】。请您确认是否准许本次修改。`,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
@@ -283,49 +551,76 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
// 常规参谋解答
|
||||
if (text.includes('通过') || text.includes('同意')) {
|
||||
if (docAST.lifecycleState === 'reviewing' && currentDocRole === 'reviewer') {
|
||||
handlePassReview();
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'ast-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'assistant',
|
||||
authorName: '水木东方协同参谋',
|
||||
intentTag: '📋 审核通过流转',
|
||||
content: `已确认审核通过!《${docAST.title}》状态已流转至【待批准 (approving)】,已呈报王总监终审。`,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (docAST.lifecycleState === 'approving' && currentDocRole === 'approver') {
|
||||
handleApprove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'reply-' + Date.now(),
|
||||
id: 'ast-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'assistant',
|
||||
intentTag: '💡 参谋解答',
|
||||
content: `收到您的指令:“${text}”。参谋小队已依据医疗器械法规标准进行综合排查,相关指标已实时对齐。您可随时在右侧文档栏双击块进行微调。`,
|
||||
authorName: '水木东方协同参谋',
|
||||
intentTag: '💡 协同参谋回复',
|
||||
content: `收到您的要求:“${text}”。参谋小队已分析当前处于【${docAST.lifecycleState}】生命周期,右侧已实时同步最新受控文档结构。`,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
}, 1200);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 采纳变更提案
|
||||
// 编制人接受变更提议
|
||||
const handleAcceptProposal = () => {
|
||||
if (!pendingProposal) return;
|
||||
const children = docAST.rootBlock.children || [];
|
||||
const tableBlock = children.find((b) => b.type === 'table');
|
||||
if (tableBlock && tableBlock.properties?.tableData) {
|
||||
tableBlock.properties.tableData.push(['电磁兼容抗扰度', '80MHz~6GHz 3V/m 强化测试', 'IEC 60601-1-2', '符合']);
|
||||
}
|
||||
recomputeNumbering(docAST.rootBlock);
|
||||
setDocAST({ ...docAST });
|
||||
setPendingProposal(null);
|
||||
const children = docAST.rootBlock.children || [];
|
||||
const pBlock = children.find((c) => c.type === 'paragraph');
|
||||
if (pBlock) {
|
||||
pBlock.content += '\n\n【工程师确认增补】前端光路采集板已加装定制高频共模扼流圈,强标抗扰度提升至最高等级。';
|
||||
pBlock.metadata = {
|
||||
lastModifiedBy: '智能体协同小队 (经张工确认签署)',
|
||||
lastModifiedAt: new Date().toISOString().replace('T', ' ').substring(0, 16),
|
||||
};
|
||||
setDocAST({ ...docAST });
|
||||
}
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'prop-acc-' + Date.now(),
|
||||
id: 'ast-p-' + Date.now(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'system',
|
||||
content: '✅ 编制负责人已确认并采纳智能体变更提案,右侧受控文档已实时更新完成。',
|
||||
role: 'assistant',
|
||||
authorName: '水木东方协同参谋',
|
||||
intentTag: '✅ 协同变更已生效',
|
||||
content: `变更提议已成功执行并落盘至《${docAST.title}》,受控文档已同步更新。`,
|
||||
timestamp: new Date().toTimeString().substring(0, 5),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// 块操作:保存编辑
|
||||
// 块操作
|
||||
const handleSaveBlockEdit = (updated: DocumentBlock) => {
|
||||
const children = docAST.rootBlock.children || [];
|
||||
const idx = children.findIndex((b) => b.id === updated.id);
|
||||
const idx = children.findIndex((c) => c.id === updated.id);
|
||||
if (idx !== -1) {
|
||||
children[idx] = updated;
|
||||
recomputeNumbering(docAST.rootBlock);
|
||||
@@ -334,16 +629,19 @@ export default function App() {
|
||||
setEditingBlock(null);
|
||||
};
|
||||
|
||||
// 块操作:增、删、移
|
||||
const handleInsertBlock = (index: number, position: 'above' | 'below') => {
|
||||
const children = docAST.rootBlock.children || [];
|
||||
const newBlock: DocumentBlock = {
|
||||
id: 'blk_' + Math.random().toString(36).substring(2, 9),
|
||||
type: 'paragraph',
|
||||
content: '新插入的受控文档段落文本,双击可就地编辑...',
|
||||
content: '【新插入技术要求】:在此填写具体受控技术规范或临床评定指标...',
|
||||
metadata: {
|
||||
lastModifiedBy: `${currentUser.name} (${currentUser.position})`,
|
||||
lastModifiedAt: new Date().toISOString().replace('T', ' ').substring(0, 16),
|
||||
},
|
||||
};
|
||||
const targetIdx = position === 'above' ? index : index + 1;
|
||||
children.splice(targetIdx, 0, newBlock);
|
||||
const children = docAST.rootBlock.children || [];
|
||||
const insertAt = position === 'above' ? index : index + 1;
|
||||
children.splice(insertAt, 0, newBlock);
|
||||
recomputeNumbering(docAST.rootBlock);
|
||||
setDocAST({ ...docAST });
|
||||
};
|
||||
@@ -467,116 +765,163 @@ export default function App() {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 测试演练:动态修改当前用户在某个文档的权限角色
|
||||
const handleRoleChangeForDoc = (docId: string, role: DocPermissionRole) => {
|
||||
setCurrentUser((prev) => ({
|
||||
...prev,
|
||||
projectPermissions: {
|
||||
...(prev.projectPermissions || {}),
|
||||
[docId]: role,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
sessions={sessions}
|
||||
activeSessionId={activeSessionId}
|
||||
onSelectSession={setActiveSessionId}
|
||||
onNewSession={() => {
|
||||
const newSess: ProjectSession = {
|
||||
sessionId: 'sess-' + Date.now(),
|
||||
projectId: Date.now(),
|
||||
title: '新建医疗器械研发项目',
|
||||
deviceType: '通用医疗器械',
|
||||
stage: 'V0.1',
|
||||
stageLabel: 'V0.1 需求摸底',
|
||||
messageCount: 0,
|
||||
updatedAt: '刚刚',
|
||||
};
|
||||
setSessions([newSess, ...sessions]);
|
||||
setActiveSessionId(newSess.sessionId);
|
||||
}}
|
||||
currentRole={currentRole}
|
||||
currentUserName={currentUserName}
|
||||
onChangeRole={(r, name) => {
|
||||
setCurrentRole(r);
|
||||
setCurrentUserName(name);
|
||||
}}
|
||||
chatContent={
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<MessageStream messages={messages} onOpenCase={setActiveDocId} />
|
||||
{pendingProposal && (
|
||||
<div style={{ padding: '0 16px' }}>
|
||||
<ProposalConfirmCard
|
||||
proposedChanges={pendingProposal.changes}
|
||||
impactAnalysis={pendingProposal.impact}
|
||||
potentialRisk={pendingProposal.risk}
|
||||
onAccept={handleAcceptProposal}
|
||||
onReject={() => setPendingProposal(null)}
|
||||
<>
|
||||
<AppShell
|
||||
sessions={sessions}
|
||||
activeSessionId={activeSessionId}
|
||||
onSelectSession={setActiveSessionId}
|
||||
onNewSession={() => {
|
||||
const newSess: ProjectSession = {
|
||||
sessionId: 'sess-' + Date.now(),
|
||||
projectId: Date.now(),
|
||||
title: '新建医疗器械研发项目',
|
||||
deviceType: '通用医疗器械',
|
||||
stage: 'V0.1',
|
||||
stageLabel: 'V0.1 需求摸底',
|
||||
messageCount: 0,
|
||||
updatedAt: '刚刚',
|
||||
};
|
||||
setSessions([newSess, ...sessions]);
|
||||
setActiveSessionId(newSess.sessionId);
|
||||
}}
|
||||
currentUser={currentUser}
|
||||
currentRole={currentDocRole}
|
||||
currentUserName={currentUser.name}
|
||||
onChangeRole={(_, __) => {}}
|
||||
onOpenRightsModal={() => setRightsModalOpen(true)}
|
||||
onOpenSettingsModal={() => setSettingsModalOpen(true)}
|
||||
chatContent={
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<MessageStream messages={messages} onOpenCase={handleSelectDoc} />
|
||||
{pendingProposal && (
|
||||
<div style={{ padding: '0 16px' }}>
|
||||
<ProposalConfirmCard
|
||||
proposedChanges={pendingProposal.changes}
|
||||
impactAnalysis={pendingProposal.impact}
|
||||
potentialRisk={pendingProposal.risk}
|
||||
onAccept={handleAcceptProposal}
|
||||
onReject={() => setPendingProposal(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<InputArea
|
||||
onSendMessage={handleSendMessage}
|
||||
isThinking={isThinking}
|
||||
isStreaming={false}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
docContent={
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', position: 'relative' }}>
|
||||
{/* 36px 极简 Tab 条 (含三分类分段切换与私密/共享识别) */}
|
||||
<SlimTabBar
|
||||
deliverables={visibleDeliverables}
|
||||
activeDocId={activeDocId}
|
||||
activeCategory={activeCategory}
|
||||
onSelectCategory={setActiveCategory}
|
||||
onSelectDoc={handleSelectDoc}
|
||||
onToggleDeckDrawer={() => setDeckDrawerOpen(!deckDrawerOpen)}
|
||||
onExportDocx={handleExportDocx}
|
||||
/>
|
||||
|
||||
{/* 全景大盘抽屉 (支持受控/共享/私密三板块与一键流转操作) */}
|
||||
<DeckDrawer
|
||||
isOpen={deckDrawerOpen}
|
||||
onClose={() => setDeckDrawerOpen(false)}
|
||||
deliverables={visibleDeliverables}
|
||||
activeDocId={activeDocId}
|
||||
currentUserId={currentUser.id}
|
||||
onSelectDoc={handleSelectDoc}
|
||||
onShareToProject={handleShareToProject}
|
||||
onRevokeShare={handleRevokeShare}
|
||||
onDeleteDoc={handleDeleteDoc}
|
||||
/>
|
||||
|
||||
{/* 块状文档渲染器 */}
|
||||
<BlockRenderer
|
||||
ast={docAST}
|
||||
currentRole={currentDocRole}
|
||||
onEditBlock={setEditingBlock}
|
||||
onInsertBlock={handleInsertBlock}
|
||||
onMoveBlock={handleMoveBlock}
|
||||
onDeleteBlock={handleDeleteBlock}
|
||||
onOpenComments={setCommentingBlock}
|
||||
onAskAgent={(snippet) => {
|
||||
handleSendMessage(
|
||||
`针对受控文档《${docAST.title}》中的条款内容:\n\n“${snippet.slice(0, 150)}${snippet.length > 150 ? '...' : ''}”\n\n请帮我审查其合规性、技术可行性或提出修改建议。`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 底部法定流转动作条 (根据受控/共享/私密形态自适应展示) */}
|
||||
<DocGovActionBar
|
||||
ast={docAST}
|
||||
currentRole={currentDocRole}
|
||||
currentUserId={currentUser.id}
|
||||
onClaim={handleClaim}
|
||||
onSubmitReview={handleSubmitReview}
|
||||
onPassReview={handlePassReview}
|
||||
onRejectReview={handleRejectReview}
|
||||
onApprove={handleApprove}
|
||||
onRejectApprove={handleRejectReview}
|
||||
onShareToProject={handleShareToProject}
|
||||
onRevokeShare={handleRevokeShare}
|
||||
/>
|
||||
|
||||
{/* 块就地编辑弹窗 (支持富文本工具栏、多维表格与未保存拦截) */}
|
||||
{editingBlock && (
|
||||
<BlockEditorModal
|
||||
block={editingBlock}
|
||||
onSave={handleSaveBlockEdit}
|
||||
onClose={() => setEditingBlock(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<InputArea
|
||||
onSendMessage={handleSendMessage}
|
||||
isThinking={isThinking}
|
||||
isStreaming={false}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
docContent={
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', position: 'relative' }}>
|
||||
{/* 36px 极简 Tab 条 */}
|
||||
<SlimTabBar
|
||||
deliverables={deliverables}
|
||||
activeDocId={activeDocId}
|
||||
onSelectDoc={setActiveDocId}
|
||||
onToggleDeckDrawer={() => setDeckDrawerOpen(!deckDrawerOpen)}
|
||||
onExportDocx={handleExportDocx}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 全景大盘抽屉 */}
|
||||
<DeckDrawer
|
||||
isOpen={deckDrawerOpen}
|
||||
onClose={() => setDeckDrawerOpen(false)}
|
||||
deliverables={deliverables}
|
||||
activeDocId={activeDocId}
|
||||
onSelectDoc={setActiveDocId}
|
||||
/>
|
||||
{/* 审核批注侧栏 */}
|
||||
{commentingBlock && (
|
||||
<CommentSidePanel
|
||||
block={commentingBlock}
|
||||
currentRole={currentDocRole}
|
||||
currentUserName={currentUser.name}
|
||||
onAddComment={handleAddComment}
|
||||
onClose={() => setCommentingBlock(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 块状文档渲染器 */}
|
||||
<BlockRenderer
|
||||
ast={docAST}
|
||||
currentRole={currentRole}
|
||||
onEditBlock={setEditingBlock}
|
||||
onInsertBlock={handleInsertBlock}
|
||||
onMoveBlock={handleMoveBlock}
|
||||
onDeleteBlock={handleDeleteBlock}
|
||||
onOpenComments={setCommentingBlock}
|
||||
/>
|
||||
{/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 (支持快速模拟账号切换) */}
|
||||
<UserProjectRightsModal
|
||||
isOpen={rightsModalOpen}
|
||||
onClose={() => setRightsModalOpen(false)}
|
||||
currentUser={currentUser}
|
||||
currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'}
|
||||
deliverables={visibleDeliverables}
|
||||
activeDocId={activeDocId}
|
||||
allUsers={MOCK_USERS}
|
||||
onSwitchUser={(u) => setCurrentUser(u)}
|
||||
onSelectDoc={handleSelectDoc}
|
||||
onRoleChangeForDoc={handleRoleChangeForDoc}
|
||||
/>
|
||||
|
||||
{/* 底部法定流转动作条 */}
|
||||
<DocGovActionBar
|
||||
ast={docAST}
|
||||
currentRole={currentRole}
|
||||
onClaim={handleClaim}
|
||||
onSubmitReview={handleSubmitReview}
|
||||
onPassReview={handlePassReview}
|
||||
onRejectReview={handleRejectReview}
|
||||
onApprove={handleApprove}
|
||||
onRejectApprove={handleRejectReview}
|
||||
/>
|
||||
|
||||
{/* 块就地编辑弹窗 */}
|
||||
{editingBlock && (
|
||||
<BlockEditorModal
|
||||
block={editingBlock}
|
||||
onSave={handleSaveBlockEdit}
|
||||
onClose={() => setEditingBlock(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 审核批注侧栏 */}
|
||||
{commentingBlock && (
|
||||
<CommentSidePanel
|
||||
block={commentingBlock}
|
||||
currentRole={currentRole}
|
||||
currentUserName={currentUserName}
|
||||
onAddComment={handleAddComment}
|
||||
onClose={() => setCommentingBlock(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{/* 预留系统设置与管理员后台弹窗 */}
|
||||
<SettingsModal
|
||||
isOpen={settingsModalOpen}
|
||||
onClose={() => setSettingsModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
import React from 'react';
|
||||
import { DeliverableItem } from '../../types/workbench';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DeliverableItem, DeliverableCategory } from '../../types/workbench';
|
||||
import { DocLifecycleState } from '../../types/docgov';
|
||||
import { X, CheckCircle2, Clock, ShieldCheck, UserCheck, AlertTriangle } from 'lucide-react';
|
||||
import {
|
||||
X,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
ShieldCheck,
|
||||
UserCheck,
|
||||
AlertTriangle,
|
||||
Lock,
|
||||
Globe,
|
||||
Share2,
|
||||
Undo2,
|
||||
Trash2,
|
||||
Shield,
|
||||
Layers,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DeckDrawerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
deliverables: DeliverableItem[];
|
||||
activeDocId: string;
|
||||
currentUserId: number;
|
||||
onSelectDoc: (docId: string) => void;
|
||||
onShareToProject?: (docId: string) => void;
|
||||
onRevokeShare?: (docId: string) => void;
|
||||
onDeleteDoc?: (docId: string) => void;
|
||||
}
|
||||
|
||||
export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
@@ -16,8 +34,24 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
onClose,
|
||||
deliverables,
|
||||
activeDocId,
|
||||
currentUserId,
|
||||
onSelectDoc,
|
||||
onShareToProject,
|
||||
onRevokeShare,
|
||||
onDeleteDoc,
|
||||
}) => {
|
||||
const [filterCategory, setFilterCategory] = useState<'all' | DeliverableCategory>('all');
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -26,28 +60,76 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
switch (state) {
|
||||
case 'approved':
|
||||
return (
|
||||
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--success)', background: 'var(--success-bg)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--success)',
|
||||
background: 'var(--success-bg)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 size={11} />
|
||||
<span>终审已生效</span>
|
||||
</span>
|
||||
);
|
||||
case 'reviewing':
|
||||
return (
|
||||
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--amber-600)', background: 'var(--amber-50)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--amber-600)',
|
||||
background: 'var(--amber-50)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<Clock size={11} />
|
||||
<span>审核中 (已锁定)</span>
|
||||
</span>
|
||||
);
|
||||
case 'approving':
|
||||
return (
|
||||
<span style={{ fontSize: '11px', fontWeight: 600, color: '#9333EA', background: '#F3E8FF', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: '#9333EA',
|
||||
background: '#F3E8FF',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<ShieldCheck size={11} />
|
||||
<span>批准中</span>
|
||||
</span>
|
||||
);
|
||||
case 'rejected':
|
||||
return (
|
||||
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--danger)', background: 'var(--danger-bg)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--danger)',
|
||||
background: 'var(--danger-bg)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={11} />
|
||||
<span>被驳回需修改</span>
|
||||
</span>
|
||||
@@ -55,7 +137,19 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
case 'drafting':
|
||||
default:
|
||||
return (
|
||||
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--brand-600)', background: 'var(--brand-50)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--brand-600)',
|
||||
background: 'var(--brand-50)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<UserCheck size={11} />
|
||||
<span>编制中 {isClaimed ? '(已认领)' : '(未认领)'}</span>
|
||||
</span>
|
||||
@@ -63,143 +157,522 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 按阶段分组
|
||||
const stages: { stage: string; title: string }[] = [
|
||||
{ stage: 'V0.1', title: 'V0.1 需求摸底与产品定义' },
|
||||
{ stage: 'V0.5', title: 'V0.5 方案成型与法规对齐' },
|
||||
{ stage: 'V1.0', title: 'V1.0 商务报价与WBS工时测算' },
|
||||
];
|
||||
|
||||
const workflowDocs = deliverables.filter((d) => d.category === 'workflow');
|
||||
const sharedDocs = deliverables.filter((d) => d.category === 'shared');
|
||||
const privateDocs = deliverables.filter((d) => d.category === 'private');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '36px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: '480px',
|
||||
background: 'var(--bg-panel)',
|
||||
borderBottom: '2px solid var(--brand-500)',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
zIndex: 50,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
backdropFilter: 'blur(16px)',
|
||||
}}
|
||||
>
|
||||
{/* 抽屉顶栏 */}
|
||||
<>
|
||||
{/* 外部遮罩 */}
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
background: 'var(--bg-subtle)',
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 45,
|
||||
background: 'rgba(15, 23, 42, 0.35)',
|
||||
backdropFilter: 'blur(3px)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
||||
当前项目阶段交付物台账大盘
|
||||
</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
||||
(共 {deliverables.length} 份受控交付物)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '26px',
|
||||
height: '26px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
color: 'var(--text-muted)',
|
||||
}}
|
||||
title="关闭大盘"
|
||||
>
|
||||
<X size={15} />
|
||||
</button>
|
||||
</div>
|
||||
/>
|
||||
|
||||
{/* 交付物卡片列表 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
padding: '14px 16px',
|
||||
position: 'absolute',
|
||||
top: '36px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: '560px',
|
||||
background: 'var(--bg-panel)',
|
||||
borderBottom: '2px solid var(--brand-500)',
|
||||
boxShadow: 'var(--shadow-xl)',
|
||||
zIndex: 50,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '14px',
|
||||
overflow: 'hidden',
|
||||
animation: 'fadeIn 0.15s ease-out',
|
||||
}}
|
||||
>
|
||||
{stages.map((stg) => {
|
||||
const docsInStage = deliverables.filter((d) => d.stage === stg.stage);
|
||||
if (docsInStage.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={stg.stage} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--brand-600)' }}>
|
||||
{stg.title}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '10px' }}>
|
||||
{docsInStage.map((doc) => {
|
||||
const isActive = doc.docId === activeDocId;
|
||||
return (
|
||||
<div
|
||||
key={doc.docId}
|
||||
onClick={() => {
|
||||
onSelectDoc(doc.docId);
|
||||
onClose();
|
||||
}}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--r-md)',
|
||||
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
|
||||
border: isActive ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
|
||||
boxShadow: 'var(--shadow-xs)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) e.currentTarget.style.borderColor = 'var(--brand-100)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) e.currentTarget.style.borderColor = 'var(--border)';
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
{renderBadge(doc.lifecycleState, doc.isClaimed)}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
||||
<span>编制: <b>{doc.author}</b></span>
|
||||
<span>审核: <b>{doc.reviewer}</b></span>
|
||||
<span>批准: <b>{doc.approver}</b></span>
|
||||
</div>
|
||||
|
||||
{doc.summary && (
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
|
||||
{doc.summary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* 抽屉顶栏 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
background: 'var(--bg-subtle)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<Layers size={16} color="var(--brand-500)" />
|
||||
<span style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
||||
交付物分类台账全景大盘
|
||||
</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
||||
(共 {deliverables.length} 份可用交付物)
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 大盘内分类筛选切换 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
background: 'var(--bg-panel)',
|
||||
padding: '2px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => setFilterCategory('all')}
|
||||
style={{
|
||||
height: '22px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: filterCategory === 'all' ? 600 : 500,
|
||||
background: filterCategory === 'all' ? 'var(--brand-500)' : 'transparent',
|
||||
color: filterCategory === 'all' ? '#ffffff' : 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
全部全景 ({deliverables.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterCategory('workflow')}
|
||||
style={{
|
||||
height: '22px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: filterCategory === 'workflow' ? 600 : 500,
|
||||
background: filterCategory === 'workflow' ? 'var(--brand-500)' : 'transparent',
|
||||
color: filterCategory === 'workflow' ? '#ffffff' : 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
流程受控 ({workflowDocs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterCategory('shared')}
|
||||
style={{
|
||||
height: '22px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: filterCategory === 'shared' ? 600 : 500,
|
||||
background: filterCategory === 'shared' ? 'var(--brand-500)' : 'transparent',
|
||||
color: filterCategory === 'shared' ? '#ffffff' : 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
项目共享 ({sharedDocs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterCategory('private')}
|
||||
style={{
|
||||
height: '22px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: filterCategory === 'private' ? 600 : 500,
|
||||
background: filterCategory === 'private' ? '#D97706' : 'transparent',
|
||||
color: filterCategory === 'private' ? '#ffffff' : 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
我的私密 ({privateDocs.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '26px',
|
||||
height: '26px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title="关闭大盘"
|
||||
>
|
||||
<X size={15} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 交付物大盘内容区 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
}}
|
||||
>
|
||||
{/* 板块一:🏛️ 研发体系预设受控交付物 */}
|
||||
{(filterCategory === 'all' || filterCategory === 'workflow') && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
paddingBottom: '6px',
|
||||
borderBottom: '1px dashed var(--border)',
|
||||
}}
|
||||
>
|
||||
<Shield size={14} color="var(--brand-500)" />
|
||||
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
||||
研发体系预设受控交付物 (固定流程 · 严格审签)
|
||||
</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
||||
共 {workflowDocs.length} 份受控文档
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{stages.map((stg) => {
|
||||
const docsInStage = workflowDocs.filter((d) => d.stage === stg.stage);
|
||||
if (docsInStage.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={stg.stage} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<div style={{ fontSize: '11px', fontWeight: 600, color: 'var(--brand-600)', paddingLeft: '4px' }}>
|
||||
{stg.title}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(290px, 1fr))', gap: '10px' }}>
|
||||
{docsInStage.map((doc) => {
|
||||
const isActive = doc.docId === activeDocId;
|
||||
return (
|
||||
<div
|
||||
key={doc.docId}
|
||||
onClick={() => {
|
||||
onSelectDoc(doc.docId);
|
||||
onClose();
|
||||
}}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--r-md)',
|
||||
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
|
||||
border: isActive ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
|
||||
boxShadow: 'var(--shadow-xs)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
{renderBadge(doc.lifecycleState, doc.isClaimed)}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
||||
<span>编制: <b>{doc.author}</b></span>
|
||||
<span>审核: <b>{doc.reviewer}</b></span>
|
||||
<span>批准: <b>{doc.approver}</b></span>
|
||||
</div>
|
||||
|
||||
{doc.summary && (
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
|
||||
{doc.summary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 板块二:🌐 项目协作参考资料库 */}
|
||||
{(filterCategory === 'all' || filterCategory === 'shared') && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
paddingBottom: '6px',
|
||||
borderBottom: '1px dashed var(--border)',
|
||||
}}
|
||||
>
|
||||
<Globe size={14} color="var(--brand-500)" />
|
||||
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
||||
项目协作参考资料库 (团队共享 · 方案推演)
|
||||
</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
||||
共 {sharedDocs.length} 份共享参考材料
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{sharedDocs.length === 0 ? (
|
||||
<div style={{ fontSize: '12px', color: 'var(--text-faint)', padding: '12px', textAlign: 'center' }}>
|
||||
当前暂无团队成员共享的参考资料。个人私密生成物成熟后可一键共享至此处。
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(290px, 1fr))', gap: '10px' }}>
|
||||
{sharedDocs.map((doc) => {
|
||||
const isActive = doc.docId === activeDocId;
|
||||
const isCreator = doc.creatorId === currentUserId;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={doc.docId}
|
||||
onClick={() => {
|
||||
onSelectDoc(doc.docId);
|
||||
onClose();
|
||||
}}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--r-md)',
|
||||
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
|
||||
border: isActive ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
|
||||
boxShadow: 'var(--shadow-xs)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10.5px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--brand-600)',
|
||||
background: 'var(--brand-50)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<Globe size={10} />
|
||||
<span>项目共享</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
||||
<span>共享人: <b>{doc.creatorName}</b></span>
|
||||
<span>共享时间: <b>{doc.sharedAt || doc.updatedAt}</b></span>
|
||||
</div>
|
||||
|
||||
{doc.summary && (
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
|
||||
{doc.summary}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 共享人专属撤回操作 */}
|
||||
{isCreator && onRevokeShare && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '4px' }}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRevokeShare(doc.docId);
|
||||
}}
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
padding: '3px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--bg-subtle)',
|
||||
color: 'var(--text)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
title="将该共享资料撤回为个人私密草稿 (仅自己可见)"
|
||||
>
|
||||
<Undo2 size={11} />
|
||||
<span>取消共享 (恢复私有)</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 板块三:🔒 我的私密生成档案 */}
|
||||
{(filterCategory === 'all' || filterCategory === 'private') && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
paddingBottom: '6px',
|
||||
borderBottom: '1px dashed var(--border)',
|
||||
}}
|
||||
>
|
||||
<Lock size={14} color="#D97706" />
|
||||
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
||||
我的私密生成档案 (仅您本人可见 · 商业与推演隐私)
|
||||
</span>
|
||||
<span style={{ fontSize: '11px', color: '#D97706', fontWeight: 600 }}>
|
||||
共 {privateDocs.length} 份私有草稿
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{privateDocs.length === 0 ? (
|
||||
<div style={{ fontSize: '12px', color: 'var(--text-faint)', padding: '12px', textAlign: 'center' }}>
|
||||
您当前未生成私密文档。在中间栏向智能体提出测算、草拟需求时,生成的文档将自动归档于此处。
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(290px, 1fr))', gap: '10px' }}>
|
||||
{privateDocs.map((doc) => {
|
||||
const isActive = doc.docId === activeDocId;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={doc.docId}
|
||||
onClick={() => {
|
||||
onSelectDoc(doc.docId);
|
||||
onClose();
|
||||
}}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--r-md)',
|
||||
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
|
||||
border: isActive ? '1.5px solid #D97706' : '1px solid var(--border)',
|
||||
boxShadow: 'var(--shadow-xs)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10.5px',
|
||||
fontWeight: 600,
|
||||
color: '#D97706',
|
||||
background: '#FEF3C7',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<Lock size={10} />
|
||||
<span>仅我可见</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
||||
<span>创建人: <b>本人 ({doc.creatorName})</b></span>
|
||||
<span>生成时间: <b>{doc.updatedAt}</b></span>
|
||||
</div>
|
||||
|
||||
{doc.summary && (
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
|
||||
{doc.summary}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '6px', marginTop: '4px' }}>
|
||||
{onShareToProject && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShareToProject(doc.docId);
|
||||
}}
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
padding: '3px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--brand-500)',
|
||||
background: 'var(--brand-500)',
|
||||
color: '#ffffff',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
title="将该私密文档一键共享给项目组成员作为协作参考资料"
|
||||
>
|
||||
<Share2 size={11} />
|
||||
<span>一键共享给项目组</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{onDeleteDoc && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteDoc(doc.docId);
|
||||
}}
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
padding: '3px 6px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--bg-subtle)',
|
||||
color: 'var(--danger)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
}}
|
||||
title="删除此私密草稿"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DocumentAST } from '../../types/block';
|
||||
import { UserRole } from '../../types/docgov';
|
||||
import { Send, CheckCircle2, XCircle, Award, UserCheck, Lock } from 'lucide-react';
|
||||
import {
|
||||
Send,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Award,
|
||||
UserCheck,
|
||||
Lock,
|
||||
Globe,
|
||||
Share2,
|
||||
Undo2,
|
||||
Shield,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DocGovActionBarProps {
|
||||
ast: DocumentAST;
|
||||
currentRole: UserRole;
|
||||
currentUserId?: number;
|
||||
onClaim: () => void;
|
||||
onSubmitReview: () => void;
|
||||
onPassReview: () => void;
|
||||
onRejectReview: (reason: string) => void;
|
||||
onApprove: () => void;
|
||||
onRejectApprove: (reason: string) => void;
|
||||
onShareToProject?: (docId: string) => void;
|
||||
onRevokeShare?: (docId: string) => void;
|
||||
}
|
||||
|
||||
export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
ast,
|
||||
currentRole,
|
||||
currentUserId,
|
||||
onClaim,
|
||||
onSubmitReview,
|
||||
onPassReview,
|
||||
onRejectReview,
|
||||
onApprove,
|
||||
onRejectApprove,
|
||||
onShareToProject,
|
||||
onRevokeShare,
|
||||
}) => {
|
||||
const [rejectModalOpen, setRejectModalOpen] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
@@ -44,6 +61,10 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
setRejectModalOpen(false);
|
||||
};
|
||||
|
||||
const isPrivate = ast.category === 'private';
|
||||
const isShared = ast.category === 'shared';
|
||||
const isWorkflow = !isPrivate && !isShared;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -109,7 +130,14 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => setRejectModalOpen(false)}
|
||||
style={{ padding: '6px 12px', borderRadius: 'var(--r-sm)', background: 'var(--bg-subtle)', color: 'var(--text)' }}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--bg-subtle)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@@ -120,9 +148,11 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
padding: '6px 14px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--danger)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
opacity: rejectReason.trim() ? 1 : 0.5,
|
||||
cursor: rejectReason.trim() ? 'pointer' : 'not-allowed',
|
||||
}}
|
||||
>
|
||||
确认驳回退回编制
|
||||
@@ -132,216 +162,355 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 左侧状态标识 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
当前生命周期:
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
background:
|
||||
ast.lifecycleState === 'approved'
|
||||
? 'var(--success-bg)'
|
||||
: ast.lifecycleState === 'reviewing'
|
||||
? 'var(--amber-50)'
|
||||
: ast.lifecycleState === 'approving'
|
||||
? '#F3E8FF'
|
||||
: 'var(--brand-50)',
|
||||
color:
|
||||
ast.lifecycleState === 'approved'
|
||||
? 'var(--success)'
|
||||
: ast.lifecycleState === 'reviewing'
|
||||
? 'var(--amber-600)'
|
||||
: ast.lifecycleState === 'approving'
|
||||
? '#9333EA'
|
||||
: 'var(--brand-600)',
|
||||
}}
|
||||
>
|
||||
{ast.lifecycleState === 'approved'
|
||||
? '已终审生效'
|
||||
: ast.lifecycleState === 'reviewing'
|
||||
? '审核中'
|
||||
: ast.lifecycleState === 'approving'
|
||||
? '批准中'
|
||||
: '编制中'}
|
||||
</span>
|
||||
{ast.isClaimed && (
|
||||
<span style={{ fontSize: '11px', color: 'var(--brand-500)', fontWeight: 500 }}>
|
||||
• 已被编制人认领
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧动作流转按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{/* 1. 编制阶段 */}
|
||||
{ast.lifecycleState === 'drafting' && (
|
||||
<>
|
||||
{currentRole === 'author' ? (
|
||||
<>
|
||||
{!ast.isClaimed ? (
|
||||
<button
|
||||
onClick={onClaim}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--bg-subtle)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
}}
|
||||
>
|
||||
<UserCheck size={13} style={{ color: 'var(--brand-500)' }} />
|
||||
<span>认领此文档</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
onClick={onSubmitReview}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--brand-grad)',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
boxShadow: '0 2px 6px rgba(79, 107, 255, 0.25)',
|
||||
}}
|
||||
>
|
||||
<Send size={13} />
|
||||
<span>提交送审 (流转至审核人)</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
等待编制负责人({ast.author})完成初稿并提交送审
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 2. 审核阶段 */}
|
||||
{ast.lifecycleState === 'reviewing' && (
|
||||
<>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-faint)', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<Lock size={12} /> 文档已锁定
|
||||
{/* 场景 1: 个人私密文档专属条 */}
|
||||
{isPrivate && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
background: '#FEF3C7',
|
||||
color: '#D97706',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<Lock size={12} />
|
||||
<span>个人私密草稿</span>
|
||||
</span>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
由助手针对您的对话需求生成,<strong>仅您本人可见</strong>。不参与项目受控审签体系。
|
||||
</span>
|
||||
{currentRole === 'reviewer' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openReject('review')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--danger-bg)',
|
||||
border: '1px solid var(--danger)',
|
||||
color: 'var(--danger)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<XCircle size={13} />
|
||||
<span>驳回修改</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onPassReview}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--amber-500)',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
boxShadow: '0 2px 6px rgba(242, 169, 59, 0.3)',
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 size={13} />
|
||||
<span>审核通过 (送呈批准人)</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
等待审核负责人({ast.reviewer})完成审核签署
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 3. 批准阶段 */}
|
||||
{ast.lifecycleState === 'approving' && (
|
||||
<>
|
||||
{currentRole === 'approver' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openReject('approve')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--danger-bg)',
|
||||
border: '1px solid var(--danger)',
|
||||
color: 'var(--danger)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<XCircle size={13} />
|
||||
<span>驳回修改</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onApprove}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--success)',
|
||||
color: '#fff',
|
||||
fontSize: '12.5px',
|
||||
fontWeight: 700,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
boxShadow: '0 2px 8px rgba(33, 166, 106, 0.35)',
|
||||
}}
|
||||
>
|
||||
<Award size={14} />
|
||||
<span>终审批准生效 (自动入标杆库)</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
等待批准负责人({ast.approver})终审批准发布
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 4. 已批准终态 */}
|
||||
{ast.lifecycleState === 'approved' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--success)', fontSize: '12.5px', fontWeight: 600 }}>
|
||||
<Award size={16} />
|
||||
<span>受控归档交付物 (已沉淀为全局标杆经验)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{onShareToProject && (
|
||||
<button
|
||||
onClick={() => onShareToProject(ast.docId)}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--brand-500)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 6px rgba(79, 107, 255, 0.25)',
|
||||
}}
|
||||
title="将此推演草稿一键共享给项目组成员查阅与讨论"
|
||||
>
|
||||
<Share2 size={13} />
|
||||
<span>一键共享给项目组</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 场景 2: 项目协作参考资料专属条 */}
|
||||
{isShared && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
background: 'var(--brand-50)',
|
||||
color: 'var(--brand-600)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<Globe size={12} />
|
||||
<span>项目协作参考资料</span>
|
||||
</span>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
共享人: <b>{ast.creatorName || ast.author}</b>
|
||||
{ast.sharedAt ? ` (${ast.sharedAt})` : ''} · 项目全员可查阅、引用与批注
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{/* 仅创建者本人有权取消共享 */}
|
||||
{ast.creatorId === currentUserId && onRevokeShare && (
|
||||
<button
|
||||
onClick={() => onRevokeShare(ast.docId)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--bg-subtle)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title="撤回该文档的共享状态,恢复为您个人私有"
|
||||
>
|
||||
<Undo2 size={13} />
|
||||
<span>取消共享 (恢复私有)</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 场景 3: 研发体系预设受控交付物 (严格三权分立审签) */}
|
||||
{isWorkflow && (
|
||||
<>
|
||||
{/* 左侧状态标识 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Shield size={14} color="var(--brand-500)" />
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
受控状态:
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
background:
|
||||
ast.lifecycleState === 'approved'
|
||||
? 'var(--success-bg)'
|
||||
: ast.lifecycleState === 'reviewing'
|
||||
? 'var(--amber-50)'
|
||||
: ast.lifecycleState === 'approving'
|
||||
? '#F3E8FF'
|
||||
: 'var(--brand-50)',
|
||||
color:
|
||||
ast.lifecycleState === 'approved'
|
||||
? 'var(--success)'
|
||||
: ast.lifecycleState === 'reviewing'
|
||||
? 'var(--amber-600)'
|
||||
: ast.lifecycleState === 'approving'
|
||||
? '#9333EA'
|
||||
: 'var(--brand-600)',
|
||||
}}
|
||||
>
|
||||
{ast.lifecycleState === 'approved'
|
||||
? '已终审生效'
|
||||
: ast.lifecycleState === 'reviewing'
|
||||
? '审核中'
|
||||
: ast.lifecycleState === 'approving'
|
||||
? '批准中'
|
||||
: '编制中'}
|
||||
</span>
|
||||
{ast.isClaimed && (
|
||||
<span style={{ fontSize: '11px', color: 'var(--brand-500)', fontWeight: 500 }}>
|
||||
• 已被编制人认领
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧动作流转按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{/* 1. 编制阶段 */}
|
||||
{ast.lifecycleState === 'drafting' && (
|
||||
<>
|
||||
{currentRole === 'author' ? (
|
||||
<>
|
||||
{!ast.isClaimed ? (
|
||||
<button
|
||||
onClick={onClaim}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--bg-subtle)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<UserCheck size={13} style={{ color: 'var(--brand-500)' }} />
|
||||
<span>认领此文档</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
onClick={onSubmitReview}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--brand-grad)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 6px rgba(79, 107, 255, 0.25)',
|
||||
}}
|
||||
>
|
||||
<Send size={13} />
|
||||
<span>提交送审 (流转至审核人)</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
等待编制负责人({ast.author})完成初稿并提交送审
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 2. 审核阶段 */}
|
||||
{ast.lifecycleState === 'reviewing' && (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-faint)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<Lock size={12} /> 文档已锁定
|
||||
</span>
|
||||
{currentRole === 'reviewer' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openReject('review')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--danger-bg)',
|
||||
border: '1px solid var(--danger)',
|
||||
color: 'var(--danger)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<XCircle size={13} />
|
||||
<span>驳回修改</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onPassReview}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--amber-500)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 6px rgba(242, 169, 59, 0.3)',
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 size={13} />
|
||||
<span>审核通过 (送呈批准人)</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
等待审核负责人({ast.reviewer})完成审核签署
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 3. 批准阶段 */}
|
||||
{ast.lifecycleState === 'approving' && (
|
||||
<>
|
||||
{currentRole === 'approver' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openReject('approve')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--danger-bg)',
|
||||
border: '1px solid var(--danger)',
|
||||
color: 'var(--danger)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<XCircle size={13} />
|
||||
<span>驳回修改</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onApprove}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
background: 'var(--success)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '12.5px',
|
||||
fontWeight: 700,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 8px rgba(33, 166, 106, 0.35)',
|
||||
}}
|
||||
>
|
||||
<Award size={14} />
|
||||
<span>终审批准生效 (自动入标杆库)</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
等待批准负责人({ast.approver})终审批准发布
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 4. 已批准终态 */}
|
||||
{ast.lifecycleState === 'approved' && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
color: 'var(--success)',
|
||||
fontSize: '12.5px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Award size={16} />
|
||||
<span>受控归档交付物 (已沉淀为全局标杆经验)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { DeliverableItem } from '../../types/workbench';
|
||||
import { DeliverableItem, DeliverableCategory } from '../../types/workbench';
|
||||
import { DocLifecycleState } from '../../types/docgov';
|
||||
import { FolderKanban, FileDown, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { FolderKanban, FileDown, Maximize2, Minimize2, Lock, Globe, Shield } from 'lucide-react';
|
||||
|
||||
interface SlimTabBarProps {
|
||||
deliverables: DeliverableItem[];
|
||||
activeDocId: string;
|
||||
activeCategory: DeliverableCategory;
|
||||
onSelectCategory: (category: DeliverableCategory) => void;
|
||||
onSelectDoc: (docId: string) => void;
|
||||
onToggleDeckDrawer: () => void;
|
||||
onExportDocx?: () => void;
|
||||
@@ -16,6 +18,8 @@ interface SlimTabBarProps {
|
||||
export const SlimTabBar: React.FC<SlimTabBarProps> = ({
|
||||
deliverables,
|
||||
activeDocId,
|
||||
activeCategory,
|
||||
onSelectCategory,
|
||||
onSelectDoc,
|
||||
onToggleDeckDrawer,
|
||||
onExportDocx,
|
||||
@@ -38,6 +42,20 @@ export const SlimTabBar: React.FC<SlimTabBarProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const workflowDocs = deliverables.filter((d) => d.category === 'workflow');
|
||||
const sharedDocs = deliverables.filter((d) => d.category === 'shared');
|
||||
const privateDocs = deliverables.filter((d) => d.category === 'private');
|
||||
|
||||
const visibleTabs = deliverables.filter((d) => d.category === activeCategory);
|
||||
|
||||
const handleCategoryClick = (cat: DeliverableCategory) => {
|
||||
onSelectCategory(cat);
|
||||
const targetDocs = deliverables.filter((d) => d.category === cat);
|
||||
if (targetDocs.length > 0 && !targetDocs.some((d) => d.docId === activeDocId)) {
|
||||
onSelectDoc(targetDocs[0].docId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -47,62 +65,196 @@ export const SlimTabBar: React.FC<SlimTabBarProps> = ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
gap: '4px',
|
||||
gap: '8px',
|
||||
overflowX: 'auto',
|
||||
flexShrink: 0,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{/* 交付物 Tabs */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', flex: 1, overflowX: 'auto' }}>
|
||||
{deliverables.map((doc) => {
|
||||
const isActive = doc.docId === activeDocId;
|
||||
const status = getStatusDot(doc.lifecycleState);
|
||||
{/* 分类切换分段 Pill */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'var(--bg-panel)',
|
||||
padding: '2px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--border)',
|
||||
gap: '2px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleCategoryClick('workflow')}
|
||||
style={{
|
||||
height: '24px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
fontWeight: activeCategory === 'workflow' ? 600 : 500,
|
||||
background: activeCategory === 'workflow' ? 'var(--brand-500)' : 'transparent',
|
||||
color: activeCategory === 'workflow' ? '#ffffff' : 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
title="研发体系预设受控交付物 (全员可见 · 严格审签)"
|
||||
>
|
||||
<Shield size={12} />
|
||||
<span>流程受控</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '0 4px',
|
||||
borderRadius: '8px',
|
||||
background: activeCategory === 'workflow' ? 'rgba(255,255,255,0.25)' : 'var(--bg-subtle)',
|
||||
color: activeCategory === 'workflow' ? '#ffffff' : 'var(--text-faint)',
|
||||
}}
|
||||
>
|
||||
{workflowDocs.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={doc.docId}
|
||||
onClick={() => onSelectDoc(doc.docId)}
|
||||
style={{
|
||||
height: '28px',
|
||||
padding: '0 10px',
|
||||
borderRadius: '6px 6px 0 0',
|
||||
background: isActive ? 'var(--bg-panel)' : 'transparent',
|
||||
border: isActive ? '1px solid var(--border)' : '1px solid transparent',
|
||||
borderBottom: isActive ? '1px solid var(--bg-panel)' : '1px solid transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
color: isActive ? 'var(--text-strong)' : 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) e.currentTarget.style.color = 'var(--text)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) e.currentTarget.style.color = 'var(--text-muted)';
|
||||
}}
|
||||
title={`${doc.title} (${status.label})`}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleCategoryClick('shared')}
|
||||
style={{
|
||||
height: '24px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
fontWeight: activeCategory === 'shared' ? 600 : 500,
|
||||
background: activeCategory === 'shared' ? 'var(--brand-500)' : 'transparent',
|
||||
color: activeCategory === 'shared' ? '#ffffff' : 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
title="由成员对话推演并共享至项目的协作参考资料"
|
||||
>
|
||||
<Globe size={12} />
|
||||
<span>项目共享</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '0 4px',
|
||||
borderRadius: '8px',
|
||||
background: activeCategory === 'shared' ? 'rgba(255,255,255,0.25)' : 'var(--bg-subtle)',
|
||||
color: activeCategory === 'shared' ? '#ffffff' : 'var(--text-faint)',
|
||||
}}
|
||||
>
|
||||
{sharedDocs.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleCategoryClick('private')}
|
||||
style={{
|
||||
height: '24px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
fontSize: '11px',
|
||||
fontWeight: activeCategory === 'private' ? 600 : 500,
|
||||
background: activeCategory === 'private' ? '#D97706' : 'transparent',
|
||||
color: activeCategory === 'private' ? '#ffffff' : 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
title="您在对话中要求助手生成的私密文档 (仅您本人可见)"
|
||||
>
|
||||
<Lock size={12} />
|
||||
<span>我的私密</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '0 4px',
|
||||
borderRadius: '8px',
|
||||
background: activeCategory === 'private' ? 'rgba(255,255,255,0.25)' : 'var(--bg-subtle)',
|
||||
color: activeCategory === 'private' ? '#ffffff' : 'var(--text-faint)',
|
||||
}}
|
||||
>
|
||||
{privateDocs.length}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ width: '1px', height: '18px', background: 'var(--border)', flexShrink: 0 }} />
|
||||
|
||||
{/* 交付物 Tabs (当前分类下的文档) */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', flex: 1, overflowX: 'auto' }}>
|
||||
{visibleTabs.length === 0 ? (
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-faint)', padding: '0 8px' }}>
|
||||
{activeCategory === 'private'
|
||||
? '暂无个人私密生成文档 (在对话中要求助手生成即可)'
|
||||
: activeCategory === 'shared'
|
||||
? '暂无项目共享参考资料'
|
||||
: '暂无流程文档'}
|
||||
</div>
|
||||
) : (
|
||||
visibleTabs.map((doc) => {
|
||||
const isActive = doc.docId === activeDocId;
|
||||
const status = getStatusDot(doc.lifecycleState);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={doc.docId}
|
||||
onClick={() => onSelectDoc(doc.docId)}
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: status.color,
|
||||
boxShadow: `0 0 5px ${status.color}`,
|
||||
height: '28px',
|
||||
padding: '0 10px',
|
||||
borderRadius: '6px 6px 0 0',
|
||||
background: isActive ? 'var(--bg-panel)' : 'transparent',
|
||||
border: isActive ? '1px solid var(--border)' : '1px solid transparent',
|
||||
borderBottom: isActive ? '1px solid var(--bg-panel)' : '1px solid transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
color: isActive ? 'var(--text-strong)' : 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
/>
|
||||
<span style={{ maxWidth: '140px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) e.currentTarget.style.color = 'var(--text)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) e.currentTarget.style.color = 'var(--text-muted)';
|
||||
}}
|
||||
title={`${doc.title} (${doc.category === 'private' ? '仅我可见' : doc.category === 'shared' ? '项目共享' : status.label})`}
|
||||
>
|
||||
{doc.category === 'private' ? (
|
||||
<Lock size={12} color="#D97706" />
|
||||
) : doc.category === 'shared' ? (
|
||||
<Globe size={12} color="var(--brand-500)" />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: status.color,
|
||||
boxShadow: `0 0 5px ${status.color}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span style={{ maxWidth: '140px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧全局操作区 */}
|
||||
@@ -122,11 +274,12 @@ export const SlimTabBar: React.FC<SlimTabBarProps> = ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title="展开当前项目各阶段全部交付物状态大盘"
|
||||
>
|
||||
<FolderKanban size={13} style={{ color: 'var(--brand-500)' }} />
|
||||
<span>全部交付物 ({deliverables.length}) ▾</span>
|
||||
<span>全景大盘 ({deliverables.length}) ▾</span>
|
||||
</button>
|
||||
|
||||
{/* 导出受控 Word 按钮 */}
|
||||
@@ -145,11 +298,12 @@ export const SlimTabBar: React.FC<SlimTabBarProps> = ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title="导出为符合严肃合规模板的受控 DOCX 交付物"
|
||||
>
|
||||
<FileDown size={13} />
|
||||
<span>导出受控 DOCX</span>
|
||||
<span>导出 DOCX</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -161,10 +315,13 @@ export const SlimTabBar: React.FC<SlimTabBarProps> = ({
|
||||
width: '26px',
|
||||
height: '26px',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title={isFullscreen ? '退出全屏' : '全屏沉浸阅读与编辑'}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import {
|
||||
X,
|
||||
ShieldCheck,
|
||||
FileText,
|
||||
Briefcase,
|
||||
ExternalLink,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { UserProfile, DocPermissionRole } from '../../types/docgov';
|
||||
import { DeliverableItem } from '../../types/workbench';
|
||||
|
||||
interface UserProjectRightsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
currentUser: UserProfile;
|
||||
currentProjectTitle: string;
|
||||
deliverables: DeliverableItem[];
|
||||
activeDocId: string;
|
||||
allUsers?: UserProfile[];
|
||||
onSwitchUser?: (user: UserProfile) => void;
|
||||
onSelectDoc: (docId: string) => void;
|
||||
onRoleChangeForDoc?: (docId: string, role: DocPermissionRole) => void;
|
||||
}
|
||||
|
||||
export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentUser,
|
||||
currentProjectTitle,
|
||||
deliverables,
|
||||
activeDocId,
|
||||
allUsers,
|
||||
onSwitchUser,
|
||||
onSelectDoc,
|
||||
onRoleChangeForDoc,
|
||||
}) => {
|
||||
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;
|
||||
|
||||
// Compute stats
|
||||
const permissions = currentUser.projectPermissions || {};
|
||||
let authorCount = 0;
|
||||
let reviewerCount = 0;
|
||||
let approverCount = 0;
|
||||
let viewerCount = 0;
|
||||
|
||||
deliverables.forEach((item) => {
|
||||
const role = permissions[item.docId] || (item.author === currentUser.name ? 'author' : 'viewer');
|
||||
if (role === 'author') authorCount++;
|
||||
else if (role === 'reviewer') reviewerCount++;
|
||||
else if (role === 'approver') approverCount++;
|
||||
else viewerCount++;
|
||||
});
|
||||
|
||||
const getRoleBadge = (role: DocPermissionRole) => {
|
||||
switch (role) {
|
||||
case 'author':
|
||||
return {
|
||||
label: '✍️ 编制负责人',
|
||||
color: '#3B82F6',
|
||||
bg: 'rgba(59, 130, 246, 0.15)',
|
||||
desc: '拥有正文就地编辑、向智能体发起修改、认领与送审权限',
|
||||
};
|
||||
case 'reviewer':
|
||||
return {
|
||||
label: '🔍 审核负责人',
|
||||
color: '#F59E0B',
|
||||
bg: 'rgba(245, 158, 11, 0.15)',
|
||||
desc: '文档锁定只读,拥有行内批注、评审通过或驳回权限',
|
||||
};
|
||||
case 'approver':
|
||||
return {
|
||||
label: '👑 批准负责人',
|
||||
color: '#A855F7',
|
||||
bg: 'rgba(168, 85, 247, 0.15)',
|
||||
desc: '终审决策人,拥有最终批准盖章或否决驳回权限',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: '👁️ 查阅者 (只读)',
|
||||
color: '#94A3B8',
|
||||
bg: 'rgba(148, 163, 184, 0.15)',
|
||||
desc: '无该文档编制或评审职责,拥有全局受控版本查阅权',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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: '740px',
|
||||
maxWidth: '95vw',
|
||||
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',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 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: '36px',
|
||||
height: '36px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'rgba(79, 107, 255, 0.15)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--brand-500)',
|
||||
}}
|
||||
>
|
||||
<ShieldCheck size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
当前账号与项目职责权限矩阵
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
|
||||
用户身份与职位全局固定 · 在不同项目与交付物中履行独立法定职责
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--text-muted)',
|
||||
padding: '6px',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Body */}
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
backgroundColor: 'var(--bg-panel, #131820)',
|
||||
}}
|
||||
>
|
||||
{/* User Profile Card */}
|
||||
<div
|
||||
style={{
|
||||
padding: '14px 18px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '46px',
|
||||
height: '46px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--brand-500)',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 600,
|
||||
fontSize: '18px',
|
||||
boxShadow: '0 2px 8px rgba(79, 107, 255, 0.3)',
|
||||
}}
|
||||
>
|
||||
{currentUser.name.slice(0, 1)}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '16px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{currentUser.name}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-muted)',
|
||||
backgroundColor: 'rgba(148, 163, 184, 0.1)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
}}
|
||||
>
|
||||
工号: {currentUser.employeeId || 'SMDF-0821'}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: 'var(--text)',
|
||||
marginTop: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
<Briefcase size={14} color="var(--text-muted)" />
|
||||
<span>{currentUser.department || '机器人与微创内窥镜研发中心'}</span>
|
||||
<span style={{ color: 'var(--border)' }}>·</span>
|
||||
<span style={{ fontWeight: 500, color: 'var(--text-strong)' }}>
|
||||
{currentUser.position || '资深医疗器械方案工程师'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Position Pill */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--success)',
|
||||
backgroundColor: 'var(--success-bg)',
|
||||
border: '1px solid rgba(53, 199, 126, 0.25)',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '6px',
|
||||
textAlign: 'right',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
<div>法定资质有效</div>
|
||||
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>ISO 13485 / GMP 认证</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Project Banner & Stats */}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--text-strong)',
|
||||
marginBottom: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span>当前激活项目:{currentProjectTitle}</span>
|
||||
<span style={{ fontSize: '12px', fontWeight: 400, color: 'var(--text-muted)' }}>
|
||||
交付物总计: {deliverables.length} 份
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats Badges */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '10px' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.25)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: '#60A5FA' }}>待我编制</span>
|
||||
<span style={{ fontSize: '18px', fontWeight: 700, color: '#60A5FA' }}>{authorCount} 份</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.25)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: '#FBBF24' }}>待我审核</span>
|
||||
<span style={{ fontSize: '18px', fontWeight: 700, color: '#FBBF24' }}>{reviewerCount} 份</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'rgba(168, 85, 247, 0.1)',
|
||||
border: '1px solid rgba(168, 85, 247, 0.25)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: '#C084FC' }}>待我批准</span>
|
||||
<span style={{ fontSize: '18px', fontWeight: 700, color: '#C084FC' }}>{approverCount} 份</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'rgba(148, 163, 184, 0.1)',
|
||||
border: '1px solid rgba(148, 163, 184, 0.25)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: '#94A3B8' }}>查阅权限</span>
|
||||
<span style={{ fontSize: '18px', fontWeight: 700, color: '#94A3B8' }}>{viewerCount} 份</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document Responsibilities Table */}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--text-strong)',
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
<FileText size={15} color="var(--brand-500)" />
|
||||
<span>本项目的交付物分工与法定操作权限</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'var(--bg-card, #151B25)',
|
||||
}}
|
||||
>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '12px' }}>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
textAlign: 'left',
|
||||
color: 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
<th style={{ padding: '8px 12px', width: '90px' }}>阶段</th>
|
||||
<th style={{ padding: '8px 12px' }}>受控交付物</th>
|
||||
<th style={{ padding: '8px 12px', width: '180px' }}>您在此文档的法定职责</th>
|
||||
<th style={{ padding: '8px 12px', width: '90px', textAlign: 'center' }}>快捷操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{deliverables.map((item, idx) => {
|
||||
const role =
|
||||
permissions[item.docId] ||
|
||||
(item.author === currentUser.name
|
||||
? 'author'
|
||||
: item.reviewer === currentUser.name
|
||||
? 'reviewer'
|
||||
: 'viewer');
|
||||
const badge = getRoleBadge(role);
|
||||
const isCurrentActive = item.docId === activeDocId;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={item.docId}
|
||||
style={{
|
||||
borderBottom: idx === deliverables.length - 1 ? 'none' : '1px solid var(--border)',
|
||||
backgroundColor: isCurrentActive ? 'rgba(79, 107, 255, 0.08)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--text)' }}>
|
||||
<span
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'rgba(148, 163, 184, 0.1)',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{item.stage}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '10px 12px' }}>
|
||||
<div style={{ fontWeight: 500, color: 'var(--text-strong)' }}>
|
||||
{item.title}
|
||||
{isCurrentActive && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '6px',
|
||||
fontSize: '10px',
|
||||
color: 'var(--brand-500)',
|
||||
backgroundColor: 'rgba(79, 107, 255, 0.15)',
|
||||
padding: '1px 5px',
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
当前打开
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
|
||||
状态: {item.lifecycleState}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '10px 12px' }}>
|
||||
{onRoleChangeForDoc ? (
|
||||
<select
|
||||
value={role}
|
||||
onChange={(e) => onRoleChangeForDoc(item.docId, e.target.value as DocPermissionRole)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
backgroundColor: badge.bg,
|
||||
color: badge.color,
|
||||
fontWeight: 500,
|
||||
fontSize: '11px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title="演练测试:支持切换该文档下的负责角色"
|
||||
>
|
||||
<option value="author">✍️ 编制负责人</option>
|
||||
<option value="reviewer">🔍 审核负责人</option>
|
||||
<option value="approver">👑 批准负责人</option>
|
||||
<option value="viewer">👁️ 查阅者 (只读)</option>
|
||||
</select>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '3px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: badge.bg,
|
||||
color: badge.color,
|
||||
fontWeight: 500,
|
||||
fontSize: '11px',
|
||||
}}
|
||||
title={badge.desc}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '10px 12px', textAlign: 'center' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
onSelectDoc(item.docId);
|
||||
onClose();
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
color: 'var(--text)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
<span>前往</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Explanatory Tip */}
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'rgba(79, 107, 255, 0.08)',
|
||||
border: '1px solid rgba(79, 107, 255, 0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '8px',
|
||||
fontSize: '11px',
|
||||
color: 'var(--text)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
<Info size={14} color="var(--brand-500)" style={{ flexShrink: 0, marginTop: '2px' }} />
|
||||
<div>
|
||||
<strong>权限与操作联动说明:</strong>
|
||||
当您切换查看某份文档时,右侧工作栏将实时根据您在该文档上的法定职责赋予对应权限——如在《注册评估》中作为编制人可就地编辑与送审;在《竞品调研》中作为审核人则只读锁定并开放批注与通过/驳回动作。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{allUsers && allUsers.length > 0 ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>切换当前账号测试隔离:</span>
|
||||
{allUsers.map((u) => {
|
||||
const isCurrent = u.id === currentUser.id;
|
||||
return (
|
||||
<button
|
||||
key={u.id}
|
||||
onClick={() => onSwitchUser?.(u)}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: '4px',
|
||||
border: isCurrent ? '1px solid var(--brand-500)' : '1px solid var(--border)',
|
||||
backgroundColor: isCurrent ? 'var(--brand-500)' : 'var(--bg-panel, #131820)',
|
||||
color: isCurrent ? '#fff' : 'var(--text)',
|
||||
fontSize: '11px',
|
||||
fontWeight: isCurrent ? 600 : 400,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
{u.name} ({u.position.slice(0, 4)})
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : <div />}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'var(--brand-500)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
关闭面板
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user