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:
@@ -55,6 +55,8 @@ const baseGuid = ref("");
|
||||
/** 载入源定义的状态(已发布/停用版本进入编辑器时提示保存会新建草稿版本) */
|
||||
const sourceStatus = ref(0);
|
||||
const sourceVersion = ref(0);
|
||||
/** 源流程线 flowGuid(新建态为空,右面板显示「保存后生成」) */
|
||||
const sourceFlowGuid = ref("");
|
||||
/** 归属应用(flw_def.app_key;按应用筛选维度,空=未分配) */
|
||||
const appKey = ref("");
|
||||
/** 应用清单下拉(应用设置白名单 flw_app enabled=1,显示应用名),拉取失败降级为仅当前值 */
|
||||
@@ -71,6 +73,44 @@ const routers = reactive<RouterOption[]>([]);
|
||||
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
||||
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 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
|
||||
function canonicalJson(content: any): string {
|
||||
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();
|
||||
sourceStatus.value = d?.status ?? 0;
|
||||
sourceVersion.value = d?.version ?? 0;
|
||||
sourceFlowGuid.value = d?.flowGuid || queryGuid || "";
|
||||
// 非草稿进入提示(2026-10-10 议题2):结构改动保存将升版本;仅改名称/归属应用请回列表用「编辑信息」
|
||||
if (d?.status === 1 || d?.status === 2) {
|
||||
Message.info("结构改动保存将升版本;仅改名称/归属应用请返回列表用「编辑信息」(不升版本)");
|
||||
@@ -536,17 +577,98 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main v-if="!loading" class="ed-canvas">
|
||||
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
||||
<div class="ed-flow">
|
||||
<div class="capsule start">发起开始</div>
|
||||
<NodeList :items="model.list" container="root" />
|
||||
<div class="capsule end">流程结束</div>
|
||||
</div>
|
||||
</main>
|
||||
<main v-else-if="loading" class="ed-canvas loading">
|
||||
<Spin size="large" fix />
|
||||
</main>
|
||||
<div class="ed-body">
|
||||
<main v-if="!loading" class="ed-canvas">
|
||||
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
||||
<div class="ed-flow">
|
||||
<div class="capsule start">发起开始</div>
|
||||
<NodeList :items="model.list" container="root" />
|
||||
<div class="capsule end">流程结束</div>
|
||||
</div>
|
||||
</main>
|
||||
<main v-else class="ed-canvas loading">
|
||||
<Spin size="large" fix />
|
||||
</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 }">
|
||||
<span v-if="countErrors(issues).length" class="st-bad">
|
||||
@@ -645,7 +767,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px 16px 60px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
@@ -756,4 +878,146 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
color: #b0b6c1;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user