diff --git a/src/StkTable/features/useAreaSelection.ts b/src/StkTable/features/useAreaSelection.ts index edbdafc2..63eea823 100644 --- a/src/StkTable/features/useAreaSelection.ts +++ b/src/StkTable/features/useAreaSelection.ts @@ -158,6 +158,51 @@ export function useAreaSelection
>( /** 根据 selectionRanges 计算所有选区内 cellKey 的并集(非响应式,用于 DOM 操作) */ let selectedCellKeys: Set = new Set(); + let observedThead: HTMLTableSectionElement | null = null; + let observedTfoot: HTMLTableSectionElement | null = null; + let cachedHeaderHeight = 0; + let cachedFooterHeight = 0; + const sectionResizeObserver = + typeof ResizeObserver === 'undefined' + ? null + : new ResizeObserver(entries => { + for (const entry of entries) { + const height = entry.target.getBoundingClientRect().height; + if (entry.target === observedThead) cachedHeaderHeight = height; + if (entry.target === observedTfoot) cachedFooterHeight = height; + } + }); + + function observeTableSection( + next: HTMLTableSectionElement | null, + current: HTMLTableSectionElement | null, + setCurrent: (section: HTMLTableSectionElement | null) => void, + setHeight: (height: number) => void, + ) { + if (next === current) return; + if (current) sectionResizeObserver?.unobserve(current); + setCurrent(next); + setHeight(next?.getBoundingClientRect().height ?? 0); + if (next) sectionResizeObserver?.observe(next); + } + + function getTableSectionHeights(container: HTMLElement) { + const thead = container.querySelector('thead'); + const tfoot = container.querySelector('tfoot'); + observeTableSection( + thead, + observedThead, + section => (observedThead = section), + height => (cachedHeaderHeight = height), + ); + observeTableSection( + tfoot, + observedTfoot, + section => (observedTfoot = section), + height => (cachedFooterHeight = height), + ); + return [cachedHeaderHeight, cachedFooterHeight] as const; + } /** 重新计算 selectedCellKeys */ function recomputeSelectedCellKeys() { @@ -235,6 +280,22 @@ export function useAreaSelection
>( const lastRange = ranges[ranges.length - 1]; const { minRow: lrMinRow, maxRow: lrMaxRow, minCol: lrMinCol, maxCol: lrMaxCol } = normalizeRange(lastRange); + // 键盘导航的常见路径只有一个单元格,直接定位目标行和列, + // 避免每次按键都扫描全部可见行。 + if (ranges.length === 1 && lrMinRow === lrMaxRow && lrMinCol === lrMaxCol) { + const tr = tbody.querySelector(`tr[data-row-i="${lrMinRow}"]`); + const col = tableHeaderLast.value[lrMinCol]; + const targetColKey = col ? String(colKeyGen.value(col)) : ''; + const cells = tr?.querySelectorAll('td[data-col-key]') ?? []; + for (let i = 0; i < cells.length; i++) { + const td = cells[i] as HTMLElement; + if (td.getAttribute('data-col-key') !== targetColKey) continue; + td.classList.add(CELL_RANGE_SELECTED, CELL_RANGE_TOP, CELL_RANGE_BOTTOM, CELL_RANGE_LEFT, CELL_RANGE_RIGHT); + break; + } + return; + } + // 遍历所有可见行 const trs = tbody.querySelectorAll('tr[data-row-i]'); for (let t = 0; t < trs.length; t++) { @@ -317,6 +378,7 @@ export function useAreaSelection
>( }); onBeforeUnmount(() => { removeListener(); + sectionResizeObserver?.disconnect(); }); /** @@ -860,10 +922,7 @@ export function useAreaSelection
>( const col = tableHeaderLast.value[colIndex]; if (!row || !col) return; - const thead = container.querySelector('thead'); - const headerHeight = thead ? thead.offsetHeight : 0; - const tfoot = container.querySelector('tfoot'); - const footerHeight = tfoot ? tfoot.offsetHeight : 0; + const [headerHeight, footerHeight] = getTableSectionHeights(container); const vs = virtualScroll.value; const vsx = virtualScrollX.value; diff --git a/lib/StkTable-QUd4TJCF.js b/lib/StkTable-QUd4TJCF.js index 512545c2..80fc32bd 100644 --- a/lib/StkTable-QUd4TJCF.js +++ b/lib/StkTable-QUd4TJCF.js @@ -444,6 +444,31 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table }); /** 根据 selectionRanges 计算所有选区内 cellKey 的并集(非响应式,用于 DOM 操作) */ let selectedCellKeys = /* @__PURE__ */ new Set(); + let observedThead = null; + let observedTfoot = null; + let cachedHeaderHeight = 0; + let cachedFooterHeight = 0; + const sectionResizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver((entries) => { + for (const entry of entries) { + const height = entry.target.getBoundingClientRect().height; + if (entry.target === observedThead) cachedHeaderHeight = height; + if (entry.target === observedTfoot) cachedFooterHeight = height; + } + }); + function observeTableSection(next, current, setCurrent, setHeight) { + if (next === current) return; + if (current) sectionResizeObserver?.unobserve(current); + setCurrent(next); + setHeight(next?.getBoundingClientRect().height ?? 0); + if (next) sectionResizeObserver?.observe(next); + } + function getTableSectionHeights(container) { + const thead = container.querySelector("thead"); + const tfoot = container.querySelector("tfoot"); + observeTableSection(thead, observedThead, (section) => observedThead = section, (height) => cachedHeaderHeight = height); + observeTableSection(tfoot, observedTfoot, (section) => observedTfoot = section, (height) => cachedFooterHeight = height); + return [cachedHeaderHeight, cachedFooterHeight]; + } /** 重新计算 selectedCellKeys */ function recomputeSelectedCellKeys() { const ranges = selectionRanges.value; @@ -497,6 +522,19 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table if (cellHighlight) { const lastRange = ranges[ranges.length - 1]; const { minRow: lrMinRow, maxRow: lrMaxRow, minCol: lrMinCol, maxCol: lrMaxCol } = normalizeRange(lastRange); + if (ranges.length === 1 && lrMinRow === lrMaxRow && lrMinCol === lrMaxCol) { + const tr = tbody.querySelector(`tr[data-row-i="${lrMinRow}"]`); + const col = tableHeaderLast.value[lrMinCol]; + const targetColKey = col ? String(colKeyGen.value(col)) : ""; + const cells = tr?.querySelectorAll("td[data-col-key]") ?? []; + for (let i = 0; i < cells.length; i++) { + const td = cells[i]; + if (td.getAttribute("data-col-key") !== targetColKey) continue; + td.classList.add(CELL_RANGE_SELECTED, CELL_RANGE_TOP, CELL_RANGE_BOTTOM, CELL_RANGE_LEFT, CELL_RANGE_RIGHT); + break; + } + return; + } const trs = tbody.querySelectorAll("tr[data-row-i]"); for (let t = 0; t < trs.length; t++) { const tr = trs[t]; @@ -559,6 +597,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table }); onBeforeUnmount(() => { removeListener(); + sectionResizeObserver?.disconnect(); }); /** * 监听数据行数/列数变化,当行列变少时钳制选区与锚点,避免越界 @@ -966,10 +1005,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table const row = dataSourceCopy.value[rowIndex]; const col = tableHeaderLast.value[colIndex]; if (!row || !col) return; - const thead = container.querySelector("thead"); - const headerHeight = thead ? thead.offsetHeight : 0; - const tfoot = container.querySelector("tfoot"); - const footerHeight = tfoot ? tfoot.offsetHeight : 0; + const [headerHeight, footerHeight] = getTableSectionHeights(container); const vs = virtualScroll.value; const vsx = virtualScrollX.value; const isScrollRowByRow = props.scrollRowByRow;