Pārlūkot izejas kodu

feat: 业务流程发起普通表单展示100%

luoyali 1 gadu atpakaļ
vecāks
revīzija
f7cf961be8

+ 244 - 0
src/components/Flow/FlowTrend.vue

@@ -0,0 +1,244 @@
+<template>
+	<div>
+		<!-- 流程走向图 -->
+		<el-timeline class="timeline-wrap">
+			<el-timeline-item v-for="(v, index) in processTimelineList" :key="index">
+				<template v-if="v.conditionNodes">
+					<el-radio-group v-model="processChecked[v.nodeKey]" size="small">
+						<el-radio-button v-for="c of v.conditionNodes" :key="c.nodeKey" :label="c.nodeKey">{{ c.nodeName }}</el-radio-button>
+					</el-radio-group>
+				</template>
+				<template v-else>
+					<div style="padding-bottom: 6px">{{ v.nodeName }}</div>
+					<span v-if="v.type === 5" class="text-gray-500">调用子流程 [ {{ v.callProcess.split(':')[1] }} ]</span>
+					<div v-if="assigneeMap[v.nodeKey]" style="display: flex; align-items: center; gap: 6px; flex-wrap: wrap">
+						<template v-if="assigneeMap[v.nodeKey].type === 1">
+							<el-tooltip v-if="!assigneeMap[v.nodeKey].disabled" content="添加用户" placement="left">
+								<el-button style="width: 32px" @click="selectHandler(v.nodeKey, 1)">
+									<svg-icon style="font-size: 18px" icon-class="flow-user-add" />
+								</el-button>
+							</el-tooltip>
+							<FlowNodeAvatar v-for="(item, index) in assigneeMap[v.nodeKey].assignees" :key="index" :name="item.name" style="margin-top: 5px" />
+						</template>
+						<template v-else>
+							<el-tooltip v-if="!assigneeMap[v.nodeKey].disabled" content="添加角色" placement="left">
+								<el-button style="width: 32px" @click="selectHandler(v.nodeKey, 3)">
+									<svg-icon style="font-size: 18px" icon-class="flow-group-add" />
+								</el-button>
+							</el-tooltip>
+							<FlowNodeAvatar v-for="(item, index) in assigneeMap[v.nodeKey].assignees" :key="index" :name="item.name" style="margin-top: 5px">
+								<template #avatar>
+									<svg-icon icon-class="flow-group" color="#fff" />
+								</template>
+							</FlowNodeAvatar>
+						</template>
+					</div>
+					<div v-else-if="assigneeDesc[v.nodeKey]">
+						<!-- 没有assigneeMap 的情况下 尝试获取描述 -->
+						<el-tag>{{ assigneeDesc[v.nodeKey] }}</el-tag>
+					</div>
+				</template>
+			</el-timeline-item>
+			<el-timeline-item>
+				<div style="padding-bottom: 6px">结束</div>
+			</el-timeline-item>
+		</el-timeline>
+
+		<!--  选择人员/角色-->
+		<use-select ref="useSelectRef" v-bind="active_selectOpts" @update:selected="updateActive_assigneeMap"></use-select>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive } from 'vue'
+import UseSelect from '@/components/scWorkflow/select.vue'
+import { setTypeOptions_config } from '@/components/scWorkflow/nodes/config'
+import type { ModelContentConfig } from '@/views/approve/components/config.ts'
+type Props = {
+	modelValue: object
+}
+const props = defineProps<Props>()
+const emit = defineEmits<{
+	'update:modelValue': [value: object] // 具名元组语法
+	update: [value: string]
+}>()
+
+type Assignee = {
+	type: 1 | 3 // 1: 用户 3: 角色
+	assignees: { [key: string]: any /*name, id*/ }
+	disabled?: boolean
+	selectOpts?: any
+	// minSelected?: number
+	// maxSelected?: number
+}
+const useSelectRef = ref()
+const assigneeMap = ref<{
+	[key: string]: Assignee
+}>({})
+const assigneeDesc = ref<{
+	[key: string]: string
+}>({})
+const active_assigneeKey = ref<string>()
+const active_selectOpts = ref({})
+const updateActive_assigneeMap = (assignees: any[]) => {
+	const _cur = assigneeMap.value[active_assigneeKey.value]
+	if (_cur) {
+		_cur.assignees = assignees
+	}
+}
+const selectHandler = (nodeKey: string, type: 1 | 3) => {
+	if (!assigneeMap.value[nodeKey]) {
+		assigneeMap.value[nodeKey] = { assignees: [], type }
+	}
+	const config = assigneeMap.value[nodeKey]
+	active_assigneeKey.value = nodeKey
+	active_selectOpts.value = config.selectOpts || {}
+	useSelectRef.value.open(type, config.assignees)
+}
+
+// 当前form 表单数据字符串
+const processChecked = reactive<{ [nodeKey: string]: any }>({})
+const packageProcess = (data: ModelContentConfig, list = []) => {
+	const new_list = [data]
+	let curData = data
+	while (curData.childNode) {
+		new_list.push(curData.childNode)
+		curData = curData.childNode
+	}
+	return new_list.reduce((_list, config) => {
+		// 条件分支
+		if (Array.isArray(config.conditionNodes)) {
+			// console.log('条件节点', config)
+			_list.push(config)
+			if (Array.isArray(config.conditionNodes) && config.conditionNodes.length) {
+				const _val = processChecked[config.nodeKey]
+				let condition: any = config.conditionNodes[0]
+				if (_val) {
+					config.conditionNodes.some(_condition => {
+						if (_condition.nodeKey === _val) {
+							condition = _condition
+							return true
+						}
+					})
+				} else {
+					// console.error('else......', condition)
+					processChecked[config.nodeKey] = condition.nodeKey
+				}
+				// console.warn('条件节点', condition.nodeName)
+				if (condition.childNode) {
+					packageProcess(condition.childNode, _list)
+				}
+			}
+		} else {
+			// console.log(config.nodeName, 'nodeName 普通节点名称', config, data)
+			// 0,发起人 1,审批人 2,抄送人 3,条件审批 4,条件分支 5,办理子流程 6,定时器在务 7,触发器在务
+			switch (+config.type) {
+				case 0: {
+					// 发起人
+					// console.error('发起人')
+					break
+				}
+				case 1: {
+					// 审批人
+					// 针对审核人 不同情况控制
+					if (Reflect.has(config, 'setType')) {
+						let disabled = false
+						let selectOpts = {}
+						const user_fn = () => {
+							const _key = config.nodeKey
+							if (!assigneeMap.value[_key]) {
+								assigneeMap.value[_key] = { assignees: config.nodeAssigneeList, type: 1, disabled, selectOpts }
+							}
+						}
+						switch (config.setType) {
+							case 1:
+								// 指定人员 (不允许重新选择) // 但展示
+								disabled = true
+								user_fn()
+								break
+							case 2:
+								// 主管 (不需要选择)
+								assigneeDesc.value[config.nodeKey] = config.examineLevel === 1 ? '直接主管' : `发起人的第${config.examineLevel}级主管`
+								break
+							case 3:
+								// 角色 选择角色 (允许重新选择)
+								const _key = config.nodeKey
+								if (!assigneeMap.value[_key]) {
+									assigneeMap.value[_key] = { assignees: config.nodeAssigneeList, type: 3 }
+								}
+								break
+							case 4:
+								// 发起人自选 (1: 选择一个人, 2: 选择多个人)
+								// const isMultiple = config.selectMode === 2
+								if (config.selectMode === 1) {
+									selectOpts = { maxSelected: 1 }
+								}
+								user_fn()
+								break
+							case 5: // 发起人自己 (不能选择)
+								assigneeDesc.value[config.nodeKey] = setTypeOptions_config[5]
+								break
+							case 6:
+								// 连续多级主管 (不能选择)
+								assigneeDesc.value[config.nodeKey] = config.examineLevel === 1 ? '直接主管' : `发起人的第${config.examineLevel}级主管`
+								break
+						}
+					}
+					break
+				}
+				case 2: {
+					// 抄送人
+					// 选择人员 & allowSelection 控制 true 允许选择 否则  隐藏
+					const _key = config.nodeKey
+					if (!assigneeMap.value[_key]) {
+						assigneeMap.value[_key] = { assignees: config.nodeAssigneeList, type: 1, disabled: !config.allowSelection }
+					}
+					break
+				}
+				/*case 3: {
+					// 条件审批
+					console.error('条件审批')
+					break
+				}
+				case 4: {
+					// 条件分支
+					console.error('条件分支')
+					break
+				}
+				case 5: {
+					// 办理子流程
+					console.error('办理子流程')
+					break
+				}
+				case 6: {
+					// 定时器在务
+					console.error('定时器在务')
+					break
+				}
+				case 7: {
+					// 触发器在务
+					console.error('触发器在务')
+					break
+				}*/
+			}
+			_list.push(config)
+		}
+		return _list
+	}, list)
+}
+
+const processTimelineList = computed(() => {
+	return packageProcess(props.modelValue)
+})
+
+defineExpose({
+	// 把数据暴露给父节点使用
+})
+</script>
+
+<style scoped lang="scss">
+.timeline-wrap {
+	flex-shrink: 0;
+	padding-left: 60px;
+}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2 - 12
src/views/flow/test/business.vue


