/** * HeadingBlock.tsx - 标题块组件 * * @module components/Editor/blocks */ import React, { useCallback } from 'react'; import { message } from 'antd'; import type { HeadingBlock as HeadingBlockType, RichText } from '../../../types/editor'; import { useEditorStore } from '../../../stores/editorStore'; import { RichTextEditor } from '../RichTextEditor'; import { resolveBlockStyle } from '../../../utils/styleResolver'; import { BlockMenu } from './BlockMenu'; import './HeadingBlock.css'; export interface HeadingBlockProps { block: HeadingBlockType; readOnly?: boolean; } /** * HeadingBlock - 标题块(带富文本编辑) */ export const HeadingBlock: React.FC = ({ block, readOnly, }) => { const updateBlock = useEditorStore((state) => state.updateBlock); const saveBlock = useEditorStore((state) => state.saveBlock); const deleteBlock = useEditorStore((state) => state.deleteBlock); const addBlock = useEditorStore((state) => state.addBlock); const blocks = useEditorStore((state) => state.blocks); // 解析样式 const blockStyle = resolveBlockStyle(block.word_style, block.style); // 添加对齐样式 if (block.style?.align) { blockStyle.textAlign = block.style.align; } // 查找当前块的位置 const currentIndex = blocks.findIndex((b) => b.id === block.id); const isFirst = currentIndex === 0; const isLast = currentIndex === blocks.length - 1; // 处理内容变更 - 只需调用updateBlock,store会自动处理保存 const handleChange = useCallback( (newContent: RichText[]) => { updateBlock(block.id, { content: newContent }); }, [block.id, updateBlock] ); // 处理对齐方式变更 const handleAlignChange = useCallback( (align: 'left' | 'center' | 'right' | 'justify') => { updateBlock(block.id, { style: { ...block.style, align } }); }, [block.id, block.style, updateBlock] ); const handleContentFormatChange = useCallback(async ( format: 'paragraph' | 'ordered-list' | `heading-${1 | 2 | 3 | 4 | 5 | 6}` ) => { const isParagraph = format === 'paragraph' || format === 'ordered-list'; const isOrderedList = format === 'ordered-list'; const level = isParagraph ? 0 : Number(format.split('-')[1]) as 1 | 2 | 3 | 4 | 5 | 6; const style = block.style.align ? { align: block.style.align } : {}; updateBlock(block.id, isParagraph ? { type: 'paragraph', level: 0, word_style: 'Normal', style, metadata: { parent_heading_id: block.metadata.parent_id ?? null, ...(isOrderedList ? { list_type: 'ordered' } : {}), }, } : { level, word_style: `Heading ${level}`, style, } ); try { await saveBlock(block.id); message.success( isOrderedList ? '已设为有序列表' : isParagraph ? '已设为正文' : `已设为${level}级标题` ); } catch (error) { message.error(error instanceof Error ? error.message : '标题格式更新失败'); } }, [block.id, block.metadata.parent_id, block.style.align, saveBlock, updateBlock]); // ── 块操作回调 ────────────────────────────────────────────────────────── const handleDelete = useCallback(async () => { try { await deleteBlock(block.id); message.success('已删除标题'); } catch (error) { // 错误已经在deleteBlock中处理了 } }, [block.id, deleteBlock]); const handleDuplicate = useCallback(() => { addBlock( { type: 'heading', level: block.level, content: block.content, word_style: block.word_style, style: block.style, metadata: block.metadata, }, block.id // 在当前块后插入 ); message.success('已复制标题'); }, [block, addBlock]); const handleMoveUp = useCallback(() => { if (currentIndex > 0) { const prevBlock = blocks[currentIndex - 1]; const prevOrder = currentIndex > 1 ? blocks[currentIndex - 2].block_order : -100; const newOrder = Math.floor((prevOrder + prevBlock.block_order) / 2); updateBlock(block.id, { block_order: newOrder }); message.success('已上移'); } }, [currentIndex, blocks, block.id, updateBlock]); const handleMoveDown = useCallback(() => { if (currentIndex < blocks.length - 1) { const nextBlock = blocks[currentIndex + 1]; const nextNextOrder = currentIndex < blocks.length - 2 ? blocks[currentIndex + 2].block_order : nextBlock.block_order + 200; const newOrder = Math.floor((nextBlock.block_order + nextNextOrder) / 2); updateBlock(block.id, { block_order: newOrder }); message.success('已下移'); } }, [currentIndex, blocks, block.id, updateBlock]); // ── 插入操作回调 ──────────────────────────────────────────────────────── const handleInsertHeading = useCallback((level: 1 | 2 | 3 | 4 | 5 | 6) => { addBlock( { type: 'heading' as const, level, content: '', word_style: `Heading ${level}`, style: {}, metadata: { parent_id: null, }, }, block.id // 在当前块后插入 ); message.success(`已插入${level}级标题`); }, [block.id, addBlock]); const handleInsertParagraph = useCallback(() => { addBlock( { type: 'paragraph' as const, level: 0, content: '', word_style: 'Normal', style: {}, metadata: { parent_heading_id: null, }, }, block.id // 在当前块后插入 ); message.success('已插入段落'); }, [block.id, addBlock]); const handleInsertImage = useCallback((dataUrl: string, fileName: string, width: number, height: number) => { addBlock( { type: 'image' as const, level: 0, content: dataUrl, word_style: 'Normal', style: { width, height, unit: 'cm' as const, align: 'center' as const, }, metadata: { alt: fileName, para_style: 'Normal', parent_heading_id: null, }, }, block.id // 在当前块后插入 ); }, [block.id, addBlock]); const handleInsertTable = useCallback(() => { // 创建一个5行5列的表格 const rows = 5; const cols = 5; const defaultColWidth = 95.6; // 478pt总宽 / 5列 = 95.6pt每列 const defaultRowHeight = 58; // 默认行高58磅 // 创建空单元格数组 const createEmptyCell = (colIndex: number) => ({ text: '', rowspan: 1, colspan: 1, col_index: colIndex + 1, width: defaultColWidth, style: {}, word_style: 'Normal', }); // 创建表格行 const tableRows = Array(rows).fill(null).map(() => ({ cells: Array(cols).fill(null).map((_, colIdx) => createEmptyCell(colIdx)), height: defaultRowHeight, })); addBlock( { type: 'table' as const, level: 0, index: 0, content: { rows: tableRows, col_widths: Array(cols).fill(defaultColWidth), }, word_style: 'Table Grid', // 表格专用样式 style: {}, metadata: { cols, rows, table_width: 100, table_width_unit: 'percent' as const, col_widths: Array(cols).fill(100 / cols), // 平均分配百分比 parent_heading_id: null, }, }, block.id // 在当前块后插入 ); message.success('已插入表格'); }, [block.id, addBlock]); const Tag = `h${block.level}` as keyof JSX.IntrinsicElements; return (
{/* 块操作菜单 */} {!readOnly && ( )}
); }; export default HeadingBlock;