批量优化

This commit is contained in:
hero920103 committed 2026-10-08 15:30:37 +08:00
1 parent 438145cae7
commit c925b198d1
50 files changed
+5716 -291

No files matched your search

+166 -32
View File
@@ -144,29 +144,43 @@
<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-form-item label="通知类型" required>
<el-select
v-model="nodeForm.notifyTypes"
multiple
collapse-tags
style="width: 100%"
placeholder="选择通知类型(可多选)"
@change="onNotifyTypesChange"
>
<el-option v-for="t in approverTypes" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<el-form-item v-else label=" ">
<span class="hint">该类型运行期自动定位通知对象,无需指定</span>
<el-form-item
v-for="t in selectedNotifyMetas"
:key="t.value"
:label="t.selectLabel || t.label"
required
>
<el-select v-model="nodeForm.notifyIdsMap[t.value]" multiple filterable style="width: 100%" :placeholder="'选择' + (t.selectLabel || t.label) + '(可多选)'">
<el-option v-for="opt in poolOf(t.poolKey)" :key="opt.id" :label="opt.name" :value="opt.id" />
</el-select>
</el-form-item>
<el-form-item v-if="!selectedNotifyMetas.length" label=" ">
<span class="hint">所选类型运行期自动定位通知对象,无需指定</span>
</el-form-item>
<el-form-item label=" ">
<span class="hint">通知节点不阻塞流程,仅发送消息给指定人员,流程自动继续往下走。</span>
<span class="hint">通知节点不阻塞流程,流程自动继续往下走。可同时选择多种类型(如「发起人本人」+「指定用户」)。</span>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button type="danger" plain :disabled="!editingNodeId" @click="removeCurrentNode">删除节点</el-button>
<span style="flex: 1"></span>
<el-button @click="nodeDialog = false">取消</el-button>
<el-button type="primary" @click="confirmNode">确定</el-button>
<div class="node-dialog-footer">
<el-button type="danger" plain :disabled="!editingNodeId" @click="removeCurrentNode">删除节点</el-button>
<span class="footer-spacer"></span>
<el-button @click="nodeDialog = false">取消</el-button>
<el-button type="primary" @click="confirmNode">确定</el-button>
</div>
</template>
</el-dialog>
@@ -249,7 +263,19 @@ function approverSummary(node: any) {
return `${approverTypeLabel(node.approverType)}:${names.length ? names.join('、') : '未选'}(${mode})`
}
if (node.nodeType === NODE_TYPE.DECISION) return '按分支条件路由'
if (node.nodeType === NODE_TYPE.NOTIFY) return '发送通知给指定人员'
if (node.nodeType === NODE_TYPE.NOTIFY) {
const types: number[] = node.notifyTypes?.length ? node.notifyTypes : [node.approverType]
const parts = types.map((t) => {
const meta = (props.approverTypes as any[]).find((x) => x.value === t)
if (!meta) return '未知类型'
if (!meta.needsSelection) return meta.label
const pool = (props.pools as any)[meta.poolKey] || []
const ids = (node.notifyIdsMap || {})[t] || []
const names = ids.map((id: number) => pool.find((p: any) => p.id === id)?.name).filter(Boolean)
return `${meta.label}:${names.length ? names.join('、') : '未选'}`
})
return '通知 ' + parts.join(' + ')
}
return ''
}
@@ -279,7 +305,7 @@ function buildGraph(payload: any) {
function makeNodeData(n: any) {
const nodeType = n.node_type || n.nodeType || NODE_TYPE.APPROVAL
const data = {
const data: any = {
nodeType,
cls: clsOf(nodeType),
name: n.node_name || n.name || '',
@@ -289,10 +315,44 @@ function makeNodeData(n: any) {
allowReturn: n.allow_return ?? n.allowReturn ?? 0,
timeoutHours: n.timeout_hours ?? n.timeoutHours ?? 48,
}
if (nodeType === NODE_TYPE.NOTIFY) {
applyNotifyData(data, n)
}
data.summary = approverSummary(data)
return data
}
// 通知节点:解析多类型存储 notify_targets: ["1:52","5:0"];兼容旧单类型格式
//(纯数字 approver_ids + approver_type),并统一归一化为 notifyTargets 字符串数组。
function applyNotifyData(data: any, n: any) {
const targets: string[] = n.notify_targets || n.notifyTargets || []
if (targets.length) {
const types: number[] = []
const map: Record<number, number[]> = {}
targets.forEach((s) => {
const [tStr, idStr] = String(s).split(':')
const t = Number(tStr)
if (!t) return
if (!map[t]) {
map[t] = []
types.push(t)
}
const id = Number(idStr)
if (id > 0) map[t].push(id)
})
data.notifyTargets = targets.map(String)
data.notifyTypes = types
data.notifyIdsMap = map
data.approverType = types[0] || data.approverType
} else {
const t = data.approverType
const ids: number[] = [...(data.approverIds || [])]
data.notifyTypes = [t]
data.notifyIdsMap = ids.length ? { [t]: ids } : {}
data.notifyTargets = ids.length ? ids.map((id: number) => `${t}:${id}`) : [`${t}:0`]
}
}
// 初始化 / 外部切换流程时重建
function reload() {
const { ns, es } = buildGraph(props.modelValue || {})
@@ -378,6 +438,9 @@ const nodeForm = reactive<any>({
multiMode: 1,
allowReturn: 0,
timeoutHours: 48,
// 通知节点多类型:选中的类型 + 各类型对应的对象 ID
notifyTypes: [] as number[],
notifyIdsMap: {} as Record<number, number[]>,
})
const nodeDialogTitle = computed(() => {
@@ -395,11 +458,28 @@ const currentPool = computed(() => {
return meta ? (props.pools as any)[meta.poolKey] || [] : []
})
const poolOf = (poolKey: string) => (props.pools as any)[poolKey] || []
// 通知节点:已选类型中需要手动指定对象的元信息
const selectedNotifyMetas = computed(() =>
(props.approverTypes as any[]).filter(
(t) => (nodeForm.notifyTypes || []).includes(t.value) && t.needsSelection
)
)
function onNotifyTypesChange(vals: number[]) {
const keep = new Set<number>(vals)
Object.keys(nodeForm.notifyIdsMap).forEach((k) => {
if (!keep.has(Number(k))) delete nodeForm.notifyIdsMap[k]
})
}
function onNodeDblClick(evt: any) {
const node = evt.node
if (!node) return
editingNodeId.value = node.id
const d = node.data
const isNotify = d.nodeType === NODE_TYPE.NOTIFY
Object.assign(nodeForm, {
name: d.name,
nodeType: d.nodeType,
@@ -408,6 +488,8 @@ function onNodeDblClick(evt: any) {
multiMode: d.multiMode,
allowReturn: d.allowReturn,
timeoutHours: d.timeoutHours,
notifyTypes: isNotify ? [...(d.notifyTypes || [d.approverType])] : [],
notifyIdsMap: isNotify ? JSON.parse(JSON.stringify(d.notifyIdsMap || {})) : {},
})
nodeDialog.value = true
}
@@ -426,9 +508,44 @@ function confirmNode() {
ElMessage.warning('请填写节点名称')
return
}
// 通知节点:多类型 + 各类型对象,归一化为 "类型:ID" 列表
if (nodeForm.nodeType === NODE_TYPE.NOTIFY) {
if (!nodeForm.notifyTypes.length) {
ElMessage.warning('请选择通知类型')
return
}
for (const t of nodeForm.notifyTypes) {
const m = (props.approverTypes as any[]).find((x) => x.value === t)
if (m?.needsSelection && !(nodeForm.notifyIdsMap[t] || []).length) {
ElMessage.warning(`请选择「${m.label}」的通知对象`)
return
}
}
const targets: string[] = []
nodeForm.notifyTypes.forEach((t: number) => {
const ids = nodeForm.notifyIdsMap[t] || []
if (ids.length) {
ids.forEach((id: number) => targets.push(`${t}:${id}`))
} else {
targets.push(`${t}:0`)
}
})
node.data = makeNodeData({
node_type: NODE_TYPE.NOTIFY,
node_name: nodeForm.name,
notify_targets: targets,
approver_type: nodeForm.notifyTypes[0],
approver_ids: [],
multi_mode: nodeForm.multiMode,
allow_return: nodeForm.allowReturn,
timeout_hours: nodeForm.timeoutHours,
})
nodeDialog.value = false
return
}
const meta = currentApproverMeta.value
if ((nodeForm.nodeType === NODE_TYPE.APPROVAL || nodeForm.nodeType === NODE_TYPE.NOTIFY) && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
ElMessage.warning(nodeForm.nodeType === NODE_TYPE.NOTIFY ? '请选择通知对象' : '请选择审批人')
if (nodeForm.nodeType === NODE_TYPE.APPROVAL && meta && meta.needsSelection && !(nodeForm.approverIds || []).length) {
ElMessage.warning('请选择审批人')
return
}
node.data = makeNodeData({
@@ -481,19 +598,25 @@ function removeCurrentEdge() {
// ==================== 对外序列化 ====================
function serialize() {
const outNodes = nodes.value.map((n: any, i: number) => ({
node_key: n.id,
node_name: n.data.name,
node_type: n.data.nodeType,
approver_type: n.data.approverType,
approver_ids: n.data.approverIds,
multi_mode: n.data.multiMode,
allow_return: n.data.allowReturn,
timeout_hours: n.data.timeoutHours,
sort: i + 1,
pos_x: Math.round(n.position.x),
pos_y: Math.round(n.position.y),
}))
const outNodes = nodes.value.map((n: any, i: number) => {
const isNotify = n.data.nodeType === NODE_TYPE.NOTIFY
return {
node_key: n.id,
node_name: n.data.name,
node_type: n.data.nodeType,
approver_type: isNotify
? (n.data.notifyTypes?.length ? n.data.notifyTypes[0] : n.data.approverType)
: n.data.approverType,
approver_ids: isNotify ? [] : n.data.approverIds,
notify_targets: isNotify ? n.data.notifyTargets || [] : [],
multi_mode: n.data.multiMode,
allow_return: n.data.allowReturn,
timeout_hours: n.data.timeoutHours,
sort: i + 1,
pos_x: Math.round(n.position.x),
pos_y: Math.round(n.position.y),
}
})
const outEdges = edges.value.map((e: any) => ({
from: e.source,
to: e.target,
@@ -841,4 +964,15 @@ defineExpose({ serialize, fitView })
margin-bottom: 4px;
color: #ffd04b;
}
// 节点弹窗底部:删除靠左,取消/确定靠右
.node-dialog-footer {
display: flex;
align-items: center;
width: 100%;
.footer-spacer {
flex: 1;
}
}
</style>