20260723225202
This commit is contained in:
1 parent
5f73b54802
commit
4d118a0037
22 files changed
+1403
-259
No files matched your search
@@ -2,6 +2,7 @@
|
||||
import { computed, nextTick, ref, useSlots } from "vue";
|
||||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
||||
import { StkTable } from "stk-table-vue";
|
||||
import { createActionsCellRenderer } from "./utils/createActionsCellRenderer";
|
||||
import { createEditableCellRenderer } from "./utils/createEditableCellRenderer";
|
||||
import { createSelectionCellRenderers } from "./utils/createSelectionCellRenderers";
|
||||
|
||||
@@ -41,14 +42,20 @@ const { SelectionCell, SelectionHeaderCell } = createSelectionCellRenderers({
|
||||
onChange: (keys, rows) => props.rowSelection?.onChange?.(keys, rows),
|
||||
});
|
||||
|
||||
// StkTable 的编辑单元格和多选列统一在封装层生成,业务页面只声明 editor / rowSelection。
|
||||
// StkTable 的编辑单元格、操作列和多选列统一在封装层生成,业务页面只声明 editor / actions / rowSelection。
|
||||
const tableColumnsModel = computed({
|
||||
get: () => {
|
||||
const columns = props.columns.map((column) =>
|
||||
column.editor && !column.customCell
|
||||
? { ...column, customCell: EditableCell, __fmsEditor: true }
|
||||
: column,
|
||||
);
|
||||
// 依赖 activeRowKey:切换编辑行时触发 columns 重算,迫使 StkTable 重渲染单元格
|
||||
void props.activeRowKey;
|
||||
const columns = props.columns.map((column) => {
|
||||
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 [
|
||||
{
|
||||
@@ -72,10 +79,11 @@ const tableColumnsModel = computed({
|
||||
value
|
||||
.filter((column) => !column.__fmsSelection)
|
||||
.map((column) => {
|
||||
if (!column.__fmsEditor) return column;
|
||||
if (!column.__fmsEditor && !column.__fmsAction) return column;
|
||||
const rest = { ...column };
|
||||
delete rest.customCell;
|
||||
delete rest.__fmsEditor;
|
||||
delete rest.__fmsAction;
|
||||
return rest;
|
||||
}),
|
||||
),
|
||||
@@ -117,6 +125,18 @@ const forwardSlots = computed(() => {
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
// 滚动到指定行;用于新增后自动聚焦
|
||||
async function scrollToRow(rowKey) {
|
||||
await nextTick();
|
||||
const el = tableRef.value?.$el;
|
||||
if (!el) return;
|
||||
const key = String(rowKey);
|
||||
const rowEl = el.querySelector(`[data-row-key="${key}"]`);
|
||||
rowEl?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
|
||||
}
|
||||
|
||||
defineExpose({ scrollToRow });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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,42 @@
|
||||
import { h } from "vue";
|
||||
|
||||
// ── 静态常量(避免每次渲染重复创建) ────────────────────────
|
||||
|
||||
const stopBubble = (e) => e.stopPropagation();
|
||||
|
||||
// ── 基础工厂 ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 通用 cell 渲染器工厂。
|
||||
*
|
||||
* 返回的函数是纯渲染函数,不保存 row 引用、不产生额外组件层级。
|
||||
*
|
||||
* @param {object} config
|
||||
* @param {object} config.component Vue 组件(Button / Checkbox / Select …)
|
||||
* @param {object} [config.props] 静态 props(创建一次,不随行变化)
|
||||
* @param {(row, col, ctx) => object} [config.getProps] 动态 props(每次 cell 渲染调用)
|
||||
* @param {(row, col, ctx) => object} [config.getChildren] 动态 children / slots
|
||||
* @param {boolean} [config.stopPropagation=true] 自动注入 onClick stopPropagation
|
||||
* @returns {Function} (ctx) => VNode customCell 渲染函数
|
||||
*/
|
||||
export function createCellRenderer({
|
||||
component,
|
||||
props = {},
|
||||
getProps,
|
||||
getChildren,
|
||||
stopPropagation = true,
|
||||
}) {
|
||||
return (ctx) => {
|
||||
const merged = getProps ? { ...props, ...getProps(ctx.row, ctx.col, ctx) } : { ...props };
|
||||
|
||||
if (stopPropagation) {
|
||||
const click = merged.onClick;
|
||||
merged.onClick = click ? (e) => (e.stopPropagation(), click(e)) : stopBubble;
|
||||
}
|
||||
|
||||
const children = getChildren ? getChildren(ctx.row, ctx.col, ctx) : undefined;
|
||||
return h(component, merged, children);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,24 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey }) {
|
||||
const activeRowKey = getActiveRowKey();
|
||||
const isEditing = activeRowKey != null && String(getRowKey(row)) === String(activeRowKey);
|
||||
|
||||
const update = (value) => {
|
||||
row[col.dataIndex] = value;
|
||||
editor.onChange?.(value, row, col);
|
||||
};
|
||||
|
||||
// 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),
|
||||
});
|
||||
}
|
||||
|
||||
if (!isEditing) {
|
||||
if (editor.type === "checkbox") {
|
||||
return h(Checkbox, {
|
||||
checked: cellValue === (editor.checkedValue ?? 1),
|
||||
style: { pointerEvents: "none" },
|
||||
});
|
||||
}
|
||||
if (editor.type === "select") {
|
||||
const options = resolveOption(editor.options, row) || [];
|
||||
const option = options.find((item) => item.value === cellValue);
|
||||
@@ -26,14 +37,17 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey }) {
|
||||
return h("span", { class: "fms-cell-text" }, cellValue ?? "");
|
||||
}
|
||||
|
||||
const update = (value) => {
|
||||
row[col.dataIndex] = value;
|
||||
editor.onChange?.(value, row, col);
|
||||
};
|
||||
const common = {
|
||||
size: "small",
|
||||
style: { width: "100%" },
|
||||
onClick: (event) => event.stopPropagation(),
|
||||
// 编辑态首次渲染自动聚焦
|
||||
ref: (instance) => {
|
||||
if (instance) {
|
||||
const target = instance.focus ? instance : instance.$el;
|
||||
target?.focus?.();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (editor.type === "number") {
|
||||
@@ -56,16 +70,6 @@ export function createEditableCellRenderer({ getRowKey, getActiveRowKey }) {
|
||||
"onUpdate:value": update,
|
||||
});
|
||||
}
|
||||
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),
|
||||
});
|
||||
}
|
||||
return h(Input, {
|
||||
...common,
|
||||
value: cellValue,
|
||||
|
||||
Reference in new issue
Block a user