feat(flow): 图示页砍画布视图档,布局改后端 positions 单源
This commit is contained in:
+16
-121
@@ -1,20 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
// 流程示意图(方案 §4):深链页不挂菜单;query: flowGuid|instanceId|defId(三选一)+ mode(dept|user,缺省 user)。
|
||||
// 三视图(2026-09-17):auto=示意图(手写 SVG 自动布局,零图形库)|canvas=画布视图(bpmn-js 只读渲染,
|
||||
// 复用设计器的 contentJson→BPMN 通道,与示意图视觉区分)|img=进度图(服务端 PNG 流)。
|
||||
// 出图形态两档(2026-10-03 画布视图退役):auto=示意图(后端 positions 单源,手写 SVG)|img=进度图(服务端 PNG 流)。
|
||||
// 显示口径(2026-09-18):只两档 dept=科室/user=人员,不传=人员;与办件详情/办理页的轨迹口径同参数。
|
||||
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from "vue";
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import SourceDrawer from "@/views/flow/components/SourceDrawer.vue";
|
||||
import config from "@/assets/config/host.config";
|
||||
import NavigatedViewer from "bpmn-js/lib/NavigatedViewer";
|
||||
import { toBpmnXml, CanvasJson } from "@/views/flow/admin/def-editor/bpmn/serialize";
|
||||
// bpmn-js 渲染样式(设计器 custom.ts 已全局引过,此处再引由打包器去重;漏引连线/图标异常)
|
||||
import "diagram-js/assets/diagram-js.css";
|
||||
import "bpmn-js/dist/assets/bpmn-js.css";
|
||||
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -29,10 +22,9 @@ const sourceText = ref("");
|
||||
const sourceTitle = ref("");
|
||||
const sourceLoading = ref(false);
|
||||
|
||||
/** 出图形态(2026-09-17 三选一):auto=示意图(自动布局)|canvas=画布视图(bpmn-js 只读)|img=进度图(服务端 PNG 流) */
|
||||
const chartStyle = ref<string>((route.query.chart as string) || "auto");
|
||||
/** 出图形态两档(2026-10-03 砍画布视图):auto=示意图|img=进度图;旧深链 chart=canvas 归一到 auto */
|
||||
const chartStyle = ref<string>(route.query.chart === "img" ? "img" : "auto");
|
||||
const showImg = computed(() => chartStyle.value === "img");
|
||||
const showCanvas = computed(() => chartStyle.value === "canvas");
|
||||
|
||||
/** 进度图 img 直嵌源(同 InstanceDetail 的"CAS 验证码式",img 不走 alova 显式带 serviceHost;管理面 defId 走 admin 端点) */
|
||||
const pngSrc = computed(() => {
|
||||
@@ -49,8 +41,18 @@ const DEF_STATUS: Record<number, string> = { 0: "草稿", 1: "已发布", 2: "
|
||||
// 布局常量与拓扑分层算法在 src/utils/flowLayout.ts(与设计器画布共用;并行分支组垂直居中)
|
||||
import { layout as autoLayout, PAD, Pos } from "@/utils/flowLayout";
|
||||
|
||||
/** 布局:后端 positions 单源(内容区坐标);旧后端未下发时回退本地自动布局(灰度兜底) */
|
||||
const positions = computed<Record<string, Pos>>(() => {
|
||||
return autoLayout({ nodes: vo.value?.nodes || [], edges: vo.value?.edges || [] });
|
||||
const raw = (vo.value as any)?.positions as Record<string, { x: number; y: number; w: number; h: number }> | undefined;
|
||||
const nodes: any[] = vo.value?.nodes || [];
|
||||
if (raw && nodes.length && nodes.every((n) => raw[String(n.id)])) {
|
||||
const out: Record<string, Pos> = {};
|
||||
Object.entries(raw).forEach(([id, p]) => {
|
||||
out[id] = { x: p.x, y: p.y, w: p.w, h: p.h, depth: 0, row: 0 };
|
||||
});
|
||||
return out;
|
||||
}
|
||||
return autoLayout({ nodes, edges: vo.value?.edges || [] });
|
||||
});
|
||||
|
||||
const vbSize = computed(() => {
|
||||
@@ -178,94 +180,10 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 画布视图(bpmn-js 只读渲染,2026-09-17 与示意图区分) ====================
|
||||
|
||||
const bpmnHost = ref<HTMLDivElement | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let bpmnViewer: any = null;
|
||||
|
||||
/** DiagramVo → 画布视图数据(layout 为后端透传的设计器中心坐标;缺省回退共享自动布局) */
|
||||
const bpmnDef = computed<CanvasJson | null>(() => {
|
||||
const nodes: any[] = vo.value?.nodes || [];
|
||||
if (!nodes.length) return null;
|
||||
return {
|
||||
code: "",
|
||||
name: vo.value?.name || "",
|
||||
start: "start",
|
||||
nodes: nodes.map((n) => ({
|
||||
id: String(n.id),
|
||||
type: n.type,
|
||||
name: n.subLabel ? `${n.label}\n${n.subLabel}` : n.label,
|
||||
})),
|
||||
edges: ((vo.value?.edges || []) as any[]).map((e) => ({
|
||||
from: String(e.from), to: String(e.to), name: e.name,
|
||||
})),
|
||||
settings: {},
|
||||
layout: vo.value?.layout as CanvasJson["layout"],
|
||||
};
|
||||
});
|
||||
|
||||
async function initBpmn() {
|
||||
const host = bpmnHost.value;
|
||||
const def = bpmnDef.value;
|
||||
if (!host || !def) return;
|
||||
bpmnViewer?.destroy();
|
||||
bpmnViewer = new NavigatedViewer({ container: host });
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (bpmnViewer as any).importXML(toBpmnXml(def));
|
||||
} catch (e: any) {
|
||||
Message.error(`画布视图载入失败:${e?.message || e}`);
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const canvas = bpmnViewer.get("canvas") as any;
|
||||
canvas.zoom("fit-viewport", "auto");
|
||||
// 运行时状态着色(instance 口径):marker 类 + <style> 里 .fc-* 规则
|
||||
nodesWithState().forEach(([id, state]) => canvas.addMarker(id, `fc-${state}`));
|
||||
}
|
||||
|
||||
function nodesWithState(): Array<[string, string]> {
|
||||
return ((vo.value?.nodes || []) as any[])
|
||||
.filter((n) => n.state)
|
||||
.map((n) => [String(n.id), String(n.state)]);
|
||||
}
|
||||
|
||||
watch([showCanvas, bpmnDef], ([on]) => {
|
||||
if (on) void nextTick(initBpmn);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
bpmnViewer?.destroy();
|
||||
bpmnViewer = null;
|
||||
});
|
||||
|
||||
function bpmnZoom(delta: number) {
|
||||
if (!bpmnViewer) return;
|
||||
const c = bpmnViewer.get("canvas");
|
||||
c.zoom(c.zoom() + delta);
|
||||
}
|
||||
|
||||
function bpmnFit() {
|
||||
bpmnViewer?.get("canvas").zoom("fit-viewport", "auto");
|
||||
}
|
||||
|
||||
// ==================== 导出 PNG(方案 §4.4) ====================
|
||||
|
||||
/** 当前形态的 SVG 源:示意图取 DOM,画布视图取 bpmn saveSVG(补 width/height 供 Image 取自然尺寸) */
|
||||
/** 当前形态的 SVG 源:示意图取 DOM */
|
||||
async function currentSvg(): Promise<string | null> {
|
||||
if (chartStyle.value === "canvas") {
|
||||
if (!bpmnViewer) return null;
|
||||
const { svg } = await bpmnViewer.saveSVG();
|
||||
if (!/width="/.test(svg)) {
|
||||
const m = svg.match(/viewBox="([\d.\s-]+)"/);
|
||||
if (m) {
|
||||
const parts = m[1].trim().split(/\s+/);
|
||||
return svg.replace(/<svg /, `<svg width="${parts[2]}" height="${parts[3]}" `);
|
||||
}
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
if (!svgRef.value) return null;
|
||||
return new XMLSerializer().serializeToString(svgRef.value);
|
||||
}
|
||||
@@ -330,7 +248,6 @@ watch(() => route.query, () => load());
|
||||
{{ DEF_STATUS[vo.status] || vo.status }}</Tag>
|
||||
<RadioGroup :model-value="chartStyle" size="small" type="button" @on-change="switchChartStyle">
|
||||
<Radio label="auto">示意图</Radio>
|
||||
<Radio label="canvas">画布视图</Radio>
|
||||
<Radio label="img">进度图</Radio>
|
||||
</RadioGroup>
|
||||
<RadioGroup v-if="!showImg" :model-value="viewMode" size="small" @on-change="switchMode">
|
||||
@@ -346,15 +263,6 @@ watch(() => route.query, () => load());
|
||||
<div class="canvas img-box" v-if="vo && showImg">
|
||||
<img :src="pngSrc" alt="流程示意图" />
|
||||
</div>
|
||||
<!-- 画布视图:bpmn-js 只读渲染(bpmn.io 水印为协议要求,勿删) -->
|
||||
<div class="canvas bpmn-box" v-else-if="vo && showCanvas">
|
||||
<div ref="bpmnHost" class="bpmn-host"></div>
|
||||
<div class="bpmn-ops">
|
||||
<Button size="small" @click="bpmnZoom(-0.1)">−</Button>
|
||||
<Button size="small" @click="bpmnFit">复位</Button>
|
||||
<Button size="small" @click="bpmnZoom(0.1)">+</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 示意图:手写 SVG 自动布局 -->
|
||||
<div class="canvas" v-else-if="vo">
|
||||
<svg :width="vbSize.w" :height="vbSize.h" :viewBox="`0 0 ${vbSize.w} ${vbSize.h}`"
|
||||
@@ -473,17 +381,4 @@ watch(() => route.query, () => load());
|
||||
0%, 100% { filter: none; }
|
||||
50% { filter: drop-shadow(0 0 6px rgba(237, 64, 20, 0.8)); }
|
||||
}
|
||||
|
||||
/* 画布视图 */
|
||||
.bpmn-box { position: relative; }
|
||||
.bpmn-host { height: 560px; }
|
||||
.bpmn-ops { position: absolute; top: 10px; right: 12px; display: flex; gap: 6px; z-index: 5; }
|
||||
/* bpmn.io 水印按协议保留 */
|
||||
.bpmn-box :deep(.bjs-powered-by) { display: block; }
|
||||
/* 运行时状态着色(instance 口径 marker:done/active/rejected/pending) */
|
||||
.bpmn-box :deep(.fc-done > .djs-visual > :nth-child(1)) { stroke: #19be6b !important; }
|
||||
.bpmn-box :deep(.fc-active > .djs-visual > :nth-child(1)) { stroke: #ed4014 !important; stroke-width: 2.5px !important; }
|
||||
.bpmn-box :deep(.fc-rejected > .djs-visual > :nth-child(1)) { stroke: #ed4014 !important; fill: #fff1f0 !important; }
|
||||
.bpmn-box :deep(.fc-pending > .djs-visual > :nth-child(1)) { stroke: #b7bcc4 !important; }
|
||||
.bpmn-box :deep(.fc-done > .djs-visual > .djs-label) { fill: #4f5b66; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user