Files
flow-center-front/src/views/flow/admin/Defs.vue
T
chenxing f2841e2740
build / build (push) Successful in 1m33s
release: FL 2.0 前端候选(流程线停发/发起页指派预览/测试夹具)
配套 service 候选 500a9a2:
- flow.ts 增加 admin/def/offlineLine、enableLine(T03 线级停发/恢复)
- Defs.vue 接线级停发开关与状态展示;lineOffline.ts 收敛判定与文案
- AssignPreview.vue + assignEmbed.ts 发起时指派预览(T02 契约)
- def-editor 表单坐标绑定微调
- 新增 vitest 组件测试与 line-offline 契约测试(npm test 可跑)
2026-10-27 12:07:40 +08:00

610 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
// 流程定义管理:列表 + 发布/启用/停用/升版本/版本历史/图示/源码。
// 新建与编辑走独立的全屏可视化编辑器(/admin/defs/editor,钉钉式卡片流,见《流程编辑器方案.md》);
// 本页不再直接编辑 JSON 文本(编辑器内置"源码"高级模式兜底);行内「源码」为只读查看+复制。
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { Message, Table, Tag, Tooltip } from "view-ui-plus";
import MyConfirm from "@/components/MyConfirm";
import SourceDrawer from "@/views/flow/components/SourceDrawer.vue";
import { flowApi } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import { lineActionErrorDetail, runLineAction } from "@/utils/lineOffline";
import { useElementHeight } from "@/utils/hook/useElementHeight";
const router = useRouter();
const DEF_STATUS: Record<number, string> = { 0: "草稿", 1: "已发布", 2: "停用" };
const loading = ref(false);
const rows = ref<any[]>([]);
// 线级启停在途标记(按 flowGuid):确认后的请求期间禁用入口并展示「处理中」
const lineBusyGuid = ref("");
const versionVisible = ref(false);
const versions = ref<any[]>([]);
// 源码抽屉(列表行已带回 contentJson,无需再请求)
const sourceVisible = ref(false);
const sourceText = ref("");
const sourceTitle = ref("");
// 操作按钮区 + 表格,无搜索、无分页:表格高度实测 .table-region(flex:1)
const defsRegionRef = ref<HTMLElement | null>(null);
const defsH = useElementHeight(defsRegionRef);
// 按应用筛选(flw_def.app_key,2026-10-10):下拉数据源来自应用设置白名单(flw_app enabled=1)
const apps = ref<{ appKey: string; appName: string }[]>([]);
const appMap = ref<Record<string, string>>({});
const filterApp = ref("");
// 议题二②:列表视图(group=同 flowGuid 折叠一行,默认;flat=现状全版本平铺)——纯前端切换,defList 已全量返回
const viewMode = ref<"group" | "flat">("group");
// 分组展开态(key=flowGuid):Table 数据由 computed 派生,展开态必须存外面才不会丢
const openMap = ref<Record<string, boolean>>({});
// 议题二④:flowGuid → 在途办件数;拉取失败降级空 map(行内显示「—」,列表不崩)
const inflightMap = ref<Record<string, number>>({});
/** 在途展示:数字=件数(0 也展示);undefined=接口无该线数据 → 「—」 */
function inflightText(guid: string): string {
const n = inflightMap.value[guid];
return n == null ? "在途 —" : `在途 ${n} 件`;
}
/** 创建时间格式化 YYYY-MM-DD HH:mm(后端 LocalDateTime 带 T,空值显示 -) */
function fmtTime(raw: any): string {
return raw ? String(raw).replace("T", " ").slice(0, 16) : "-";
}
/** 应用列(未归属标记 2026-10-12 appKey 必填化):平铺/分组/展开行共用,口径不漂移 */
const renderApp = (h: any, p: any) => {
if (!p.row.appKey) {
return h(Tag, { color: "warning" }, () => "未归属");
}
return h("span", appMap.value[p.row.appKey] || p.row.appKey);
};
/** 状态列(草稿橙/已发布绿/停用灰):三态共用 */
const renderStatus = (h: any, p: any) => {
const c = p.row.status === 1 ? "green" : p.row.status === 0 ? "orange" : "gray";
return h(Tag, { color: c }, () => DEF_STATUS[p.row.status] || p.row.status);
};
/** 流程线级停发状态(T03):与版本 status 正交,必须独立展示,不能混读成“发布/停用” */
const renderLineState = (h: any, p: any) => {
if (!p.row.flowGuid) {
return h("span", { style: "color:#999" }, "—");
}
const offline = Number(p.row.lineOffline) === 1;
const tag = h(Tooltip, {
content: offline
? "流程线已停发:新发起/选人预览均被拒绝,在途办件按原版本跑完;与版本发布状态相互独立"
: "流程线正常:新发起可用;与版本发布状态相互独立",
transfer: true,
maxWidth: 360,
}, () => h(Tag, { color: offline ? "red" : "default" }, () => (offline ? "已停发" : "正常")));
return isLineBusy(p.row)
? h("span", {}, [tag, h(Tag, { color: "blue", style: "margin-left:6px" }, () => "处理中…")])
: tag;
};
function isLineBusy(row: any): boolean {
const guid = String(row?.flowGuid || "").trim();
return !!guid && lineBusyGuid.value === guid;
}
/** 生效/在途 徽标(2026-10-14 从「流程」列拆出独立成列,口径不变:计数仍按流程线) */
const renderRunState = (h: any, p: any) => {
const n = inflightMap.value[p.row.flowGuid];
return h("span", {}, [
h(Tag, { color: p.row.hasPublished ? "green" : "default" },
() => (p.row.hasPublished ? `当前生效 v${p.row.version}` : "无生效版")),
h(Tag, { color: n ? "blue" : "default", style: "margin-left:12px" }, () => inflightText(p.row.flowGuid)),
]);
};
/** 固定宽文本列 + 省略号 + Tooltip:列上写了 render 时 iView 内置 column.tooltip 不生效(cell 只在
* renderType==='normal' 分支渲染 tooltip),故手挂 Tooltip;ellipsis 仍走 column 配置。 */
const ellipsisCell = (key: string, tag = "span") => (h: any, p: any) => h(Tooltip, {
content: p.row[key] ?? "", transfer: true, maxWidth: 480,
}, () => h(tag, p.row[key] ?? ""));
/** 版本级操作集(平铺态、分组展开行同一套;禁用规则按 status,勿与线级启停混用) */
const versionActions = [
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => onEdit(r), disabledWhen: (r: any) => r.status !== 0 },
{ key: "publish", label: "发布", icon: "icon-fasong", on: (r: any) => onPublish(r), disabledWhen: (r: any) => r.status !== 0 },
{ key: "newVersion", label: "升版本", icon: "icon-tianjia", on: (r: any) => onNewVersion(r), disabledWhen: (r: any) => r.status !== 1 },
// 停用:破坏性动作(已发布→停用),danger 沉底并进「更多」
{ key: "offline", label: "停用", icon: "icon-stop", on: (r: any) => onOffline(r), disabledWhen: (r: any) => r.status !== 1, danger: true },
// 与「停用」对称的专用启用通道(后端 /def/enable:仅停用版→已发布,重跑发布校验;
// 草稿不走这里,草稿用「发布」)
{ key: "enable", label: "启用", icon: "icon-play", on: (r: any) => onEnable(r), disabledWhen: (r: any) => r.status !== 2 },
// 元数据原地编辑(2026-10-10 议题2):改名称/归属应用不升版本(含已发布/停用)
{ key: "editMeta", label: "编辑信息", icon: "icon-bianji", on: (r: any) => onEditMeta(r) },
{ key: "diagram", label: "图示", icon: "icon-tupian", on: (r: any) => onDiagram(r) },
{ key: "source", label: "源码", icon: "icon-liebiao", on: (r: any) => onSource(r) },
{ key: "versions", label: "版本历史", icon: "icon-shijian", on: (r: any) => onVersions(r) },
];
/** 流程线级操作(T03):按 flowGuid 批量停发/恢复,只在有流程线标识时出现 */
const lineActions = [
{ key: "lineOffline", label: "停发流程线", icon: "icon-stop", on: (r: any) => onLineOffline(r), when: (r: any) => !!r.flowGuid && Number(r.lineOffline) !== 1, disabledWhen: (r: any) => isLineBusy(r), danger: true },
{ key: "lineEnable", label: "恢复流程线", icon: "icon-play", on: (r: any) => onLineEnable(r), when: (r: any) => !!r.flowGuid && Number(r.lineOffline) === 1, disabledWhen: (r: any) => isLineBusy(r) },
];
/** 平铺态(每行=一个版本)同时提供线级入口;展开的版本子表仍只用版本级操作,避免误操作整线 */
const flatActions = [...versionActions, ...lineActions];
const columns = [
{ title: "序号", type: "index", width: 70, align: "center" },
{ title: "流程", key: "name", minWidth: 180 },
{ title: "编码", key: "code", width: 200 },
{ title: "应用", key: "appKey", width: 130, align: "center", render: renderApp },
{ title: "版本", key: "version", width: 70, align: "center" },
{ title: "状态", width: 90, align: "center", render: renderStatus },
{ title: "线状态", width: 96, align: "center", render: renderLineState },
{ title: "GUID", key: "flowGuid", width: 320, render: (h: any, p: any) => {
// 平铺态也带在途(议题二④):计数按流程线口径,故挂在 GUID 旁;列宽同分组态,保证 32 位 guid 单行全显
const n = inflightMap.value[p.row.flowGuid];
return h("span", {}, [
p.row.flowGuid,
h(Tag, { color: n ? "blue" : "default", style: "margin-left:6px" }, () => inflightText(p.row.flowGuid)),
]);
} },
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.publishTime)) },
actionColumn(flatActions, { mode: "text", width: 180, fixed: "right" }),
];
async function load() {
loading.value = true;
try {
rows.value = (await flowApi.defList({ appKey: filterApp.value || undefined }).send()) || [];
loadInflight();
} finally {
loading.value = false;
}
}
/** 在途统计(议题二④):列表到位后取 distinct flowGuids 批量查,失败静默降级(行内显示「—」) */
async function loadInflight() {
const guids = Array.from(new Set(rows.value.map((r: any) => r.flowGuid).filter(Boolean))).slice(0, 100);
if (!guids.length) {
inflightMap.value = {};
return;
}
try {
inflightMap.value = (await flowApi.defInflight(guids).send()) || {};
} catch {
inflightMap.value = {};
}
}
/** 分组行(议题二②):同 flowGuid 折叠一行,代表行=已发布中最高版;无已发布取最高版并标「无生效版」。
* 空 flowGuid(历史脏数据)无流程线可折叠,各自单列为一行,避免被整体丢弃。 */
const groups = computed(() => {
const bucket: Record<string, any[]> = {};
const order: string[] = [];
const noGuid: any[] = [];
rows.value.forEach((r: any) => {
const g = r.flowGuid || "";
if (!g) {
noGuid.push(r);
return;
}
if (!bucket[g]) {
bucket[g] = [];
order.push(g);
}
bucket[g].push(r);
});
const out = order.map((g) => {
const vs = bucket[g].slice().sort((a: any, b: any) => b.version - a.version);
const published = vs.filter((v: any) => v.status === 1);
const cur = published[0] || vs[0];
return {
_key: g, flowGuid: g, id: cur.id, name: cur.name, code: cur.code, appKey: cur.appKey,
version: cur.version, status: cur.status, publishTime: cur.publishTime,
// 线状态全版本同值;任一行标记停发即按停发展示(防历史脏行让管理员误判可用)
lineOffline: vs.some((v: any) => Number(v.lineOffline) === 1) ? 1 : 0,
total: vs.length, hasPublished: published.length > 0,
versions: vs, _expanded: !!openMap.value[g],
};
});
// 空 flowGuid 兜底:openMap 用合成 key,避免与其他行共用 "" 键互相牵连
noGuid.forEach((r: any) => {
const k = `__noguid__${r.id}`;
out.push({
_key: k, flowGuid: "", id: r.id, name: r.name, code: r.code, appKey: r.appKey,
version: r.version, status: r.status, publishTime: r.publishTime,
lineOffline: 0,
total: 1, hasPublished: r.status === 1, versions: [r], _expanded: !!openMap.value[k],
});
});
return out;
});
/** 展开行:该流程线全部版本明细(行对象即 FlwDef,操作只用版本级 versionActions)。
* 在途计数接口按 flowGuid 聚合,没有按版本的可靠数据,故明细列不铺在途、只在分组行展示。 */
const versionColumns = [
{ title: "版本", width: 70, render: (h: any, p: any) => h("b", `v${p.row.version}`) },
{ title: "状态", width: 90, align: "center", render: renderStatus },
{ title: "创建人", width: 110, render: (h: any, p: any) => h("span", p.row.createBy || "-") },
// 创建时间「2026-10-13 23:44」实测行盒 2(折行)→ 取 160,与主表「发布时间」同宽(同文案已证单行)
{ title: "创建时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.createTime)) },
// 改动说明:固定窄列 + 省略号 + Tooltip(2026-10-14 用户要求压短,完整内容进 Tooltip)
{ title: "改动说明", width: 260, ellipsis: true, render: (h: any, p: any) => (p.row.changeLog
? ellipsisCell("changeLog")(h, p)
: h("span", { style: "color:#999" }, "(未填写)")) },
actionColumn(versionActions, { mode: "text", width: 210 }),
];
/** 子表各列声明宽之和:作为内层 Table 的 width 传下去,避免 iView 把展开行剩余空间摊到各列
* (否则「改动说明」会被撑回数百像素,等于没压短),同时让子表右端不再压到固定条带下方。 */
const VERSIONS_TABLE_WIDTH = 70 + 90 + 110 + 160 + 260 + 210;
const groupColumns = [
{ type: "expand", width: 40, render: (h: any, p: any) => h(Table, {
columns: versionColumns, data: p.row.versions, size: "small", noDataText: "暂无数据",
width: VERSIONS_TABLE_WIDTH,
}) },
{ title: "序号", type: "index", width: 60, align: "center" },
// 标题列固定宽 + 超长省略 + Tooltip;生效/在途 徽标已拆到独立列(2026-10-14)
{ title: "流程", width: 260, ellipsis: true, render: ellipsisCell("name", "b") },
{ title: "生效/在途", width: 240, align: "center", render: renderRunState },
// 编码实测单行 155px + 内边距 24 → 160 会折行,故 190
{ title: "编码", key: "code", width: 200 },
{ title: "应用", key: "appKey", width: 130, align: "center", render: renderApp },
{ title: "版本", width: 120, render: (h: any, p: any) => h("span", `v${p.row.version} · 共 ${p.row.total} 版`) },
{ title: "状态", width: 90, align: "center", render: renderStatus },
{ title: "线状态", width: 96, align: "center", render: renderLineState },
// GUID 列宽按 32 位 uuid 实测单行 259px + 内边距 24 定尺寸(留余量,防不同字形更宽);不再截 8 位
{ title: "GUID", width: 320, render: (h: any, p: any) => (p.row.flowGuid
? h("span", { title: p.row.flowGuid }, p.row.flowGuid)
: h("span", { style: "color:#999" }, "—")) },
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.publishTime)) },
actionColumn(
[
...lineActions,
{ key: "versions", label: "版本历史", icon: "icon-shijian", on: (r: any) => onVersions(r) },
{ key: "editMeta", label: "编辑信息", icon: "icon-bianji", on: (r: any) => onEditMeta(r) },
],
{ mode: "text", width: 260, fixed: "right" },
),
];
/** 分组态展开/收起:只由左侧箭头触发(Table 的 on-expand),行内任意位置点击不再切换 */
function onGroupExpand(row?: any, status?: boolean) {
if (row && row._key) {
openMap.value[row._key] = !!status;
}
}
/** 应用清单(筛选下拉):应用设置白名单(flw_app enabled=1),失败降级为空下拉 */
async function loadAppKeys() {
try {
const rows: any[] = (await flowApi.appList().send()) || [];
const enabled = rows.filter((r) => r.enabled === 1);
apps.value = enabled.map((r) => ({ appKey: String(r.appKey), appName: r.appName || String(r.appKey) }));
const map: Record<string, string> = {};
rows.forEach((r) => { if (r.appKey != null) map[String(r.appKey)] = r.appName || String(r.appKey); });
appMap.value = map;
} catch {
apps.value = [];
appMap.value = {};
}
}
/** 新建:编辑器空模板(默认含一个待配置审批节点) */
function onNew() {
router.push({ path: "/admin/defs/editor" });
}
/** 编辑草稿:编辑器载入该草稿内容,保存直接改本行 */
function onEdit(row: any) {
router.push({ path: "/admin/defs/editor", query: { id: String(row.id) } });
}
/** 升版本:以已发布内容为底稿,保存时走 baseGuid 建新草稿版本(version 自动 +1,flowGuid 不变) */
function onNewVersion(row: any) {
Message.info("已载入当前版本内容,保存即创建新草稿版本");
router.push({ path: "/admin/defs/editor", query: { id: String(row.id), baseGuid: row.flowGuid } });
}
async function onPublish(row: any) {
try {
await flowApi.defPublish(row.id).send();
Message.success("发布成功");
load();
} catch (e: any) {
Message.error(e?.message || "发布校验失败");
}
}
function onOffline(row: any) {
// 议题二④:在途数 >0 才追加提示(0 或接口降级态沿用原文案)
const n = inflightMap.value[row.flowGuid] || 0;
MyConfirm({
title: "停用",
msg: `确定停用流程「${row.name}」吗?`,
info: "停用后无法发起新办件,进行中的办件不受影响"
+ (n ? `;当前在途 ${n} 件仍跑本版直至完成` : ""),
async onOk() {
await flowApi.defOffline(row.id).send();
Message.success("已停用");
load();
},
});
}
/** 启用停用版:专用 enable 通道(与停用对称;重跑静态校验后置回已发布) */
function onEnable(row: any) {
MyConfirm({
title: "启用",
msg: `确定启用流程「${row.name}」吗?`,
info: "启用将重新执行发布校验",
async onOk() {
try {
await flowApi.defEnable(row.id).send();
Message.success("已启用");
load();
} catch (e: any) {
Message.error(e?.message || "启用失败");
}
},
});
}
/** 流程线级停发:与版本级「停用」正交,确认文案明确讲清影响面 */
function onLineOffline(row: any) {
if (!row.flowGuid) {
Message.warning("该行缺少流程线标识,无法执行线级停发");
return;
}
if (isLineBusy(row)) {
Message.info("该流程线操作正在处理中,请稍候");
return;
}
if (Number(row.lineOffline) === 1) {
Message.info("该流程线已处于停发状态");
return;
}
const n = inflightMap.value[row.flowGuid] || 0;
MyConfirm({
title: "停发流程线",
msg: `确定停发流程线「${row.name}」吗?`,
info: "停发后整条流程线不可发起新办件/选人预览,各版本发布状态不变,在途办件不受影响"
+ (n ? `;当前在途 ${n} 件仍按原版本跑完` : ""),
async onOk() {
await applyLineAction(row, "offline");
},
});
}
/** 流程线级恢复:只恢复线可用性,不改任何版本状态或发布内容 */
function onLineEnable(row: any) {
if (!row.flowGuid) {
Message.warning("该行缺少流程线标识,无法执行线级恢复");
return;
}
if (isLineBusy(row)) {
Message.info("该流程线操作正在处理中,请稍候");
return;
}
if (Number(row.lineOffline) !== 1) {
Message.info("该流程线当前未被停发");
return;
}
MyConfirm({
title: "恢复流程线",
msg: `确定恢复流程线「${row.name}」吗?`,
info: "仅恢复流程线可用性,不改动各版本状态或发布内容;恢复后新发起使用当前最高已发布版本",
async onOk() {
await applyLineAction(row, "enable");
},
});
}
/** 线级启停统一编排:状态守卫 → 调后端 → 按受影响行数反馈 → 刷新列表(失败不误报成功) */
async function applyLineAction(row: any, action: "offline" | "enable") {
const label = action === "offline" ? "停发" : "恢复";
if (isLineBusy(row)) {
Message.info("该流程线操作正在处理中,请稍候");
return;
}
lineBusyGuid.value = String(row.flowGuid || "").trim();
try {
const out = await runLineAction({
flowGuid: row.flowGuid,
lineOffline: Number(row.lineOffline) || 0,
action,
request: async (guid) => Number(await (action === "offline"
? flowApi.defOfflineLine(guid).send()
: flowApi.defEnableLine(guid).send())) || 0,
reload: () => load(),
});
if (out.kind === "skipped") {
if (out.reason === "no-guid") {
Message.warning(`该行缺少流程线标识,无法执行线级${label}`);
} else if (out.reason === "already-offline") {
Message.info("该流程线已处于停发状态");
} else {
Message.info("该流程线当前未被停发");
}
return;
}
if (out.kind === "empty") {
Message.warning("未更新任何版本,流程线状态可能已变化,请刷新后确认");
} else {
Message.success(action === "offline"
? `流程线已停发(${out.affected} 个版本同步标记,在途办件不受影响)`
: `流程线已恢复(${out.affected} 个版本同步恢复,版本状态与发布内容未改动)`);
}
if (out.reloadFailed) {
Message.warning("操作已提交,但列表刷新失败,请手动刷新后确认最新状态");
}
} catch (e: any) {
Message.error(`${label}流程线失败:${lineActionErrorDetail(e?.message || "")}`);
} finally {
if (lineBusyGuid.value === String(row.flowGuid || "").trim()) {
lineBusyGuid.value = "";
}
}
}
/** 源码:列表行已带回 contentJson(defList 返回全量实体),直接开抽屉,无需再拉接口 */
function onSource(row: any) {
sourceText.value = prettyJson(row.contentJson);
sourceTitle.value = `${row.name}(${row.code} v${row.version})`;
sourceVisible.value = true;
}
/** contentJson 字符串 → 缩进 JSON(解析失败则原样展示,不让展示把页面弄崩) */
function prettyJson(raw: any): string {
if (!raw) return "";
try {
return JSON.stringify(typeof raw === "string" ? JSON.parse(raw) : raw, null, 2);
} catch {
return String(raw);
}
}
async function onVersions(row: any) {
versions.value = (await flowApi.defVersions(row.flowGuid).send()) || [];
versionVisible.value = true;
}
function onDiagram(row: any) {
// 不传 mode:后端缺省=人员视图(两档由图示页自身的 科室/人员 切换控制)
router.push({ path: "/diagram", query: { defId: row.id } });
}
// ---- 编辑信息(元数据原地编辑,2026-10-10 议题2:改名称/归属应用不升版本) ----
const metaVisible = ref(false);
const metaSaving = ref(false);
const metaForm = ref<{ id: number; name: string; appKey: string; version: number; status: number }>({
id: 0, name: "", appKey: "", version: 0, status: 0,
});
function onEditMeta(row: any) {
metaForm.value = { id: row.id, name: row.name || "", appKey: row.appKey || "", version: row.version, status: row.status };
metaVisible.value = true;
}
/** 当前 appKey 不在启用集合时兜底插入(保存会被后端拦,提示用户先开启或清空) */
const metaAppOptions = computed(() => {
const list = apps.value.map((a) => ({ value: a.appKey, label: a.appName }));
const cur = metaForm.value.appKey;
if (cur && !list.some((o) => o.value === cur)) {
list.unshift({ value: cur, label: `${cur}(未开启,保存将被拦)` });
}
return list;
});
async function onMetaSave() {
if (!metaForm.value.name.trim()) {
Message.warning("流程名称不能为空");
return;
}
// 归属应用必填(2026-10-12 拍板):存量未归属行在此原地补填
if (!metaForm.value.appKey) {
Message.warning("请选择归属应用(应用需先在 流程管理 > 应用设置 中开启)");
return;
}
metaSaving.value = true;
try {
await flowApi.defUpdateMeta({
id: metaForm.value.id,
name: metaForm.value.name.trim(),
appKey: metaForm.value.appKey,
}).send();
Message.success("已更新(版本不变)");
metaVisible.value = false;
load();
} catch (e: any) {
Message.error(e?.message || "保存失败");
} finally {
metaSaving.value = false;
}
}
onMounted(() => {
load();
loadAppKeys();
});
</script>
<template>
<div class="page-wrapper yh-fill">
<div class="main-container">
<!-- 操作按钮区:主操作靠左(bsch应用页规范 §1) -->
<div class="fix mb-12">
<Button class="fl" type="primary" size="large" @click="onNew">新建流程</Button>
<Select v-model="filterApp" clearable filterable placeholder="按应用筛选" class="fl"
style="width: 200px; margin-left: 8px" @on-change="load">
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
</Select>
<!-- 议题二②:分组(同 flowGuid 折叠一行,默认)/ 平铺(现状全版本行)纯前端切换 -->
<RadioGroup v-model="viewMode" type="button" button-style="solid" class="fl" style="margin-left: 8px">
<Radio label="group">仅最新版(分组)</Radio>
<Radio label="flat">显示全部版本</Radio>
</RadioGroup>
<span class="fl" style="margin-left: 12px; color: #808695; font-size: 12px; line-height: 32px">
{{ viewMode === "group"
? "同 flowGuid 多版折叠为一行,点行展开可见各版创建人 / 创建时间 / 改动说明;「线状态」按 flowGuid 停发,与版本发布状态相互独立"
: "平铺所有版本行(现状口径);「线状态」为整条流程线共享,停发/恢复对每个版本行均可操作" }}
</span>
</div>
<div class="table-region" ref="defsRegionRef">
<Table v-if="viewMode === 'group'" :columns="groupColumns" :data="groups" :loading="loading"
class="link-table table-bottom-box" :height="defsH || undefined" no-data-text="暂无数据"
@on-expand="onGroupExpand" />
<Table v-else :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
:height="defsH || undefined" no-data-text="暂无数据" />
</div>
<Modal v-model="versionVisible" title="版本历史" width="820">
<p style="color: #808695; font-size: 12px; line-height: 1.6; margin-bottom: 8px">
审计信息:创建人 / 创建时间 / 改动说明为本次新增列(存量版本未填说明时显示「(未填写)」)
</p>
<Table :columns="[
{ title: '版本', key: 'version', width: 70, render: (h, p) => h('b', 'v' + p.row.version) },
{ title: '名称', key: 'name', width: 150 },
{ title: '状态', width: 80, render: (h, p) => h(Tag, () => DEF_STATUS[p.row.status]) },
{ title: '发布时间', width: 132, render: (h, p) => (p.row.publishTime || '').replace('T', ' ').slice(0, 16) || '-' },
{ title: '创建人', width: 90, render: (h, p) => p.row.createBy || '-' },
{ title: '创建时间', width: 132, render: (h, p) => (p.row.createTime || '').replace('T', ' ').slice(0, 16) || '-' },
{ title: '改动说明', minWidth: 230, render: (h, p) => p.row.changeLog
? h('span', p.row.changeLog) : h('span', { style: 'color:#999' }, '(未填写)') },
]" :data="versions" size="small" no-data-text="暂无数据" />
</Modal>
<SourceDrawer v-model="sourceVisible" :text="sourceText" :subtitle="sourceTitle" />
<Modal v-model="metaVisible" title="编辑流程信息(不升版本)" :mask-closable="false" width="460">
<p style="color: #808695; font-size: 12px; line-height: 1.6; margin-bottom: 10px">
只修改名称与归属应用,版本与流程内容保持不变(v{{ metaForm.version }},{{
DEF_STATUS[metaForm.status] || metaForm.status }})。结构改动请走「{{
metaForm.status === 0 ? "编辑" : "升版本" }}」进编辑器。
</p>
<Form :label-width="90">
<FormItem label="流程名称">
<Input v-model="metaForm.name" placeholder="流程名称" :maxlength="30" />
</FormItem>
<FormItem label="归属应用">
<Select v-model="metaForm.appKey" filterable placeholder="归属应用(必选)">
<Option v-for="o in metaAppOptions" :key="o.value" :value="o.value">{{ o.label }}</Option>
</Select>
</FormItem>
</Form>
<template #footer>
<Button @click="metaVisible = false">取消</Button>
<Button type="primary" :loading="metaSaving" @click="onMetaSave">保存</Button>
</template>
</Modal>
</div>
</div>
</template>
<style scoped>
/* 无分页脚:通道只对含 .page-panel 的卡片自动转纵向分栏,本页自声明保证 table-region 撑满 */
.main-container {
display: flex;
flex-direction: column;
}
</style>