Merge remote-tracking branch 'yudao/feature/bpm' into feature/bpm-n

pull/652/head
Lesan 2025-01-07 13:34:45 +08:00
commit 0da3a9bf7b
7 changed files with 739 additions and 5 deletions

View File

@ -46,6 +46,13 @@
</div>
<div class="handler-item-text">延迟器</div>
</div>
<div class="handler-item" @click="addNode(NodeType.ROUTE_BRANCH_NODE)">
<!-- TODO @芋艿 需要更换一下iconfont的图标 -->
<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>
@ -120,7 +127,10 @@ const addNode = (type: number) => {
type: AssignEmptyHandlerType.APPROVE
},
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode
childNode: props.childNode,
createTaskListener: {
enable: false
}
}
emits('update:childNode', data)
}
@ -225,6 +235,17 @@ const addNode = (type: number) => {
}
emits('update:childNode', data)
}
if (type === NodeType.ROUTE_BRANCH_NODE) {
const data: SimpleFlowNode = {
id: 'GateWay_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.ROUTE_BRANCH_NODE) as string,
showText: '',
type: NodeType.ROUTE_BRANCH_NODE,
childNode: props.childNode,
defaultFlowId: 'Flow_' + generateUUID()
}
emits('update:childNode', data)
}
}
</script>

View File

@ -44,6 +44,12 @@
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 路由分支节点 -->
<RouteNode
v-if="currentNode && currentNode.type === NodeType.ROUTE_BRANCH_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
@ -67,6 +73,7 @@ import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
import InclusiveNode from './nodes/InclusiveNode.vue'
import DelayTimerNode from './nodes/DelayTimerNode.vue'
import RouteNode from './nodes/RouteNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({

View File

