Files
Agent-System/web/src/App.tsx
T

1571 lines
58 KiB
TypeScript

import { useState, useMemo, useEffect } from 'react';
import { AppShell } from './components/layout/AppShell';
import { MessageStream } from './components/chat/MessageStream';
import { InputArea } from './components/chat/InputArea';
import { SlimTabBar } from './components/doc/SlimTabBar';
import { DeckDrawer } from './components/doc/DeckDrawer';
import { BlockRenderer } from './components/doc/BlockRenderer';
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 { UserSettingsModal } from './components/user/UserSettingsModal';
import { AdminSettingsModal } from './components/admin/AdminSettingsModal';
import { DocVisibilityModal } from './components/doc/DocVisibilityModal';
import { LoginView } from './components/auth/LoginView';
import { FirstLoginPasswordModal } from './components/auth/FirstLoginPasswordModal';
import { DevRoleSwitcher } from './components/dev/DevRoleSwitcher';
import { authClient } from './api/auth';
import { LoginResult, SystemAuthConfig } from './types/auth';
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,
DeliverableCategory,
InteractiveCardData,
} from './types/workbench';
import { UserRole, UserProfile, DocPermissionRole } from './types/docgov';
import { RoleDefinition, DocVisibilityRule } from './types/rbac';
import {
DEFAULT_ROLES,
hasPermission,
filterVisibleDeliverablesWithRBAC,
} from './engine/rbac';
import { MOCK_DOCUMENTS } from './data/mockDocuments';
import { exportDocumentToDocx } from './engine/docxRenderer';
import { exportDocumentToXlsx } from './engine/xlsxRenderer';
import {
findMatchingBlockForReview,
createRejectConfirmationCard,
isAffirmativeResponse,
findLastActiveCard,
decrementActiveCardsTtl,
applyConfirmRejectToDoc,
applyUndoRejectToDoc,
resolveCardInMessages,
cancelCardInMessages,
} from './engine/cardActionEngine';
import { createProjectTemplateDeliverables } from './engine/templateFactory';
import {
loadSessions,
saveSessions,
loadActiveSessionId,
saveActiveSessionId,
loadMessages,
saveMessages,
loadDeliverables,
saveDeliverables,
loadRoles,
saveRoles,
loadUsers,
saveUsers,
} from './data/storage';
export const INITIAL_DELIVERABLES: DeliverableItem[] = [
{
docId: 'doc-001',
projectId: 1,
taskKey: 'feasibility_reg',
title: '4K荧光内窥镜立项与可行性报告',
stage: 'V0.5',
category: 'workflow',
creatorId: 1,
creatorName: '张工',
isFixedWorkflow: true,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
author: '张工',
reviewer: '李主管',
approver: '王总监',
updatedAt: '2026-09-08 14:15',
summary: '针对4K荧光腹腔镜分类界定、临床评价路径与法规标准准入全景评估。',
},
{
docId: 'doc-002',
projectId: 1,
taskKey: 'research',
title: '微创外科全球竞品深度调研',
stage: 'V0.5',
category: 'workflow',
creatorId: 1,
creatorName: '张工',
isFixedWorkflow: true,
isTemplateOnly: false,
lifecycleState: 'reviewing',
isClaimed: true,
author: '张工',
reviewer: '李主管',
approver: '王总监',
updatedAt: '2026-09-08 11:30',
summary: '针对史赛克1688、卡尔史托斯Rubina荧光内窥镜性能全方位对比分析。',
},
{
docId: 'doc-003',
projectId: 1,
taskKey: 'prelim_design',
title: '光学分光与电路初步设计规格书',
stage: 'V0.5',
category: 'workflow',
creatorId: 1,
creatorName: '张工',
isFixedWorkflow: true,
isTemplateOnly: false,
lifecycleState: 'approving',
isClaimed: true,
author: '张工',
reviewer: '李主管',
approver: '王总监',
updatedAt: '2026-09-07 17:00',
summary: '4K荧光双路分光棱镜光路设计与近红外消色差物镜参数设计。',
},
{
docId: 'doc-004',
projectId: 1,
taskKey: 'cdo_quote',
title: 'CDO商业报价单与工时清单',
stage: 'V1.0',
category: 'workflow',
creatorId: 4,
creatorName: '王商务',
isFixedWorkflow: true,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
author: '王商务',
reviewer: '张工',
approver: '李总',
updatedAt: '2026-09-08 15:30',
summary: '基于WBS千工时分解与CDO成本模型的真实商业报价核算。',
},
{
docId: 'doc-shared-001',
projectId: 1,
taskKey: 'supply_chain_analysis',
title: '4K双路CMOS供应链与成本灵敏度分析',
stage: 'V0.5',
category: 'shared',
creatorId: 1,
creatorName: '张工',
sharedAt: '2026-09-08 15:20',
isFixedWorkflow: false,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
author: '张工',
reviewer: '李主管',
approver: '王总监',
updatedAt: '2026-09-08 15:20',
summary: '张工在与助手对话中推演的 Sony IMX485 与国产长光辰芯双路 CMOS 传感器成本灵敏度测算。',
},
{
docId: 'doc-private-001',
projectId: 1,
taskKey: 'negotiation_memo',
title: '微创医疗战略商务谈判底线与预付款让利推演备忘',
stage: 'V1.0',
category: 'private',
creatorId: 1,
creatorName: '张工',
isFixedWorkflow: false,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
author: '张工',
reviewer: '李主管',
approver: '王总监',
updatedAt: '2026-09-08 16:10',
summary: '张工个人与助手对话进行的商务谈判底线、毛利率红线测算草案,含个人思考,仅本人可见。',
},
{
docId: 'doc-private-002',
projectId: 1,
taskKey: 'lead_capacity_memo',
title: '团队绩效核算与阶段资源饱和度评估',
stage: 'V0.5',
category: 'private',
creatorId: 2,
creatorName: '李主管',
isFixedWorkflow: false,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
author: '李主管',
reviewer: '李主管',
approver: '王总监',
updatedAt: '2026-09-08 15:50',
summary: '李主管个人对话生成的团队工时饱和度与绩效预算底稿,仅李主管可见。',
},
];
// 为默认第二项目 (便携式超声探头,projectId: 2) 预置全套 7 份生命周期模板副本
const P2_INITIAL = createProjectTemplateDeliverables({
projectId: 2,
projectName: '便携式掌上无线超声诊断仪',
deviceType: '便携式超声探头',
creatorId: 1,
creatorName: '张工',
});
export const ALL_INITIAL_DELIVERABLES: DeliverableItem[] = [
...INITIAL_DELIVERABLES,
...P2_INITIAL.deliverables,
];
export const MOCK_USERS: UserProfile[] = [
{
id: 1,
name: '张工',
employeeId: 'SMDF-0821',
department: '机器人与微创内窥镜研发中心',
position: '资深医疗器械方案工程师',
roleId: 'role-engineer',
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: '研发项目技术主管',
roleId: 'role-tech-lead',
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: '微创器械研发总监',
roleId: 'role-director',
projectPermissions: {
'doc-001': 'approver',
'doc-002': 'approver',
'doc-003': 'author',
'doc-004': 'approver',
'doc-shared-001': 'approver',
},
},
{
id: 4,
name: '赵管理员',
employeeId: 'SMDF-0001',
department: '系统安全与技术委员会',
position: '系统全域管理总管',
roleId: 'role-admin',
projectPermissions: {
'doc-001': 'approver',
'doc-002': 'approver',
'doc-003': 'approver',
},
},
{
id: 5,
name: '钱审计',
employeeId: 'SMDF-9901',
department: '第三方合规审计与认证机构',
position: '外部合规审查专员',
roleId: 'role-guest',
projectPermissions: {},
},
];
export default function App() {
// 会话与用户状态 (支持 localStorage 刷新持久化)
const [sessions, setSessions] = useState<ProjectSession[]>(() => loadSessions());
const [activeSessionId, setActiveSessionId] = useState<string>(() => loadActiveSessionId('sess-001'));
// 自动持久化 sessions 与 activeSessionId
useEffect(() => {
saveSessions(sessions);
}, [sessions]);
useEffect(() => {
saveActiveSessionId(activeSessionId);
}, [activeSessionId]);
// 全局角色库与人员名单状态 (支持动态增删改与持久化)
const [roles, setRoles] = useState<RoleDefinition[]>(() => loadRoles(DEFAULT_ROLES));
const [allUsers, setAllUsers] = useState<UserProfile[]>(() => loadUsers(MOCK_USERS));
// 自动持久化 roles 与 allUsers
useEffect(() => {
saveRoles(roles);
}, [roles]);
useEffect(() => {
saveUsers(allUsers);
}, [allUsers]);
// 当前用户身份 (职位在全公司唯一不变,绑定全局角色,但在不同文档有不同法定职责)
const [currentUser, setCurrentUser] = useState<UserProfile>(() => allUsers[0] || MOCK_USERS[0]);
// 全生命周期完整交付物大盘清单 (含工作流受控、项目协作共享、个人私密生成三大分类,支持持久化)
const [deliverables, setDeliverables] = useState<DeliverableItem[]>(() =>
loadDeliverables(ALL_INITIAL_DELIVERABLES)
);
// 自动持久化 deliverables
useEffect(() => {
saveDeliverables(deliverables);
}, [deliverables]);
const [activeCategory, setActiveCategory] = useState<DeliverableCategory>('workflow');
const [activeDocId, setActiveDocId] = useState('doc-001');
const [deckDrawerOpen, setDeckDrawerOpen] = useState(false);
// 认证与会话状态
const [session, setSession] = useState<LoginResult | null>(() => authClient.loadSavedSession());
const [systemEnv, setSystemEnv] = useState<string>('dev');
const [systemAuthConfig, setSystemAuthConfig] = useState<SystemAuthConfig>({
allowRegistration: true,
requireApproval: false,
});
// 模态浮层控制
const [rightsModalOpen, setRightsModalOpen] = useState(false);
const [userSettingsOpen, setUserSettingsOpen] = useState(false);
const [adminSettingsOpen, setAdminSettingsOpen] = useState(false);
const [docVisibilityModalOpen, setDocVisibilityModalOpen] = useState(false);
const [targetDocForVisibility, setTargetDocForVisibility] = useState<DeliverableItem | null>(null);
useEffect(() => {
authClient.getSystemAuthConfig().then(setSystemAuthConfig).catch(() => {});
const saved = authClient.loadSavedSession();
if (saved) {
setSession(saved);
if (saved.env) setSystemEnv(saved.env);
const match = allUsers.find((u) => u.id === saved.user.id);
if (match) {
setCurrentUser(match);
} else {
setCurrentUser(saved.user);
}
}
}, []);
const handleLogout = () => {
authClient.logout();
setSession(null);
};
// 核心功能权限计算
const canCreateProject = useMemo(() => {
return hasPermission(currentUser, roles, 'project:create');
}, [currentUser, roles]);
const canAssignLead = useMemo(() => {
return hasPermission(currentUser, roles, 'project:assign_lead');
}, [currentUser, roles]);
const canOverrideVisibility = useMemo(() => {
return (
hasPermission(currentUser, roles, 'doc:override_visibility') ||
hasPermission(currentUser, roles, 'perm:manage')
);
}, [currentUser, roles]);
// 获取当前激活会话的项目ID (默认为 1)
const currentProjectId = useMemo(() => {
const activeSess = sessions.find((s) => s.sessionId === activeSessionId);
return activeSess ? activeSess.projectId : 1;
}, [sessions, activeSessionId]);
// 严格按 projectId 隔离当前项目的交付物
const currentProjectDeliverables = useMemo(() => {
return deliverables.filter((doc) => doc.projectId === currentProjectId);
}, [deliverables, currentProjectId]);
// 基于当前登录用户与 RBAC 规则过滤可见交付物 (私密文档严格按 creatorId 隔离,单文档覆盖优先于工作流基线)
const visibleDeliverables = useMemo(() => {
return filterVisibleDeliverablesWithRBAC(currentProjectDeliverables, currentUser, roles);
}, [currentProjectDeliverables, currentUser, roles]);
// 单文档可见性覆盖保存处理
const handleSaveDocVisibilityRule = (docId: string, rule: DocVisibilityRule | undefined) => {
setDeliverables((prev) =>
prev.map((d) => (d.docId === docId ? { ...d, visibilityRule: rule } : d))
);
setDocCache((prev) => {
if (!prev[docId]) return prev;
return {
...prev,
[docId]: {
...prev[docId],
visibilityRule: rule,
},
};
});
};
// 当用户切换或可见列表变动时,自动校准激活文档
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> = { ...P2_INITIAL.docASTMap };
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] ||
(visibleDeliverables[0] && docCache[visibleDeliverables[0].docId]) ||
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);
const [commentingBlock, setCommentingBlock] = useState<DocumentBlock | null>(null);
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(null);
const [pendingProposal, setPendingProposal] = useState<{
changes: string;
impact: string;
risk: string;
patchBlockId?: string;
} | null>(null);
// 消息流与思考脉冲 (支持 localStorage 持久化与项目会话严格隔离)
const [isThinking, setIsThinking] = useState(false);
const [messages, setMessages] = useState<ChatMessage[]>(() => loadMessages());
// 自动持久化消息流
useEffect(() => {
saveMessages(messages);
}, [messages]);
// 基于当前激活的项目会话严格隔离对话记录
const currentSessionMessages = useMemo(() => {
return messages.filter((m) => m.sessionId === activeSessionId);
}, [messages, activeSessionId]);
// 处理交互卡片动作流转 (驳回门禁确认、撤回、取消与终审联动)
const handleCardAction = (cardId: string, actionId: string, payload?: any) => {
// 查找对应的卡片
let targetCard: InteractiveCardData | undefined;
for (const m of messages) {
if (m.interactiveCard && m.interactiveCard.cardId === cardId) {
targetCard = m.interactiveCard;
break;
}
}
const targetDocId =
payload?.targetDocId || payload?.docId || targetCard?.targetDocId || activeDocId;
const reason =
payload?.reason || '关键竞品性能指标与电气安全指标待补充,退回编制人重新修订。';
if (actionId === 'confirm_reject') {
// 1. 调用既有文档驳回逻辑 (将目标文档生命周期由 reviewing 回退为 drafting)
setDeliverables((prev) =>
prev.map((d) => (d.docId === targetDocId ? { ...d, lifecycleState: 'drafting' } : d))
);
// 2. 扫描目标文档 AST,在匹配块上追加 type: 'issue' 批注
let targetBlockId: string | null = null;
let targetSectionName = '第3节';
setDocCache((prev) => {
let currentDoc = prev[targetDocId];
if (!currentDoc && MOCK_DOCUMENTS[targetDocId]) {
const meta = MOCK_DOCUMENTS[targetDocId];
currentDoc = 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,
});
}
if (!currentDoc) return prev;
const { updatedDoc, matchingBlock } = applyConfirmRejectToDoc(
currentDoc,
cardId,
currentUser.name,
reason,
targetCard?.sectionTitle || payload?.targetSection
);
targetBlockId = matchingBlock.id;
if (matchingBlock.numbering) {
targetSectionName = `第${matchingBlock.numbering.replace('.', '')}节`;
}
return {
...prev,
[targetDocId]: updatedDoc,
};
});
// 3. 更新卡片状态为 resolved
const sectionLabel =
targetCard?.sectionTitle?.match(/第[0-9一二三四五六七八九]+节/)?.[0] || targetSectionName || '第3节';
const resolutionSummary = `已正式退回编制阶段,批注已挂载至${sectionLabel}`;
setMessages((prev) => {
const updatedMessages = resolveCardInMessages(
prev,
cardId,
resolutionSummary,
currentUser.name
);
return [
...updatedMessages,
{
id: 'sys-rej-' + Date.now(),
sessionId: activeSessionId,
role: 'system',
content: `⚠️ ${currentUser.name}(审核负责人)驳回了《${targetCard?.targetDocTitle || targetDocId}》,已退回编制阶段。驳回原因:${reason}`,
timestamp: new Date().toTimeString().substring(0, 5),
},
];
});
// 4. 切换当前右侧激活文档为目标文档并高亮匹配块
setActiveDocId(targetDocId);
const targetDeliverable = deliverables.find((d) => d.docId === targetDocId);
if (targetDeliverable) {
setActiveCategory(targetDeliverable.category);
}
if (targetBlockId) {
setHighlightedBlockId(targetBlockId);
setTimeout(() => {
setHighlightedBlockId(null);
}, 2500);
}
} else if (actionId === 'cancel') {
// 标记卡片为 cancelled 并追加说明
setMessages((prev) => [
...cancelCardInMessages(prev, cardId),
{
id: 'ast-cancel-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🚫 取消操作',
content: '已取消驳回操作,文档保持待审核状态。',
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
} else if (actionId === 'undo') {
// 撤销驳回:恢复为待审核状态 (reviewing)
setDeliverables((prev) =>
prev.map((d) => (d.docId === targetDocId ? { ...d, lifecycleState: 'reviewing' } : d))
);
// 移除注入的批注并恢复 reviewing 状态
setDocCache((prev) => {
const currentDoc = prev[targetDocId];
if (!currentDoc) return prev;
const updatedDoc = applyUndoRejectToDoc(currentDoc, cardId);
return {
...prev,
[targetDocId]: updatedDoc,
};
});
// 标记卡片为 cancelled 并追加说明
setMessages((prev) => [
...cancelCardInMessages(prev, cardId),
{
id: 'ast-undo-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '↩️ 撤回驳回',
content: '已在合法窗口期撤回驳回,文档已恢复为待审核状态。',
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
} else if (actionId === 'confirm_final_approve') {
// 终审批准
setDeliverables((prev) =>
prev.map((d) => (d.docId === targetDocId ? { ...d, lifecycleState: 'approved' } : d))
);
setDocCache((prev) => {
const currentDoc = prev[targetDocId];
if (!currentDoc) return prev;
return {
...prev,
[targetDocId]: { ...currentDoc, lifecycleState: 'approved' as const },
};
});
setMessages((prev) => [
...resolveCardInMessages(
prev,
cardId,
'已终审批准生效',
currentUser.name
),
{
id: 'appr-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🏆 终审批准生效',
content: `🎉 祝贺!${currentUser.name}(批准人)已终审批准《${targetCard?.targetDocTitle || targetDocId}》!系统已自动对该受控交付物进行微粒度章节切片与向量化,成功注入全局标杆案例经验库!`,
timestamp: new Date().toTimeString().substring(0, 5),
recommendedCases: [
{
docId: targetDocId,
taskKey: targetCard?.targetDocId || 'research',
artifactTitle: targetCard?.targetDocTitle || '受控交付物',
sectionTitle: '终审已生效标杆案例',
similarityScore: 1.0,
keyRegulations: ['GB 9706.1-2020', 'YY 0569-2011', 'IEC 60601-1-2'],
summarySnippet: '本交付物已通过编制、审核、批准法定三级签署闭环,作为往期标杆经验向全平台开放自推荐。',
},
],
},
]);
}
};
// 发送消息与对话驱动双轨逻辑
const handleSendMessage = (text: string, attachments?: { name: string; size: string; type: string }[]) => {
const trimmed = text.trim();
const userMsg: ChatMessage = {
id: 'msg-' + Date.now(),
sessionId: activeSessionId,
role: 'user',
authorName: currentUser.name,
content: text,
timestamp: new Date().toTimeString().substring(0, 5),
attachments,
};
// 检查是否为肯定回复(双轨同效判定:打字回复等同于点击卡片默认动作)
const isAffirmative = isAffirmativeResponse(trimmed);
const lastActive = findLastActiveCard(messages, activeSessionId);
if (isAffirmative && lastActive) {
const { card } = lastActive;
const defaultAction = card.actions.find((a) => a.isAffirmativeDefault) || card.actions[0];
if (defaultAction) {
setMessages((prev) => [...prev, userMsg]);
handleCardAction(card.cardId, defaultAction.actionId, defaultAction.payload);
return;
}
}
// 递减消息列表中尚未完结的 active 卡片 ttlTurns,若 <= 0 则标记为 expired
setMessages((prev) => [...decrementActiveCardsTtl(prev, activeSessionId), userMsg]);
setIsThinking(true);
setTimeout(() => {
setIsThinking(false);
// 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,
projectId: sessions.find((s) => s.sessionId === activeSessionId)?.projectId || 1,
taskKey: 'custom_draft',
title: docTitle,
stage: 'V0.5',
category: 'private',
creatorId: currentUser.id,
creatorName: currentUser.name,
isFixedWorkflow: false,
isTemplateOnly: 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: 'ast-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🔒 个人私密文档已生成',
content: `收到您的要求,已为您即时生成专属推演文档《${docTitle}》!\n\n根据系统隐私保护机制,该文档已自动存入【我的私密】库,**仅您本人可见**。右侧工作台已为您自动切至该文档,您可以自由查看与编辑。推演成熟后,可随时点击底部「一键共享给项目组」作为团队协作参考资料。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
return;
}
// 2. 用户发送驳回意图 (且存在处于 reviewing 的目标交付物)
if (text.includes('驳回') || text.includes('退回')) {
const targetReviewDoc =
docAST.lifecycleState === 'reviewing'
? docAST
: deliverables.find((d) => d.lifecycleState === 'reviewing')
? (docCache[deliverables.find((d) => d.lifecycleState === 'reviewing')!.docId] || docAST)
: null;
if (targetReviewDoc && targetReviewDoc.lifecycleState === 'reviewing') {
const matchingBlock = findMatchingBlockForReview(targetReviewDoc);
const card = createRejectConfirmationCard(targetReviewDoc, matchingBlock);
setMessages((prev) => [
...prev,
{
id: 'ast-reject-gate-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🛡️ 审核驳回门禁',
content: `检测到您对《${targetReviewDoc.title}》提出驳回审核意图。驳回将退回编制阶段并解除锁定,同时向编制人下达整改批注。请确认操作:`,
timestamp: new Date().toTimeString().substring(0, 5),
interactiveCard: card,
},
]);
return;
} else {
setMessages((prev) => [
...prev,
{
id: 'ast-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '⚠️ 状态校验提示',
content: `当前受控文档《${docAST.title}》处于【${docAST.lifecycleState}】阶段,非待审核状态,无需执行驳回。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
return;
}
}
if (text.includes('修改') || text.includes('更新') || text.includes('调整')) {
if (docAST.isClaimed && currentDocRole === 'author') {
setPendingProposal({
changes: '拟在第2节「监管分类与强制性合规标准矩阵」中,强化补充高频电刀共模抑制比设计指标。',
impact: '将直接影响电路原理图物料选型与型式检验 EMC 滤波网络参数。',
risk: '低风险。符合 GB 9706.1-2020 强标,提升系统抗扰度,无侵权风险。',
});
setMessages((prev) => [
...prev,
{
id: 'ast-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🛡️ 文档认领保护机制拦截',
content: `检测到《${docAST.title}》已处于【编制人已认领】状态。根据医疗器械质量体系规范,智能体小队不能擅自修改,已为您生成【变更方案提议】。请您确认是否准许本次修改。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
return;
}
}
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;
}
}
// 3. 检查当前项目中是否存在待填报的模板副本
const matchedTemplate = currentProjectDeliverables.find((d) => {
if (!d.isTemplateOnly) return false;
if (text.includes('需求') || text.includes('摸底') || text.includes('用户')) {
return d.taskKey === 'intake' || d.taskKey === 'user_requirements';
}
if (text.includes('调研') || text.includes('竞品')) {
return d.taskKey === 'research';
}
if (text.includes('可行性') || text.includes('注册') || text.includes('报告')) {
return d.taskKey === 'feasibility_reg' || d.taskKey === 'feasibility_dev';
}
if (text.includes('设计') || text.includes('规格') || text.includes('架构')) {
return d.taskKey === 'prelim_design';
}
if (text.includes('报价') || text.includes('工时') || text.includes('预算')) {
return d.taskKey === 'cdo_quote';
}
return false;
});
if (matchedTemplate) {
const existingAst = docCache[matchedTemplate.docId];
const updatedAst: DocumentAST = {
...existingAst,
lifecycleState: 'drafting',
isClaimed: true,
summary: `智能体参谋小队已依据您的指令“${text.slice(0, 40)}...”完成深度填报与法规对齐。`,
updatedAt: new Date().toISOString().replace('T', ' ').substring(0, 16),
};
if (updatedAst.rootBlock && updatedAst.rootBlock.children) {
const firstP = updatedAst.rootBlock.children.find((b) => b.type === 'paragraph');
if (firstP) {
firstP.content += `\n\n> 🤖 **智能体填报生效**:参谋小队已结合当前项目基线与权威法规知识库完成编制,骨架模板已平滑升级为受控草稿。`;
}
}
setDeliverables((prev) =>
prev.map((d) =>
d.docId === matchedTemplate.docId
? {
...d,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
summary: updatedAst.summary,
updatedAt: '刚刚',
}
: d
)
);
setDocCache((prev) => ({ ...prev, [matchedTemplate.docId]: updatedAst }));
setActiveDocId(matchedTemplate.docId);
setActiveCategory('workflow');
setMessages((prev) => [
...prev,
{
id: 'ast-fill-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🎯 模板填报升级',
content: `收到!参谋小队已依据您的指令完成《${matchedTemplate.title}》的填报。\n\n该文档已成功从【📄 初始模板】平滑升格为【✍️ 编制中 (受控草稿)】,右侧工作台已为您实时呈现填报正文,请审阅。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
return;
}
setMessages((prev) => [
...prev,
{
id: 'ast-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '💡 协同参谋回复',
content: `收到您的要求:“${text}”。参谋小队已分析当前处于【${docAST.lifecycleState}】生命周期,右侧已实时同步最新受控文档结构。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
}, 1000);
};
// 编制人接受变更提议
const handleAcceptProposal = () => {
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: 'ast-p-' + Date.now(),
sessionId: activeSessionId,
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((c) => c.id === updated.id);
if (idx !== -1) {
children[idx] = updated;
recomputeNumbering(docAST.rootBlock);
setDocAST({ ...docAST });
}
setEditingBlock(null);
};
const handleInsertBlock = (index: number, position: 'above' | 'below') => {
const newBlock: DocumentBlock = {
id: 'blk_' + Math.random().toString(36).substring(2, 9),
type: 'paragraph',
content: '【新插入技术要求】:在此填写具体受控技术规范或临床评定指标...',
metadata: {
lastModifiedBy: `${currentUser.name} (${currentUser.position})`,
lastModifiedAt: new Date().toISOString().replace('T', ' ').substring(0, 16),
},
};
const children = docAST.rootBlock.children || [];
const insertAt = position === 'above' ? index : index + 1;
children.splice(insertAt, 0, newBlock);
recomputeNumbering(docAST.rootBlock);
setDocAST({ ...docAST });
};
const handleMoveBlock = (index: number, direction: 'up' | 'down') => {
const children = docAST.rootBlock.children || [];
const targetIdx = direction === 'up' ? index - 1 : index + 1;
if (targetIdx >= 0 && targetIdx < children.length) {
const temp = children[index];
children[index] = children[targetIdx];
children[targetIdx] = temp;
recomputeNumbering(docAST.rootBlock);
setDocAST({ ...docAST });
}
};
const handleDeleteBlock = (index: number) => {
const children = docAST.rootBlock.children || [];
children.splice(index, 1);
recomputeNumbering(docAST.rootBlock);
setDocAST({ ...docAST });
};
// 批注操作
const handleAddComment = (comment: Omit<BlockComment, 'id' | 'createdAt'>) => {
if (!commentingBlock) return;
const newCmt: BlockComment = {
...comment,
id: 'cmt_' + Math.random().toString(36).substring(2, 8),
createdAt: new Date().toTimeString().substring(0, 5),
};
commentingBlock.comments = [...(commentingBlock.comments || []), newCmt];
setDocAST({ ...docAST });
};
// 法定生命周期流转
const handleClaim = () => {
docAST.isClaimed = true;
setDocAST({ ...docAST });
setDeliverables((prev) =>
prev.map((d) => (d.docId === activeDocId ? { ...d, isClaimed: true } : d))
);
};
const handleSubmitReview = () => {
docAST.lifecycleState = 'reviewing';
setDocAST({ ...docAST });
setDeliverables((prev) =>
prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'reviewing' } : d))
);
};
const handlePassReview = () => {
docAST.lifecycleState = 'approving';
setDocAST({ ...docAST });
setDeliverables((prev) =>
prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'approving' } : d))
);
};
const handleRejectReview = (reason: string) => {
docAST.lifecycleState = 'drafting';
setDocAST({ ...docAST });
setDeliverables((prev) =>
prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'drafting' } : d))
);
setMessages((prev) => [
...prev,
{
id: 'rej-' + Date.now(),
sessionId: activeSessionId,
role: 'system',
content: `⚠️ 李主管(审核负责人)驳回了《${docAST.title}》,已退回编制阶段。驳回原因:${reason}`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
};
const handleApprove = () => {
docAST.lifecycleState = 'approved';
setDocAST({ ...docAST });
setDeliverables((prev) =>
prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'approved' } : d))
);
setMessages((prev) => [
...prev,
{
id: 'appr-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🏆 终审批准生效',
content: `🎉 祝贺!王总监(批准人)已终审批准《${docAST.title}》!系统已自动对该受控交付物进行微粒度章节切片与向量化,成功注入全局标杆案例经验库!`,
timestamp: new Date().toTimeString().substring(0, 5),
recommendedCases: [
{
docId: docAST.docId,
taskKey: docAST.taskKey,
artifactTitle: docAST.title,
sectionTitle: '终审已生效标杆案例',
similarityScore: 1.0,
keyRegulations: ['GB 9706.1-2020', 'YY 0569-2011', 'IEC 60601-1-2'],
summarySnippet: '本交付物已通过编制、审核、批准法定三级签署闭环,作为往期标杆经验向全平台开放自推荐。',
},
],
},
]);
};
// 导出 DOCX (Word 渲染器)
const handleExportDocx = async () => {
try {
await exportDocumentToDocx(docAST);
} catch (err) {
console.error('导出 DOCX 失败:', err);
}
};
// 导出 XLSX (Excel 渲染器)
const handleExportXlsx = async () => {
try {
await exportDocumentToXlsx(docAST);
} catch (err) {
console.error('导出 XLSX 失败:', err);
}
};
// 导出 Markdown 原文
const handleExportMarkdown = () => {
const md = blockASTToMarkdown(docAST);
const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${docAST.title}_${docAST.version}_受控交付物.md`;
a.click();
URL.revokeObjectURL(url);
};
// 测试演练:动态修改当前用户在某个文档的权限角色
const handleRoleChangeForDoc = (docId: string, role: DocPermissionRole) => {
setCurrentUser((prev) => ({
...prev,
projectPermissions: {
...(prev.projectPermissions || {}),
[docId]: role,
},
}));
};
// 未登录拦截门禁:展示现代化医疗合规登录与注册页面
if (!session) {
return (
<LoginView
onLoginSuccess={(newSession) => {
setSession(newSession);
if (newSession.env) setSystemEnv(newSession.env);
const match = allUsers.find((u) => u.id === newSession.user.id);
if (match) {
setCurrentUser(match);
} else {
setCurrentUser(newSession.user);
}
}}
systemConfig={systemAuthConfig}
env={systemEnv}
/>
);
}
return (
<>
<AppShell
sessions={sessions}
activeSessionId={activeSessionId}
onSelectSession={setActiveSessionId}
onNewSession={() => {
const newSessId = 'sess-' + Date.now();
const newProjId = Date.now();
const newProjectTitle = '新建医疗器械研发项目';
const newDeviceType = '通用医疗器械';
const newSess: ProjectSession = {
sessionId: newSessId,
projectId: newProjId,
title: newProjectTitle,
deviceType: newDeviceType,
stage: 'V0.1',
stageLabel: 'V0.1 需求摸底',
messageCount: 1,
updatedAt: '刚刚',
};
const initMsg: ChatMessage = {
id: 'm-init-' + Date.now(),
sessionId: newSessId,
role: 'system',
content: '水木东方协同参谋小队已就位,当前新项目进入【V0.1 需求摸底与产品定义】阶段。全套研发工作流与阶段文档模板已克隆就绪,请在右侧查阅或在此输入您的研发构想。',
timestamp: new Date().toTimeString().substring(0, 5),
};
// 即时克隆全生命周期 7 份受控模板副本
const { deliverables: clonedDocs, docASTMap: clonedAsts } = createProjectTemplateDeliverables({
projectId: newProjId,
projectName: newProjectTitle,
deviceType: newDeviceType,
creatorId: currentUser.id,
creatorName: currentUser.name,
});
setSessions((prev) => [newSess, ...prev]);
setMessages((prev) => [...prev, initMsg]);
setDeliverables((prev) => [...prev, ...clonedDocs]);
setDocCache((prev) => ({ ...prev, ...clonedAsts }));
setActiveSessionId(newSessId);
setActiveDocId(clonedDocs[0].docId);
setActiveCategory('workflow');
}}
canCreateProject={canCreateProject}
currentUser={currentUser}
currentRole={currentDocRole}
currentUserName={currentUser.name}
onChangeRole={(_, __) => {}}
onOpenRightsModal={() => setRightsModalOpen(true)}
onOpenUserSettings={() => setUserSettingsOpen(true)}
onOpenAdminSettings={() => setAdminSettingsOpen(true)}
chatContent={
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<MessageStream
messages={currentSessionMessages}
onOpenCase={handleSelectDoc}
onCardAction={handleCardAction}
/>
{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}
onExportXlsx={handleExportXlsx}
onExportMarkdown={handleExportMarkdown}
/>
{/* 全景大盘抽屉 (支持受控/共享/私密三板块与一键流转操作) */}
<DeckDrawer
isOpen={deckDrawerOpen}
onClose={() => setDeckDrawerOpen(false)}
deliverables={visibleDeliverables}
activeDocId={activeDocId}
currentUserId={currentUser.id}
onSelectDoc={handleSelectDoc}
onShareToProject={handleShareToProject}
onRevokeShare={handleRevokeShare}
onDeleteDoc={handleDeleteDoc}
onOpenDocVisibility={(doc) => {
setTargetDocForVisibility(doc);
setDocVisibilityModalOpen(true);
}}
/>
{/* 块状文档渲染器 */}
<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请帮我审查其合规性、技术可行性或提出修改建议。`
);
}}
highlightedBlockId={highlightedBlockId}
/>
{/* 底部法定流转动作条 (根据受控/共享/私密形态自适应展示) */}
<DocGovActionBar
ast={docAST}
currentRole={currentDocRole}
currentUserId={currentUser.id}
canOverrideVisibility={canOverrideVisibility}
onOpenVisibilityModal={() => {
const currentDoc = deliverables.find((d) => d.docId === activeDocId) || null;
setTargetDocForVisibility(currentDoc);
setDocVisibilityModalOpen(true);
}}
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)}
/>
)}
{/* 审核批注侧栏 */}
{commentingBlock && (
<CommentSidePanel
block={commentingBlock}
currentRole={currentDocRole}
currentUserName={currentUser.name}
onAddComment={handleAddComment}
onClose={() => setCommentingBlock(null)}
/>
)}
</div>
}
/>
{/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 (支持快速模拟账号切换) */}
<UserProjectRightsModal
isOpen={rightsModalOpen}
onClose={() => setRightsModalOpen(false)}
currentUser={currentUser}
currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'}
deliverables={visibleDeliverables}
activeDocId={activeDocId}
allUsers={allUsers}
canAssignLead={canAssignLead}
allowDevSwitch={systemEnv === 'dev'}
onSwitchUser={(u) => {
const fresh = allUsers.find((x) => x.id === u.id) || u;
setCurrentUser(fresh);
}}
onSelectDoc={handleSelectDoc}
onRoleChangeForDoc={handleRoleChangeForDoc}
/>
{/* 首次登录强制改密阻断弹窗 */}
<FirstLoginPasswordModal
isOpen={!!session?.mustChangePassword}
user={currentUser}
onPasswordChanged={() => {
const updated = { ...session, mustChangePassword: false };
setSession(updated);
authClient.saveSession(updated);
}}
onLogout={handleLogout}
/>
{/* 用户个人设置与偏好中心 (全员可用) */}
<UserSettingsModal
isOpen={userSettingsOpen}
onClose={() => setUserSettingsOpen(false)}
currentUser={currentUser}
roles={roles}
onUpdateProfile={(updatedProfile) => {
setAllUsers((prev) =>
prev.map((u) => (u.id === currentUser.id ? { ...u, ...updatedProfile } : u))
);
setCurrentUser((prev) => ({ ...prev, ...updatedProfile }));
}}
onLogout={handleLogout}
/>
{/* 系统管理员独占治理控制台 (仅 role-admin 可入) */}
<AdminSettingsModal
isOpen={adminSettingsOpen}
onClose={() => setAdminSettingsOpen(false)}
currentUser={currentUser}
roles={roles}
onSaveRoles={(newRoles) => setRoles(newRoles)}
allUsers={allUsers}
onUpdateUserRole={(userId, roleId) => {
const targetRole = roles.find((r) => r.id === roleId);
setAllUsers((prev) =>
prev.map((u) => (u.id === userId ? { ...u, roleId, position: targetRole?.name || u.position } : u))
);
if (currentUser.id === userId) {
setCurrentUser((prev) => ({ ...prev, roleId, position: targetRole?.name || prev.position }));
}
}}
systemConfig={systemAuthConfig}
onUpdateSystemConfig={(cfg) => setSystemAuthConfig((prev) => ({ ...prev, ...cfg }))}
onRefreshUsers={() => {
authClient
.getAdminUsers()
.then((users) => {
setAllUsers(users);
})
.catch(() => {});
}}
env={systemEnv}
/>
{/* 研发调试快捷身份切换浮窗 (仅在 DEV 环境生效,生产与测试环境彻底剥离) */}
<DevRoleSwitcher
env={systemEnv}
currentUser={currentUser}
allUsers={allUsers}
onSwitchUser={(newUser) => {
const fresh = allUsers.find((u) => u.id === newUser.id) || newUser;
setCurrentUser(fresh);
}}
/>
{/* 单文档可见性覆盖弹窗 */}
<DocVisibilityModal
isOpen={docVisibilityModalOpen}
onClose={() => setDocVisibilityModalOpen(false)}
doc={targetDocForVisibility}
roles={roles}
currentUserName={currentUser.name}
onSaveRule={handleSaveDocVisibilityRule}
/>
</>
);
}