Browse Source

feat: 包容分支添加完毕

luoyali 11 months ago
parent
commit
b10f05367a

+ 1 - 0
src/assets/icons/flow/merge.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721889038462" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7639" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M149.333333 64c-46.933333 0-85.333333 38.4-85.333333 85.333333v469.333334c0 46.933333 38.4 85.333333 85.333333 85.333333h170.666667v170.666667c0 46.933333 38.4 85.333333 85.333333 85.333333h469.333334c46.933333 0 85.333333-38.4 85.333333-85.333333v-469.333334c0-46.933333-38.4-85.333333-85.333333-85.333333h-170.666667v-170.666667c0-46.933333-38.4-85.333333-85.333333-85.333333h-469.333334m0 85.333333h469.333334v170.666667h-213.333334c-46.933333 0-85.333333 38.4-85.333333 85.333333v213.333334h-170.666667v-469.333334m256 256h213.333334v213.333334h-213.333334v-213.333334m298.666667 0h170.666667v469.333334h-469.333334v-170.666667h213.333334c46.933333 0 85.333333-38.4 85.333333-85.333333v-213.333334z" p-id="7640"></path></svg>

+ 12 - 0
src/components/scWorkflow/index.vue

@@ -727,4 +727,16 @@ $header_height: 55px;
 		}
 	}
 }
+
+// 公用的样式
+.svg-icon-box {
+	position: absolute;
+	bottom: -20px;
+	right: 50%;
+	background-color: rgb(255, 255, 255);
+	padding: 10px;
+	border-radius: 50%;
+	margin-right: -18px;
+	z-index: 99;
+}
 </style>

+ 10 - 1
src/components/scWorkflow/nodeWrap.vue

@@ -31,6 +31,13 @@
 		</template>
 	</parallel-branch>
 
+	<!-- 包容分支 -->
+	<merge-branch v-if="nodeConfig.type == 9" v-model="nodeConfig" :disabled="disabled">
+		<template #default="slot">
+			<node-wrap v-if="slot.node" v-model="slot.node.childNode" :disabled="disabled"></node-wrap>
+		</template>
+	</merge-branch>
+
 	<node-wrap v-if="nodeConfig.childNode" v-model="nodeConfig.childNode" :disabled="disabled"></node-wrap>
 </template>
 
@@ -39,6 +46,7 @@ import approver from './nodes/approver'
 import promoter from './nodes/promoter'
 import branch from './nodes/branch'
 import parallelBranch from './nodes/parallelBranch'
+import mergeBranch from './nodes/mergeBranch'
 import send from './nodes/send'
 import delayProcess from './nodes/delayProcess'
 import trigger from './nodes/trigger'
@@ -53,7 +61,8 @@ export default {
 		delayProcess,
 		trigger,
 		subProcess,
-		parallelBranch
+		parallelBranch,
+		mergeBranch
 	},
 	props: {
 		modelValue: { type: Object, default: () => {} },

+ 30 - 0
src/components/scWorkflow/nodes/addNode.vue

@@ -35,6 +35,10 @@
 							<el-icon style="color: #626aef" @click="addType(8)"><Operation /></el-icon>
 							<p>并行分支</p>
 						</li>
+						<li>
+							<el-icon style="color: #345da2" @click="addType(9)"><CopyDocument /></el-icon>
+							<p>包容分支</p>
+						</li>
 					</ul>
 				</div>
 			</el-popover>
@@ -176,6 +180,32 @@ export default {
 					],
 					childNode: this.modelValue
 				}
+			} else if (type === 9) {
+				const _key = getNodeKey()
+				node = {
+					nodeName: '包容路由',
+					nodeKey: _key,
+					type: 9,
+					parallelNodes: [
+						{
+							nodeName: '包容条件1',
+							nodeKey: _key + 1,
+							type: 3,
+							priorityLevel: 1,
+							conditionMode: 1,
+							conditionList: []
+						},
+						{
+							nodeName: '包容条件2',
+							nodeKey: _key + 2,
+							type: 3,
+							priorityLevel: 2,
+							conditionMode: 1,
+							conditionList: []
+						}
+					],
+					childNode: this.modelValue
+				}
 			}
 			this.$emit('update:modelValue', node)
 		}

+ 445 - 0
src/components/scWorkflow/nodes/mergeBranch.vue

