| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- /**
- * 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<HeadingBlockProps> = ({
- 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 (
- <div className="heading-block-wrapper" data-block-id={block.id} style={{ position: 'relative' }}>
- {/* 块操作菜单 */}
- {!readOnly && (
- <BlockMenu
- blockId={block.id}
- isFirst={isFirst}
- isLast={isLast}
- onDelete={handleDelete}
- onDuplicate={handleDuplicate}
- onMoveUp={handleMoveUp}
- onMoveDown={handleMoveDown}
- onInsertHeading={handleInsertHeading}
- onInsertParagraph={handleInsertParagraph}
- onInsertImage={handleInsertImage}
- onInsertTable={handleInsertTable}
- />
- )}
- <Tag className={`heading-block heading-${block.level}`}>
- <RichTextEditor
- value={block.content}
- onChange={handleChange}
- currentContentFormat={`heading-${block.level}`}
- onContentFormatChange={handleContentFormatChange}
- onAlignChange={handleAlignChange}
- currentAlign={block.style?.align}
- readOnly={readOnly}
- placeholder={`${block.level}级标题`}
- singleLine={true}
- baseStyle={blockStyle}
- />
- </Tag>
- </div>
- );
- };
- export default HeadingBlock;
|