150 lines
5.7 KiB
JavaScript
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]);
|
|
};
|
|
}
|