+ 184 - 0
src/views/flow/test/businessLaunch.vue

@@ -0,0 +1,184 @@
+<template>
+	<el-drawer
+		:close-on-click-modal="false"
+		class="local-launch_drawer-wrap"
+		:title="record.processName"
+		:model-value="modelValue"
+		size="760px"
+		@update:model-value="updateModelValue"
+	>
+		<div v-if="validateForm.loading" v-loading="true" class="local_loading"></div>
+
+		<div class="info-wrap">
+			<el-divider content-position="left">{{ record.processName }}表单</el-divider>
+			<!-- 表单设计 -->
+			<div class="self-Everright-formEditor">
+				<er-form-preview ref="EReditorRef" :is-show-complete-button="false" :file-upload-u-r-i="uploadFileApi" />
+			</div>
+
+			<!-- vue自定义 -->
+			<el-divider content-position="left">审批流程</el-divider>
+			<flow-trend v-model="modelContentConfig"></flow-trend>
+			<el-divider></el-divider>
+		</div>
+
+		<template #footer>
+			<el-button @click="updateModelValue(false)">{{ $t('le.btn.cancel') }}</el-button>
+			<el-button :disabled="validateForm.loading" type="primary" style="margin-left: 8px" @click="onSubmit">{{ $t('le.btn.confirm') }}</el-button>
+		</template>
+	</el-drawer>
+</template>
+
+<script setup lang="ts">
+import model from '@/api/flow/process'
+import { nextTick, onMounted, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { erFormPreview } from '@ER/formEditor'
+import FlowTrend from '@/components/Flow/FlowTrend.vue'
+import type { ModelContentConfig } from '@/views/approve/components/config.ts'
+
+type Props = {
+	modelValue: boolean
+	record: { processId: string; processName: string; [key: string]: any }
+}
+type Assignee = {
+	type: 1 | 3 // 1: 用户 3: 角色
+	assignees: { [key: string]: any /*name, id*/ }
+	disabled?: boolean
+	selectOpts?: any
+	// minSelected?: number
+	// maxSelected?: number
+}
+
+// 当前form 表单数据字符串
+let cur_processForm_str = '{}'
+
+const updateModelValue = (bool: boolean) => emit('update:modelValue', bool)
+const props = defineProps<Props>()
+const emit = defineEmits<{
+	'update:modelValue': [bool: boolean] // 具名元组语法
+	update: [value: string]
+}>()
+const validateForm = ref({ loading: false })
+const assigneeMap = ref<{
+	[key: string]: Assignee
+}>({})
+const { VITE_APP_BASE_API } = import.meta.env
+const uploadFileApi = ref(`${VITE_APP_BASE_API}/v1/oss/upload`)
+const EReditorRef = ref()
+const modelContentConfig = ref<ModelContentConfig | any>({})
+
+const onSubmit = async () => {
+	const processId = props.record.processId
+	const form = EReditorRef.value.getSelfFormRef()
+	form.validate((valid: any) => {
+		if (valid) {
+			// 表单验证通过 进行保存
+			validateForm.value.loading = true
+			const formData = EReditorRef.value.getData()
+			let processForm = JSON.parse(cur_processForm_str)
+			processForm = { ...processForm, formData }
+			// 这里要从子节点获取流程图信息 进行保存
+			const _assigneeMap = assigneeMap.value
+			const assigneeMap_ = Object.keys(_assigneeMap).reduce((obj, nodeKey: string) => {
+				const _o = _assigneeMap[nodeKey]
+				obj[nodeKey] = {
+					assigneeList: _o.assignees,
+					type: _o.type
+				}
+				return obj
+			}, {} as { [nodeKey: string]: any })
+			model
+				.processLaunchApi({
+					processId, // 流程ID
+					processForm: JSON.stringify(processForm), // 流程表单JSON内容 & local_value 保存
+					assigneeMap: assigneeMap_
+				})
+				.then(res => {
+					ElMessage.success('提交成功')
+					updateModelValue(false)
+				})
+				.finally(() => {
+					validateForm.value.loading = false
+				})
+		}
+	})
+}
+
+// 获取当前表单中的详情
+const getDetailInfo = () => {
+	validateForm.value.loading = true
+	let modelContent_config = {}
+	const modelContent = JSON.parse(props.record.modelContent) // modelContent 这个是后台返回来的
+	modelContent_config = modelContent.nodeConfig ?? modelContent.childNode
+	modelContentConfig.value = modelContent_config
+	const { content, type } = props.record.formTemplate
+	if (!type) {
+		// type: 0 表单设计  1 vue自定义表单
+		cur_processForm_str = content || '{}' //  processForm 这个是后台返回来的
+		const formStructure = JSON.parse(cur_processForm_str)
+		EReditorRef.value.setData(formStructure)
+	}
+	validateForm.value.loading = false
+}
+onMounted(() => {
+	nextTick(() => {
+		getDetailInfo()
+	})
+})
+</script>
+
+<style lang="scss">
+// 和ItemDrawer一样可以公用,待提取
+.local-launch_drawer-wrap {
+	.el-drawer__header {
+		display: flex;
+		padding: 16px 24px;
+		align-items: center;
+		justify-content: space-between;
+		background-color: var(--el-color-info-light-9);
+		text-align: left;
+		/* margin-right: 0; */
+		margin-bottom: 0;
+	}
+	.el-drawer__close-btn {
+		padding: 0;
+		margin-right: -12px;
+	}
+	.el-drawer__body {
+		position: relative;
+		display: flex;
+		flex-direction: column;
+	}
+	.el-drawer__footer {
+		border-top: 1px solid var(--el-border-color-lighter);
+		padding: 12px 24px;
+	}
+	.local_loading {
+		position: absolute;
+		left: 0;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 100%;
+		height: 100%;
+		z-index: 999;
+		background: rgba(0, 0, 0, 0.05);
+	}
+}
+</style>
+
+<style scoped lang="scss">
+.info-wrap {
+	.form-wrap {
+		flex: 1;
+	}
+
+	// 修改everright 表单的样式
+	.self-Everright-formEditor {
+		:deep(.Everright-formEditor-selectElement) {
+			padding: 0px 16px;
+		}
+	}
+}
+</style>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels