Przeglądaj źródła

Merge remote-tracking branch 'origin/feature/业务流程自定义vue开发'

# Conflicts:
#	.env.development
luoyali 1 rok temu
rodzic
commit
9e50c22357

+ 1 - 1
.env.development

@@ -2,4 +2,4 @@
 NODE_ENV='development'
 # 测试接口
 VITE_APP_BASE_API = 'https://apiboot.aizuda.com'
-# VITE_APP_BASE_API = 'http://localhost:8088'
+# VITE_APP_BASE_API = 'http://localhost:8088'

+ 13 - 4
src/api/flow/process.ts

@@ -16,7 +16,8 @@ const api = {
 	processLaunch: '/v1/process/launch', // 发起流程
 	childProcessTop10: '/v1/process/list-child-top10', // 查询满足条件的前10条子流程列表
 	pageHistoryList: '/v1/process/page-history', // 历史分页列表
-	checkoutHistory: '/v1/process/checkout' // 根据Id签出历史流程
+	checkoutHistory: '/v1/process/checkout', // 根据Id签出历史流程
+	businessProcess: '/v1/process/business' // 根据Key查询业务流程信息
 }
 
 function progressCreateApi(data: any): AxiosPromise {
@@ -106,8 +107,8 @@ function processDetailApi(id: any): AxiosPromise {
 }
 
 // pageHistoryList: '/v1/process/page-history', // 历史分页列表
-	// checkoutHistory: '/v1/process/checkout' // 根据Id签出历史流程
-	//{
+// checkoutHistory: '/v1/process/checkout' // 根据Id签出历史流程
+//{
 //   "page": 0,
 //   "pageSize": 0,
 //   "data": {
@@ -130,6 +131,13 @@ function checkoutHistoryApi(id: any): AxiosPromise {
 	})
 }
 
+export function progressBusinessApi(): AxiosPromise {
+	return request({
+		url: api.businessProcess + '?key=purchaseOrder',
+		method: 'get'
+	})
+}
+
 export default {
 	progressCreateApi,
 	progressDeleteApi,
@@ -144,5 +152,6 @@ export default {
 	releaseProcessApi,
 	childProcessTop10Api,
 	pageHistoryListApi,
-	checkoutHistoryApi
+	checkoutHistoryApi,
+	progressBusinessApi
 }

+ 15 - 2
src/api/test/purchaseOrder.ts

@@ -6,7 +6,8 @@ const api = {
 	page: '/v1/purchase-order/page',
 	create: '/v1/purchase-order/create',
 	update: '/v1/purchase-order/update',
-	delete: '/v1/purchase-order/delete'
+	delete: '/v1/purchase-order/delete',
+	launch: '/v1/purchase-order/launch'
 }
 
 /**
@@ -41,8 +42,20 @@ function postDeleteApi(data: any): AxiosPromise {
 		data
 	})
 }
+
+/**
+ * 采购订单管理 - 删除
+ */
+function postlaunchApi(data: any): AxiosPromise {
+	return request({
+		url: api.launch,
+		method: 'post',
+		data
+	})
+}
 export default {
 	postPageApi,
 	postAddOrEditSaveApi,
-	postDeleteApi
+	postDeleteApi,
+	postlaunchApi
 }

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

@@ -0,0 +1,250 @@
+<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 FlowNodeAvatar from '@/components/Flow/FlowNodeAvatar.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)
+})
+
+const getAssigneeMap = () => {
+	return assigneeMap.value
+}
+
+defineExpose({
+	// 把数据暴露给父节点使用
+	getAssigneeMap
+})
+</script>
+
+<style scoped lang="scss">
+.timeline-wrap {
+	flex-shrink: 0;
+	padding-left: 60px;
+}
+</style>

Plik diff jest za duży
+ 22 - 6
src/views/flow/test/business.vue


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

@@ -0,0 +1,224 @@
+<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>
+			<!-- 表单设计 -->
+			<template v-if="record.formTemplate.type === 0">
+				<div class="self-Everright-formEditor">
+					<er-form-preview ref="EReditorRef" :is-show-complete-button="false" :file-upload-u-r-i="uploadFileApi" />
+				</div>
+			</template>
+
+			<!-- vue自定义 -->
+			<template v-if="record.formTemplate.type === 1">
+				<component :is="dyVueComponent" ref="dyVueComponentRef"></component>
+			</template>
+
+			<el-divider content-position="left">审批流程</el-divider>
+			<flow-trend ref="flowTrendRef" 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 purchaseOrder from '@/api/test/purchaseOrder'
+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'
+import { nextTick, onMounted, ref, markRaw, defineAsyncComponent, provide } from 'vue'
+
+type Props = {
+	modelValue: boolean
+	record: { processId: string; processName: string; [key: string]: any }
+}
+
+// 当前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 { 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 dyVueComponent = ref(undefined)
+const dyVueComponentRef = ref()
+const flowTrendRef = ref()
+const dyVueForm = ref({})
+
+provide('dyVueForm', dyVueForm) // 这里主要是存放动态的form的属性值
+
+const onSubmit = async () => {
+	const processId = props.record.processId
+	const { type } = props.record.formTemplate
+	let params = {}
+	// 这里要从子节点获取流程图信息 进行保存
+	const _assigneeMap = flowTrendRef.value.getAssigneeMap()
+	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 })
+	if (!type) {
+		// type: 0 表单设计  1 vue自定义表单
+		const form = EReditorRef.value.getSelfFormRef()
+		form.validate((valid: any) => {
+			if (!valid) return false
+			// 表单验证通过 进行保存
+			validateForm.value.loading = true
+			const formData = EReditorRef.value.getData()
+			let processForm = JSON.parse(cur_processForm_str)
+			processForm = { ...processForm, formData }
+			params = {
+				id: props.record.rowId,
+				test: '',
+				processStart: {
+					processId, // 流程ID
+					processForm: JSON.stringify(processForm), // 流程表单JSON内容 & local_value 保存
+					assigneeMap: assigneeMap_
+				}
+			}
+		})
+	} else {
+		const formData = dyVueComponentRef.value.getData()
+		const testData = {
+			formStructure: '@/views/flow/test/test1.vue',
+			formData: formData
+		}
+		params = {
+			id: props.record.rowId,
+			test: JSON.stringify(testData),
+			processStart: {
+				processId, // 流程ID
+				processForm: '', // 流程表单JSON内容 & local_value 保存
+				assigneeMap: assigneeMap_
+			}
+		}
+	}
+
+	purchaseOrder
+		.postlaunchApi(params)
+		.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 = `{"formStructure":${content}}` || '{}' //  processForm 这个是后台返回来的
+		const { formStructure } = JSON.parse(cur_processForm_str)
+		EReditorRef.value.setData(formStructure)
+	} else {
+		// dyVueForm.value = {
+		// 	dy: {
+		// 		name: '测试名字',
+		// 		region: 1,
+		// 		delivery: true,
+		// 		type: ['1'],
+		// 		resource: 'Sponsor',
+		// 		desc: '啦啦啦'
+		// 	}
+		// }
+		dyVueComponent.value = markRaw(defineAsyncComponent(() => import('@/views/flow/test/test1.vue')))
+	}
+	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>

