20260724173425

This commit is contained in:
oneao committed 2026-07-24 17:34:26 +08:00
1 parent 4f40ad9f11
commit b33c743236
27 files changed
+1310 -437

No files matched your search

@@ -6,16 +6,67 @@ function resolveOption(option, row) {
}
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(getRowKey(row)) === String(activeRowKey);
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;
editor.onChange?.(value, row, col);
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 始终可交互(显示态与编辑态一致)
@@ -30,32 +81,40 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey, getActi
});
}
if (!isEditing) {
if (editor.type === "select") {
const options = resolveOption(editor.options, row) || [];
const option = options.find((item) => item.value === cellValue);
return h("span", { class: "fms-cell-text" }, option?.label ?? cellValue ?? "");
}
return h("span", { class: "fms-cell-text" }, cellValue ?? "");
}
// 文本态内容
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: { width: "100%" },
onClick: (event) => event.stopPropagation(),
// 仅被点击的列首次渲染时自动聚焦
ref: shouldFocus
? (instance) => {
if (instance) {
const target = instance.focus ? instance : instance.$el;
target?.focus?.();
}
}
: undefined,
style: { ...fullWidth, display: isEditing ? "inline-block" : "none" },
onClick: stopPropagation,
ref: getFocusRef(cellKey, shouldFocus),
};
let editComponent;
if (editor.type === "number") {
return h(InputNumber, {
editComponent = h(InputNumber, {
...common,
value: cellValue,
min: resolveOption(editor.min, row),
@@ -64,25 +123,27 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey, getActi
disabled: resolveOption(editor.disabled, row),
"onUpdate:value": update,
});
}
if (editor.type === "select") {
return h(Select, {
} 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(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 ?? true,
"onUpdate:value": update,
});
return h("div", { class: "fms-editable-cell-wrap" }, [textSpan, editComponent]);
};
}