Files
zkxw/web-admin/src/components/oa-approval/flow-tree/FlowBranch.vue
T
2026-08-06 16:54:32 +08:00

355 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 递归渲染线性分支:节点 → + → 下一节点 -->
<template>
<div class="flow-branch">
<template v-if="node.type === 'end'">
<div class="flow-end">{{ endLabel }}</div>
</template>
<template v-else>
<div
class="flow-node"
:class="node.type"
@click="emit('edit', node)"
>
<div class="hd">
<span>{{ node.name || typeLabel(node.type) }}</span>
<el-button
v-if="node.type !== 'start'"
class="flow-node__del"
text
type="danger"
@click.stop="emit('remove', node.id)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
<div class="bd">{{ nodeSummary(node) }} ›</div>
</div>
<template v-if="node.type === 'condition'">
<div class="flow-cond-wrap">
<div
v-for="(g, idx) in node.conditionGroups || []"
:key="`${node.id}_g_${idx}`"
class="flow-cond-col"
>
<div class="flow-cond-col__title" @click.stop="emit('edit', node)">
<span class="flow-cond-col__name">{{ branchName(g, idx) }}</span>
<span
v-for="(line, lineIdx) in branchConditionLines(g)"
:key="`${node.id}_g_${idx}_r_${lineIdx}`"
class="muted"
>
{{ line }}
</span>
</div>
<!-- 分支仅有结束节点时,先露出「+」才能往分支内插入 -->
<el-dropdown
v-if="!g.child || g.child.type === 'end'"
trigger="click"
@command="(cmd: string) => onAddInGroup(idx, cmd)"
>
<button type="button" class="flow-plus" title="在本分支添加节点">+</button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="approval">审批人</el-dropdown-item>
<el-dropdown-item command="cc">抄送人</el-dropdown-item>
<el-dropdown-item command="condition">条件分支</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<FlowBranch
v-if="g.child && g.child.type !== 'end'"
:node="g.child"
:depth="depth + 1"
:end-label="branchEndLabel"
:candidates="candidates"
:value-label-maps="valueLabelMaps"
@edit="(n) => emit('edit', n)"
@remove="(id) => emit('remove', id)"
@add="(payload) => emit('add', payload)"
/>
<div v-else class="flow-end">{{ branchEndLabel }}</div>
</div>
</div>
<!-- 条件分支汇合后继续主链 -->
<el-dropdown trigger="click" @command="(cmd: string) => onAdd(cmd)">
<button type="button" class="flow-plus" title="在条件分支后添加节点">+</button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="approval">审批人</el-dropdown-item>
<el-dropdown-item command="cc">抄送人</el-dropdown-item>
<el-dropdown-item command="condition">条件分支</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<FlowBranch
v-if="node.child"
:node="node.child"
:depth="depth"
:end-label="endLabel"
:candidates="candidates"
:value-label-maps="valueLabelMaps"
@edit="(n) => emit('edit', n)"
@remove="(id) => emit('remove', id)"
@add="(payload) => emit('add', payload)"
/>
<div v-else class="flow-end">{{ endLabel }}</div>
</template>
<template v-else>
<el-dropdown trigger="click" @command="(cmd: string) => onAdd(cmd)">
<button type="button" class="flow-plus" title="添加节点">+</button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="approval">审批人</el-dropdown-item>
<el-dropdown-item command="cc">抄送人</el-dropdown-item>
<el-dropdown-item command="condition">条件分支</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<FlowBranch
v-if="node.child"
:node="node.child"
:depth="depth"
:end-label="endLabel"
:candidates="candidates"
:value-label-maps="valueLabelMaps"
@edit="(n) => emit('edit', n)"
@remove="(id) => emit('remove', id)"
@add="(payload) => emit('add', payload)"
/>
<div v-else class="flow-end">{{ endLabel }}</div>
</template>
</template>
</div>
</template>
<script setup lang="ts">
import { Delete } from '@element-plus/icons-vue'
import {
formatConditionPreviewLines,
type ConditionCandidate,
type ConditionValueLabelMaps
} from '@/components/oa-approval/oa-condition-fields'
import type { ConditionGroup, FlowNode } from './types'
import { summarizeApprover } from './types'
// 递归自引用
import FlowBranch from './FlowBranch.vue'
defineOptions({ name: 'OaFlowBranch' })
const props = withDefaults(
defineProps<{
node: FlowNode
depth?: number
/** 分支/主链末端文案;有汇合后主链时分支内传「汇合」 */
endLabel?: string
candidates?: ConditionCandidate[]
valueLabelMaps?: ConditionValueLabelMaps
}>(),
{ depth: 0, endLabel: '流程结束', candidates: () => [], valueLabelMaps: () => ({}) }
)
/** 本条件有汇合后 child,或外层已标明汇合时,分支列末端用「汇合」 */
const branchEndLabel = computed(() =>
props.node.child || props.endLabel === '汇合' ? '汇合' : props.endLabel
)
const emit = defineEmits<{
edit: [node: FlowNode]
remove: [nodeId: string]
add: [
payload: {
parentId: string
type: 'approval' | 'cc' | 'condition'
/** 写入条件节点的第 N 个分支;缺省则写入 parent.child(汇合后主链) */
groupIndex?: number
}
]
}>()
function typeLabel(t: string) {
if (t === 'start') return '发起人'
if (t === 'approval') return '审批人'
if (t === 'cc') return '抄送人'
if (t === 'condition') return '条件分支'
return t
}
function branchName(g: ConditionGroup, idx: number) {
if (g.isDefault) return g.name || '默认 · 其他情况'
return g.name || `条件${idx + 1}`
}
function branchConditionLines(g: ConditionGroup) {
if (g.isDefault) return [] as string[]
return formatConditionPreviewLines(
g.rules || [],
props.candidates || [],
props.valueLabelMaps
)
}
function nodeSummary(n: FlowNode) {
if (n.type === 'start') return '谁可以发起本审批'
if (n.type === 'condition') {
const g = n.conditionGroups?.length || 0
return `${g} 个分支`
}
return summarizeApprover(n.approverConfig, n.type)
}
function onAdd(cmd: string) {
if (cmd === 'approval' || cmd === 'cc' || cmd === 'condition') {
emit('add', { parentId: props.node.id, type: cmd })
}
}
function onAddInGroup(groupIndex: number, cmd: string) {
if (cmd === 'approval' || cmd === 'cc' || cmd === 'condition') {
emit('add', { parentId: props.node.id, type: cmd, groupIndex })
}
}
</script>
<style scoped lang="scss">
.flow-branch {
display: flex;
flex-direction: column;
align-items: center;
}
.flow-node {
width: 220px;
overflow: hidden;
cursor: pointer;
background: #fff;
border: 1px solid var(--mint-border, #e2e8f0);
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: border-color 0.15s, box-shadow 0.15s;
&:hover {
border-color: var(--primary, #10b981);
box-shadow: 0 0 0 3px var(--primary-bg, #ecfdf5);
}
.hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
color: #fff;
font-size: 13px;
font-weight: 600;
}
.bd {
padding: 10px 12px;
color: var(--mint-text-muted, #64748b);
font-size: 13px;
}
&.start .hd {
background: #0f766e;
}
&.approval .hd {
background: #d97706;
}
&.cc .hd {
background: #0891b2;
}
&.condition .hd {
background: #7c3aed;
}
}
.flow-node__del {
padding: 0;
color: #fff !important;
opacity: 0.85;
background: transparent !important;
&:hover,
&:focus {
color: #ef4444 !important;
opacity: 1;
background: transparent !important;
}
}
.flow-plus {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin: 8px 0;
color: var(--primary, #10b981);
font-weight: 700;
cursor: pointer;
background: #fff;
border: 1px solid var(--primary, #10b981);
border-radius: 50%;
}
.flow-end {
margin-top: 8px;
padding: 6px 16px;
color: var(--mint-text-muted, #64748b);
font-size: 12px;
background: #e2e8f0;
border-radius: 999px;
}
.flow-cond-wrap {
display: flex;
gap: 16px;
align-items: flex-start;
width: max-content;
margin-top: 8px;
padding: 12px;
background: #f1f5f9;
border-radius: 10px;
}
.flow-cond-col {
display: flex;
flex-direction: column;
align-items: center;
min-width: 240px;
}
.flow-cond-col__title {
margin-bottom: 8px;
padding: 4px 10px;
color: #5b21b6;
font-size: 12px;
font-weight: 600;
text-align: center;
cursor: pointer;
background: #ede9fe;
border-radius: 6px;
.flow-cond-col__name {
display: block;
}
.muted {
display: block;
margin-top: 2px;
color: var(--mint-text-muted, #94a3b8);
font-weight: 400;
line-height: 1.4;
text-align: center;
word-break: break-word;
}
}
</style>