diff --git a/src/api/module/flow.ts b/src/api/module/flow.ts index fdc0c20..18decd6 100644 --- a/src/api/module/flow.ts +++ b/src/api/module/flow.ts @@ -144,7 +144,7 @@ class FlowApi { defGet(id: number | string) { return get(`${ROOT}/admin/def/get`, { id }); } - defSave(body: { id?: number; code: string; name: string; contentJson: string; baseGuid?: string; appKey?: string }) { + defSave(body: { id?: number; code: string; name: string; contentJson: string; baseGuid?: string; appKey?: string; changeLog?: string }) { return post(`${ROOT}/admin/def/save`, body); } /** 元数据原地编辑(2026-10-10 议题2):只改 name/appKey 不升版本;已发布亦可 */ @@ -164,6 +164,10 @@ class FlowApi { defVersions(flowGuid: string) { return get(`${ROOT}/admin/def/versions`, { flowGuid }); } + /** 在途办件数(议题二④):flowGuid → 审批中办件数,无在途的流程线也返回 0 键 */ + defInflight(flowGuids: string[]) { + return get(`${ROOT}/admin/def/inflight`, { flowGuids: flowGuids.join(",") }); + } /** 流程定义示意图(管理面:defId 任意版本,草稿可预览) */ adminDefDiagram(params: { id: number | string; mode?: string }) { return get(`${ROOT}/admin/def/diagram`, params); diff --git a/src/views/flow/admin/Defs.vue b/src/views/flow/admin/Defs.vue index 55b2f31..3ff5e7a 100644 --- a/src/views/flow/admin/Defs.vue +++ b/src/views/flow/admin/Defs.vue @@ -4,7 +4,7 @@ // 本页不再直接编辑 JSON 文本(编辑器内置"源码"高级模式兜底);行内「源码」为只读查看+复制。 import { ref, computed, onMounted } from "vue"; import { useRouter } from "vue-router"; -import { Message, Tag } from "view-ui-plus"; +import { Message, Table, Tag } from "view-ui-plus"; import MyConfirm from "@/components/MyConfirm"; import SourceDrawer from "@/views/flow/components/SourceDrawer.vue"; import { flowApi } from "@/api/module/flow"; @@ -29,54 +29,173 @@ const defsH = useElementHeight(defsRegionRef); const apps = ref<{ appKey: string; appName: string }[]>([]); const appMap = ref>({}); const filterApp = ref(""); +// 议题二②:列表视图(group=同 flowGuid 折叠一行,默认;flat=现状全版本平铺)——纯前端切换,defList 已全量返回 +const viewMode = ref<"group" | "flat">("group"); +// 分组展开态(key=flowGuid):Table 数据由 computed 派生,展开态必须存外面才不会丢 +const openMap = ref>({}); +// 议题二④:flowGuid → 在途办件数;拉取失败降级空 map(行内显示「—」,列表不崩) +const inflightMap = ref>({}); + +/** 在途展示:数字=件数(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); +}; + +/** 行操作集(平铺态、分组展开行同一套;禁用规则按 status) */ +const rowActions = [ + { 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) }, +]; const columns = [ { title: "序号", type: "index", width: 70, align: "center" }, { title: "流程", key: "name", minWidth: 180 }, { title: "编码", key: "code", width: 160 }, - { title: "应用", key: "appKey", width: 130, align: "center", render: (h: any, p: any) => { - // 未归属标记(2026-10-12 appKey 必填化):存量 NULL 线在此醒目,随编辑/升版本收敛 - if (!p.row.appKey) { - return h(Tag, { color: "warning" }, () => "未归属"); - } - return h("span", appMap.value[p.row.appKey] || p.row.appKey); - } }, + { title: "应用", key: "appKey", width: 130, align: "center", render: renderApp }, { title: "版本", key: "version", width: 70, align: "center" }, - { title: "状态", width: 90, align: "center", render: (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); + { title: "状态", width: 90, align: "center", render: renderStatus }, + { title: "GUID", key: "flowGuid", width: 240, render: (h: any, p: any) => { + // 平铺态也带在途(议题二④):计数按流程线口径,故挂在 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: "GUID", key: "flowGuid", width: 200 }, - { title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") }, - actionColumn( - [ - { 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) }, - ], - { mode: "text", width: 180, fixed: "right" }, - ), + { title: "发布时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.publishTime)) }, + actionColumn(rowActions, { 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 折叠一行,代表行=已发布中最高版;无已发布取最高版并标「无生效版」 */ +const groups = computed(() => { + const bucket: Record = {}; + const order: string[] = []; + rows.value.forEach((r: any) => { + const g = r.flowGuid || ""; + if (!g) return; + if (!bucket[g]) { + bucket[g] = []; + order.push(g); + } + bucket[g].push(r); + }); + return 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 { + flowGuid: g, id: cur.id, name: cur.name, code: cur.code, appKey: cur.appKey, + version: cur.version, status: cur.status, publishTime: cur.publishTime, + total: vs.length, hasPublished: published.length > 0, + versions: vs, _expanded: !!openMap.value[g], + }; + }); +}); + +/** 展开行:该流程线全部版本明细(行对象即 FlwDef,操作复用 rowActions)。 + * 在途计数接口按 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: 100, render: (h: any, p: any) => h("span", p.row.createBy || "-") }, + { title: "创建时间", width: 140, render: (h: any, p: any) => h("span", fmtTime(p.row.createTime)) }, + { title: "改动说明", minWidth: 230, render: (h: any, p: any) => (p.row.changeLog + ? h("span", p.row.changeLog) + : h("span", { style: "color:#999" }, "(未填写)")) }, + actionColumn(rowActions, { mode: "text", width: 210 }), +]; + +const groupColumns = [ + { type: "expand", width: 40, render: (h: any, p: any) => h(Table, { + columns: versionColumns, data: p.row.versions, size: "small", noDataText: "暂无数据", + }) }, + { title: "序号", type: "index", width: 60, align: "center" }, + { title: "流程", minWidth: 260, render: (h: any, p: any) => { + const n = inflightMap.value[p.row.flowGuid]; + return h("span", {}, [ + h("b", p.row.name), + h(Tag, { color: p.row.hasPublished ? "green" : "default", style: "margin-left:6px" }, + () => (p.row.hasPublished ? `当前生效 v${p.row.version}` : "无生效版")), + h(Tag, { color: n ? "blue" : "default", style: "margin-left:6px" }, () => inflightText(p.row.flowGuid)), + ]); + } }, + { title: "编码", key: "code", width: 160 }, + { 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: "GUID", width: 130, render: (h: any, p: any) => h("span", { title: p.row.flowGuid }, `${String(p.row.flowGuid).slice(0, 8)}…`) }, + { title: "发布时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.publishTime)) }, + actionColumn( + [ + { 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: 140, fixed: "right" }, + ), +]; + +/** 分组态点行展开/收起(展开箭头同一路径:箭头内部态会被下一帧的 _expanded 重新校准) */ +function onGroupRowClick(row: any) { + if (row && row.flowGuid) { + openMap.value[row.flowGuid] = !openMap.value[row.flowGuid]; + } +} + /** 应用清单(筛选下拉):应用设置白名单(flw_app enabled=1),失败降级为空下拉 */ async function loadAppKeys() { try { @@ -119,10 +238,13 @@ async function onPublish(row: any) { } function onOffline(row: any) { + // 议题二④:在途数 >0 才追加提示(0 或接口降级态沿用原文案) + const n = inflightMap.value[row.flowGuid] || 0; MyConfirm({ title: "停用", msg: `确定停用流程「${row.name}」吗?`, - info: "停用后无法发起新办件,进行中的办件不受影响", + info: "停用后无法发起新办件,进行中的办件不受影响" + + (n ? `;当前在途 ${n} 件仍跑本版直至完成` : ""), async onOk() { await flowApi.defOffline(row.id).send(); Message.success("已停用"); @@ -241,19 +363,39 @@ onMounted(() => { style="width: 200px; margin-left: 8px" @on-change="load"> + + + 仅最新版(分组) + 显示全部版本 + + + {{ viewMode === "group" + ? "同 flowGuid 多版折叠为一行,点行展开可见各版创建人 / 创建时间 / 改动说明" + : "平铺所有版本行(现状口径)" }} +
- + - + +

+ 审计信息:创建人 / 创建时间 / 改动说明为本次新增列(存量版本未填说明时显示「(未填写)」) +

diff --git a/src/views/flow/admin/def-editor/index.vue b/src/views/flow/admin/def-editor/index.vue index 3f322a7..cdd0653 100644 --- a/src/views/flow/admin/def-editor/index.vue +++ b/src/views/flow/admin/def-editor/index.vue @@ -100,6 +100,10 @@ const conditionDrawer = ref(false); const settingsDrawer = ref(false); const jsonDrawer = ref(false); const editingUid = ref(""); +// 议题二③:升版本改动说明弹窗(仅「本次保存将创建新版本」时出现,可跳过) +const logModal = ref(false); +const logText = ref(""); +const LOG_CHIPS = ["修复审批人缺失", "调整审批链", "新增抄送对象", "修改条件表达式", "调整流程设置"]; const editingNode = computed(() => { const item = editingUid.value ? findItem(model, editingUid.value) : null; @@ -399,6 +403,28 @@ async function onSave(): Promise { } async function doSave(): Promise { + // 议题二③:本次保存将复制出新版本行时先问改动说明(挂在 doSave 入口,onSave 与同编码软确认两条路径都经过) + if (willCreateVersion()) { + logText.value = ""; + logModal.value = true; + return; + } + await submitSave(); +} + +/** 升版本判据:baseGuid 入口(列表「升版本」)必带 baseGuid;编辑已发布/停用版会走后端复制升版分支 */ +function willCreateVersion(): boolean { + return (!editId.value && !!baseGuid.value) || (!!editId.value && sourceStatus.value !== 0); +} + +/** 常用改动类型点选:追加到说明文本(分号连接),可再手工补充 */ +function appendLogChip(text: string): void { + const cur = logText.value.trim(); + logText.value = cur ? `${cur};${text}` : text; +} + +async function submitSave(changeLog?: string): Promise { + logModal.value = false; const content = JSON.stringify(toContentJson(model)); saving.value = true; try { @@ -410,9 +436,12 @@ async function doSave(): Promise { contentJson: content, baseGuid: baseGuid.value || undefined, appKey: (appKey.value || "").trim() || undefined, + changeLog: changeLog || undefined, }).send(); savedSnapshot.value = currentSnapshot.value; - Message.success("已保存草稿"); + Message.success(willCreateVersion() + ? (changeLog ? "已保存草稿 · 改动说明已留存" : "已保存草稿(未填写改动说明)") + : "已保存草稿"); router.push("/admin/defs"); } finally { saving.value = false; @@ -534,6 +563,25 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload)); + + + +

+ 当前为「升版本 · 基于 v{{ sourceVersion }}({{ DEF_STATUS[sourceStatus] }})」, + 保存将创建新版本草稿(flowGuid 不变,版本号自动 +1)。 +

+
本次改动说明可跳过
+
+ {{ c }} +
+ + +
@@ -660,4 +708,52 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload)); color: #b0b6c1; white-space: nowrap; } +/* 升版本改动说明弹窗(议题二③,规格对齐原型 V2) */ +.cl-note { + background: #fff7e6; + border: 1px solid #ffd77a; + border-radius: 6px; + padding: 9px 12px; + font-size: 12px; + color: #8a5a17; + line-height: 1.7; + margin-bottom: 14px; +} +.cl-label { + font-size: 13px; + font-weight: 600; + color: #17233d; + margin-bottom: 8px; +} +.cl-skip { + font-weight: 400; + color: #808695; + font-size: 12px; + margin-left: 6px; +} +.cl-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} +.cl-chip { + border: 1px solid #e3e6eb; + background: #fff; + border-radius: 12px; + padding: 3px 11px; + font-size: 12px; + color: #515a6e; + cursor: pointer; +} +.cl-chip:hover { + border-color: var(--brand); + color: var(--brand); +} +.cl-hint { + float: left; + font-size: 12px; + color: #b0b6c1; + line-height: 32px; +}