Pre Merge pull request !624 from Lesan/feature/bpm

pull/624/MERGE
Lesan 2024-12-20 09:05:05 +00:00 committed by Gitee
commit a7f29cfcb7
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
11 changed files with 400 additions and 7 deletions

View File

@ -72,3 +72,10 @@ export const deleteModel = async (id: number) => {
export const deployModel = async (id: number) => {
return await request.post({ url: '/bpm/model/deploy?id=' + id })
}
export const getProcessListByProcessType = async (processType) => {
return await request.get({
url: '/bpm/model/list-all-simple?processType=' + processType
})
}

View File

@ -0,0 +1 @@
<svg t="1734419297528" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4299" width="200" height="200"><path d="M375.466667 213.333333a85.333333 85.333333 0 1 1-85.333334 85.333334 85.333333 85.333333 0 0 1 85.333334-85.333334m0-85.333333a170.666667 170.666667 0 1 0 170.666666 170.666667 170.666667 170.666667 0 0 0-170.666666-170.666667z" fill="#ffffff" p-id="4300"></path><path d="M716.8 597.333333m-85.333333 0a85.333333 85.333333 0 1 0 170.666666 0 85.333333 85.333333 0 1 0-170.666666 0Z" fill="#ffffff" p-id="4301"></path><path d="M716.8 853.333333m-85.333333 0a85.333333 85.333333 0 1 0 170.666666 0 85.333333 85.333333 0 1 0-170.666666 0Z" fill="#ffffff" p-id="4302"></path><path d="M418.133333 384h-85.333333v426.666667a85.333333 85.333333 0 0 0 85.333333 85.333333V384zM674.133333 554.666667h-256v85.333333h256v-85.333333zM674.133333 810.666667h-256v85.333333h256v-85.333333z" fill="#ffffff" p-id="4303"></path></svg>

After

Width:  |  Height:  |  Size: 971 B

View File

@ -39,6 +39,13 @@
</div>
<div class="handler-item-text">包容分支</div>
</div>
<!-- TODO @芋艿这里需要换一下子流程的图标~ -->
<div class="handler-item" @click="addNode(NodeType.CHILD_PROCESS_NODE)">
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">子流程</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
@ -208,6 +215,16 @@ const addNode = (type: number) => {
}
emits('update:childNode', data)
}
if (type === NodeType.CHILD_PROCESS_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.CHILD_PROCESS_NODE) as string,
showText: '',
type: NodeType.CHILD_PROCESS_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
}
</script>

View File

@ -17,6 +17,12 @@
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 子流程节点 -->
<ChildProcessNode
v-if="currentNode && currentNode.type === NodeType.CHILD_PROCESS_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
@ -57,6 +63,7 @@ import StartUserNode from './nodes/StartUserNode.vue'
import EndEventNode from './nodes/EndEventNode.vue'
import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ChildProcessNode from './nodes/ChildProcessNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
import InclusiveNode from './nodes/InclusiveNode.vue'

View File

