Files
Agent-System/web/src/components/doc/BlockRenderer.tsx
T

263 lines
10 KiB
TypeScript

import React, { useState } from 'react';
import { DocumentAST, DocumentBlock } from '../../types/block';
import { UserRole } from '../../types/docgov';
import { HeadingBlockView } from './blocks/HeadingBlockView';
import { ParagraphBlockView } from './blocks/ParagraphBlockView';
import { TableBlockView } from './blocks/TableBlockView';
import { CalloutBlockView } from './blocks/CalloutBlockView';
import { MermaidBlockView } from './blocks/MermaidBlockView';
import { MathBlockView } from './blocks/MathBlockView';
import { BlockContextMenu } from './BlockContextMenu';
import { Plus, ArrowUp, ArrowDown, Trash2, MessageSquare } from 'lucide-react';
interface BlockRendererProps {
ast: DocumentAST;
currentRole: UserRole;
onEditBlock: (block: DocumentBlock) => void;
onInsertBlock: (index: number, position: 'above' | 'below') => void;
onMoveBlock: (index: number, direction: 'up' | 'down') => void;
onDeleteBlock: (index: number) => void;
onOpenComments: (block: DocumentBlock) => void;
onAskAgent?: (content: string) => void;
}
export const BlockRenderer: React.FC<BlockRendererProps> = ({
ast,
currentRole,
onEditBlock,
onInsertBlock,
onMoveBlock,
onDeleteBlock,
onOpenComments,
onAskAgent,
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [contextMenu, setContextMenu] = useState<{
x: number;
y: number;
block: DocumentBlock;
index: number;
} | null>(null);
const isEditable = currentRole === 'author' && ast.lifecycleState === 'drafting';
const children = ast.rootBlock.children || [];
const renderBlockContent = (block: DocumentBlock) => {
switch (block.type) {
case 'heading':
return <HeadingBlockView block={block} />;
case 'table':
return <TableBlockView block={block} />;
case 'callout':
return <CalloutBlockView block={block} />;
case 'mermaid':
return <MermaidBlockView block={block} />;
case 'math':
return <MathBlockView block={block} />;
case 'divider':
return <hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '16px 0' }} />;
case 'paragraph':
default:
return <ParagraphBlockView block={block} />;
}
};
return (
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '24px 32px 64px',
maxWidth: '860px',
margin: '0 auto',
width: '100%',
color: 'var(--text)',
}}
>
{/* 顶部受控文件大标题与状态条 */}
<div style={{ marginBottom: '20px', borderBottom: '1px solid var(--border-light)', paddingBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--brand-600)', background: 'var(--brand-50)', padding: '2px 8px', borderRadius: 'var(--r-pill)' }}>
受控版本 {ast.version}
</span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
更新时间: {ast.updatedAt}
</span>
</div>
<div style={{ display: 'flex', gap: '12px', fontSize: '12px', color: 'var(--text-muted)' }}>
<span>编制人: <b>{ast.author}</b></span>
<span>审核人: <b>{ast.reviewer}</b></span>
<span>批准人: <b>{ast.approver}</b></span>
</div>
</div>
{/* 认领或锁定提示 */}
{ast.lifecycleState !== 'drafting' ? (
<div style={{ marginTop: '8px', padding: '6px 10px', background: 'var(--amber-50)', borderRadius: 'var(--r-sm)', fontSize: '11.5px', color: 'var(--amber-600)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span>🔒 本文档当前处于【{ast.lifecycleState === 'reviewing' ? '审核中' : ast.lifecycleState === 'approving' ? '批准中' : '已生效受控'}】锁定状态,正文只读,禁止篡改。可通过鼠标右键或右侧浮标添加批注。</span>
</div>
) : isEditable ? (
<div style={{ marginTop: '8px', padding: '4px 8px', background: 'var(--bg-subtle)', borderRadius: 'var(--r-sm)', fontSize: '11px', color: 'var(--text-muted)' }}>
💡 提示:双击任意段落就地编辑;支持鼠标右键唤出快捷操作菜单(编辑、批注、复制、插块)。
</div>
) : null}
</div>
{/* 块流 */}
{children.map((block, idx) => {
const isHovered = hoveredIndex === idx;
const commentCount = block.comments?.length || 0;
return (
<div
key={block.id || idx}
onMouseEnter={() => setHoveredIndex(idx)}
onMouseLeave={() => setHoveredIndex(null)}
onDoubleClick={() => {
if (isEditable) {
onEditBlock(block);
}
}}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({
x: e.clientX,
y: e.clientY,
block,
index: idx,
});
}}
style={{
position: 'relative',
borderRadius: 'var(--r-sm)',
padding: '3px 8px',
margin: '3px -8px',
border: isHovered && isEditable ? '1px dashed var(--brand-500)' : '1px solid transparent',
background: isHovered && isEditable ? 'var(--bg-hover)' : 'transparent',
transition: 'background 0.15s ease, border-color 0.15s ease',
cursor: isEditable ? 'text' : 'default',
}}
>
{/* 块微操作浮动条 */}
{isHovered && (
<div
style={{
position: 'absolute',
right: '4px',
top: '-14px',
background: 'var(--bg-panel)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-pill)',
boxShadow: 'var(--shadow-sm)',
display: 'flex',
alignItems: 'center',
padding: '2px 4px',
gap: '2px',
zIndex: 20,
}}
>
{isEditable && (
<>
<button
onClick={(e) => {
e.stopPropagation();
onInsertBlock(idx, 'above');
}}
style={{ padding: '3px', borderRadius: '4px', color: 'var(--text-muted)' }}
title="在此块上方插入新段落"
>
<Plus size={12} />
</button>
{idx > 0 && (
<button
onClick={(e) => {
e.stopPropagation();
onMoveBlock(idx, 'up');
}}
style={{ padding: '3px', borderRadius: '4px', color: 'var(--text-muted)' }}
title="上移此块"
>
<ArrowUp size={12} />
</button>
)}
{idx < children.length - 1 && (
<button
onClick={(e) => {
e.stopPropagation();
onMoveBlock(idx, 'down');
}}
style={{ padding: '3px', borderRadius: '4px', color: 'var(--text-muted)' }}
title="下移此块"
>
<ArrowDown size={12} />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
onDeleteBlock(idx);
}}
style={{ padding: '3px', borderRadius: '4px', color: 'var(--danger)' }}
title="删除此块"
>
<Trash2 size={12} />
</button>
</>
)}
{/* 批注按钮 (所有角色均可点击查看/添加) */}
<button
onClick={(e) => {
e.stopPropagation();
onOpenComments(block);
}}
style={{
padding: '3px 6px',
borderRadius: '4px',
color: commentCount > 0 ? 'var(--amber-500)' : 'var(--text-muted)',
display: 'flex',
alignItems: 'center',
gap: '2px',
fontSize: '11px',
fontWeight: 600,
}}
title="查看或添加审核批注"
>
<MessageSquare size={12} />
{commentCount > 0 && <span>{commentCount}</span>}
</button>
</div>
)}
{/* 块正文内容 */}
{renderBlockContent(block)}
</div>
);
})}
{/* 鼠标右键上下文菜单 */}
{contextMenu && (
<BlockContextMenu
x={contextMenu.x}
y={contextMenu.y}
block={contextMenu.block}
index={contextMenu.index}
isEditable={isEditable}
onClose={() => setContextMenu(null)}
onEdit={() => onEditBlock(contextMenu.block)}
onInsertAbove={() => onInsertBlock(contextMenu.index, 'above')}
onInsertBelow={() => onInsertBlock(contextMenu.index, 'below')}
onMoveUp={() => onMoveBlock(contextMenu.index, 'up')}
onMoveDown={() => onMoveBlock(contextMenu.index, 'down')}
onDelete={() => onDeleteBlock(contextMenu.index)}
onComment={() => onOpenComments(contextMenu.block)}
onAskAgent={onAskAgent}
/>
)}
</div>
);
};