diff --git a/web/src/App.tsx b/web/src/App.tsx index 47a30470..8030df9c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -50,6 +50,7 @@ import { } from './lib/projectMemberAdapter'; import { mergeWorkflowDocumentsFromServer } from './lib/deliverableSync'; import { resolveCurrentDocRole } from './lib/currentDocRole'; +import { canAddBlockComment } from './lib/commentPolicy'; import { pollUntilPostTaskServerDoc, shouldSwitchTemplateSlotToServer, @@ -1233,7 +1234,12 @@ export default function App() { reviewer: docAST.reviewer, approver: docAST.approver, projectPermission: currentUser.projectPermissions?.[activeDocId], - isLegacyLocalDoc: !docAST.authorId && !docAST.reviewerId && !docAST.approverId, + dutiesAuthoritative: docAST.dutiesAuthoritative, + governanceMode: docAST.governanceMode, + isLegacyLocalDoc: + docAST.dutiesAuthoritative !== true && + docAST.governanceMode !== 'controlled' && + /^doc-p\d+-/.test(docAST.docId), }); const setDocAST = (newAst: DocumentAST) => { @@ -3416,7 +3422,7 @@ export default function App() { // 批注操作 const handleAddComment = (comment: Omit) => { - if (!commentingBlock) return; + if (!commentingBlock || !canAddBlockComment(currentDocRole)) return; const newCmt: BlockComment = { ...comment, id: 'cmt_' + Math.random().toString(36).substring(2, 8), diff --git a/web/src/components/doc/BlockRenderer.tsx b/web/src/components/doc/BlockRenderer.tsx index 1a7d8d31..5654ec64 100644 --- a/web/src/components/doc/BlockRenderer.tsx +++ b/web/src/components/doc/BlockRenderer.tsx @@ -11,6 +11,7 @@ import { MermaidBlockView } from './blocks/MermaidBlockView'; import { MathBlockView } from './blocks/MathBlockView'; import { ListBlockView } from './blocks/ListBlockView'; import { BlockContextMenu } from './BlockContextMenu'; +import { canAddBlockComment } from '../../lib/commentPolicy'; import { Plus, ArrowUp, ArrowDown, Trash2, MessageSquare } from 'lucide-react'; export interface BlockRendererProps { @@ -61,7 +62,7 @@ export const BlockRenderer: React.FC = ({ !!ast.editLease?.holderType && (!ast.editLease.expiresAt || new Date(ast.editLease.expiresAt).getTime() > Date.now()); const isEditable = currentRole === 'author' && ast.lifecycleState === 'drafting' && !leaseActive; - const canAnnotate = !leaseActive; + const canAnnotate = !leaseActive && canAddBlockComment(currentRole); const children = ast.rootBlock.children || []; const renderBlockContent = (block: DocumentBlock) => { @@ -267,7 +268,7 @@ export const BlockRenderer: React.FC = ({ )} {/* 批注按钮:编辑租约占用期间禁用 */} - {canAnnotate && ( + {(canAnnotate || commentCount > 0) && ( + ) : ( +
+ 当前为查阅者,仅可阅读已有批注 +
+ )} ); }; diff --git a/web/src/lib/commentPolicy.test.ts b/web/src/lib/commentPolicy.test.ts new file mode 100644 index 00000000..17881a73 --- /dev/null +++ b/web/src/lib/commentPolicy.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + canAddBlockComment, + resolveCommentAuthorRole, +} from './commentPolicy'; + +describe('commentPolicy', () => { + it('lets author/reviewer/approver annotate and keeps their comment role', () => { + assert.equal(canAddBlockComment('author'), true); + assert.equal(canAddBlockComment('reviewer'), true); + assert.equal(canAddBlockComment('approver'), true); + assert.equal(resolveCommentAuthorRole('author'), 'reviewer'); + assert.equal(resolveCommentAuthorRole('reviewer'), 'reviewer'); + assert.equal(resolveCommentAuthorRole('approver'), 'approver'); + }); + + it('keeps viewer read-only: no add entry and no reviewer/approver cast', () => { + assert.equal(canAddBlockComment('viewer'), false); + assert.equal(resolveCommentAuthorRole('viewer'), null); + }); +}); diff --git a/web/src/lib/commentPolicy.ts b/web/src/lib/commentPolicy.ts new file mode 100644 index 00000000..de1d1a83 --- /dev/null +++ b/web/src/lib/commentPolicy.ts @@ -0,0 +1,15 @@ +import type { UserRole } from '../types/docgov'; + +export type CommentAuthorRole = 'reviewer' | 'approver'; + +/** viewer 只能读批注,不能打开新增入口或提交。 */ +export function canAddBlockComment(role: UserRole | string): boolean { + return role === 'author' || role === 'reviewer' || role === 'approver'; +} + +/** author 的批注按既有语义记为 reviewer;viewer 不得被 cast。 */ +export function resolveCommentAuthorRole(role: UserRole | string): CommentAuthorRole | null { + if (role === 'approver') return 'approver'; + if (role === 'reviewer' || role === 'author') return 'reviewer'; + return null; +}