@ -56,7 +56,7 @@ const props = defineProps({
required: false
},
//
startUserIds : {
startUserIds: {
type: Array,
required: false
},
@ -91,6 +91,7 @@ provide('startUserIds', props.startUserIds)
const message = useMessage() //
const processNodeTree = ref<SimpleFlowNode | undefined>()
provide('processNodeTree', processNodeTree)
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []

View File

@ -44,7 +44,11 @@ export enum NodeType {
/**
* ()
*/
INCLUSIVE_BRANCH_NODE = 53
INCLUSIVE_BRANCH_NODE = 53,
/**
*
*/
ROUTE_BRANCH_NODE = 54
}
export enum NodeId {
@ -93,6 +97,8 @@ export interface SimpleFlowNode {
assignEmptyHandler?: AssignEmptyHandler
// 审批节点的审批人与发起人相同时,对应的处理类型
assignStartUserHandlerType?: number
// 创建任务监听器
createTaskListener: ListenerHandler
// 条件类型
conditionType?: ConditionType
// 条件表达式
@ -105,6 +111,9 @@ export interface SimpleFlowNode {
activityStatus?: TaskStatusEnum
// 延迟设置
delaySetting?: DelaySetting
// 路由分支
routeGroup?: RouteCondition[]
defaultFlowId?: string
}
// 候选人策略枚举 用于审批节点。抄送节点 )
export enum CandidateStrategy {
@ -222,6 +231,31 @@ export type AssignEmptyHandler = {
userIds?: number[]
}
/**
*
*/
export type ListenerHandler = {
enable: boolean
path: string
header: ListenerMap[]
body: ListenerMap[]
}
export type ListenerMap = {
key: string
type: number
value: string
}
export const LISTENER_MAP_TYPES = [
{
value: 1,
label: '固定值'
},
{
value: 2,
label: '表单'
}
]
// 审批拒绝类型枚举
export enum RejectHandlerType {
/**
@ -421,6 +455,7 @@ 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.DELAY_TIMER_NODE, '请设置延迟器')
NODE_DEFAULT_TEXT.set(NodeType.ROUTE_BRANCH_NODE, '请设置路由节点')
export const NODE_DEFAULT_NAME = new Map<number, string>()
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
@ -428,6 +463,7 @@ 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.DELAY_TIMER_NODE, '延迟器')
NODE_DEFAULT_NAME.set(NodeType.ROUTE_BRANCH_NODE, '路由分支')
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataVO[] = [
@ -604,3 +640,13 @@ export const DELAY_TYPE = [
{ label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
{ label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }
]
/**
*
*/
export type RouteCondition = {
nodeId: string
conditionType: number
conditionExpression: string
conditionGroups: ConditionGroup
}

View File

@ -0,0 +1,405 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="630"
: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>
<div>
<el-form label-position="top">
<el-card class="mb-15px" v-for="(item, index) in routeGroup" :key="index">
<template #header>
<div class="flex flex-items-center">
<el-text size="large">路由{{ index + 1 }}</el-text>
<el-select class="ml-15px" v-model="item.nodeId" style="width: 180px">
<el-option
v-for="node in nodeOptions"
:key="node.value"
:label="node.label"
:value="node.value"
/>
</el-select>
<el-button class="mla" type="danger" link @click="deleteRouteGroup(index)"
>删除</el-button
>
</div>
</template>
<el-form-item label="配置方式" prop="conditionType">
<el-radio-group v-model="item.conditionType" @change="changeConditionType">
<el-radio
v-for="(dict, indexConditionType) in conditionConfigTypes"
:key="indexConditionType"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="item.conditionType === 1"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="item.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item v-if="item.conditionType === 2" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="item.conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="item.conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(condition, cIdx) in item.conditionGroups.conditions"
:key="cIdx"
>
<div
class="condition-group-delete"
v-if="item.conditionGroups.conditions.length > 1"
>
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(item.conditionGroups.conditions, cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="condition.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
<div class="mr-2">
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(field, fIdx) in fieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="operator in COMPARISON_OPERATORS"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="rule.rightSide" style="width: 160px" />
</div>
<div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
<Icon
icon="ep:delete"
:size="18"
@click="deleteConditionRule(condition, rIdx)"
/>
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon
color="#0089ff"
icon="ep:plus"
:size="24"
@click="addConditionGroup(item.conditionGroups.conditions)"
/>
</div>
</el-form-item>
</el-card>
</el-form>
<el-button class="w-1/1" type="primary" :icon="Plus" @click="addRouteGroup">
新增路由分支
</el-button>
</div>
<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 { Plus } from '@element-plus/icons-vue'
import {
SimpleFlowNode,
NodeType,
CONDITION_CONFIG_TYPES,
ConditionType,
COMPARISON_OPERATORS,
RouteCondition,
ProcessVariableEnum
} from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node'
import { BpmModelFormType } from '@/utils/constants'
import { useFormFields } from '../node'
defineOptions({
name: 'RouteNodeConfig'
})
const message = useMessage() //
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree')
//
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
//
const currentNode = useWatchNode(props)
//
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTE_BRANCH_NODE)
const formType = inject<Ref<number>>('formType') //
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
//
if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
return false
} else {
return true
}
})
})
/** 条件规则可选择的表单字段 */
const fieldOptions = computed(() => {
const fieldsCopy = useFormFields().slice()
// ID
fieldsCopy.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
required: true
})
return fieldsCopy
})
const routeGroup = ref<RouteCondition[]>([])
const nodeOptions = ref()
//
const saveConfig = async () => {
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.showText = showText
currentNode.value.routeGroup = routeGroup.value
settingVisible.value = false
return true
}
//
const showRouteNodeConfig = (node: SimpleFlowNode) => {
getRoutableNode()
routeGroup.value = []
nodeName.value = node.name
if (node.routeGroup) {
routeGroup.value = node.routeGroup
}
}
const getShowText = () => {
if (!routeGroup.value || !Array.isArray(routeGroup.value) || routeGroup.value.length <= 0) {
message.warning('请配置路由!')
return ''
}
for (const route of routeGroup.value) {
if (!route.nodeId || !route.conditionType) {
message.warning('请完善路由配置项!')
return ''
}
if (route.conditionType === ConditionType.EXPRESSION && !route.conditionExpression) {
message.warning('请完善路由配置项!')
return ''
}
if (route.conditionType === ConditionType.RULE) {
for (const condition of route.conditionGroups.conditions) {
for (const rule of condition.rules) {
if (!rule.leftSide || !rule.rightSide) {
message.warning('请完善路由配置项!')
return ''
}
}
}
}
}
return `${routeGroup.value.length}条路由分支`
}
const changeConditionType = () => {}
const deleteConditionGroup = (conditions, index) => {
conditions.splice(index, 1)
}
const deleteConditionRule = (condition, index) => {
condition.rules.splice(index, 1)
}
const addConditionRule = (condition, index) => {
const rule = {
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(index + 1, 0, rule)
}
const addConditionGroup = (conditions) => {
const condition = {
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditions.push(condition)
}
const addRouteGroup = () => {
routeGroup.value.push({
nodeId: '',
conditionType: ConditionType.EXPRESSION,
conditionExpression: '',
conditionGroups: {
and: true,
conditions: [
{
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
}
})
}
const deleteRouteGroup = (index) => {
routeGroup.value.splice(index, 1)
}
const getRoutableNode = () => {
// TODO
//
//
let node = processNodeTree?.value
nodeOptions.value = []
while (true) {
if (!node) break
if (node.type !== NodeType.ROUTE_BRANCH_NODE) {
nodeOptions.value.push({
label: node.name,
value: node.id
})
}
if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
break
}
node = node.childNode
}
}
defineExpose({ openDrawer, showRouteNodeConfig }) //
</script>
<style lang="scss" scoped>
.condition-group-tool {
display: flex;
justify-content: space-between;
width: 500px;
margin-bottom: 20px;
}
.condition-group {
position: relative;
&:hover {
border-color: #0089ff;
.condition-group-delete {
opacity: 1;
}
}
.condition-group-delete {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: pointer;
opacity: 0;
}
}
::v-deep(.el-card__header) {
padding: 8px var(--el-card-padding);
border-bottom: 1px solid var(--el-card-border-color);
box-sizing: border-box;
}
</style>

View File

@ -19,7 +19,8 @@
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
{{ nodeName }}
<Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
@ -434,6 +435,137 @@
</div>
</div>
</el-tab-pane>
<el-tab-pane label="监听器" name="listener">
<div>
<el-form label-position="top">
<el-divider content-position="left">
<el-text tag="b" size="large">创建任务</el-text>
</el-divider>
<!-- TODO @lesancreateTaskListenerEnablecreateTaskListenerPath idea 红色的告警 -->
<el-form-item prop="createTaskListenerEnable">
<el-switch
v-model="configForm.createTaskListenerEnable"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
<div v-if="configForm.createTaskListenerEnable">
<el-form-item>
<el-alert
title="仅支持 POST 请求,以请求体方式接收参数"
type="warning"
show-icon
:closable="false"
/>
</el-form-item>
<el-form-item label="请求地址" prop="createTaskListenerPath">
<el-input v-model="configForm.createTaskListenerPath" />
</el-form-item>
<el-form-item label="请求头" prop="createTaskListenerHeader">
<div
class="flex pt-2"
v-for="(item, index) in configForm.createTaskListenerHeader"
:key="index"
>
<!-- TODO @lesancss 尽量用 unocss -->
<div class="mr-2">
<el-input v-model="item.key" style="width: 160px" />
</div>
<div class="mr-2">
<el-select v-model="item.type" style="width: 100px">
<el-option
v-for="types in LISTENER_MAP_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="item.value" style="width: 160px" />
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="deleteTaskListenerMap(configForm.createTaskListenerHeader, index)"
/>
</div>
</div>
<el-button
type="primary"
text
@click="addTaskListenerMap(configForm.createTaskListenerHeader)"
>
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
<el-form-item label="请求体" prop="createTaskListenerBody">
<div
class="flex pt-2"
v-for="(item, index) in configForm.createTaskListenerBody"
:key="index"
>
<div class="mr-2">
<el-input v-model="item.key" style="width: 160px" />
</div>
<div class="mr-2">
<el-select v-model="item.type" style="width: 100px">
<el-option
v-for="types in LISTENER_MAP_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="item.value" style="width: 160px" />
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="deleteTaskListenerMap(configForm.createTaskListenerBody, index)"
/>
</div>
</div>
<el-button
type="primary"
text
@click="addTaskListenerMap(configForm.createTaskListenerBody)"
>
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
</div>
<!-- TODO lesan待实现 -->
<el-divider content-position="left">
<el-text tag="b" size="large">指派任务执行人员</el-text>
</el-divider>
<el-form-item prop="assignTaskListenerEnable">
<el-switch
v-model="configForm.assignTaskListenerEnable"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
<!-- TODO lesan待实现 -->
<el-divider content-position="left">
<el-text tag="b" size="large">完成任务</el-text>
</el-divider>
<el-form-item prop="completeTaskListenerEnable">
<el-switch
v-model="configForm.completeTaskListenerEnable"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
@ -469,7 +601,8 @@ import {
ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType,
FieldPermissionType,
ProcessVariableEnum
ProcessVariableEnum,
LISTENER_MAP_TYPES
} from '../consts'
import {
@ -662,6 +795,13 @@ const saveConfig = async () => {
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
//
currentNode.value.buttonsSetting = buttonsSetting.value
//
currentNode.value.createTaskListener = {
enable: configForm.value.createTaskListenerEnable,
path: configForm.value.createTaskListenerPath,
header: configForm.value.createTaskListenerHeader,
body: configForm.value.createTaskListenerBody
}
currentNode.value.showText = showText
settingVisible.value = false
@ -713,6 +853,11 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
// 4.
getNodeConfigFormFields(node.fieldsPermission)
// 5.
configForm.value.createTaskListenerEnable = node.createTaskListener.enable
configForm.value.createTaskListenerPath = node.createTaskListener.path
configForm.value.createTaskListenerHeader = node.createTaskListener.header ?? []
configForm.value.createTaskListenerBody = node.createTaskListener.body ?? []
}
defineExpose({ openDrawer, showUserTaskNodeConfig }) //
@ -825,6 +970,17 @@ function useTimeoutHandler() {
cTimeoutMaxRemindCount
}
}
const addTaskListenerMap = (arr) => {
arr.push({
key: '',
type: 1,
value: ''
})
}
const deleteTaskListenerMap = (arr, index) => {
arr.splice(index, 1)
}
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,98 @@
<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">
<!-- TODO @芋艿 需要更换一下iconfont的图标 -->
<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.ROUTE_BRANCH_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>
<RouteNodeConfig
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 RouteNodeConfig from '../nodes-config/RouteNodeConfig.vue'
defineOptions({
name: 'RouteNode'
})
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.ROUTE_BRANCH_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showRouteNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
//
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>