@ -23,6 +23,11 @@ export enum NodeType {
*/
COPY_TASK_NODE = 12,
/**
*
*/
CHILD_PROCESS_NODE = 20,
/**
*
*/
@ -98,6 +103,8 @@ export interface SimpleFlowNode {
defaultFlow?: boolean
// 活动的状态,用于前端节点状态展示
activityStatus?: TaskStatusEnum
// 调用子流程
childProcess?: ChildProcess
}
// 候选人策略枚举 用于审批节点。抄送节点 )
export enum CandidateStrategy {
@ -413,12 +420,14 @@ NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
NODE_DEFAULT_TEXT.set(NodeType.CHILD_PROCESS_NODE, '请设置子流程')
export const NODE_DEFAULT_NAME = new Map<number, string>()
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
NODE_DEFAULT_NAME.set(NodeType.CHILD_PROCESS_NODE, '子流程')
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataVO[] = [
@ -568,3 +577,26 @@ export enum ProcessVariableEnum {
*/
START_USER_ID = 'PROCESS_START_USER_ID'
}
/**
*
*/
export enum ProcessTypeEnum {
/**
*
*/
MAIN = 1,
/**
*
*/
CHILD = 2
}
/**
*
*/
export type ChildProcess = {
processKey: string
processName: string
}

View File

@ -0,0 +1,202 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="子流程" name="child">
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="选择子流程" prop="childProcessKey">
<el-select v-model="configForm.childProcessKey" clearable>
<el-option
v-for="(item, index) in childProcessOptions"
:key="index"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-form-item>
<el-form-item label="是否多实例" prop="multiInstance">
<el-radio-group v-model="configForm.multiInstance">
<el-radio :value="true" :label="true"></el-radio>
<el-radio :value="false" :label="false"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="多人审批方式" prop="approveMethod" v-if="configForm.multiInstance">
<el-radio-group v-model="configForm.approveMethod">
<div class="flex-col">
<div
v-for="(item, index) in APPROVE_METHODS"
:key="index"
class="flex items-center"
>
<el-radio :value="item.value" :label="item.value">
{{ item.label }}
</el-radio>
<el-form-item prop="approveRatio">
<el-input-number
v-model="configForm.approveRatio"
:min="10"
:max="100"
:step="10"
size="small"
v-if="
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO
"
/>
</el-form-item>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
label="多实例来源"
prop="multiInstanceSource"
v-if="configForm.multiInstance"
>
<el-input
type="textarea"
v-model="configForm.multiInstanceSource"
clearable
style="width: 100%"
placeholder="请输入多实例来源(目前仅支持流程表达式!"
/>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig"> </el-button>
<el-button @click="closeDrawer"> </el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { getProcessListByProcessType } from '@/api/bpm/model'
import {
SimpleFlowNode,
NodeType,
APPROVE_METHODS,
ApproveMethodType,
CandidateStrategy,
ProcessTypeEnum
} from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node'
defineOptions({
name: 'ChildProcessNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
//
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
//
const currentNode = useWatchNode(props)
//
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.CHILD_PROCESS_NODE)
// Tab
const activeTabName = ref('child')
//
const formRef = ref() // Ref
//
const formRules = reactive({
childProcessKey: [{ required: true, message: '子流程不能为空', trigger: 'change' }],
multiInstance: [{ required: true, message: '是否多实例不能为空', trigger: 'change' }],
approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
approveRatio: [{ required: true, message: '通过比例不能为空', trigger: 'change' }],
multiInstanceSource: [{ required: true, message: '多实例来源不能为空', trigger: 'blur' }]
})
const configForm = ref({
childProcessKey: '',
multiInstance: false,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
approveRatio: 100,
multiInstanceSource: ''
})
const childProcessOptions = ref()
//
const saveConfig = async () => {
activeTabName.value = 'child'
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const childInfo = childProcessOptions.value.find(
(option) => option.key === configForm.value.childProcessKey
)
currentNode.value.name = nodeName.value!
currentNode.value.childProcess = {
processKey: childInfo.key,
processName: childInfo.name
}
currentNode.value.showText = `调用子流程:${childInfo.name}`
if (configForm.value.multiInstance) {
currentNode.value.candidateStrategy = CandidateStrategy.EXPRESSION
currentNode.value.candidateParam = configForm.value.multiInstanceSource
currentNode.value.approveMethod = configForm.value.approveMethod
if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
currentNode.value.approveRatio = configForm.value.approveRatio
}
} else {
currentNode.value.candidateStrategy = undefined
currentNode.value.candidateParam = undefined
currentNode.value.approveMethod = undefined
currentNode.value.approveRatio = undefined
}
settingVisible.value = false
return true
}
//
const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
if (node.childProcess) {
configForm.value.childProcessKey = node.childProcess.processKey
}
if (node.candidateStrategy) {
configForm.value.multiInstance = true
configForm.value.multiInstanceSource = node.candidateParam
configForm.value.approveMethod = node.approveMethod
if (node.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
configForm.value.approveRatio = node.approveRatio
}
}
}
defineExpose({ openDrawer, showChildProcessNodeConfig }) //
onMounted(async () => {
childProcessOptions.value = await getProcessListByProcessType(ProcessTypeEnum.CHILD)
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,97 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CHILD_PROCESS_NODE) }}
</div>
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<ChildProcessNodeConfig
v-if="!readonly && currentNode"
ref="nodeSetting"
:flow-node="currentNode"
/>
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import ChildProcessNodeConfig from '../nodes-config/ChildProcessNodeConfig.vue'
defineOptions({
name: 'ChildProcessNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
//
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
//
const readonly = inject<Boolean>('readonly')
//
const currentNode = useWatchNode(props)
//
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.CHILD_PROCESS_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showChildProcessNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
//
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style scoped></style>

View File

@ -690,10 +690,22 @@ const processSave = async () => {
onBeforeMount(() => {
console.log(props, 'propspropspropsprops')
})
onMounted(() => {
initBpmnModeler()
createNewDiagram(props.value)
})
watch(
() => props.value,
(val) => {
val &&
val.length &&
nextTick(() => {
initBpmnModeler()
createNewDiagram(props.value)
})
},
{ immediate: true }
)
// onMounted(() => {
// initBpmnModeler()
// createNewDiagram(props.value)
// })
onBeforeUnmount(() => {
// this.$once('hook:beforeDestroy', () => {
// })

View File

@ -144,6 +144,7 @@ export enum DICT_TYPE {
// ========== BPM 模块 ==========
BPM_MODEL_TYPE = 'bpm_model_type',
BPM_PROCESS_TYPE = 'bpm_process_type',
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',

View File

@ -108,6 +108,19 @@ const setSimpleModelNodeTaskStatus = (
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
}
//
if (simpleModel.type === NodeType.CHILD_PROCESS_NODE) {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
if (rejectedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.REJECT
} else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.RUNNING
} else if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
}
}
// SequenceFlow
if (simpleModel.type === NodeType.CONDITION_NODE) {
//
@ -157,7 +170,7 @@ const setSimpleModelNodeTaskStatus = (
.process-viewer-container {
height: 100%;
width: 100%;
:deep(.process-viewer) {
height: 100% !important;
min-height: 100%;

View File

@ -168,6 +168,7 @@ import copySvg from '@/assets/svgs/bpm/copy.svg'
import conditionSvg from '@/assets/svgs/bpm/condition.svg'
import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
import finishSvg from '@/assets/svgs/bpm/finish.svg'
import childProcessSvg from '@/assets/svgs/bpm/childProcess.svg'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
withDefaults(
@ -234,7 +235,9 @@ const nodeTypeSvgMap = {
//
[NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
//
[NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg }
[NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg },
//
[NodeType.CHILD_PROCESS_NODE]: { color: '#3296fb', svg: childProcessSvg }
}
// -101 icon
@ -253,7 +256,8 @@ const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
if (
nodeType === NodeType.START_USER_NODE ||
nodeType === NodeType.USER_TASK_NODE ||
nodeType === NodeType.END_EVENT_NODE
nodeType === NodeType.END_EVENT_NODE ||
nodeType === NodeType.CHILD_PROCESS_NODE
) {
return statusIconMap[taskStatus]?.icon
}