20260725172954
This commit is contained in:
1 parent
1b86e0043a
commit
e3c7c6f8da
16 files changed
+714
-796
No files matched your search
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
name: fms-table-column-type-auto-renderer
|
||||||
|
overview: 在 FmsTable 内部根据列的 type 属性自动生成 customCell 渲染器,让面板只需声明 type(checkbox/actions)而不必手写 h() 渲染函数。
|
||||||
|
todos:
|
||||||
|
- id: impl-actions-renderer
|
||||||
|
content: 实现 createActionsCellRenderer.js,导出 createActionsCell(actions) 函数,支持声明式操作按钮配置(原生 button + Modal.confirm)
|
||||||
|
status: completed
|
||||||
|
- id: enhance-fmstable-columns
|
||||||
|
content: 增强 FmsTable.vue 的 displayColumns computed,根据 column.type 自动注入 checkbox / actions 的 customCell 渲染器
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- impl-actions-renderer
|
||||||
|
- id: simplify-powers-panel
|
||||||
|
content: 简化 ModulePowersPanel.vue,移除 CanUseCell 和 ActionsCell 渲染函数及 .pw-checkbox 样式,改用 type 声明
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- enhance-fmstable-columns
|
||||||
|
- id: simplify-i18n-panel
|
||||||
|
content: 简化 ModuleI18nPanel.vue,移除 ActionsCell 渲染函数及 h 导入,改用 type 声明
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- enhance-fmstable-columns
|
||||||
|
---
|
||||||
|
|
||||||
|
## 用户需求
|
||||||
|
|
||||||
|
简化 FmsTable 的列定义方式。当前调用方(ModulePowersPanel、ModuleI18nPanel 等)需要在每个需要特殊渲染的列上手动编写 `customCell` 渲染函数,导致大量重复的 checkbox 和操作按钮渲染逻辑散落在各个面板中。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
- **`type: "checkbox"` 列自动渲染**:列定义只需声明 `type: "checkbox"`,FmsTable 自动注入原生 checkbox 渲染器。切换时自动 `row[dataIndex] = checked ? 1 : 0`,然后调用 `column.onChange?.()` 通知父组件
|
||||||
|
- **`type: "actions"` 列自动渲染**:列定义声明 `type: "actions"` 并配置 `actions: [{ type, label, confirm?, onClick(row) }]` 数组,FmsTable 通过 `createActionsCellRenderer.js` 自动生成操作按钮
|
||||||
|
- **无 `type` 列保持默认**:不传 `type` 的列维持 stk-table 原生文本渲染
|
||||||
|
- **特殊列保留 `customCell`**:I18nKeyCell(多语言键列:文本 + 翻译 icon 按钮组合)过于特殊,保持原有 `customCell` 方式
|
||||||
|
|
||||||
|
## 技术方案
|
||||||
|
|
||||||
|
### 实现策略
|
||||||
|
|
||||||
|
核心思路:在 `FmsTable.vue` 的 `displayColumns` computed 中对用户传入的 columns 做一层增强映射——检测 `type` 属性,为 `checkbox` / `actions` 类型的列自动注入 `customCell` 渲染函数。父组件只需声明类型和配置,无需关心渲染细节。
|
||||||
|
|
||||||
|
### 关键设计决策
|
||||||
|
|
||||||
|
1. **checkbox 渲染复用 `.fms-checkbox` 全局样式**:和选择列共用同一套 CSS,零新增样式。外观与 antdv-next Checkbox 一致(蓝色背景 + 白色勾)
|
||||||
|
2. **actions 渲染独立到 `createActionsCellRenderer.js`**:操作列逻辑较复杂(多按钮、confirm 弹窗),独立文件便于扩展。每个 action 使用原生 `<button>` 元素,不创建 antdv 组件实例
|
||||||
|
3. **类型处理在 `displayColumns` 中进行**:在传给 stk-table 之前的最后一刻做增强,不影响父组件的 columns 引用。`onColumnsUpdate` 回传时不会带出内部注入的 `customCell`
|
||||||
|
4. **性能零退化**:所有注入的渲染器均使用原生 DOM(`h("label")` / `h("input")` / `h("button")`),不创建 Vue 组件实例,与当前性能优化方向完全一致
|
||||||
|
|
||||||
|
### 实现细节
|
||||||
|
|
||||||
|
**`createActionsCellRenderer.js`**
|
||||||
|
|
||||||
|
- 导出 `createActionsCell(actions)`,接收 `[{ type, label, confirm?, onClick(row) }]` 数组
|
||||||
|
- 返回 `({ row }) => VNode` 渲染函数
|
||||||
|
- 每个 action 渲染为原生 `<button>`,内联红色文字样式
|
||||||
|
- `confirm` 存在时,点击先弹出 `Modal.confirm`,确认后调用 `onClick(row)`
|
||||||
|
- 按钮容器为 flex 布局,间距 4px
|
||||||
|
|
||||||
|
**`FmsTable.vue` 中的 `displayColumns` 增强**
|
||||||
|
|
||||||
|
- 在 `[...props.columns]` 的基础上做 `.map()`,对每列检查 `type`
|
||||||
|
- `type === "checkbox"` → 注入 checkbox 渲染器(复用 `.fms-checkbox` 样式,`cellValue === 1` 时添加 `checked` 类)
|
||||||
|
- `type === "actions"` → 调用 `createActionsCell(col.actions)` 注入
|
||||||
|
- 其余列原样透传
|
||||||
|
|
||||||
|
**ModulePowersPanel.vue 简化**
|
||||||
|
|
||||||
|
- 删除 `CanUseCell`(157-180 行)和 `ActionsCell`(183-211 行)渲染函数
|
||||||
|
- `b_canuse` 列改为:`{ ..., type: "checkbox", onChange: () => emitUpdate() }`
|
||||||
|
- `actions` 列改为:`{ ..., type: "actions", actions: [{ type: "delete", label: "删除", confirm: "确定删除该记录?", onClick: (row) => deleteRow(row) }] }`
|
||||||
|
- 删除 `.pw-checkbox` 全局样式块(281-328 行),checkbox 样式由 FmsTable 的 `.fms-checkbox` 接管
|
||||||
|
|
||||||
|
**ModuleI18nPanel.vue 简化**
|
||||||
|
|
||||||
|
- 删除 `ActionsCell` 渲染函数(177-206 行)
|
||||||
|
- `actions` 列改为:`{ ..., type: "actions", actions: [{ type: "delete", label: "删除", confirm: "确定删除该记录(含其全部语言)?", onClick: (row) => deleteRow(row) }] }`
|
||||||
|
- 移除 `h` 导入(不再使用)
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, ref, useSlots } from "vue";
|
import { computed, nextTick, ref, useSlots } from "vue";
|
||||||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
|
||||||
import { StkTable } from "stk-table-vue";
|
import { StkTable } from "stk-table-vue";
|
||||||
import { createActionsCellRenderer } from "./utils/createActionsCellRenderer";
|
import { createSelectionColumn, isSelectionColumn } from "./utils/createSelectionCellRenderers";
|
||||||
import { createEditableCellRenderer } from "./utils/createEditableCellRenderer";
|
import { resolveCustomCell } from "./utils/createCellRenderer";
|
||||||
import { createSelectionCellRenderers } from "./utils/createSelectionCellRenderers";
|
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false });
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
@@ -15,214 +13,95 @@ const props = defineProps({
|
|||||||
bordered: { type: [Boolean, String], default: true },
|
bordered: { type: [Boolean, String], default: true },
|
||||||
noDataFull: { type: Boolean, default: true },
|
noDataFull: { type: Boolean, default: true },
|
||||||
columns: { type: Array, default: () => [] },
|
columns: { type: Array, default: () => [] },
|
||||||
|
// 序号列:false=关闭;true=默认;{ title, width }=自定义
|
||||||
|
seq: { type: [Boolean, Object], default: false },
|
||||||
|
// 行多选:{ selectedRowKeys, onChange },提供时在最前列插入复选框列
|
||||||
|
rowSelection: { type: Object, default: null },
|
||||||
dataSource: { type: Array, default: () => [] },
|
dataSource: { type: Array, default: () => [] },
|
||||||
rowKey: { type: [String, Function], default: "id" },
|
rowKey: { type: [String, Function], default: "id" },
|
||||||
rowSelection: { type: Object, default: null },
|
|
||||||
// 当前处于编辑状态的行 key;为 null 时所有行均以纯文本展示(单行编辑模式)。
|
|
||||||
activeRowKey: { type: [String, Number], default: null },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:columns", "row-click"]);
|
const emit = defineEmits(["update:columns"]);
|
||||||
|
|
||||||
|
const tableRef = ref(null);
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
// 由 seq 派生:enabled / title / width
|
||||||
|
const seqConfig = computed(() => {
|
||||||
|
const enabled = !!props.seq;
|
||||||
|
if (!enabled) return { enabled: false, title: "序号", width: 50 };
|
||||||
|
const cfg = props.seq === true ? {} : props.seq;
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
title: cfg.title ?? "序号",
|
||||||
|
width: Number(cfg.width ?? 50),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayColumns = computed(() => {
|
||||||
|
const cols = props.columns.map(resolveCustomCell);
|
||||||
|
if (selectionColumn) cols.unshift(selectionColumn);
|
||||||
|
const seq = seqConfig.value;
|
||||||
|
if (seq.enabled) {
|
||||||
|
cols.unshift({
|
||||||
|
type: "seq",
|
||||||
|
width: seq.width,
|
||||||
|
dataIndex: "",
|
||||||
|
title: seq.title,
|
||||||
|
resizable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cols;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 剥离内部列(选择列、序号列)后回传,避免污染父组件 columns
|
||||||
|
function onColumnsUpdate(columns) {
|
||||||
|
const real = (columns || []).filter(
|
||||||
|
(c) => !isSelectionColumn(c) && c.type !== "seq" && c.dataIndex !== "",
|
||||||
|
);
|
||||||
|
emit("update:columns", real);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行高同时作为 CSS 变量与 StkTable 的 row-height / header-row-height
|
||||||
|
const rowHeight = 34;
|
||||||
|
const headerRowHeight = 40;
|
||||||
|
const customVars = {
|
||||||
|
"--header-row-height": `${headerRowHeight}px`,
|
||||||
|
"--row-height": `${rowHeight}px`,
|
||||||
|
"--footer-row-height": "36px",
|
||||||
|
};
|
||||||
|
|
||||||
function getRowKey(row) {
|
function getRowKey(row) {
|
||||||
return typeof props.rowKey === "function" ? props.rowKey(row) : row?.[props.rowKey];
|
return typeof props.rowKey === "function" ? props.rowKey(row) : row?.[props.rowKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRowKeySet = computed(() => new Set(props.rowSelection?.selectedRowKeys || []));
|
// 选择列:已选状态管理 + 渲染逻辑全部内聚在 createSelectionColumn 中
|
||||||
|
const selectionColumn = props.rowSelection
|
||||||
|
? createSelectionColumn({
|
||||||
|
getDataSource: () => props.dataSource,
|
||||||
|
getRowKey,
|
||||||
|
getSelectedRowKeys: () => props.rowSelection.selectedRowKeys ?? [],
|
||||||
|
onChange: (keys, rows) => props.rowSelection?.onChange?.(keys, rows),
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
// 追踪当前编辑列,用于 EditableCell 决定哪个单元格自动聚焦
|
// 滚动到指定行(新增后聚焦)
|
||||||
const activeColKey = ref(null);
|
|
||||||
|
|
||||||
function onStkRowClick(ev, row, data) {
|
|
||||||
const td = ev.target.closest?.("td");
|
|
||||||
activeColKey.value = td?.dataset?.colKey || null;
|
|
||||||
emit("row-click", ev, row, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 区域选取驱动键盘导航 ====================
|
|
||||||
// 移除自实现的 onTableKeydown 键盘导航,改用 StkTable area-selection 内置键盘导航。
|
|
||||||
// 选区变化(鼠标拖拽 / 方向键 / Ctrl 多选 / Shift 扩选)统一由 area-selection-change
|
|
||||||
// 事件处理:定位当前激活单元格,设置编辑列 activeColKey 并通知父组件切换编辑行。
|
|
||||||
// 选区索引说明见 AreaSelectionRange:index.begin / index.end 为 { row, col } 索引(含两端)。
|
|
||||||
|
|
||||||
function onAreaSelectionChange(ranges) {
|
|
||||||
const range = ranges?.[0];
|
|
||||||
if (!range) {
|
|
||||||
// 选区被清空(如按 Esc)时取消编辑列高亮,但保留当前编辑行
|
|
||||||
activeColKey.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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, row, props.dataSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ==================== 区域选取驱动键盘导航 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({
|
|
||||||
getDataSource: () => props.dataSource,
|
|
||||||
getRowKey,
|
|
||||||
getSelectedRowKeySet: () => selectedRowKeySet.value,
|
|
||||||
onChange: (keys, rows) => props.rowSelection?.onChange?.(keys, rows),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 区域选取:启用内置键盘导航(方向键 / Tab / Ctrl 多选 / Shift 扩选)与单元格高亮,
|
|
||||||
// 键盘导航与鼠标拖拽产生的选区变化统一由 @area-selection-change 事件处理(见 onAreaSelectionChange)。
|
|
||||||
const areaSelectionConfig = ref({
|
|
||||||
enabled: true,
|
|
||||||
keyboard: true,
|
|
||||||
highlight: {
|
|
||||||
cell: true,
|
|
||||||
row: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 列宽拖拽结果缓存:StkTable 拖拽列宽依赖 v-model:columns 回写;
|
|
||||||
// 父组件多为单向 :columns 绑定,这里在封装层内部保留拖拽后的宽度,避免重渲染时被重置。
|
|
||||||
const resizedWidths = ref({});
|
|
||||||
|
|
||||||
function getColumnKey(column) {
|
|
||||||
return column.dataIndex ?? column.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// StkTable 的编辑单元格、操作列和多选列统一在封装层生成,业务页面只声明 editor / actions / rowSelection。
|
|
||||||
const tableColumnsModel = computed({
|
|
||||||
get: () => {
|
|
||||||
// 不依赖 activeRowKey:EditableCell 作为函数式组件,内部已通过 getActiveRowKey()/
|
|
||||||
// getActiveColKey() 读取响应式 props.activeRowKey 与 activeColKey,切换编辑行时
|
|
||||||
// 只有可见的编辑单元格会自动重渲染。若在此处依赖 activeRowKey,每次切换都会生成
|
|
||||||
// 全新列对象数组,触发 StkTable 的 watch(columns) → handleDealColumns 全表重渲染,
|
|
||||||
// 这是编辑/新增时卡顿的根源(对比 TableDemo:customCell 直接读 ref 即可精确更新)。
|
|
||||||
const columns = props.columns.map((rawColumn) => {
|
|
||||||
const width = resizedWidths.value[getColumnKey(rawColumn)];
|
|
||||||
const column = width != null ? { ...rawColumn, width } : rawColumn;
|
|
||||||
if (column.actions && !column.customCell) {
|
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
customCell: createActionsCellRenderer(column.actions),
|
|
||||||
__fmsAction: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (column.editor && !column.customCell) {
|
|
||||||
return { ...column, customCell: EditableCell, __fmsEditor: true };
|
|
||||||
}
|
|
||||||
return column;
|
|
||||||
});
|
|
||||||
if (!props.rowSelection) return columns;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
dataIndex: "__fmsSelection",
|
|
||||||
key: "__fmsSelection",
|
|
||||||
title: "",
|
|
||||||
width: 44,
|
|
||||||
fixed: "left",
|
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
customCell: SelectionCell,
|
|
||||||
customHeaderCell: SelectionHeaderCell,
|
|
||||||
__fmsSelection: true,
|
|
||||||
},
|
|
||||||
...columns,
|
|
||||||
];
|
|
||||||
},
|
|
||||||
set: (value) => {
|
|
||||||
// 缓存拖拽后的列宽,供 get 重新应用(即使父组件只用单向 :columns 绑定)
|
|
||||||
for (const column of value) {
|
|
||||||
if (column.width != null) {
|
|
||||||
resizedWidths.value[getColumnKey(column)] = column.width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit(
|
|
||||||
"update:columns",
|
|
||||||
value
|
|
||||||
.filter((column) => !column.__fmsSelection)
|
|
||||||
.map((column) => {
|
|
||||||
if (!column.__fmsEditor && !column.__fmsAction) return column;
|
|
||||||
const rest = { ...column };
|
|
||||||
delete rest.customCell;
|
|
||||||
delete rest.__fmsEditor;
|
|
||||||
delete rest.__fmsAction;
|
|
||||||
return rest;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const slots = useSlots();
|
|
||||||
const tableRef = ref(null);
|
|
||||||
|
|
||||||
// 表格行高:既作为 CSS 变量控制实际 DOM 行高,又作为 StkTable 的 row-height /
|
|
||||||
// header-row-height prop 传入,确保虚拟滚动按实际 DOM 行高计算总高度、pageSize、
|
|
||||||
// offsetTop/offsetBottom。否则 StkTable 默认 rowHeight=28 与 CSS 变量 34 不一致,
|
|
||||||
// 会导致滚动到底部时计算高度与实际内容高度不匹配而回弹。
|
|
||||||
const rowHeight = 34;
|
|
||||||
const headerRowHeight = 40;
|
|
||||||
const customVars = ref({
|
|
||||||
"--header-row-height": `${headerRowHeight}px`,
|
|
||||||
"--row-height": `${rowHeight}px`,
|
|
||||||
"--footer-row-height": "36px",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { resume: refreshLayout } = useRafFn(
|
|
||||||
async () => {
|
|
||||||
await nextTick();
|
|
||||||
tableRef.value?.initVirtualScroll?.();
|
|
||||||
await nextTick();
|
|
||||||
},
|
|
||||||
{ immediate: false, once: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
useResizeObserver(
|
|
||||||
() => tableRef.value?.$el,
|
|
||||||
([entry]) => {
|
|
||||||
const { width, height } = entry.contentRect;
|
|
||||||
if (width > 0 && height > 0) refreshLayout();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Forward every slot except `empty`; the `empty` slot is handled below so the
|
|
||||||
// default Result fallback only renders when the parent does not provide one.
|
|
||||||
const forwardSlots = computed(() => {
|
|
||||||
const result = {};
|
|
||||||
for (const name in slots) {
|
|
||||||
if (name !== "empty") result[name] = slots[name];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 滚动到指定行;用于新增后自动聚焦
|
|
||||||
async function scrollToRow(rowKey) {
|
async function scrollToRow(rowKey) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
const table = tableRef.value;
|
const table = tableRef.value;
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
const rows = props.dataSource;
|
const idx = props.dataSource.findIndex(
|
||||||
const idx = rows.findIndex((row) => String(getRowKey(row)) === String(rowKey));
|
(row) => String(getRowKey(row)) === String(rowKey),
|
||||||
|
);
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
table.scrollTo(idx * rowHeight, null);
|
table.scrollTo(idx * rowHeight, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ scrollToRow });
|
function getSelectedArea() {
|
||||||
|
return tableRef.value?.getSelectedArea?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ scrollToRow, getSelectedArea });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -231,29 +110,29 @@ defineExpose({ scrollToRow });
|
|||||||
class="fms-table"
|
class="fms-table"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:style="customVars"
|
:style="customVars"
|
||||||
:auto-resize="false"
|
|
||||||
:row-height="rowHeight"
|
:row-height="rowHeight"
|
||||||
:header-row-height="headerRowHeight"
|
:header-row-height="headerRowHeight"
|
||||||
:virtual="virtual"
|
:virtual="virtual"
|
||||||
:virtual-x="virtualX"
|
:virtual-x="virtualX"
|
||||||
:col-resizable="colResizable"
|
:col-resizable="colResizable"
|
||||||
v-model:columns="tableColumnsModel"
|
:columns="displayColumns"
|
||||||
|
@update:columns="onColumnsUpdate"
|
||||||
:data-source="dataSource"
|
:data-source="dataSource"
|
||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
:bordered="bordered"
|
:bordered="bordered"
|
||||||
:no-data-full="noDataFull"
|
:no-data-full="noDataFull"
|
||||||
:area-selection="areaSelectionConfig"
|
:area-selection="{
|
||||||
@row-click="onStkRowClick"
|
enabled:true,
|
||||||
@area-selection-change="onAreaSelectionChange"
|
keyboard:true,
|
||||||
|
ctrl:false,
|
||||||
|
shift:false
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template v-for="(slot, name) in forwardSlots" :key="name" #[name]="slotProps">
|
|
||||||
<slot :name="name" v-bind="slotProps || {}" />
|
|
||||||
</template>
|
|
||||||
<template v-if="slots.empty" #empty>
|
<template v-if="slots.empty" #empty>
|
||||||
<slot name="empty" />
|
<slot name="empty" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else #empty>
|
<template v-else #empty>
|
||||||
<a-result status="info" title="暂无数据" sub-title="当前没有可显示的内容" />
|
<a-result status="info" title="暂无数据" sub-title="当前没有可显示的内容" size="small"/>
|
||||||
</template>
|
</template>
|
||||||
</StkTable>
|
</StkTable>
|
||||||
</template>
|
</template>
|
||||||
@@ -263,70 +142,110 @@ defineExpose({ scrollToRow });
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
:deep(.fms-cell-text) {
|
<!-- 非 scoped 全局样式:选择列原生 checkbox 外观模拟 antdv-next Checkbox,
|
||||||
display: inline-block;
|
避免每行创建 Checkbox 组件实例导致的滚动性能退化 -->
|
||||||
max-width: 100%;
|
<style lang="scss">
|
||||||
overflow: hidden;
|
.fms-checkbox {
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 编辑单元格 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: 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) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
:deep(
|
|
||||||
.fms-editable-cell-wrap .ant-input-number,
|
|
||||||
.fms-editable-cell-wrap .ant-select,
|
|
||||||
.fms-editable-cell-wrap .ant-input
|
|
||||||
) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 让 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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fms-checkbox-box {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checked .fms-checkbox-box {
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 2px;
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
border: solid #fff;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indeterminate .fms-checkbox-box {
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 3px;
|
||||||
|
top: 7px;
|
||||||
|
width: 8px;
|
||||||
|
height: 2px;
|
||||||
|
background: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 1px;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除:原 hide areaSelection 覆盖层;现已启用 StkTable 内置区域选取与单元格高亮,
|
// actions 列按钮容器
|
||||||
编辑态下由 EditableCell 的 input/select 展示焦点,选区边框由 .cell-range-selected 提供。 */
|
.fms-actions-cell {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作按钮基础样式
|
||||||
|
.fms-action-btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "更多" 展开按钮样式
|
||||||
|
.fms-action-more {
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
|
||||||
|
&.fms-action-more-open {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { h } from "vue";
|
|
||||||
import { Button, Popconfirm, Dropdown } from "antdv-next";
|
|
||||||
|
|
||||||
const stopBubble = (e) => e.stopPropagation();
|
|
||||||
|
|
||||||
function resolve(val, row) {
|
|
||||||
return typeof val === "function" ? val(row) : val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 actions 数组中的 type 映射为 antdv-next Button 的 type + danger 属性。
|
|
||||||
*
|
|
||||||
* type 语义是按钮颜色:
|
|
||||||
* "danger" → type="link" danger
|
|
||||||
* "primary" → type="primary"
|
|
||||||
* 其他 → 直接作为 Button type
|
|
||||||
*/
|
|
||||||
function mapButtonType(type) {
|
|
||||||
if (type === "danger") return { btnType: "link", danger: true };
|
|
||||||
return { btnType: type || "link", danger: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 声明式操作列渲染器。
|
|
||||||
*
|
|
||||||
* actions 数组每项:
|
|
||||||
* { type: "primary"|"danger"|"link"|..., label, confirm?, danger?, disabled?, visible?, onClick }
|
|
||||||
* { type: "more", label?:"更多", items: [{ key, label, danger?, disabled?, onClick }], visible? }
|
|
||||||
*
|
|
||||||
* @param {Array} actions
|
|
||||||
* @returns {Function} customCell 渲染函数
|
|
||||||
*/
|
|
||||||
export function createActionsCellRenderer(actions) {
|
|
||||||
return function ActionsCell({ row }) {
|
|
||||||
const visibleActions = actions.filter((a) => {
|
|
||||||
if (a.visible === undefined) return true;
|
|
||||||
return resolve(a.visible, row);
|
|
||||||
});
|
|
||||||
|
|
||||||
const children = visibleActions.map((action, idx) => {
|
|
||||||
// ── more 下拉菜单 ──
|
|
||||||
if (action.type === "more") {
|
|
||||||
const items = (action.items || []).map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
label: resolve(item.label, row),
|
|
||||||
danger: item.danger,
|
|
||||||
disabled: resolve(item.disabled, row),
|
|
||||||
}));
|
|
||||||
return h(
|
|
||||||
Dropdown,
|
|
||||||
{
|
|
||||||
key: idx,
|
|
||||||
menu: {
|
|
||||||
items,
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
const item = action.items.find((i) => i.key === key);
|
|
||||||
item?.onClick?.(row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
trigger: ["click"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () =>
|
|
||||||
h(
|
|
||||||
Button,
|
|
||||||
{
|
|
||||||
type: "link",
|
|
||||||
size: "small",
|
|
||||||
disabled: resolve(action.disabled, row),
|
|
||||||
onClick: stopBubble,
|
|
||||||
},
|
|
||||||
() => resolve(action.label, row) || "更多",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 普通按钮(可包含 confirm) ──
|
|
||||||
const { btnType, danger } = mapButtonType(action.type);
|
|
||||||
const btnProps = {
|
|
||||||
type: btnType,
|
|
||||||
size: "small",
|
|
||||||
danger: action.danger ?? danger,
|
|
||||||
disabled: resolve(action.disabled, row),
|
|
||||||
onClick: (e) => {
|
|
||||||
stopBubble(e);
|
|
||||||
if (!action.confirm) action.onClick?.(row);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const btnNode = h(Button, btnProps, () => resolve(action.label, row));
|
|
||||||
|
|
||||||
if (action.confirm) {
|
|
||||||
const confirmConf = typeof action.confirm === "object" ? action.confirm : {};
|
|
||||||
return h(
|
|
||||||
Popconfirm,
|
|
||||||
{
|
|
||||||
key: idx,
|
|
||||||
title: resolve(confirmConf.title, row) ?? "确定?",
|
|
||||||
okText: resolve(confirmConf.okText, row),
|
|
||||||
cancelText: resolve(confirmConf.cancelText, row),
|
|
||||||
onConfirm: () => action.onClick?.(row),
|
|
||||||
},
|
|
||||||
{ default: () => btnNode },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return h("span", { key: idx }, btnNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
return h(
|
|
||||||
"span",
|
|
||||||
{ style: { display: "inline-flex", gap: "4px", alignItems: "center" } },
|
|
||||||
children,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { h, ref } from "vue";
|
||||||
|
import { Dropdown, Menu, MenuItem, Modal } from "antdv-next";
|
||||||
|
import { createInputCell } from "./createEditableCellRenderer";
|
||||||
|
|
||||||
|
const stop = (e) => e.stopPropagation();
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
// 1. 通用操作触发:支持 confirm 弹窗
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
function triggerAction(action, row) {
|
||||||
|
if (action.confirm) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: action.confirm,
|
||||||
|
okText: "确定",
|
||||||
|
okType: "danger",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk: () => action.onClick?.(row),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
action.onClick?.(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
// 2. checkbox 类型渲染器
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
function createCheckboxCell(col) {
|
||||||
|
return ({ row, cellValue }) => {
|
||||||
|
const checked = cellValue === 1;
|
||||||
|
return h(
|
||||||
|
"label",
|
||||||
|
{
|
||||||
|
class: checked ? "fms-checkbox checked" : "fms-checkbox",
|
||||||
|
onClick: stop,
|
||||||
|
onMousedown: stop,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked,
|
||||||
|
onChange: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
row[col.dataIndex] = e.target.checked ? 1 : 0;
|
||||||
|
col.onChange?.();
|
||||||
|
},
|
||||||
|
onClick: stop,
|
||||||
|
onMousedown: stop,
|
||||||
|
}),
|
||||||
|
h("span", { class: "fms-checkbox-box" }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
// 3. actions 类型渲染器
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
function createActionsCell(actions) {
|
||||||
|
if (!actions?.length) return undefined;
|
||||||
|
|
||||||
|
const inlineActions = actions.filter((a) => a.type !== "more");
|
||||||
|
const moreAction = actions.find((a) => a.type === "more");
|
||||||
|
const moreOpen = ref(false);
|
||||||
|
const moreRow = ref(null);
|
||||||
|
|
||||||
|
function makeButton(action, row) {
|
||||||
|
return h(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
class: "fms-action-btn",
|
||||||
|
onMousedown: stop,
|
||||||
|
onClick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
triggerAction(action, row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action.label || action.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({ row }) => {
|
||||||
|
const children = [];
|
||||||
|
|
||||||
|
for (const action of inlineActions) {
|
||||||
|
children.push(makeButton(action, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moreAction?.items?.length) {
|
||||||
|
if (!moreOpen.value) {
|
||||||
|
children.push(
|
||||||
|
h(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
class: "fms-action-btn fms-action-more",
|
||||||
|
onMousedown: stop,
|
||||||
|
onClick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
moreOpen.value = true;
|
||||||
|
moreRow.value = row;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"更多",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.push(
|
||||||
|
h(
|
||||||
|
Dropdown,
|
||||||
|
{
|
||||||
|
visible: true,
|
||||||
|
trigger: "click",
|
||||||
|
placement: "bottomRight",
|
||||||
|
onVisibleChange: (v) => {
|
||||||
|
if (!v) {
|
||||||
|
moreOpen.value = false;
|
||||||
|
moreRow.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () =>
|
||||||
|
h(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
class: "fms-action-btn fms-action-more fms-action-more-open",
|
||||||
|
onMousedown: stop,
|
||||||
|
},
|
||||||
|
"更多",
|
||||||
|
),
|
||||||
|
overlay: () =>
|
||||||
|
h(Menu, null, {
|
||||||
|
default: () =>
|
||||||
|
moreAction.items.map((item) =>
|
||||||
|
h(MenuItem, { key: item.label }, {
|
||||||
|
default: () =>
|
||||||
|
h(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
onClick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
moreOpen.value = false;
|
||||||
|
triggerAction(item, moreRow.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
item.label,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h("span", { class: "fms-actions-cell" }, children);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
// 4. 统一入口:根据 col.type 注入 customCell
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
const cellCache = new WeakMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据列定义的 type 属性自动注入 customCell 渲染器。
|
||||||
|
* - `type: "input"` → 原生 input(编辑态通过 row._editing 控制,非编辑态零 VNode 纯文本)
|
||||||
|
* - `type: "checkbox"` → 原生 checkbox
|
||||||
|
* - `type: "actions"` → 操作按钮列(从 col.actions 读取按钮配置)
|
||||||
|
* - 无 type 或有自定义 customCell → 保持原样
|
||||||
|
*
|
||||||
|
* input 类型不做缓存:因为 customCell 读的是 row._editing 运行时状态,
|
||||||
|
* 不依赖闭包捕获的外部值,每次创建相同函数亦可安全返回。
|
||||||
|
* checkbox/actions 使用 WeakMap 缓存,避免重复创建。
|
||||||
|
*/
|
||||||
|
export function resolveCustomCell(col) {
|
||||||
|
if (col.customCell) return col;
|
||||||
|
|
||||||
|
const cached = cellCache.get(col);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
let result = col;
|
||||||
|
if (col.type === "input") {
|
||||||
|
// 不缓存:row._editing 在渲染时动态读取
|
||||||
|
return { ...col, customCell: createInputCell(col) };
|
||||||
|
} else if (col.type === "checkbox") {
|
||||||
|
result = { ...col, customCell: createCheckboxCell(col) };
|
||||||
|
} else if (col.type === "actions") {
|
||||||
|
const fn = createActionsCell(col.actions);
|
||||||
|
if (fn) result = { ...col, customCell: fn };
|
||||||
|
}
|
||||||
|
|
||||||
|
cellCache.set(col, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,185 +1,52 @@
|
|||||||
import { h, nextTick, watch } from "vue";
|
import { h } from "vue";
|
||||||
import { Checkbox, Input, InputNumber, Select } from "antdv-next";
|
|
||||||
|
|
||||||
function resolveOption(option, row) {
|
/**
|
||||||
return typeof option === "function" ? option(row) : option;
|
* 为 type:"input" 的列创建 customCell 渲染器。
|
||||||
}
|
*
|
||||||
|
* 编辑状态通过 row._editing 控制:
|
||||||
export function createEditableCellRenderer({
|
* - row._editing = { mode: "cell", col: "b_code" } → 该列显示 <input>(自动聚焦+全选)
|
||||||
getRowKey,
|
* - row._editing = undefined 或 col 不匹配 → 非编辑态,直接返回纯文本字符串(零 VNode 开销)
|
||||||
getActiveRowKey,
|
*
|
||||||
getActiveColKey,
|
* 退出编辑时调用 col._onEditEnd(),由父组件触发全表重渲染。
|
||||||
getFirstEditableColKey,
|
*/
|
||||||
}) {
|
export function createInputCell(col) {
|
||||||
// 每个单元格独立的 debounce timer,避免每次击键都触发
|
return ({ row, cellValue }) => {
|
||||||
// onChange → emitUpdate → emit → parent drafts → watch → localData 新引用
|
const editing = row._editing;
|
||||||
// → FmsTable dataSource 新引用 → StkTable 全量虚拟滚动重算 → 卡顿。
|
if (!editing || editing.mode !== "cell" || editing.col !== col.dataIndex) {
|
||||||
// 150ms debounce 让连续打字期间只触发一次同步。
|
return cellValue ?? "";
|
||||||
const changeTimers = new Map();
|
|
||||||
|
|
||||||
// 追踪已聚焦过的单元格 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%", height: "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?.();
|
|
||||||
// 记录当前 input 元素,供下方 watch 兜底重新聚焦(应对 area-selection 抢焦点)。
|
|
||||||
lastFocusedEl = inputEl || el || null;
|
|
||||||
};
|
|
||||||
focusRefCache.set(cellKey, fn);
|
|
||||||
}
|
}
|
||||||
return fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兜底重新聚焦:activeRowKey / activeColKey 变化(来自 area-selection 键盘导航
|
const exitEdit = () => {
|
||||||
// 或鼠标点击)后,StkTable 会在按键同步阶段把焦点抢回表格容器(blurCellElement),
|
delete row._editing;
|
||||||
// 这里在 nextTick(晚于该抢占)把焦点交还当前编辑单元格的 Input 并选中文本,
|
col._onEditEnd?.();
|
||||||
// 保证编辑态下光标稳定、可直接覆盖输入。
|
};
|
||||||
watch(
|
|
||||||
() => [getActiveRowKey(), getActiveColKey ? getActiveColKey() : null],
|
return h("input", {
|
||||||
() => {
|
type: "text",
|
||||||
nextTick(() => {
|
value: cellValue ?? "",
|
||||||
if (!lastFocusedEl || !lastFocusedEl.isConnected) return;
|
class: "fms-editable-input",
|
||||||
if (document.activeElement !== lastFocusedEl) {
|
onInput: (e) => {
|
||||||
lastFocusedEl.focus?.({ preventScroll: true });
|
row[col.dataIndex] = e.target.value;
|
||||||
|
},
|
||||||
|
onBlur: () => {
|
||||||
|
col.onChange?.();
|
||||||
|
exitEdit();
|
||||||
|
},
|
||||||
|
onKeydown: (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
col.onChange?.();
|
||||||
|
exitEdit();
|
||||||
}
|
}
|
||||||
lastFocusedEl.select?.();
|
if (e.key === "Enter") {
|
||||||
});
|
col.onChange?.();
|
||||||
},
|
}
|
||||||
);
|
},
|
||||||
|
ref: (el) => {
|
||||||
return function EditableCell({ row, col, cellValue }) {
|
if (el) {
|
||||||
const editor = typeof col.editor === "string" ? { type: col.editor } : col.editor || {};
|
el.focus();
|
||||||
const rowKey = String(getRowKey(row));
|
el.select();
|
||||||
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 targetColKey = activeColKey != null ? activeColKey : (getFirstEditableColKey?.() ?? null);
|
|
||||||
const shouldFocus = isEditing && col.dataIndex === targetColKey;
|
|
||||||
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 ?? "");
|
|
||||||
|
|
||||||
// 仅“编辑行”才渲染 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,
|
|
||||||
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" }, [editComponent]);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
import { h } from "vue";
|
import { computed, h } from "vue";
|
||||||
import { Checkbox } from "antdv-next";
|
|
||||||
|
// 单例事件处理器:避免每行渲染创建新闭包
|
||||||
|
const stop = (e) => e.stopPropagation();
|
||||||
|
|
||||||
|
// ── 导出 ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建选择列定义,返回可直接插入 columns 的列对象。
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Function} options.getDataSource - 获取当前数据源(响应式)
|
||||||
|
* @param {Function} options.getRowKey - 从行数据提取 key
|
||||||
|
* @param {Function} options.getSelectedRowKeys - 获取已选 key 数组(响应式)
|
||||||
|
* @param {Function} options.onChange - 选中变化回调 (keys, rows)
|
||||||
|
* @returns {Object} selectionColumn
|
||||||
|
*/
|
||||||
|
export function createSelectionColumn({ getDataSource, getRowKey, getSelectedRowKeys, onChange }) {
|
||||||
|
// 已选 key 集合,内部响应式计算,仅对外暴露 column 对象
|
||||||
|
const selectedRowKeySet = computed(() => new Set(getSelectedRowKeys()));
|
||||||
|
|
||||||
export function createSelectionCellRenderers({
|
|
||||||
getDataSource,
|
|
||||||
getRowKey,
|
|
||||||
getSelectedRowKeySet,
|
|
||||||
onChange,
|
|
||||||
}) {
|
|
||||||
function updateSelection(nextKeys) {
|
function updateSelection(nextKeys) {
|
||||||
const keySet = new Set(nextKeys);
|
const keySet = new Set(nextKeys);
|
||||||
const rows = getDataSource().filter((row) => keySet.has(getRowKey(row)));
|
const rows = getDataSource().filter((row) => keySet.has(getRowKey(row)));
|
||||||
@@ -14,7 +26,7 @@ export function createSelectionCellRenderers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeRow(row, checked) {
|
function changeRow(row, checked) {
|
||||||
const nextKeys = new Set(getSelectedRowKeySet());
|
const nextKeys = new Set(selectedRowKeySet.value);
|
||||||
const key = getRowKey(row);
|
const key = getRowKey(row);
|
||||||
if (checked) nextKeys.add(key);
|
if (checked) nextKeys.add(key);
|
||||||
else nextKeys.delete(key);
|
else nextKeys.delete(key);
|
||||||
@@ -22,7 +34,7 @@ export function createSelectionCellRenderers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeAll(checked) {
|
function changeAll(checked) {
|
||||||
const nextKeys = new Set(getSelectedRowKeySet());
|
const nextKeys = new Set(selectedRowKeySet.value);
|
||||||
getDataSource().forEach((row) => {
|
getDataSource().forEach((row) => {
|
||||||
const key = getRowKey(row);
|
const key = getRowKey(row);
|
||||||
if (checked) nextKeys.add(key);
|
if (checked) nextKeys.add(key);
|
||||||
@@ -31,26 +43,71 @@ export function createSelectionCellRenderers({
|
|||||||
updateSelection(Array.from(nextKeys));
|
updateSelection(Array.from(nextKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectionCell = ({ row }) =>
|
// 行复选框:原生 <input type="checkbox">,零组件开销
|
||||||
h(Checkbox, {
|
const SelectionCell = ({ row }) => {
|
||||||
checked: getSelectedRowKeySet().has(getRowKey(row)),
|
const key = getRowKey(row);
|
||||||
"onUpdate:checked": (checked) => changeRow(row, checked),
|
const checked = selectedRowKeySet.value.has(key);
|
||||||
onClick: (event) => event.stopPropagation(),
|
return h(
|
||||||
// 阻止 mousedown 冒泡到表格根,避免触发 area-selection 误将本行设入编辑态
|
"label",
|
||||||
onMousedown: (event) => event.stopPropagation(),
|
{ class: checked ? "fms-checkbox checked" : "fms-checkbox", onClick: stop, onMousedown: stop },
|
||||||
});
|
[
|
||||||
|
h("input", {
|
||||||
const SelectionHeaderCell = () => {
|
type: "checkbox",
|
||||||
const rows = getDataSource();
|
checked,
|
||||||
const selectedRowKeySet = getSelectedRowKeySet();
|
onChange: (e) => {
|
||||||
const selectedCount = rows.filter((row) => selectedRowKeySet.has(getRowKey(row))).length;
|
e.stopPropagation();
|
||||||
return h(Checkbox, {
|
changeRow(row, e.target.checked);
|
||||||
checked: rows.length > 0 && selectedCount === rows.length,
|
},
|
||||||
indeterminate: selectedCount > 0 && selectedCount < rows.length,
|
onClick: stop,
|
||||||
"onUpdate:checked": changeAll,
|
onMousedown: stop,
|
||||||
onClick: (event) => event.stopPropagation(),
|
}),
|
||||||
});
|
h("span", { class: "fms-checkbox-box" }),
|
||||||
|
],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { SelectionCell, SelectionHeaderCell };
|
// 表头全选复选框:直接使用 Set.size 判断已选数,避免 filter 全表遍历
|
||||||
|
const SelectionHeaderCell = () => {
|
||||||
|
const rows = getDataSource();
|
||||||
|
const selectedCount = selectedRowKeySet.value.size;
|
||||||
|
const allChecked = rows.length > 0 && selectedCount === rows.length;
|
||||||
|
const indeterminate = selectedCount > 0 && selectedCount < rows.length;
|
||||||
|
|
||||||
|
const labelClass = allChecked
|
||||||
|
? "fms-checkbox checked"
|
||||||
|
: indeterminate
|
||||||
|
? "fms-checkbox indeterminate"
|
||||||
|
: "fms-checkbox";
|
||||||
|
|
||||||
|
return h("label", { class: labelClass, onClick: stop }, [
|
||||||
|
h("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked: allChecked,
|
||||||
|
onChange: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
changeAll(e.target.checked);
|
||||||
|
},
|
||||||
|
onClick: stop,
|
||||||
|
}),
|
||||||
|
h("span", { class: "fms-checkbox-box" }),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataIndex: "__fmsSelection",
|
||||||
|
key: "__fmsSelection",
|
||||||
|
title: "",
|
||||||
|
width: 44,
|
||||||
|
fixed: "left",
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
customCell: SelectionCell,
|
||||||
|
customHeaderCell: SelectionHeaderCell,
|
||||||
|
__fmsSelection: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断某列是否为选择列(用于 onColumnsUpdate 剥离内部列) */
|
||||||
|
export function isSelectionColumn(col) {
|
||||||
|
return col?.__fmsSelection === true;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { h, ref } from "vue";
|
import { h, shallowRef } from "vue";
|
||||||
import { message } from "antdv-next";
|
import { message } from "antdv-next";
|
||||||
import { Input, InputNumber, Select } from "antdv-next";
|
import { Input, InputNumber, Select } from "antdv-next";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
@@ -46,11 +46,11 @@ function buildData() {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSource = ref(buildData());
|
const dataSource = shallowRef(buildData());
|
||||||
|
|
||||||
// 当前编辑行 key;进入编辑时留存快照,取消可还原
|
// 当前编辑行 key;进入编辑时留存快照,取消可还原
|
||||||
const editingKey = ref(null);
|
const editingKey = shallowRef(null);
|
||||||
const editSnapshot = ref(null);
|
const editSnapshot = shallowRef(null);
|
||||||
|
|
||||||
function editConfigFor(c) {
|
function editConfigFor(c) {
|
||||||
switch (c % 5) {
|
switch (c % 5) {
|
||||||
@@ -63,17 +63,20 @@ function editConfigFor(c) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单元格渲染:编辑行显示控件,其他行显示纯文本
|
// 单元格渲染:编辑行显示控件,其他行直接返回文本值(让 stk-table 用原生 textContent 渲染,零 VNode 开销)
|
||||||
function makeCellEditor(dataIndex, edit) {
|
function makeCellEditor(dataIndex, edit) {
|
||||||
|
const selectOptions = edit.options || [];
|
||||||
return (props) => {
|
return (props) => {
|
||||||
const { row, cellValue } = props;
|
const { row, cellValue } = props;
|
||||||
if (row.id !== editingKey.value) {
|
if (row.id !== editingKey.value) {
|
||||||
|
// 非编辑态:直接返回文本字符串,不创建 VNode 包装
|
||||||
if (edit.type === "select") {
|
if (edit.type === "select") {
|
||||||
const opt = (edit.options || []).find((o) => o.value === cellValue);
|
const opt = selectOptions.find((o) => o.value === cellValue);
|
||||||
return h("span", { class: "cell-display" }, opt ? opt.label : (cellValue ?? ""));
|
return opt ? opt.label : (cellValue ?? "");
|
||||||
}
|
}
|
||||||
return h("span", { class: "cell-display" }, cellValue ?? "");
|
return cellValue ?? "";
|
||||||
}
|
}
|
||||||
|
// 编辑态:返回编辑组件 VNode
|
||||||
const update = (v) => {
|
const update = (v) => {
|
||||||
row[dataIndex] = v;
|
row[dataIndex] = v;
|
||||||
};
|
};
|
||||||
@@ -96,7 +99,7 @@ function makeCellEditor(dataIndex, edit) {
|
|||||||
return h(Select, {
|
return h(Select, {
|
||||||
...common,
|
...common,
|
||||||
value: cellValue,
|
value: cellValue,
|
||||||
options: edit.options || [],
|
options: selectOptions,
|
||||||
"onUpdate:value": update,
|
"onUpdate:value": update,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,7 +135,7 @@ function buildColumns() {
|
|||||||
return cols;
|
return cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = ref(buildColumns());
|
const columns = shallowRef(buildColumns());
|
||||||
|
|
||||||
function onRowClick(ev, row) {
|
function onRowClick(ev, row) {
|
||||||
if (row.id === editingKey.value) return;
|
if (row.id === editingKey.value) return;
|
||||||
@@ -193,6 +196,7 @@ function rowClassName(row) {
|
|||||||
:data-source="dataSource"
|
:data-source="dataSource"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:row-class-name="rowClassName"
|
:row-class-name="rowClassName"
|
||||||
|
show-overflow
|
||||||
@row-click="onRowClick"
|
@row-click="onRowClick"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -249,16 +253,7 @@ function rowClassName(row) {
|
|||||||
padding: 12px 16px 16px;
|
padding: 12px 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.fms-row-editing) {
|
::deep(.fms-row-editing) {
|
||||||
background: rgba(22, 119, 255, 0.08) !important;
|
background: rgba(22, 119, 255, 0.08) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.cell-display) {
|
|
||||||
display: block;
|
|
||||||
min-height: 22px;
|
|
||||||
padding: 0 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { h, ref } from "vue";
|
import { h, shallowRef } from "vue";
|
||||||
import { message } from "antdv-next";
|
import { message } from "antdv-next";
|
||||||
import { Input, InputNumber, Select } from "antdv-next";
|
import { Input, InputNumber, Select } from "antdv-next";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
@@ -47,11 +47,11 @@ function buildData() {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSource = ref(buildData());
|
const dataSource = shallowRef(buildData());
|
||||||
|
|
||||||
// 当前编辑行 key;进入编辑时留存快照,取消可还原
|
// 当前编辑行 key;进入编辑时留存快照,取消可还原
|
||||||
const editingKey = ref(null);
|
const editingKey = shallowRef(null);
|
||||||
const editSnapshot = ref(null);
|
const editSnapshot = shallowRef(null);
|
||||||
|
|
||||||
function editConfigFor(c) {
|
function editConfigFor(c) {
|
||||||
switch (c % 5) {
|
switch (c % 5) {
|
||||||
@@ -64,17 +64,20 @@ function editConfigFor(c) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单元格渲染:编辑行显示控件,其他行显示纯文本
|
// 单元格渲染:编辑行显示控件,其他行直接返回文本值(让 stk-table 用原生 textContent 渲染,零 VNode 开销)
|
||||||
function makeCellEditor(dataIndex, edit) {
|
function makeCellEditor(dataIndex, edit) {
|
||||||
|
const selectOptions = edit.options || [];
|
||||||
return (props) => {
|
return (props) => {
|
||||||
const { row, cellValue } = props;
|
const { row, cellValue } = props;
|
||||||
if (row.id !== editingKey.value) {
|
if (row.id !== editingKey.value) {
|
||||||
|
// 非编辑态:直接返回文本字符串,不创建 VNode 包装
|
||||||
if (edit.type === "select") {
|
if (edit.type === "select") {
|
||||||
const opt = (edit.options || []).find((o) => o.value === cellValue);
|
const opt = selectOptions.find((o) => o.value === cellValue);
|
||||||
return h("span", { class: "cell-display" }, opt ? opt.label : (cellValue ?? ""));
|
return opt ? opt.label : (cellValue ?? "");
|
||||||
}
|
}
|
||||||
return h("span", { class: "cell-display" }, cellValue ?? "");
|
return cellValue ?? "";
|
||||||
}
|
}
|
||||||
|
// 编辑态:返回编辑组件 VNode
|
||||||
const update = (v) => {
|
const update = (v) => {
|
||||||
row[dataIndex] = v;
|
row[dataIndex] = v;
|
||||||
};
|
};
|
||||||
@@ -97,7 +100,7 @@ function makeCellEditor(dataIndex, edit) {
|
|||||||
return h(Select, {
|
return h(Select, {
|
||||||
...common,
|
...common,
|
||||||
value: cellValue,
|
value: cellValue,
|
||||||
options: edit.options || [],
|
options: selectOptions,
|
||||||
"onUpdate:value": update,
|
"onUpdate:value": update,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -133,7 +136,7 @@ function buildColumns() {
|
|||||||
return cols;
|
return cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = ref(buildColumns());
|
const columns = shallowRef(buildColumns());
|
||||||
|
|
||||||
function onRowClick(ev, row) {
|
function onRowClick(ev, row) {
|
||||||
if (row.id === editingKey.value) return;
|
if (row.id === editingKey.value) return;
|
||||||
@@ -195,6 +198,7 @@ function rowClassName(row) {
|
|||||||
:data-source="dataSource"
|
:data-source="dataSource"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:row-class-name="rowClassName"
|
:row-class-name="rowClassName"
|
||||||
|
show-overflow
|
||||||
@row-click="onRowClick"
|
@row-click="onRowClick"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -252,16 +256,7 @@ function rowClassName(row) {
|
|||||||
padding: 12px 16px 16px;
|
padding: 12px 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.fms-row-editing) {
|
::deep(.fms-row-editing) {
|
||||||
background: rgba(22, 119, 255, 0.08) !important;
|
background: rgba(22, 119, 255, 0.08) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.cell-display) {
|
|
||||||
display: block;
|
|
||||||
min-height: 22px;
|
|
||||||
padding: 0 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, shallowRef, watch } from "vue";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
import { createTempId } from "@/utils/tempId";
|
import { createTempId } from "@/utils/tempId";
|
||||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||||
@@ -74,7 +74,7 @@ function makeSwitchCol(field, title, width) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = shallowRef([
|
||||||
{
|
{
|
||||||
dataIndex: "b_field_id",
|
dataIndex: "b_field_id",
|
||||||
title: "字段",
|
title: "字段",
|
||||||
@@ -113,7 +113,7 @@ const columns = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -126,7 +126,7 @@ const columns = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FmsTable
|
<FmsTable
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="localEdit"
|
:data-source="localEdit"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:active-row-key="activeKey"
|
:active-row-key="activeKey"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from "vue";
|
import { ref, shallowRef, watch } from "vue";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
|
|
||||||
const FIELD_TYPES = [
|
const FIELD_TYPES = [
|
||||||
@@ -40,7 +40,7 @@ function emitUpdate() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = shallowRef([
|
||||||
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
||||||
{ dataIndex: "b_name", title: "字段名称", width: 140 },
|
{ dataIndex: "b_name", title: "字段名称", width: 140 },
|
||||||
{
|
{
|
||||||
@@ -62,7 +62,7 @@ const columns = [
|
|||||||
align: "center",
|
align: "center",
|
||||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -76,7 +76,7 @@ const columns = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FmsTable
|
<FmsTable
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="localFields"
|
:data-source="localFields"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:active-row-key="activeKey"
|
:active-row-key="activeKey"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h, nextTick, ref, watch } from "vue";
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import { message, Modal } from "antdv-next";
|
import { message, Modal } from "antdv-next";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
import FpsMonitor from "@/components/FpsMonitor.vue";
|
import FpsMonitor from "@/components/FpsMonitor.vue";
|
||||||
@@ -174,38 +174,6 @@ const displayedData = computed(() => {
|
|||||||
return pivotRows.value.filter((r) => (r.b_key || "").toLowerCase().includes(kw));
|
return pivotRows.value.filter((r) => (r.b_key || "").toLowerCase().includes(kw));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 操作列单元格:普通文字「删除」按钮(原生 button,避免每行一个组件实例开销)
|
|
||||||
const ActionsCell = ({ row }) =>
|
|
||||||
h(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
title: "删除",
|
|
||||||
onMousedown: (e) => e.stopPropagation(),
|
|
||||||
style: {
|
|
||||||
border: "none",
|
|
||||||
background: "transparent",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "2px 4px",
|
|
||||||
color: "#ff4d4f",
|
|
||||||
fontSize: "12px",
|
|
||||||
lineHeight: "20px",
|
|
||||||
},
|
|
||||||
onClick: (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
Modal.confirm({
|
|
||||||
title: "确定删除该记录(含其全部语言)?",
|
|
||||||
okText: "删除",
|
|
||||||
okType: "danger",
|
|
||||||
cancelText: "取消",
|
|
||||||
onOk: () => deleteRow(row),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"删除",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 动态列:键 + 每种语言一列 + 操作列
|
|
||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
const cols = [{ dataIndex: "b_key", title: "键", width: 240, editor: { type: "input" } }];
|
const cols = [{ dataIndex: "b_key", title: "键", width: 240, editor: { type: "input" } }];
|
||||||
for (const opt of langOptions.value) {
|
for (const opt of langOptions.value) {
|
||||||
@@ -221,7 +189,10 @@ const columns = computed(() => {
|
|||||||
title: "操作",
|
title: "操作",
|
||||||
width: 60,
|
width: 60,
|
||||||
align: "center",
|
align: "center",
|
||||||
customCell: ActionsCell,
|
type: "actions",
|
||||||
|
actions: [
|
||||||
|
{ type: "delete", label: "删除", confirm: "确定删除该记录(含其全部语言)?", onClick: (row) => deleteRow(row) },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
return cols;
|
return cols;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, shallowRef, watch } from "vue";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
import { createTempId } from "@/utils/tempId";
|
import { createTempId } from "@/utils/tempId";
|
||||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||||
@@ -66,7 +66,7 @@ function deleteRow(record) {
|
|||||||
emitUpdate();
|
emitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = shallowRef([
|
||||||
{
|
{
|
||||||
dataIndex: "b_field_id",
|
dataIndex: "b_field_id",
|
||||||
title: "字段",
|
title: "字段",
|
||||||
@@ -118,7 +118,7 @@ const columns = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -131,7 +131,7 @@ const columns = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FmsTable
|
<FmsTable
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="localList"
|
:data-source="localList"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:active-row-key="activeKey"
|
:active-row-key="activeKey"
|
||||||
|
|||||||
@@ -97,6 +97,49 @@ function batchDelete() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 编辑状态管理 ──
|
||||||
|
// 通过 row._editing = { mode: "cell", col: dataIndex } 控制单元格编辑。
|
||||||
|
// area-selection-change 统一响应鼠标点击和键盘方向键,无需额外 row-click 处理。
|
||||||
|
// 编辑状态变更后需触发表格重渲染。
|
||||||
|
function handleEditEnd() {
|
||||||
|
let hasEditing = false;
|
||||||
|
localPowers.value.forEach((r) => {
|
||||||
|
if (r._editing) {
|
||||||
|
delete r._editing;
|
||||||
|
hasEditing = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasEditing) localPowers.value = [...localPowers.value];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAreaSelectionChange(ranges) {
|
||||||
|
if (!ranges?.length) {
|
||||||
|
handleEditEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const begin = ranges[0].index.begin;
|
||||||
|
const row = localPowers.value[begin.row];
|
||||||
|
const area = tableRef.value?.getSelectedArea();
|
||||||
|
if (!row || !area) return;
|
||||||
|
|
||||||
|
const col = area.cols[begin.col];
|
||||||
|
if (!col || col.type !== "input") {
|
||||||
|
handleEditEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowId = String(row.b_id);
|
||||||
|
// 目标未变则跳过,避免方向键连续触发全表重渲染
|
||||||
|
const cur = row._editing;
|
||||||
|
if (cur && cur.mode === "cell" && cur.col === col.dataIndex && cur.rowId === rowId) return;
|
||||||
|
|
||||||
|
localPowers.value.forEach((r) => {
|
||||||
|
if (r._editing) delete r._editing;
|
||||||
|
});
|
||||||
|
row._editing = { mode: "cell", col: col.dataIndex, rowId };
|
||||||
|
localPowers.value = [...localPowers.value];
|
||||||
|
}
|
||||||
|
|
||||||
// ── 快速设置多语言 ──
|
// ── 快速设置多语言 ──
|
||||||
const i18nModalRef = ref(null);
|
const i18nModalRef = ref(null);
|
||||||
|
|
||||||
@@ -153,71 +196,16 @@ const I18nKeyCell = ({ row, cellValue }) =>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 启用状态单元格:原生 checkbox + 自定义 CSS 模拟 antdv-next 外观,不创建组件实例
|
const columns = ref([
|
||||||
const CanUseCell = ({ row, cellValue }) => {
|
{ dataIndex: "b_code", title: "操作编码", width: 160, type: "input", onChange: () => emitUpdate(), _onEditEnd: handleEditEnd },
|
||||||
const checked = cellValue === 1;
|
{ dataIndex: "b_name", title: "操作名称", width: 160, type: "input", onChange: () => emitUpdate(), _onEditEnd: handleEditEnd },
|
||||||
return h(
|
|
||||||
"label",
|
|
||||||
{
|
|
||||||
class: "pw-checkbox",
|
|
||||||
onClick: (e) => e.stopPropagation(),
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h("input", {
|
|
||||||
type: "checkbox",
|
|
||||||
checked: checked,
|
|
||||||
onChange: (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
row.b_canuse = e.target.checked ? 1 : 0;
|
|
||||||
emitUpdate();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
h("span", {
|
|
||||||
class: ["pw-checkbox-box", checked ? "checked" : null].filter(Boolean).join(" "),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 操作列单元格:普通文字「删除」按钮(原生 button,避免每行一个组件实例开销)
|
|
||||||
const ActionsCell = ({ row }) =>
|
|
||||||
h(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
title: "删除",
|
|
||||||
onMousedown: (e) => e.stopPropagation(),
|
|
||||||
style: {
|
|
||||||
border: "none",
|
|
||||||
background: "transparent",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "2px 4px",
|
|
||||||
color: "#ff4d4f",
|
|
||||||
fontSize: "12px",
|
|
||||||
lineHeight: "20px",
|
|
||||||
},
|
|
||||||
onClick: (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
Modal.confirm({
|
|
||||||
title: "确定删除该记录?",
|
|
||||||
okText: "删除",
|
|
||||||
okType: "danger",
|
|
||||||
cancelText: "取消",
|
|
||||||
onOk: () => deleteRow(row),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"删除",
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ dataIndex: "b_code", title: "操作编码", width: 160, editor: { onChange: () => emitUpdate() } },
|
|
||||||
{ dataIndex: "b_name", title: "操作名称", width: 160, editor: { onChange: () => emitUpdate() } },
|
|
||||||
{ dataIndex: "b_i18n", title: "多语言键", width: 160, customCell: I18nKeyCell },
|
{ dataIndex: "b_i18n", title: "多语言键", width: 160, customCell: I18nKeyCell },
|
||||||
{ dataIndex: "b_xh", title: "排序", width: 80, editor: { onChange: () => emitUpdate() } },
|
{ dataIndex: "b_xh", title: "排序", width: 80, type: "input", onChange: () => emitUpdate(), _onEditEnd: handleEditEnd },
|
||||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", customCell: CanUseCell },
|
{ dataIndex: "b_canuse", title: "启用", width: 70, type: "checkbox", onChange: () => emitUpdate() },
|
||||||
{ dataIndex: "actions", title: "操作", width: 60, align: "center", customCell: ActionsCell },
|
{ dataIndex: "actions", title: "操作", width: 60, type: "actions", actions: [
|
||||||
];
|
{ type: "delete", label: "删除", confirm: "确定删除该记录?", onClick: (row) => deleteRow(row) },
|
||||||
|
] },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -234,12 +222,14 @@ const columns = [
|
|||||||
<FpsMonitor />
|
<FpsMonitor />
|
||||||
<FmsTable
|
<FmsTable
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="localPowers"
|
:data-source="localPowers"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:active-row-key="activeKey"
|
:active-row-key="activeKey"
|
||||||
:row-selection="rowSelection"
|
:row-selection="rowSelection"
|
||||||
|
:area-selection="true"
|
||||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||||
|
@area-selection-change="handleAreaSelectionChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<I18nQuickSetModal ref="i18nModalRef" />
|
<I18nQuickSetModal ref="i18nModalRef" />
|
||||||
@@ -273,57 +263,22 @@ const columns = [
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- 非 scoped 全局样式:customCell 通过 h() 渲染的元素不带 data-v 属性,
|
// 编辑态 input 样式
|
||||||
scoped 的 :deep() 可能不生效,用全局 style 确保 checkbox 样式注入。
|
:deep(.fms-editable-input) {
|
||||||
基于 antdv-next Checkbox 源码(checkbox/style/index.js)精确模拟外观。 -->
|
|
||||||
<style lang="scss">
|
|
||||||
.pw-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
height: 100%;
|
||||||
line-height: 1;
|
border: 1px solid #1677ff;
|
||||||
margin: 0;
|
border-radius: 4px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
input {
|
&:focus {
|
||||||
position: absolute;
|
border-color: #1677ff;
|
||||||
width: 0;
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.12);
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pw-checkbox-box {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
transition: all 0.2s;
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex: none;
|
|
||||||
|
|
||||||
&.checked {
|
|
||||||
background: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 5px;
|
|
||||||
top: 2px;
|
|
||||||
width: 4px;
|
|
||||||
height: 8px;
|
|
||||||
border: solid #fff;
|
|
||||||
border-width: 0 2px 2px 0;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, shallowRef, watch } from "vue";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
import { createTempId } from "@/utils/tempId";
|
import { createTempId } from "@/utils/tempId";
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ function deleteRow(record) {
|
|||||||
emitUpdate();
|
emitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = shallowRef([
|
||||||
{
|
{
|
||||||
dataIndex: "b_field_id",
|
dataIndex: "b_field_id",
|
||||||
title: "字段",
|
title: "字段",
|
||||||
@@ -123,7 +123,7 @@ const columns = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -135,7 +135,7 @@ const columns = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FmsTable
|
<FmsTable
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="localQuery"
|
:data-source="localQuery"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:active-row-key="activeKey"
|
:active-row-key="activeKey"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { message, Modal } from "antdv-next";
|
import { message, Modal } from "antdv-next";
|
||||||
import FmsTree from "@/components/FmsTree.vue";
|
import FmsTree from "@/components/FmsTree.vue";
|
||||||
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h, onMounted, ref } from "vue";
|
import { computed, h, onMounted, ref, shallowRef } from "vue";
|
||||||
import { message, Switch, Button, Popconfirm, Tag } from "antdv-next";
|
import { message, Switch, Button, Popconfirm, Tag } from "antdv-next";
|
||||||
import { Plus } from "@lucide/vue";
|
import { Plus } from "@lucide/vue";
|
||||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||||
@@ -101,7 +101,7 @@ function createActionsCell() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = shallowRef([
|
||||||
{ dataIndex: "b_code", title: "简称 / 编码", width: 150 },
|
{ dataIndex: "b_code", title: "简称 / 编码", width: 150 },
|
||||||
{ dataIndex: "b_name", title: "名称", width: 160 },
|
{ dataIndex: "b_name", title: "名称", width: 160 },
|
||||||
{
|
{
|
||||||
@@ -126,7 +126,7 @@ const columns = [
|
|||||||
align: "center",
|
align: "center",
|
||||||
customCell: createActionsCell(),
|
customCell: createActionsCell(),
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
/* ---------- 行内开关即时保存 ---------- */
|
/* ---------- 行内开关即时保存 ---------- */
|
||||||
async function saveRow(row, patch) {
|
async function saveRow(row, patch) {
|
||||||
@@ -247,7 +247,7 @@ async function saveForm() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FmsTable
|
<FmsTable
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="data"
|
:data-source="data"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
|||||||
Reference in new issue
Block a user