+ 61 - 0
src/views/flow/test/test1.vue

@@ -0,0 +1,61 @@
+<template>
+	<el-form :model="form" label-width="auto" style="max-width: 600px">
+		<el-form-item label="Activity name">
+			<el-input v-model="form.name" />
+		</el-form-item>
+		<el-form-item label="Activity zone">
+			<el-select v-model="form.region" placeholder="please select your zone">
+				<el-option label="1" value="shanghai" />
+				<el-option label="2" value="beijing" />
+			</el-select>
+		</el-form-item>
+		<el-form-item label="Instant delivery">
+			<el-switch v-model="form.delivery" />
+		</el-form-item>
+		<el-form-item label="Activity type">
+			<el-checkbox-group v-model="form.type">
+				<el-checkbox label="1" name="type"> Online activities </el-checkbox>
+				<el-checkbox label="2" name="type"> Promotion activities </el-checkbox>
+				<el-checkbox label="3" name="type"> Offline activities </el-checkbox>
+				<el-checkbox label="4" name="type"> Simple brand exposure </el-checkbox>
+			</el-checkbox-group>
+		</el-form-item>
+		<el-form-item label="Resources">
+			<el-radio-group v-model="form.resource">
+				<el-radio label="Sponsor">Sponsor</el-radio>
+				<el-radio label="Venue">Venue</el-radio>
+			</el-radio-group>
+		</el-form-item>
+		<el-form-item label="Activity form">
+			<el-input v-model="form.desc" type="textarea" />
+		</el-form-item>
+	</el-form>
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref, defineExpose, inject, nextTick, onMounted, watch } from 'vue'
+import { get, set } from 'lodash'
+
+const dyVueForm = inject('dyVueForm')
+
+const form = ref(get(dyVueForm.value, 'dy', dyVueForm.value))
+set(dyVueForm.value, 'dy', form.value)
+
+watch(
+	() => get(dyVueForm.value, 'dy'),
+	vl => (form.value = vl)
+)
+watch(
+	() => form.value,
+	v => set(dyVueForm.value, 'dy', v)
+)
+
+const getData = () => {
+	return form.value
+}
+
+defineExpose({
+	// 把数据暴露给父节点使用
+	getData
+})
+</script>

+ 55 - 0
src/views/flow/test/test2.vue

@@ -0,0 +1,55 @@
+<template>
+	<el-form :model="form" label-width="auto" style="max-width: 600px">
+		<el-form-item label="Activity name">
+			<el-input v-model="form.name" />
+		</el-form-item>
+		<el-form-item label="Activity zone">
+			<el-select v-model="form.region" placeholder="please select your zone">
+				<el-option label="Zone one" value="shanghai" />
+				<el-option label="Zone two" value="beijing" />
+			</el-select>
+		</el-form-item>
+		<el-form-item label="Instant delivery">
+			<el-switch v-model="form.delivery" />
+		</el-form-item>
+		<el-form-item label="Activity type">
+			<el-checkbox-group v-model="form.type">
+				<el-checkbox value="Online activities" name="type"> Online activities </el-checkbox>
+				<el-checkbox value="Promotion activities" name="type"> Promotion activities </el-checkbox>
+				<el-checkbox value="Offline activities" name="type"> Offline activities </el-checkbox>
+				<el-checkbox value="Simple brand exposure" name="type"> Simple brand exposure </el-checkbox>
+			</el-checkbox-group>
+		</el-form-item>
+		<el-form-item label="Resources">
+			<el-radio-group v-model="form.resource">
+				<el-radio value="Sponsor">Sponsor</el-radio>
+				<el-radio value="Venue">Venue</el-radio>
+			</el-radio-group>
+		</el-form-item>
+		<el-form-item label="Activity form">
+			<el-input v-model="form.desc" type="textarea" />
+		</el-form-item>
+	</el-form>
+</template>
+
+<script lang="ts" setup>
+import { reactive } from 'vue'
+
+// do not use same name with ref
+const form = reactive({
+	name: '',
+	region: '',
+	delivery: false,
+	type: [],
+	resource: '',
+	desc: ''
+})
+const getData = () => {
+	return form
+}
+
+defineExpose({
+	// 把数据暴露给父节点使用
+	getData
+})
+</script>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików