Переглянути джерело

Merge branch 'refs/heads/feature/routeBranch'

lanceJiang 6 місяців тому
батько
коміт
5d4522342b

+ 1 - 1
src/assets/icons/route.svg

@@ -1 +1 @@
-<svg t="1612242856287" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5203" width="128" height="128"><path d="M422.4 395.392l-27.2-327.68L256.768 194.88 114.688 64 65.472 109.12 207.616 240 69.184 367.168 422.4 395.392zM960 66.752 604.096 91.776l138.112 127.488L600.064 350.08l49.216 45.312 142.016-130.88 138.112 127.552L960 66.752zM423.936 675.392 374.848 630.016l-142.144 130.88L94.592 633.344 64 958.656l355.84-25.024-138.112-127.488L423.936 675.392zM958.528 914.88l-142.016-130.88 138.368-127.104L601.472 628.608l27.328 327.744 138.432-127.232L909.248 960 958.528 914.88z" p-id="5204" fill="#666666"></path></svg>
+<svg t="1733031414875" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24951" width="200" height="200"><path d="M47.104 744.448c0-81.408 66.048-147.968 147.456-147.968 64 0 120.832 40.96 140.288 101.376 78.848-16.896 140.288-91.136 144.384-182.784v-13.824c0-123.904 87.04-228.352 203.264-247.808 14.848-80.384 91.136-133.632 171.52-119.296S988.16 225.28 973.312 305.664 882.176 439.296 801.792 424.96c-49.152-8.704-90.112-41.472-109.568-87.552-80.896 15.36-144.896 89.6-148.48 183.808v14.336c0 61.44-21.504 120.32-61.44 166.4H686.08c23.552-78.336 105.984-122.368 184.32-98.816 62.464 18.432 104.96 75.776 105.472 140.8 0 81.408-66.048 147.968-147.456 147.968-65.536 0-122.88-43.008-141.824-105.984H336.896l-1.536 5.12c-26.112 77.824-110.592 119.296-187.392 92.672-59.904-19.968-100.352-76.288-100.864-139.264z m718.336 0c0 34.816 28.16 63.488 63.488 63.488 34.816 0 63.488-28.16 63.488-63.488 0-34.816-28.16-63.488-63.488-63.488-34.816 0-63.488 28.16-63.488 63.488z m0-464.384c0 34.816 28.16 63.488 63.488 63.488 34.816 0 63.488-28.16 63.488-63.488s-28.16-63.488-63.488-63.488c-34.816 0-63.488 28.16-63.488 63.488zM131.584 744.448c0 34.816 28.16 63.488 63.488 63.488s63.488-28.16 63.488-63.488c0-34.816-28.16-63.488-63.488-63.488s-63.488 28.16-63.488 63.488z" fill="currentColor" p-id="24952"></path></svg>

+ 6 - 1
src/components/scWorkflow/index.vue

@@ -40,7 +40,8 @@ export default {
 	},
 	provide() {
 		return {
-			select: this.selectHandle
+			select: this.selectHandle,
+			getRootConfig: this.getCurrentConfig
 		}
 	},
 	props: {
@@ -74,6 +75,9 @@ export default {
 	},
 	mounted() {},
 	methods: {
+		getCurrentConfig() {
+			return this.nodeConfig
+		},
 		update_activeSelected(newSelected) {
 			this.activeSelected.splice(0, this.activeSelected.length, ...newSelected)
 		},
@@ -655,6 +659,7 @@ $bg_color: #f6f8f9;
 }
 .node-wrap-drawer__title label {
 	cursor: pointer;
+	font-size: 14px;
 }
 .node-wrap-drawer__title label:hover {
 	border-bottom: 1px dashed var(--el-color-primary);

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

@@ -37,6 +37,12 @@
 			<node-wrap v-if="slot.node" v-model="slot.node.childNode" :disabled="disabled"></node-wrap>
 		</template>
 	</merge-branch>
+	<!-- 路由分支 -->
+	<route-branch v-if="nodeConfig.type == 10" v-model="nodeConfig" :disabled="disabled">
+		<template #default="slot">
+			<node-wrap v-if="slot.node" v-model="slot.node.childNode" :disabled="disabled"></node-wrap>
+		</template>
+	</route-branch>
 
 	<node-wrap v-if="nodeConfig.childNode" v-model="nodeConfig.childNode" :disabled="disabled"></node-wrap>
 </template>
@@ -51,6 +57,7 @@ import send from './nodes/send'
 import delayProcess from './nodes/delayProcess'
 import trigger from './nodes/trigger'
 import subProcess from './nodes/subProcess'
+import routeBranch from './nodes/routeBranch'
 
 export default {
 	components: {
@@ -62,7 +69,8 @@ export default {
 		trigger,
 		subProcess,
 		parallelBranch,
-		mergeBranch
+		mergeBranch,
+		routeBranch
 	},
 	props: {
 		modelValue: { type: Object, default: () => {} },

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

@@ -39,6 +39,10 @@
 							<el-icon style="color: #345da2" @click="addType(9)"><CopyDocument /></el-icon>
 							<p>包容分支</p>
 						</li>
+						<li>
+							<el-icon class="text-red-600" @click="addType(10)"><SvgIcon icon-class="route" /></el-icon>
+							<p>路由分支</p>
+						</li>
 					</ul>
 				</div>
 			</el-popover>
@@ -206,6 +210,26 @@ export default {
 							conditionList: []
 						}
 					],
+					childNode: this.modelValue
+				}
+			} else if (type === 10) {
+				const _key = getNodeKey()
+				node = {
+					nodeName: '路由分支',
+					nodeKey: _key,
+					type: 10,
+
+					routeNodes: [
+						/*{
+							nodeName: '条件1',
+							nodeKey: _key + 1,
+							type: 3,  // ==>  type: 23
+							priorityLevel: 1,
+							conditionMode: 1,
+							conditionList: []
+						},*/
+					],
+
 					childNode: this.modelValue
 				}
 			}

+ 5 - 1
src/components/scWorkflow/nodes/branch.vue

@@ -200,7 +200,7 @@ export default {
 		},
 		processForm: {
 			handler(processForm) {
-				// console.error(val, 'processForm  change...', typeof val)
+				// console.error(processForm, 'processForm  change...', typeof processForm)
 				const { formStructure } = JSON.parse(processForm) // 表单设计字段
 				this.local_formStructure = formStructure
 				// 使用 filter 方法找到 required 属性为 true 的对象
@@ -370,6 +370,10 @@ export default {
 
 	.branch-delete-icon {
 		font-size: 18px;
+		cursor: pointer;
+		&:hover {
+			color: var(--el-color-danger);
+		}
 	}
 
 	.header {

+ 450 - 0
src/components/scWorkflow/nodes/routeBranch.vue

@@ -0,0 +1,450 @@
+<!--路由分支-->
+<template>
+	<div class="node-wrap">
+		<div class="node-wrap-box" :class="[disabled ? 'node-wrap-box--disabled' : '', `node-wrap-box--${nodeConfig.local_status}`]">
+			<div class="title bg-red-600">
+				<el-icon class="icon"><SvgIcon icon-class="route" /></el-icon>
+				<span v-show="!isEditTitle" class="title_label" @click="editTitle('box_nodeTitle')"
+					>{{ nodeConfig.nodeName }}<el-icon v-if="!disabled" class="edit-icon"><edit /></el-icon
+				></span>
+				<el-input
+					v-show="isEditTitle"
+					ref="box_nodeTitle"
+					v-model="nodeConfig.nodeName"
+					clearable
+					size="small"
+					@blur="saveTitle"
+					@keyup.enter="saveTitle"
+				></el-input>
+				<el-icon v-if="!disabled" class="close" @click.stop="delNode()"><close /></el-icon>
+			</div>
+			<div class="content" @click="show">
+				<div v-html="toText(nodeConfig)" />
+			</div>
+		</div>
+		<add-node v-model="nodeConfig.childNode" :disabled="disabled"></add-node>
+		<el-drawer v-model="drawer" title="路由分支设置" destroy-on-close append-to-body :size="600" class="aDrawer">
+			<template #header>
+				<div class="node-wrap-drawer__title">
+					<label v-show="!isEditTitle" @click="editTitle('nodeTitle')"
+						>{{ form.nodeName }}<el-icon class="node-wrap-drawer__title-edit"><edit /></el-icon
+					></label>
+					<el-input
+						v-show="isEditTitle"
+						ref="nodeTitle"
+						v-model="form.nodeName"
+						clearable
+						class="w-40"
+						@blur="saveTitle"
+						@keyup.enter="saveTitle"
+					></el-input>
+					<el-input v-model="form.nodeKey" clearable class="w-40 pl-1.5" placeholder="请填写nodeKey"></el-input>
+				</div>
+			</template>
+			<el-container>
+				<el-main>
+					<el-form ref="ruleFormRef" label-position="top" :model="form">
+						<el-card v-for="(route, index) of form.routeNodes" :key="route.nodeKey" shadow="hover" class="mb-[10px]">
+							<template #header>
+								<div class="flex justify-between route-header">
+									<label
+										v-show="!route.local_isEdit"
+										class="inline-flex items-center cursor-pointer text-[14px]"
+										@click="editRouteTitle(route, index)"
+										>{{ route.nodeName }}<el-icon class="le-link ml-2"><edit /></el-icon
+									></label>
+									<el-input
+										v-show="route.local_isEdit"
+										ref="routeNodeName"
+										v-model="route.nodeName"
+										size="small"
+										clearable
+										class="w-40"
+										@blur="saveRouteTitle(route)"
+										@keyup.enter="saveRouteTitle(route)"
+									></el-input>
+									<el-form-item
+										style="margin-bottom: 0"
+										class="ml-2"
+										:prop="`routeNodes.${index}.nodeKey`"
+										:rules="[{ required: true, message: '请选择流程节点' }]"
+									>
+										<el-select v-model="route.nodeKey" size="small" placeholder="请选择流程节点">
+											<el-option v-for="item in validNodeList" :key="item.nodeKey" :label="item.nodeName" :value="item.nodeKey">
+												<div class="w-[230px] flex justify-between -ml-[8px] -mr-[20px]">
+													<le-text :value="item.nodeName || '-'"></le-text>
+													<span class="text-gray-400">{{ item.nodeKey }}</span>
+												</div>
+											</el-option>
+										</el-select>
+									</el-form-item>
+									<el-button class="ml-auto" link type="danger" icon="Delete" @click="removeRouteNode(index)" />
+								</div>
+							</template>
+							<template v-for="(conditionGroup, conditionGroupIdx) in route.conditionList" :key="conditionGroupIdx">
+								<div v-if="conditionGroupIdx === 0" class="tip">满足以下条件时进入当前分支</div>
+								<div v-else class="tip mt-[10px]">或满足</div>
+								<div class="condition-group-editor">
+									<div class="header">
+										<span>条件组 {{ conditionGroupIdx + 1 }}</span>
+										<div @click="deleteConditionGroup(route, 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(conditionGroup, idx)"
+													/>
+													<el-select
+														v-if="condition.type === 'form'"
+														v-model="condition.field"
+														filterable
+														placeholder="表单条件字段"
+														@change="getCurrentItemLabel(conditionGroup, 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(route)"> 添加条件组 </el-button>
+						</el-card>
+						<!--						<el-form-item label="选择要抄送的人员">
+						</el-form-item>
+						<el-form-item label="">
+							<el-checkbox v-model="form.allowSelection" label="允许发起人自选抄送人"></el-checkbox>
+						</el-form-item>-->
+						<el-button style="width: 100%" type="primary" icon="Plus" @click="addRouteNode"> 添加路由分支 </el-button>
+					</el-form>
+				</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 lang="jsx">
+import addNode from './addNode'
+import { operatorType } from './config'
+import { ElMessage } from 'element-plus'
+import { mapState } from 'pinia'
+import useFlowStore from '@/store/modules/flow'
+import { getNodeKey } from '@/utils/workflow'
+import { $log } from '@/utils'
+
+export default {
+	components: {
+		addNode
+	},
+	inject: ['getRootConfig'],
+	props: {
+		modelValue: { type: Object, default: () => {} },
+		disabled: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data() {
+		return {
+			operatorType: JSON.parse(JSON.stringify(operatorType)),
+			nodeConfig: {},
+			drawer: false,
+			isEditTitle: false,
+			form: {},
+			expressionFormList: [],
+			validNodeList: []
+		}
+	},
+	computed: {
+		...mapState(useFlowStore, ['processForm']) //映射函数,取出processForm
+	},
+	watch: {
+		modelValue() {
+			this.nodeConfig = this.modelValue
+		},
+		processForm: {
+			handler(processForm) {
+				const { formStructure } = JSON.parse(processForm) // 表单设计字段
+				this.local_formStructure = formStructure
+				// 使用 filter 方法找到 required 属性为 true 的对象
+				this.expressionFormList = (formStructure?.fields || []).filter(item => item.options && item.options.required === true)
+				$log(this.expressionFormList, '这里打印出符合条件的表单对象')
+			},
+			immediate: true
+		}
+	},
+	mounted() {
+		this.nodeConfig = this.modelValue
+	},
+	methods: {
+		queryValidNodeList() {
+			const validNodeList = []
+			const fn = data => {
+				if (!data) return
+				// validNodeList.push({ nodeKey: data.nodeKey, nodeName: data.nodeName, type: data.type })
+				validNodeList.push(data)
+				if (data.conditionNodes && Array.isArray(data.conditionNodes)) {
+					// 条件分支节点
+					data.conditionNodes.forEach(v => {
+						fn(v.childNode)
+					})
+				}
+
+				if (data.childNode) {
+					// 正常子节点
+					fn(data.childNode)
+				}
+			}
+			const rootConfig = this.getRootConfig()
+			fn(rootConfig)
+			// 只允许选择审核人
+			this.validNodeList = validNodeList.filter(v => v.type === 1)
+		},
+		show() {
+			if (this.disabled) return
+			this.form = {}
+			this.form = JSON.parse(JSON.stringify(this.nodeConfig))
+			this.queryValidNodeList()
+			this.drawer = true
+		},
+		editTitle(refName) {
+			if (this.disabled) return
+			this.isEditTitle = true
+			this.$nextTick(() => {
+				if (this.$refs[refName]) {
+					this.$refs[refName].focus()
+				}
+			})
+		},
+		saveTitle() {
+			this.isEditTitle = false
+		},
+		save() {
+			if (!this.form.nodeKey) {
+				return ElMessage.error('请填写nodeKey')
+			}
+			this.$refs.ruleFormRef.validate(valid => {
+				if (valid) {
+					this.$emit('update:modelValue', this.form)
+					this.drawer = false
+				}
+			})
+		},
+		delNode() {
+			this.$emit('update:modelValue', this.nodeConfig.childNode)
+		},
+		addRouteNode() {
+			const len = this.form.routeNodes.length + 1
+			this.form.routeNodes.push({
+				nodeName: `路由${len}`,
+				nodeKey: undefined, // 必填
+				// type: 3,  // ==>  type: 23
+				type: 23,
+				priorityLevel: len,
+				conditionMode: 1,
+				conditionList: []
+			})
+		},
+		removeRouteNode(index) {
+			this.form.routeNodes.splice(index, 1)
+		},
+		editRouteTitle(route, index) {
+			if (this.disabled) return
+			route.local_isEdit = true
+			this.$nextTick(() => {
+				const curRef = this.$refs.routeNodeName[index]
+				if (curRef) {
+					curRef.focus()
+				}
+			})
+		},
+		saveRouteTitle(route) {
+			route.local_isEdit = false
+		},
+		addConditionList(conditionList, type) {
+			conditionList.push({
+				label: '',
+				field: '',
+				operator: '==',
+				value: '',
+				type
+			})
+		},
+		addConditionGroup(route) {
+			this.addConditionList(route.conditionList[route.conditionList.push([]) - 1], 'custom')
+		},
+		deleteConditionGroup(route, index) {
+			route.conditionList.splice(index, 1)
+		},
+		deleteConditionList(conditionList, index) {
+			conditionList.splice(index, 1)
+		},
+		getCurrentItemLabel(conditionGroup, idx) {
+			// const currentCondition = this.form.conditionList[conditionIdx]
+			// const field = currentCondition[idx].field
+			const field = conditionGroup[idx].field
+			const labelObj = this.expressionFormList.find(i => i.key === field)
+			conditionGroup[idx].showLabel = labelObj.label
+			conditionGroup[idx].label = field
+		},
+		getCurrentItemField(conditionGroup, idx) {
+			// const currentCondition = this.form.conditionList[conditionIdx]
+			conditionGroup[idx].field = conditionGroup[idx].label
+		},
+		toText(nodeConfig) {
+			const routeNodes = nodeConfig.routeNodes || []
+			if (routeNodes.length > 0) {
+				return `${routeNodes.length}条动态路由`
+				// return routeNodes.map(v => `路由至(${v.nodeName})`).join(',')
+			}
+			return '<span class="placeholder">请设置路由节点</span>'
+		}
+	}
+}
+</script>
+
+<style scoped lang="scss">
+.tip {
+	color: #646a73;
+	margin-bottom: 10px;
+}
+.condition-group-editor {
+	user-select: none;
+	border-radius: 4px;
+	border: 1px solid #e4e5e7;
+	position: relative;
+	margin-bottom: 10px;
+
+	.branch-delete-icon {
+		font-size: 18px;
+		cursor: pointer;
+		&:hover {
+			color: var(--el-color-danger);
+		}
+	}
+
+	.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;
+	}
+}
+.route-header {
+}
+</style>