优化审批流

This commit is contained in:
hero920103 committed 2026-10-07 22:56:35 +08:00
1 parent ffaadc26ae
commit 438145cae7
17 files changed
+1131 -187

No files matched your search

+5
View File
@@ -52,3 +52,8 @@ export function getApprovalFlowPick(params) {
params,
});
}
// 业务侧发起前拉可用流(按当前用户部门过滤)
export function getAvailableFlows(params) {
return request({ url: '/backend/approval/flow/available', method: 'get', params });
}
+325 -43
View File
@@ -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">支持 &gt; &gt;= &lt; &lt;= == != 与 &amp;&amp; / ||。条件仅在“条件网关”的出边上生效。</span>
<span class="hint">支持 &gt; &gt;= &lt; &lt;= == != 与 &amp;&amp; / ||。条件仅在“判断节点”的出边上生效。</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;
+55 -9
View File
@@ -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
}
+90 -37
View File
@@ -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 取字符串指针/普通字符串中第一个非空值
+53 -27
View File
@@ -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{}
+34
View File
@@ -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`;
+29
View File
@@ -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 多对多)';
+1
View File
@@ -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
View File
@@ -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"
}
+2
View File
@@ -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),
+1
View File
@@ -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 注册租户端「产品购买」路由。
+1
View File
@@ -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")
+137 -7
View File
@@ -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{
+5
View File
@@ -60,3 +60,8 @@ export function getApprovalFlowPick(params) {
params
})
}
// 业务侧发起前拉可用流
export function getAvailableFlows(params) {
return request({ url: '/platform/approval/flow/available', method: 'get', params })
}
+324 -43
View File
@@ -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">支持 &gt; &gt;= &lt; &lt;= == != 与 &amp;&amp; / ||。条件仅在“条件网关”的出边上生效。</span>
<span class="hint">支持 &gt; &gt;= &lt; &lt;= == != 与 &amp;&amp; / ||。条件仅在“判断节点”的出边上生效。</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" />