@@ -0,0 +1,445 @@
+<template>
+	<!--  `branch-wrap--${nodeConfig.local_status}`-->
+	<div class="branch-wrap">
+		<div class="branch-box-wrap">
+			<div class="branch-box" style="margin-bottom: 15px">
+				<el-button class="add-branch" color="#345da2" plain round @click="addTerm"> 添加条件 </el-button>
+				<div v-for="(item, index) in nodeConfig.parallelNodes" :key="index" class="col-box">
+					<div class="condition-node">
+						<div class="condition-node-box">
+							<div :class="['auto-judge', `auto-judge--${item.local_status}`]" @click="show(index)">
+								<!-- 不是第一个 也不是 最后一个-->
+								<div v-if="index != 0 && index != nodeConfig.parallelNodes.length - 1" class="sort-left" @click.stop="arrTransfer(index, -1)">
+									<el-icon><ArrowLeft /></el-icon>
+								</div>
+								<div class="title">
+									<span style="display: inline-block; width: 115px"
+										><le-text class="flex-1" tag="b" :value="index === nodeConfig.parallelNodes.length - 1 ? '默认条件' : item.nodeName"
+									/></span>
+									<div class="close" v-if="index != nodeConfig.parallelNodes.length - 1">
+										<el-icon class="mx-1" @click.stop="copyTerm(index)">
+											<CopyDocument />
+										</el-icon>
+										<el-icon @click.stop="delTerm(index)">
+											<Close />
+										</el-icon>
+									</div>
+
+								</div>
+								<div
+									class="content"
+									:class="[index === nodeConfig.parallelNodes.length - 1 ? 'last-child-title' : '']"
+									style="width: 200px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis"
+								>
+									<span v-if="toText(nodeConfig, index)" :title="toText(nodeConfig, index)">{{ toText(nodeConfig, index) }}</span>
+									<span v-else class="placeholder"> 请设置条件 </span>
+								</div>
+								<!-- 最后一个没有,长度大于2倒数第二个没有 -->
+								<div
+									v-if="
+										!(nodeConfig.parallelNodes.length == 2 && (index === nodeConfig.parallelNodes.length - 1 || index === 0)) &&
+										!(
+											(nodeConfig.parallelNodes.length > 2 && index === nodeConfig.parallelNodes.length - 1) ||
+											index === nodeConfig.parallelNodes.length - 2
+										)
+									"
+									class="sort-right"
+									@click.stop="arrTransfer(index)"
+								>
+									<el-icon><ArrowRight /></el-icon>
+								</div>
+							</div>
+							<add-node v-model="item.childNode" :disabled="disabled"></add-node>
+						</div>
+					</div>
+					<slot v-if="item.childNode" :node="item"></slot>
+					<div v-if="index == 0" class="top-left-cover-line"></div>
+					<div v-if="index == 0" class="bottom-left-cover-line"></div>
+					<div v-if="index == nodeConfig.parallelNodes.length - 1" class="top-right-cover-line"></div>
+					<div v-if="index == nodeConfig.parallelNodes.length - 1" class="bottom-right-cover-line"></div>
+				</div>
+				<div class="svg-icon-box">
+					<svg-icon style="font-size: 18px" color="#345da2" icon-class="flow-merge" />
+				</div>
+			</div>
+			<add-node v-model="nodeConfig.childNode" :disabled="disabled"></add-node>
+		</div>
+		<el-drawer v-model="drawer" title="条件设置" destroy-on-close append-to-body :size="600">
+			<template #header>
+				<div class="node-wrap-drawer__title">
+					<label v-if="!isEditTitle" @click="editTitle">
+						{{ form.nodeName }}<el-icon class="node-wrap-drawer__title-edit"><Edit /></el-icon>
+						<!--						<div @click="rmConditionGroup(conditionGroup)">-->
+						<!--							-->
+						<!--						</div>-->
+					</label>
+					<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
+				</div>
+			</template>
+			<el-container>
+				<el-main style="padding: 0 0 20px 0">
+					<div class="top-tips">满足以下条件时进入当前分支</div>
+					<template v-for="(conditionGroup, conditionGroupIdx) in form.conditionList" :key="conditionGroupIdx">
+						<div v-if="conditionGroupIdx != 0" class="or-branch-link-tip">或满足</div>
+						<div class="condition-group-editor">
+							<div class="header">
+								<span>条件组 {{ conditionGroupIdx + 1 }}</span>
+								<div @click="deleteConditionGroup(conditionGroupIdx)">
+									<el-icon class="branch-delete-icon"><Delete /></el-icon>
+								</div>
+							</div>
+
+							<div class="main-content">
+								<!-- 单个条件 -->
+								<div class="condition-content-box cell-box">
+									<div v-if="false">描述</div>
+									<div>条件字段</div>
+									<div>运算符</div>
+									<div>值</div>
+								</div>
+								<div v-for="(condition, idx) in conditionGroup" :key="idx" class="condition-content">
+									<div class="condition-relation">
+										<span>{{ idx == 0 ? '当' : '且' }}</span>
+										<div v-if="conditionGroup.length > 1" @click="deleteConditionList(conditionGroup, idx)">
+											<el-icon class="branch-delete-icon"><Delete /></el-icon>
+										</div>
+									</div>
+									<div class="condition-content">
+										<div class="condition-content-box">
+											<el-input
+												v-if="condition.type === 'custom'"
+												v-model="condition.label"
+												placeholder="自定义条件字段"
+												@input="getCurrentItemField(conditionGroupIdx, idx)"
+											/>
+											<el-select
+												v-if="condition.type === 'form'"
+												v-model="condition.field"
+												filterable
+												placeholder="表单条件字段"
+												@change="getCurrentItemLabel(conditionGroupIdx, idx)"
+											>
+												<el-option v-for="{ id, label, key } in expressionFormList" :key="id" :label="label" :value="key" />
+											</el-select>
+											<el-select v-model="condition.operator" placeholder="请选择表达式">
+												<el-option v-for="{ value, label } in operatorType" :key="label" :label="label" :value="value"></el-option>
+											</el-select>
+											<el-input v-model="condition.value" placeholder="值" />
+										</div>
+									</div>
+								</div>
+							</div>
+							<div class="sub-content">
+								<el-button link type="primary" :icon="Plus" @click="addConditionList(conditionGroup, 'custom')"> 添加自定义条件 </el-button>
+								<el-button
+									v-if="expressionFormList.length"
+									link
+									type="primary"
+									:icon="Plus"
+									style="margin-left: 8px"
+									@click="addConditionList(conditionGroup, 'form')"
+								>
+									添加表单条件
+								</el-button>
+							</div>
+						</div>
+					</template>
+					<el-button style="width: 100%" type="info" :icon="Plus" text bg @click="addConditionGroup"> 添加条件组 </el-button>
+				</el-main>
+				<el-footer>
+					<el-button type="primary" @click="save"> 保存 </el-button>
+					<el-button @click="drawer = false">取消</el-button>
+				</el-footer>
+			</el-container>
+		</el-drawer>
+	</div>
+</template>
+
+<script>
+import addNode from './addNode.vue'
+import { operatorType } from './config'
+import useFlowStore from '@/store/modules/flow'
+import { Delete, Plus, ArrowLeft, Close, ArrowRight, Edit } from '@element-plus/icons-vue'
+import { mapState } from 'pinia'
+import { getNodeKey } from '@/utils/workflow'
+
+export default {
+	components: {
+		addNode
+	},
+	props: {
+		modelValue: { type: Object, default: () => {} },
+		disabled: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data() {
+		return {
+			operatorType,
+			nodeConfig: {},
+			drawer: false,
+			isEditTitle: false,
+			index: 0,
+			form: {},
+			expressionFormList: []
+		}
+	},
+	computed: {
+		Plus() {
+			return Plus
+		},
+		...mapState(useFlowStore, ['processForm']) //映射函数,取出processForm
+	},
+	watch: {
+		modelValue() {
+			this.nodeConfig = this.modelValue
+		}
+	},
+	mounted() {
+		this.nodeConfig = this.modelValue
+	},
+	methods: {
+		show(index) {
+			if (this.disabled) return
+			if (index === this.nodeConfig.parallelNodes.length - 1) {
+				// 最后一个节点不能编辑
+				return
+			}
+			this.index = index
+			this.form = {}
+			this.form = JSON.parse(JSON.stringify(this.nodeConfig.parallelNodes[index]))
+			const { formStructure } = JSON.parse(this.processForm) // 表单设计字段
+			// 使用 filter 方法找到 required 属性为 true 的对象
+			const requiredList = (formStructure?.fields || []).filter(item => item.options && item.options.required === true)
+			console.log(`%c 这里打印出符合条件的表单对象-=== ${JSON.stringify(requiredList)}`, 'background: orange; color: #fff')
+			this.expressionFormList = requiredList || []
+			this.drawer = true
+		},
+		editTitle() {
+			this.isEditTitle = true
+			this.$nextTick(() => {
+				this.$refs.nodeTitle.focus()
+			})
+		},
+		saveTitle() {
+			this.isEditTitle = false
+		},
+		save() {
+			this.nodeConfig.parallelNodes[this.index] = this.form
+			this.$emit('update:modelValue', this.nodeConfig)
+			this.drawer = false
+		},
+		addTerm() {
+			let len = this.nodeConfig.parallelNodes.length + 1
+			this.nodeConfig.parallelNodes.push({
+				nodeName: '包容条件' + len,
+				nodeKey: getNodeKey(),
+				type: 3,
+				priorityLevel: len,
+				conditionMode: 1,
+				conditionList: []
+			})
+		},
+		delTerm(index) {
+			this.nodeConfig.parallelNodes.splice(index, 1)
+			if (this.nodeConfig.parallelNodes.length == 1) {
+				if (this.nodeConfig.childNode) {
+					if (this.nodeConfig.parallelNodes[0].childNode) {
+						this.reData(this.nodeConfig.parallelNodes[0].childNode, this.nodeConfig.childNode)
+					} else {
+						this.nodeConfig.parallelNodes[0].childNode = this.nodeConfig.childNode
+					}
+				}
+				this.$emit('update:modelValue', this.nodeConfig.parallelNodes[0].childNode)
+			}
+		},
+		copyTerm(index) {
+			let len = this.nodeConfig.parallelNodes.length + 1
+			const currentNode = JSON.parse(JSON.stringify(this.nodeConfig.parallelNodes[index]))
+			const item = {
+				...currentNode,
+				nodeKey: getNodeKey(),
+				nodeName: currentNode.nodeName + '-Copy',
+				priorityLevel: len
+			}
+			this.nodeConfig.parallelNodes.push(item)
+			this.$emit('update:modelValue', this.nodeConfig)
+		},
+		reData(data, addData) {
+			if (!data.childNode) {
+				data.childNode = addData
+			} else {
+				this.reData(data.childNode, addData)
+			}
+		},
+		arrTransfer(index, type = 1) {
+			this.nodeConfig.parallelNodes[index] = this.nodeConfig.parallelNodes.splice(index + type, 1, this.nodeConfig.parallelNodes[index])[0]
+			this.nodeConfig.parallelNodes.map((item, index) => {
+				item.priorityLevel = index + 1
+			})
+			this.$emit('update:modelValue', this.nodeConfig)
+		},
+		addConditionList(conditionList, type) {
+			conditionList.push({
+				label: '',
+				field: '',
+				operator: '==',
+				value: '',
+				type
+			})
+		},
+		deleteConditionList(conditionList, index) {
+			conditionList.splice(index, 1)
+		},
+		addConditionGroup() {
+			this.addConditionList(this.form.conditionList[this.form.conditionList.push([]) - 1], 'custom')
+		},
+		deleteConditionGroup(index) {
+			this.form.conditionList.splice(index, 1)
+		},
+		toText(nodeConfig, index) {
+			var { conditionList } = nodeConfig.parallelNodes[index]
+			if (conditionList && conditionList.length == 1) {
+				const text = conditionList.map(conditionGroup =>
+					conditionGroup
+						.map(item => {
+							const showOperator = this.operatorType.find(i => i.value === item.operator).label
+							if (item.type === 'form') {
+								return `${item.showLabel}${showOperator}${item.value}`
+							} else {
+								return `${item.label}${showOperator}${item.value}`
+							}
+						})
+						.join(' 和 ')
+				)
+				return text
+			} else if (conditionList && conditionList.length > 1) {
+				return conditionList.length + '个条件,或满足'
+			} else {
+				if (index == nodeConfig.parallelNodes.length - 1) {
+					return '未满足时其他条件时,将进入默认流程'
+				} else {
+					return false
+				}
+			}
+		},
+		getCurrentItemLabel(conditionIdx, idx) {
+			const currentCondition = this.form.conditionList[conditionIdx]
+			const field = currentCondition[idx].field
+			const labelObj = this.expressionFormList.find(i => i.key === field)
+			currentCondition[idx].showLabel = labelObj.label
+			currentCondition[idx].label = field
+		},
+		getCurrentItemField(conditionIdx, idx) {
+			const currentCondition = this.form.conditionList[conditionIdx]
+			currentCondition[idx].field = currentCondition[idx].label
+		}
+	}
+}
+</script>
+
+<style scoped lang="scss">
+.top-tips {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 12px;
+	color: #646a73;
+}
+
+.or-branch-link-tip {
+	margin: 10px 0;
+	color: #646a73;
+}
+
+.condition-group-editor {
+	user-select: none;
+	border-radius: 4px;
+	border: 1px solid #e4e5e7;
+	position: relative;
+	margin-bottom: 16px;
+
+	.branch-delete-icon {
+		font-size: 18px;
+	}
+
+	.header {
+		background-color: #f4f6f8;
+		padding: 0 12px;
+		font-size: 14px;
+		color: #171e31;
+		height: 36px;
+		display: flex;
+		align-items: center;
+
+		span {
+			flex: 1;
+		}
+	}
+
+	.main-content {
+		padding: 0 12px;
+
+		.condition-relation {
+			color: #9ca2a9;
+			display: flex;
+			align-items: center;
+			height: 36px;
+			display: flex;
+			justify-content: space-between;
+			padding: 0 2px;
+		}
+
+		.condition-content-box {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+
+			div {
+				width: 100%;
+				min-width: 120px;
+			}
+
+			div:not(:first-child) {
+				margin-left: 16px;
+			}
+		}
+
+		.cell-box {
+			div {
+				padding: 16px 0;
+				width: 100%;
+				min-width: 120px;
+				color: #909399;
+				font-size: 14px;
+				font-weight: 600;
+				text-align: center;
+			}
+		}
+
+		.condition-content {
+			display: flex;
+			flex-direction: column;
+
+			:deep(.el-input__wrapper) {
+				border-top-left-radius: 0;
+				border-bottom-left-radius: 0;
+			}
+
+			.content {
+				flex: 1;
+				padding: 0 0 4px 0;
+				display: flex;
+				align-items: center;
+				min-height: 31.6px;
+				flex-wrap: wrap;
+			}
+		}
+	}
+
+	.sub-content {
+		padding: 12px;
+	}
+}
+</style>

+ 6 - 15
src/components/scWorkflow/nodes/parallelBranch.vue

@@ -37,18 +37,7 @@
 					<div v-if="index == nodeConfig.parallelNodes.length - 1" class="top-right-cover-line"></div>
 					<div v-if="index == nodeConfig.parallelNodes.length - 1" class="bottom-right-cover-line"></div>
 				</div>
-				<div
-					style="
-						position: absolute;
-						bottom: -20px;
-						right: 50%;
-						background-color: rgb(255, 255, 255);
-						padding: 10px;
-						border-radius: 50%;
-						margin-right: -18px;
-						z-index: 99;
-					"
-				>
+				<div class="svg-icon-box">
 					<svg-icon style="font-size: 18px" color="#626aef" icon-class="flow-paraller-branch" />
 				</div>
 			</div>
@@ -71,7 +60,7 @@
 import addNode from './addNode.vue'
 import { Delete, Plus, ArrowLeft, Close, ArrowRight, Edit } from '@element-plus/icons-vue'
 import { getNodeKey } from '@/utils/workflow'
-
+import _ from 'lodash-es'
 export default {
 	components: {
 		addNode
@@ -103,9 +92,10 @@ export default {
 	},
 	methods: {
 		show(index) {
+			const self = this
 			this.index = index
 			this.form = {}
-			this.form = JSON.parse(JSON.stringify(this.nodeConfig.parallelNodes[index]))
+			this.form = _.cloneDeep(self.nodeConfig.parallelNodes[index])
 			this.drawer = true
 		},
 		editTitle() {
@@ -148,8 +138,9 @@ export default {
 		},
 		// 复制分支
 		copyTerm(index) {
+			const self = this
 			let len = this.nodeConfig.parallelNodes.length + 1
-			const currentNode = this.nodeConfig.parallelNodes[index]
+			const currentNode =_.cloneDeep(self.nodeConfig.parallelNodes[index])
 			const item = {
 				...currentNode,
 				nodeKey: getNodeKey(),