/** * 表格操作列统一原子(方案:outputs/table-action-buttons/操作列统一方案-20261010.md) * * 三条变化轴各自独立,每加一种形态 = 加 1 个渲染器 + RENDERERS 表加 1 行,不动既有分支: * 轴1 形态 actionIcon / actionText / actionMore —— 只画节点,零策略 * 轴2 动作集 normalizeActions —— 纯数据:when 过滤 / disabled 归一 / danger 沉底 * 轴3 溢出 splitActions —— 纯函数:直出与「更多」分档,零 DOM 可单测 * 组装 actionCell / actionColumn —— 唯一懂 DOM 结构与表格列 * * 形态由页面【显式声明】(mode: "icon" | "text",缺省 text),不按动作数自动切换—— * 工程间风格区分是既定口径(Notice=图标钮 / UserBase=文字链),各自恒定不漂移。 * 「更多▾」只是长尾收纳(动作数 ≥ maxInline+2 时启用),不改变形态、不分档。 * 菜单项纯文字不带图标(动作一多找不到贴切图标,文字更准);transfer 到 body, * 避免 fixed/sticky 操作列裁剪与层叠上下文遮盖(原型已实证)。 */ import { h, resolveComponent } from "vue"; export interface ActionItem { /** 稳定标识:溢出分桶按 key 去重,必填 */ key: string; label: string; /** 点击回调,normalizeActions 阶段闭包绑定行数据 */ on: (row: any) => void; /** iconfont 类名(仅 mode:"icon" 时消费;通道 icons.css 全集见 YschTheme) */ icon?: string; /** 危险动作:文字转红(text-error)、恒沉底、「更多」内分隔线隔离 */ danger?: boolean; /** 返回 false → 该行不渲染此动作(决定「有没有」) */ when?: (row: any) => boolean; /** 直接置灰 */ disabled?: boolean; /** 按行判定置灰(决定「能不能点」),disabled 显式值优先 */ disabledWhen?: (row: any) => boolean; } export interface ActionCellOpts { /** 形态显式声明:icon=图标圆钮 / text=文字链,缺省 text;不按动作数自动切换 */ mode?: "icon" | "text"; /** 超额动作收进「更多」,默认开(防 5+ 动作页面忘传爆宽) */ collapsible?: boolean; /** 直出上限,默认 2;n ≤ maxInline+1 时全直出(3 个动作不进更多) */ maxInline?: number; /** 列宽;缺省 icon = n*40+40 / text = 180 */ width?: number; align?: "center" | "left" | "right"; /** 缺省不固定列(与存量页面一致);需要吸右显式传 "right" */ fixed?: "right" | "left" | false; } /* ============ 轴 1:渲染器(只画节点,零策略) ============ */ /** 图标圆钮:Tooltip > div.operating-btn(沿用通道既有 class,主题层零改动) */ export function actionIcon(h: any, item: ActionItem) { return h( resolveComponent("Tooltip"), { content: item.label, placement: "bottom", transfer: true }, { default: () => [ h("div", { class: `operating-btn iconfont ${item.icon}${item.disabled ? " disabled" : ""}`, onClick: item.disabled ? null : item.on, }), ], }, ); } /** 文字链:danger 输出既有 text-error 类对接主题红色 */ export function actionText(h: any, item: ActionItem) { return h( "div", { class: `operating-link${item.danger ? " text-error" : ""}${item.disabled ? " disabled" : ""}`, onClick: item.disabled ? null : item.on, }, item.label, ); } /** 「更多▾」+ 下拉菜单:iView Dropdown transfer 模式,菜单挂 body */ export function actionMore(h: any, items: ActionItem[]) { return h( resolveComponent("Dropdown"), { class: "op-more", placement: "bottom-end", transfer: true }, { default: () => [ h("div", { class: "operating-link op-more-label" }, [ "更多", h(resolveComponent("Icon"), { type: "ios-arrow-down", class: "op-more-arrow" }), ]), ], list: () => [ h(resolveComponent("DropdownMenu"), {}, { default: () => items.map((it) => h( resolveComponent("DropdownItem"), { key: it.key, divided: !!(it.danger && items.length > 1), disabled: !!it.disabled, class: it.danger ? "op-item-danger" : "", onClick: () => it.on(), }, { default: () => it.label }, ), ), }), ], }, ); } /* ============ 轴 2:归一化(纯数据) ============ */ export function normalizeActions(items: ActionItem[], row: any): ActionItem[] { return items .filter((it) => (it.when ? it.when(row) : true)) .map((it) => ({ ...it, disabled: it.disabled ?? (it.disabledWhen ? it.disabledWhen(row) : false), on: () => it.on(row), })) .sort((a, b) => Number(!!a.danger) - Number(!!b.danger)); // danger 恒沉底(稳定排序) } /* ============ 轴 3:溢出策略(纯函数,可单测、零 DOM) ============ */ export function splitActions(items: ActionItem[], maxInline = 2): { inline: ActionItem[]; overflow: ActionItem[] } { if (items.length <= maxInline + 1) return { inline: items, overflow: [] }; const inline = items.filter((it) => !it.danger).slice(0, maxInline); const inlineKeys = new Set(inline.map((it) => it.key)); return { inline, overflow: items.filter((it) => !inlineKeys.has(it.key)) }; } /* ============ 组装层 ============ */ const RENDERERS: Record any> = { icon: actionIcon, text: actionText }; export function actionCell(h: any, items: ActionItem[], opts: ActionCellOpts = {}) { const mode = opts.mode ?? "text"; const { inline, overflow } = (opts.collapsible ?? true) ? splitActions(items, opts.maxInline ?? 2) : { inline: items, overflow: [] }; const render = RENDERERS[mode] || actionText; const children = inline.map((it) => render(h, it)); if (overflow.length) children.push(actionMore(h, overflow)); return h("div", { class: "op-cell" }, children); // flex 横排容器,见 main.scss } /** 产出可直接塞进 columns 的操作列对象;列宽按「声明数」估算,when 过滤后只宽不窄 */ export function actionColumn(items: ActionItem[], opts: ActionCellOpts = {}) { const mode = opts.mode ?? "text"; return { title: "操作", key: "action", width: opts.width ?? (mode === "icon" ? items.length * 40 + 40 : 180), align: opts.align ?? "center", ...(opts.fixed === undefined ? {} : { fixed: opts.fixed === false ? undefined : opts.fixed }), render: (h: any, p: any) => actionCell(h, normalizeActions(items, p.row), opts), }; }