仿钉钉流程设计器- 审批节点配置新增拒绝处理方式

This commit is contained in:
jason 2024-05-26 11:02:04 +08:00
parent 142b0f7203
commit 0e7dbbb04d
7 changed files with 221 additions and 72 deletions

View File

@ -1,37 +1,42 @@
<template>
<div class="node-handler-wrapper">
<div class="node-handler" v-if="props.showAdd">
<el-popover trigger="hover" v-model:visible="popoverShow" placement="right-start" width="auto">
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
<div class="approve handler-item-icon">
<span class="iconfont icon-approve icon-size"></span>
</div>
<div class="handler-item-text">审批人</div>
</div>
<div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">抄送</div>
</div>
<div class="handler-item" @click="addNode(NodeType.EXCLUSIVE_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-exclusive"></span>
</div>
<div class="handler-item-text">条件分支</div>
<el-popover
trigger="hover"
v-model:visible="popoverShow"
placement="right-start"
width="auto"
>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
<div class="approve handler-item-icon">
<span class="iconfont icon-approve icon-size"></span>
</div>
<div class="handler-item-text">审批人</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
<div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">抄送</div>
</div>
<div class="handler-item" @click="addNode(NodeType.EXCLUSIVE_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-exclusive"></span>
</div>
<div class="handler-item-text">条件分支</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
</el-popover>
</div>
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_NAME, ApproveMethodType, CandidateStrategy } from './consts'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_NAME, ApproveMethodType, RejectHandlerType, CandidateStrategy } from './consts'
import { generateUUID } from '@/utils'
defineOptions({
name: 'NodeHandler'
@ -71,6 +76,9 @@ const addNode = (type: number) => {
//
timeoutHandler: {
enable: false
},
rejectHandler: {
type: RejectHandlerType.TERMINATION
}
},
childNode: props.childNode

View File

@ -1,58 +1,103 @@
<template>
<!-- 开始节点 -->
<StartEventNode
v-if="currentNode && currentNode.type === NodeType.START_EVENT_NODE"
:flow-node ="currentNode"
@update:model-value="handleModelValueUpdate" />
v-if="currentNode && currentNode.type === NodeType.START_EVENT_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
/>
<!-- 审批节点 -->
<UserTaskNode
v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
:flow-node ="currentNode" @update:model-value="handleModelValueUpdate"/>
<UserTaskNode
v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 抄送节点 -->
<CopyTaskNode
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
:flow-node ="currentNode" @update:model-value="handleModelValueUpdate"/>
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.EXCLUSIVE_NODE"
:flow-node ="currentNode" @update:model-value="handleModelValueUpdate"/>
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.EXCLUSIVE_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree v-if="currentNode && currentNode.childNode" v-model:flow-node="currentNode.childNode"/>
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
v-model:flow-node="currentNode.childNode"
:parent-node= "currentNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
<!-- 结束节点 -->
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE"/>
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
</template>
<script setup lang='ts'>
import StartEventNode from './nodes/StartEventNode.vue';
import EndEventNode from './nodes/EndEventNode.vue';
import UserTaskNode from './nodes/UserTaskNode.vue';
import CopyTaskNode from './nodes/CopyTaskNode.vue';
import ExclusiveNode from './nodes/ExclusiveNode.vue';
import { SimpleFlowNode, NodeType } from './consts';
<script setup lang="ts">
import StartEventNode from './nodes/StartEventNode.vue'
import EndEventNode from './nodes/EndEventNode.vue'
import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
defineOptions({
name: 'ProcessNodeTree'
})
const props = defineProps({
flowNode : {
parentNode: {
type: Object as () => SimpleFlowNode,
default: () => null
default: () => null
},
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
const emits = defineEmits(['update:flowNode'])
const emits = defineEmits<{
'update:flowNode',
'find:recursiveFindParentNode': [nodeList: SimpleFlowNode[], curentNode: SimpleFlowNode, nodeType: number]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode);
const currentNode = ref<SimpleFlowNode>(props.flowNode)
// .
watch(() => props.flowNode, (newValue) => {
currentNode.value = newValue;
}
);
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const handleModelValueUpdate = (updateValue) => {
console.log('Process Node Tree handleModelValueUpdate', updateValue)
emits('update:flowNode', updateValue);
}
</script>
<style lang='scss' scoped>
emits('update:flowNode', updateValue)
}
</style>
const findFromParentNode = (
nodeList: SimpleFlowNode[],
nodeType: number
) => {
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
//
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
findNode: SimpleFlowNode,
nodeType: number
) => {
if (!findNode || findNode.type === NodeType.START_EVENT_NODE) {
return
}
if (findNode.type === nodeType) {
nodeList.push(findNode)
}
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
</script>
<style lang="scss" scoped></style>

View File

@ -15,7 +15,7 @@
</div>
</div>
<div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
@ -55,6 +55,18 @@ const processNodeTree = ref<SimpleFlowNode>({
}
})
// const rootNode = ref<SimpleFlowNode>({
// name: '',
// type: NodeType.START_EVENT_NODE,
// id: 'StartEvent_1'
// })
// const childNode = ref<SimpleFlowNode>({
// id: 'EndEvent_1',
// name: '',
// type: NodeType.END_EVENT_NODE
// })
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
@ -148,6 +160,8 @@ onMounted(async () => {
if (result) {
console.log('the result is ', result)
processNodeTree.value = result
// rootNode.value = result
// childNode.value = result.childNode
}
})
</script>

View File

@ -62,6 +62,17 @@ export enum TimeUnitType {
DAY = 3
}
export enum RejectHandlerType {
/**
*
*/
TERMINATION = 1,
/**
*
*/
RETURN_PRE_USER_TASK = 2
}
// 条件配置类型 用于条件节点配置
export enum ConditionConfigType {
@ -186,12 +197,6 @@ NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
export const TIME_UNIT_MAP = new Map<number,string>()
NODE_DEFAULT_NAME.set(1, 'M')
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
export const APPROVE_METHODS: DictDataVO [] = [
{ label: '单人审批', value: 1 },
{ label: '多人会签(需所有审批人同意)', value: 2 },
@ -216,6 +221,10 @@ export const TIMEOUT_HANDLER_ACTION_TYPES: DictDataVO [] = [
{ label: '自动同意', value: 2 },
{ label: '自动拒绝', value: 3 },
]
export const REJECT_HANDLER_TYPES: DictDataVO [] = [
{ label: '结束流程', value: RejectHandlerType.TERMINATION },
{ label: '驳回到指定节点', value: RejectHandlerType.RETURN_PRE_USER_TASK }
]
// 比较运算符
export const COMPARISON_OPERATORS : DictDataVO = [

View File

@ -131,7 +131,6 @@
/>
</el-select>
</el-form-item>
<el-form-item
v-if="currentNode.attributes.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
@ -144,7 +143,6 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item label="审批方式" prop="approveMethod">
<el-radio-group v-model="currentNode.attributes.approveMethod">
<div class="flex-col">
@ -163,8 +161,35 @@
</div>
</el-radio-group>
</el-form-item>
<el-form-item label="超时处理" prop="timeoutHandlerEnable">
<el-divider content-position="left">审批人拒绝时</el-divider>
<el-form-item label="处理方式" prop="rejectHandler">
<el-radio-group v-model="currentNode.attributes.rejectHandler.type" @change="rejectHandlerTypeChange">
<el-radio
:border="true"
v-for="item in REJECT_HANDLER_TYPES"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="currentNode.attributes.rejectHandler.type == RejectHandlerType.RETURN_PRE_USER_TASK"
label="驳回节点"
prop="rejectHandlerNode"
>
<el-select v-model="currentNode.attributes.rejectHandler.returnNodeId" clearable style="width: 100%">
<el-option
v-for="item in returnTaskList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人超时未处理时</el-divider>
<el-form-item label="启用开关" prop="timeoutHandlerEnable">
<el-switch
v-model="currentNode.attributes.timeoutHandler.enable"
active-text="开启"
@ -281,8 +306,10 @@ import {
NodeType,
ApproveMethodType,
TimeUnitType,
RejectHandlerType,
TIMEOUT_HANDLER_ACTION_TYPES,
TIME_UNIT_TYPES,
REJECT_HANDLER_TYPES,
NODE_DEFAULT_NAME
} from '../consts'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@ -303,6 +330,9 @@ const props = defineProps({
required: true
}
})
const emits = defineEmits<{
'find:returnTaskNodes': [nodeList: SimpleFlowNode[]]
}>()
const notAllowedMultiApprovers = ref(false)
const currentNode = ref<SimpleFlowNode>(props.flowNode)
@ -316,7 +346,7 @@ const deptTreeOptions = inject('deptTree') // 部门树
const formType = inject('formType') //
const formFields = inject<Ref<string[]>>('formFields')
const candidateParamArray = ref<any[]>([])
const returnTaskList = ref<SimpleFlowNode[]>([])
const closeDrawer = () => {
settingVisible.value = false
}
@ -443,6 +473,10 @@ const setCurrentNode = (node: SimpleFlowNode) => {
timeDuration.value = parseInt(parseTime)
timeUnit.value = convertTimeUnit(parseTimeUnit)
}
//
const matchNodeList = [];
emits('find:returnTaskNodes', matchNodeList);
returnTaskList.value = matchNodeList;
}
defineExpose({ open, setCurrentNode }) //
@ -483,6 +517,12 @@ const blurEvent = () => {
currentNode.value.name =
currentNode.value.name || (NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string)
}
const rejectHandlerTypeChange = () => {
if (currentNode.value.attributes?.rejectHandler.type === RejectHandlerType.RETURN_PRE_USER_TASK) {
console.log('nodeList is {}', returnTaskList.value);
}
}
// 6
const timeDuration = ref(6)
const timeUnit = ref(TimeUnitType.HOUR)

View File

@ -57,7 +57,11 @@
</div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
<!-- 递归显示子节点 -->
<ProcessNodeTree v-if="item && item.childNode" v-model:flow-node="item.childNode" />
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
@ -76,6 +80,10 @@ defineOptions({
name: 'ExclusiveNode'
})
const props = defineProps({
// parentNode : {
// type: Object as () => SimpleFlowNode,
// required: true
// },
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
@ -83,7 +91,9 @@ const props = defineProps({
})
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'update:modelValue': [node: SimpleFlowNode | undefined],
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number],
'find:recursiveFindParentNode': [nodeList: SimpleFlowNode[], curentNode: SimpleFlowNode, nodeType: number]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
@ -156,7 +166,21 @@ const moveNode = (index: number, to: number) => {
}
}
//
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// (NodeType.CONDITION_NODE) NodeType.EXCLUSIVE_NODE)
emits('find:parentNode', nodeList, nodeType)
}
</script>

View File

@ -38,6 +38,7 @@
v-if="currentNode"
ref="nodeSetting"
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
/>
</template>
<script setup lang="ts">
@ -55,7 +56,8 @@ const props = defineProps({
}
})
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'update:modelValue': [node: SimpleFlowNode | undefined],
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
@ -106,5 +108,12 @@ const copyNode = () => {
currentNode.value = newCopyNode
emits('update:modelValue', currentNode.value)
}
//
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[], //
) => {
//
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE);
}
</script>
<style lang="scss" scoped></style>