Explorar o código

feat(编辑器): 优化富文本工具栏在表格模式下的交互与样式控制

- 移除表格模式下浮动格式工具栏的显示逻辑,表格单元格编辑时不再显示浮动工具栏
- 简化焦点处理,移除表格模式特殊的工具栏定位逻辑
- 优化颜色选择器的事件处理,添加document级别的mousedown和mousemove监听以正确关闭弹出窗口
- 增加Escape键支持,按下时关闭颜色选择器和展开的工具栏菜单
- 优化字体大小和颜色变化的状态管理流程,避免不必要的中间状态更新
- 修复颜色选择器在表格模式下的验证和关闭时序
- 添加工具栏hover状态回调,支持父组件监听工具栏交互状态
- 删除冗余的TableBorderControl、TableStylePanel组件及其样式文件,统一由TableBlock管理样式控制
Zhang Yice hai 1 mes
pai
achega
7fd5d62475

+ 5 - 23
src/components/Editor/RichTextEditor/RichTextEditor.tsx

@@ -362,15 +362,9 @@ export const RichTextEditor: React.FC<RichTextEditorProps> = ({
362 362
 
363 363
     const selection = window.getSelection();
364 364
     
365
-    // 表格模式:即使没有选中文本也显示工具栏(用于样式控制)
365
+    // 表格单元格不显示浮动格式工具栏。
366 366
     if (tableContext) {
367
-      if (!selection || selection.rangeCount === 0) return;
368
-      
369
-      setToolbarPosition({
370
-        top: 0,
371
-        left: -40,
372
-      });
373
-      
367
+      setHasTextSelection(false);
374 368
       return;
375 369
     }
376 370
     
@@ -393,24 +387,12 @@ export const RichTextEditor: React.FC<RichTextEditorProps> = ({
393 387
     
394 388
   }, [readOnly, tableContext]);
395 389
 
396
-  // ── 处理聚焦(表格模式显示工具栏) ─────────────────────────────────────────────
390
+  // ── 处理聚焦 ───────────────────────────────────────────────────────────────
397 391
   const handleFocus = useCallback(() => {
398 392
     if (readOnly) return;
399 393
 
400 394
     setIsEditorFocused(true);
401
-
402
-    if (!tableContext) return;
403
-    
404
-    // 延迟一下,确保光标已经定位
405
-    setTimeout(() => {
406
-      if (!editorRef.current) return;
407
-      
408
-      setToolbarPosition({
409
-        top: 0,
410
-        left: -40,
411
-      });
412
-    }, 50);
413
-  }, [readOnly, tableContext]);
395
+  }, [readOnly]);
414 396
 
415 397
   // ── 粘贴处理(只保留纯文本) ─────────────────────────────────────────────────
