/** * TableToolbar.tsx - 表格工具栏 * * @module components/Editor/blocks */ import React, { useCallback } from 'react'; import { Button, Space, Divider, message, Popconfirm } from 'antd'; import { MinusOutlined, ArrowUpOutlined, ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, MergeCellsOutlined, SplitCellsOutlined, DeleteOutlined, } from '@ant-design/icons'; import type { TableBlock } from '../../../types/editor'; import { useEditorStore } from '../../../stores/editorStore'; import { TableWidthControl } from './TableWidthControl'; import { insertTableRow, insertTableRowBefore, insertTableColumn, insertTableColumnBefore, deleteTableRow, deleteTableColumn, mergeCellsByVisualBounds, splitCell, getTableSelectionBounds, getTableCellRangeForVisualBounds, getTableCellReference, } from '../../../utils/blockOperations'; import './TableToolbar.css'; function getOperationError(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback; } // ══════════════════════════════════════════════════════════════════════════════ // Component Props // ══════════════════════════════════════════════════════════════════════════════ export interface TableToolbarProps { /** 表格块 */ block: TableBlock; /** 选中的单元格 */ selectedCell: { row: number; col: number }; /** 选中的范围 */ selectedRange?: { startRow: number; startCol: number; endRow: number; endCol: number; } | null; selectedVisualRange?: { rowStart: number; rowEnd: number; colStart: number; colEnd: number; } | null; /** 关闭回调 */ onClose: () => void; } // ══════════════════════════════════════════════════════════════════════════════ // Component // ══════════════════════════════════════════════════════════════════════════════ /** * TableToolbar - 表格工具栏 * * 提供表格行列操作: * - 插入行/列 * - 删除行/列 * - 合并单元格 */ export const TableToolbar: React.FC = ({ block, selectedCell, selectedRange, selectedVisualRange, onClose, }) => { const updateBlock = useEditorStore((state) => state.updateBlock); const deleteBlock = useEditorStore((state) => state.deleteBlock); const rowCount = block.content.rows.length; const columnCount = block.metadata.cols; const hasValidSelectedCell = Number.isInteger(selectedCell.row) && Number.isInteger(selectedCell.col) && selectedCell.row >= 0 && selectedCell.row < rowCount && selectedCell.col >= 0 && selectedCell.col < columnCount && !!block.content.rows[selectedCell.row]?.cells[selectedCell.col] && (block.content.rows[selectedCell.row]?.cells[selectedCell.col]?.rowspan ?? 1) > 0 && (block.content.rows[selectedCell.row]?.cells[selectedCell.col]?.colspan ?? 1) > 0; const selectionBounds = selectedVisualRange || (selectedRange ? getTableSelectionBounds( block, selectedRange.startRow, selectedRange.startCol, selectedRange.endRow, selectedRange.endCol, ) : null); const visualCellRange = selectionBounds ? getTableCellRangeForVisualBounds(block, selectionBounds) : null; const hasValidRange = !!selectionBounds && !!visualCellRange; const isMultiCellRange = hasValidRange && (selectionBounds.rowEnd > selectionBounds.rowStart || selectionBounds.colEnd > selectionBounds.colStart); const selectedCellData = hasValidSelectedCell ? block.content.rows[selectedCell.row]?.cells[selectedCell.col] : undefined; const selectedReference = getTableCellReference(block, selectedCell.row, selectedCell.col); const operationRow = selectedReference?.visual.rowStart ?? selectedCell.row; const operationCol = selectedReference?.visual.colStart ?? selectedCell.col; const canEditTableStructure = hasValidSelectedCell; const isMergedCell = hasValidSelectedCell && ((selectedCellData?.rowspan ?? 1) > 1 || (selectedCellData?.colspan ?? 1) > 1); // 插入行 const handleInsertRow = useCallback(() => { if (!canEditTableStructure) return; try { const newBlock = insertTableRow(block, operationRow); updateBlock(block.id, { content: newBlock.content, metadata: newBlock.metadata, }); message.success('已插入行'); } catch (error: unknown) { message.error(getOperationError(error, '插入行失败')); } }, [block, canEditTableStructure, operationRow, updateBlock]); // 在上方插入行 const handleInsertRowBefore = useCallback(() => { if (!canEditTableStructure) return; try { const newBlock = insertTableRowBefore(block, operationRow); updateBlock(block.id, { content: newBlock.content, metadata: newBlock.metadata, }); message.success('已在上方插入行'); } catch (error: unknown) { message.error(getOperationError(error, '插入行失败')); } }, [block, canEditTableStructure, operationRow, updateBlock]); // 插入列 const handleInsertColumn = useCallback(() => { if (!canEditTableStructure) return; try { const newBlock = insertTableColumn(block, operationCol); updateBlock(block.id, { content: newBlock.content, metadata: newBlock.metadata, }); message.success('已插入列'); } catch (error: unknown) { message.error(getOperationError(error, '插入列失败')); } }, [block, canEditTableStructure, operationCol, updateBlock]); // 在左侧插入列 const handleInsertColumnBefore = useCallback(() => { if (!canEditTableStructure) return; try { const newBlock = insertTableColumnBefore(block, operationCol); updateBlock(block.id, { content: newBlock.content, metadata: newBlock.metadata, }); message.success('已在左侧插入列'); } catch (error: unknown) { message.error(getOperationError(error, '插入列失败')); } }, [block, canEditTableStructure, operationCol, updateBlock]); // 删除行 const handleDeleteRow = useCallback(() => { if (!canEditTableStructure) return; try { const newBlock = deleteTableRow(block, operationRow); updateBlock(block.id, { content: newBlock.content, metadata: newBlock.metadata, }); message.success('已删除行'); onClose(); } catch (error: unknown) { message.error(getOperationError(error, '删除行失败')); } }, [block, canEditTableStructure, operationRow, updateBlock, onClose]); // 删除列 const handleDeleteColumn = useCallback(() => { if (!canEditTableStructure) return; try { const newBlock = deleteTableColumn(block, operationCol); updateBlock(block.id, { content: newBlock.content, metadata: newBlock.metadata, }); message.success('已删除列'); onClose(); } catch (error: unknown) { message.error(getOperationError(error, '删除列失败')); } }, [block, canEditTableStructure, operationCol, updateBlock, onClose]); // 合并单元格 const handleMergeCells = useCallback(() => { if (!isMultiCellRange || !visualCellRange) { message.warning('请先选择要合并的单元格范围(Shift+点击)'); return; } try { const newBlock = mergeCellsByVisualBounds(block, selectionBounds); updateBlock(block.id, { content: newBlock.content, }); message.success('已合并单元格'); onClose(); } catch (error: unknown) { message.error(getOperationError(error, '合并单元格失败')); } }, [block, isMultiCellRange, selectionBounds, visualCellRange, updateBlock, onClose]); // 拆分单元格 const handleSplitCell = useCallback(() => { if (!hasValidSelectedCell || !isMergedCell) return; try { const newBlock = splitCell(block, selectedCell.row, selectedCell.col); updateBlock(block.id, { content: newBlock.content, }); message.success('已拆分单元格'); onClose(); } catch (error: unknown) { message.error(getOperationError(error, '拆分单元格失败')); } }, [block, hasValidSelectedCell, isMergedCell, selectedCell, updateBlock, onClose]); // 删除整个表格 const handleDeleteTable = useCallback(async () => { try { await deleteBlock(block.id); message.success('已删除表格'); onClose(); } catch (error: unknown) { message.error(getOperationError(error, '删除表格失败')); } }, [block.id, deleteBlock, onClose]); return (
{/* 表格宽度控制 */} {/* 插入行 */} {/* 插入列 */} {/* 合并单元格 */} {/* 拆分单元格 */} {/* 删除行 */} {/* 删除列 */} {/* 删除表格 */}
); }; export default TableToolbar;