【功能新增】Simple 设计器新增触发器节点

pull/683/MERGE
jason 2025-01-24 23:36:26 +08:00
parent 7043dea354
commit b7329b0c44
9 changed files with 508 additions and 209 deletions

View File

@ -51,15 +51,13 @@
</div>
<div class="handler-item-text">路由分支</div>
</div>
<!-- TODO 触发器
<div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
<div class="handler-item-icon trigger">
<span class="iconfont icon-size icon-trigger"></span>
</div>
<div class="handler-item-text">触发器</div>
<div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
<div class="handler-item-icon trigger">
<span class="iconfont icon-size icon-trigger"></span>
</div>
-->
</div>
<div class="handler-item-text">触发器</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
@ -272,7 +270,7 @@ const addNode = (type: number) => {
if (type === NodeType.TRIGGER_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string,
showText: '',
type: NodeType.TRIGGER_NODE,
childNode: props.childNode

View File

@ -49,6 +49,12 @@
v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 触发器节点 -->
<TriggerNode
v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
@ -74,6 +80,7 @@ import ParallelNode from './nodes/ParallelNode.vue'
import InclusiveNode from './nodes/InclusiveNode.vue'
import DelayTimerNode from './nodes/DelayTimerNode.vue'
import RouterNode from './nodes/RouterNode.vue'
import TriggerNode from './nodes/TriggerNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({

View File

@ -121,6 +121,8 @@ export interface SimpleFlowNode {
signEnable?: boolean
// 审批意见
reasonRequire?: boolean
// 触发器设置
triggerSetting?: TriggerSetting
}
// 候选人策略枚举 用于审批节点。抄送节点 )
export enum CandidateStrategy {
@ -707,3 +709,38 @@ export type RouterSetting = {
conditionExpression: string
conditionGroups: ConditionGroup
}
// ==================== 触发器相关定义 ====================
/**
*
*/
export type TriggerSetting = {
type: TriggerTypeEnum
httpRequestSetting: HttpRequestTriggerSetting
}
/**
*
*/
export enum TriggerTypeEnum {
/**
* HTTP
*/
HTTP_REQUEST = 1,
}
/**
* HTTP
*/
export type HttpRequestTriggerSetting = {
// 请求 URL
url: string
// 请求头参数设置
header?: ListenerParam[] // TODO 需要重命名一下
// 请求体参数设置
body?: ListenerParam[]
}
export const TRIGGER_TYPES: DictDataVO[] = [
{ label: 'HTTP 请求', value: TriggerTypeEnum.HTTP_REQUEST }
]

View File

@ -137,16 +137,22 @@ export type UserTaskFormType = {
buttonsSetting: any[]
taskCreateListenerEnable?: boolean
taskCreateListenerPath?: string
taskCreateListenerHeader?: ListenerParam[]
taskCreateListenerBody?: ListenerParam[]
taskCreateListener?: {
header: ListenerParam[],
body: ListenerParam[]
}
taskAssignListenerEnable?: boolean
taskAssignListenerPath?: string
taskAssignListenerHeader?: ListenerParam[]
taskAssignListenerBody?: ListenerParam[]
taskAssignListener?: {
header: ListenerParam[],
body: ListenerParam[]
}
taskCompleteListenerEnable?: boolean
taskCompleteListenerPath?: string
taskCompleteListenerHeader?: ListenerParam[]
taskCompleteListenerBody?: ListenerParam[]
taskCompleteListener?:{
header: ListenerParam[],
body: ListenerParam[]
}
signEnable: boolean
reasonRequire: boolean
}

View File

@ -0,0 +1,140 @@
<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>
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="触发器类型" prop="type">
<el-select v-model="configForm.type">
<el-option
v-for="(item, index) in TRIGGER_TYPES"
:key="index"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<div
v-if="configForm.type === TriggerTypeEnum.HTTP_REQUEST && configForm.httpRequestSetting"
>
<el-form-item>
<el-alert
title="仅支持 POST 请求,以请求体方式接收参数"
type="warning"
show-icon
:closable="false"
/>
</el-form-item>
<el-form-item label="请求地址" prop="httpRequestSetting.url">
<el-input v-model="configForm.httpRequestSetting.url" />
</el-form-item>
<HttpRequestParamSetting
:header="configForm.httpRequestSetting.header"
:body="configForm.httpRequestSetting.body"
:bind="'httpRequestSetting'"
/>
</div>
</el-form>
</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 { SimpleFlowNode, NodeType, TriggerSetting, TRIGGER_TYPES, TriggerTypeEnum } from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node'
import HttpRequestParamSetting from './components/HttpRequestParamSetting.vue'
defineOptions({
name: 'TriggerNodeConfig'
})
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.TRIGGER_NODE)
//
const formRef = ref() // Ref
//
const formRules = reactive({
type: [{ required: true, message: '触发器类型不能为空', trigger: 'change' }],
httpRequestSetting: {
url: [{ required: true, message: '请求地址不能为空', trigger: 'blur' }]
}
})
//
const configForm = ref<TriggerSetting>({
type: TriggerTypeEnum.HTTP_REQUEST,
httpRequestSetting: {
url: '',
header: [],
body: []
}
})
//
const saveConfig = async () => {
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.showText = showText
currentNode.value.triggerSetting = configForm.value
settingVisible.value = false
return true
}
//
const getShowText = (): string => {
let showText = ''
if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
showText = `${configForm.value.httpRequestSetting.url}`
}
return showText
}
//
const showTriggerNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
if (node.triggerSetting) {
configForm.value.type = node.triggerSetting.type
configForm.value.httpRequestSetting = node.triggerSetting.httpRequestSetting
}
}
defineExpose({ openDrawer, showTriggerNodeConfig }) //
</script>
<style lang="scss" scoped></style>

