build / build (push) Successful in 1m33s
配套 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 可跑)
610 lines
30 KiB
Vue
610 lines
30 KiB
Vue
<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>
|