配套 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 可跑)
This commit is contained in:
@@ -9,6 +9,7 @@ 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();
|
||||
@@ -16,6 +17,8 @@ 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,无需再请求)
|
||||
@@ -61,6 +64,29 @@ const renderStatus = (h: any, p: any) => {
|
||||
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];
|
||||
@@ -77,8 +103,8 @@ const ellipsisCell = (key: string, tag = "span") => (h: any, p: any) => h(Toolti
|
||||
content: p.row[key] ?? "", transfer: true, maxWidth: 480,
|
||||
}, () => h(tag, p.row[key] ?? ""));
|
||||
|
||||
/** 行操作集(平铺态、分组展开行同一套;禁用规则按 status) */
|
||||
const rowActions = [
|
||||
/** 版本级操作集(平铺态、分组展开行同一套;禁用规则按 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 },
|
||||
@@ -94,6 +120,15 @@ const rowActions = [
|
||||
{ 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 },
|
||||
@@ -101,6 +136,7 @@ const columns = [
|
||||
{ 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];
|
||||
@@ -110,7 +146,7 @@ const columns = [
|
||||
]);
|
||||
} },
|
||||
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.publishTime)) },
|
||||
actionColumn(rowActions, { mode: "text", width: 180, fixed: "right" }),
|
||||
actionColumn(flatActions, { mode: "text", width: 180, fixed: "right" }),
|
||||
];
|
||||
|
||||
async function load() {
|
||||
@@ -162,6 +198,8 @@ const groups = computed(() => {
|
||||
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],
|
||||
};
|
||||
@@ -172,13 +210,14 @@ const groups = computed(() => {
|
||||
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,操作复用 rowActions)。
|
||||
/** 展开行:该流程线全部版本明细(行对象即 FlwDef,操作只用版本级 versionActions)。
|
||||
* 在途计数接口按 flowGuid 聚合,没有按版本的可靠数据,故明细列不铺在途、只在分组行展示。 */
|
||||
const versionColumns = [
|
||||
{ title: "版本", width: 70, render: (h: any, p: any) => h("b", `v${p.row.version}`) },
|
||||
@@ -190,7 +229,7 @@ const versionColumns = [
|
||||
{ title: "改动说明", width: 260, ellipsis: true, render: (h: any, p: any) => (p.row.changeLog
|
||||
? ellipsisCell("changeLog")(h, p)
|
||||
: h("span", { style: "color:#999" }, "(未填写)")) },
|
||||
actionColumn(rowActions, { mode: "text", width: 210 }),
|
||||
actionColumn(versionActions, { mode: "text", width: 210 }),
|
||||
];
|
||||
|
||||
/** 子表各列声明宽之和:作为内层 Table 的 width 传下去,避免 iView 把展开行剩余空间摊到各列
|
||||
@@ -211,6 +250,7 @@ const groupColumns = [
|
||||
{ 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)
|
||||
@@ -218,15 +258,16 @@ const groupColumns = [
|
||||
{ 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: 190, fixed: "right" },
|
||||
{ mode: "text", width: 260, fixed: "right" },
|
||||
),
|
||||
];
|
||||
|
||||
/** 分组态展开/收起:只由左侧箭头触发(Table 的 on-expand),行内任意位置点击不再切换 */
|
||||
function onGroupExpand(row: any, status: boolean) {
|
||||
function onGroupExpand(row?: any, status?: boolean) {
|
||||
if (row && row._key) {
|
||||
openMap.value[row._key] = !!status;
|
||||
}
|
||||
@@ -307,6 +348,103 @@ function onEnable(row: any) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 流程线级停发:与版本级「停用」正交,确认文案明确讲清影响面 */
|
||||
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);
|
||||
@@ -406,8 +544,8 @@ onMounted(() => {
|
||||
</RadioGroup>
|
||||
<span class="fl" style="margin-left: 12px; color: #808695; font-size: 12px; line-height: 32px">
|
||||
{{ viewMode === "group"
|
||||
? "同 flowGuid 多版折叠为一行,点行展开可见各版创建人 / 创建时间 / 改动说明"
|
||||
: "平铺所有版本行(现状口径)" }}
|
||||
? "同 flowGuid 多版折叠为一行,点行展开可见各版创建人 / 创建时间 / 改动说明;「线状态」按 flowGuid 停发,与版本发布状态相互独立"
|
||||
: "平铺所有版本行(现状口径);「线状态」为整条流程线共享,停发/恢复对每个版本行均可操作" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -469,12 +469,9 @@ async function onSave(): Promise<void> {
|
||||
Message.warning("请填写流程编码");
|
||||
return;
|
||||
}
|
||||
// 归属应用必填(2026-10-12 拍板):新建必须选;升版本继承源线(baseGuid 载入时已带回),
|
||||
// 源线未归属(存量 NULL)时必须在此补选
|
||||
if (!(appKey.value || "").trim()) {
|
||||
Message.warning("请选择归属应用(应用需先在 流程管理 > 应用设置 中开启)");
|
||||
return;
|
||||
}
|
||||
// 归属应用改可选(2026-10-25 T03 定义不绑定唯一应用):appKey 只是"创建方应用"筛选/审计维度,
|
||||
// 不再是新发起资格(资格=flw_usage_grant 独立授权)。显式传入时后端仍校白名单。
|
||||
// 线级停发入口提示见列表页;此处不阻断无归属定义的保存。
|
||||
// 关联表单必须成对:改造前只有关联、没有版本号的存量行,重新选一次才能保存(后端同样会拒)
|
||||
if (formUuid.value && !formVersion.value) {
|
||||
Message.warning("该流程关联的表单还没有确切版本号,请在「关联表单」里重新选择一次(版本由表单中心给出,不可手填)");
|
||||
|
||||
Reference in New Issue
Block a user