Files
2026-08-13 22:26:56 +08:00

176 lines
8.2 KiB
Diff

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<DT extends Record<string, any>>(
/** 根据 selectionRanges 计算所有选区内 cellKey 的并集(非响应式,用于 DOM 操作) */
let selectedCellKeys: Set<string> = 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<HTMLTableSectionElement>('thead');
+ const tfoot = container.querySelector<HTMLTableSectionElement>('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<DT extends Record<string, any>>(
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<DT extends Record<string, any>>(
});
onBeforeUnmount(() => {
removeListener();
+ sectionResizeObserver?.disconnect();
});
/**
@@ -860,10 +922,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
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;