| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- /**
- * 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,
- getTableVisualCellPositions,
- } 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<TableToolbarProps> = ({
- 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 selectedVisualPosition = getTableVisualCellPositions(block).get(
- `${selectedCell.row}-${selectedCell.col}`,
- );
- const operationRow = selectedVisualPosition?.rowStart ?? selectedCell.row;
- const operationCol = selectedVisualPosition?.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 (
- <div className="table-toolbar">
- <Space size="small">
- {/* 表格宽度控制 */}
- <TableWidthControl
- key={`${block.id}-${block.metadata.table_width}-${block.metadata.table_width_unit}`}
- block={block}
- />
-
- <Divider type="vertical" style={{ margin: '0 4px' }} />
- {/* 插入行 */}
- <Button
- type="text"
- size="small"
- icon={<ArrowUpOutlined />}
- onClick={handleInsertRowBefore}
- disabled={!canEditTableStructure}
- title={canEditTableStructure ? '在上方插入行' : '请选择未合并的可见单元格'}
- >
- 上方插入行
- </Button>
- <Button
- type="text"
- size="small"
- icon={<ArrowDownOutlined />}
- onClick={handleInsertRow}
- disabled={!canEditTableStructure}
- title={canEditTableStructure ? '在下方插入行' : '请选择未合并的可见单元格'}
- >
- 在下方插入行
- </Button>
- {/* 插入列 */}
- <Button
- type="text"
- size="small"
- icon={<ArrowLeftOutlined />}
- onClick={handleInsertColumnBefore}
- disabled={!canEditTableStructure}
- title={canEditTableStructure ? '在左侧插入列' : '请选择未合并的可见单元格'}
- >
- 左侧插入列
- </Button>
- <Button
- type="text"
- size="small"
- icon={<ArrowRightOutlined />}
- onClick={handleInsertColumn}
- disabled={!canEditTableStructure}
- title={canEditTableStructure ? '在右侧插入列' : '请选择未合并的可见单元格'}
- >
- 在右侧插入列
- </Button>
- <Divider type="vertical" style={{ margin: '0 4px' }} />
- {/* 合并单元格 */}
- <Button
- type="text"
- size="small"
- icon={<MergeCellsOutlined />}
- onClick={handleMergeCells}
- disabled={!isMultiCellRange}
- title={isMultiCellRange ? '合并选中的单元格' : '请选择连续且不截断已有合并的单元格'}
- >
- 合并单元格
- </Button>
- {/* 拆分单元格 */}
- <Button
- type="text"
- size="small"
- icon={<SplitCellsOutlined />}
- onClick={handleSplitCell}
- disabled={!isMergedCell}
- title={isMergedCell ? '拆分此单元格' : '此单元格未合并'}
- >
- 拆分单元格
- </Button>
- <Divider type="vertical" style={{ margin: '0 4px' }} />
- {/* 删除行 */}
- <Popconfirm
- title="确定删除此行?"
- onConfirm={handleDeleteRow}
- okText="确定"
- cancelText="取消"
- >
- <Button
- type="text"
- size="small"
- icon={<MinusOutlined />}
- danger
- disabled={!canEditTableStructure || rowCount <= 1}
- title={canEditTableStructure ? '删除当前行' : '请选择未合并的可见单元格'}
- >
- 删除行
- </Button>
- </Popconfirm>
- {/* 删除列 */}
- <Popconfirm
- title="确定删除此列?"
- onConfirm={handleDeleteColumn}
- okText="确定"
- cancelText="取消"
- >
- <Button
- type="text"
- size="small"
- icon={<MinusOutlined />}
- danger
- disabled={!canEditTableStructure || columnCount <= 1}
- title={canEditTableStructure ? '删除当前列' : '请选择未合并的可见单元格'}
- >
- 删除列
- </Button>
- </Popconfirm>
- <Divider type="vertical" style={{ margin: '0 4px' }} />
- {/* 删除表格 */}
- <Popconfirm
- title="确定删除整个表格?"
- description="此操作不可恢复,表格中的所有数据将被删除"
- onConfirm={handleDeleteTable}
- okText="确定"
- cancelText="取消"
- okButtonProps={{ danger: true }}
- >
- <Button
- type="text"
- size="small"
- icon={<DeleteOutlined />}
- danger
- title="删除整个表格"
- >
- 删除表格
- </Button>
- </Popconfirm>
- </Space>
- </div>
- );
- };
- export default TableToolbar;
|