优化审批流
This commit is contained in:
1 parent
ffaadc26ae
commit
438145cae7
17 files changed
+1131
-187
No files matched your search
@@ -52,3 +52,8 @@ export function getApprovalFlowPick(params) {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 业务侧发起前拉可用流(按当前用户部门过滤)
|
||||
export function getAvailableFlows(params) {
|
||||
return request({ url: '/backend/approval/flow/available', method: 'get', params });
|
||||
}
|
||||
@@ -3,19 +3,29 @@
|
||||
<!-- 左侧节点面板 -->
|
||||
<div class="palette">
|
||||
<div class="palette-title">节点面板</div>
|
||||
<div
|
||||
v-for="p in palette"
|
||||
:key="p.type"
|
||||
class="palette-item"
|
||||
:class="'pal-' + p.cls"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, p)"
|
||||
>
|
||||
<el-icon class="pal-icon"><component :is="p.icon" /></el-icon>
|
||||
<span>{{ p.label }}</span>
|
||||
<div class="palette-list">
|
||||
<div
|
||||
v-for="p in palette"
|
||||
:key="p.type"
|
||||
class="palette-item"
|
||||
:class="'pal-' + p.cls"
|
||||
draggable="true"
|
||||
:title="'点击或拖拽添加「' + p.label + '」'"
|
||||
@dragstart="onDragStart($event, p)"
|
||||
@dragend="onDragEnd"
|
||||
@click="onPaletteAdd(p)"
|
||||
>
|
||||
<span class="pal-shape-wrap">
|
||||
<span class="pal-shape" :class="'shape-' + p.cls"></span>
|
||||
</span>
|
||||
<div class="pal-info">
|
||||
<div class="pal-label">{{ p.label }}</div>
|
||||
<div class="pal-desc">{{ p.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="palette-tip">
|
||||
拖入画布添加节点;从节点右侧圆点拖到目标节点连线;双击节点配置;点击连线设置分支条件。
|
||||
点击或拖拽即可添加节点;<br />从右侧小圆点拖到目标节点连线;<br />双击节点配置 · 点击连线设条件。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +41,33 @@
|
||||
@edge-click="onEdgeClick"
|
||||
>
|
||||
<template #node-flow="nodeProps">
|
||||
<div class="fn" :class="'fn-' + nodeProps.data.cls">
|
||||
<!-- 判断节点:菱形 -->
|
||||
<div v-if="nodeProps.data.cls === 'decision'" class="fn-diamond">
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<div class="fn-diamond-shape"></div>
|
||||
<div class="fn-diamond-label">{{ nodeProps.data.name || '判断' }}</div>
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
<!-- 开始 / 结束:胶囊 -->
|
||||
<div
|
||||
v-else-if="nodeProps.data.cls === 'start' || nodeProps.data.cls === 'end'"
|
||||
class="fn-pill"
|
||||
:class="'fn-pill-' + nodeProps.data.cls"
|
||||
>
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<el-icon class="fn-pill-icon"><component :is="nodeProps.data.cls === 'start' ? VideoPlay : SwitchButton" /></el-icon>
|
||||
<span>{{ nodeProps.data.name || (nodeProps.data.cls === 'start' ? '开始' : '结束') }}</span>
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
<!-- 通知节点:圆形 -->
|
||||
<div v-else-if="nodeProps.data.cls === 'notify'" class="fn-circle fn-circle-notify">
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<el-icon class="fn-circle-icon"><Bell /></el-icon>
|
||||
<div class="fn-circle-label">{{ nodeProps.data.name || '通知' }}</div>
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
<!-- 审批节点:矩形卡片 -->
|
||||
<div v-else class="fn" :class="'fn-' + nodeProps.data.cls">
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<div class="fn-badge">{{ typeShort(nodeProps.data.nodeType) }}</div>
|
||||
<div class="fn-title">{{ nodeProps.data.name || '未命名' }}</div>
|
||||
@@ -105,8 +141,26 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item v-else-if="nodeForm.nodeType === 4" label=" ">
|
||||
<span class="hint">条件网关:拖出多条连线,点击连线分别填写分支条件(留空为默认分支)。</span>
|
||||
<span class="hint">判断节点:拖出多条连线,点击连线分别填写分支条件(留空为默认分支)。运行期按条件命中其中一条分支。</span>
|
||||
</el-form-item>
|
||||
<template v-if="nodeForm.nodeType === 5">
|
||||
<el-form-item label="通知类型">
|
||||
<el-radio-group v-model="nodeForm.approverType" @change="onApproverTypeChange">
|
||||
<el-radio v-for="t in approverTypes" :key="t.value" :value="t.value">{{ t.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="currentApproverMeta && currentApproverMeta.needsSelection" :label="currentApproverMeta.selectLabel" required>
|
||||
<el-select v-model="nodeForm.approverIds" multiple filterable style="width: 100%" :placeholder="'选择' + currentApproverMeta.selectLabel + '(可多选)'">
|
||||
<el-option v-for="opt in currentPool" :key="opt.id" :label="opt.name" :value="opt.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label=" ">
|
||||
<span class="hint">该类型运行期自动定位通知对象,无需指定</span>
|
||||
</el-form-item>
|
||||
<el-form-item label=" ">
|
||||
<span class="hint">通知节点不阻塞流程,仅发送消息给指定人员,流程自动继续往下走。</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="danger" plain :disabled="!editingNodeId" @click="removeCurrentNode">删除节点</el-button>
|
||||
@@ -123,7 +177,7 @@
|
||||
<el-input v-model="edgeForm.condition" placeholder="如 amount>5000 且 dept=='研发' ;留空为默认分支" />
|
||||
</el-form-item>
|
||||
<el-form-item label=" ">
|
||||
<span class="hint">支持 > >= < <= == != 与 && / ||。条件仅在“条件网关”的出边上生效。</span>
|
||||
<span class="hint">支持 > >= < <= == != 与 && / ||。条件仅在“判断节点”的出边上生效。</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -139,7 +193,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VideoPlay, User, Share, SwitchButton, QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { VideoPlay, User, Share, SwitchButton, QuestionFilled, Bell } from '@element-plus/icons-vue'
|
||||
import { VueFlow, Handle, Position, MarkerType, useVueFlow } from '@vue-flow/core'
|
||||
import { Background } from '@vue-flow/background'
|
||||
import { Controls } from '@vue-flow/controls'
|
||||
@@ -164,20 +218,21 @@ const { screenToFlowCoordinate, onConnect } = useVueFlow()
|
||||
let keySeq = 1
|
||||
const nextKey = () => `n${Date.now().toString(36)}${(keySeq++).toString(36)}`
|
||||
|
||||
const NODE_TYPE = { START: 1, APPROVAL: 2, END: 3, CONDITION: 4 }
|
||||
const NODE_TYPE = { START: 1, APPROVAL: 2, END: 3, DECISION: 4, NOTIFY: 5 }
|
||||
|
||||
const palette = [
|
||||
{ type: NODE_TYPE.START, cls: 'start', label: '开始', icon: VideoPlay },
|
||||
{ type: NODE_TYPE.APPROVAL, cls: 'approval', label: '审批节点', icon: User },
|
||||
{ type: NODE_TYPE.CONDITION, cls: 'condition', label: '条件网关', icon: Share },
|
||||
{ type: NODE_TYPE.END, cls: 'end', label: '结束', icon: SwitchButton },
|
||||
{ type: NODE_TYPE.START, cls: 'start', label: '开始', desc: '流程唯一入口', icon: VideoPlay },
|
||||
{ type: NODE_TYPE.APPROVAL, cls: 'approval', label: '审批节点', desc: '指定审批人处理', icon: User },
|
||||
{ type: NODE_TYPE.DECISION, cls: 'decision', label: '判断节点', desc: '按条件分支路由', icon: Share },
|
||||
{ type: NODE_TYPE.NOTIFY, cls: 'notify', label: '通知节点', desc: '通知不阻塞流程', icon: Bell },
|
||||
{ type: NODE_TYPE.END, cls: 'end', label: '结束', desc: '流程出口', icon: SwitchButton },
|
||||
]
|
||||
|
||||
const clsOf = (t: number) =>
|
||||
t === NODE_TYPE.START ? 'start' : t === NODE_TYPE.END ? 'end' : t === NODE_TYPE.CONDITION ? 'condition' : 'approval'
|
||||
t === NODE_TYPE.START ? 'start' : t === NODE_TYPE.END ? 'end' : t === NODE_TYPE.DECISION ? 'decision' : t === NODE_TYPE.NOTIFY ? 'notify' : 'approval'
|
||||
|
||||
const typeShort = (t: number) =>
|
||||
t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.CONDITION ? '条件' : '审批'
|
||||
t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.DECISION ? '判断' : t === NODE_TYPE.NOTIFY ? '通知' : '审批'
|
||||
|
||||
const approverTypeLabel = (t: number) => {
|
||||
const m = (props.approverTypes as any[]).find((x) => x.value === t)
|
||||
@@ -193,7 +248,8 @@ function approverSummary(node: any) {
|
||||
const mode = node.multiMode === 2 ? '会签' : '或签'
|
||||
return `${approverTypeLabel(node.approverType)}:${names.length ? names.join('、') : '未选'}(${mode})`
|
||||
}
|
||||
if (node.nodeType === NODE_TYPE.CONDITION) return '按分支条件路由'
|
||||
if (node.nodeType === NODE_TYPE.DECISION) return '按分支条件路由'
|
||||
if (node.nodeType === NODE_TYPE.NOTIFY) return '发送通知给指定人员'
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -258,8 +314,15 @@ watch(() => props.modelValue, reload, { deep: false, immediate: true })
|
||||
|
||||
// ==================== 拖拽添加 ====================
|
||||
let dragType = 0
|
||||
let dragging = false
|
||||
function onDragStart(_evt: DragEvent, p: any) {
|
||||
dragType = p.type
|
||||
dragging = true
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
// 延迟重置,避免拖拽后紧接的 click 事件误新增
|
||||
setTimeout(() => { dragging = false; dragType = 0 }, 0)
|
||||
}
|
||||
|
||||
function onDrop(evt: DragEvent) {
|
||||
@@ -270,8 +333,23 @@ function onDrop(evt: DragEvent) {
|
||||
dragType = 0
|
||||
}
|
||||
|
||||
// 单击 palette 项:自动在画布中心附近新增节点(小量抖动避免重叠)
|
||||
function onPaletteAdd(p: any) {
|
||||
if (dragging) return
|
||||
if (!canvasWrap.value) return
|
||||
const rect = canvasWrap.value.getBoundingClientRect()
|
||||
const base = screenToFlowCoordinate({
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
})
|
||||
const jitter = (nodes.value.length % 6) * 26
|
||||
const position = { x: base.x - 80 + jitter, y: base.y - 40 + jitter }
|
||||
const data = makeNodeData({ node_type: p.type, node_name: defaultName(p.type) })
|
||||
nodes.value.push({ id: nextKey(), type: 'flow', position, data })
|
||||
}
|
||||
|
||||
function defaultName(t: number) {
|
||||
return t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.CONDITION ? '条件网关' : '审批节点'
|
||||
return t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.DECISION ? '判断节点' : t === NODE_TYPE.NOTIFY ? '通知节点' : '审批节点'
|
||||
}
|
||||
|
||||
// ==================== 连线 ====================
|
||||
@@ -303,7 +381,8 @@ const nodeForm = reactive<any>({
|
||||
})
|
||||
|
||||
const nodeDialogTitle = computed(() => {
|
||||
if (nodeForm.nodeType === NODE_TYPE.CONDITION) return '配置条件网关'
|
||||
if (nodeForm.nodeType === NODE_TYPE.DECISION) return '配置判断节点'
|
||||
if (nodeForm.nodeType === NODE_TYPE.NOTIFY) return '配置通知节点'
|
||||
if (nodeForm.nodeType === NODE_TYPE.APPROVAL) return '配置审批节点'
|
||||
return '配置节点'
|
||||
})
|
||||
@@ -348,8 +427,8 @@ function confirmNode() {
|
||||
return
|
||||
}
|
||||
const meta = currentApproverMeta.value
|
||||
if (nodeForm.nodeType === NODE_TYPE.APPROVAL && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
|
||||
ElMessage.warning('请选择审批人')
|
||||
if ((nodeForm.nodeType === NODE_TYPE.APPROVAL || nodeForm.nodeType === NODE_TYPE.NOTIFY) && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
|
||||
ElMessage.warning(nodeForm.nodeType === NODE_TYPE.NOTIFY ? '请选择通知对象' : '请选择审批人')
|
||||
return
|
||||
}
|
||||
node.data = makeNodeData({
|
||||
@@ -440,49 +519,156 @@ defineExpose({ serialize, fitView })
|
||||
}
|
||||
|
||||
.palette {
|
||||
width: 160px;
|
||||
flex: 0 0 160px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
width: 220px;
|
||||
flex: 0 0 220px;
|
||||
padding: 14px;
|
||||
background: #fafbfc;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
overflow-y: auto;
|
||||
|
||||
.palette-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: #409eff;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.palette-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
min-height: 48px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #fff;
|
||||
cursor: grab;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
background: #f5faff;
|
||||
box-shadow: 0 3px 10px rgba(64, 158, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pal-icon {
|
||||
font-size: 15px;
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.pal-shape-wrap {
|
||||
flex: 0 0 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pal-shape {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// 开始/结束:胶囊
|
||||
.shape-start,
|
||||
.shape-end {
|
||||
width: 22px;
|
||||
height: 11px;
|
||||
border-radius: 999px;
|
||||
background: #67c23a;
|
||||
}
|
||||
.shape-end {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
// 审批节点:矩形
|
||||
.shape-approval {
|
||||
width: 20px;
|
||||
height: 15px;
|
||||
border-radius: 3px;
|
||||
background: #409eff;
|
||||
}
|
||||
|
||||
// 判断节点:菱形(旋转方块)
|
||||
.shape-decision {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 2px;
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
// 通知节点:圆形
|
||||
.shape-notify {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 50%;
|
||||
background: #9254de;
|
||||
}
|
||||
|
||||
.pal-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.pal-label {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pal-desc {
|
||||
color: #909399;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.pal-start { border-left: 3px solid #67c23a; }
|
||||
.pal-approval { border-left: 3px solid #409eff; }
|
||||
.pal-condition { border-left: 3px solid #e6a23c; }
|
||||
.pal-decision { border-left: 3px solid #e6a23c; }
|
||||
.pal-notify { border-left: 3px solid #9254de; }
|
||||
.pal-end { border-left: 3px solid #909399; }
|
||||
|
||||
.palette-tip {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
margin-top: 14px;
|
||||
padding: 10px 12px;
|
||||
font-size: 11.5px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
line-height: 1.7;
|
||||
background: rgba(64, 158, 255, 0.05);
|
||||
border: 1px dashed rgba(64, 158, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,9 +714,105 @@ defineExpose({ serialize, fitView })
|
||||
|
||||
.fn-start { border-top: 3px solid #67c23a; .fn-badge { background: #67c23a; } }
|
||||
.fn-approval { border-top: 3px solid #409eff; .fn-badge { background: #409eff; } }
|
||||
.fn-condition { border-top: 3px solid #e6a23c; .fn-badge { background: #e6a23c; } }
|
||||
.fn-end { border-top: 3px solid #909399; .fn-badge { background: #909399; } }
|
||||
|
||||
// 开始 / 结束:胶囊
|
||||
.fn-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 90px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.fn-pill-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.fn-pill-start {
|
||||
border-color: #67c23a;
|
||||
color: #529b2e;
|
||||
background: #f0f9eb;
|
||||
}
|
||||
.fn-pill-end {
|
||||
border-color: #909399;
|
||||
color: #73767a;
|
||||
background: #f4f4f5;
|
||||
}
|
||||
|
||||
// 判断节点:菱形
|
||||
.fn-diamond {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fn-diamond-shape {
|
||||
position: absolute;
|
||||
inset: 18px;
|
||||
transform: rotate(45deg);
|
||||
background: #fdf6ec;
|
||||
border: 2px solid #e6a23c;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.fn-diamond-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #b88230;
|
||||
text-align: center;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 通知节点:圆形
|
||||
.fn-circle {
|
||||
position: relative;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.fn-circle-notify {
|
||||
border-color: #d3adf7;
|
||||
background: #f9f0ff;
|
||||
color: #531dab;
|
||||
}
|
||||
.fn-circle-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fn-circle-label {
|
||||
max-width: 70px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
>
|
||||
<div class="flow-name">
|
||||
{{ f.flow_name }}
|
||||
<el-tag v-if="f.is_default === 1" size="small" type="success">默认</el-tag>
|
||||
</div>
|
||||
<div class="flow-code">{{ f.flow_code || '(保存后生成编号)' }}</div>
|
||||
<div class="flow-sub">
|
||||
@@ -75,18 +74,23 @@
|
||||
<el-input v-model="current.description" type="textarea" :rows="2" placeholder="流程用途说明" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设为默认">
|
||||
<el-switch v-model="current.is_default" :active-value="1" :inactive-value="0" />
|
||||
<span class="hint">同分类仅一条默认,新申请走默认流程</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="current.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="适用部门">
|
||||
<el-tree-select v-model="current.apply_dept_ids" :data="orgTree"
|
||||
multiple show-checkbox check-strictly default-expand-all
|
||||
:render-after-expand="false"
|
||||
node-key="id" :props="{ label: 'name', children: 'children', value: 'id' }"
|
||||
clearable filterable
|
||||
@change="onDeptChange"
|
||||
placeholder="留空表示全公司通用(选公司=全员;选部门=覆及子部门)"
|
||||
style="width:100%" />
|
||||
<!-- <div class="hint">祖先链命中:选中上级部门或公司时,其下子部门成员都会命中本流程</div> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider content-position="left">流程编排(拖拽节点 · 手动连线 · 条件分支)</el-divider>
|
||||
@@ -119,6 +123,7 @@ import {
|
||||
saveApprovalFlow,
|
||||
deleteApprovalFlow
|
||||
} from '@/api/approval'
|
||||
import { buildOrgTree } from '@/views/apps/organization/composables'
|
||||
|
||||
const modules = ref<any[]>([])
|
||||
const activeModule = ref<string>('')
|
||||
@@ -127,7 +132,6 @@ const current = ref<any>(null)
|
||||
const saving = ref(false)
|
||||
|
||||
const canvasRef = ref<any>(null)
|
||||
// 传给画布的模型:{ nodes, edges }
|
||||
const graphModel = ref<any>({ nodes: [], edges: [] })
|
||||
|
||||
const pools = reactive<{ users: any[]; roles: any[]; orgs: any[]; positions: any[] }>({
|
||||
@@ -137,6 +141,44 @@ const pools = reactive<{ users: any[]; roles: any[]; orgs: any[]; positions: any
|
||||
positions: []
|
||||
})
|
||||
|
||||
// 将平面部门列表拼成树(复用组织管理页的 buildOrgTree,内部做 Number 强转)
|
||||
// 去除空 children 数组,避免 el-tree-select 叶节点渲染异常导致无法点击选中
|
||||
function stripEmptyChildren(nodes: any[]): any[] {
|
||||
return nodes.map((n) => {
|
||||
const node = { ...n }
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children = stripEmptyChildren(node.children)
|
||||
} else {
|
||||
delete node.children
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
const orgTree = computed(() => stripEmptyChildren(buildOrgTree(pools.orgs)))
|
||||
|
||||
// 祖先剪枝:若某选中节点的上级(含公司)也已被勾选,则子节点冗余,自动剔除
|
||||
function pruneRedundantDepts(ids: any[]): number[] {
|
||||
if (!ids || !ids.length) return []
|
||||
const numIds = ids.map((v) => Number(v))
|
||||
const set = new Set(numIds)
|
||||
const parentOf = new Map<number, number>()
|
||||
pools.orgs.forEach((o: any) => parentOf.set(Number(o.id), Number(o.parent_id || 0)))
|
||||
return numIds.filter((id) => {
|
||||
let p = parentOf.get(id) || 0
|
||||
const guard = new Set<number>([id])
|
||||
while (p && parentOf.has(p) && !guard.has(p)) {
|
||||
if (set.has(p)) return false // 祖先已选 → 冗余
|
||||
guard.add(p)
|
||||
p = parentOf.get(p) || 0
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function onDeptChange(ids: any[]) {
|
||||
if (current.value) current.value.apply_dept_ids = pruneRedundantDepts(ids)
|
||||
}
|
||||
|
||||
// 租户侧 6 种审批人类型
|
||||
const approverTypes = [
|
||||
{ value: 1, label: '指定用户', needsSelection: true, selectLabel: '审批人', poolKey: 'users' },
|
||||
@@ -185,6 +227,8 @@ async function selectFlow(id: number) {
|
||||
const res: any = await getApprovalFlowDetail(id)
|
||||
if (res.code === 200) {
|
||||
const d = res.data || {}
|
||||
// 回显时对历史冗余选择做一次剪枝
|
||||
d.apply_dept_ids = pruneRedundantDepts(d.apply_dept_ids || [])
|
||||
current.value = d
|
||||
graphModel.value = { nodes: d.nodes || [], edges: d.edges || [] }
|
||||
}
|
||||
@@ -199,7 +243,8 @@ function openNewFlow() {
|
||||
flow_code: '',
|
||||
description: '',
|
||||
is_default: 0,
|
||||
status: 1
|
||||
status: 1,
|
||||
apply_dept_ids: [] as number[]
|
||||
}
|
||||
// 预置开始/结束,方便用户直接拖审批节点连线
|
||||
graphModel.value = {
|
||||
@@ -231,6 +276,7 @@ async function saveFlow() {
|
||||
description: current.value.description,
|
||||
is_default: current.value.is_default,
|
||||
status: current.value.status,
|
||||
apply_dept_ids: current.value.apply_dept_ids || [],
|
||||
nodes: graph.nodes,
|
||||
edges: graph.edges
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
approvalEngine "server/services/approval"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
@@ -62,16 +63,17 @@ func (c *BackendApprovalController) FlowList() {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.TenantApprovalNode)).
|
||||
Filter("flow_id", r.ID).Filter("is_deleted", 0).Count()
|
||||
list = append(list, approvalFlowView{
|
||||
ID: r.ID,
|
||||
ModuleCode: r.ModuleCode,
|
||||
BizType: r.BizType,
|
||||
FlowCode: r.FlowCode,
|
||||
FlowName: r.FlowName,
|
||||
Description: r.Description,
|
||||
IsDefault: r.IsDefault,
|
||||
Status: r.Status,
|
||||
NodeCount: int(cnt),
|
||||
CreateTime: r.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
ID: r.ID,
|
||||
ModuleCode: r.ModuleCode,
|
||||
BizType: r.BizType,
|
||||
FlowCode: r.FlowCode,
|
||||
FlowName: r.FlowName,
|
||||
Description: r.Description,
|
||||
IsDefault: r.IsDefault,
|
||||
Status: r.Status,
|
||||
NodeCount: int(cnt),
|
||||
CreateTime: r.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
ApplyDeptIDs: parseIDSlice(r.ApplyDeptIDs),
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
|
||||
@@ -105,17 +107,18 @@ func tenantFlowToView(f models.TenantApprovalFlow) approvalFlowView {
|
||||
Filter("flow_id", f.ID).Filter("is_deleted", 0).
|
||||
OrderBy("sort", "id").All(&nodes)
|
||||
view := approvalFlowView{
|
||||
ID: f.ID,
|
||||
ModuleCode: f.ModuleCode,
|
||||
BizType: f.BizType,
|
||||
FlowCode: f.FlowCode,
|
||||
FlowName: f.FlowName,
|
||||
Description: f.Description,
|
||||
IsDefault: f.IsDefault,
|
||||
Status: f.Status,
|
||||
NodeCount: len(nodes),
|
||||
CreateTime: f.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
Nodes: make([]approvalNodePayload, 0, len(nodes)),
|
||||
ID: f.ID,
|
||||
ModuleCode: f.ModuleCode,
|
||||
BizType: f.BizType,
|
||||
FlowCode: f.FlowCode,
|
||||
FlowName: f.FlowName,
|
||||
Description: f.Description,
|
||||
IsDefault: f.IsDefault,
|
||||
Status: f.Status,
|
||||
NodeCount: len(nodes),
|
||||
CreateTime: f.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
ApplyDeptIDs: parseIDSlice(f.ApplyDeptIDs),
|
||||
Nodes: make([]approvalNodePayload, 0, len(nodes)),
|
||||
}
|
||||
edgeNodes := make([]flowEdgeNode, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
@@ -176,6 +179,18 @@ func (c *BackendApprovalController) FlowSave() {
|
||||
g := buildGraph(p.Nodes, p.Edges)
|
||||
now := time.Now()
|
||||
|
||||
// 适用部门写入前校验:需全部属于当前租户(空列表=全公司通用,直接放行)
|
||||
if len(p.ApplyDeptIDs) > 0 {
|
||||
count, _ := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("tid", tid).Filter("id__in", p.ApplyDeptIDs).Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if int(count) != len(p.ApplyDeptIDs) {
|
||||
jsonErr(&c.Controller, 400, 400, "适用部门包含非本租户组织,请重新选择")
|
||||
return
|
||||
}
|
||||
}
|
||||
applyDeptCSV := joinIDs(p.ApplyDeptIDs)
|
||||
|
||||
o := models.Orm
|
||||
var flowID uint64
|
||||
var flowCode string
|
||||
@@ -190,13 +205,14 @@ func (c *BackendApprovalController) FlowSave() {
|
||||
flowCode = exist.FlowCode
|
||||
_, err := o.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("id", flowID).Filter("tid", tid).Update(orm.Params{
|
||||
"module_code": p.ModuleCode,
|
||||
"biz_type": p.BizType,
|
||||
"flow_name": p.FlowName,
|
||||
"description": p.Description,
|
||||
"is_default": p.IsDefault,
|
||||
"status": p.Status,
|
||||
"update_time": now,
|
||||
"module_code": p.ModuleCode,
|
||||
"biz_type": p.BizType,
|
||||
"flow_name": p.FlowName,
|
||||
"description": p.Description,
|
||||
"is_default": p.IsDefault,
|
||||
"status": p.Status,
|
||||
"apply_dept_ids": applyDeptCSV,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
|
||||
@@ -204,14 +220,15 @@ func (c *BackendApprovalController) FlowSave() {
|
||||
}
|
||||
} else {
|
||||
row := models.TenantApprovalFlow{
|
||||
Tid: tid,
|
||||
ModuleCode: p.ModuleCode,
|
||||
BizType: p.BizType,
|
||||
FlowName: p.FlowName,
|
||||
Description: p.Description,
|
||||
IsDefault: p.IsDefault,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
Tid: tid,
|
||||
ModuleCode: p.ModuleCode,
|
||||
BizType: p.BizType,
|
||||
FlowName: p.FlowName,
|
||||
Description: p.Description,
|
||||
IsDefault: p.IsDefault,
|
||||
Status: p.Status,
|
||||
ApplyDeptIDs: applyDeptCSV,
|
||||
CreateTime: now,
|
||||
}
|
||||
id, err := o.Insert(&row)
|
||||
if err != nil {
|
||||
@@ -305,6 +322,7 @@ func (c *BackendApprovalController) Pick() {
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": r.ID,
|
||||
"module_code": r.ModuleCode,
|
||||
"flow_code": r.FlowCode,
|
||||
"flow_name": r.FlowName,
|
||||
@@ -404,7 +422,10 @@ func (c *BackendApprovalController) Options() {
|
||||
OrderBy("sort", "id").All(&og)
|
||||
orgList := make([]map[string]interface{}, 0, len(og))
|
||||
for _, o := range og {
|
||||
orgList = append(orgList, map[string]interface{}{"id": o.ID, "name": o.OrgName})
|
||||
orgList = append(orgList, map[string]interface{}{
|
||||
"id": o.ID, "name": o.OrgName,
|
||||
"parent_id": o.ParentID, "is_company": o.IsCompany,
|
||||
})
|
||||
}
|
||||
|
||||
var ps []models.BackendPosition
|
||||
@@ -429,6 +450,38 @@ func (c *BackendApprovalController) Options() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// AvailableFlows GET /backend/approval/flow/available?biz_type=reimburse
|
||||
// 业务侧“发起前拉可用流”:基于当前登录用户的部门,过滤启用的流程。
|
||||
// 前端基于返回结果判定:空则提示配置;一条自动带入;多条下拉选。
|
||||
func (c *BackendApprovalController) AvailableFlows() {
|
||||
claims, err := requireBackend(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
bizType := strings.TrimSpace(c.GetString("biz_type"))
|
||||
if bizType == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "缺少 biz_type 参数")
|
||||
return
|
||||
}
|
||||
// 取当前登录用户的部门(未分配时 OrgID=0,仅命中“全公司通用”流)
|
||||
var tu models.SystemTenantUser
|
||||
var userOrgID uint64
|
||||
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).Filter("uid", claims.UserID).Filter("delete_time__isnull", true).
|
||||
One(&tu); e == nil {
|
||||
userOrgID = tu.OrgID
|
||||
}
|
||||
list, err := approvalEngine.ListAvailableFlows(models.ApprovalScopeTenant, tid, bizType, userOrgID)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ============================ 辅助函数 ============================
|
||||
|
||||
// firstNonEmptyStr 取字符串指针/普通字符串中第一个非空值
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
approvalEngine "server/services/approval"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
@@ -26,12 +27,12 @@ type PlatformApprovalController struct {
|
||||
// ---- 配置侧 JSON 结构 ----
|
||||
|
||||
// approvalNodePayload 单个节点(前端画布节点)。
|
||||
// node_type:1开始 2审批 3结束 4条件网关;pos_x/pos_y 为画布坐标回显;
|
||||
// condition_expr 为该节点作为「条件网关出边目标」时的进入条件(由 edges 反推落库)。
|
||||
// node_type:1开始 2审批 3结束 4判断 5通知;pos_x/pos_y 为画布坐标回显;
|
||||
// condition_expr 为该节点作为「判断节点出边目标」时的进入条件(由 edges 反推落库)。
|
||||
type approvalNodePayload struct {
|
||||
NodeKey string `json:"node_key"`
|
||||
NodeName string `json:"node_name"`
|
||||
NodeType int8 `json:"node_type"` // 1开始 2审批 3结束 4条件网关
|
||||
NodeType int8 `json:"node_type"` // 1开始 2审批 3结束 4判断 5通知
|
||||
ApproverType int8 `json:"approver_type"` // 1指定用户 2指定角色 ...
|
||||
ApproverIDs []uint64 `json:"approver_ids"`
|
||||
MultiMode int8 `json:"multi_mode"` // 1或签 2会签
|
||||
@@ -52,31 +53,33 @@ type approvalEdge struct {
|
||||
|
||||
// approvalFlowSave 保存流程(图:节点 + 连线,节点全量覆盖)
|
||||
type approvalFlowSave struct {
|
||||
ID uint64 `json:"id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
BizType string `json:"biz_type"`
|
||||
FlowName string `json:"flow_name"`
|
||||
Description string `json:"description"`
|
||||
IsDefault int8 `json:"is_default"`
|
||||
Status int8 `json:"status"`
|
||||
Nodes []approvalNodePayload `json:"nodes"`
|
||||
Edges []approvalEdge `json:"edges"`
|
||||
ID uint64 `json:"id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
BizType string `json:"biz_type"`
|
||||
FlowName string `json:"flow_name"`
|
||||
Description string `json:"description"`
|
||||
IsDefault int8 `json:"is_default"`
|
||||
Status int8 `json:"status"`
|
||||
ApplyDeptIDs []uint64 `json:"apply_dept_ids"` // 仅租户侧使用,空=全公司通用
|
||||
Nodes []approvalNodePayload `json:"nodes"`
|
||||
Edges []approvalEdge `json:"edges"`
|
||||
}
|
||||
|
||||
// approvalFlowView 流程 + 节点 + 连线(详情返回)
|
||||
type approvalFlowView struct {
|
||||
ID uint64 `json:"id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
BizType string `json:"biz_type"`
|
||||
FlowCode string `json:"flow_code"`
|
||||
FlowName string `json:"flow_name"`
|
||||
Description string `json:"description"`
|
||||
IsDefault int8 `json:"is_default"`
|
||||
Status int8 `json:"status"`
|
||||
NodeCount int `json:"node_count"`
|
||||
CreateTime string `json:"create_time"`
|
||||
Nodes []approvalNodePayload `json:"nodes"`
|
||||
Edges []approvalEdge `json:"edges"`
|
||||
ID uint64 `json:"id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
BizType string `json:"biz_type"`
|
||||
FlowCode string `json:"flow_code"`
|
||||
FlowName string `json:"flow_name"`
|
||||
Description string `json:"description"`
|
||||
IsDefault int8 `json:"is_default"`
|
||||
Status int8 `json:"status"`
|
||||
NodeCount int `json:"node_count"`
|
||||
CreateTime string `json:"create_time"`
|
||||
ApplyDeptIDs []uint64 `json:"apply_dept_ids"` // 仅租户侧回显
|
||||
Nodes []approvalNodePayload `json:"nodes"`
|
||||
Edges []approvalEdge `json:"edges"`
|
||||
}
|
||||
|
||||
// readApprovalBody 读取并解析 JSON 请求体
|
||||
@@ -355,6 +358,7 @@ func (c *PlatformApprovalController) Pick() {
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": r.ID,
|
||||
"module_code": r.ModuleCode,
|
||||
"biz_type": r.BizType,
|
||||
"flow_code": r.FlowCode,
|
||||
@@ -457,6 +461,28 @@ func (c *PlatformApprovalController) BizTypes() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// AvailableFlows GET /platform/approval/flow/available?biz_type=domain
|
||||
// 业务侧“发起前拉可用流”:返回当前 biz_type 启用的平台流程列表。
|
||||
// 平台无部门概念,userOrgID 固定为 0(引擎内平台侧不做部门过滤)。
|
||||
func (c *PlatformApprovalController) AvailableFlows() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bizType := strings.TrimSpace(c.GetString("biz_type"))
|
||||
if bizType == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "缺少 biz_type 参数")
|
||||
return
|
||||
}
|
||||
list, err := approvalEngine.ListAvailableFlows(models.ApprovalScopePlatform, 0, bizType, 0)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ============================ 辅助函数 ============================
|
||||
|
||||
// approverDisplayName 优先姓名,回退账号(AdminUser.Account 为普通字符串)
|
||||
@@ -467,7 +493,7 @@ func approverDisplayName(name *string, account string) string {
|
||||
return strings.TrimSpace(account)
|
||||
}
|
||||
|
||||
// validateApprovalNodes 校验审批节点:必须选定审批人来源且非空(开始/结束/条件网关跳过)
|
||||
// validateApprovalNodes 校验审批节点:必须选定审批人来源且非空(开始/结束/判断/通知跳过)
|
||||
func validateApprovalNodes(nodes []approvalNodePayload) error {
|
||||
for i, nd := range nodes {
|
||||
if nd.NodeType == models.ApprovalNodeStart || nd.NodeType == models.ApprovalNodeEnd ||
|
||||
@@ -587,7 +613,7 @@ func rebuildEdges(nodes []flowEdgeNode) []approvalEdge {
|
||||
return edges
|
||||
}
|
||||
|
||||
// validateApprovalGraph 校验画布图的完整性:单一开始、审批节点≥1、网关出边≥2、全部可达、连线端点有效
|
||||
// validateApprovalGraph 校验画布图的完整性:单一开始、审批节点≥1、判断节点出边≥2、全部可达、连线端点有效
|
||||
func validateApprovalGraph(nodes []approvalNodePayload, edges []approvalEdge) error {
|
||||
if len(nodes) == 0 {
|
||||
return fmt.Errorf("流程至少需包含开始与审批节点")
|
||||
@@ -632,7 +658,7 @@ func validateApprovalGraph(nodes []approvalNodePayload, edges []approvalEdge) er
|
||||
}
|
||||
for _, k := range condKeys {
|
||||
if len(nextAdj[k]) < 2 {
|
||||
return fmt.Errorf("条件网关需至少两条分支连线")
|
||||
return fmt.Errorf("判断节点需至少两条分支连线")
|
||||
}
|
||||
}
|
||||
visited := map[string]bool{}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
-- =============================================================
|
||||
-- 审批流双层绑定重构 v2 - 增量迁移
|
||||
--
|
||||
-- 场景:上一版 create_approval_binding.sql 已建表但含 condition_expr/priority
|
||||
-- 本次将绑定层简化为纯关联,流程表加 apply_dept_ids 部门作用域。
|
||||
--
|
||||
-- 执行顺序:
|
||||
-- 1) DROP + 重建绑定表(结构变简,无法原地迁移)
|
||||
-- 2) 后端流程表补 apply_dept_ids
|
||||
-- =============================================================
|
||||
|
||||
-- 1. 绑定表:先删后建(如已有绑定数据请手工备份后再执行)
|
||||
DROP TABLE IF EXISTS `yz_approval_binding`;
|
||||
|
||||
CREATE TABLE `yz_approval_binding` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`scope` TINYINT NOT NULL DEFAULT 1 COMMENT '1租户 2平台',
|
||||
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID(平台侧为0)',
|
||||
`biz_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '功能/单据类型:reimburse/bid/domain',
|
||||
`flow_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '绑定的审批流ID',
|
||||
`flow_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批流编号(冗余)',
|
||||
`remark` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_scope_tid_biz_flow` (`scope`,`tid`,`biz_type`,`flow_id`),
|
||||
KEY `idx_flow` (`flow_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批流功能绑定(biz_type↔flow 多对多)';
|
||||
|
||||
-- 2. 后端流程表:补部门作用域列
|
||||
ALTER TABLE `yz_backend_approval_flow`
|
||||
ADD COLUMN `apply_dept_ids` VARCHAR(500) NOT NULL DEFAULT ''
|
||||
COMMENT '适用部门ID列表 CSV,空=全公司通用' AFTER `flow_code`;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- =============================================================
|
||||
-- 审批流图编辑器 - 增量迁移(对已存在的旧表补列)
|
||||
-- MySQL 兼容写法:如列已存在会报错,跳过该条即可
|
||||
-- 如果表本身不存在,请先执行 create_approval_flow.sql
|
||||
-- =============================================================
|
||||
|
||||
-- 1. 租户侧流程表:补 flow_code
|
||||
ALTER TABLE `yz_backend_approval_flow`
|
||||
ADD COLUMN `flow_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批流编号(自动生成 FL%06d)' AFTER `biz_type`;
|
||||
|
||||
ALTER TABLE `yz_backend_approval_flow`
|
||||
ADD INDEX `idx_tid_code` (`tid`, `flow_code`);
|
||||
|
||||
-- 2. 租户侧节点表:补 pos_x / pos_y
|
||||
ALTER TABLE `yz_backend_approval_node`
|
||||
ADD COLUMN `pos_x` INT NOT NULL DEFAULT 0 COMMENT '画布X坐标' AFTER `sort`,
|
||||
ADD COLUMN `pos_y` INT NOT NULL DEFAULT 0 COMMENT '画布Y坐标' AFTER `pos_x`;
|
||||
|
||||
-- 3. 平台侧流程表:补 flow_code
|
||||
ALTER TABLE `yz_platform_approval_flow`
|
||||
ADD COLUMN `flow_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批流编号(自动生成 FL%06d)' AFTER `biz_type`;
|
||||
|
||||
ALTER TABLE `yz_platform_approval_flow`
|
||||
ADD INDEX `idx_code` (`flow_code`);
|
||||
|
||||
-- 4. 平台侧节点表:补 pos_x / pos_y
|
||||
ALTER TABLE `yz_platform_approval_node`
|
||||
ADD COLUMN `pos_x` INT NOT NULL DEFAULT 0 COMMENT '画布X坐标' AFTER `sort`,
|
||||
ADD COLUMN `pos_y` INT NOT NULL DEFAULT 0 COMMENT '画布Y坐标' AFTER `pos_x`;
|
||||
|
||||
-- 5. 实例表:补 form_data(条件求值用表单快照 JSON)
|
||||
ALTER TABLE `yz_approval_instance`
|
||||
ADD COLUMN `form_data` TEXT NULL COMMENT '发起表单数据快照(JSON)' AFTER `submit_dept_id`;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- =============================================================
|
||||
-- 审批流功能绑定(biz_type ↔ flow 多对多关联)
|
||||
--
|
||||
-- 对应模型:go/models/approval.go ApprovalBinding
|
||||
-- 用途:给某个功能(biz_type)绑定"可选审批流"清单,业务代码通过
|
||||
-- ListAvailableFlows(scope, tid, bizType, userOrgID) 拿到过滤后的
|
||||
-- 可用流;若一条流程设置了 apply_dept_ids,则仅该部门成员可见。
|
||||
--
|
||||
-- 注意:本表**不含条件表达式 / 优先级**——条件分支在流程内部(条件网关)
|
||||
-- 实现,与绑定层无关。
|
||||
-- =============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `yz_approval_binding` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`scope` TINYINT NOT NULL DEFAULT 1 COMMENT '1租户 2平台',
|
||||
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID(平台侧为0)',
|
||||
`biz_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '功能/单据类型:reimburse/bid/domain',
|
||||
`flow_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '绑定的审批流ID',
|
||||
`flow_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批流编号(冗余,方便查询)',
|
||||
`remark` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_scope_tid_biz_flow` (`scope`,`tid`,`biz_type`,`flow_id`),
|
||||
KEY `idx_flow` (`flow_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批流功能绑定(biz_type↔flow 多对多)';
|
||||
@@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `yz_backend_approval_flow` (
|
||||
`module_code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '所属功能模块 code(yz_system_modules.code):oa/erp/crm/cms',
|
||||
`biz_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '适用单据类型:reimburse/bid/domain/...',
|
||||
`flow_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批流编号(自动生成、全局唯一,供各程序按编号发起):FL000007',
|
||||
`apply_dept_ids` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '适用部门ID列表 CSV(yz_backend_organization.id),空=全公司通用',
|
||||
`flow_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '流程名称',
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '流程说明',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否该单据类型的默认流程:1是 0否',
|
||||
|
||||
+36
-14
@@ -22,7 +22,8 @@ const (
|
||||
ApprovalNodeStart = 1 // 开始
|
||||
ApprovalNodeApproval = 2 // 审批
|
||||
ApprovalNodeEnd = 3 // 结束
|
||||
ApprovalNodeCondition = 4 // 条件分支(预留)
|
||||
ApprovalNodeCondition = 4 // 判断节点
|
||||
ApprovalNodeNotify = 5 // 通知节点
|
||||
)
|
||||
|
||||
// 审批人类型
|
||||
@@ -52,19 +53,20 @@ const (
|
||||
|
||||
// TenantApprovalFlow 租户侧审批流程 yz_backend_approval_flow
|
||||
type TenantApprovalFlow struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
ModuleCode string `orm:"column(module_code);size(50);default()" json:"module_code"`
|
||||
BizType string `orm:"column(biz_type);size(50);default()" json:"biz_type"`
|
||||
FlowCode string `orm:"column(flow_code);size(64);default()" json:"flow_code"`
|
||||
FlowName string `orm:"column(flow_name);size(100);default()" json:"flow_name"`
|
||||
Description string `orm:"column(description);size(500);default()" json:"description"`
|
||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
ModuleCode string `orm:"column(module_code);size(50);default()" json:"module_code"`
|
||||
BizType string `orm:"column(biz_type);size(50);default()" json:"biz_type"`
|
||||
FlowCode string `orm:"column(flow_code);size(64);default()" json:"flow_code"`
|
||||
ApplyDeptIDs string `orm:"column(apply_dept_ids);size(500);default()" json:"apply_dept_ids"`
|
||||
FlowName string `orm:"column(flow_name);size(100);default()" json:"flow_name"`
|
||||
Description string `orm:"column(description);size(500);default()" json:"description"`
|
||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantApprovalFlow) TableName() string {
|
||||
@@ -172,6 +174,7 @@ const (
|
||||
ApprovalTaskReturned = 3 // 已退回
|
||||
ApprovalTaskSkipped = 4 // 已跳过
|
||||
ApprovalTaskTimeout = 5 // 已超时
|
||||
ApprovalTaskNotified = 6 // 已通知
|
||||
)
|
||||
|
||||
// 任务动作
|
||||
@@ -244,3 +247,22 @@ type ApprovalTask struct {
|
||||
func (m *ApprovalTask) TableName() string {
|
||||
return "yz_approval_task"
|
||||
}
|
||||
|
||||
// ApprovalBinding 审批流功能绑定 yz_approval_binding
|
||||
// 纯关联表:一个 biz_type 可绑多条流程;条件分支在流程内部(条件网关)实现,本表不包含条件字段。
|
||||
type ApprovalBinding struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Scope int8 `orm:"column(scope);default(1)" json:"scope"`
|
||||
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
|
||||
BizType string `orm:"column(biz_type);size(50);default()" json:"biz_type"`
|
||||
FlowID uint64 `orm:"column(flow_id);default(0)" json:"flow_id"`
|
||||
FlowCode string `orm:"column(flow_code);size(64);default()" json:"flow_code"`
|
||||
Remark string `orm:"column(remark);size(200);default()" json:"remark"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
}
|
||||
|
||||
func (m *ApprovalBinding) TableName() string {
|
||||
return "yz_approval_binding"
|
||||
}
|
||||
@@ -57,6 +57,8 @@ func Init(_ string) {
|
||||
new(PlatformApprovalNode),
|
||||
new(ApprovalInstance),
|
||||
new(ApprovalTask),
|
||||
// 审批流功能绑定(docs/sql/create_approval_binding.sql)
|
||||
new(ApprovalBinding),
|
||||
new(BackendReimbursement),
|
||||
new(BackendReimbursementItem),
|
||||
new(BackendReimbursementExpenseType),
|
||||
|
||||
@@ -27,6 +27,7 @@ func RegisterApprovalRoutes() {
|
||||
beego.Router("/backend/approval/flow/save", &controllers.BackendApprovalController{}, "post:FlowSave")
|
||||
beego.Router("/backend/approval/flow/delete/:id", &controllers.BackendApprovalController{}, "delete:FlowDelete")
|
||||
beego.Router("/backend/approval/flow/pick", &controllers.BackendApprovalController{}, "get:Pick")
|
||||
beego.Router("/backend/approval/flow/available", &controllers.BackendApprovalController{}, "get:AvailableFlows")
|
||||
}
|
||||
|
||||
// RegisterProductRoutes 注册租户端「产品购买」路由。
|
||||
|
||||
@@ -178,6 +178,7 @@ func Register() {
|
||||
beego.Router("/platform/approval/options", &controllers.PlatformApprovalController{}, "get:Options")
|
||||
beego.Router("/platform/approval/biz-types", &controllers.PlatformApprovalController{}, "get:BizTypes")
|
||||
beego.Router("/platform/approval/flow/pick", &controllers.PlatformApprovalController{}, "get:Pick")
|
||||
beego.Router("/platform/approval/flow/available", &controllers.PlatformApprovalController{}, "get:AvailableFlows")
|
||||
|
||||
// 模块管理(yz_system_modules)
|
||||
beego.Router("/platform/modules/list", &controllers.PlatformModulesController{}, "get:GetList")
|
||||
|
||||
@@ -568,6 +568,99 @@ func dedupeApprovers(in []Approver) []Approver {
|
||||
return out
|
||||
}
|
||||
|
||||
// ============================ 运行时:发起前的可用流查询 ============================
|
||||
|
||||
// AvailableFlow 业务侧选择用的轻量结构
|
||||
type AvailableFlow struct {
|
||||
FlowID uint64 `json:"flow_id"`
|
||||
FlowCode string `json:"flow_code"`
|
||||
FlowName string `json:"flow_name"`
|
||||
BizType string `json:"biz_type"`
|
||||
}
|
||||
|
||||
// ListAvailableFlows 按 bizType + 用户部门 过滤“当前可用”审批流。
|
||||
// 直接查询 flow 表中 biz_type 匹配且 status=1 未删除的流程。
|
||||
// 租户侧额外按 apply_dept_ids 部门过滤。
|
||||
// 平台侧(scope=2)忽略部门过滤(因平台 flow 无 apply_dept_ids)。
|
||||
func ListAvailableFlows(scope int8, tid uint64, bizType string, userOrgID uint64) ([]AvailableFlow, error) {
|
||||
// 租户侧预计算一次当前用户组织的“祖先链”(含自身),选中上级部门或公司均命中
|
||||
var path []uint64
|
||||
if scope == models.ApprovalScopeTenant && userOrgID > 0 {
|
||||
path = buildOrgPath(tid, userOrgID)
|
||||
}
|
||||
out := make([]AvailableFlow, 0)
|
||||
if scope == models.ApprovalScopePlatform {
|
||||
var rows []models.PlatformApprovalFlow
|
||||
_, _ = models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
|
||||
Filter("biz_type", bizType).Filter("is_deleted", 0).Filter("status", 1).
|
||||
OrderBy("id").All(&rows)
|
||||
for _, f := range rows {
|
||||
out = append(out, AvailableFlow{FlowID: f.ID, FlowCode: f.FlowCode, FlowName: f.FlowName, BizType: f.BizType})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
var rows []models.TenantApprovalFlow
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("tid", tid).Filter("biz_type", bizType).Filter("is_deleted", 0).Filter("status", 1).
|
||||
OrderBy("id").All(&rows)
|
||||
for _, f := range rows {
|
||||
if !deptMatch(f.ApplyDeptIDs, path) {
|
||||
continue
|
||||
}
|
||||
out = append(out, AvailableFlow{FlowID: f.ID, FlowCode: f.FlowCode, FlowName: f.FlowName, BizType: f.BizType})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// deptMatch csv 空视为全公司通用;否则与 path (包含自身与祖先部门ID) 任一命中即命中。
|
||||
func deptMatch(csv string, path []uint64) bool {
|
||||
csv = strings.TrimSpace(csv)
|
||||
if csv == "" {
|
||||
return true
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return false
|
||||
}
|
||||
inPath := make(map[uint64]bool, len(path))
|
||||
for _, id := range path {
|
||||
inPath[id] = true
|
||||
}
|
||||
for _, part := range strings.Split(csv, ",") {
|
||||
if v, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64); err == nil && inPath[v] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// buildOrgPath 从当前部门沿 parent_id 向上追到根,返回含自身的 ID 链(环保护)
|
||||
func buildOrgPath(tid, orgID uint64) []uint64 {
|
||||
if orgID == 0 {
|
||||
return nil
|
||||
}
|
||||
var all []models.BackendOrganization
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("tid", tid).Filter("delete_time__isnull", true).
|
||||
All(&all, "id", "parent_id")
|
||||
parent := make(map[uint64]uint64, len(all))
|
||||
for _, o := range all {
|
||||
parent[o.ID] = o.ParentID
|
||||
}
|
||||
out := []uint64{orgID}
|
||||
seen := map[uint64]bool{orgID: true}
|
||||
cur := orgID
|
||||
for {
|
||||
p, ok := parent[cur]
|
||||
if !ok || p == 0 || seen[p] {
|
||||
break
|
||||
}
|
||||
out = append(out, p)
|
||||
seen[p] = true
|
||||
cur = p
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ============================ 运行时:提交 ============================
|
||||
|
||||
// Submit 提交单据进入审批,创建实例并按图展开首个可执行节点的任务。
|
||||
@@ -614,7 +707,8 @@ func Submit(in SubmitInput) (uint64, error) {
|
||||
|
||||
start := flow.ByKey[flow.StartKey]
|
||||
hasApproval, firstApprovalName, firstApprovalKey := anyApprovalNode(flow)
|
||||
node, approvers := nextExecutable(flow, in.FormData, start, in)
|
||||
notifyFn := func(n *Node) { fireNotifyTasks(uint64(instID), in, n) }
|
||||
node, approvers := nextExecutable(flow, in.FormData, start, in, notifyFn)
|
||||
if node == nil {
|
||||
// 无可执行审批节点
|
||||
if hasApproval {
|
||||
@@ -647,8 +741,9 @@ func anyApprovalNode(flow *Flow) (bool, string, string) {
|
||||
return false, "", ""
|
||||
}
|
||||
|
||||
// advanceFrom 从 cur 沿出边前进,遇条件网关求值选路,返回首个审批节点(nil 表示到终点)
|
||||
func advanceFrom(flow *Flow, formData map[string]interface{}, cur *Node) *Node {
|
||||
// advanceFrom 从 cur 沿出边前进,遇条件网关求值选路,返回首个审批节点(nil 表示到终点)。
|
||||
// onNotify 可选回调:当路径经过通知节点时触发,由调用方负责创建通知任务。
|
||||
func advanceFrom(flow *Flow, formData map[string]interface{}, cur *Node, onNotify ...func(*Node)) *Node {
|
||||
visited := make(map[string]bool)
|
||||
node := pickNext(flow, formData, cur)
|
||||
for node != nil {
|
||||
@@ -661,6 +756,11 @@ func advanceFrom(flow *Flow, formData map[string]interface{}, cur *Node) *Node {
|
||||
return node
|
||||
case models.ApprovalNodeEnd:
|
||||
return nil
|
||||
case models.ApprovalNodeNotify:
|
||||
if len(onNotify) > 0 && onNotify[0] != nil {
|
||||
onNotify[0](node)
|
||||
}
|
||||
node = pickNext(flow, formData, node)
|
||||
default: // start / condition:继续向后
|
||||
node = pickNext(flow, formData, node)
|
||||
}
|
||||
@@ -700,8 +800,9 @@ func pickNext(flow *Flow, formData map[string]interface{}, node *Node) *Node {
|
||||
}
|
||||
|
||||
// nextExecutable 沿图找到下一个「有有效审批人」的审批节点;无人审批节点自动跳过
|
||||
func nextExecutable(flow *Flow, formData map[string]interface{}, cur *Node, in SubmitInput) (*Node, []Approver) {
|
||||
node := advanceFrom(flow, formData, cur)
|
||||
// nextExecutable 获取下一个可执行审批节点及其审批人(跳过通知节点并通过 onNotify 回调触发)
|
||||
func nextExecutable(flow *Flow, formData map[string]interface{}, cur *Node, in SubmitInput, onNotify ...func(*Node)) (*Node, []Approver) {
|
||||
node := advanceFrom(flow, formData, cur, onNotify...)
|
||||
visited := make(map[string]bool)
|
||||
for node != nil {
|
||||
if visited[node.NodeKey] {
|
||||
@@ -715,7 +816,7 @@ func nextExecutable(flow *Flow, formData map[string]interface{}, cur *Node, in S
|
||||
return node, approvers
|
||||
}
|
||||
}
|
||||
node = advanceFrom(flow, formData, node)
|
||||
node = advanceFrom(flow, formData, node, onNotify...)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -792,6 +893,34 @@ func createTasks(instID uint64, in SubmitInput, node Node, approvers []Approver)
|
||||
return err
|
||||
}
|
||||
|
||||
// fireNotifyTasks 为通知节点创建已读通知任务(不阻塞流程)
|
||||
func fireNotifyTasks(instID uint64, in SubmitInput, notifyNode *Node) {
|
||||
recipients, _ := resolveApprovers(in.Scope, *notifyNode, in)
|
||||
recipients = dedupeApprovers(recipients)
|
||||
if len(recipients) == 0 {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
tasks := make([]models.ApprovalTask, 0, len(recipients))
|
||||
for _, r := range recipients {
|
||||
tasks = append(tasks, models.ApprovalTask{
|
||||
InstanceID: instID,
|
||||
Scope: in.Scope,
|
||||
Tid: in.Tid,
|
||||
BizType: in.BizType,
|
||||
BizID: in.BizID,
|
||||
NodeKey: notifyNode.NodeKey,
|
||||
NodeName: notifyNode.NodeName,
|
||||
Sort: notifyNode.Sort,
|
||||
ApproverID: r.ID,
|
||||
ApproverName: r.Name,
|
||||
Status: models.ApprovalTaskNotified,
|
||||
FinishTime: &now,
|
||||
})
|
||||
}
|
||||
_, _ = models.Orm.InsertMulti(len(tasks), tasks)
|
||||
}
|
||||
|
||||
// finishInstance 结束实例
|
||||
func finishInstance(instID uint64, status int8) {
|
||||
now := time.Now()
|
||||
@@ -947,7 +1076,8 @@ func advance(instID uint64, currentKey string, in ApproveInput) (*ApproveResult,
|
||||
formData := instanceFormData(&inst)
|
||||
si := submitFromInstance(&inst, formData)
|
||||
cur := flow.ByKey[currentKey]
|
||||
node, approvers := nextExecutable(flow, formData, cur, si)
|
||||
notifyFn := func(n *Node) { fireNotifyTasks(instID, si, n) }
|
||||
node, approvers := nextExecutable(flow, formData, cur, si, notifyFn)
|
||||
if node == nil {
|
||||
finishInstance(instID, models.ApprovalInstanceApproved)
|
||||
return &ApproveResult{
|
||||
|
||||
@@ -60,3 +60,8 @@ export function getApprovalFlowPick(params) {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 业务侧发起前拉可用流
|
||||
export function getAvailableFlows(params) {
|
||||
return request({ url: '/platform/approval/flow/available', method: 'get', params })
|
||||
}
|
||||
@@ -3,19 +3,29 @@
|
||||
<!-- 左侧节点面板 -->
|
||||
<div class="palette">
|
||||
<div class="palette-title">节点面板</div>
|
||||
<div
|
||||
v-for="p in palette"
|
||||
:key="p.type"
|
||||
class="palette-item"
|
||||
:class="'pal-' + p.cls"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, p)"
|
||||
>
|
||||
<el-icon class="pal-icon"><component :is="p.icon" /></el-icon>
|
||||
<span>{{ p.label }}</span>
|
||||
<div class="palette-list">
|
||||
<div
|
||||
v-for="p in palette"
|
||||
:key="p.type"
|
||||
class="palette-item"
|
||||
:class="'pal-' + p.cls"
|
||||
draggable="true"
|
||||
:title="'点击或拖拽添加「' + p.label + '」'"
|
||||
@dragstart="onDragStart($event, p)"
|
||||
@dragend="onDragEnd"
|
||||
@click="onPaletteAdd(p)"
|
||||
>
|
||||
<span class="pal-shape-wrap">
|
||||
<span class="pal-shape" :class="'shape-' + p.cls"></span>
|
||||
</span>
|
||||
<div class="pal-info">
|
||||
<div class="pal-label">{{ p.label }}</div>
|
||||
<div class="pal-desc">{{ p.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="palette-tip">
|
||||
拖入画布添加节点;从节点右侧圆点拖到目标节点连线;双击节点配置;点击连线设置分支条件。
|
||||
点击或拖拽即可添加节点;<br />从右侧小圆点拖到目标节点连线;<br />双击节点配置 · 点击连线设条件。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +41,33 @@
|
||||
@edge-click="onEdgeClick"
|
||||
>
|
||||
<template #node-flow="nodeProps">
|
||||
<div class="fn" :class="'fn-' + nodeProps.data.cls">
|
||||
<!-- 判断节点:菱形 -->
|
||||
<div v-if="nodeProps.data.cls === 'decision'" class="fn-diamond">
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<div class="fn-diamond-shape"></div>
|
||||
<div class="fn-diamond-label">{{ nodeProps.data.name || '判断' }}</div>
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
<!-- 开始 / 结束:胶囊 -->
|
||||
<div
|
||||
v-else-if="nodeProps.data.cls === 'start' || nodeProps.data.cls === 'end'"
|
||||
class="fn-pill"
|
||||
:class="'fn-pill-' + nodeProps.data.cls"
|
||||
>
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<el-icon class="fn-pill-icon"><component :is="nodeProps.data.cls === 'start' ? VideoPlay : SwitchButton" /></el-icon>
|
||||
<span>{{ nodeProps.data.name || (nodeProps.data.cls === 'start' ? '开始' : '结束') }}</span>
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
<!-- 通知节点:圆形 -->
|
||||
<div v-else-if="nodeProps.data.cls === 'notify'" class="fn-circle fn-circle-notify">
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<el-icon class="fn-circle-icon"><Bell /></el-icon>
|
||||
<div class="fn-circle-label">{{ nodeProps.data.name || '通知' }}</div>
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
<!-- 审批节点:矩形卡片 -->
|
||||
<div v-else class="fn" :class="'fn-' + nodeProps.data.cls">
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<div class="fn-badge">{{ typeShort(nodeProps.data.nodeType) }}</div>
|
||||
<div class="fn-title">{{ nodeProps.data.name || '未命名' }}</div>
|
||||
@@ -105,8 +141,26 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item v-else-if="nodeForm.nodeType === 4" label=" ">
|
||||
<span class="hint">条件网关:拖出多条连线,点击连线分别填写分支条件(留空为默认分支)。</span>
|
||||
<span class="hint">判断节点:拖出多条连线,点击连线分别填写分支条件(留空为默认分支)。运行期按条件命中其中一条分支。</span>
|
||||
</el-form-item>
|
||||
<template v-if="nodeForm.nodeType === 5">
|
||||
<el-form-item label="通知类型">
|
||||
<el-radio-group v-model="nodeForm.approverType" @change="onApproverTypeChange">
|
||||
<el-radio v-for="t in approverTypes" :key="t.value" :value="t.value">{{ t.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="currentApproverMeta && currentApproverMeta.needsSelection" :label="currentApproverMeta.selectLabel" required>
|
||||
<el-select v-model="nodeForm.approverIds" multiple filterable style="width: 100%" :placeholder="'选择' + currentApproverMeta.selectLabel + '(可多选)'">
|
||||
<el-option v-for="opt in currentPool" :key="opt.id" :label="opt.name" :value="opt.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label=" ">
|
||||
<span class="hint">该类型运行期自动定位通知对象,无需指定</span>
|
||||
</el-form-item>
|
||||
<el-form-item label=" ">
|
||||
<span class="hint">通知节点不阻塞流程,仅发送消息给指定人员,流程自动继续往下走。</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="danger" plain :disabled="!editingNodeId" @click="removeCurrentNode">删除节点</el-button>
|
||||
@@ -123,7 +177,7 @@
|
||||
<el-input v-model="edgeForm.condition" placeholder="如 amount>5000 且 dept=='研发' ;留空为默认分支" />
|
||||
</el-form-item>
|
||||
<el-form-item label=" ">
|
||||
<span class="hint">支持 > >= < <= == != 与 && / ||。条件仅在“条件网关”的出边上生效。</span>
|
||||
<span class="hint">支持 > >= < <= == != 与 && / ||。条件仅在“判断节点”的出边上生效。</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -139,7 +193,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VideoPlay, User, Share, SwitchButton, QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { VideoPlay, User, Share, SwitchButton, QuestionFilled, Bell } from '@element-plus/icons-vue'
|
||||
import { VueFlow, Handle, Position, MarkerType, useVueFlow } from '@vue-flow/core'
|
||||
import { Background } from '@vue-flow/background'
|
||||
import { Controls } from '@vue-flow/controls'
|
||||
@@ -164,20 +218,21 @@ const { screenToFlowCoordinate, onConnect } = useVueFlow()
|
||||
let keySeq = 1
|
||||
const nextKey = () => `n${Date.now().toString(36)}${(keySeq++).toString(36)}`
|
||||
|
||||
const NODE_TYPE = { START: 1, APPROVAL: 2, END: 3, CONDITION: 4 }
|
||||
const NODE_TYPE = { START: 1, APPROVAL: 2, END: 3, DECISION: 4, NOTIFY: 5 }
|
||||
|
||||
const palette = [
|
||||
{ type: NODE_TYPE.START, cls: 'start', label: '开始', icon: VideoPlay },
|
||||
{ type: NODE_TYPE.APPROVAL, cls: 'approval', label: '审批节点', icon: User },
|
||||
{ type: NODE_TYPE.CONDITION, cls: 'condition', label: '条件网关', icon: Share },
|
||||
{ type: NODE_TYPE.END, cls: 'end', label: '结束', icon: SwitchButton },
|
||||
{ type: NODE_TYPE.START, cls: 'start', label: '开始', desc: '流程唯一入口', icon: VideoPlay },
|
||||
{ type: NODE_TYPE.APPROVAL, cls: 'approval', label: '审批节点', desc: '指定审批人处理', icon: User },
|
||||
{ type: NODE_TYPE.DECISION, cls: 'decision', label: '判断节点', desc: '按条件分支路由', icon: Share },
|
||||
{ type: NODE_TYPE.NOTIFY, cls: 'notify', label: '通知节点', desc: '通知不阻塞流程', icon: Bell },
|
||||
{ type: NODE_TYPE.END, cls: 'end', label: '结束', desc: '流程出口', icon: SwitchButton },
|
||||
]
|
||||
|
||||
const clsOf = (t: number) =>
|
||||
t === NODE_TYPE.START ? 'start' : t === NODE_TYPE.END ? 'end' : t === NODE_TYPE.CONDITION ? 'condition' : 'approval'
|
||||
t === NODE_TYPE.START ? 'start' : t === NODE_TYPE.END ? 'end' : t === NODE_TYPE.DECISION ? 'decision' : t === NODE_TYPE.NOTIFY ? 'notify' : 'approval'
|
||||
|
||||
const typeShort = (t: number) =>
|
||||
t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.CONDITION ? '条件' : '审批'
|
||||
t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.DECISION ? '判断' : t === NODE_TYPE.NOTIFY ? '通知' : '审批'
|
||||
|
||||
const approverTypeLabel = (t: number) => {
|
||||
const m = (props.approverTypes as any[]).find((x) => x.value === t)
|
||||
@@ -193,7 +248,8 @@ function approverSummary(node: any) {
|
||||
const mode = node.multiMode === 2 ? '会签' : '或签'
|
||||
return `${approverTypeLabel(node.approverType)}:${names.length ? names.join('、') : '未选'}(${mode})`
|
||||
}
|
||||
if (node.nodeType === NODE_TYPE.CONDITION) return '按分支条件路由'
|
||||
if (node.nodeType === NODE_TYPE.DECISION) return '按分支条件路由'
|
||||
if (node.nodeType === NODE_TYPE.NOTIFY) return '发送通知给指定人员'
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -258,8 +314,14 @@ watch(() => props.modelValue, reload, { deep: false, immediate: true })
|
||||
|
||||
// ==================== 拖拽添加 ====================
|
||||
let dragType = 0
|
||||
let dragging = false
|
||||
function onDragStart(_evt: DragEvent, p: any) {
|
||||
dragType = p.type
|
||||
dragging = true
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
setTimeout(() => { dragging = false; dragType = 0 }, 0)
|
||||
}
|
||||
|
||||
function onDrop(evt: DragEvent) {
|
||||
@@ -270,8 +332,23 @@ function onDrop(evt: DragEvent) {
|
||||
dragType = 0
|
||||
}
|
||||
|
||||
// 单击 palette 项:自动在画布中心附近新增节点(小量抖动避免重叠)
|
||||
function onPaletteAdd(p: any) {
|
||||
if (dragging) return
|
||||
if (!canvasWrap.value) return
|
||||
const rect = canvasWrap.value.getBoundingClientRect()
|
||||
const base = screenToFlowCoordinate({
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
})
|
||||
const jitter = (nodes.value.length % 6) * 26
|
||||
const position = { x: base.x - 80 + jitter, y: base.y - 40 + jitter }
|
||||
const data = makeNodeData({ node_type: p.type, node_name: defaultName(p.type) })
|
||||
nodes.value.push({ id: nextKey(), type: 'flow', position, data })
|
||||
}
|
||||
|
||||
function defaultName(t: number) {
|
||||
return t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.CONDITION ? '条件网关' : '审批节点'
|
||||
return t === NODE_TYPE.START ? '开始' : t === NODE_TYPE.END ? '结束' : t === NODE_TYPE.DECISION ? '判断节点' : t === NODE_TYPE.NOTIFY ? '通知节点' : '审批节点'
|
||||
}
|
||||
|
||||
// ==================== 连线 ====================
|
||||
@@ -303,7 +380,8 @@ const nodeForm = reactive<any>({
|
||||
})
|
||||
|
||||
const nodeDialogTitle = computed(() => {
|
||||
if (nodeForm.nodeType === NODE_TYPE.CONDITION) return '配置条件网关'
|
||||
if (nodeForm.nodeType === NODE_TYPE.DECISION) return '配置判断节点'
|
||||
if (nodeForm.nodeType === NODE_TYPE.NOTIFY) return '配置通知节点'
|
||||
if (nodeForm.nodeType === NODE_TYPE.APPROVAL) return '配置审批节点'
|
||||
return '配置节点'
|
||||
})
|
||||
@@ -348,8 +426,8 @@ function confirmNode() {
|
||||
return
|
||||
}
|
||||
const meta = currentApproverMeta.value
|
||||
if (nodeForm.nodeType === NODE_TYPE.APPROVAL && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
|
||||
ElMessage.warning('请选择审批人')
|
||||
if ((nodeForm.nodeType === NODE_TYPE.APPROVAL || nodeForm.nodeType === NODE_TYPE.NOTIFY) && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
|
||||
ElMessage.warning(nodeForm.nodeType === NODE_TYPE.NOTIFY ? '请选择通知对象' : '请选择审批人')
|
||||
return
|
||||
}
|
||||
node.data = makeNodeData({
|
||||
@@ -440,49 +518,156 @@ defineExpose({ serialize, fitView })
|
||||
}
|
||||
|
||||
.palette {
|
||||
width: 160px;
|
||||
flex: 0 0 160px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
width: 220px;
|
||||
flex: 0 0 220px;
|
||||
padding: 14px;
|
||||
background: #fafbfc;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
overflow-y: auto;
|
||||
|
||||
.palette-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: #409eff;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.palette-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
min-height: 48px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #fff;
|
||||
cursor: grab;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
background: #f5faff;
|
||||
box-shadow: 0 3px 10px rgba(64, 158, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pal-icon {
|
||||
font-size: 15px;
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.pal-shape-wrap {
|
||||
flex: 0 0 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pal-shape {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// 开始/结束:胶囊
|
||||
.shape-start,
|
||||
.shape-end {
|
||||
width: 22px;
|
||||
height: 11px;
|
||||
border-radius: 999px;
|
||||
background: #67c23a;
|
||||
}
|
||||
.shape-end {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
// 审批节点:矩形
|
||||
.shape-approval {
|
||||
width: 20px;
|
||||
height: 15px;
|
||||
border-radius: 3px;
|
||||
background: #409eff;
|
||||
}
|
||||
|
||||
// 判断节点:菱形(旋转方块)
|
||||
.shape-decision {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 2px;
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
// 通知节点:圆形
|
||||
.shape-notify {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 50%;
|
||||
background: #9254de;
|
||||
}
|
||||
|
||||
.pal-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.pal-label {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pal-desc {
|
||||
color: #909399;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.pal-start { border-left: 3px solid #67c23a; }
|
||||
.pal-approval { border-left: 3px solid #409eff; }
|
||||
.pal-condition { border-left: 3px solid #e6a23c; }
|
||||
.pal-decision { border-left: 3px solid #e6a23c; }
|
||||
.pal-notify { border-left: 3px solid #9254de; }
|
||||
.pal-end { border-left: 3px solid #909399; }
|
||||
|
||||
.palette-tip {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
margin-top: 14px;
|
||||
padding: 10px 12px;
|
||||
font-size: 11.5px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
line-height: 1.7;
|
||||
background: rgba(64, 158, 255, 0.05);
|
||||
border: 1px dashed rgba(64, 158, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,9 +713,105 @@ defineExpose({ serialize, fitView })
|
||||
|
||||
.fn-start { border-top: 3px solid #67c23a; .fn-badge { background: #67c23a; } }
|
||||
.fn-approval { border-top: 3px solid #409eff; .fn-badge { background: #409eff; } }
|
||||
.fn-condition { border-top: 3px solid #e6a23c; .fn-badge { background: #e6a23c; } }
|
||||
.fn-end { border-top: 3px solid #909399; .fn-badge { background: #909399; } }
|
||||
|
||||
// 开始 / 结束:胶囊
|
||||
.fn-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 90px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.fn-pill-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.fn-pill-start {
|
||||
border-color: #67c23a;
|
||||
color: #529b2e;
|
||||
background: #f0f9eb;
|
||||
}
|
||||
.fn-pill-end {
|
||||
border-color: #909399;
|
||||
color: #73767a;
|
||||
background: #f4f4f5;
|
||||
}
|
||||
|
||||
// 判断节点:菱形
|
||||
.fn-diamond {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fn-diamond-shape {
|
||||
position: absolute;
|
||||
inset: 18px;
|
||||
transform: rotate(45deg);
|
||||
background: #fdf6ec;
|
||||
border: 2px solid #e6a23c;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.fn-diamond-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #b88230;
|
||||
text-align: center;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 通知节点:圆形
|
||||
.fn-circle {
|
||||
position: relative;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.fn-circle-notify {
|
||||
border-color: #d3adf7;
|
||||
background: #f9f0ff;
|
||||
color: #531dab;
|
||||
}
|
||||
.fn-circle-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fn-circle-label {
|
||||
max-width: 70px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
>
|
||||
<div class="flow-name">
|
||||
{{ f.flow_name }}
|
||||
<el-tag v-if="f.is_default === 1" size="small" type="success">默认</el-tag>
|
||||
</div>
|
||||
<div class="flow-code">{{ f.flow_code || '(保存后生成编号)' }}</div>
|
||||
<div class="flow-sub">
|
||||
@@ -76,12 +75,6 @@
|
||||
<el-input v-model="current.description" type="textarea" :rows="2" placeholder="流程用途说明" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设为默认">
|
||||
<el-switch v-model="current.is_default" :active-value="1" :inactive-value="0" />
|
||||
<span class="hint">同分类仅一条默认,新申请走默认流程</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="current.status" :active-value="1" :inactive-value="0" />
|
||||
|
||||
Reference in new issue
Block a user