View File

@ -627,7 +627,7 @@ const userTaskListenerRef = ref()
//
const saveConfig = async () => {
activeTabName.value = 'user'
// activeTabName.value = 'user'
//
currentNode.value.name = nodeName.value!
//
@ -684,22 +684,22 @@ const saveConfig = async () => {
currentNode.value.taskCreateListener = {
enable: configForm.value.taskCreateListenerEnable ?? false,
path: configForm.value.taskCreateListenerPath,
header: configForm.value.taskCreateListenerHeader,
body: configForm.value.taskCreateListenerBody
header: configForm.value.taskCreateListener?.header,
body: configForm.value.taskCreateListener?.body
}
//
currentNode.value.taskAssignListener = {
enable: configForm.value.taskAssignListenerEnable ?? false,
path: configForm.value.taskAssignListenerPath,
header: configForm.value.taskAssignListenerHeader,
body: configForm.value.taskAssignListenerBody
header: configForm.value.taskAssignListener?.header,
body: configForm.value.taskAssignListener?.body
}
//
currentNode.value.taskCompleteListener = {
enable: configForm.value.taskCompleteListenerEnable ?? false,
path: configForm.value.taskCompleteListenerPath,
header: configForm.value.taskCompleteListenerHeader,
body: configForm.value.taskCompleteListenerBody
header: configForm.value.taskCompleteListener?.header,
body: configForm.value.taskCompleteListener?.body
}
//
currentNode.value.signEnable = configForm.value.signEnable
@ -760,18 +760,24 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
// 5.1
configForm.value.taskCreateListenerEnable = node.taskCreateListener!.enable
configForm.value.taskCreateListenerPath = node.taskCreateListener!.path
configForm.value.taskCreateListenerHeader = node.taskCreateListener?.header ?? []
configForm.value.taskCreateListenerBody = node.taskCreateListener?.body ?? []
configForm.value.taskCreateListener = {
header: node.taskCreateListener?.header ?? [],
body: node.taskCreateListener?.body ?? []
}
// 5.2
configForm.value.taskAssignListenerEnable = node.taskAssignListener!.enable
configForm.value.taskAssignListenerPath = node.taskAssignListener!.path
configForm.value.taskAssignListenerHeader = node.taskAssignListener?.header ?? []
configForm.value.taskAssignListenerBody = node.taskAssignListener?.body ?? []
// 5.3
configForm.value.taskAssignListener = {
header: node.taskAssignListener?.header ?? [],
body: node.taskAssignListener?.body ?? []
}
// 5.3
configForm.value.taskCompleteListenerEnable = node.taskCompleteListener!.enable
configForm.value.taskCompleteListenerPath = node.taskCompleteListener!.path
configForm.value.taskCompleteListenerHeader = node.taskCompleteListener?.header ?? []
configForm.value.taskCompleteListenerBody = node.taskCompleteListener?.body ?? []
configForm.value.taskCompleteListener = {
header: node.taskCompleteListener?.header ?? [],
body: node.taskCompleteListener?.body ?? []
}
// 6.
configForm.value.signEnable = node?.signEnable ?? false
// 7.

View File

@ -0,0 +1,181 @@
<template>
<el-form-item label="请求头">
<div class="flex pt-2" v-for="(item, index) in props.header" :key="index">
<div class="mr-2">
<el-form-item
:prop="`${bind}.header.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<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-form-item
:prop="`${bind}.header.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`${bind}.header.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.header, index)" />
</div>
</div>
<el-button type="primary" text @click="addHttpRequestParam(props.header)">
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
<el-form-item label="请求体">
<div class="flex pt-2" v-for="(item, index) in props.body" :key="index">
<div class="mr-2">
<el-form-item
:prop="`${bind}.body.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<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-form-item
:prop="`${bind}.body.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`${bind}.body.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.body, index)" />
</div>
</div>
<el-button type="primary" text @click="addHttpRequestParam(props.body)">
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
</template>
<script setup lang="ts">
import { ListenerParam, LISTENER_MAP_TYPES, ListenerParamTypeEnum } from '../../consts'
import { useFormFields } from '../../node'
defineOptions({
name: 'HttpRequestParamSetting'
})
const props = defineProps({
header: {
type: Array as () => ListenerParam[],
required: false,
default: () => []
},
body: {
type: Array as () => ListenerParam[],
required: false,
default: () => []
},
bind: {
type: String,
required: true
}
})
const formFieldOptions = useFormFields()
const addHttpRequestParam = (arr) => {
arr.push({
key: '',
type: 1,
value: ''
})
}
const deleteHttpRequestParam = (arr, index) => {
arr.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>

View File

@ -31,181 +31,19 @@
>
<el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
</el-form-item>
<el-form-item label="请求头">
<div
class="flex pt-2"
v-for="(item, index) in configForm[`task${listener.type}ListenerHeader`]"
:key="index"
>
<div class="mr-2">
<el-form-item
:prop="`task${listener.type}ListenerHeader.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<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-form-item
:prop="`task${listener.type}ListenerHeader.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`task${listener.type}ListenerHeader.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="
deleteTaskListenerParam(configForm[`task${listener.type}ListenerHeader`], index)
"
/>
</div>
</div>
<el-button
type="primary"
text
@click="addTaskListenerParam(configForm[`task${listener.type}ListenerHeader`])"
>
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
<el-form-item label="请求体">
<div
class="flex pt-2"
v-for="(item, index) in configForm[`task${listener.type}ListenerBody`]"
:key="index"
>
<div class="mr-2">
<el-form-item
:prop="`task${listener.type}ListenerBody.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<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-form-item
:prop="`task${listener.type}ListenerBody.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`task${listener.type}ListenerBody.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon
icon="ep:delete"
:size="18"
@click="
deleteTaskListenerParam(configForm[`task${listener.type}ListenerBody`], index)
"
/>
</div>
</div>
<el-button
type="primary"
text
@click="addTaskListenerParam(configForm[`task${listener.type}ListenerBody`])"
>
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
<HttpRequestParamSetting
:header="configForm[`task${listener.type}Listener`].header"
:body="configForm[`task${listener.type}Listener`].body"
:bind="`task${listener.type}Listener`"
/>
</div>
</div>
</el-form>
</template>
<script setup lang="ts">
import { LISTENER_MAP_TYPES, ListenerParamTypeEnum } from '../../consts'
// import { LISTENER_MAP_TYPES, ListenerParamTypeEnum } from '../../consts'
import HttpRequestParamSetting from './HttpRequestParamSetting.vue'
const props = defineProps({
modelValue: {
type: Object,
@ -241,17 +79,6 @@ const taskListener = ref([
}
])
const addTaskListenerParam = (arr) => {
arr.push({
key: '',
type: 1,
value: ''
})
}
const deleteTaskListenerParam = (arr, index) => {
arr.splice(index, 1)
}
const validate = async () => {
if (!listenerFormRef) return false
return await listenerFormRef.value.validate()

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 trigger-node">
<span class="iconfont icon-trigger"></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.TRIGGER_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>
<TriggerNodeConfig 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 TriggerNodeConfig from '../nodes-config/TriggerNodeConfig.vue'
defineOptions({
name: 'TriggerNode'
})
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.TRIGGER_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showTriggerNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
//
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>