feat(web): implement 36px slim tabs, deck drawer, and block views with medical styling

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-13 02:50:19 +00:00
committed by weichen
co-authored by Cursor
parent ef6665288c
commit 2cb8d9b8d2
8 changed files with 881 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
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 { 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;
}
export const BlockRenderer: React.FC<BlockRendererProps> = ({
ast,
currentRole,
onEditBlock,
onInsertBlock,
onMoveBlock,
onDeleteBlock,
onOpenComments,
}) => {
const [hoveredIndex, setHoveredIndex] = useState<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 '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);
}
}}
style={{
position: 'relative',
borderRadius: 'var(--r-sm)',
padding: '2px 6px',
margin: '2px -6px',
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>
);
})}
</div>
);
};
+205
View File
@@ -0,0 +1,205 @@
import React from 'react';
import { DeliverableItem } from '../../types/workbench';
import { DocLifecycleState } from '../../types/docgov';
import { X, CheckCircle2, Clock, ShieldCheck, UserCheck, AlertTriangle } from 'lucide-react';
interface DeckDrawerProps {
isOpen: boolean;
onClose: () => void;
deliverables: DeliverableItem[];
activeDocId: string;
onSelectDoc: (docId: string) => void;
}
export const DeckDrawer: React.FC<DeckDrawerProps> = ({
isOpen,
onClose,
deliverables,
activeDocId,
onSelectDoc,
}) => {
if (!isOpen) {
return null;
}
const renderBadge = (state: DocLifecycleState, isClaimed: boolean) => {
switch (state) {
case 'approved':
return (
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--success)', background: 'var(--success-bg)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
<CheckCircle2 size={11} />
<span>终审已生效</span>
</span>
);
case 'reviewing':
return (
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--amber-600)', background: 'var(--amber-50)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
<Clock size={11} />
<span>审核中 (已锁定)</span>
</span>
);
case 'approving':
return (
<span style={{ fontSize: '11px', fontWeight: 600, color: '#9333EA', background: '#F3E8FF', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
<ShieldCheck size={11} />
<span>批准中</span>
</span>
);
case 'rejected':
return (
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--danger)', background: 'var(--danger-bg)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
<AlertTriangle size={11} />
<span>被驳回需修改</span>
</span>
);
case 'drafting':
default:
return (
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--brand-600)', background: 'var(--brand-50)', padding: '2px 8px', borderRadius: 'var(--r-pill)', display: 'flex', alignItems: 'center', gap: '3px' }}>
<UserCheck size={11} />
<span>编制中 {isClaimed ? '(已认领)' : '(未认领)'}</span>
</span>
);
}
};
// 按阶段分组
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工时测算' },
];
return (
<div
style={{
position: 'absolute',
top: '36px',
left: 0,
right: 0,
maxHeight: '480px',
background: 'var(--bg-panel)',
borderBottom: '2px solid var(--brand-500)',
boxShadow: 'var(--shadow-lg)',
zIndex: 50,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
backdropFilter: 'blur(16px)',
}}
>
{/* 抽屉顶栏 */}
<div
style={{
padding: '10px 16px',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
background: 'var(--bg-subtle)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-strong)' }}>
当前项目阶段交付物台账大盘
</span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
(共 {deliverables.length} 份受控交付物)
</span>
</div>
<button
onClick={onClose}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '26px',
height: '26px',
borderRadius: 'var(--r-sm)',
color: 'var(--text-muted)',
}}
title="关闭大盘"
>
<X size={15} />
</button>
</div>
{/* 交付物卡片列表 */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '14px 16px',
display: 'flex',
flexDirection: 'column',
gap: '14px',
}}
>
{stages.map((stg) => {
const docsInStage = deliverables.filter((d) => d.stage === stg.stage);
if (docsInStage.length === 0) return null;
return (
<div key={stg.stage} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--brand-600)' }}>
{stg.title}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '10px' }}>
{docsInStage.map((doc) => {
const isActive = doc.docId === activeDocId;
return (
<div
key={doc.docId}
onClick={() => {
onSelectDoc(doc.docId);
onClose();
}}
style={{
padding: '10px 12px',
borderRadius: 'var(--r-md)',
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
border: isActive ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
boxShadow: 'var(--shadow-xs)',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
gap: '6px',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.borderColor = 'var(--brand-100)';
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.borderColor = 'var(--border)';
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
{doc.title}
</span>
{renderBadge(doc.lifecycleState, doc.isClaimed)}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
<span>编制: <b>{doc.author}</b></span>
<span>审核: <b>{doc.reviewer}</b></span>
<span>批准: <b>{doc.approver}</b></span>
</div>
{doc.summary && (
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
{doc.summary}
</div>
)}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
};
+177
View File
@@ -0,0 +1,177 @@
import React from 'react';
import { DeliverableItem } from '../../types/workbench';
import { DocLifecycleState } from '../../types/docgov';
import { FolderKanban, FileDown, Maximize2, Minimize2 } from 'lucide-react';
interface SlimTabBarProps {
deliverables: DeliverableItem[];
activeDocId: string;
onSelectDoc: (docId: string) => void;
onToggleDeckDrawer: () => void;
onExportDocx?: () => void;
isFullscreen?: boolean;
onToggleFullscreen?: () => void;
}
export const SlimTabBar: React.FC<SlimTabBarProps> = ({
deliverables,
activeDocId,
onSelectDoc,
onToggleDeckDrawer,
onExportDocx,
isFullscreen,
onToggleFullscreen,
}) => {
const getStatusDot = (state: DocLifecycleState) => {
switch (state) {
case 'approved':
return { color: 'var(--success)', label: '已生效受控' };
case 'reviewing':
return { color: 'var(--amber-500)', label: '审核中' };
case 'approving':
return { color: '#A855F7', label: '批准中' };
case 'rejected':
return { color: 'var(--danger)', label: '已驳回' };
case 'drafting':
default:
return { color: 'var(--brand-500)', label: '编制中' };
}
};
return (
<div
style={{
height: '36px',
borderBottom: '1px solid var(--border)',
background: 'var(--bg-subtle)',
display: 'flex',
alignItems: 'center',
padding: '0 8px',
gap: '4px',
overflowX: 'auto',
flexShrink: 0,
userSelect: 'none',
}}
>
{/* 交付物 Tabs */}
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', flex: 1, overflowX: 'auto' }}>
{deliverables.map((doc) => {
const isActive = doc.docId === activeDocId;
const status = getStatusDot(doc.lifecycleState);
return (
<div
key={doc.docId}
onClick={() => onSelectDoc(doc.docId)}
style={{
height: '28px',
padding: '0 10px',
borderRadius: '6px 6px 0 0',
background: isActive ? 'var(--bg-panel)' : 'transparent',
border: isActive ? '1px solid var(--border)' : '1px solid transparent',
borderBottom: isActive ? '1px solid var(--bg-panel)' : '1px solid transparent',
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '12px',
fontWeight: isActive ? 600 : 500,
color: isActive ? 'var(--text-strong)' : 'var(--text-muted)',
cursor: 'pointer',
whiteSpace: 'nowrap',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.color = 'var(--text)';
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.color = 'var(--text-muted)';
}}
title={`${doc.title} (${status.label})`}
>
<div
style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: status.color,
boxShadow: `0 0 5px ${status.color}`,
}}
/>
<span style={{ maxWidth: '140px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{doc.title}
</span>
</div>
);
})}
</div>
{/* 右侧全局操作区 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginLeft: 'auto', flexShrink: 0 }}>
{/* 全部交付物大盘抽屉入口 */}
<button
onClick={onToggleDeckDrawer}
style={{
height: '26px',
padding: '0 8px',
borderRadius: 'var(--r-sm)',
background: 'var(--bg-panel)',
border: '1px solid var(--border)',
color: 'var(--text)',
fontSize: '11px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
title="展开当前项目各阶段全部交付物状态大盘"
>
<FolderKanban size={13} style={{ color: 'var(--brand-500)' }} />
<span>全部交付物 ({deliverables.length}) ▾</span>
</button>
{/* 导出受控 Word 按钮 */}
{onExportDocx && (
<button
onClick={onExportDocx}
style={{
height: '26px',
padding: '0 8px',
borderRadius: 'var(--r-sm)',
background: 'var(--brand-50)',
border: '1px solid var(--brand-100)',
color: 'var(--brand-600)',
fontSize: '11px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
title="导出为符合严肃合规模板的受控 DOCX 交付物"
>
<FileDown size={13} />
<span>导出受控 DOCX</span>
</button>
)}
{/* 全屏切换 */}
{onToggleFullscreen && (
<button
onClick={onToggleFullscreen}
style={{
width: '26px',
height: '26px',
borderRadius: 'var(--r-sm)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--text-muted)',
}}
title={isFullscreen ? '退出全屏' : '全屏沉浸阅读与编辑'}
>
{isFullscreen ? <Minimize2 size={13} /> : <Maximize2 size={13} />}
</button>
)}
</div>
</div>
);
};
@@ -0,0 +1,39 @@
import React from 'react';
import { DocumentBlock } from '../../../types/block';
import { ShieldAlert, Info } from 'lucide-react';
interface CalloutBlockViewProps {
block: DocumentBlock;
}
export const CalloutBlockView: React.FC<CalloutBlockViewProps> = ({ block }) => {
const isReg = block.properties?.calloutType === 'regulatory';
return (
<div
style={{
margin: '12px 0',
padding: '10px 14px',
borderRadius: 'var(--r-md)',
background: isReg ? 'var(--amber-50)' : 'var(--info-bg)',
borderLeft: `4px solid ${isReg ? 'var(--amber-500)' : 'var(--info)'}`,
borderTop: '1px solid var(--border)',
borderRight: '1px solid var(--border)',
borderBottom: '1px solid var(--border)',
display: 'flex',
gap: '10px',
alignItems: 'flex-start',
}}
>
<div style={{ marginTop: '2px', color: isReg ? 'var(--amber-600)' : 'var(--info)' }}>
{isReg ? <ShieldAlert size={16} /> : <Info size={16} />}
</div>
<div style={{ flex: 1, fontSize: '13px', lineHeight: 1.6, color: 'var(--text-strong)' }}>
<div style={{ fontWeight: 600, marginBottom: '2px', color: isReg ? 'var(--amber-600)' : 'var(--info)' }}>
{isReg ? '受控合规依据' : '提示说明'}
</div>
<div>{block.content}</div>
</div>
</div>
);
};
@@ -0,0 +1,56 @@
import React from 'react';
import { DocumentBlock } from '../../../types/block';
interface HeadingBlockViewProps {
block: DocumentBlock;
}
export const HeadingBlockView: React.FC<HeadingBlockViewProps> = ({ block }) => {
const level = block.level || 1;
const fontSizes: Record<number, string> = {
1: '20px',
2: '16.5px',
3: '14.5px',
4: '13.5px',
5: '13px',
6: '12.5px',
};
const margins: Record<number, string> = {
1: '24px 0 12px',
2: '18px 0 8px',
3: '14px 0 6px',
4: '10px 0 4px',
5: '8px 0 4px',
6: '6px 0 4px',
};
return (
<div
style={{
margin: margins[level] || '12px 0 6px',
display: 'flex',
alignItems: 'baseline',
gap: '8px',
color: 'var(--text-strong)',
fontWeight: level <= 2 ? 700 : 600,
fontSize: fontSizes[level] || '14px',
lineHeight: 1.4,
}}
>
{block.numbering && (
<span
style={{
color: 'var(--brand-500)',
fontFamily: 'var(--font-mono)',
fontWeight: 700,
}}
>
{block.numbering}
</span>
)}
<span>{block.content}</span>
</div>
);
};
@@ -0,0 +1,48 @@
import React from 'react';
import { DocumentBlock } from '../../../types/block';
import { GitBranch } from 'lucide-react';
interface MermaidBlockViewProps {
block: DocumentBlock;
}
export const MermaidBlockView: React.FC<MermaidBlockViewProps> = ({ block }) => {
const caption = block.properties?.caption || '系统架构与执行时序图';
return (
<div style={{ margin: '16px 0', display: 'flex', flexDirection: 'column', gap: '6px' }}>
{/* 题注 (图 1-1 ...) */}
<div
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-strong)',
textAlign: 'center',
letterSpacing: '0.3px',
}}
>
<span>{block.numbering ? `${block.numbering} ` : ''}</span>
<span>{caption}</span>
</div>
<div
style={{
padding: '12px 14px',
borderRadius: 'var(--r-md)',
background: 'var(--bg-subtle)',
border: '1px solid var(--border)',
fontFamily: 'var(--font-mono)',
fontSize: '12px',
color: 'var(--brand-600)',
position: 'relative',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '6px', color: 'var(--text-muted)' }}>
<GitBranch size={13} />
<span style={{ fontSize: '11px', fontWeight: 600 }}>Mermaid 架构流程图渲染</span>
</div>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', lineHeight: 1.5 }}>{block.content}</pre>
</div>
</div>
);
};
@@ -0,0 +1,39 @@
import React from 'react';
import { DocumentBlock } from '../../../types/block';
interface ParagraphBlockViewProps {
block: DocumentBlock;
}
export const ParagraphBlockView: React.FC<ParagraphBlockViewProps> = ({ block }) => {
const regs = block.properties?.regulations;
return (
<div style={{ margin: '8px 0', lineHeight: 1.7, fontSize: '13.5px', color: 'var(--text)' }}>
<p style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{block.content}</p>
{/* 识别出的法规条款快速标签 */}
{regs && regs.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px' }}>
{regs.map((reg, i) => (
<span
key={i}
style={{
fontSize: '10.5px',
padding: '1px 6px',
borderRadius: 'var(--r-pill)',
background: 'var(--brand-50)',
color: 'var(--brand-600)',
border: '1px solid var(--brand-100)',
fontWeight: 600,
}}
title="法规知识库引证条款"
>
§ {reg}
</span>
))}
</div>
)}
</div>
);
};
@@ -0,0 +1,97 @@
import React from 'react';
import { DocumentBlock } from '../../../types/block';
interface TableBlockViewProps {
block: DocumentBlock;
}
export const TableBlockView: React.FC<TableBlockViewProps> = ({ block }) => {
const tableData = block.properties?.tableData;
const caption = block.properties?.caption || '技术参数对照表';
if (!tableData || tableData.length === 0) {
return null;
}
const headerRow = tableData[0];
const bodyRows = tableData.slice(1);
return (
<div style={{ margin: '16px 0', display: 'flex', flexDirection: 'column', gap: '6px' }}>
{/* 题注 (表 1-1 ...) */}
<div
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-strong)',
textAlign: 'center',
letterSpacing: '0.3px',
}}
>
<span>{block.numbering ? `${block.numbering} ` : ''}</span>
<span>{caption}</span>
</div>
{/* 医疗标准规范三线表容器 */}
<div style={{ overflowX: 'auto' }}>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
borderTop: '2px solid var(--text-strong)',
borderBottom: '2px solid var(--text-strong)',
fontSize: '12.5px',
textAlign: 'left',
}}
>
<thead>
<tr style={{ borderBottom: '1px solid var(--text-strong)', background: 'var(--bg-subtle)' }}>
{headerRow.map((cell, idx) => (
<th
key={idx}
style={{
padding: '8px 10px',
fontWeight: 600,
color: 'var(--text-strong)',
}}
>
{cell}
</th>
))}
</tr>
</thead>
<tbody>
{bodyRows.map((row, rIdx) => (
<tr
key={rIdx}
style={{
borderBottom: '1px solid var(--border-light)',
transition: 'background 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--bg-hover)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
{row.map((cell, cIdx) => (
<td
key={cIdx}
style={{
padding: '7px 10px',
color: 'var(--text)',
lineHeight: 1.4,
}}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};