20260723225202

This commit is contained in:
oneao committed 2026-07-23 22:52:02 +08:00
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,