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:
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
// 条件分支容器(2026-09-24):紫系区分并行橙;与 ParallelBox 的本质差异——无 JOIN 语义,
|
||||
// 分支按出口名互斥选路(运行时调判断服务),分支尾隐式汇合到容器后继。
|
||||
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约)。
|
||||
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 routerLabel = computed(() => {
|
||||
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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="condition-box" :class="{ error: hasError }">
|
||||
<div class="cb-title" @click="ctx.openNode(node.uid)">
|
||||
条件分支(按出口互斥选路)
|
||||
<span class="cb-hint">(点击标题配置判断服务)</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">{{ 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>
|
||||
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||||
</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-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-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>
|
||||
@@ -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>
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// 节点序列渲染单元(方案 §3.2):容器内的"+卡片+"重复模式;并行分支与主流程共用,天然支持嵌套
|
||||
// 节点序列渲染单元(方案 §3.2):容器内的"+卡片+"重复模式;并行/条件分支与主流程共用,天然支持嵌套
|
||||
import { EditorItem } from "../model";
|
||||
import NodeCard from "./NodeCard.vue";
|
||||
import NotifyCard from "./NotifyCard.vue";
|
||||
import ParallelBox from "./ParallelBox.vue";
|
||||
import ConditionBox from "./ConditionBox.vue";
|
||||
import PlusButton from "./PlusButton.vue";
|
||||
|
||||
defineProps<{
|
||||
items: EditorItem[];
|
||||
/** 容器标识:root 或 `${parallelUid}:${branchIndex}` */
|
||||
/** 容器标识:root 或 `${容器uid}:${branchIndex}`(并行/条件分支同格式) */
|
||||
container: string;
|
||||
}>();
|
||||
</script>
|
||||
@@ -17,7 +19,9 @@ defineProps<{
|
||||
<PlusButton :container="container" :index="0" />
|
||||
<template v-for="(item, i) in items" :key="item.uid">
|
||||
<NodeCard v-if="item.kind === 'approval'" :node="item" />
|
||||
<ParallelBox v-else :node="item" />
|
||||
<NotifyCard v-else-if="item.kind === 'notify'" :node="item" />
|
||||
<ParallelBox v-else-if="item.kind === 'parallel'" :node="item" />
|
||||
<ConditionBox v-else :node="item" />
|
||||
<PlusButton :container="container" :index="i + 1" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
// 抄送节点卡片(2026-09-23):青色系区分审批蓝;标题 + 抄送对象摘要 + 就地校验标注;hover 显示删除
|
||||
import { computed, inject } from "vue";
|
||||
import { NotifyNode, EDITOR_CTX, receiversText } from "../model";
|
||||
|
||||
const props = defineProps<{ node: NotifyNode }>();
|
||||
|
||||
const ctx = inject(EDITOR_CTX)!;
|
||||
|
||||
const issues = computed(() => ctx.issueOf(props.node.uid));
|
||||
const hasError = computed(() => issues.value.some((i) => i.level === "error"));
|
||||
const summary = computed(() => receiversText(props.node.receivers, ctx.depts, ctx.userNames));
|
||||
|
||||
// 删除控件在卡片内,点击不应顺带打开配置抽屉(确认气泡已 transfer 到 body,不受影响)
|
||||
function onOpen(e: MouseEvent): void {
|
||||
if ((e.target as HTMLElement).closest(".nc-del")) return;
|
||||
ctx.openNode(props.node.uid);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="notify-card" :class="{ error: hasError }" @click="onOpen">
|
||||
<div class="nc-head">
|
||||
<span class="nc-icon">抄</span>
|
||||
<span class="nc-name">{{ node.name || "未命名节点" }}</span>
|
||||
<!-- view-ui-plus 无 Popconfirm 组件,二次确认用 Poptip 的 confirm 模式 -->
|
||||
<Poptip
|
||||
confirm
|
||||
title="删除该抄送节点?"
|
||||
ok-text="删除"
|
||||
cancel-text="取消"
|
||||
transfer
|
||||
@on-ok="ctx.removeNode(node.uid)"
|
||||
>
|
||||
<span class="nc-del" title="删除节点">×</span>
|
||||
</Poptip>
|
||||
</div>
|
||||
<div class="nc-sub">{{ summary }}</div>
|
||||
<div class="nc-sub last"><span class="tag">知会 · 不产生待办</span></div>
|
||||
<div v-for="(i, k) in issues" :key="k" class="nc-issue" :class="i.level">⚠ {{ i.msg }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notify-card {
|
||||
width: 264px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.notify-card:hover {
|
||||
border-color: #13a8a8;
|
||||
}
|
||||
.notify-card.error {
|
||||
border-color: #ed4014;
|
||||
}
|
||||
.nc-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px 2px;
|
||||
}
|
||||
.nc-icon {
|
||||
flex: none;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
background: #e6f6f6;
|
||||
color: #13a8a8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.nc-name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nc-del {
|
||||
flex: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #c5c9d1;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
.notify-card:hover .nc-del {
|
||||
display: inline-flex;
|
||||
}
|
||||
.nc-del:hover {
|
||||
background: #ed4014;
|
||||
}
|
||||
.nc-sub {
|
||||
padding: 4px 16px 0;
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
.nc-sub.last {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
background: #e6f6f6;
|
||||
color: #13a8a8;
|
||||
}
|
||||
.nc-issue {
|
||||
padding: 4px 16px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.nc-issue.error {
|
||||
color: #ed4014;
|
||||
}
|
||||
.nc-issue.warn {
|
||||
color: #ff9900;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
<script setup lang="ts">
|
||||
// 抄送节点配置抽屉(2026-09-23):结构与 ApprovalDrawer 同源(本地表单 + 确定回写),
|
||||
// 去掉审批专属的会签/时限;选人同用 UserCheckModal(teacherTree),科室口径同审批人。
|
||||
import { computed, inject, reactive, ref, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import { NotifyNode, EDITOR_CTX } from "../model";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
node: NotifyNode | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [boolean];
|
||||
/** 节点已删除(由抽屉内删除按钮触发),由外层关闭抽屉并重算校验 */
|
||||
removed: [];
|
||||
}>();
|
||||
|
||||
const ctx = inject(EDITOR_CTX)!;
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
type: "DEPT_KEY" as "DEPT_KEY" | "USERS",
|
||||
deptKey: "",
|
||||
userIds: [] as number[],
|
||||
});
|
||||
|
||||
const userCheckModal = ref<any>();
|
||||
const userModalSources = [
|
||||
{
|
||||
name: "基础平台",
|
||||
loader: () => flowApi.adminTeacherTree().send() as Promise<any[]>,
|
||||
},
|
||||
];
|
||||
|
||||
function syncFromNode(): void {
|
||||
const n = props.node;
|
||||
if (!n) return;
|
||||
form.name = n.name;
|
||||
form.type = n.receivers?.type === "USERS" ? "USERS" : "DEPT_KEY";
|
||||
form.deptKey = n.receivers?.type === "DEPT_KEY" ? n.receivers.deptKey : "";
|
||||
form.userIds = n.receivers?.type === "USERS" ? [...n.receivers.userIds] : [];
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.node?.uid],
|
||||
() => {
|
||||
if (props.modelValue) syncFromNode();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/** 科室下拉项:mapList 聚合 + 当前值(可能已不在映射表中)兜底,避免选项丢失 */
|
||||
const deptOptions = computed(() => {
|
||||
const list = ctx.depts.map((d) => ({
|
||||
value: d.deptKey,
|
||||
label: `${d.deptName || d.deptKey}(${d.deptKey})· ${d.count} 人`,
|
||||
}));
|
||||
if (form.deptKey && !list.some((o) => o.value === form.deptKey)) {
|
||||
list.unshift({ value: form.deptKey, label: `${form.deptKey}(未配置映射)` });
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
/** 当前科室的映射状态提示(抄送运行时宽容:无人只跳过不阻塞,故仅提示不谈"拦截") */
|
||||
const deptHint = computed(() => {
|
||||
if (form.type !== "DEPT_KEY" || !form.deptKey) return "";
|
||||
const d = ctx.depts.find((x) => x.deptKey === form.deptKey);
|
||||
if (!d) return `科室映射「${form.deptKey}」不存在,发布会被拦截`;
|
||||
if (!d.count) return `科室「${d.deptName || form.deptKey}」无启用人员,抄送会落空(不阻塞流程)`;
|
||||
return "";
|
||||
});
|
||||
|
||||
function userNameOf(id: number): string {
|
||||
return ctx.userNames[id] || `#${id}`;
|
||||
}
|
||||
|
||||
/** 选人弹窗提交:选中后的 id+name 合入本地 userIds 与姓名快照(不再逐个 byId 检索) */
|
||||
function onUsersPicked(arr: any[]): void {
|
||||
let added = 0;
|
||||
for (const u of arr || []) {
|
||||
if (u?.id == null) continue;
|
||||
const id = Number(u.id);
|
||||
if (!form.userIds.includes(id)) {
|
||||
form.userIds.push(id);
|
||||
added++;
|
||||
}
|
||||
if (u.name) ctx.userNames[id] = u.name;
|
||||
}
|
||||
Message.success(`已选择 ${added} 名用户`);
|
||||
}
|
||||
|
||||
function removeUser(id: number): void {
|
||||
const i = form.userIds.indexOf(id);
|
||||
if (i >= 0) form.userIds.splice(i, 1);
|
||||
}
|
||||
|
||||
function onOk(): void {
|
||||
const n = props.node;
|
||||
if (!n) {
|
||||
emit("update:modelValue", false);
|
||||
return;
|
||||
}
|
||||
if (!form.name.trim()) {
|
||||
Message.warning("节点名称不能为空");
|
||||
return;
|
||||
}
|
||||
n.name = form.name.trim();
|
||||
n.receivers =
|
||||
form.type === "USERS"
|
||||
? { type: "USERS", userIds: [...form.userIds] }
|
||||
: { type: "DEPT_KEY", deptKey: form.deptKey };
|
||||
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>
|
||||
<RadioGroup v-model="form.type" type="button" button-style="solid">
|
||||
<Radio label="DEPT_KEY">按科室映射</Radio>
|
||||
<Radio label="USERS">指定用户</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div v-if="form.type === 'DEPT_KEY'" class="fld">
|
||||
<label>科室映射</label>
|
||||
<Select v-model="form.deptKey" filterable placeholder="请选择科室">
|
||||
<Option v-for="o in deptOptions" :key="o.value" :value="o.value" :label="o.label" />
|
||||
</Select>
|
||||
<div v-if="deptHint" class="hint warn">⚠ {{ deptHint }}</div>
|
||||
<div v-else class="hint">科室启用中的全部人员都会收到抄送;运行时无人可抄送时只跳过,不阻塞流程</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="fld">
|
||||
<label>指定用户</label>
|
||||
<div class="user-add">
|
||||
<Button type="primary" @click="userCheckModal?.show()">选择用户</Button>
|
||||
</div>
|
||||
<div class="user-tags">
|
||||
<Tag
|
||||
v-for="id in form.userIds"
|
||||
:key="id"
|
||||
closable
|
||||
color="cyan"
|
||||
@on-close="removeUser(id)"
|
||||
>
|
||||
{{ userNameOf(id) }}
|
||||
</Tag>
|
||||
<span v-if="!form.userIds.length" class="hint">尚未添加用户</span>
|
||||
</div>
|
||||
<div class="hint">抄送是知会:每人一条站内通知,不产生待办、不需要办理</div>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<UserCheckModal
|
||||
ref="userCheckModal"
|
||||
:sources="userModalSources"
|
||||
:userids="form.userIds"
|
||||
@on-submit="onUsersPicked"
|
||||
/>
|
||||
</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;
|
||||
}
|
||||
.user-add {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.user-tags {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 0;
|
||||
}
|
||||
.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>
|
||||
@@ -12,7 +12,7 @@ const props = defineProps<{
|
||||
const ctx = inject(EDITOR_CTX)!;
|
||||
const visible = ref(false);
|
||||
|
||||
function pick(kind: "approval" | "parallel"): void {
|
||||
function pick(kind: "approval" | "parallel" | "notify" | "condition"): void {
|
||||
visible.value = false;
|
||||
ctx.insertItem(props.container, props.index, kind);
|
||||
}
|
||||
@@ -30,6 +30,13 @@ function pick(kind: "approval" | "parallel"): void {
|
||||
<i>配置审批人 / 会签 / 时限</i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="pick('notify')">
|
||||
<span class="mi-icon notify">抄</span>
|
||||
<span class="mi-text">
|
||||
<b>抄送节点</b>
|
||||
<i>知会指定人,不产生待办、不阻塞流程</i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="pick('parallel')">
|
||||
<span class="mi-icon parallel">⇉</span>
|
||||
<span class="mi-text">
|
||||
@@ -37,6 +44,13 @@ function pick(kind: "approval" | "parallel"): void {
|
||||
<i>多分支同时进行,全部完成后继续</i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="pick('condition')">
|
||||
<span class="mi-icon condition">◇</span>
|
||||
<span class="mi-text">
|
||||
<b>条件分支</b>
|
||||
<i>系统判断按出口互斥选路(判断服务)</i>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Poptip>
|
||||
</div>
|
||||
@@ -105,9 +119,15 @@ function pick(kind: "approval" | "parallel"): void {
|
||||
.mi-icon.approval {
|
||||
background: #2d8cf0;
|
||||
}
|
||||
.mi-icon.notify {
|
||||
background: #13a8a8;
|
||||
}
|
||||
.mi-icon.parallel {
|
||||
background: #ff9900;
|
||||
}
|
||||
.mi-icon.condition {
|
||||
background: #9254de;
|
||||
}
|
||||
.mi-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user