- 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
234 lines
7.0 KiB
Vue
234 lines
7.0 KiB
Vue
<script setup lang="ts">
|
||
// 条件分支容器(2026-09-24):紫系区分并行橙;与 ParallelBox 的本质差异——无 JOIN 语义,
|
||
// 分支按出口名互斥选路(运行时调判断服务/表达式求值),分支尾隐式汇合到容器后继。
|
||
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约);
|
||
// EXPRESSION 模式(2026-10-10)下分支头部再就地编辑出边 condition,序号即求值优先级。
|
||
import { computed, inject } from "vue";
|
||
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ConditionNode } from "../model";
|
||
import NodeList from "./NodeList.vue";
|
||
|
||
const props = defineProps<{ node: ConditionNode }>();
|
||
|
||
const ctx = inject(EDITOR_CTX)!;
|
||
|
||
const issues = computed(() => ctx.issueOf(props.node.uid));
|
||
const hasError = computed(() => issues.value.some((i) => i.level === "error"));
|
||
const canRemoveBranch = computed(() => props.node.branches.length > MIN_BRANCHES);
|
||
const canAddBranch = computed(() => props.node.branches.length < MAX_BRANCHES);
|
||
const expression = computed(() => props.node.judgeMode === "EXPRESSION");
|
||
|
||
const routerLabel = computed(() => {
|
||
if (expression.value) return "表达式判断(按分支顺序取首个为真)";
|
||
if (!props.node.routerKey) return "判断服务:未选择";
|
||
const r = ctx.routers.find((x) => x.routerKey === props.node.routerKey);
|
||
return r ? `判断服务:${r.name}(${props.node.routerKey})` : `判断服务:${props.node.routerKey}`;
|
||
});
|
||
|
||
/** 出口名就地编辑(重复/空由校验提示;与 branches 下标一一对应) */
|
||
function renameOutlet(i: number, v: string): void {
|
||
props.node.outlets[i] = v;
|
||
}
|
||
|
||
/** 出口条件就地编辑(EXPRESSION 模式;与 outlets 下标一一对应) */
|
||
function editCondition(i: number, v: string): void {
|
||
props.node.conditions = props.node.conditions || [];
|
||
while (props.node.conditions.length <= i) props.node.conditions.push("");
|
||
props.node.conditions[i] = v;
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div :id="'nd-' + node.uid" class="condition-box" :class="{ error: hasError }">
|
||
<div class="cb-title" @click="ctx.openNode(node.uid)">
|
||
条件分支(按出口互斥选路)
|
||
<span class="cb-hint">(点击标题配置{{ expression ? "表达式" : "判断服务" }})</span>
|
||
</div>
|
||
<div class="cb-router">{{ routerLabel }}</div>
|
||
<div class="cb-cols">
|
||
<div v-for="(branch, i) in node.branches" :key="i" class="cb-col">
|
||
<div class="cb-col-head">
|
||
<span class="cb-col-no" :title="expression ? '表达式求值优先级' : undefined">
|
||
{{ BRANCH_LABELS[i] || i + 1 }}
|
||
</span>
|
||
<input
|
||
class="cb-outlet"
|
||
:value="node.outlets[i]"
|
||
placeholder="出口名(必填唯一)"
|
||
maxlength="32"
|
||
@input="renameOutlet(i, ($event.target as HTMLInputElement).value)"
|
||
/>
|
||
<Poptip
|
||
v-if="canRemoveBranch"
|
||
confirm
|
||
title="删除该分支及其节点?"
|
||
ok-text="删除"
|
||
cancel-text="取消"
|
||
transfer
|
||
@on-ok="ctx.removeBranch(node.uid, i)"
|
||
>
|
||
<span class="cb-col-del" title="删除分支">×</span>
|
||
</Poptip>
|
||
</div>
|
||
<input
|
||
v-if="expression"
|
||
class="cb-cond"
|
||
:value="(node.conditions || [])[i] || ''"
|
||
placeholder="条件表达式,如 amount > 1000"
|
||
maxlength="200"
|
||
@input="editCondition(i, ($event.target as HTMLInputElement).value)"
|
||
/>
|
||
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||
<div v-if="!branch.length" class="cb-empty">该出口直达汇合点(直接通过)</div>
|
||
</div>
|
||
</div>
|
||
<Button v-if="canAddBranch" class="cb-add" size="small" long @click="ctx.addBranch(node.uid)">
|
||
+ 添加分支
|
||
</Button>
|
||
<div v-for="(issue, k) in issues" :key="k" class="cb-issue" :class="issue.level">⚠ {{ issue.text }}</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.condition-box {
|
||
border: 1.5px dashed #b37feb;
|
||
border-radius: 10px;
|
||
padding: 14px 14px 12px;
|
||
background: rgba(249, 240, 255, 0.45);
|
||
}
|
||
.condition-box.error {
|
||
border-color: #ed4014;
|
||
}
|
||
.cb-title {
|
||
text-align: center;
|
||
color: #722ed1;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
}
|
||
.cb-title:hover {
|
||
text-decoration: underline;
|
||
}
|
||
.cb-hint {
|
||
font-weight: 400;
|
||
font-size: 11px;
|
||
color: #b37feb;
|
||
}
|
||
.cb-router {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #9254de;
|
||
margin: 6px 0 10px;
|
||
}
|
||
.cb-empty {
|
||
margin: 4px 0 8px;
|
||
padding: 8px 6px;
|
||
border: 1px dashed #d3c6e8;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-size: 11px;
|
||
color: #9254de;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
}
|
||
.cb-cols {
|
||
display: flex;
|
||
gap: 14px;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
}
|
||
.cb-col {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 12px 10px 14px;
|
||
min-width: 230px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.cb-col-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
width: 100%;
|
||
margin-bottom: 8px;
|
||
padding: 0 2px;
|
||
}
|
||
.cb-col-no {
|
||
flex: none;
|
||
color: #9254de;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
.cb-outlet {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border: 1px solid #e3e5ee;
|
||
border-radius: 6px;
|
||
padding: 2px 8px;
|
||
font-size: 12px;
|
||
color: #722ed1;
|
||
font-weight: 600;
|
||
}
|
||
.cb-outlet:focus {
|
||
outline: none;
|
||
border-color: #9254de;
|
||
}
|
||
.cb-cond {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
border: 1px dashed #d3b8f9;
|
||
border-radius: 6px;
|
||
padding: 2px 8px;
|
||
font-size: 12px;
|
||
color: #5b3fa8;
|
||
font-family: Consolas, Monaco, monospace;
|
||
margin-bottom: 8px;
|
||
}
|
||
.cb-cond:focus {
|
||
outline: none;
|
||
border-color: #9254de;
|
||
border-style: solid;
|
||
}
|
||
.cb-cond::placeholder {
|
||
color: #c5b3e8;
|
||
}
|
||
.cb-col-del {
|
||
flex: none;
|
||
cursor: pointer;
|
||
color: #c5c9d1;
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
}
|
||
.cb-col-del:hover {
|
||
color: #ed4014;
|
||
}
|
||
.cb-add {
|
||
margin-top: 12px;
|
||
border-style: dashed;
|
||
color: #9254de;
|
||
border-color: #9254de;
|
||
background: transparent;
|
||
width: auto;
|
||
padding: 0 16px;
|
||
display: block;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
.cb-add:hover {
|
||
background: #f9f0ff;
|
||
color: #722ed1;
|
||
border-color: #9254de;
|
||
}
|
||
.cb-issue {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
padding-top: 8px;
|
||
}
|
||
.cb-issue.error {
|
||
color: #ed4014;
|
||
}
|
||
.cb-issue.warn {
|
||
color: #ff9900;
|
||
}
|
||
</style>
|