feat(admin): 抄送(NOTIFY)与条件分支(CONDITION)全栈接入编辑器/轨迹/管理面
build / build (push) Successful in 1m55s

NOTIFY(2026-09-23):编辑器抄送卡片+抽屉+画布 bpmn:ServiceTask 映射、工作台「抄送我的」页签、轨迹 kind=CC 行、示意图青框卡片
CONDITION(2026-09-24):ConditionBox 互斥分支容器(分支列出口名就地编辑、无 JOIN 语义分支尾隐式汇合)、ConditionDrawer 判断服务下拉(flw_router)、convert 双向序列化(CONDITION+命名出边、uid+c 尾缀往返收敛)、validate 镜像后端口径、画布 bpmn:ExclusiveGateway 映射、轨迹「系统判断→出口」行(失败挂起提示)、示意图紫菱形(SVG 与服务端 PNG 同配色)、管理面 flw_router 维护页(/admin/routers)与办件记录「重跑路由」入口
convert 往返 54 项全过、npm run build 通过
This commit is contained in:
chenxing
2026-09-24 01:19:15 +08:00
parent cdf04f0930
commit e30e7ab665
20 changed files with 1614 additions and 84 deletions
@@ -0,0 +1,164 @@
<script setup lang="ts">
// 条件分支配置抽屉(2026-09-24):结构与 NotifyDrawer 同源(本地表单 + 确定回写)。
// 只管节点名与判断服务(flw_router 下拉);出口命名/分支编排 ConditionBox 就地做。
import { computed, inject, reactive, watch } from "vue";
import { Message } from "view-ui-plus";
import { ConditionNode, EDITOR_CTX } from "../model";
const props = defineProps<{
modelValue: boolean;
node: ConditionNode | null;
}>();
const emit = defineEmits<{
"update:modelValue": [boolean];
/** 节点已删除(由抽屉内删除按钮触发),由外层关闭抽屉并重算校验 */
removed: [];
}>();
const ctx = inject(EDITOR_CTX)!;
const form = reactive({
name: "",
routerKey: "",
});
function syncFromNode(): void {
const n = props.node;
if (!n) return;
form.name = n.name;
form.routerKey = n.routerKey || "";
}
watch(
() => [props.modelValue, props.node?.uid],
() => {
if (props.modelValue) syncFromNode();
},
{ immediate: true }
);
/** 判断服务下拉:仅启用中的注册(ctx.routers 由编辑器载入);当前值兜底防选项丢失 */
const routerOptions = computed(() => {
const list = ctx.routers
.filter((r) => r.status === 1)
.map((r) => ({ value: r.routerKey, label: `${r.name}(${r.routerKey})` }));
if (form.routerKey && !list.some((o) => o.value === form.routerKey)) {
list.unshift({ value: form.routerKey, label: `${form.routerKey}(未注册或已停用)` });
}
return list;
});
const routerHint = computed(() => {
if (!form.routerKey) return "";
const r = ctx.routers.find((x) => x.routerKey === form.routerKey);
if (!r) return `判断服务「${form.routerKey}」未注册或已停用,发布会被拦截`;
return `调用 ${r.serviceKey}${r.path};返回出口名即分支走向`;
});
function onOk(): void {
const n = props.node;
if (!n) {
emit("update:modelValue", false);
return;
}
if (!form.name.trim()) {
Message.warning("节点名称不能为空");
return;
}
if (!form.routerKey) {
Message.warning("请选择判断服务(未配置的判断服务运行到该节点会挂起)");
return;
}
n.name = form.name.trim();
n.routerKey = form.routerKey;
emit("update:modelValue", false);
}
function onRemove(): void {
if (props.node) ctx.removeNode(props.node.uid);
emit("removed");
}
</script>
<template>
<Drawer
:model-value="modelValue"
title="配置条件分支"
:width="430"
:mask-closable="false"
@update:model-value="emit('update:modelValue', $event)"
>
<Form :label-width="0" class="dw-form">
<div class="fld">
<label>节点名称</label>
<Input v-model="form.name" placeholder="如:发文部门判断" :maxlength="30" />
</div>
<div class="fld">
<label>判断服务</label>
<Select v-model="form.routerKey" filterable placeholder="请选择判断服务(flw_router 注册)">
<Option v-for="o in routerOptions" :key="o.value" :value="o.value" :label="o.label" />
</Select>
<div v-if="routerHint" class="hint" :class="{ warn: routerHint.includes('拦截') }">
{{ routerHint.includes("拦截") ? "⚠ " : "" }}{{ routerHint }}
</div>
<div v-else class="hint">
判断逻辑由业务方实现:引擎运行到该节点时同步调用判断接口,按返回出口名走对应分支
</div>
</div>
<div class="fld">
<label>出口与分支</label>
<div class="hint">
出口名在各分支列头部就地编辑(互斥分支,出口名即判断依据,必填且唯一);判断失败流程挂起,
由管理端"重跑路由"恢复,不配默认出口
</div>
</div>
</Form>
<div class="dw-foot">
<Poptip confirm title="删除该条件分支及其全部分支?" ok-text="删除" cancel-text="取消" transfer @on-ok="onRemove">
<Button type="error" ghost>删除节点</Button>
</Poptip>
<span style="flex: 1" />
<Button @click="emit('update:modelValue', false)">取消</Button>
<Button type="primary" @click="onOk">确定</Button>
</div>
</Drawer>
</template>
<style scoped>
.dw-form {
padding-bottom: 8px;
}
.fld {
margin-bottom: 18px;
}
.fld > label {
display: block;
margin-bottom: 6px;
font-weight: 600;
font-size: 13px;
}
.hint {
color: #808695;
font-size: 12px;
line-height: 1.6;
margin-top: 5px;
}
.hint.warn {
color: #ff9900;
}
.dw-foot {
position: sticky;
bottom: -16px;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
margin-top: 8px;
background: #fff;
border-top: 1px solid #f0f0f0;
}
</style>