/** * BlockCanvas.tsx - 块画布组件 * * 渲染所有的文档块,管理块的排序和交互 * 支持在标题上方显示TOC占位符 * * @module components/Editor */ import React from 'react'; import type { DocumentBlock } from '../../types/editor'; import { BlockRenderer } from './BlockRenderer'; import { TOCPlaceholder } from './blocks/TOCPlaceholder'; import { getHeadingNumberMap } from '../../utils/headingNumbering'; import { formatOrderedListMarker, getOrderedListNumberMap } from '../../utils/listNumbering'; import './BlockCanvas.css'; // ══════════════════════════════════════════════════════════════════════════════ // Component Props // ══════════════════════════════════════════════════════════════════════════════ export interface BlockCanvasProps { /** 文档块数组 */ blocks: DocumentBlock[]; /** 是否只读 */ readOnly?: boolean; } // ══════════════════════════════════════════════════════════════════════════════ // Component // ══════════════════════════════════════════════════════════════════════════════ /** * BlockCanvas - 块画布 * * 在第一个标题块上方显示TOC占位符(如果当前没有TOC块) * 在每个标题块上方显示TOC占位符(如果当前没有TOC块) */ export const BlockCanvas = React.memo(function BlockCanvas({ blocks, readOnly = false, }: BlockCanvasProps) { const [collapsedHeadingIds, setCollapsedHeadingIds] = React.useState>(new Set()); const { sortedBlocks, hasTOC, firstHeadingIndex, headingNumbers, orderedListNumbers, collapsibleHeadingIds } = React.useMemo(() => { const sorted = [...blocks].sort((a, b) => a.block_order - b.block_order); let tocFound = false; let headingIndex = -1; sorted.forEach((block, index) => { if (block.type === 'toc') tocFound = true; if (headingIndex === -1 && block.type === 'heading') { headingIndex = index; } }); const collapsible = new Set(); sorted.forEach((block, index) => { if (block.type !== 'heading') return; const nextBlock = sorted[index + 1]; if ( nextBlock && (nextBlock.type !== 'heading' || nextBlock.level > block.level) ) { collapsible.add(block.id); } }); return { sortedBlocks: sorted, hasTOC: tocFound, headingNumbers: getHeadingNumberMap(sorted), firstHeadingIndex: headingIndex, orderedListNumbers: getOrderedListNumberMap(sorted), collapsibleHeadingIds: collapsible, }; }, [blocks]); const visibleBlocks = React.useMemo(() => { const hiddenByLevel: number[] = []; return sortedBlocks.filter((block) => { if (block.type === 'heading') { while (hiddenByLevel.length > 0 && hiddenByLevel[hiddenByLevel.length - 1] >= block.level) { hiddenByLevel.pop(); } const isHidden = hiddenByLevel.length > 0; if (collapsedHeadingIds.has(block.id)) hiddenByLevel.push(block.level); return !isHidden; } return hiddenByLevel.length === 0; }); }, [sortedBlocks, collapsedHeadingIds]); const toggleHeadingCollapse = React.useCallback((headingId: string) => { setCollapsedHeadingIds((current) => { const next = new Set(current); if (next.has(headingId)) next.delete(headingId); else next.add(headingId); return next; }); }, []); // 空状态 if (visibleBlocks.length === 0) { return (

文档为空,点击工具栏添加内容

); } return (
{visibleBlocks.map((block, index) => { const prevBlock = index > 0 ? visibleBlocks[index - 1] : null; // 在标题上方显示TOC占位符的条件: // 1. 当前文档没有TOC块 // 2. 当前块是标题块 // 3. 不是只读模式 const showPlaceholderBefore = !hasTOC && index === firstHeadingIndex && !readOnly; return ( {/* 在标题上方显示TOC占位符 */} {showPlaceholderBefore && ( )} {/* 渲染当前块 */} ); })}
); }); export default BlockCanvas;