Files
workspace/code/fms/fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js
T
2026-07-24 17:34:26 +08:00

150 lines
5.7 KiB
JavaScript

import { h } 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 }) {
// 每个单元格独立的 debounce timer,避免每次击键都触发
// onChange → emitUpdate → emit → parent drafts → watch → localData 新引用
// → FmsTable dataSource 新引用 → StkTable 全量虚拟滚动重算 → 卡顿。
// 150ms debounce 让连续打字期间只触发一次同步。
const changeTimers = new Map();
// 追踪已聚焦过的单元格 key,避免在同一个编辑会话内重复 focus+select
//(例如父组件数据变化导致行重渲染时不应打断正在输入的用户)。
// 与旧实现不同:不依赖 DOM dataset,因为优化后编辑组件始终驻留 DOM。
const focusedCells = new Map();
// 缓存每个 cellKey 的 focusRef 闭包引用,避免每次渲染创建新引用导致
// Vue ref 回调反复触发(ref 变化 → ref(null) → ref(instance) → focus+select)。
const focusRefCache = new Map();
// 稳定引用:Vue VNode diff 按引用比较 props,每次 render 创建新函数/对象
// 会触发事件监听器重绑定和样式对比。提到外面让引用恒定,消除击键时的无意义 diff。
const stopPropagation = (e) => e.stopPropagation();
const fullWidth = { width: "100%" };
function getFocusRef(cellKey, enabled) {
if (!enabled) return undefined;
let fn = focusRefCache.get(cellKey);
if (!fn) {
fn = (instance) => {
if (!instance) return;
if (focusedCells.get(cellKey)) return;
focusedCells.set(cellKey, true);
const el = instance.$el;
const target = instance.focus ? instance : el;
target?.focus?.({ preventScroll: true });
const inputEl = el?.matches?.("input") ? el : el?.querySelector?.("input");
inputEl?.select?.();
};
focusRefCache.set(cellKey, fn);
}
return fn;
}
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 isEditing = activeRowKey != null && String(activeRowKey) === rowKey;
const activeColKey = getActiveColKey?.();
const shouldFocus = isEditing && (activeColKey == null || col.dataIndex === activeColKey);
const cellKey = `${rowKey}_${col.dataIndex}`;
const update = (value) => {
row[col.dataIndex] = value;
if (editor.onChange) {
const timerKey = `${rowKey}_${col.dataIndex}`;
clearTimeout(changeTimers.get(timerKey));
changeTimers.set(
timerKey,
setTimeout(() => {
changeTimers.delete(timerKey);
editor.onChange?.(value, row, col);
}, 150),
);
}
};
// checkbox 始终可交互(显示态与编辑态一致)
if (editor.type === "checkbox") {
const checkedValue = editor.checkedValue ?? 1;
const uncheckedValue = editor.uncheckedValue ?? 0;
return h(Checkbox, {
checked: cellValue === checkedValue,
disabled: resolveOption(editor.disabled, row),
onClick: (event) => event.stopPropagation(),
"onUpdate:checked": (checked) => update(checked ? checkedValue : uncheckedValue),
});
}
// 文本态内容
const textContent =
editor.type === "select"
? ((resolveOption(editor.options, row) || []).find((item) => item.value === cellValue)
?.label ??
cellValue ??
"")
: (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,
);
const common = {
size: "small",
style: { ...fullWidth, display: isEditing ? "inline-block" : "none" },
onClick: stopPropagation,
ref: getFocusRef(cellKey, shouldFocus),
};
let editComponent;
if (editor.type === "number") {
editComponent = h(InputNumber, {
...common,
value: cellValue,
min: resolveOption(editor.min, row),
max: resolveOption(editor.max, row),
precision: resolveOption(editor.precision, row),
disabled: resolveOption(editor.disabled, row),
"onUpdate:value": update,
});
} else if (editor.type === "select") {
editComponent = h(Select, {
...common,
value: cellValue,
options: resolveOption(editor.options, row) || [],
disabled: resolveOption(editor.disabled, row),
"onUpdate:value": update,
});
} else {
editComponent = h(Input, {
...common,
value: cellValue,
disabled: resolveOption(editor.disabled, row),
maxlength: resolveOption(editor.maxlength, row),
placeholder: resolveOption(editor.placeholder, row),
status: resolveOption(editor.status, row),
allowClear: editor.allowClear ?? false,
"onUpdate:value": update,
});
}
return h("div", { class: "fms-editable-cell-wrap" }, [textSpan, editComponent]);
};
}