diff --git a/web/src/App.tsx b/web/src/App.tsx index 5f103498..7bf2c6fe 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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('author'); - const [currentUserName, setCurrentUserName] = useState('张工'); - // 交付物状态大盘 + // 当前用户身份 (职位在全公司唯一不变,但在不同文档有不同法定职责) + const [currentUser, setCurrentUser] = useState(MOCK_USERS[0]); + + // 全生命周期完整交付物大盘清单 (含工作流受控、项目协作共享、个人私密生成三大分类) const [deliverables, setDeliverables] = useState([ { 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(() => - 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('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>(() => { + const initialCache: Record = {}; + 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(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 ( - { - 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={ -
- - {pendingProposal && ( -
- setPendingProposal(null)} + <> + { + 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={ +
+ + {pendingProposal && ( +
+ setPendingProposal(null)} + /> +
+ )} + +
+ } + docContent={ +
+ {/* 36px 极简 Tab 条 (含三分类分段切换与私密/共享识别) */} + setDeckDrawerOpen(!deckDrawerOpen)} + onExportDocx={handleExportDocx} + /> + + {/* 全景大盘抽屉 (支持受控/共享/私密三板块与一键流转操作) */} + setDeckDrawerOpen(false)} + deliverables={visibleDeliverables} + activeDocId={activeDocId} + currentUserId={currentUser.id} + onSelectDoc={handleSelectDoc} + onShareToProject={handleShareToProject} + onRevokeShare={handleRevokeShare} + onDeleteDoc={handleDeleteDoc} + /> + + {/* 块状文档渲染器 */} + { + handleSendMessage( + `针对受控文档《${docAST.title}》中的条款内容:\n\n“${snippet.slice(0, 150)}${snippet.length > 150 ? '...' : ''}”\n\n请帮我审查其合规性、技术可行性或提出修改建议。` + ); + }} + /> + + {/* 底部法定流转动作条 (根据受控/共享/私密形态自适应展示) */} + + + {/* 块就地编辑弹窗 (支持富文本工具栏、多维表格与未保存拦截) */} + {editingBlock && ( + setEditingBlock(null)} /> -
- )} - -
- } - docContent={ -
- {/* 36px 极简 Tab 条 */} - setDeckDrawerOpen(!deckDrawerOpen)} - onExportDocx={handleExportDocx} - /> + )} - {/* 全景大盘抽屉 */} - setDeckDrawerOpen(false)} - deliverables={deliverables} - activeDocId={activeDocId} - onSelectDoc={setActiveDocId} - /> + {/* 审核批注侧栏 */} + {commentingBlock && ( + setCommentingBlock(null)} + /> + )} +
+ } + /> - {/* 块状文档渲染器 */} - + {/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 (支持快速模拟账号切换) */} + 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} + /> - {/* 底部法定流转动作条 */} - - - {/* 块就地编辑弹窗 */} - {editingBlock && ( - setEditingBlock(null)} - /> - )} - - {/* 审核批注侧栏 */} - {commentingBlock && ( - setCommentingBlock(null)} - /> - )} -
- } - /> + {/* 预留系统设置与管理员后台弹窗 */} + setSettingsModalOpen(false)} + /> + ); } diff --git a/web/src/components/doc/DeckDrawer.tsx b/web/src/components/doc/DeckDrawer.tsx index 0ca89e3d..a288174f 100644 --- a/web/src/components/doc/DeckDrawer.tsx +++ b/web/src/components/doc/DeckDrawer.tsx @@ -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 = ({ @@ -16,8 +34,24 @@ export const DeckDrawer: React.FC = ({ 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 = ({ switch (state) { case 'approved': return ( - + 终审已生效 ); case 'reviewing': return ( - + 审核中 (已锁定) ); case 'approving': return ( - + 批准中 ); case 'rejected': return ( - + 被驳回需修改 @@ -55,7 +137,19 @@ export const DeckDrawer: React.FC = ({ case 'drafting': default: return ( - + 编制中 {isClaimed ? '(已认领)' : '(未认领)'} @@ -63,143 +157,522 @@ export const DeckDrawer: React.FC = ({ } }; - // 按阶段分组 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 ( -
- {/* 抽屉顶栏 */} + <> + {/* 外部遮罩 */}
-
- - 当前项目阶段交付物台账大盘 - - - (共 {deliverables.length} 份受控交付物) - -
- -
+ /> - {/* 交付物卡片列表 */}
- {stages.map((stg) => { - const docsInStage = deliverables.filter((d) => d.stage === stg.stage); - if (docsInStage.length === 0) return null; - - return ( -
-
- {stg.title} -
- -
- {docsInStage.map((doc) => { - const isActive = doc.docId === activeDocId; - return ( -
{ - 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)'; - }} - > -
- - {doc.title} - - {renderBadge(doc.lifecycleState, doc.isClaimed)} -
- -
- 编制: {doc.author} - 审核: {doc.reviewer} - 批准: {doc.approver} -
- - {doc.summary && ( -
- {doc.summary} -
- )} -
- ); - })} -
+ {/* 抽屉顶栏 */} +
+
+
+ + + 交付物分类台账全景大盘 + + + (共 {deliverables.length} 份可用交付物) +
- ); - })} + + {/* 大盘内分类筛选切换 */} +
+ + + + +
+
+ + +
+ + {/* 交付物大盘内容区 */} +
+ {/* 板块一:🏛️ 研发体系预设受控交付物 */} + {(filterCategory === 'all' || filterCategory === 'workflow') && ( +
+
+ + + 研发体系预设受控交付物 (固定流程 · 严格审签) + + + 共 {workflowDocs.length} 份受控文档 + +
+ + {stages.map((stg) => { + const docsInStage = workflowDocs.filter((d) => d.stage === stg.stage); + if (docsInStage.length === 0) return null; + + return ( +
+
+ {stg.title} +
+ +
+ {docsInStage.map((doc) => { + const isActive = doc.docId === activeDocId; + return ( +
{ + 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', + }} + > +
+ + {doc.title} + + {renderBadge(doc.lifecycleState, doc.isClaimed)} +
+ +
+ 编制: {doc.author} + 审核: {doc.reviewer} + 批准: {doc.approver} +
+ + {doc.summary && ( +
+ {doc.summary} +
+ )} +
+ ); + })} +
+
+ ); + })} +
+ )} + + {/* 板块二:🌐 项目协作参考资料库 */} + {(filterCategory === 'all' || filterCategory === 'shared') && ( +
+
+ + + 项目协作参考资料库 (团队共享 · 方案推演) + + + 共 {sharedDocs.length} 份共享参考材料 + +
+ + {sharedDocs.length === 0 ? ( +
+ 当前暂无团队成员共享的参考资料。个人私密生成物成熟后可一键共享至此处。 +
+ ) : ( +
+ {sharedDocs.map((doc) => { + const isActive = doc.docId === activeDocId; + const isCreator = doc.creatorId === currentUserId; + + return ( +
{ + 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', + }} + > +
+ + {doc.title} + + + + 项目共享 + +
+ +
+ 共享人: {doc.creatorName} + 共享时间: {doc.sharedAt || doc.updatedAt} +
+ + {doc.summary && ( +
+ {doc.summary} +
+ )} + + {/* 共享人专属撤回操作 */} + {isCreator && onRevokeShare && ( +
+ +
+ )} +
+ ); + })} +
+ )} +
+ )} + + {/* 板块三:🔒 我的私密生成档案 */} + {(filterCategory === 'all' || filterCategory === 'private') && ( +
+
+ + + 我的私密生成档案 (仅您本人可见 · 商业与推演隐私) + + + 共 {privateDocs.length} 份私有草稿 + +
+ + {privateDocs.length === 0 ? ( +
+ 您当前未生成私密文档。在中间栏向智能体提出测算、草拟需求时,生成的文档将自动归档于此处。 +
+ ) : ( +
+ {privateDocs.map((doc) => { + const isActive = doc.docId === activeDocId; + + return ( +
{ + 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', + }} + > +
+ + {doc.title} + + + + 仅我可见 + +
+ +
+ 创建人: 本人 ({doc.creatorName}) + 生成时间: {doc.updatedAt} +
+ + {doc.summary && ( +
+ {doc.summary} +
+ )} + +
+ {onShareToProject && ( + + )} + + {onDeleteDoc && ( + + )} +
+
+ ); + })} +
+ )} +
+ )} +
-
+ ); }; diff --git a/web/src/components/doc/DocGovActionBar.tsx b/web/src/components/doc/DocGovActionBar.tsx index 6ffc51d3..a3cbf3aa 100644 --- a/web/src/components/doc/DocGovActionBar.tsx +++ b/web/src/components/doc/DocGovActionBar.tsx @@ -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 = ({ 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 = ({ setRejectModalOpen(false); }; + const isPrivate = ast.category === 'private'; + const isShared = ast.category === 'shared'; + const isWorkflow = !isPrivate && !isShared; + return (
= ({
@@ -120,9 +148,11 @@ export const DocGovActionBar: React.FC = ({ 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 = ({
)} - {/* 左侧状态标识 */} -
- - 当前生命周期: - - - {ast.lifecycleState === 'approved' - ? '已终审生效' - : ast.lifecycleState === 'reviewing' - ? '审核中' - : ast.lifecycleState === 'approving' - ? '批准中' - : '编制中'} - - {ast.isClaimed && ( - - • 已被编制人认领 - - )} -
- - {/* 右侧动作流转按钮 */} -
- {/* 1. 编制阶段 */} - {ast.lifecycleState === 'drafting' && ( - <> - {currentRole === 'author' ? ( - <> - {!ast.isClaimed ? ( - - ) : null} - - - - ) : ( - - 等待编制负责人({ast.author})完成初稿并提交送审 - - )} - - )} - - {/* 2. 审核阶段 */} - {ast.lifecycleState === 'reviewing' && ( - <> - - 文档已锁定 + {/* 场景 1: 个人私密文档专属条 */} + {isPrivate && ( + <> +
+ + + 个人私密草稿 + + + 由助手针对您的对话需求生成,仅您本人可见。不参与项目受控审签体系。 - {currentRole === 'reviewer' ? ( - <> - - - - ) : ( - - 等待审核负责人({ast.reviewer})完成审核签署 - - )} - - )} - - {/* 3. 批准阶段 */} - {ast.lifecycleState === 'approving' && ( - <> - {currentRole === 'approver' ? ( - <> - - - - ) : ( - - 等待批准负责人({ast.approver})终审批准发布 - - )} - - )} - - {/* 4. 已批准终态 */} - {ast.lifecycleState === 'approved' && ( -
- - 受控归档交付物 (已沉淀为全局标杆经验)
- )} -
+ +
+ {onShareToProject && ( + + )} +
+ + )} + + {/* 场景 2: 项目协作参考资料专属条 */} + {isShared && ( + <> +
+ + + 项目协作参考资料 + + + 共享人: {ast.creatorName || ast.author} + {ast.sharedAt ? ` (${ast.sharedAt})` : ''} · 项目全员可查阅、引用与批注 + +
+ +
+ {/* 仅创建者本人有权取消共享 */} + {ast.creatorId === currentUserId && onRevokeShare && ( + + )} +
+ + )} + + {/* 场景 3: 研发体系预设受控交付物 (严格三权分立审签) */} + {isWorkflow && ( + <> + {/* 左侧状态标识 */} +
+ + + 受控状态: + + + {ast.lifecycleState === 'approved' + ? '已终审生效' + : ast.lifecycleState === 'reviewing' + ? '审核中' + : ast.lifecycleState === 'approving' + ? '批准中' + : '编制中'} + + {ast.isClaimed && ( + + • 已被编制人认领 + + )} +
+ + {/* 右侧动作流转按钮 */} +
+ {/* 1. 编制阶段 */} + {ast.lifecycleState === 'drafting' && ( + <> + {currentRole === 'author' ? ( + <> + {!ast.isClaimed ? ( + + ) : null} + + + + ) : ( + + 等待编制负责人({ast.author})完成初稿并提交送审 + + )} + + )} + + {/* 2. 审核阶段 */} + {ast.lifecycleState === 'reviewing' && ( + <> + + 文档已锁定 + + {currentRole === 'reviewer' ? ( + <> + + + + ) : ( + + 等待审核负责人({ast.reviewer})完成审核签署 + + )} + + )} + + {/* 3. 批准阶段 */} + {ast.lifecycleState === 'approving' && ( + <> + {currentRole === 'approver' ? ( + <> + + + + ) : ( + + 等待批准负责人({ast.approver})终审批准发布 + + )} + + )} + + {/* 4. 已批准终态 */} + {ast.lifecycleState === 'approved' && ( +
+ + 受控归档交付物 (已沉淀为全局标杆经验) +
+ )} +
+ + )}
); }; diff --git a/web/src/components/doc/SlimTabBar.tsx b/web/src/components/doc/SlimTabBar.tsx index f7a42ee6..b6f7b03e 100644 --- a/web/src/components/doc/SlimTabBar.tsx +++ b/web/src/components/doc/SlimTabBar.tsx @@ -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 = ({ deliverables, activeDocId, + activeCategory, + onSelectCategory, onSelectDoc, onToggleDeckDrawer, onExportDocx, @@ -38,6 +42,20 @@ export const SlimTabBar: React.FC = ({ } }; + 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 (
= ({ display: 'flex', alignItems: 'center', padding: '0 8px', - gap: '4px', + gap: '8px', overflowX: 'auto', flexShrink: 0, userSelect: 'none', }} > - {/* 交付物 Tabs */} -
- {deliverables.map((doc) => { - const isActive = doc.docId === activeDocId; - const status = getStatusDot(doc.lifecycleState); + {/* 分类切换分段 Pill */} +
+ - return ( -
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})`} - > + + + +
+ +
+ + {/* 交付物 Tabs (当前分类下的文档) */} +
+ {visibleTabs.length === 0 ? ( +
+ {activeCategory === 'private' + ? '暂无个人私密生成文档 (在对话中要求助手生成即可)' + : activeCategory === 'shared' + ? '暂无项目共享参考资料' + : '暂无流程文档'} +
+ ) : ( + visibleTabs.map((doc) => { + const isActive = doc.docId === activeDocId; + const status = getStatusDot(doc.lifecycleState); + + return (
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', }} - /> - - {doc.title} - -
- ); - })} + 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' ? ( + + ) : doc.category === 'shared' ? ( + + ) : ( +
+ )} + + {doc.title} + +
+ ); + }) + )}
{/* 右侧全局操作区 */} @@ -122,11 +274,12 @@ export const SlimTabBar: React.FC = ({ display: 'flex', alignItems: 'center', gap: '4px', + cursor: 'pointer', }} title="展开当前项目各阶段全部交付物状态大盘" > - 全部交付物 ({deliverables.length}) ▾ + 全景大盘 ({deliverables.length}) ▾ {/* 导出受控 Word 按钮 */} @@ -145,11 +298,12 @@ export const SlimTabBar: React.FC = ({ display: 'flex', alignItems: 'center', gap: '4px', + cursor: 'pointer', }} title="导出为符合严肃合规模板的受控 DOCX 交付物" > - 导出受控 DOCX + 导出 DOCX )} @@ -161,10 +315,13 @@ export const SlimTabBar: React.FC = ({ 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 ? '退出全屏' : '全屏沉浸阅读与编辑'} > diff --git a/web/src/components/user/UserProjectRightsModal.tsx b/web/src/components/user/UserProjectRightsModal.tsx new file mode 100644 index 00000000..e800eaef --- /dev/null +++ b/web/src/components/user/UserProjectRightsModal.tsx @@ -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 = ({ + 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 ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
+ {/* Header */} +
+
+
+ +
+
+
+ 当前账号与项目职责权限矩阵 +
+
+ 用户身份与职位全局固定 · 在不同项目与交付物中履行独立法定职责 +
+
+
+ +
+ + {/* Content Body */} +
+ {/* User Profile Card */} +
+
+
+ {currentUser.name.slice(0, 1)} +
+
+
+ + {currentUser.name} + + + 工号: {currentUser.employeeId || 'SMDF-0821'} + +
+
+ + {currentUser.department || '机器人与微创内窥镜研发中心'} + · + + {currentUser.position || '资深医疗器械方案工程师'} + +
+
+
+ + {/* Global Position Pill */} +
+
法定资质有效
+
ISO 13485 / GMP 认证
+
+
+ + {/* Active Project Banner & Stats */} +
+
+ 当前激活项目:{currentProjectTitle} + + 交付物总计: {deliverables.length} 份 + +
+ + {/* Quick Stats Badges */} +
+
+ 待我编制 + {authorCount} 份 +
+
+ 待我审核 + {reviewerCount} 份 +
+
+ 待我批准 + {approverCount} 份 +
+
+ 查阅权限 + {viewerCount} 份 +
+
+
+ + {/* Document Responsibilities Table */} +
+
+ + 本项目的交付物分工与法定操作权限 +
+ +
+ + + + + + + + + + + {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 ( + + + + + + + ); + })} + +
阶段受控交付物您在此文档的法定职责快捷操作
+ + {item.stage} + + +
+ {item.title} + {isCurrentActive && ( + + 当前打开 + + )} +
+
+ 状态: {item.lifecycleState} +
+
+ {onRoleChangeForDoc ? ( + + ) : ( + + {badge.label} + + )} + + +
+
+
+ + {/* Explanatory Tip */} +
+ +
+ 权限与操作联动说明: + 当您切换查看某份文档时,右侧工作栏将实时根据您在该文档上的法定职责赋予对应权限——如在《注册评估》中作为编制人可就地编辑与送审;在《竞品调研》中作为审核人则只读锁定并开放批注与通过/驳回动作。 +
+
+
+ + {/* Footer */} +
+ {allUsers && allUsers.length > 0 ? ( +
+ 切换当前账号测试隔离: + {allUsers.map((u) => { + const isCurrent = u.id === currentUser.id; + return ( + + ); + })} +
+ ) :
} + + +
+
+
+ ); +};