release: FL 2.0 前端候选(流程线停发/发起页指派预览/测试夹具)
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 可跑)
This commit is contained in:
chenxing
2026-10-27 12:07:40 +08:00
parent cdea9c559b
commit f2841e2740
13 changed files with 2517 additions and 171 deletions
+147 -9
View File
@@ -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>
+3 -6
View File
@@ -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("该流程关联的表单还没有确切版本号,请在「关联表单」里重新选择一次(版本由表单中心给出,不可手填)");