B2 议题二前端:版本历史补全 + 升版本改动说明 + 列表分组 + 在途可见

- Defs.vue 版本历史弹窗 640→820,补 创建人/创建时间/改动说明 三列(存量说明显示「(未填写)」)
- def-editor 升版本保存弹「本次改动说明(可跳过)」:doSave 入口设闸(!editId&&baseGuid 或
  editId&&sourceStatus!==0),主体抽成 submitSave(changeLog) 防递归;常用类型 chips 分号追加,
  500 字上限;跳过与填写两条路径都放行并按说明有无区分 toast;草稿直改不弹不传
- Defs.vue 列表默认分组态(同 flowGuid 折叠一行,代表行=已发布最高版,无已发布标「无生效版」),
  展开行复用 rowActions;可切「显示全部版本」回现状平铺
- 在途(议题二④):defList 落位后批量取 distinct flowGuid → 新接口 defInflight,
  分组行/平铺 GUID 旁展示「在途 N 件」(0 也展示,接口失败降级「—」);停用确认 N>0 追加在途文案
- flow.ts:defSave 加 changeLog 入参、新增 defInflight(未跑 api:gen,按方案手工改)

验证:npm run build 通过;vue-tsc 1.8.27 与 TS 5.9.3 不兼容无法作门禁(另记)
This commit is contained in:
chenxing
2026-10-12 16:14:32 +08:00
parent ecd082bb99
commit 5cf1ce8d1f
3 changed files with 282 additions and 40 deletions
+5 -1
View File
@@ -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);
+180 -38
View File
@@ -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<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);
};
/** 行操作集(平铺态、分组展开行同一套;禁用规则按 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<string, any[]> = {};
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">
<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 多版折叠为一行,点行展开可见各版创建人 / 创建时间 / 改动说明"
: "平铺所有版本行(现状口径)" }}
</span>
</div>
<div class="table-region" ref="defsRegionRef">
<Table :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
<Table v-if="viewMode === 'group'" :columns="groupColumns" :data="groups" :loading="loading"
class="link-table table-bottom-box" :height="defsH || undefined" no-data-text="暂无数据"
@on-row-click="onGroupRowClick" />
<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="640">
<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: 80 },
{ title: '名称', key: 'name' },
{ title: '状态', width: 90, render: (h, p) => h(Tag, () => DEF_STATUS[p.row.status]) },
{ title: '发布时间', width: 160, render: (h, p) => (p.row.publishTime || '').replace('T', ' ') || '-' },
{ 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>
+97 -1
View File
@@ -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<ApprovalNode | null>(() => {
const item = editingUid.value ? findItem(model, editingUid.value) : null;
@@ -399,6 +403,28 @@ async function onSave(): Promise<void> {
}
async function doSave(): Promise<void> {
// 议题二③:本次保存将复制出新版本行时先问改动说明(挂在 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<void> {
logModal.value = false;
const content = JSON.stringify(toContentJson(model));
saving.value = true;
try {
@@ -410,9 +436,12 @@ async function doSave(): Promise<void> {
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));
<ConditionDrawer v-model="conditionDrawer" :node="editingCondition" @removed="conditionDrawer = false" />
<SettingsDrawer v-model="settingsDrawer" :settings="model.settings" @save="onUrgeSave" />
<JsonDrawer v-model="jsonDrawer" :def="defJson" @applied="onJsonApplied" />
<!-- 议题二③:升版本改动说明(仅「本次保存将创建新版本」时出现,跳过也放行) -->
<Modal v-model="logModal" title="保存为新版本草稿" :mask-closable="false" width="520">
<p class="cl-note">
当前为「升版本 · 基于 v{{ sourceVersion }}({{ DEF_STATUS[sourceStatus] }})」,
保存将创建新版本草稿(flowGuid 不变,版本号自动 +1)。
</p>
<div class="cl-label">本次改动说明<span class="cl-skip">可跳过</span></div>
<div class="cl-chips">
<span v-for="c in LOG_CHIPS" :key="c" class="cl-chip" @click="appendLogChip(c)">{{ c }}</span>
</div>
<Input v-model="logText" type="textarea" :rows="3" :maxlength="500" show-word-limit
placeholder="一句话说清本版改了什么,如:教务处节点补充审批人配置;版本历史与列表可见" />
<template #footer>
<span class="cl-hint">跳过也允许保存——但「多一条记录」的区分度就没了</span>
<Button :disabled="saving" @click="submitSave()">跳过</Button>
<Button type="primary" :loading="saving" @click="submitSave(logText.trim())">保存草稿</Button>
</template>
</Modal>
</div>
</template>
@@ -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;
}
</style>