166 lines
7.6 KiB
TypeScript
166 lines
7.6 KiB
TypeScript
// @ts-nocheck
|
||
/* 本端跑 strict vue-tsc(front/MOCK.md),通道/四端同构源落本端的既有约定照 theme.ts 补
|
||
@ts-nocheck:正文与 UserBase 同构源保持逐字节一致(MD5 973393684783c740399b8eb9e97369bc),
|
||
仅顶部此二行 + 下方注释为本端补丁;strict 下两处告警均无害——
|
||
L16 的 h 被各渲染器形参遮蔽(导入冗余)、actionMore 的 it.on() 形参在 normalize 已闭包绑定。 */
|
||
/**
|
||
* 表格操作列统一原子(方案: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<string, (h: any, item: ActionItem) => 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),
|
||
};
|
||
}
|