应用页规范(bsch)重构: - 6 个列表页迁规范骨架: page-wrapper/tabs-wrapper + search-wrap 搜索区(关键字 200px 即输即搜, 去查询按钮) + link-table table-bottom-box 定高表格 + page-panel 分页栏 - 分页列表统一接 TablePageAlovaNew(页长 [10,20,30,40]/customPageCount 记忆/增删智能翻页); dataApi 须返回 Method, 调 .send() 会导致表格静默空数据 - 操作列统一图标式 operatingHandle(Tooltip+fixed right 居中); 各列表补序号列; 危险操作统一 MyConfirm 二次确认 - 弹窗表单必填红星 + 统一提示「请填写红色星号内容!」 - TablePageAlovaNew 补页长记忆与重载后清空勾选; 新增 src/types OperatorEnum 工作台与流程轨迹优化: - 已办列表去重: 每办件一行, doneCount>1 显示「共办理 N 次」标签 - 新增 TraceTimeline 轨迹时间流(时间升序/kind/轮次, 按人员/科室切换写回 URL), 接入详情/办理页 - 新增流程源码只读抽屉 SourceDrawer, 接入定义列表与图示页 - 办理人映射/流程定义新增「启用」回路(/admin/map/enable, /admin/def/enable) - 图示视图口径统一 dept/user 两档, 缺省人员视图; 状态列 Tag 色映射统一导出
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// 回调管理(概览 §9.2):注册列表/启停/secret 重置 + 推送日志 + 挂起(持续重试可视/标记死信)+ 死信重推
|
||||
// 回调管理(概览 §9.2):注册列表/启停/secret 重置 + 推送日志 + 挂起(持续重试可视/标记死信)+ 死信重推。
|
||||
// 规范骨架(bsch应用页规范 §1):tabs-wrapper + Tabs type=card,Tab 切换只 v-show 内容区;「重推选中」落在分页栏左侧批量操作位
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { Message } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import Utils from "@/utils";
|
||||
|
||||
@@ -11,105 +15,125 @@ const EVENT_OPTIONS = ["flow.submitted", "flow.task.created", "flow.task.handled
|
||||
const tab = ref("regs");
|
||||
const loading = ref(false);
|
||||
|
||||
// tabs 页高度:在常规骨架上再扣 Tab 条;日志页含搜索区+分页,挂起页含提示条
|
||||
const regsTableHeight = ref(Utils.useTableHeight({ isTabs: true }));
|
||||
const logTableHeight = ref(Utils.useTableHeight({ isTabs: true, isSearch: true, isPage: true }));
|
||||
const hungTableHeight = ref(Utils.useTableHeight({ isTabs: true, otherH: 46 }));
|
||||
|
||||
// ---- 注册列表 ----
|
||||
const regs = ref<any[]>([]);
|
||||
const regColumns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "应用 appKey", key: "appKey", width: 110 },
|
||||
{ title: "流程 GUID", key: "flowGuid", width: 210 },
|
||||
{ title: "回调地址", key: "callbackUrl", minWidth: 240 },
|
||||
{ title: "订阅事件", key: "eventTypes", width: 200, render: (h: any, p: any) => h("span", p.row.eventTypes || "全部") },
|
||||
{ title: "状态", width: 80, render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
|
||||
{ title: "操作", width: 220, render: (h: any, p: any) => {
|
||||
return [
|
||||
Utils.operatingTextHandle(h, p.row.status === 0 ? "停用" : "启用", "", () => onRegStatus(p.row, p.row.status === 0)),
|
||||
Utils.operatingTextHandle(h, "重置secret", "text-error", () => onResetSecret(p.row)),
|
||||
];
|
||||
} },
|
||||
{ title: "状态", width: 80, align: "center", render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
|
||||
{ title: "操作", width: 140, align: "center", fixed: "right", render: (h: any, p: any) => [
|
||||
Utils.operatingHandle(h, p.row.status === 0 ? "停用" : "启用", p.row.status === 0 ? "icon-stop" : "icon-shiyong", () => onRegStatus(p.row, p.row.status === 0)),
|
||||
Utils.operatingHandle(h, "重置secret", "icon-shezhi", () => onResetSecret(p.row)),
|
||||
] },
|
||||
];
|
||||
|
||||
// ---- 推送日志 ----
|
||||
const logs = ref<any[]>([]);
|
||||
const logPage = ref(1);
|
||||
const logTotal = ref(0);
|
||||
const logFilter = ref<any>({ status: null, event: "", instanceId: null });
|
||||
const selectedLogIds = ref<number[]>([]);
|
||||
const logTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
const logFilter = ref<any>({ status: null, event: "", instanceId: "" });
|
||||
const logColumns = [
|
||||
{ type: "selection", width: 50 },
|
||||
{ type: "selection", width: 50, align: "center" },
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "事件", key: "event", width: 150 },
|
||||
{ title: "办件", key: "instanceId", width: 80 },
|
||||
{ title: "biz", width: 160, render: (h: any, p: any) => h("span", `${p.row.bizType}/${p.row.bizId}`) },
|
||||
{ title: "状态", width: 110, render: (h: any, p: any) => {
|
||||
{ title: "状态", width: 110, align: "center", render: (h: any, p: any) => {
|
||||
const c = p.row.status === 1 ? "green" : p.row.status === 2 ? "red" : "orange";
|
||||
return h("Tag", { color: c }, () => LOG_STATUS[p.row.status]);
|
||||
} },
|
||||
{ title: "重试", key: "retryCount", width: 60 },
|
||||
{ title: "重试", key: "retryCount", width: 60, align: "center" },
|
||||
{ title: "末次结果", key: "lastError", minWidth: 200, render: (h: any, p: any) => h("span", p.row.lastError || p.row.lastHttpStatus || "-") },
|
||||
{ title: "下次重推", width: 160, render: (h: any, p: any) => h("span", (p.row.nextRetryTime || "").replace("T", " ") || "-") },
|
||||
];
|
||||
|
||||
// ---- 挂起列表(持续重试中:待推且重试达阈值;人工判定无意义 → 标记死信止血) ----
|
||||
const hungs = ref<any[]>([]);
|
||||
const hungPage = ref(1);
|
||||
const hungTotal = ref(0);
|
||||
const hungLoading = ref(false);
|
||||
const hungColumns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "事件", key: "event", width: 150 },
|
||||
{ title: "办件", key: "instanceId", width: 80 },
|
||||
{ title: "biz", width: 160, render: (h: any, p: any) => h("span", `${p.row.bizType}/${p.row.bizId}`) },
|
||||
{ title: "重试", key: "retryCount", width: 70, render: (h: any, p: any) => h("Tag", { color: "orange" }, () => `${p.row.retryCount} 次`) },
|
||||
{ title: "重试", key: "retryCount", width: 70, align: "center", render: (h: any, p: any) => h("Tag", { color: "orange" }, () => `${p.row.retryCount} 次`) },
|
||||
{ title: "末次结果", key: "lastError", minWidth: 220, render: (h: any, p: any) => h("span", p.row.lastError || p.row.lastHttpStatus || "-") },
|
||||
{ title: "下次重推", width: 160, render: (h: any, p: any) => h("span", (p.row.nextRetryTime || "").replace("T", " ") || "-") },
|
||||
{ title: "操作", width: 120, render: (h: any, p: any) =>
|
||||
Utils.operatingTextHandle(h, "标记死信", "text-error", () => onMarkDead(p.row)) },
|
||||
{ title: "操作", width: 120, align: "center", fixed: "right", render: (h: any, p: any) =>
|
||||
Utils.operatingHandle(h, "标记死信", "icon-ashbin", () => onMarkDead(p.row)) },
|
||||
];
|
||||
|
||||
async function loadRegs() {
|
||||
regs.value = (await flowApi.regList().send()) || [];
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = { page: logPage.value, size: 10 };
|
||||
if (logFilter.value.status !== null && logFilter.value.status !== undefined) params.status = logFilter.value.status;
|
||||
if (logFilter.value.event) params.event = logFilter.value.event;
|
||||
if (logFilter.value.instanceId) params.instanceId = logFilter.value.instanceId;
|
||||
const data: any = await flowApi.callbackLogs(params).send();
|
||||
logs.value = data?.content || [];
|
||||
logTotal.value = data?.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
|
||||
const logApi = (p: { pageIndex: number; pageSize: number }) => {
|
||||
const params: any = { page: p.pageIndex, size: p.pageSize };
|
||||
if (logFilter.value.status !== null && logFilter.value.status !== undefined) params.status = logFilter.value.status;
|
||||
if (logFilter.value.event) params.event = logFilter.value.event;
|
||||
if (logFilter.value.instanceId) params.instanceId = logFilter.value.instanceId;
|
||||
return flowApi.callbackLogs(params);
|
||||
};
|
||||
|
||||
function onLogSearch() {
|
||||
logTableRef.value?.refreshData();
|
||||
}
|
||||
|
||||
async function loadHungs() {
|
||||
hungLoading.value = true;
|
||||
try {
|
||||
const data: any = await flowApi.callbackHung({ page: hungPage.value, size: 10 }).send();
|
||||
const data: any = await flowApi.callbackHung({ page: 1, size: 100 }).send();
|
||||
hungs.value = data?.content || [];
|
||||
hungTotal.value = data?.total || 0;
|
||||
} finally {
|
||||
hungLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onRegStatus(row: any, disable: boolean) {
|
||||
await flowApi.regStatus(row.id, !disable).send();
|
||||
Message.success("已更新");
|
||||
loadRegs();
|
||||
function onRegStatus(row: any, disable: boolean) {
|
||||
const doStatus = async () => {
|
||||
await flowApi.regStatus(row.id, !disable).send();
|
||||
Message.success("已更新");
|
||||
loadRegs();
|
||||
};
|
||||
if (!disable) {
|
||||
doStatus();
|
||||
return;
|
||||
}
|
||||
MyConfirm({
|
||||
title: "停用",
|
||||
msg: `确定停用应用「${row.appKey}」的回调注册吗?`,
|
||||
info: "停用后该应用的流程事件将不再推送",
|
||||
async onOk() {
|
||||
await doStatus();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onResetSecret(row: any) {
|
||||
const d: any = await flowApi.regResetSecret(row.id).send();
|
||||
Message.info({ content: `新 secret(仅本次展示):${d?.secret}`, duration: 8 });
|
||||
loadRegs();
|
||||
function onResetSecret(row: any) {
|
||||
MyConfirm({
|
||||
title: "重置secret",
|
||||
msg: `确定重置应用「${row.appKey}」的 secret 吗?`,
|
||||
info: "重置后原 secret 立即失效",
|
||||
async onOk() {
|
||||
const d: any = await flowApi.regResetSecret(row.id).send();
|
||||
Message.info({ content: `新 secret(仅本次展示):${d?.secret}`, duration: 8 });
|
||||
loadRegs();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onMarkDead(row: any) {
|
||||
Modal.confirm({
|
||||
MyConfirm({
|
||||
title: "标记死信",
|
||||
content: `办件 ${row.instanceId} 事件 ${row.event} 已持续重试 ${row.retryCount} 次。确认该事件已无意义、停止重试并转入死信?`,
|
||||
onOk: async () => {
|
||||
msg: `办件 ${row.instanceId} 事件 ${row.event} 已持续重试 ${row.retryCount} 次。确认该事件已无意义、停止重试并转入死信?`,
|
||||
info: "标记后可在推送日志(死信)中重推复活",
|
||||
async onOk() {
|
||||
await flowApi.callbackMarkDead(row.id).send();
|
||||
Message.success("已标记死信(可在死信列表重推复活)");
|
||||
loadHungs();
|
||||
@@ -117,70 +141,82 @@ function onMarkDead(row: any) {
|
||||
});
|
||||
}
|
||||
|
||||
function onSelectionChange(selection: any[]) {
|
||||
selectedLogIds.value = selection.map((r) => r.id);
|
||||
}
|
||||
|
||||
async function onRepush() {
|
||||
if (!selectedLogIds.value.length) {
|
||||
async function onRepush(checkList: number[]) {
|
||||
if (!checkList.length) {
|
||||
Message.warning("请先勾选日志行");
|
||||
return;
|
||||
}
|
||||
await flowApi.callbackRepush(selectedLogIds.value).send();
|
||||
await flowApi.callbackRepush(checkList).send();
|
||||
Message.success("已触发重推");
|
||||
loadLogs();
|
||||
// 重推后状态会变,留在当前页刷新
|
||||
logTableRef.value?.reloadData(OperatorEnum.edit);
|
||||
}
|
||||
|
||||
// 三个内容区 v-show 共存,日志/挂起首次切换才拉取(保持原惰性加载行为)
|
||||
const logsLoaded = ref(false);
|
||||
const hungLoaded = ref(false);
|
||||
function switchTab(name: string) {
|
||||
tab.value = name;
|
||||
if (name === "logs") loadLogs();
|
||||
if (name === "hung") loadHungs();
|
||||
if (name === "logs" && !logsLoaded.value) {
|
||||
logsLoaded.value = true;
|
||||
logTableRef.value?.reloadData(OperatorEnum.sel);
|
||||
}
|
||||
if (name === "hung" && !hungLoaded.value) {
|
||||
hungLoaded.value = true;
|
||||
loadHungs();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRegs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Tabs :value="tab" @on-click="switchTab">
|
||||
<TabPane label="回调注册" name="regs">
|
||||
<Table :columns="regColumns" :data="regs" :loading="loading" stripe />
|
||||
</TabPane>
|
||||
<TabPane label="推送日志" name="logs">
|
||||
<div class="filter">
|
||||
<Select v-model="logFilter.status" clearable placeholder="状态" style="width: 130px">
|
||||
<Option :value="0" label="待推/重试中" />
|
||||
<Option :value="1" label="成功" />
|
||||
<Option :value="2" label="死信" />
|
||||
</Select>
|
||||
<Select v-model="logFilter.event" clearable placeholder="事件" style="width: 190px">
|
||||
<Option v-for="e in EVENT_OPTIONS" :key="e" :value="e" :label="e" />
|
||||
</Select>
|
||||
<Input v-model="logFilter.instanceId" placeholder="办件ID" style="width: 120px" clearable />
|
||||
<Button @click="logPage = 1; loadLogs()">查询</Button>
|
||||
<Button type="primary" @click="onRepush">重推选中</Button>
|
||||
</div>
|
||||
<Table :columns="logColumns" :data="logs" :loading="loading" stripe @on-selection-change="onSelectionChange" />
|
||||
<div class="pager">
|
||||
<Page :total="logTotal" :current="logPage" :page-size="10" show-total @on-change="(p: number) => { logPage = p; loadLogs(); }" />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane label="挂起中" name="hung">
|
||||
<div class="tip">
|
||||
持续重试中的事件(消费方疑似停机;消费方恢复后自动追平,无需干预)。仅当人工判定事件已无意义时才标记死信。
|
||||
</div>
|
||||
<Table :columns="hungColumns" :data="hungs" :loading="hungLoading" stripe />
|
||||
<div class="pager">
|
||||
<Page :total="hungTotal" :current="hungPage" :page-size="10" show-total @on-change="(p: number) => { hungPage = p; loadHungs(); }" />
|
||||
</div>
|
||||
</TabPane>
|
||||
<div class="tabs-wrapper">
|
||||
<Tabs type="card" :value="tab" @on-click="switchTab">
|
||||
<TabPane label="回调注册" name="regs" />
|
||||
<TabPane label="推送日志" name="logs" />
|
||||
<TabPane label="挂起中" name="hung" />
|
||||
</Tabs>
|
||||
|
||||
<!-- 回调注册:无分页 -->
|
||||
<div class="main-container" v-show="tab === 'regs'">
|
||||
<Table :columns="regColumns" :data="regs" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="regsTableHeight" no-data-text="暂无数据" />
|
||||
</div>
|
||||
|
||||
<!-- 推送日志:搜索区 + 分页,重推选中落分页栏左侧批量操作位 -->
|
||||
<div class="main-container" v-show="tab === 'logs'">
|
||||
<div class="search-wrap fix">
|
||||
<Select v-model="logFilter.status" class="search-wrap_sel" clearable placeholder="状态" @on-change="onLogSearch">
|
||||
<Option :value="0" label="待推/重试中" />
|
||||
<Option :value="1" label="成功" />
|
||||
<Option :value="2" label="死信" />
|
||||
</Select>
|
||||
<Select v-model="logFilter.event" class="search-wrap_sel ml-12" clearable placeholder="事件" @on-change="onLogSearch">
|
||||
<Option v-for="e in EVENT_OPTIONS" :key="e" :value="e" :label="e" />
|
||||
</Select>
|
||||
<Input class="fr" v-model="logFilter.instanceId" placeholder="办件ID" search suffix="ios-search"
|
||||
style="width: 200px" @on-search="onLogSearch" @on-clear="onLogSearch" />
|
||||
</div>
|
||||
<TablePageAlovaNew ref="logTableRef" :dataApi="logApi" :columns="logColumns"
|
||||
:tableHeight="logTableHeight" :autoLoad="false">
|
||||
<template #default="{ checkList }">
|
||||
<Button size="small" type="primary" :disabled="!checkList.length" @click="onRepush(checkList)">重推选中</Button>
|
||||
</template>
|
||||
</TablePageAlovaNew>
|
||||
</div>
|
||||
|
||||
<!-- 挂起中:无分页 -->
|
||||
<div class="main-container" v-show="tab === 'hung'">
|
||||
<div class="tip">
|
||||
持续重试中的事件(消费方疑似停机;消费方恢复后自动追平,无需干预)。仅当人工判定事件已无意义时才标记死信。
|
||||
</div>
|
||||
<Table :columns="hungColumns" :data="hungs" :loading="hungLoading" class="link-table table-bottom-box"
|
||||
:height="hungTableHeight" no-data-text="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 12px 16px; }
|
||||
.filter { margin-bottom: 10px; display: flex; gap: 8px; }
|
||||
.pager { margin-top: 12px; text-align: right; }
|
||||
.tip { margin-bottom: 10px; padding: 6px 10px; background: #f8f8f9; border-radius: 4px; color: #515a6e; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
// 流程定义管理:列表 + 发布/停用/升版本/版本历史/图示。
|
||||
// 新建与编辑走独立的全屏可视化编辑器(/admin/defs/editor,钉钉式卡片流,见《流程中心流程编辑器方案.md》);
|
||||
// 本页不再直接编辑 JSON 文本(原 M1 textarea 已由编辑器取代,编辑器内置"源码"高级模式兜底)。
|
||||
// 流程定义管理:列表 + 发布/启用/停用/升版本/版本历史/图示/源码。
|
||||
// 新建与编辑走独立的全屏可视化编辑器(/admin/defs/editor,钉钉式卡片流,见《流程编辑器方案.md》);
|
||||
// 本页不再直接编辑 JSON 文本(编辑器内置"源码"高级模式兜底);行内「源码」为只读查看+复制。
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import SourceDrawer from "@/views/flow/components/SourceDrawer.vue";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import Utils from "@/utils";
|
||||
|
||||
@@ -15,28 +17,40 @@ const loading = ref(false);
|
||||
const rows = ref<any[]>([]);
|
||||
const versionVisible = ref(false);
|
||||
const versions = ref<any[]>([]);
|
||||
// 源码抽屉(列表行已带回 contentJson,无需再请求)
|
||||
const sourceVisible = ref(false);
|
||||
const sourceText = ref("");
|
||||
const sourceTitle = ref("");
|
||||
// 操作按钮区 + 表格,无搜索、无分页
|
||||
const tableHeight = ref(Utils.useTableHeight({ isOperat: true }));
|
||||
|
||||
const columns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "流程", key: "name", minWidth: 180 },
|
||||
{ title: "编码", key: "code", width: 160 },
|
||||
{ title: "版本", key: "version", width: 70 },
|
||||
{ title: "状态", width: 90, render: (h: any, p: any) => {
|
||||
{ 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: "GUID", key: "flowGuid", width: 200 },
|
||||
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") },
|
||||
{ title: "操作", width: 320, fixed: "right", render: (h: any, p: any) => {
|
||||
{ title: "操作", width: 250, align: "center", fixed: "right", render: (h: any, p: any) => {
|
||||
const btns: any[] = [];
|
||||
if (p.row.status === 0) {
|
||||
btns.push(Utils.operatingTextHandle(h, "编辑", "", () => onEdit(p.row)));
|
||||
btns.push(Utils.operatingTextHandle(h, "发布", "", () => onPublish(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "编辑", "icon-edit", () => onEdit(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "发布", "icon-fasong", () => onPublish(p.row)));
|
||||
} else if (p.row.status === 1) {
|
||||
btns.push(Utils.operatingTextHandle(h, "升版本", "", () => onNewVersion(p.row)));
|
||||
btns.push(Utils.operatingTextHandle(h, "停用", "text-error", () => onOffline(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "升版本", "icon-tianjia", () => onNewVersion(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "停用", "icon-stop", () => onOffline(p.row)));
|
||||
} else if (p.row.status === 2) {
|
||||
// 与「停用」对称的专用启用通道(后端 /def/enable:仅停用版→已发布,重跑发布校验;
|
||||
// 草稿不走这里,草稿用「发布」)
|
||||
btns.push(Utils.operatingHandle(h, "启用", "icon-play", () => onEnable(p.row)));
|
||||
}
|
||||
btns.push(Utils.operatingTextHandle(h, "图示", "", () => onDiagram(p.row)));
|
||||
btns.push(Utils.operatingTextHandle(h, "版本历史", "", () => onVersions(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "图示", "icon-tupian", () => onDiagram(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "源码", "icon-liebiao", () => onSource(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "版本历史", "icon-shijian", () => onVersions(p.row)));
|
||||
return btns;
|
||||
} },
|
||||
];
|
||||
@@ -76,10 +90,52 @@ async function onPublish(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onOffline(row: any) {
|
||||
await flowApi.defOffline(row.id).send();
|
||||
Message.success("已停用");
|
||||
load();
|
||||
function onOffline(row: any) {
|
||||
MyConfirm({
|
||||
title: "停用",
|
||||
msg: `确定停用流程「${row.name}」吗?`,
|
||||
info: "停用后无法发起新办件,进行中的办件不受影响",
|
||||
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 || "启用失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 源码:列表行已带回 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) {
|
||||
@@ -88,31 +144,34 @@ async function onVersions(row: any) {
|
||||
}
|
||||
|
||||
function onDiagram(row: any) {
|
||||
router.push({ path: "/diagram", query: { defId: row.id, mode: "auto" } });
|
||||
// 不传 mode:后端缺省=人员视图(两档由图示页自身的 科室/人员 切换控制)
|
||||
router.push({ path: "/diagram", query: { defId: row.id } });
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="filter">
|
||||
<Button type="primary" @click="onNew">新建流程</Button>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" :loading="loading" stripe />
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<!-- 操作按钮区:主操作靠左(bsch应用页规范 §1) -->
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="onNew">新建流程</Button>
|
||||
</div>
|
||||
|
||||
<Modal v-model="versionVisible" title="版本历史" width="640">
|
||||
<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', ' ') || '-' },
|
||||
]" :data="versions" size="small" />
|
||||
</Modal>
|
||||
<Table :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
|
||||
<Modal v-model="versionVisible" title="版本历史" width="640">
|
||||
<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', ' ') || '-' },
|
||||
]" :data="versions" size="small" no-data-text="暂无数据" />
|
||||
</Modal>
|
||||
|
||||
<SourceDrawer v-model="sourceVisible" :text="sourceText" :subtitle="sourceTitle" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 12px 16px; }
|
||||
.filter { margin-bottom: 10px; }
|
||||
</style>
|
||||
|
||||
@@ -1,68 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
// 办件记录(管理端):全校办件查询
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { flowApi, INSTANCE_STATUS } from "@/api/module/flow";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
|
||||
import Utils from "@/utils";
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const total = ref(0);
|
||||
const filter = ref<any>({ status: null, keyword: "" });
|
||||
const rows = ref<any[]>([]);
|
||||
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isPage: true }));
|
||||
const dataParams = ref<{ status?: number; keyword?: string }>({ status: undefined, keyword: "" });
|
||||
|
||||
const columns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "办件标题", key: "bizTitle", minWidth: 220 },
|
||||
{ title: "业务", width: 170, render: (h: any, p: any) => h("span", `${p.row.bizType || "-"}/${p.row.bizId || "-"}`) },
|
||||
{ title: "状态", width: 100, render: (h: any, p: any) => {
|
||||
const c = p.row.status === 10 ? "blue" : p.row.status === 20 ? "green" : p.row.status === 30 ? "red" : "gray";
|
||||
return h("Tag", { color: c }, () => INSTANCE_STATUS[p.row.status] || p.row.status);
|
||||
} },
|
||||
{ title: "状态", width: 100, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: INSTANCE_STATUS_COLOR[p.row.status] || "gray" }, () => INSTANCE_STATUS[p.row.status] || p.row.status) },
|
||||
{ title: "提交人", key: "submitterName", width: 100 },
|
||||
{ title: "提交时间", width: 160, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) },
|
||||
{ title: "操作", width: 90, render: (h: any, p: any) =>
|
||||
Utils.operatingTextHandle(h, "详情", "", () => router.push(`/instance/${p.row.id}?pid=102`)) },
|
||||
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) =>
|
||||
Utils.operatingHandle(h, "详情", "icon-browse", () => router.push(`/instance/${p.row.id}?pid=102`)) },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = { page: page.value, size: 10 };
|
||||
if (filter.value.status !== null && filter.value.status !== undefined) params.status = filter.value.status;
|
||||
if (filter.value.keyword) params.keyword = filter.value.keyword;
|
||||
const data: any = await flowApi.adminInstances(params).send();
|
||||
rows.value = data?.content || [];
|
||||
total.value = data?.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
|
||||
const tableApi = (p: { pageIndex: number; pageSize: number }) => {
|
||||
const params: any = { page: p.pageIndex, size: p.pageSize };
|
||||
if (dataParams.value.status) params.status = dataParams.value.status;
|
||||
if (dataParams.value.keyword) params.keyword = dataParams.value.keyword.trim();
|
||||
return flowApi.adminInstances(params);
|
||||
};
|
||||
|
||||
onMounted(load);
|
||||
function onSearch() {
|
||||
tableRef.value?.refreshData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="filter">
|
||||
<Select v-model="filter.status" clearable placeholder="状态" style="width: 130px">
|
||||
<Option :value="10" label="审批中" />
|
||||
<Option :value="20" label="已完成" />
|
||||
<Option :value="30" label="已拒绝" />
|
||||
<Option :value="40" label="已撤回" />
|
||||
</Select>
|
||||
<Input v-model="filter.keyword" placeholder="标题关键字" style="width: 200px" clearable @on-enter="load" />
|
||||
<Button @click="page = 1; load()">查询</Button>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" :loading="loading" stripe />
|
||||
<div class="pager">
|
||||
<Page :total="total" :current="page" :page-size="10" show-total @on-change="(p: number) => { page = p; load(); }" />
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<div class="search-wrap fix">
|
||||
<Select v-model="dataParams.status" class="search-wrap_sel" clearable placeholder="状态" @on-change="onSearch">
|
||||
<Option :value="10" label="审批中" />
|
||||
<Option :value="20" label="已完成" />
|
||||
<Option :value="30" label="已拒绝" />
|
||||
<Option :value="40" label="已撤回" />
|
||||
</Select>
|
||||
<Input class="fr" v-model="dataParams.keyword" placeholder="标题关键字" search suffix="ios-search"
|
||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
</div>
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 12px 16px; }
|
||||
.filter { margin-bottom: 10px; display: flex; gap: 8px; }
|
||||
.pager { margin-top: 12px; text-align: right; }
|
||||
</style>
|
||||
|
||||
+119
-81
@@ -2,6 +2,7 @@
|
||||
// 办理人映射管理(概览 §4.7/§六):DEPT_KEY → 办理人;办理人改为弹窗选人(参照 Notice 选人范式)
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import UserSelectInput from "@/components/UserSelectInput.vue";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import Utils from "@/utils";
|
||||
@@ -9,10 +10,8 @@ import Utils from "@/utils";
|
||||
const loading = ref(false);
|
||||
const deptKeyFilter = ref("");
|
||||
const rows = ref<any[]>([]);
|
||||
|
||||
const saveVisible = ref(false);
|
||||
const saving = ref(false);
|
||||
const form = ref<any>({ deptKey: "", deptName: "", userId: null, userName: "" });
|
||||
// 操作按钮区 + 搜索区、无分页
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isOperat: true }));
|
||||
|
||||
// 一键拉取教师组(预览确认后批量导入)
|
||||
const groupVisible = ref(false);
|
||||
@@ -22,7 +21,7 @@ const groupRows = ref<any[]>([]);
|
||||
const groupColumns = [
|
||||
{ title: "科室编码", key: "deptKey", width: 100 },
|
||||
{ title: "科室名称", key: "deptName", width: 160 },
|
||||
{ title: "成员数", key: "memberCount", width: 90, render: (h: any, p: any) => h("span", String(p.row.users?.length || 0)) },
|
||||
{ title: "成员数", key: "memberCount", width: 90, align: "center", render: (h: any, p: any) => h("span", String(p.row.users?.length || 0)) },
|
||||
{ title: "成员", key: "members", render: (h: any, p: any) => h("span", (p.row.users || []).map((u: any) => `${u.userName}(${u.userId})`).join("、") || "(空,导入后可手工补)") },
|
||||
];
|
||||
|
||||
@@ -54,26 +53,41 @@ async function onGroupImport() {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "科室编码", key: "deptKey", width: 140 },
|
||||
{ title: "科室名称", key: "deptName", width: 160, render: (h: any, p: any) => h("span", p.row.deptName || "-") },
|
||||
{ title: "科室名称", width: 160, render: (h: any, p: any) => h("span", p.row.deptName || "-") },
|
||||
{ title: "办理人", key: "userName", width: 120 },
|
||||
{ title: "用户ID", key: "userId", width: 90 },
|
||||
{ title: "状态", width: 90, render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
|
||||
{ title: "操作", width: 100, render: (h: any, p: any) =>
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
|
||||
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) =>
|
||||
p.row.status === 0
|
||||
? Utils.operatingTextHandle(h, "停用", "text-error", () => onDisable(p.row))
|
||||
: h("span", "-") },
|
||||
? Utils.operatingHandle(h, "停用", "icon-stop", () => onDisable(p.row))
|
||||
: Utils.operatingHandle(h, "启用", "icon-play", () => onEnable(p.row)) },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
rows.value = (await flowApi.mapList(deptKeyFilter.value || undefined).send()) || [];
|
||||
rows.value = (await flowApi.mapList(deptKeyFilter.value.trim() || undefined).send()) || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
load();
|
||||
}
|
||||
|
||||
// ---- 新增/更新映射弹窗(规范:必填红星标注,校验失败提示统一文案) ----
|
||||
const saveVisible = ref(false);
|
||||
const saving = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const form = ref<any>({ deptKey: "", deptName: "", userId: null, userName: "" });
|
||||
const saveRules = {
|
||||
deptKey: [{ required: true, message: "请填写红色星号内容!", trigger: "blur" }],
|
||||
userId: [{ required: true, message: "请填写红色星号内容!", trigger: "change" }],
|
||||
};
|
||||
|
||||
function onAdd() {
|
||||
form.value = { deptKey: "", deptName: "", userId: null, userName: "" };
|
||||
saveVisible.value = true;
|
||||
@@ -84,84 +98,108 @@ function onUserPick(u: any) {
|
||||
form.value.userName = u?.name || "";
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!form.value.deptKey.trim() || !form.value.userId) {
|
||||
Message.warning("科室编码与办理人必填");
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await flowApi.mapSave({
|
||||
deptKey: form.value.deptKey.trim(),
|
||||
deptName: form.value.deptName,
|
||||
userId: Number(form.value.userId),
|
||||
userName: form.value.userName,
|
||||
}).send();
|
||||
Message.success("已保存");
|
||||
saveVisible.value = false;
|
||||
load();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
function onSave() {
|
||||
formRef.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await flowApi.mapSave({
|
||||
deptKey: form.value.deptKey.trim(),
|
||||
deptName: form.value.deptName,
|
||||
userId: Number(form.value.userId),
|
||||
userName: form.value.userName,
|
||||
}).send();
|
||||
Message.success("已保存");
|
||||
saveVisible.value = false;
|
||||
load();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function onDisable(row: any) {
|
||||
await flowApi.mapDisable(row.id).send();
|
||||
Message.success("已停用");
|
||||
load();
|
||||
function onDisable(row: any) {
|
||||
MyConfirm({
|
||||
title: "停用",
|
||||
msg: `确定停用科室「${row.deptName || row.deptKey}」的办理人映射吗?`,
|
||||
info: "停用后该科室流程节点需重新配置办理人",
|
||||
async onOk() {
|
||||
await flowApi.mapDisable(row.id).send();
|
||||
Message.success("已停用");
|
||||
load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 启用(2026-09-18 补齐:原来停用后只能重新"新增映射"恢复) */
|
||||
function onEnable(row: any) {
|
||||
MyConfirm({
|
||||
title: "启用",
|
||||
msg: `确定启用科室「${row.deptName || row.deptKey}」的办理人「${row.userName || row.userId}」吗?`,
|
||||
info: "启用后该科室的流程节点会重新解析到这名办理人",
|
||||
async onOk() {
|
||||
await flowApi.mapEnable(row.id).send();
|
||||
Message.success("已启用");
|
||||
load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="filter">
|
||||
<Input v-model="deptKeyFilter" placeholder="按科室编码筛选" style="width: 200px" clearable @on-enter="load" />
|
||||
<Button @click="load">查询</Button>
|
||||
<Button type="primary" @click="onAdd">新增映射</Button>
|
||||
<Button type="info" :loading="groupLoading" @click="onGroupPreview">一键拉取教师组</Button>
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<!-- 操作按钮区:主操作靠左、其余靠右(bsch应用页规范 §1) -->
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="onAdd">新增映射</Button>
|
||||
<Button class="fr" size="large" :loading="groupLoading" @click="onGroupPreview">一键拉取教师组</Button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区:关键字即输即搜,不设查询按钮 -->
|
||||
<div class="search-wrap fix">
|
||||
<Input class="fr" v-model="deptKeyFilter" placeholder="按科室编码筛选" search suffix="ios-search"
|
||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
</div>
|
||||
|
||||
<Table :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
|
||||
<Modal v-model="saveVisible" title="新增/更新映射" :mask-closable="false">
|
||||
<Form ref="formRef" :model="form" :rules="saveRules" :label-width="90">
|
||||
<FormItem label="科室编码" prop="deptKey">
|
||||
<Input v-model="form.deptKey" placeholder="流程 JSON 中 DEPT_KEY 引用的编码,如 ybb" />
|
||||
</FormItem>
|
||||
<FormItem label="科室名称">
|
||||
<Input v-model="form.deptName" placeholder="如 医保办(可空)" />
|
||||
</FormItem>
|
||||
<FormItem label="办理人" prop="userId">
|
||||
<UserSelectInput
|
||||
:model-value="form.userId"
|
||||
:display-name="form.userName"
|
||||
@update:model-value="form.userId = $event"
|
||||
@update:display-name="form.userName = $event"
|
||||
@on-change="onUserPick"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="saveVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="onSave">提交</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="groupVisible" title="一键拉取教师组(UserBase 学校组树)" width="720" :mask-closable="false">
|
||||
<p style="margin-bottom: 8px; color: #808695">
|
||||
按学校组树中类型=教师组(type 7)的容器组取其子组作为科室(无子组时容器自身即科室),科室编码=UserBase 组 id,成员=组内用户。已存在且启用中的映射自动跳过。
|
||||
</p>
|
||||
<Table :columns="groupColumns" :data="groupRows" :loading="groupLoading" max-height="360" no-data-text="暂无数据" />
|
||||
<template #footer>
|
||||
<Button @click="groupVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="groupImporting" :disabled="!groupRows.length" @click="onGroupImport">确认导入</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" :loading="loading" stripe />
|
||||
|
||||
<Modal v-model="saveVisible" title="新增/更新映射" :mask-closable="false">
|
||||
<Form :label-width="90">
|
||||
<FormItem label="科室编码">
|
||||
<Input v-model="form.deptKey" placeholder="流程 JSON 中 DEPT_KEY 引用的编码,如 ybb" />
|
||||
</FormItem>
|
||||
<FormItem label="科室名称">
|
||||
<Input v-model="form.deptName" placeholder="如 医保办(可空)" />
|
||||
</FormItem>
|
||||
<FormItem label="办理人">
|
||||
<UserSelectInput
|
||||
:model-value="form.userId"
|
||||
:display-name="form.userName"
|
||||
@update:model-value="form.userId = $event"
|
||||
@update:display-name="form.userName = $event"
|
||||
@on-change="onUserPick"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="saveVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="onSave">保存</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="groupVisible" title="一键拉取教师组(UserBase 学校组树)" width="720" :mask-closable="false">
|
||||
<p style="margin-bottom: 8px; color: #808695">
|
||||
按学校组树中类型=教师组(type 7)的容器组取其子组作为科室(无子组时容器自身即科室),科室编码=UserBase 组 id,成员=组内用户。已存在且启用中的映射自动跳过。
|
||||
</p>
|
||||
<Table :columns="groupColumns" :data="groupRows" :loading="groupLoading" max-height="360" stripe />
|
||||
<template #footer>
|
||||
<Button @click="groupVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="groupImporting" :disabled="!groupRows.length" @click="onGroupImport">确认导入</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 12px 16px; }
|
||||
.filter { margin-bottom: 10px; display: flex; gap: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -162,3 +162,102 @@ export function toCanvasJson(contentJson: string | CanvasJson): CanvasJson {
|
||||
}
|
||||
return contentJson;
|
||||
}
|
||||
|
||||
/** 查看器 BPMN DI 形状尺寸(事件 36、网关 50、任务两行文本 150x76) */
|
||||
const DI_SIZE: Record<string, { w: number; h: number }> = {
|
||||
START: { w: 36, h: 36 },
|
||||
END: { w: 36, h: 36 },
|
||||
APPROVAL: { w: 150, h: 76 },
|
||||
PARALLEL_SPLIT: { w: 50, h: 50 },
|
||||
PARALLEL_JOIN: { w: 50, h: 50 },
|
||||
};
|
||||
|
||||
/**
|
||||
* contentJson(+layout) → 可导入只读 Viewer 的 BPMN XML(含 DI 布局)。
|
||||
* 画布视图专用:bpmn-js Viewer 没有 modeling 模块,不能走 toBpmn 的程序化布点,
|
||||
* 直接生成带 BPMNShape/BPMNEdge 的 XML 一次导入;连线给正交 waypoints(右中→中线→左中)。
|
||||
*/
|
||||
export function toBpmnXml(def: CanvasJson): string {
|
||||
const nodes: any[] = def.nodes || [];
|
||||
const edges: any[] = def.edges || [];
|
||||
// 元素中心点:layout 覆盖全部节点用 layout(编辑器保存的是中心坐标),否则回退自动布局
|
||||
const saved = def.layout || {};
|
||||
const useSaved = nodes.length > 0 && nodes.every((n) => saved[String(n.id)]);
|
||||
const centers: Record<string, { x: number; y: number }> = {};
|
||||
if (useSaved) {
|
||||
nodes.forEach((n) => { centers[String(n.id)] = saved[String(n.id)]; });
|
||||
} else {
|
||||
const auto = autoLayout({ nodes, edges });
|
||||
nodes.forEach((n) => {
|
||||
const p = auto[String(n.id)];
|
||||
if (p) centers[String(n.id)] = { x: p.x + p.w / 2, y: p.y + p.h / 2 };
|
||||
});
|
||||
}
|
||||
const esc = (s: unknown): string =>
|
||||
String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
const body = nodes.filter((n) => BPMN_TYPE[String(n.type)] && centers[String(n.id)]);
|
||||
const flows = edges.filter((e) => centers[String(e.from)] && centers[String(e.to)]);
|
||||
|
||||
const elems = body
|
||||
.map((n) => {
|
||||
const tag = BPMN_TYPE[String(n.type)].replace("bpmn:", "");
|
||||
const name = n.name ? ` name="${esc(n.name)}"` : "";
|
||||
return ` <bpmn:${tag} id="${esc(n.id)}"${name} />`;
|
||||
})
|
||||
.join("\n");
|
||||
const flowElems = flows
|
||||
.map((e, i) => {
|
||||
const name = e.name ? ` name="${esc(e.name)}"` : "";
|
||||
return ` <bpmn:sequenceFlow id="flow_${i}"${name} sourceRef="${esc(e.from)}" targetRef="${esc(e.to)}" />`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const bounds = (id: string): { x: number; y: number; w: number; h: number } => {
|
||||
const c = centers[id];
|
||||
const type = String(nodes.find((n) => String(n.id) === id)?.type || "APPROVAL");
|
||||
const s = DI_SIZE[type] || DI_SIZE.APPROVAL;
|
||||
return { x: Math.round(c.x - s.w / 2), y: Math.round(c.y - s.h / 2), w: s.w, h: s.h };
|
||||
};
|
||||
const shapes = body
|
||||
.map((n) => {
|
||||
const b = bounds(String(n.id));
|
||||
return ` <bpmndi:BPMNShape id="di_${esc(n.id)}" bpmnElement="${esc(n.id)}">
|
||||
<dc:Bounds x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" />
|
||||
</bpmndi:BPMNShape>`;
|
||||
})
|
||||
.join("\n");
|
||||
const edgeDi = flows
|
||||
.map((e, i) => {
|
||||
const fb = bounds(String(e.from));
|
||||
const tb = bounds(String(e.to));
|
||||
const x1 = fb.x + fb.w;
|
||||
const y1 = fb.y + fb.h / 2;
|
||||
const x2 = tb.x;
|
||||
const y2 = tb.y + tb.h / 2;
|
||||
const mid = Math.round((x1 + x2) / 2);
|
||||
const pts = Math.abs(y1 - y2) < 2
|
||||
? [[x1, y1], [x2, y2]]
|
||||
: [[x1, y1], [mid, y1], [mid, y2], [x2, y2]];
|
||||
const wp = pts.map(([x, y]) => ` <di:waypoint x="${Math.round(x)}" y="${Math.round(y)}" />`).join("\n");
|
||||
return ` <bpmndi:BPMNEdge id="di_flow_${i}" bpmnElement="flow_${i}">\n${wp}\n </bpmndi:BPMNEdge>`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
||||
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
||||
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
||||
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
|
||||
targetNamespace="http://bpmn.io/schema/bpmn">
|
||||
<bpmn:process id="lc_process" isExecutable="true">
|
||||
${elems}
|
||||
${flowElems}
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="lc_process">
|
||||
${shapes}
|
||||
${edgeDi}
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user