| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- /**
- * useTableResize.ts - 表格调整大小Hook
- *
- * 实现高性能的表格列宽/行高拖拽调整功能
- * 参考Notion、飞书等主流编辑器的最佳实践
- */
- import { useState, useEffect, useCallback } from 'react';
- import type { RefObject } from 'react';
- import {
- getResizeBoundaries,
- isNearBoundary,
- pixelToPt,
- normalizePercents,
- percentToPtWidths,
- type ResizeBoundary
- } from '../utils/tableUtils';
- // ══════════════════════════════════════════════════════════════════════════════
- // Types
- // ══════════════════════════════════════════════════════════════════════════════
- /**
- * 拖拽状态
- */
- export interface ResizeState {
- isResizing: boolean; // 是否正在拖拽
- resizeType: 'column' | 'row'; // 拖拽类型
- targetIndex: number; // 目标列/行索引
- startPos: { x: number; y: number }; // 拖拽起始坐标
- currentPos: { x: number; y: number }; // 当前鼠标坐标
- originalSize: number; // 原始尺寸(百分比或pt)
- minSize: number; // 最小约束(px)
- startLinePosition: number; // 拖拽线起始位置(px)
- tableWidthPx: number; // 拖拽开始时的表格宽度(px)
- }
- /**
- * Hover状态
- */
- export interface HoverState {
- isHovering: boolean;
- boundary: ResizeBoundary | null;
- }
- /**
- * Hook参数
- */
- export interface UseTableResizeOptions {
- /** 表格元素引用 */
- tableRef: RefObject<HTMLTableElement>;
- /** 列宽数组(百分比) */
- colWidths: number[];
- /** 行高数组(pt) */
- rowHeights: number[];
- /** 表格总宽度百分比 */
- tableWidth: number;
- /** 是否只读 */
- readOnly?: boolean;
- /** 列宽变更回调 */
- onColumnResize?: (colWidths: number[], colWidthsPt: number[]) => void;
- /** 行高变更回调 */
- onRowResize?: (rowIndex: number, heightPt: number) => void;
- }
- /**
- * Hook返回值
- */
- export interface UseTableResizeReturn {
- /** 拖拽状态 */
- resizeState: ResizeState | null;
- /** Hover状态 */
- hoverState: HoverState;
- /** 虚拟拖拽线位置(px) */
- resizeLinePosition: number | null;
- /** 当前尺寸提示 */
- currentSizeTooltip: string | null;
- }
- // ══════════════════════════════════════════════════════════════════════════════
- // Constants
- // ══════════════════════════════════════════════════════════════════════════════
- const MIN_CELL_SIZE = 40; // 最小单元格尺寸(px)
- const HOVER_THRESHOLD = 5; // Hover检测阈值(px)
- // ══════════════════════════════════════════════════════════════════════════════
- // Hook Implementation
- // ══════════════════════════════════════════════════════════════════════════════
- /**
- * 表格调整大小Hook
- */
- export function useTableResize(options: UseTableResizeOptions): UseTableResizeReturn {
- const {
- tableRef,
- colWidths,
- rowHeights,
- tableWidth,
- readOnly = false,
- onColumnResize,
- onRowResize,
- } = options;
- // ────────────────────────────────────────────────────────────────────────────
- // State
- // ────────────────────────────────────────────────────────────────────────────
- const [resizeState, setResizeState] = useState<ResizeState | null>(null);
- const [hoverState, setHoverState] = useState<HoverState>({
- isHovering: false,
- boundary: null,
- });
- const [resizeLinePosition, setResizeLinePosition] = useState<number | null>(null);
- // ────────────────────────────────────────────────────────────────────────────
- // Hover Detection
- // ────────────────────────────────────────────────────────────────────────────
- /**
- * 处理鼠标移动 - 检测边界Hover
- */
- const handleTableMouseMove = useCallback(
- (e: MouseEvent) => {
- if (readOnly || resizeState?.isResizing) return;
- const table = tableRef.current;
- if (!table) return;
- const rect = table.getBoundingClientRect();
- const mouseX = e.clientX - rect.left;
- const mouseY = e.clientY - rect.top;
- // 获取所有边界
- const boundaries = getResizeBoundaries(table);
- // 查找鼠标附近的边界
- const hoveredBoundary = boundaries.find((boundary: ResizeBoundary) =>
- isNearBoundary({ x: mouseX, y: mouseY }, boundary, HOVER_THRESHOLD)
- );
- if (hoveredBoundary) {
- setHoverState({
- isHovering: true,
- boundary: hoveredBoundary,
- });
- // 边界位置需要加上表格的偏移量(考虑padding等)
- setResizeLinePosition(hoveredBoundary.position);
- table.style.cursor = hoveredBoundary.type === 'column' ? 'col-resize' : 'row-resize';
- } else {
- setHoverState({
- isHovering: false,
- boundary: null,
- });
- setResizeLinePosition(null);
- table.style.cursor = 'default';
- }
- },
- [tableRef, readOnly, resizeState]
- );
- /**
- * 处理鼠标离开表格
- */
- const handleTableMouseLeave = useCallback(() => {
- if (!resizeState?.isResizing) {
- setHoverState({
- isHovering: false,
- boundary: null,
- });
- setResizeLinePosition(null);
- if (tableRef.current) {
- tableRef.current.style.cursor = 'default';
- }
- }
- }, [tableRef, resizeState]);
- // ────────────────────────────────────────────────────────────────────────────
- // Resize Start
- // ────────────────────────────────────────────────────────────────────────────
- /**
- * 开始拖拽
- */
- const handleResizeStart = useCallback(
- (e: MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- if (readOnly || !hoverState.boundary) return;
- const table = tableRef.current;
- if (!table) return;
- const boundary = hoverState.boundary;
- const originalSize =
- boundary.type === 'column'
- ? colWidths[boundary.index]
- : rowHeights[boundary.index];
- setResizeState({
- isResizing: true,
- resizeType: boundary.type,
- targetIndex: boundary.index,
- startPos: { x: e.clientX, y: e.clientY },
- currentPos: { x: e.clientX, y: e.clientY },
- originalSize,
- minSize: MIN_CELL_SIZE,
- startLinePosition: boundary.position,
- tableWidthPx: table.offsetWidth,
- });
- // 更新拖拽线初始位置
- setResizeLinePosition(boundary.position);
- // 添加全局样式
- document.body.style.cursor = boundary.type === 'column' ? 'col-resize' : 'row-resize';
- document.body.style.userSelect = 'none';
- },
- [tableRef, hoverState, colWidths, rowHeights, readOnly]
- );
- // ────────────────────────────────────────────────────────────────────────────
- // Resize Move
- // ────────────────────────────────────────────────────────────────────────────
- const handleResizeMove = useCallback((e: MouseEvent) => {
- const currentPos = { x: e.clientX, y: e.clientY };
- setResizeState((currentState) =>
- currentState?.isResizing
- ? { ...currentState, currentPos }
- : currentState
- );
- }, []);
- // ────────────────────────────────────────────────────────────────────────────
- // Resize End
- // ────────────────────────────────────────────────────────────────────────────
- /**
- * 结束拖拽 - 计算最终尺寸并更新
- */
- const handleResizeEnd = useCallback(
- (e: MouseEvent) => {
- if (!resizeState?.isResizing) return;
- const table = tableRef.current;
- if (!table) return;
- // 计算最终偏移量
- const finalOffset =
- resizeState.resizeType === 'column'
- ? e.clientX - resizeState.startPos.x
- : e.clientY - resizeState.startPos.y;
- if (resizeState.resizeType === 'column') {
- // ──────────────────────────────────────────────────────────────────────
- // 列宽调整
- // ──────────────────────────────────────────────────────────────────────
- const tableWidthPx = table.offsetWidth;
- if (tableWidthPx <= 0) {
- setResizeState(null);
- setResizeLinePosition(null);
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- return;
- }
- const deltaPercent = (finalOffset / tableWidthPx) * 100;
- const newColWidths = [...colWidths];
- let newWidth = resizeState.originalSize + deltaPercent;
- // 调整相邻列以保持总宽度不变
- if (resizeState.targetIndex < newColWidths.length - 1) {
- const nextColWidth = newColWidths[resizeState.targetIndex + 1];
- const widthDiff = newWidth - resizeState.originalSize;
- const newNextWidth = nextColWidth - widthDiff;
- // 最小宽度约束(5%)
- if (newWidth < 5) {
- newWidth = 5;
- } else if (newNextWidth < 5) {
- newWidth = resizeState.originalSize + (nextColWidth - 5);
- }
- newColWidths[resizeState.targetIndex] = newWidth;
- newColWidths[resizeState.targetIndex + 1] = nextColWidth - (newWidth - resizeState.originalSize);
- } else {
- // 最后一列
- newColWidths[resizeState.targetIndex] = Math.max(5, newWidth);
- }
- // 归一化百分比
- const normalizedWidths = normalizePercents(newColWidths);
- // 计算pt单位的列宽
- const colWidthsPt = percentToPtWidths(normalizedWidths, tableWidth);
- // 触发回调
- onColumnResize?.(normalizedWidths, colWidthsPt);
- } else {
- // ──────────────────────────────────────────────────────────────────────
- // 行高调整
- // ──────────────────────────────────────────────────────────────────────
- const deltaPt = pixelToPt(finalOffset);
- const newHeight = Math.max(15, resizeState.originalSize + deltaPt); // 最小15pt
- const roundedHeight = Math.round(newHeight * 10) / 10;
- // 触发回调
- onRowResize?.(resizeState.targetIndex, roundedHeight);
- }
- // 清理状态
- setResizeState(null);
- setResizeLinePosition(null);
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- },
- [resizeState, tableRef, colWidths, tableWidth, onColumnResize, onRowResize]
- );
- // ────────────────────────────────────────────────────────────────────────────
- // Event Binding
- // ────────────────────────────────────────────────────────────────────────────
- /**
- * 绑定表格Hover事件
- */
- useEffect(() => {
- const table = tableRef.current;
- if (!table || readOnly) return;
- table.addEventListener('mousemove', handleTableMouseMove);
- table.addEventListener('mouseleave', handleTableMouseLeave);
- return () => {
- table.removeEventListener('mousemove', handleTableMouseMove);
- table.removeEventListener('mouseleave', handleTableMouseLeave);
- };
- }, [tableRef, readOnly, handleTableMouseMove, handleTableMouseLeave]);
- /**
- * 绑定全局拖拽事件
- */
- useEffect(() => {
- if (resizeState?.isResizing) {
- // 开始拖拽时添加mousedown监听
- const table = tableRef.current;
- if (table && hoverState.boundary) {
- table.addEventListener('mousedown', handleResizeStart);
- }
- window.addEventListener('mousemove', handleResizeMove);
- window.addEventListener('mouseup', handleResizeEnd);
- return () => {
- if (table) {
- table.removeEventListener('mousedown', handleResizeStart);
- }
- window.removeEventListener('mousemove', handleResizeMove);
- window.removeEventListener('mouseup', handleResizeEnd);
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- };
- }
- return undefined;
- }, [resizeState, tableRef, hoverState, handleResizeStart, handleResizeMove, handleResizeEnd]);
- /**
- * Hover状态变化时绑定mousedown
- */
- useEffect(() => {
- const table = tableRef.current;
- if (!table || readOnly || !hoverState.isHovering) return;
- table.addEventListener('mousedown', handleResizeStart);
- return () => {
- table.removeEventListener('mousedown', handleResizeStart);
- };
- }, [tableRef, readOnly, hoverState, handleResizeStart]);
- // ────────────────────────────────────────────────────────────────────────────
- // Return
- // ────────────────────────────────────────────────────────────────────────────
- // 计算尺寸提示
- const activeResizeLinePosition = resizeState?.isResizing
- ? resizeState.startLinePosition + (
- resizeState.resizeType === 'column'
- ? resizeState.currentPos.x - resizeState.startPos.x
- : resizeState.currentPos.y - resizeState.startPos.y
- )
- : resizeLinePosition;
- const currentSizeTooltip = resizeState?.isResizing
- ? (() => {
- if (resizeState.resizeType === 'column') {
- const offset = resizeState.currentPos.x - resizeState.startPos.x;
- const deltaPercent = (offset / resizeState.tableWidthPx) * 100;
- const newWidth = resizeState.originalSize + deltaPercent;
-
- // 计算pt值
- const widthPt = Math.round((tableWidth / 100) * newWidth * 478 / 100 * 10) / 10;
- return `宽度: ${widthPt}pt`;
- } else {
- const offset = resizeState.currentPos.y - resizeState.startPos.y;
- const deltaPt = pixelToPt(offset);
- const newHeight = Math.max(15, resizeState.originalSize + deltaPt);
- const roundedHeight = Math.round(newHeight * 10) / 10;
- return `高度: ${roundedHeight}pt`;
- }
- })()
- : null;
- return {
- resizeState,
- hoverState,
- resizeLinePosition: activeResizeLinePosition,
- currentSizeTooltip,
- };
- }
|