feat(condition): 编辑器支持表达式判断——判断方式切换 + 出口条件就地编辑 + 表达式调试
- ConditionNode 增 judgeMode/conditions(SERVICE 缺省,旧定义天然兼容);DefEdgeJson/DefNodeJson 透传 condition/judgeMode - convert.ts 双向透传:CONDITION 分支首边带 condition,序列化顺序=分支顺序(求值优先级保序) - ConditionDrawer 判断方式 Radio(服务/表达式);ConditionBox 表达式模式分支头部条件输入(序号即优先级) - validate.ts 镜像校验:表达式模式每出口条件必填(语法由后端发布期拦) - Routers 页加「表达式调试」Modal(/admin/router/eval,与线上同一求值核心) - addBranch/removeBranch 同步维护 conditions 占位
This commit is contained in:
@@ -236,6 +236,10 @@ class FlowApi {
|
|||||||
instanceReroute(instanceId: number | string) {
|
instanceReroute(instanceId: number | string) {
|
||||||
return post(`${ROOT}/admin/instance/${instanceId}/reroute`);
|
return post(`${ROOT}/admin/instance/${instanceId}/reroute`);
|
||||||
}
|
}
|
||||||
|
/** 表达式调试求值(2026-10-10:与运行时同一求值核心;不落库不走流程) */
|
||||||
|
routerEval(body: { expression: string; variablesJson?: string }) {
|
||||||
|
return post(`${ROOT}/admin/router/eval`, body);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 管理面:办件查询 ====================
|
// ==================== 管理面:办件查询 ====================
|
||||||
adminInstances(params: { status?: number; keyword?: string; page?: number; size?: number }) {
|
adminInstances(params: { status?: number; keyword?: string; page?: number; size?: number }) {
|
||||||
|
|||||||
@@ -131,6 +131,35 @@ function onEnable(row: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 表达式调试(2026-10-10 四议题):与运行时同一求值核心(/admin/router/eval) ----
|
||||||
|
const evalVisible = ref(false);
|
||||||
|
const evaluating = ref(false);
|
||||||
|
const evalForm = ref<{ expression: string; variablesJson: string }>({ expression: "", variablesJson: "" });
|
||||||
|
const evalResult = ref<{ result: boolean; value?: string; error?: string } | null>(null);
|
||||||
|
|
||||||
|
function onEval() {
|
||||||
|
evalForm.value = { expression: "", variablesJson: "" };
|
||||||
|
evalResult.value = null;
|
||||||
|
evalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doEval() {
|
||||||
|
if (!evalForm.value.expression.trim()) {
|
||||||
|
Message.warning("请填写表达式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
evaluating.value = true;
|
||||||
|
evalResult.value = null;
|
||||||
|
try {
|
||||||
|
evalResult.value = (await flowApi.routerEval({
|
||||||
|
expression: evalForm.value.expression.trim(),
|
||||||
|
variablesJson: evalForm.value.variablesJson.trim(),
|
||||||
|
}).send()) || null;
|
||||||
|
} finally {
|
||||||
|
evaluating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -139,6 +168,7 @@ onMounted(load);
|
|||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="fix mb-12">
|
<div class="fix mb-12">
|
||||||
<Button class="fl" type="primary" size="large" @click="onAdd">新增注册</Button>
|
<Button class="fl" type="primary" size="large" @click="onAdd">新增注册</Button>
|
||||||
|
<Button class="fl" size="large" style="margin-left: 8px" @click="onEval">表达式调试</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-wrap fix">
|
<div class="search-wrap fix">
|
||||||
@@ -177,6 +207,35 @@ onMounted(load);
|
|||||||
<Button type="primary" :loading="saving" @click="onSave">提交</Button>
|
<Button type="primary" :loading="saving" @click="onSave">提交</Button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal v-model="evalVisible" title="表达式调试" :mask-closable="false" :width="560">
|
||||||
|
<p style="color: #808695; font-size: 12px; line-height: 1.6; margin-bottom: 10px">
|
||||||
|
与 CONDITION 节点 EXPRESSION 模式同一套求值(AviatorScript 沙箱)。变量 = 样例 JSON 的
|
||||||
|
键 + 线上另注入 schId/bizType/bizId/submitterId/instanceId/instanceVersion/nodeId/nodeName
|
||||||
|
(调试时请在样例里自行提供内置变量取值)。变量未提供会报错——线上同口径(宁挂起不误放行)
|
||||||
|
</p>
|
||||||
|
<Input v-model="evalForm.expression" placeholder="表达式,如 amount > 1000" :maxlength="200"
|
||||||
|
style="margin-bottom: 8px" />
|
||||||
|
<Input v-model="evalForm.variablesJson" type="textarea" :rows="5"
|
||||||
|
placeholder='样例变量 JSON,如 {"amount": 2000, "bizType": "leave"}' />
|
||||||
|
<div v-if="evalResult" style="margin-top: 10px; font-size: 13px">
|
||||||
|
<template v-if="evalResult.error">
|
||||||
|
<span style="color: #ed4014">✗ 求值失败:{{ evalResult.error }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span :style="{ color: evalResult.result ? '#19be6b' : '#ff9900' }">
|
||||||
|
{{ evalResult.result ? "✓ true(命中该出口)" : "✗ false(不命中)" }}
|
||||||
|
</span>
|
||||||
|
<span v-if="evalResult.value !== undefined" style="color: #808695; margin-left: 8px">
|
||||||
|
求值值:{{ evalResult.value }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="evalVisible = false">关闭</Button>
|
||||||
|
<Button type="primary" :loading="evaluating" @click="doEval">求值</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 条件分支容器(2026-09-24):紫系区分并行橙;与 ParallelBox 的本质差异——无 JOIN 语义,
|
// 条件分支容器(2026-09-24):紫系区分并行橙;与 ParallelBox 的本质差异——无 JOIN 语义,
|
||||||
// 分支按出口名互斥选路(运行时调判断服务),分支尾隐式汇合到容器后继。
|
// 分支按出口名互斥选路(运行时调判断服务/表达式求值),分支尾隐式汇合到容器后继。
|
||||||
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约)。
|
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约);
|
||||||
|
// EXPRESSION 模式(2026-10-10)下分支头部再就地编辑出边 condition,序号即求值优先级。
|
||||||
import { computed, inject } from "vue";
|
import { computed, inject } from "vue";
|
||||||
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ConditionNode } from "../model";
|
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ConditionNode } from "../model";
|
||||||
import NodeList from "./NodeList.vue";
|
import NodeList from "./NodeList.vue";
|
||||||
@@ -14,8 +15,10 @@ const issues = computed(() => ctx.issueOf(props.node.uid));
|
|||||||
const hasError = computed(() => issues.value.some((i) => i.level === "error"));
|
const hasError = computed(() => issues.value.some((i) => i.level === "error"));
|
||||||
const canRemoveBranch = computed(() => props.node.branches.length > MIN_BRANCHES);
|
const canRemoveBranch = computed(() => props.node.branches.length > MIN_BRANCHES);
|
||||||
const canAddBranch = computed(() => props.node.branches.length < MAX_BRANCHES);
|
const canAddBranch = computed(() => props.node.branches.length < MAX_BRANCHES);
|
||||||
|
const expression = computed(() => props.node.judgeMode === "EXPRESSION");
|
||||||
|
|
||||||
const routerLabel = computed(() => {
|
const routerLabel = computed(() => {
|
||||||
|
if (expression.value) return "表达式判断(按分支顺序取首个为真)";
|
||||||
if (!props.node.routerKey) return "判断服务:未选择";
|
if (!props.node.routerKey) return "判断服务:未选择";
|
||||||
const r = ctx.routers.find((x) => x.routerKey === props.node.routerKey);
|
const r = ctx.routers.find((x) => x.routerKey === props.node.routerKey);
|
||||||
return r ? `判断服务:${r.name}(${props.node.routerKey})` : `判断服务:${props.node.routerKey}`;
|
return r ? `判断服务:${r.name}(${props.node.routerKey})` : `判断服务:${props.node.routerKey}`;
|
||||||
@@ -25,19 +28,28 @@ const routerLabel = computed(() => {
|
|||||||
function renameOutlet(i: number, v: string): void {
|
function renameOutlet(i: number, v: string): void {
|
||||||
props.node.outlets[i] = v;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="condition-box" :class="{ error: hasError }">
|
<div 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">(点击标题配置判断服务)</span>
|
<span class="cb-hint">(点击标题配置{{ expression ? "表达式" : "判断服务" }})</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cb-router">{{ routerLabel }}</div>
|
<div class="cb-router">{{ routerLabel }}</div>
|
||||||
<div class="cb-cols">
|
<div class="cb-cols">
|
||||||
<div v-for="(branch, i) in node.branches" :key="i" class="cb-col">
|
<div v-for="(branch, i) in node.branches" :key="i" class="cb-col">
|
||||||
<div class="cb-col-head">
|
<div class="cb-col-head">
|
||||||
<span class="cb-col-no">{{ BRANCH_LABELS[i] || i + 1 }}</span>
|
<span class="cb-col-no" :title="expression ? '表达式求值优先级' : undefined">
|
||||||
|
{{ BRANCH_LABELS[i] || i + 1 }}
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
class="cb-outlet"
|
class="cb-outlet"
|
||||||
:value="node.outlets[i]"
|
:value="node.outlets[i]"
|
||||||
@@ -57,6 +69,14 @@ function renameOutlet(i: number, v: string): void {
|
|||||||
<span class="cb-col-del" title="删除分支">×</span>
|
<span class="cb-col-del" title="删除分支">×</span>
|
||||||
</Poptip>
|
</Poptip>
|
||||||
</div>
|
</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}`" />
|
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,6 +162,25 @@ function renameOutlet(i: number, v: string): void {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: #9254de;
|
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 {
|
.cb-col-del {
|
||||||
flex: none;
|
flex: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 条件分支配置抽屉(2026-09-24):结构与 NotifyDrawer 同源(本地表单 + 确定回写)。
|
// 条件分支配置抽屉(2026-09-24):结构与 NotifyDrawer 同源(本地表单 + 确定回写)。
|
||||||
// 只管节点名与判断服务(flw_router 下拉);出口命名/分支编排 ConditionBox 就地做。
|
// 管节点名与判断方式(2026-10-10 二选一:服务/表达式);出口命名/条件表达式/分支编排 ConditionBox 就地做。
|
||||||
import { computed, inject, reactive, watch } from "vue";
|
import { computed, inject, reactive, watch } from "vue";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import { ConditionNode, EDITOR_CTX } from "../model";
|
import { ConditionNode, EDITOR_CTX, JudgeMode } from "../model";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
@@ -21,6 +21,7 @@ const ctx = inject(EDITOR_CTX)!;
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
routerKey: "",
|
routerKey: "",
|
||||||
|
judgeMode: "SERVICE" as JudgeMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
function syncFromNode(): void {
|
function syncFromNode(): void {
|
||||||
@@ -28,6 +29,7 @@ function syncFromNode(): void {
|
|||||||
if (!n) return;
|
if (!n) return;
|
||||||
form.name = n.name;
|
form.name = n.name;
|
||||||
form.routerKey = n.routerKey || "";
|
form.routerKey = n.routerKey || "";
|
||||||
|
form.judgeMode = n.judgeMode === "EXPRESSION" ? "EXPRESSION" : "SERVICE";
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -56,6 +58,16 @@ const routerHint = computed(() => {
|
|||||||
return `调用 ${r.serviceKey}${r.path};返回出口名即分支走向`;
|
return `调用 ${r.serviceKey}${r.path};返回出口名即分支走向`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 切换判断方式:表达式模式下 conditions 补齐占位(与 outlets 同位) */
|
||||||
|
function switchJudgeMode(mode: JudgeMode): void {
|
||||||
|
form.judgeMode = mode;
|
||||||
|
const n = props.node;
|
||||||
|
if (n && mode === "EXPRESSION") {
|
||||||
|
n.conditions = n.conditions || [];
|
||||||
|
while (n.conditions.length < n.outlets.length) n.conditions.push("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onOk(): void {
|
function onOk(): void {
|
||||||
const n = props.node;
|
const n = props.node;
|
||||||
if (!n) {
|
if (!n) {
|
||||||
@@ -66,12 +78,17 @@ function onOk(): void {
|
|||||||
Message.warning("节点名称不能为空");
|
Message.warning("节点名称不能为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.routerKey) {
|
if (form.judgeMode === "SERVICE" && !form.routerKey) {
|
||||||
Message.warning("请选择判断服务(未配置的判断服务运行到该节点会挂起)");
|
Message.warning("请选择判断服务(未配置的判断服务运行到该节点会挂起)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
n.name = form.name.trim();
|
n.name = form.name.trim();
|
||||||
n.routerKey = form.routerKey;
|
n.routerKey = form.judgeMode === "SERVICE" ? form.routerKey : "";
|
||||||
|
n.judgeMode = form.judgeMode;
|
||||||
|
if (form.judgeMode === "EXPRESSION") {
|
||||||
|
n.conditions = n.conditions || [];
|
||||||
|
while (n.conditions.length < n.outlets.length) n.conditions.push("");
|
||||||
|
}
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +113,17 @@ function onRemove(): void {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fld">
|
<div class="fld">
|
||||||
|
<label>判断方式</label>
|
||||||
|
<RadioGroup :model-value="form.judgeMode" @on-change="switchJudgeMode">
|
||||||
|
<Radio label="SERVICE">服务(外部判断接口)</Radio>
|
||||||
|
<Radio label="EXPRESSION">表达式</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<div class="hint">
|
||||||
|
简单条件用表达式在编辑器内闭环;复杂判断(查库/调外部系统)沿用注册的判断服务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="form.judgeMode === 'SERVICE'" class="fld">
|
||||||
<label>判断服务</label>
|
<label>判断服务</label>
|
||||||
<Select v-model="form.routerKey" filterable placeholder="请选择判断服务(flw_router 注册)">
|
<Select v-model="form.routerKey" filterable placeholder="请选择判断服务(flw_router 注册)">
|
||||||
<Option v-for="o in routerOptions" :key="o.value" :value="o.value" :label="o.label" />
|
<Option v-for="o in routerOptions" :key="o.value" :value="o.value" :label="o.label" />
|
||||||
@@ -108,6 +136,15 @@ function onRemove(): void {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="fld">
|
||||||
|
<label>表达式说明</label>
|
||||||
|
<div class="hint">
|
||||||
|
每个出口在分支列头部就地填写 AviatorScript 条件(如 amount > 1000);按分支顺序取
|
||||||
|
首个为真的出口。变量=表单字段 + 内置(schId/bizType/bizId/submitterId/instanceId 等)。
|
||||||
|
变量未提供或条件全不为真时流程挂起(不配默认出口),可在"条件路由"页调试表达式
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="fld">
|
<div class="fld">
|
||||||
<label>出口与分支</label>
|
<label>出口与分支</label>
|
||||||
<div class="hint">
|
<div class="hint">
|
||||||
|
|||||||
@@ -43,15 +43,19 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
* 条件分支(2026-09-24)无 JOIN 语义:分支尾不互连,隐式汇合到容器后继——cursor 置 null 后
|
* 条件分支(2026-09-24)无 JOIN 语义:分支尾不互连,隐式汇合到容器后继——cursor 置 null 后
|
||||||
* 下一个节点(或 END)只接收各分支尾的连线;firstEdgeName 只作用于分支首边(出口名)。
|
* 下一个节点(或 END)只接收各分支尾的连线;firstEdgeName 只作用于分支首边(出口名)。
|
||||||
* walk 返回"尾集合"(普通序列=末节点,末尾含条件分支=各分支尾)。
|
* walk 返回"尾集合"(普通序列=末节点,末尾含条件分支=各分支尾)。
|
||||||
|
* firstEdgeCondition(2026-10-10):EXPRESSION 模式分支首边的条件表达式,与出口名同位写入。
|
||||||
*/
|
*/
|
||||||
const walk = (items: EditorItem[], prev: string, firstEdgeName?: string): string[] => {
|
const walk = (items: EditorItem[], prev: string, firstEdgeName?: string,
|
||||||
|
firstEdgeCondition?: string): string[] => {
|
||||||
let cursor: string | null = prev;
|
let cursor: string | null = prev;
|
||||||
let pending: string[] = [];
|
let pending: string[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
const linkTo = (toId: string): void => {
|
const linkTo = (toId: string): void => {
|
||||||
if (cursor !== null) {
|
if (cursor !== null) {
|
||||||
if (first && firstEdgeName) {
|
if (first && firstEdgeName) {
|
||||||
edges.push({ from: cursor, to: toId, name: firstEdgeName });
|
const e: DefEdgeJson = { from: cursor, to: toId, name: firstEdgeName };
|
||||||
|
if (firstEdgeCondition !== undefined) e.condition = firstEdgeCondition;
|
||||||
|
edges.push(e);
|
||||||
} else {
|
} else {
|
||||||
edges.push({ from: cursor, to: toId });
|
edges.push({ from: cursor, to: toId });
|
||||||
}
|
}
|
||||||
@@ -102,16 +106,21 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
}
|
}
|
||||||
if (item.kind === "condition") {
|
if (item.kind === "condition") {
|
||||||
const condId = `${item.uid}c`;
|
const condId = `${item.uid}c`;
|
||||||
nodes.push({
|
const node: DefNodeJson = {
|
||||||
id: condId,
|
id: condId,
|
||||||
type: "CONDITION",
|
type: "CONDITION",
|
||||||
name: item.name || "条件判断",
|
name: item.name || "条件判断",
|
||||||
routerKey: item.routerKey || "",
|
routerKey: item.routerKey || "",
|
||||||
});
|
};
|
||||||
|
// SERVICE 是后端缺省,不写字段保持最小 JSON;EXPRESSION 显式携带
|
||||||
|
if (item.judgeMode === "EXPRESSION") node.judgeMode = "EXPRESSION";
|
||||||
|
nodes.push(node);
|
||||||
linkTo(condId);
|
linkTo(condId);
|
||||||
const tails: string[] = [];
|
const tails: string[] = [];
|
||||||
|
const expression = item.judgeMode === "EXPRESSION";
|
||||||
item.branches.forEach((branch, bi) => {
|
item.branches.forEach((branch, bi) => {
|
||||||
tails.push(...walk(branch, condId, item.outlets[bi]));
|
const cond = expression ? (item.conditions || [])[bi] || "" : undefined;
|
||||||
|
tails.push(...walk(branch, condId, item.outlets[bi], cond));
|
||||||
});
|
});
|
||||||
cursor = null;
|
cursor = null;
|
||||||
pending = tails;
|
pending = tails;
|
||||||
@@ -311,12 +320,19 @@ export function fromContentJson(def: any): ParsedDef {
|
|||||||
// uid 复用 JSON id 并剥掉生成态的 "c" 尾缀:uid+"c" 是本编辑器的 JSON id 生成约定,
|
// uid 复用 JSON id 并剥掉生成态的 "c" 尾缀:uid+"c" 是本编辑器的 JSON id 生成约定,
|
||||||
// 剥尾保证"图→树→图→树→图"的 id 收敛(否则每轮追加一个 c,往返不幂等)
|
// 剥尾保证"图→树→图→树→图"的 id 收敛(否则每轮追加一个 c,往返不幂等)
|
||||||
const uidRaw = condId.endsWith("c") ? condId.slice(0, -1) : condId;
|
const uidRaw = condId.endsWith("c") ? condId.slice(0, -1) : condId;
|
||||||
|
// 出口条件反解(2026-10-10):EXPRESSION 模式出边 condition 与 outlets 同位
|
||||||
|
const conditions = heads.map((head) => {
|
||||||
|
const e = edges.find((x) => x.from === condId && x.to === head);
|
||||||
|
return e && e.condition ? String(e.condition) : "";
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
item: {
|
item: {
|
||||||
uid: UsedIds.take(uidRaw),
|
uid: UsedIds.take(uidRaw),
|
||||||
kind: "condition",
|
kind: "condition",
|
||||||
name: n.name || "条件判断",
|
name: n.name || "条件判断",
|
||||||
routerKey: n.routerKey || "",
|
routerKey: n.routerKey || "",
|
||||||
|
judgeMode: n.judgeMode === "EXPRESSION" ? "EXPRESSION" : "SERVICE",
|
||||||
|
conditions,
|
||||||
outlets,
|
outlets,
|
||||||
branches: branches.map((b) => (b.length ? b : [createApproval()])),
|
branches: branches.map((b) => (b.length ? b : [createApproval()])),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -287,6 +287,11 @@ function addBranch(containerUid: string): void {
|
|||||||
const no = item.branches.length + 1;
|
const no = item.branches.length + 1;
|
||||||
item.branches.push([createApproval()]);
|
item.branches.push([createApproval()]);
|
||||||
item.outlets.push(`出口${["一", "二", "三", "四", "五", "六"][no - 1] || no}`);
|
item.outlets.push(`出口${["一", "二", "三", "四", "五", "六"][no - 1] || no}`);
|
||||||
|
// EXPRESSION 模式条件与 outlets 同位(SERVICE 模式不写,序列化时不携带)
|
||||||
|
if (item.judgeMode === "EXPRESSION") {
|
||||||
|
item.conditions = item.conditions || [];
|
||||||
|
while (item.conditions.length < item.outlets.length) item.conditions.push("");
|
||||||
|
}
|
||||||
} else if (item.kind === "approval" && item.outlets && item.outlets.length >= 2) {
|
} else if (item.kind === "approval" && item.outlets && item.outlets.length >= 2) {
|
||||||
const no = (item.branches?.length || 0) + 1;
|
const no = (item.branches?.length || 0) + 1;
|
||||||
(item.branches = item.branches || []).push([createApproval()]);
|
(item.branches = item.branches || []).push([createApproval()]);
|
||||||
@@ -305,6 +310,7 @@ function removeBranch(containerUid: string, index: number): void {
|
|||||||
}
|
}
|
||||||
branches.splice(index, 1);
|
branches.splice(index, 1);
|
||||||
if (item.kind === "condition" || isOutlet) item.outlets!.splice(index, 1);
|
if (item.kind === "condition" || isOutlet) item.outlets!.splice(index, 1);
|
||||||
|
if (item.kind === "condition" && item.conditions) item.conditions.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 退出多出口:仅保留出口①泳道内容,替换为「本节点(去 outlets) + ①泳道节点序列」 */
|
/** 退出多出口:仅保留出口①泳道内容,替换为「本节点(去 outlets) + ①泳道节点序列」 */
|
||||||
|
|||||||
@@ -49,17 +49,25 @@ export interface ParallelNode {
|
|||||||
branches: EditorItem[][];
|
branches: EditorItem[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** CONDITION 判断方式(2026-10-10 四议题):SERVICE=外部判断服务(缺省)|EXPRESSION=内置表达式引擎 */
|
||||||
|
export type JudgeMode = "SERVICE" | "EXPRESSION";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 条件分支容器(2026-09-24 CONDITION):运行时同步调外部判断服务(flw_router,routerKey 寻址)
|
* 条件分支容器(2026-09-24 CONDITION):按返回出口名互斥选路——与并行的本质差异:无 JOIN 语义,
|
||||||
* 按返回出口名互斥选路——与并行的本质差异:无 JOIN 语义,分支尾隐式汇合到容器后继。
|
* 分支尾隐式汇合到容器后继。outlets[i] 即第 i 条分支首边的 name(判断接口的 outlets 契约),
|
||||||
* outlets[i] 即第 i 条分支首边的 name(判断接口的 outlets 契约),必须非空且唯一。
|
* 必须非空且唯一。判断方式二选一(2026-10-10):SERVICE 调 flw_router 注册的外部判断服务;
|
||||||
|
* EXPRESSION 用出边 condition 表达式(AviatorScript,按分支顺序取首个 true)。
|
||||||
*/
|
*/
|
||||||
export interface ConditionNode {
|
export interface ConditionNode {
|
||||||
uid: string;
|
uid: string;
|
||||||
kind: "condition";
|
kind: "condition";
|
||||||
name: string;
|
name: string;
|
||||||
/** 判断服务注册键(flw_router.router_key,发布校验须已注册且启用) */
|
/** 判断服务注册键(flw_router.router_key,SERVICE 模式发布校验须已注册且启用) */
|
||||||
routerKey: string;
|
routerKey: string;
|
||||||
|
/** 判断方式:缺省 SERVICE(旧定义无此字段) */
|
||||||
|
judgeMode?: JudgeMode;
|
||||||
|
/** EXPRESSION 模式各出口条件表达式(与 outlets 一一对应;SERVICE 模式无意义) */
|
||||||
|
conditions?: string[];
|
||||||
/** 各分支出口名(与 branches 一一对应) */
|
/** 各分支出口名(与 branches 一一对应) */
|
||||||
outlets: string[];
|
outlets: string[];
|
||||||
branches: EditorItem[][];
|
branches: EditorItem[][];
|
||||||
@@ -104,6 +112,8 @@ export interface DefNodeJson {
|
|||||||
receivers?: Assignee;
|
receivers?: Assignee;
|
||||||
/** CONDITION 节点的判断服务注册键(flw_router.router_key) */
|
/** CONDITION 节点的判断服务注册键(flw_router.router_key) */
|
||||||
routerKey?: string;
|
routerKey?: string;
|
||||||
|
/** CONDITION 判断方式:SERVICE(缺省)|EXPRESSION(出边 condition 表达式) */
|
||||||
|
judgeMode?: string;
|
||||||
countersign?: Countersign;
|
countersign?: Countersign;
|
||||||
waitFor?: string;
|
waitFor?: string;
|
||||||
deadlineHours?: number;
|
deadlineHours?: number;
|
||||||
@@ -113,6 +123,8 @@ export interface DefEdgeJson {
|
|||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
/** CONDITION EXPRESSION 模式的出口条件表达式(AviatorScript;SERVICE 模式不携带) */
|
||||||
|
condition?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefJson {
|
export interface DefJson {
|
||||||
@@ -165,13 +177,15 @@ export function createParallel(): ParallelNode {
|
|||||||
return { uid: newUid(), kind: "parallel", branches: [[createApproval()], [createApproval()]] };
|
return { uid: newUid(), kind: "parallel", branches: [[createApproval()], [createApproval()]] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 条件分支:默认 2 分支占位出口名,插入后弹配置抽屉选判断服务 */
|
/** 条件分支:默认 2 分支占位出口名,插入后弹配置抽屉选判断方式(缺省服务) */
|
||||||
export function createCondition(): ConditionNode {
|
export function createCondition(): ConditionNode {
|
||||||
return {
|
return {
|
||||||
uid: newUid(),
|
uid: newUid(),
|
||||||
kind: "condition",
|
kind: "condition",
|
||||||
name: "条件判断",
|
name: "条件判断",
|
||||||
routerKey: "",
|
routerKey: "",
|
||||||
|
judgeMode: "SERVICE",
|
||||||
|
conditions: ["", ""],
|
||||||
outlets: ["出口一", "出口二"],
|
outlets: ["出口一", "出口二"],
|
||||||
branches: [[createApproval()], [createApproval()]],
|
branches: [[createApproval()], [createApproval()]],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,9 +103,19 @@ export function validateModel(model: EditorModel, depts: DeptOption[], routers:
|
|||||||
|
|
||||||
if (item.kind === "condition") {
|
if (item.kind === "condition") {
|
||||||
// 条件分支(2026-09-24):判断服务必选(未注册仅 warn,发布期后端权威拦截);
|
// 条件分支(2026-09-24):判断服务必选(未注册仅 warn,发布期后端权威拦截);
|
||||||
// 出口名即判断接口 outlets 契约,非空唯一(镜像后端 CONDITION 校验口径)
|
// 出口名即判断接口 outlets 契约,非空唯一(镜像后端 CONDITION 校验口径)。
|
||||||
|
// EXPRESSION(2026-10-10):每出口条件非空(语法后端拦,前端不重复实现)
|
||||||
if (!item.name.trim()) push(item.uid, "error", path, "节点名称不能为空");
|
if (!item.name.trim()) push(item.uid, "error", path, "节点名称不能为空");
|
||||||
if (!item.routerKey) {
|
const expression = item.judgeMode === "EXPRESSION";
|
||||||
|
if (expression) {
|
||||||
|
item.outlets.forEach((o, i) => {
|
||||||
|
const cond = (item.conditions || [])[i] || "";
|
||||||
|
if (!cond.trim()) {
|
||||||
|
push(item.uid, "error", path,
|
||||||
|
`出口「${o || i + 1}」未配条件表达式(表达式模式下每个出口必须配置,发布会被拦截)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!item.routerKey) {
|
||||||
push(item.uid, "error", path, "未选择判断服务(发布后运行到该节点会挂起)");
|
push(item.uid, "error", path, "未选择判断服务(发布后运行到该节点会挂起)");
|
||||||
} else if (routers.length && !routers.some((r) => r.routerKey === item.routerKey && r.status === 1)) {
|
} else if (routers.length && !routers.some((r) => r.routerKey === item.routerKey && r.status === 1)) {
|
||||||
push(item.uid, "warn", path, `判断服务「${item.routerKey}」未注册或已停用,发布会被拦截`);
|
push(item.uid, "warn", path, `判断服务「${item.routerKey}」未注册或已停用,发布会被拦截`);
|
||||||
|
|||||||
Reference in New Issue
Block a user