416 398
   const handlePaste = useCallback((e: React.ClipboardEvent) => {
@@ -460,7 +442,7 @@ export const RichTextEditor: React.FC<RichTextEditorProps> = ({
460 442
       />
461 443
 
462 444
       {/* 浮动工具栏放在编辑内容之后,避免 contenteditable 重绘时覆盖面板。 */}
463
-      {!readOnly && hasContent !== false && (
445
+      {!readOnly && hasContent !== false && !tableContext && (
464 446
         <RichTextToolbar
465 447
           position={toolbarPosition}
466 448
           editorElement={editorElement}

+ 70 - 12
src/components/Editor/RichTextEditor/RichTextToolbar.tsx

@@ -317,6 +317,58 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
317 317
   const [isUnderline, setIsUnderline] = useState<boolean>(false);
318 318
   const [isExpanded, setIsExpanded] = useState(false);
319 319
 
320
+  useEffect(() => {
321
+    if (!colorPickerOpen) return;
322
+
323
+    const handleDocumentMouseDown = (event: MouseEvent) => {
324
+      const target = event.target;
325
+      if (!(target instanceof Element)) return;
326
+
327
+      const isColorTrigger = Boolean(target.closest('.rich-text-color-trigger'));
328
+      const isColorPopup = Boolean(target.closest('.rich-text-color-picker'));
329
+      if (!isColorTrigger && !isColorPopup) {
330
+        setColorPickerOpen(false);
331
+      }
332
+    };
333
+
334
+    const handleDocumentMouseMove = (event: MouseEvent) => {
335
+      const target = event.target;
336
+      if (!(target instanceof Element)) return;
337
+
338
+      const isEditorArea = editorElement?.contains(target) ?? false;
339
+      const isToolbarArea = Boolean(target.closest(
340
+        '.rich-text-toolbar, .rich-text-toolbar-dropdown, .rich-text-color-picker, .ant-popover',
341
+      ));
342
+
343
+      if (!isEditorArea && !isToolbarArea) {
344
+        setColorPickerOpen(false);
345
+      }
346
+    };
347
+
348
+    document.addEventListener('mousedown', handleDocumentMouseDown);
349
+    document.addEventListener('mousemove', handleDocumentMouseMove);
350
+    return () => {
351
+      document.removeEventListener('mousedown', handleDocumentMouseDown);
352
+      document.removeEventListener('mousemove', handleDocumentMouseMove);
353
+    };
354
+  }, [colorPickerOpen, editorElement]);
355
+
356
+  useEffect(() => {
357
+    if (!isExpanded && !colorPickerOpen) return;
358
+
359
+    const handleKeyDown = (event: KeyboardEvent) => {
360
+      if (event.key !== 'Escape') return;
361
+
362
+      event.preventDefault();
363
+      setColorPickerOpen(false);
364
+      setIsExpanded(false);
365
+      onClose();
366
+    };
367
+
368
+    document.addEventListener('keydown', handleKeyDown);
369
+    return () => document.removeEventListener('keydown', handleKeyDown);
370
+  }, [isExpanded, colorPickerOpen, onClose]);
371
+
320 372
   // 判断是否处于表格编辑模式
321 373
   const isTableMode = !!tableContext;
322 374
 
@@ -514,11 +566,10 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
514 566
   const handleFontSizeChange = useCallback((value: number | null) => {
515 567
     if (value === null || value < 8 || value > 72) return;
516 568
     
517
-    setFontSize(value);
518
-    
519 569
     if (isTableMode && tableContext) {
520 570
       // 表格模式
521 571
       tableContext.onStyleChange({ font_size: value });
572
+      setFontSize(value);
522 573
       return;
523 574
     }
524 575
     
@@ -537,22 +588,23 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
537 588
       return;
538 589
     }
539 590
     
591
+    setFontSize(value);
540 592
     savedRangeRef.current = saveSelection();
541 593
     onFormat();
542 594
   }, [getEditorElement, isTableMode, tableContext, onFormat]);
543 595
 
544 596
   // ── 处理颜色变化 ───────────────────────────────────────────────────────────
545 597
   const handleColorChange = useCallback((color: string) => {
546
-    setCurrentColor(color);
547
-    
548 598
     if (isTableMode && tableContext) {
549 599
       // 表格模式
550 600
       const hexColor = color.trim().replace(/^#/, '').toUpperCase();
551 601
       if (!/^[0-9A-F]{3,8}$/.test(hexColor)) {
602
+        setColorPickerOpen(false);
552 603
         return;
553 604
       }
554 605
       tableContext.onStyleChange({ color: hexColor });
555
-      setTimeout(() => setColorPickerOpen(false), 100);
606
+      setCurrentColor(`#${hexColor}`);
607
+      setColorPickerOpen(false);
556 608
       return;
557 609
     }
558 610
     
@@ -560,14 +612,14 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
560 612
     const restored = restoreSavedOrCurrentSelection(savedRangeRef.current, getEditorElement());
561 613
     
562 614
     if (!restored) {
563
-      setTimeout(() => setColorPickerOpen(false), 100);
615
+      setColorPickerOpen(false);
564 616
       return;
565 617
     }
566 618
     
567 619
     const selection = window.getSelection();
568 620
     
569 621
     if (!selection || selection.rangeCount === 0) {
570
-      setTimeout(() => setColorPickerOpen(false), 100);
622
+      setColorPickerOpen(false);
571 623
       return;
572 624
     }
573 625
     
@@ -586,12 +638,16 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
586 638
           spanElement.classList.add('colored-text');
587 639
         }
588 640
       }
641
+      setCurrentColor(color);
642
+    } else {
643
+      setColorPickerOpen(false);
644
+      return;
589 645
     }
590 646
     
591 647
     savedRangeRef.current = saveSelection();
592 648
     onFormat();
593 649
     
594
-    setTimeout(() => setColorPickerOpen(false), 100);
650
+    setColorPickerOpen(false);
595 651
   }, [getEditorElement, isTableMode, tableContext, onFormat]);
596 652
 
597 653
   // ── 处理对齐方式 ──────────────────────────────────────────────────────────
@@ -734,6 +790,8 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
734 790
       ref={toolbarRef}
735 791
       className="rich-text-toolbar expanded"
736 792
       onMouseDown={handleMouseDown}
793
+      onMouseEnter={() => onHoverChange?.(true)}
794
+      onMouseLeave={() => onHoverChange?.(false)}
737 795
     >
738 796
       {!isTableMode && (
739 797
         <>
@@ -783,14 +841,14 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
783 841
           />
784 842
         </Tooltip>
785 843
         <Popover
786
-          open={colorPickerOpen}
844
+          open={colorPickerOpen && visible}
787 845
           onOpenChange={setColorPickerOpen}
788 846
           content={colorContent}
789 847
           trigger="click"
790 848
           placement="bottom"
791 849
           overlayClassName="rich-text-color-picker"
792 850
         >
793
-          <button type="button" className="toolbar-icon-button" title="文字颜色">
851
+          <button type="button" className="toolbar-icon-button rich-text-color-trigger" title="文字颜色">
794 852
             <FontColorsOutlined style={{ color: currentColor }} />
795 853
           </button>
796 854
         </Popover>
@@ -839,7 +897,7 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
839 897
 
840 898
   const toolbar = (
841 899
     <Dropdown
842
-      open={isExpanded}
900
+      open={isExpanded && visible}
843 901
       onOpenChange={(open) => {
844 902
         setIsExpanded(open);
845 903
         if (!open) {
@@ -847,9 +905,9 @@ export const RichTextToolbar: React.FC<RichTextToolbarProps> = ({
847 905
           onClose();
848 906
         }
849 907
       }}
850
-      trigger={['click']}
851 908
       placement="bottomLeft"
852 909
       overlayClassName="rich-text-toolbar-dropdown"
910
+      trigger={['click']}
853 911
       popupRender={() => expandedToolbar}
854 912
     >
855 913
       <div

+ 0 - 10
src/components/Editor/blocks/TableBlock.tsx

@@ -9,7 +9,6 @@ import type { TableBlock as TableBlockType, TableCell as TableCellType } from '.
9 9
 import { useEditorStore } from '../../../stores/editorStore';
10 10
 import { TableCell } from './TableCell';
11 11
 import { TableToolbar } from './TableToolbar';
12
-import { TableStylePanel } from './TableStylePanel';
13 12
 import { TableResizeHandle } from './TableResizeHandle';
14 13
 import { useTableResize } from '../../../hooks/useTableResize';
15 14
 import { getTableVisualCellPositions } from '../../../utils/blockOperations';
@@ -364,15 +363,6 @@ export const TableBlock: React.FC<TableBlockProps> = ({
364 363
         />
365 364
       )}
366 365
 
367
-      {!readOnly && selectedCell && (
368
-        <TableStylePanel
369
-          block={block}
370
-          selectedCell={selectedCell}
371
-          selectedRange={selectedRange}
372
-          selectedVisualRange={selectedVisualRange}
373
-        />
374
-      )}
375
-
376 366
       {/* 拖动尺寸提示 */}
377 367
       {resizeState?.isResizing && currentSizeTooltip && (
378 368
         <div

+ 0 - 145
src/components/Editor/blocks/TableBorderControl.tsx

@@ -1,145 +0,0 @@
1
-/**
2
- * TableBorderControl.tsx - 表格边框控制组件
3
- * 
4
- * 支持设置单元格边框样式
5
- * 
6
- * @module components/Editor/blocks
7
- */
8
-
9
-import React, { useCallback, useMemo } from 'react';
10
-import { Button, Space, ColorPicker, InputNumber } from 'antd';
11
-import type { Color } from 'antd/es/color-picker';
12
-import { BorderOutlined } from '@ant-design/icons';
13
-import type { TableBlock } from '../../../types/editor';
14
-import { useEditorStore } from '../../../stores/editorStore';
15
-import { getTableVisualCellPositions } from '../../../utils/blockOperations';
16
-
17
-// ══════════════════════════════════════════════════════════════════════════════
18
-// Component Props
19
-// ══════════════════════════════════════════════════════════════════════════════
20
-
21
-export interface TableBorderControlProps {
22
-  /** 表格块 */
23
-  block: TableBlock;
24
-  /** 选中的单元格 */
25
-  selectedCell: { row: number; col: number };
26
-  /** 选中的范围 */
27
-  selectedRange?: {
28
-    startRow: number;
29
-    startCol: number;
30
-    endRow: number;
31
-    endCol: number;
32
-  } | null;
33
-  selectedVisualRange?: {
34
-    rowStart: number;
35
-    rowEnd: number;
36
-    colStart: number;
37
-    colEnd: number;
38
-  } | null;
39
-}
40
-
41
-// ══════════════════════════════════════════════════════════════════════════════
42
-// Component
43
-// ══════════════════════════════════════════════════════════════════════════════
44
-
45
-/**
46
- * TableBorderControl - 表格边框控制
47
- * 
48
- * 提供单元格边框设置功能:
49
- * - 边框宽度
50
- * - 边框颜色
51
- */
52
-export const TableBorderControl: React.FC<TableBorderControlProps> = ({
53
-  block,
54
-  selectedCell,
55
-  selectedVisualRange,
56
-}) => {
57
-  const updateBlock = useEditorStore((state) => state.updateBlock);
58
-  const visualCellPositions = useMemo(
59
-    () => getTableVisualCellPositions(block),
60
-    [block],
61
-  );
62
-  
63
-  // 获取当前选中单元格的边框样式
64
-  const currentCell = block.content.rows[selectedCell.row]?.cells[selectedCell.col];
65
-  const currentStyle = currentCell?.style || {};
66
-
67
-  // 应用边框样式到选中的单元格或范围
68
-  const applyBorderStyle = useCallback((borderUpdates: {
69
-    border_width?: number;
70
-    border_color?: string;
71
-  }) => {
72
-    const styleRange = selectedVisualRange || (() => {
73
-      const position = visualCellPositions.get(`${selectedCell.row}-${selectedCell.col}`);
74
-      return position || null;
75
-    })();
76
-
77
-    const newRows = block.content.rows.map((row, rowIdx) => {
78
-      return {
79
-        ...row,
80
-        cells: row.cells.map((cell, colIdx) => {
81
-          const cellPosition = visualCellPositions.get(`${rowIdx}-${colIdx}`);
82
-          const isInRange = !!styleRange && !!cellPosition
83
-            && cellPosition.rowStart <= styleRange.rowEnd
84
-            && cellPosition.rowEnd >= styleRange.rowStart
85
-            && cellPosition.colStart <= styleRange.colEnd
86
-            && cellPosition.colEnd >= styleRange.colStart;
87
-
88
-          if (!isInRange) return cell;
89
-          
90
-          return {
91
-            ...cell,
92
-            style: {
93
-              ...cell.style,
94
-              ...borderUpdates,
95
-            },
96
-          };
97
-        }),
98
-      };
99
-    });
100
-
101
-    updateBlock(block.id, {
102
-      content: {
103
-        ...block.content,
104
-        rows: newRows,
105
-      },
106
-    });
107
-  }, [block, selectedCell, selectedVisualRange, updateBlock, visualCellPositions]);
108
-
109
-  // 设置边框宽度
110
-  const setBorderWidth = useCallback((border_width: number | null) => {
111
-    if (border_width !== null && border_width >= 0) {
112
-      applyBorderStyle({ border_width });
113
-    }
114
-  }, [applyBorderStyle]);
115
-
116
-  // 设置边框颜色
117
-  const setBorderColor = useCallback((color: Color) => {
118
-    const hexColor = color.toHexString().replace(/^#/, '').toUpperCase();
119
-    applyBorderStyle({ border_color: hexColor });
120
-  }, [applyBorderStyle]);
121
-
122
-  return (
123
-    <Space.Compact>
124
-      <Button size="small" icon={<BorderOutlined />} disabled title="边框" />
125
-      <InputNumber
126
-        size="small"
127
-        min={0}
128
-        max={10}
129
-        step={0.5}
130
-        value={currentStyle.border_width || 1}
131
-        onChange={setBorderWidth}
132
-        style={{ width: 60 }}
133
-        placeholder="宽度"
134
-      />
135
-      <ColorPicker
136
-        size="small"
137
-        value={currentStyle.border_color ? `#${currentStyle.border_color}` : '#000000'}
138
-        onChange={setBorderColor}
139
-        showText={false}
140
-      />
141
-    </Space.Compact>
142
-  );
143
-};
144
-
145
-export default TableBorderControl;

+ 0 - 31
src/components/Editor/blocks/TableStylePanel.css

@@ -1,31 +0,0 @@
1
-/**
2
- * TableStylePanel.css - 表格样式面板样式
3
- */
4
-
5
-.table-style-panel {
6
-  position: sticky;
7
-  top: 0;
8
-  left: 0;
9
-  z-index: 100;
10
-  background: #ffffff;
11
-  border: 1px solid #e0e0e0;
12
-  border-radius: 4px;
13
-  padding: 8px 12px;
14
-  margin-bottom: 8px;
15
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
16
-}
17
-
18
-.table-style-panel .ant-space-compact {
19
-  background: #f5f5f5;
20
-  border-radius: 4px;
21
-  padding: 2px;
22
-}
23
-
24
-.table-style-panel .ant-btn-sm {
25
-  min-width: 32px;
26
-}
27
-
28
-.table-style-panel .ant-divider-vertical {
29
-  height: 24px;
30
-  align-self: center;
31
-}

+ 0 - 323
src/components/Editor/blocks/TableStylePanel.tsx

@@ -1,323 +0,0 @@
1
-/**
2
- * TableStylePanel.tsx - 表格样式面板
3
- * 
4
- * 用于设置单元格样式(字体、对齐、颜色等)
5
- * 
6
- * @module components/Editor/blocks
7
- */
8
-
9
-import React, { useCallback, useMemo } from 'react';
10
-import { Button, Space, ColorPicker, InputNumber, Select, Divider } from 'antd';
11
-import type { Color } from 'antd/es/color-picker';
12
-import {
13
-  BoldOutlined,
14
-  ItalicOutlined,
15
-  UnderlineOutlined,
16
-  AlignLeftOutlined,
17
-  AlignCenterOutlined,
18
-  AlignRightOutlined,
19
-  FontSizeOutlined,
20
-  FontColorsOutlined,
21
-  VerticalAlignTopOutlined,
22
-  VerticalAlignMiddleOutlined,
23
-  VerticalAlignBottomOutlined,
24
-} from '@ant-design/icons';
25
-import type { TableBlock, TableCell } from '../../../types/editor';
26
-import { useEditorStore } from '../../../stores/editorStore';
27
-import {
28
-  getTableCellsForVisualBounds,
29
-  getTableVisualCellPositions,
30
-} from '../../../utils/blockOperations';
31
-import { TableBorderControl } from './TableBorderControl';
32
-import './TableStylePanel.css';
33
-
34
-// ══════════════════════════════════════════════════════════════════════════════
35
-// Component Props
36
-// ══════════════════════════════════════════════════════════════════════════════
37
-
38
-export interface TableStylePanelProps {
39
-  /** 表格块 */
40
-  block: TableBlock;
41
-  /** 选中的单元格 */
42
-  selectedCell: { row: number; col: number };
43
-  /** 选中的范围 */
44
-  selectedRange?: {
45
-    startRow: number;
46
-    startCol: number;
47
-    endRow: number;
48
-    endCol: number;
49
-  } | null;
50
-  selectedVisualRange?: {
51
-    rowStart: number;
52
-    rowEnd: number;
53
-    colStart: number;
54
-    colEnd: number;
55
-  } | null;
56
-}
57
-
58
-// ══════════════════════════════════════════════════════════════════════════════
59
-// Component
60
-// ══════════════════════════════════════════════════════════════════════════════
61
-
62
-/**
63
- * TableStylePanel - 表格样式面板
64
- * 
65
- * 提供单元格样式设置功能:
66
- * - 字体样式(加粗、斜体、下划线)
67
- * - 文本对齐(左、中、右)
68
- * - 垂直对齐(上、中、下)
69
- * - 字号
70
- * - 字体颜色
71
- */
72
-export const TableStylePanel: React.FC<TableStylePanelProps> = ({
73
-  block,
74
-  selectedCell,
75
-  selectedRange,
76
-  selectedVisualRange,
77
-}) => {
78
-  const updateBlock = useEditorStore((state) => state.updateBlock);
79
-  const visualCellPositions = useMemo(
80
-    () => getTableVisualCellPositions(block),
81
-    [block],
82
-  );
83
-  const selectedPosition = visualCellPositions.get(`${selectedCell.row}-${selectedCell.col}`);
84
-  const styleRange = selectedVisualRange || selectedPosition;
85
-  const selectedCells = styleRange
86
-    ? getTableCellsForVisualBounds(block, styleRange)
87
-    : [];
88
-  const getCommonStyleValue = <Key extends keyof TableCell['style']>(key: Key) => {
89
-    if (selectedCells.length === 0) return undefined;
90
-    const firstValue = selectedCells[0].style?.[key];
91
-    return selectedCells.every((cell) => cell.style?.[key] === firstValue) ? firstValue : undefined;
92
-  };
93
-  const commonBold = selectedCells.length > 0 && selectedCells.every((cell) => cell.style?.bold === true);
94
-  const commonItalic = selectedCells.length > 0 && selectedCells.every((cell) => cell.style?.italic === true);
95
-  const commonUnderline = selectedCells.length > 0 && selectedCells.every((cell) => cell.style?.underline === true);
96
-  const currentStyle = {
97
-    bold: commonBold,
98
-    italic: commonItalic,
99
-    underline: commonUnderline,
100
-    align: getCommonStyleValue('align'),
101
-    valign: getCommonStyleValue('valign'),
102
-    font_size: getCommonStyleValue('font_size'),
103
-    font_name: getCommonStyleValue('font_name'),
104
-    color: getCommonStyleValue('color'),
105
-  };
106
-
107
-  // 应用样式到选中的单元格或范围
108
-  const applyStyle = useCallback((styleUpdates: Partial<TableCell['style']>) => {
109
-    const newRows = block.content.rows.map((row, rowIdx) => {
110
-      return {
111
-        ...row,
112
-        cells: row.cells.map((cell, colIdx) => {
113
-          const cellPosition = visualCellPositions.get(`${rowIdx}-${colIdx}`);
114
-          const isInRange = !!styleRange && !!cellPosition
115
-            && cellPosition.rowStart <= styleRange.rowEnd
116
-            && cellPosition.rowEnd >= styleRange.rowStart
117
-            && cellPosition.colStart <= styleRange.colEnd
118
-            && cellPosition.colEnd >= styleRange.colStart;
119
-
120
-          if (!isInRange) return cell;
121
-          
122
-          return {
123
-            ...cell,
124
-            style: {
125
-              ...cell.style,
126
-              ...styleUpdates,
127
-            },
128
-          };
129
-        }),
130
-      };
131
-    });
132
-
133
-    updateBlock(block.id, {
134
-      content: {
135
-        ...block.content,
136
-        rows: newRows,
137
-      },
138
-    });
139
-  }, [block, styleRange, updateBlock, visualCellPositions]);
140
-
141
-  // 字体样式切换
142
-  const toggleBold = useCallback(() => {
143
-    applyStyle({ bold: !currentStyle.bold });
144
-  }, [currentStyle.bold, applyStyle]);
145
-
146
-  const toggleItalic = useCallback(() => {
147
-    applyStyle({ italic: !currentStyle.italic });
148
-  }, [currentStyle.italic, applyStyle]);
149
-
150
-  const toggleUnderline = useCallback(() => {
151
-    applyStyle({ underline: !currentStyle.underline });
152
-  }, [currentStyle.underline, applyStyle]);
153
-
154
-  // 文本对齐
155
-  const setTextAlign = useCallback((align: 'left' | 'center' | 'right' | 'justify') => {
156
-    applyStyle({ align });
157
-  }, [applyStyle]);
158
-
159
-  // 垂直对齐
160
-  const setVerticalAlign = useCallback((valign: 'top' | 'middle' | 'bottom') => {
161
-    applyStyle({ valign });
162
-  }, [applyStyle]);
163
-
164
-  // 字号
165
-  const setFontSize = useCallback((font_size: number | null) => {
166
-    if (font_size && font_size > 0) {
167
-      applyStyle({ font_size });
168
-    }
169
-  }, [applyStyle]);
170
-
171
-  // 字体颜色
172
-  const setFontColor = useCallback((color: Color) => {
173
-    const hexColor = color.toHexString().replace(/^#/, '').toUpperCase();
174
-    applyStyle({ color: hexColor });
175
-  }, [applyStyle]);
176
-
177
-  // 字体
178
-  const setFontFamily = useCallback((font_name: string) => {
179
-    applyStyle({ font_name });
180
-  }, [applyStyle]);
181
-
182
-  return (
183
-    <div className="table-style-panel">
184
-      <Space size="small" wrap>
185
-        {/* 字体样式 */}
186
-        <Space.Compact>
187
-          <Button
188
-            type={currentStyle.bold ? 'primary' : 'default'}
189
-            size="small"
190
-            icon={<BoldOutlined />}
191
-            onClick={toggleBold}
192
-            title="加粗"
193
-          />
194
-          <Button
195
-            type={currentStyle.italic ? 'primary' : 'default'}
196
-            size="small"
197
-            icon={<ItalicOutlined />}
198
-            onClick={toggleItalic}
199
-            title="斜体"
200
-          />
201
-          <Button
202
-            type={currentStyle.underline ? 'primary' : 'default'}
203
-            size="small"
204
-            icon={<UnderlineOutlined />}
205
-            onClick={toggleUnderline}
206
-            title="下划线"
207
-          />
208
-        </Space.Compact>
209
-
210
-        <Divider type="vertical" style={{ margin: 0 }} />
211
-
212
-        {/* 文本对齐 */}
213
-        <Space.Compact>
214
-          <Button
215
-            type={currentStyle.align === 'left' ? 'primary' : 'default'}
216
-            size="small"
217
-            icon={<AlignLeftOutlined />}
218
-            onClick={() => setTextAlign('left')}
219
-            title="左对齐"
220
-          />
221
-          <Button
222
-            type={currentStyle.align === 'center' || !currentStyle.align ? 'primary' : 'default'}
223
-            size="small"
224
-            icon={<AlignCenterOutlined />}
225
-            onClick={() => setTextAlign('center')}
226
-            title="居中对齐"
227
-          />
228
-          <Button
229
-            type={currentStyle.align === 'right' ? 'primary' : 'default'}
230
-            size="small"
231
-            icon={<AlignRightOutlined />}
232
-            onClick={() => setTextAlign('right')}
233
-            title="右对齐"
234
-          />
235
-        </Space.Compact>
236
-
237
-        <Divider type="vertical" style={{ margin: 0 }} />
238
-
239
-        {/* 垂直对齐 */}
240
-        <Space.Compact>
241
-          <Button
242
-            type={currentStyle.valign === 'top' ? 'primary' : 'default'}
243
-            size="small"
244
-            icon={<VerticalAlignTopOutlined />}
245
-            onClick={() => setVerticalAlign('top')}
246
-            title="顶部对齐"
247
-          />
248
-          <Button
249
-            type={currentStyle.valign === 'middle' || !currentStyle.valign ? 'primary' : 'default'}
250
-            size="small"
251
-            icon={<VerticalAlignMiddleOutlined />}
252
-            onClick={() => setVerticalAlign('middle')}
253
-            title="垂直居中"
254
-          />
255
-          <Button
256
-            type={currentStyle.valign === 'bottom' ? 'primary' : 'default'}
257
-            size="small"
258
-            icon={<VerticalAlignBottomOutlined />}
259
-            onClick={() => setVerticalAlign('bottom')}
260
-            title="底部对齐"
261
-          />
262
-        </Space.Compact>
263
-
264
-        <Divider type="vertical" style={{ margin: 0 }} />
265
-
266
-        {/* 字号 */}
267
-        <Space.Compact>
268
-          <Button size="small" icon={<FontSizeOutlined />} disabled title="字号" />
269
-          <InputNumber
270
-            size="small"
271
-            min={8}
272
-            max={72}
273
-            value={currentStyle.font_size || 12}
274
-            onChange={setFontSize}
275
-            style={{ width: 60 }}
276
-          />
277
-        </Space.Compact>
278
-
279
-        {/* 字体 */}
280
-        <Select
281
-          size="small"
282
-          value={currentStyle.font_name || '宋体'}
283
-          onChange={setFontFamily}
284
-          style={{ width: 100 }}
285
-          options={[
286
-            { label: '宋体', value: '宋体' },
287
-            { label: '黑体', value: '黑体' },
288
-            { label: '楷体', value: '楷体' },
289
-            { label: '仿宋', value: '仿宋' },
290
-            { label: '微软雅黑', value: '微软雅黑' },
291
-            { label: 'Arial', value: 'Arial' },
292
-            { label: 'Times New Roman', value: 'Times New Roman' },
293
-          ]}
294
-        />
295
-
296
-        {/* 字体颜色 */}
297
-        <ColorPicker
298
-          size="small"
299
-          value={currentStyle.color ? `#${currentStyle.color}` : '#000000'}
300
-          onChange={setFontColor}
301
-          showText={() => (
302
-            <Space size={4}>
303
-              <FontColorsOutlined />
304
-              颜色
305
-            </Space>
306
-          )}
307
-        />
308
-
309
-        <Divider type="vertical" style={{ margin: 0 }} />
310
-
311
-        {/* 边框控制 */}
312
-        <TableBorderControl
313
-          block={block}
314
-          selectedCell={selectedCell}
315
-          selectedRange={selectedRange}
316
-          selectedVisualRange={selectedVisualRange}
317
-        />
318
-      </Space>
319
-    </div>
320
-  );
321
-};
322
-
323
-export default TableStylePanel;