B3-1 编辑器右信息面板 + 校验清单定位(先加面板,保存链路不动)
- index.vue:画布与面板包进 .ed-body(flex row),新增 aside.ed-info 五分区—— 流程信息(名称/编码/归属应用,与画布同源同一 ref,编码沿用 !!editId||!!baseGuid 禁用)/ 状态与版本(状态 Tag + v 版本号 + flowGuid 等宽 + 复制,新建态显示「保存后生成」)/ 校验问题清单(错误在前警告在后,项内 path+msg)/ 结构统计(walkItems 递归四类计数)/ 快捷入口(流程设置/源码/图示预览,:disabled=!editId 同现状) - sourceFlowGuid ref:load() 里 defGet 后取 d.flowGuid,新建态空串 - locateIssue:清单项点击 scrollIntoView 居中 + .nd-locate 高亮 2.8s;三张卡片根元素加 :id="'nd-'+node.uid", uid 为空的清单项 class=fix 不可点;高亮样式走 :deep(卡片在子组件内) - 画布左对齐:.ed-canvas align-items center→flex-start 验证:npm run build 通过;npx tsc --noEmit 48 条=基线无新增;convert 往返 ALL PASS
This commit is contained in:
@@ -38,7 +38,7 @@ function editCondition(i: number, v: string): void {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="condition-box" :class="{ error: hasError }">
|
<div :id="'nd-' + node.uid" class="condition-box" :class="{ error: hasError }">
|
||||||
<div class="cb-title" @click="ctx.openNode(node.uid)">
|
<div class="cb-title" @click="ctx.openNode(node.uid)">
|
||||||
条件分支(按出口互斥选路)
|
条件分支(按出口互斥选路)
|
||||||
<span class="cb-hint">(点击标题配置{{ expression ? "表达式" : "判断服务" }})</span>
|
<span class="cb-hint">(点击标题配置{{ expression ? "表达式" : "判断服务" }})</span>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function onOpen(e: MouseEvent): void {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="node-card" :class="{ error: hasError }" @click="onOpen">
|
<div :id="'nd-' + node.uid" class="node-card" :class="{ error: hasError }" @click="onOpen">
|
||||||
<div class="nc-head">
|
<div class="nc-head">
|
||||||
<span class="nc-icon">审</span>
|
<span class="nc-icon">审</span>
|
||||||
<span class="nc-name">{{ node.name || "未命名节点" }}</span>
|
<span class="nc-name">{{ node.name || "未命名节点" }}</span>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function onOpen(e: MouseEvent): void {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="notify-card" :class="{ error: hasError }" @click="onOpen">
|
<div :id="'nd-' + node.uid" class="notify-card" :class="{ error: hasError }" @click="onOpen">
|
||||||
<div class="nc-head">
|
<div class="nc-head">
|
||||||
<span class="nc-icon">抄</span>
|
<span class="nc-icon">抄</span>
|
||||||
<span class="nc-name">{{ node.name || "未命名节点" }}</span>
|
<span class="nc-name">{{ node.name || "未命名节点" }}</span>
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ const baseGuid = ref("");
|
|||||||
/** 载入源定义的状态(已发布/停用版本进入编辑器时提示保存会新建草稿版本) */
|
/** 载入源定义的状态(已发布/停用版本进入编辑器时提示保存会新建草稿版本) */
|
||||||
const sourceStatus = ref(0);
|
const sourceStatus = ref(0);
|
||||||
const sourceVersion = ref(0);
|
const sourceVersion = ref(0);
|
||||||
|
/** 源流程线 flowGuid(新建态为空,右面板显示「保存后生成」) */
|
||||||
|
const sourceFlowGuid = ref("");
|
||||||
/** 归属应用(flw_def.app_key;按应用筛选维度,空=未分配) */
|
/** 归属应用(flw_def.app_key;按应用筛选维度,空=未分配) */
|
||||||
const appKey = ref("");
|
const appKey = ref("");
|
||||||
/** 应用清单下拉(应用设置白名单 flw_app enabled=1,显示应用名),拉取失败降级为仅当前值 */
|
/** 应用清单下拉(应用设置白名单 flw_app enabled=1,显示应用名),拉取失败降级为仅当前值 */
|
||||||
@@ -71,6 +73,44 @@ const routers = reactive<RouterOption[]>([]);
|
|||||||
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
||||||
const defJson = computed(() => toContentJson(model));
|
const defJson = computed(() => toContentJson(model));
|
||||||
|
|
||||||
|
/** 结构统计(右面板分区4):walkItems 已递归条件/并行分支的子树 */
|
||||||
|
const stats = computed(() => {
|
||||||
|
const s: Record<string, number> = { approval: 0, notify: 0, condition: 0, parallel: 0 };
|
||||||
|
walkItems(model.list, (item) => {
|
||||||
|
if (s[item.kind] !== undefined) s[item.kind] += 1;
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 校验清单(右面板分区3):错误项在前、警告项在后(原型 E2 顺序) */
|
||||||
|
const issueList = computed<Issue[]>(() => [
|
||||||
|
...issues.value.filter((i) => i.level === "error"),
|
||||||
|
...issues.value.filter((i) => i.level !== "error"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 点清单项 → 画布滚动到对应节点并高亮 ~3s;uid 为空(空容器类)不可定位 */
|
||||||
|
function locateIssue(uid: string): void {
|
||||||
|
const el = document.getElementById("nd-" + uid);
|
||||||
|
if (!el) return;
|
||||||
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
el.classList.add("nd-locate");
|
||||||
|
window.setTimeout(() => el.classList.remove("nd-locate"), 2800);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** flowGuid 复制:clipboard 不可用/被拒时降级 console,不打断编辑 */
|
||||||
|
function copyGuid(): void {
|
||||||
|
const g = sourceFlowGuid.value;
|
||||||
|
if (!g) return;
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
navigator.clipboard.writeText(g).then(
|
||||||
|
() => Message.success("flowGuid 已复制"),
|
||||||
|
() => console.warn("flowGuid 复制失败,请手工选取:", g),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn("flowGuid 复制失败,请手工选取:", g);
|
||||||
|
}
|
||||||
|
|
||||||
/** 归一化(去 layout 坐标、节点按 id 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
|
/** 归一化(去 layout 坐标、节点按 id 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
|
||||||
function canonicalJson(content: any): string {
|
function canonicalJson(content: any): string {
|
||||||
const copy: any = JSON.parse(JSON.stringify(content));
|
const copy: any = JSON.parse(JSON.stringify(content));
|
||||||
@@ -201,6 +241,7 @@ async function load(): Promise<void> {
|
|||||||
const d: any = await flowApi.defGet(queryId).send();
|
const d: any = await flowApi.defGet(queryId).send();
|
||||||
sourceStatus.value = d?.status ?? 0;
|
sourceStatus.value = d?.status ?? 0;
|
||||||
sourceVersion.value = d?.version ?? 0;
|
sourceVersion.value = d?.version ?? 0;
|
||||||
|
sourceFlowGuid.value = d?.flowGuid || queryGuid || "";
|
||||||
// 非草稿进入提示(2026-10-10 议题2):结构改动保存将升版本;仅改名称/归属应用请回列表用「编辑信息」
|
// 非草稿进入提示(2026-10-10 议题2):结构改动保存将升版本;仅改名称/归属应用请回列表用「编辑信息」
|
||||||
if (d?.status === 1 || d?.status === 2) {
|
if (d?.status === 1 || d?.status === 2) {
|
||||||
Message.info("结构改动保存将升版本;仅改名称/归属应用请返回列表用「编辑信息」(不升版本)");
|
Message.info("结构改动保存将升版本;仅改名称/归属应用请返回列表用「编辑信息」(不升版本)");
|
||||||
@@ -536,6 +577,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div class="ed-body">
|
||||||
<main v-if="!loading" class="ed-canvas">
|
<main v-if="!loading" class="ed-canvas">
|
||||||
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
||||||
<div class="ed-flow">
|
<div class="ed-flow">
|
||||||
@@ -544,10 +586,90 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|||||||
<div class="capsule end">流程结束</div>
|
<div class="capsule end">流程结束</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<main v-else-if="loading" class="ed-canvas loading">
|
<main v-else class="ed-canvas loading">
|
||||||
<Spin size="large" fix />
|
<Spin size="large" fix />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- 议题一:右信息面板(五分区,规格对齐原型 E1) -->
|
||||||
|
<aside class="ed-info">
|
||||||
|
<section class="ip-sec">
|
||||||
|
<h4>流程信息<span class="ip-mini">可编辑 · 保存走现有 defSave</span></h4>
|
||||||
|
<div class="ip-fld">
|
||||||
|
<label>名称</label>
|
||||||
|
<Input v-model="model.name" placeholder="流程名称" :maxlength="30" />
|
||||||
|
</div>
|
||||||
|
<div class="ip-fld">
|
||||||
|
<label>编码<span class="ip-mini">升版本态禁用(发布后不可改)</span></label>
|
||||||
|
<Input v-model="model.code" placeholder="内部管理键,发布后不可改"
|
||||||
|
:disabled="!!editId || !!baseGuid" :maxlength="40" />
|
||||||
|
</div>
|
||||||
|
<div class="ip-fld">
|
||||||
|
<label>归属应用 <span style="color: #ed4014">*</span></label>
|
||||||
|
<Select v-model="appKey" placeholder="归属应用(必选)" filterable>
|
||||||
|
<Option v-for="a in appOptions" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ip-sec">
|
||||||
|
<h4>状态与版本<span class="ip-mini">只读</span></h4>
|
||||||
|
<div class="ip-tags">
|
||||||
|
<Tag v-if="editId && sourceStatus === 0" color="orange">草稿</Tag>
|
||||||
|
<Tag v-else-if="editId" color="red">{{ DEF_STATUS[sourceStatus] }} · 保存将新建草稿版本</Tag>
|
||||||
|
<Tag v-else-if="baseGuid" color="blue">升版本 · 基于 v{{ sourceVersion }}</Tag>
|
||||||
|
<Tag v-else color="green">新建</Tag>
|
||||||
|
<Tag v-if="sourceVersion" color="gray">v{{ sourceVersion }}</Tag>
|
||||||
|
</div>
|
||||||
|
<div class="ip-guid">
|
||||||
|
<span v-if="sourceFlowGuid" class="ip-mono">{{ sourceFlowGuid }}</span>
|
||||||
|
<span v-else class="ip-mini">保存后生成</span>
|
||||||
|
<Button v-if="sourceFlowGuid" size="small" @click="copyGuid">复制</Button>
|
||||||
|
</div>
|
||||||
|
<div class="ip-tip">flowGuid 发布后不变,业务方绑定流程用此键</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ip-sec">
|
||||||
|
<h4>校验问题清单
|
||||||
|
<span class="ip-mini">{{ countErrors(issues).length ? `${issueList.length} 项待处理`
|
||||||
|
: (countWarns(issues).length ? `${countWarns(issues).length} 个警告` : "无问题") }}</span>
|
||||||
|
</h4>
|
||||||
|
<div v-if="!issueList.length" class="ip-ok">✓ 校验通过</div>
|
||||||
|
<div v-else-if="!countErrors(issues).length" class="ip-ok">
|
||||||
|
✓ 校验通过({{ countWarns(issues).length }} 个警告)
|
||||||
|
</div>
|
||||||
|
<div v-for="(i, idx) in issueList" :key="`${i.uid}-${idx}`" class="ip-issue"
|
||||||
|
:class="[(i.level === 'error' ? 'err' : 'warn'), (i.uid ? 'loc' : 'fix')]"
|
||||||
|
@click="i.uid && locateIssue(i.uid)">
|
||||||
|
<span class="ip-ic">{{ i.level === "error" ? "✗" : "!" }}</span>
|
||||||
|
<span class="ip-tx">{{ i.path }}:{{ i.msg }}
|
||||||
|
<span v-if="i.level !== 'error'" class="ip-sub">(警告,不阻塞保存)</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="i.uid" class="ip-go">定位 →</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ip-sec">
|
||||||
|
<h4>结构统计</h4>
|
||||||
|
<div class="ip-stats">
|
||||||
|
<div class="ip-stat"><b>{{ stats.approval }}</b><span>审批节点</span></div>
|
||||||
|
<div class="ip-stat"><b>{{ stats.notify }}</b><span>抄送节点</span></div>
|
||||||
|
<div class="ip-stat"><b>{{ stats.condition }}</b><span>条件分支</span></div>
|
||||||
|
<div class="ip-stat"><b>{{ stats.parallel }}</b><span>并行分支</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ip-sec">
|
||||||
|
<h4>快捷入口</h4>
|
||||||
|
<div class="ip-quick">
|
||||||
|
<Button size="small" @click="settingsDrawer = true">流程设置</Button>
|
||||||
|
<Button size="small" @click="jsonDrawer = true">源码</Button>
|
||||||
|
<Button size="small" :disabled="!editId" @click="onOpenDiagram">图示预览</Button>
|
||||||
|
</div>
|
||||||
|
<div class="ip-tip">「图示预览」仅已载入的草稿/版本可用(同现状 :disabled="!editId")</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="ed-status" :class="{ bad: countErrors(issues).length }">
|
<footer class="ed-status" :class="{ bad: countErrors(issues).length }">
|
||||||
<span v-if="countErrors(issues).length" class="st-bad">
|
<span v-if="countErrors(issues).length" class="st-bad">
|
||||||
✗ {{ countErrors(issues).length }} 个问题:{{ countErrors(issues).map((i) => i.text).join(";") }}
|
✗ {{ countErrors(issues).length }} 个问题:{{ countErrors(issues).map((i) => i.text).join(";") }}
|
||||||
@@ -645,7 +767,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
padding: 20px 16px 60px;
|
padding: 20px 16px 60px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -756,4 +878,146 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|||||||
color: #b0b6c1;
|
color: #b0b6c1;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 议题一:左画布 + 右信息面板(原型 E1,面板宽 340) */
|
||||||
|
.ed-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.ed-info {
|
||||||
|
width: 340px;
|
||||||
|
flex: none;
|
||||||
|
overflow: auto;
|
||||||
|
border-left: 1px solid #e8eaec;
|
||||||
|
background: #fff;
|
||||||
|
padding: 4px 14px 20px;
|
||||||
|
}
|
||||||
|
.ip-sec {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f1f3;
|
||||||
|
}
|
||||||
|
.ip-sec:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.ip-sec h4 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #17233d;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ip-mini {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a5abb8;
|
||||||
|
}
|
||||||
|
.ip-fld {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.ip-fld label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #808695;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.ip-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.ip-guid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ip-mono {
|
||||||
|
font-family: Consolas, Monaco, "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #515a6e;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.ip-tip {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a5abb8;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.ip-ok {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: #19be6b;
|
||||||
|
}
|
||||||
|
.ip-issue {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 5px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.ip-issue.err {
|
||||||
|
background: #fff5f5;
|
||||||
|
color: #ed4014;
|
||||||
|
}
|
||||||
|
.ip-issue.warn {
|
||||||
|
background: #fff9f0;
|
||||||
|
color: #ff9900;
|
||||||
|
}
|
||||||
|
.ip-issue.loc {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ip-issue.loc:hover {
|
||||||
|
filter: brightness(0.97);
|
||||||
|
}
|
||||||
|
.ip-issue.fix {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.ip-ic {
|
||||||
|
flex: none;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.ip-tx {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.ip-sub {
|
||||||
|
color: #a5abb8;
|
||||||
|
}
|
||||||
|
.ip-go {
|
||||||
|
flex: none;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.ip-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ip-stat {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
background: #f7f9fc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
.ip-stat b {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #17233d;
|
||||||
|
}
|
||||||
|
.ip-stat span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #808695;
|
||||||
|
}
|
||||||
|
.ip-quick {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
/* 校验清单定位高亮(原型 E2,用项目错误红不做原型紫);卡片在子组件内,需 :deep 命中 */
|
||||||
|
:deep(.nd-locate) {
|
||||||
|
box-shadow: 0 0 0 4px rgba(237, 64, 20, 0.18) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user