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]); }; }