20260724232121
This commit is contained in:
1 parent
b33c743236
commit
1b86e0043a
10 files changed
+996
-230
No files matched your search
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots } from "vue";
|
||||
import { computed, nextTick, ref, useSlots } from "vue";
|
||||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
||||
import { StkTable } from "stk-table-vue";
|
||||
import { createActionsCellRenderer } from "./utils/createActionsCellRenderer";
|
||||
@@ -36,148 +36,48 @@ const activeColKey = ref(null);
|
||||
function onStkRowClick(ev, row, data) {
|
||||
const td = ev.target.closest?.("td");
|
||||
activeColKey.value = td?.dataset?.colKey || null;
|
||||
|
||||
// 更新键盘导航的起始位置
|
||||
const rowIndex = props.dataSource.findIndex(
|
||||
(r) => String(getRowKey(r)) === String(getRowKey(row)),
|
||||
);
|
||||
const colIndex = props.columns.findIndex((c) => c.dataIndex === activeColKey.value);
|
||||
if (rowIndex >= 0 && colIndex >= 0) {
|
||||
currentPos.value = { rowIndex, colIndex };
|
||||
}
|
||||
|
||||
emit("row-click", ev, row, data);
|
||||
}
|
||||
|
||||
// ==================== 自定义键盘导航 ====================
|
||||
// StkTable 的 areaSelection keyboard 模式在按键重复(~30Hz)时内部做选区矩形计算、
|
||||
// overlay DOM 操作和虚拟滚动重算——这些不经过我们的 RAF,是 FPS ~12 的根源。
|
||||
// 这里关闭 areaSelection 内置键盘处理,自己接管方向键,只做行/列定位 + RAF 批量化更新。
|
||||
// ==================== 区域选取驱动键盘导航 ====================
|
||||
// 移除自实现的 onTableKeydown 键盘导航,改用 StkTable area-selection 内置键盘导航。
|
||||
// 选区变化(鼠标拖拽 / 方向键 / Ctrl 多选 / Shift 扩选)统一由 area-selection-change
|
||||
// 事件处理:定位当前激活单元格,设置编辑列 activeColKey 并通知父组件切换编辑行。
|
||||
// 选区索引说明见 AreaSelectionRange:index.begin / index.end 为 { row, col } 索引(含两端)。
|
||||
|
||||
const currentPos = ref({ rowIndex: -1, colIndex: -1 });
|
||||
|
||||
let navRafPending = false;
|
||||
let navPendingState = null;
|
||||
|
||||
/** RAF 批量化提交一次导航(activeColKey + row-click emit),每帧至多一次。 */
|
||||
function flushNav() {
|
||||
navRafPending = false;
|
||||
const p = navPendingState;
|
||||
navPendingState = null;
|
||||
if (!p) return;
|
||||
if (activeColKey.value !== p.colKey) {
|
||||
activeColKey.value = p.colKey;
|
||||
function onAreaSelectionChange(ranges) {
|
||||
const range = ranges?.[0];
|
||||
if (!range) {
|
||||
// 选区被清空(如按 Esc)时取消编辑列高亮,但保留当前编辑行
|
||||
activeColKey.value = null;
|
||||
return;
|
||||
}
|
||||
const rowKey = String(getRowKey(p.row));
|
||||
const { begin } = range.index;
|
||||
const row = props.dataSource[begin.row];
|
||||
if (!row) return;
|
||||
// 列索引基于实际渲染列(含行多选列),需映射到业务列 dataIndex
|
||||
const cols = tableColumnsModel.value;
|
||||
const col = cols[begin.col];
|
||||
activeColKey.value = col && !col.__fmsSelection ? (col.dataIndex ?? null) : null;
|
||||
// 通知父组件切换编辑行(单行编辑模式);行未变则不重复 emit
|
||||
const rowKey = String(getRowKey(row));
|
||||
if (String(props.activeRowKey ?? "") !== rowKey) {
|
||||
emit("row-click", null, p.row, p.rows);
|
||||
emit("row-click", null, row, props.dataSource);
|
||||
}
|
||||
}
|
||||
// ==================== 区域选取驱动键盘导航 END ====================
|
||||
|
||||
function navigateTo(rowIndex, colIndex) {
|
||||
const rows = props.dataSource;
|
||||
const cols = props.columns;
|
||||
if (rowIndex < 0 || rowIndex >= rows.length || colIndex < 0 || colIndex >= cols.length) return;
|
||||
|
||||
const row = rows[rowIndex];
|
||||
const colKey = cols[colIndex].dataIndex;
|
||||
|
||||
currentPos.value = { rowIndex, colIndex };
|
||||
navPendingState = { row, colKey, rows };
|
||||
if (!navRafPending) {
|
||||
navRafPending = true;
|
||||
requestAnimationFrame(flushNav);
|
||||
}
|
||||
|
||||
// 虚拟滚动跟随:目标行超出可视区时滚动
|
||||
const table = tableRef.value;
|
||||
if (!table) return;
|
||||
const el = table.$el;
|
||||
const scrollTop = el?.scrollTop ?? 0;
|
||||
const containerHeight = el?.clientHeight ?? 600;
|
||||
const visibleTop = scrollTop + headerRowHeight;
|
||||
const visibleBottom = scrollTop + containerHeight;
|
||||
const targetTop = rowIndex * rowHeight;
|
||||
if (targetTop < visibleTop) {
|
||||
table.scrollTo(Math.max(0, targetTop - headerRowHeight), null);
|
||||
} else if (targetTop + rowHeight > visibleBottom) {
|
||||
table.scrollTo(targetTop + rowHeight - containerHeight + headerRowHeight, null);
|
||||
}
|
||||
}
|
||||
|
||||
function onTableKeydown(event) {
|
||||
const { key, shiftKey, ctrlKey } = event;
|
||||
if (ctrlKey || ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab"].indexOf(key) < 0)
|
||||
return;
|
||||
if (key === "Tab" && shiftKey) return;
|
||||
|
||||
// 输入框/文本框内按键留给浏览器处理光标移动,不拦截为表格导航
|
||||
const tag = event.target?.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || event.target?.isContentEditable)
|
||||
return;
|
||||
|
||||
const rows = props.dataSource;
|
||||
const cols = props.columns;
|
||||
if (rows.length === 0 || cols.length === 0) return;
|
||||
|
||||
let { rowIndex, colIndex } = currentPos.value;
|
||||
|
||||
// 首次按键或位置无效时,从 activeRowKey 推导起始位置
|
||||
if (rowIndex < 0 || colIndex < 0 || rowIndex >= rows.length) {
|
||||
const activeRowKeyStr = String(props.activeRowKey ?? "");
|
||||
rowIndex = rows.findIndex((r) => String(getRowKey(r)) === activeRowKeyStr);
|
||||
colIndex = 0;
|
||||
if (rowIndex < 0) rowIndex = 0;
|
||||
currentPos.value = { rowIndex, colIndex };
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
if (rowIndex > 0) rowIndex--;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (rowIndex < rows.length - 1) rowIndex++;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
if (colIndex > 0) colIndex--;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
case "Tab":
|
||||
if (colIndex < cols.length - 1) colIndex++;
|
||||
else if (rowIndex < rows.length - 1) {
|
||||
rowIndex++;
|
||||
colIndex = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigateTo(rowIndex, colIndex);
|
||||
}
|
||||
|
||||
function attachKeyboard() {
|
||||
nextTick(() => {
|
||||
const el = tableRef.value?.$el;
|
||||
if (el) el.addEventListener("keydown", onTableKeydown, true);
|
||||
});
|
||||
}
|
||||
|
||||
function detachKeyboard() {
|
||||
const el = tableRef.value?.$el;
|
||||
if (el) el.removeEventListener("keydown", onTableKeydown, true);
|
||||
}
|
||||
|
||||
onMounted(attachKeyboard);
|
||||
onBeforeUnmount(detachKeyboard);
|
||||
// ==================== 自定义键盘导航 END ====================
|
||||
// 首个可编辑业务列(dataIndex),用于新增行/未指定列时默认聚焦该列
|
||||
const firstEditableColKey = computed(() => {
|
||||
const col = props.columns.find((c) => c.editor && !c.customCell && !c.actions);
|
||||
return col ? getColumnKey(col) : null;
|
||||
});
|
||||
|
||||
const EditableCell = createEditableCellRenderer({
|
||||
getRowKey,
|
||||
getActiveRowKey: () => props.activeRowKey,
|
||||
getActiveColKey: () => activeColKey.value,
|
||||
getFirstEditableColKey: () => firstEditableColKey.value,
|
||||
});
|
||||
|
||||
const { SelectionCell, SelectionHeaderCell } = createSelectionCellRenderers({
|
||||
@@ -187,14 +87,13 @@ const { SelectionCell, SelectionHeaderCell } = createSelectionCellRenderers({
|
||||
onChange: (keys, rows) => props.rowSelection?.onChange?.(keys, rows),
|
||||
});
|
||||
|
||||
// areaSelection 仅保留 enabled 做占位(避免 StkTable prop 类型警告),
|
||||
// keyboard 已关闭:键盘导航改为本组件自实现的 onTableKeydown 接管,
|
||||
// 绕过 StkTable 内部每帧 30+ 次的选区计算 + overlay DOM 操作。
|
||||
// 区域选取:启用内置键盘导航(方向键 / Tab / Ctrl 多选 / Shift 扩选)与单元格高亮,
|
||||
// 键盘导航与鼠标拖拽产生的选区变化统一由 @area-selection-change 事件处理(见 onAreaSelectionChange)。
|
||||
const areaSelectionConfig = ref({
|
||||
enabled: true,
|
||||
keyboard: false,
|
||||
keyboard: true,
|
||||
highlight: {
|
||||
cell: false,
|
||||
cell: true,
|
||||
row: false,
|
||||
},
|
||||
});
|
||||
@@ -312,9 +211,6 @@ const forwardSlots = computed(() => {
|
||||
return result;
|
||||
});
|
||||
|
||||
// areaSelection 只做外观占位,键盘导航由自实现接管以避免 StkTable 内部选区计算开销。
|
||||
// 鼠标点击后 keyboard 会通过 onTableKeydown 从 activeRowKey 推导起始位置继续导航。
|
||||
|
||||
// 滚动到指定行;用于新增后自动聚焦
|
||||
async function scrollToRow(rowKey) {
|
||||
await nextTick();
|
||||
@@ -348,6 +244,7 @@ defineExpose({ scrollToRow });
|
||||
:no-data-full="noDataFull"
|
||||
:area-selection="areaSelectionConfig"
|
||||
@row-click="onStkRowClick"
|
||||
@area-selection-change="onAreaSelectionChange"
|
||||
>
|
||||
<template v-for="(slot, name) in forwardSlots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
@@ -376,18 +273,27 @@ defineExpose({ scrollToRow });
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 编辑单元格 wrapper:始终同时渲染文本 span 和编辑组件,通过 display 切换。
|
||||
保证编辑/文本切换时 Vue patch 只更新 props,不触发组件 mount/unmount。 */
|
||||
::deep(.fms-editable-cell-wrap) {
|
||||
/* 编辑单元格 wrapper:仅编辑行渲染此 wrapper(内含编辑器组件),
|
||||
非编辑行只渲染 .fms-cell-text 文本 span,避免滚动时 churn 重型组件。
|
||||
|
||||
实测 DOM:customCell 根元素会经 StkTable fallthrough 合并 class="table-cell-wrapper",
|
||||
即 td > div.table-cell-wrapper.fms-editable-cell-wrap > 编辑器(同一个 div,非嵌套)。
|
||||
.table-cell-wrapper 高度默认 auto(虚拟模式仅 max-height),导致子编辑器 height:100%
|
||||
解析为 auto 而无法占满行高——这是之前几次失败的根因。
|
||||
方案:用 --row-height 定死 wrapper 高度打破循环;用负 margin + 宽度补偿突破 td 的
|
||||
--cell-padding-x(默认 8px,上下 0),使编辑器占满整个单元格(含 padding 区)。 */
|
||||
:deep(.fms-editable-cell-wrap) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% + 2 * var(--cell-padding-x, 8px));
|
||||
height: var(--row-height, 34px);
|
||||
margin: calc(0px - var(--cell-padding-y, 0px)) calc(0px - var(--cell-padding-x, 8px));
|
||||
}
|
||||
::deep(.fms-editable-cell-wrap .fms-cell-text) {
|
||||
:deep(.fms-editable-cell-wrap .fms-cell-text) {
|
||||
flex: 1;
|
||||
}
|
||||
::deep(
|
||||
:deep(
|
||||
.fms-editable-cell-wrap .ant-input-number,
|
||||
.fms-editable-cell-wrap .ant-select,
|
||||
.fms-editable-cell-wrap .ant-input
|
||||
@@ -395,9 +301,32 @@ defineExpose({ scrollToRow });
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 隐藏 areaSelection 视觉覆盖层:只借用它的键盘导航 + 虚拟滚动跟随,
|
||||
不需要蓝色的选区高亮遮罩。编辑态下由 EditableCell 的 input/select 展示焦点。 */
|
||||
::deep(.stk-table-area-selection) {
|
||||
display: none !important;
|
||||
/* 让 Input / InputNumber / Select 占满单元格的宽高(覆盖 antdv-next small 尺寸默认高度) */
|
||||
:deep(.fms-editable-cell-wrap .ant-input) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.fms-editable-cell-wrap .ant-input-number) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.fms-editable-cell-wrap .ant-input-number .ant-input-number-input) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.fms-editable-cell-wrap .ant-select) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.fms-editable-cell-wrap .ant-select .ant-select-selector) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(
|
||||
.fms-editable-cell-wrap .ant-select .ant-select-selection-item,
|
||||
.fms-editable-cell-wrap .ant-select .ant-select-selection-placeholder,
|
||||
.fms-editable-cell-wrap .ant-select .ant-select-selection-search
|
||||
) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 移除:原 hide areaSelection 覆盖层;现已启用 StkTable 内置区域选取与单元格高亮,
|
||||
编辑态下由 EditableCell 的 input/select 展示焦点,选区边框由 .cell-range-selected 提供。 */
|
||||
</style>
|
||||
@@ -1,30 +1,42 @@
|
||||
import { h } from "vue";
|
||||
import { h, nextTick, watch } from "vue";
|
||||
import { Checkbox, Input, InputNumber, Select } from "antdv-next";
|
||||
|
||||
function resolveOption(option, row) {
|
||||
return typeof option === "function" ? option(row) : option;
|
||||
}
|
||||
|
||||
export function createEditableCellRenderer({ getRowKey, getActiveRowKey, getActiveColKey }) {
|
||||
export function createEditableCellRenderer({
|
||||
getRowKey,
|
||||
getActiveRowKey,
|
||||
getActiveColKey,
|
||||
getFirstEditableColKey,
|
||||
}) {
|
||||
// 每个单元格独立的 debounce timer,避免每次击键都触发
|
||||
// onChange → emitUpdate → emit → parent drafts → watch → localData 新引用
|
||||
// → FmsTable dataSource 新引用 → StkTable 全量虚拟滚动重算 → 卡顿。
|
||||
// 150ms debounce 让连续打字期间只触发一次同步。
|
||||
const changeTimers = new Map();
|
||||
|
||||
// 追踪已聚焦过的单元格 key,避免在同一个编辑会话内重复 focus+select
|
||||
//(例如父组件数据变化导致行重渲染时不应打断正在输入的用户)。
|
||||
// 与旧实现不同:不依赖 DOM dataset,因为优化后编辑组件始终驻留 DOM。
|
||||
// 追踪已聚焦过的单元格 key,避免在同一个编辑会话(同一编辑行、父数据变化导致
|
||||
// 行重渲染)内重复 focus+select 从而打断正在输入的用户。
|
||||
// 编辑行切换(activeRowKey 变化)时会在 EditableCell 内清空本 Map,
|
||||
// 允许用户重新进入任意编辑单元格时再次自动聚焦+选中(按需挂载模式下行会 unmount/mount)。
|
||||
const focusedCells = new Map();
|
||||
|
||||
// 缓存每个 cellKey 的 focusRef 闭包引用,避免每次渲染创建新引用导致
|
||||
// Vue ref 回调反复触发(ref 变化 → ref(null) → ref(instance) → focus+select)。
|
||||
const focusRefCache = new Map();
|
||||
|
||||
// 记录当前编辑单元格的 input 元素:area-selection 内置键盘导航在每次按键后会把
|
||||
// 焦点抢回表格容器(见 stk-table-vue 的 blurCellElement),此处用于 nextTick 兜底
|
||||
// 重新聚焦,确保编辑器 Input 稳定持有焦点、文本被选中,可直接覆盖输入。
|
||||
let lastFocusedEl = null;
|
||||
let lastActiveRowKey = null;
|
||||
|
||||
// 稳定引用:Vue VNode diff 按引用比较 props,每次 render 创建新函数/对象
|
||||
// 会触发事件监听器重绑定和样式对比。提到外面让引用恒定,消除击键时的无意义 diff。
|
||||
const stopPropagation = (e) => e.stopPropagation();
|
||||
const fullWidth = { width: "100%" };
|
||||
const fullWidth = { width: "100%", height: "100%" };
|
||||
|
||||
function getFocusRef(cellKey, enabled) {
|
||||
if (!enabled) return undefined;
|
||||
@@ -39,19 +51,48 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey, getActi
|
||||
target?.focus?.({ preventScroll: true });
|
||||
const inputEl = el?.matches?.("input") ? el : el?.querySelector?.("input");
|
||||
inputEl?.select?.();
|
||||
// 记录当前 input 元素,供下方 watch 兜底重新聚焦(应对 area-selection 抢焦点)。
|
||||
lastFocusedEl = inputEl || el || null;
|
||||
};
|
||||
focusRefCache.set(cellKey, fn);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
// 兜底重新聚焦:activeRowKey / activeColKey 变化(来自 area-selection 键盘导航
|
||||
// 或鼠标点击)后,StkTable 会在按键同步阶段把焦点抢回表格容器(blurCellElement),
|
||||
// 这里在 nextTick(晚于该抢占)把焦点交还当前编辑单元格的 Input 并选中文本,
|
||||
// 保证编辑态下光标稳定、可直接覆盖输入。
|
||||
watch(
|
||||
() => [getActiveRowKey(), getActiveColKey ? getActiveColKey() : null],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
if (!lastFocusedEl || !lastFocusedEl.isConnected) return;
|
||||
if (document.activeElement !== lastFocusedEl) {
|
||||
lastFocusedEl.focus?.({ preventScroll: true });
|
||||
}
|
||||
lastFocusedEl.select?.();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return function EditableCell({ row, col, cellValue }) {
|
||||
const editor = typeof col.editor === "string" ? { type: col.editor } : col.editor || {};
|
||||
const rowKey = String(getRowKey(row));
|
||||
const activeRowKey = getActiveRowKey();
|
||||
// 编辑行切换时重置焦点追踪:允许新编辑行(含重新进入过的行)再次自动聚焦+选中;
|
||||
// 同一编辑行内父数据变化导致的重渲染不重置,避免打断正在输入的用户。
|
||||
const activeRowKeyStr = activeRowKey == null ? null : String(activeRowKey);
|
||||
if (lastActiveRowKey !== activeRowKeyStr) {
|
||||
lastActiveRowKey = activeRowKeyStr;
|
||||
focusedCells.clear();
|
||||
}
|
||||
const isEditing = activeRowKey != null && String(activeRowKey) === rowKey;
|
||||
const activeColKey = getActiveColKey?.();
|
||||
const shouldFocus = isEditing && (activeColKey == null || col.dataIndex === activeColKey);
|
||||
// 未指定具体编辑列(如点击「新增」后编程式进入编辑态)时,只聚焦首个可编辑列,
|
||||
// 避免出现“所有可编辑单元格都尝试聚焦、最右列胜出”导致聚焦到最后一列的问题。
|
||||
const targetColKey = activeColKey != null ? activeColKey : (getFirstEditableColKey?.() ?? null);
|
||||
const shouldFocus = isEditing && col.dataIndex === targetColKey;
|
||||
const cellKey = `${rowKey}_${col.dataIndex}`;
|
||||
|
||||
const update = (value) => {
|
||||
@@ -90,24 +131,19 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey, getActi
|
||||
"")
|
||||
: (cellValue ?? "");
|
||||
|
||||
// 始终同时渲染文本 span 和编辑组件,仅通过 CSS display 切换可见性。
|
||||
// 这样 Vue diff 看到的是同一个 div 容器 + 同一对子 VNode 类型,
|
||||
// 跨行切换时只更新 props/style,不触发组件卸载/挂载。
|
||||
// (之前 if(!isEditing) return span else return Input → 不同类型 VNode
|
||||
// → 强制 mount/unmount Ant Design 组件,是键盘导航时 FPS 暴跌的另一主因。)
|
||||
|
||||
const textSpan = h(
|
||||
"span",
|
||||
{
|
||||
class: "fms-cell-text",
|
||||
style: { display: isEditing ? "none" : "inline-block" },
|
||||
},
|
||||
textContent,
|
||||
);
|
||||
// 仅“编辑行”才渲染 Ant Design 编辑器组件,其余行只渲染纯文本 span。
|
||||
// 此前为保持键盘导航时不重挂载而“常驻双渲染”(始终挂载 Input/Select,
|
||||
// 仅靠 CSS display 切换显隐),导致虚拟滚动时非编辑行每帧 churn 数十至上百个
|
||||
// 重型组件实例,是权限页 6 列 5000 行滚动仅 30FPS 的主因。
|
||||
// 改为按需挂载后:编辑行通常只有一行的可编辑列挂编辑器,滚动时非编辑行零开销,
|
||||
// 与工作台(编辑行按需渲染)表现一致。键盘导航只是单行内少量 mount,亦无卡顿。
|
||||
if (!isEditing) {
|
||||
return h("span", { class: "fms-cell-text" }, textContent);
|
||||
}
|
||||
|
||||
const common = {
|
||||
size: "small",
|
||||
style: { ...fullWidth, display: isEditing ? "inline-block" : "none" },
|
||||
style: fullWidth,
|
||||
onClick: stopPropagation,
|
||||
ref: getFocusRef(cellKey, shouldFocus),
|
||||
};
|
||||
@@ -144,6 +180,6 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey, getActi
|
||||
});
|
||||
}
|
||||
|
||||
return h("div", { class: "fms-editable-cell-wrap" }, [textSpan, editComponent]);
|
||||
return h("div", { class: "fms-editable-cell-wrap" }, [editComponent]);
|
||||
};
|
||||
}
|
||||
@@ -36,6 +36,8 @@ export function createSelectionCellRenderers({
|
||||
checked: getSelectedRowKeySet().has(getRowKey(row)),
|
||||
"onUpdate:checked": (checked) => changeRow(row, checked),
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
// 阻止 mousedown 冒泡到表格根,避免触发 area-selection 误将本行设入编辑态
|
||||
onMousedown: (event) => event.stopPropagation(),
|
||||
});
|
||||
|
||||
const SelectionHeaderCell = () => {
|
||||
|
||||
Reference in new issue
Block a user