Browse Source

Merge branch 'feature/bpm-n' of https://gitee.com/LesanOuO/yudao-ui-admin-vue3 into feature/bpm

# Conflicts:
#	src/components/SimpleProcessDesignerV2/src/consts.ts
YunaiV 7 months ago
parent
commit
528ebf3ea3

+ 3 - 0
src/components/ESign/index.ts

@@ -0,0 +1,3 @@
+import ESign from './src/ESign.vue'
+
+export { ESign }

+ 288 - 0
src/components/ESign/src/ESign.vue

@@ -0,0 +1,288 @@
+<template>
+  <div style="position: relative">
+    <canvas
+      ref="canvasRef"
+      @mousedown="mouseDown"
+      @mousemove="mouseMove"
+      @mouseup="mouseUp"
+      @touchstart="touchStart"
+      @touchmove="touchMove"
+      @touchend="touchEnd"
+      style="border: 1px solid lightgrey; max-width: 100%; display: block"
+    >
+    </canvas>
+
+    <el-button
+      style="position: absolute; bottom: 20px; right: 10px"
+      type="primary"
+      text
+      size="small"
+      @click="reset"
+    >
+      <Icon icon="ep:delete" class="mr-5px" />清除
+    </el-button>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'ESign' })
+
+const emits = defineEmits(['update:bgColor'])
+const props = defineProps({
+  // 画布宽度,即导出图片的宽度
+  width: propTypes.number.def(900),
+  // 画布高度,即导出图片的高度
+  height: propTypes.number.def(400),
+  // 画笔粗细
+  lineWidth: propTypes.number.def(10),
+  // 画笔颜色
+  lineColor: propTypes.string.def('#000000'),
+  // 画布背景色,为空时画布背景透明
+  bgColor: propTypes.string.def(''),
+  // 是否裁剪,在画布设定尺寸基础上裁掉四周空白部分
+  isCrop: propTypes.bool.def(false),
+  // 清空画布时是否同时清空设置的背景色
+  isClearBgColor: propTypes.bool.def(true),
+  // 生成图片格式
+  format: propTypes.string.def('image/png'),
+  // 生成图片质量,0 到 1
+  quality: propTypes.number.def(1)
+})
+const canvasRef = ref()
+const hasDrew = ref(false)
+const resultImg = ref('')
+const points = ref<any>([])
+const canvasTxt = ref()
+const startX = ref(0)
+const startY = ref(0)
+const isDrawing = ref(false)
+const sratio = ref(1)
+
+const ratio = computed(() => {
+  return props.height / props.width
+})
+const stageInfo = computed(() => {
+  return canvasRef.value.getBoundingClientRect()
+})
+const bgColor = computed(() => {
+  return props.bgColor ? props.bgColor : 'rgba(255, 255, 255, 0)'
+})
+
+watch(
+  () => bgColor.value,
+  () => {
+    if (canvasRef.value) {
+      canvasRef.value.style.background = bgColor.value
+    }
+  },
+  {
+    immediate: true
+  }
+)
+
+const resizeHandler = () => {
+  const canvas = canvasRef.value
+  canvas.style.width = props.width + 'px'
+  const realw = parseFloat(window.getComputedStyle(canvas).width)
+  canvas.style.height = ratio.value * realw + 'px'
+  canvasTxt.value = canvas.getContext('2d')
+  canvasTxt.value.scale(1 * sratio.value, 1 * sratio.value)
+  sratio.value = realw / props.width
+  canvasTxt.value.scale(1 / sratio.value, 1 / sratio.value)
+}
+// For PC
+const mouseDown = (e) => {
+  e.preventDefault()
+  isDrawing.value = true
+  hasDrew.value = true
+  let obj = {
+    x: e.offsetX,
+    y: e.offsetY
+  }
+  drawStart(obj)
+}
+const mouseMove = (e) => {
+  e.preventDefault()
+  if (isDrawing.value) {
+    let obj = {
+      x: e.offsetX,
+      y: e.offsetY
+    }
+    drawMove(obj)
+  }
+}
+const mouseUp = (e) => {
+  e.preventDefault()
+  let obj = {
+    x: e.offsetX,
+    y: e.offsetY
+  }
+  drawEnd(obj)
+  isDrawing.value = false
+}
+// For Mobile
+const touchStart = (e) => {
+  e.preventDefault()
+  hasDrew.value = true
+  if (e.touches.length === 1) {
+    let obj = {
+      x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
+      y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
+    }
+    drawStart(obj)
+  }
+}
+const touchMove = (e) => {
+  e.preventDefault()
+  if (e.touches.length === 1) {
+    let obj = {
+      x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
+      y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
+    }
+    drawMove(obj)
+  }
+}
+const touchEnd = (e) => {
+  e.preventDefault()
+  if (e.touches.length === 1) {
+    let obj = {
+      x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
+      y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
+    }
+    drawEnd(obj)
+  }
+}
+// 绘制
+const drawStart = (obj) => {
+  startX.value = obj.x
+  startY.value = obj.y
+  canvasTxt.value.beginPath()
+  canvasTxt.value.moveTo(startX.value, startY.value)
+  canvasTxt.value.lineTo(obj.x, obj.y)
+  canvasTxt.value.lineCap = 'round'
+  canvasTxt.value.lineJoin = 'round'
+  canvasTxt.value.lineWidth = props.lineWidth * sratio.value
+  canvasTxt.value.stroke()
+  canvasTxt.value.closePath()
+  points.value.push(obj)
+}
+const drawMove = (obj) => {
+  canvasTxt.value.beginPath()
+  canvasTxt.value.moveTo(startX.value, startY.value)
+  canvasTxt.value.lineTo(obj.x, obj.y)
+  canvasTxt.value.strokeStyle = props.lineColor
+  canvasTxt.value.lineWidth = props.lineWidth * sratio.value
+  canvasTxt.value.lineCap = 'round'
+  canvasTxt.value.lineJoin = 'round'
+  canvasTxt.value.stroke()
+  canvasTxt.value.closePath()
+  startY.value = obj.y
+  startX.value = obj.x
+  points.value.push(obj)
+}
+const drawEnd = (obj) => {
+  canvasTxt.value.beginPath()
+  canvasTxt.value.moveTo(startX.value, startY.value)
+  canvasTxt.value.lineTo(obj.x, obj.y)
+  canvasTxt.value.lineCap = 'round'
+  canvasTxt.value.lineJoin = 'round'
+  canvasTxt.value.stroke()
+  canvasTxt.value.closePath()
+  points.value.push(obj)
+  points.value.push({ x: -1, y: -1 })
+}
+// 生成
+const generate = (options) => {
+  let imgFormat = options && options.format ? options.format : props.format
+  let imgQuality = options && options.quality ? options.quality : props.quality
+  const pm = new Promise((resolve, reject) => {
+    if (!hasDrew.value) {
+      reject(`Warning: Not Signned!`)
+      return
+    }
+    let resImgData = canvasTxt.value.getImageData(
+      0,
+      0,
+      canvasRef.value.width,
+      canvasRef.value.height
+    )
+    canvasTxt.value.globalCompositeOperation = 'destination-over'
+    canvasTxt.value.fillStyle = bgColor.value
+    canvasTxt.value.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height)
+    resultImg.value = canvasRef.value.toDataURL(imgFormat, imgQuality)
+    canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
+    canvasTxt.value.putImageData(resImgData, 0, 0)
+    canvasTxt.value.globalCompositeOperation = 'source-over'
+    if (props.isCrop) {
+      const crop_area = getCropArea(resImgData.data)
+      let crop_canvas = document.createElement('canvas')
+      const crop_ctx = crop_canvas.getContext('2d')
+      crop_canvas.width = crop_area[2] - crop_area[0]
+      crop_canvas.height = crop_area[3] - crop_area[1]
+      const crop_imgData = canvasTxt.value.getImageData(...crop_area)
+      crop_ctx.globalCompositeOperation = 'destination-over'
+      crop_ctx.putImageData(crop_imgData, 0, 0)
+      crop_ctx.fillStyle = bgColor.value
+      crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height)
+      resultImg.value = crop_canvas.toDataURL(imgFormat, imgQuality)
+    }
+    resolve(resultImg.value)
+  })
+  return pm
+}
+const reset = () => {
+  canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
+  if (props.isClearBgColor) {
+    emits('update:bgColor', '')
+    canvasRef.value.style.background = 'rgba(255, 255, 255, 0)'
+  }
+  points.value = []
+  hasDrew.value = false
+  resultImg.value = ''
+}
+const getCropArea = (imgData) => {
+  let topX = canvasRef.value.width
+  let btmX = 0
+  let topY = canvasRef.value.height
+  let btnY = 0
+  for (let i = 0; i < canvasRef.value.width; i++) {
+    for (let j = 0; j < canvasRef.value.height; j++) {
+      let pos = (i + canvasRef.value.width * j) * 4
+      if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
+        btnY = Math.max(j, btnY)
+        btmX = Math.max(i, btmX)
+        topY = Math.min(j, topY)
+        topX = Math.min(i, topX)
+      }
+    }
+  }
+  topX++
+  btmX++
+  topY++
+  btnY++
+  const data = [topX, topY, btmX, btnY]
+  return data
+}
+
+defineExpose({
+  generate
+})
+onBeforeMount(() => {
+  window.addEventListener('resize', resizeHandler)
+})
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeHandler)
+})
+onMounted(() => {
+  canvasRef.value.height = props.height
+  canvasRef.value.width = props.width
+  canvasRef.value.style.background = bgColor.value
+  resizeHandler()
+  // 在画板以外松开鼠标后冻结画笔
+  document.onmouseup = () => {
+    isDrawing.value = false
+  }
+})
+</script>

+ 7 - 1
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -128,7 +128,13 @@ const addNode = (type: number) => {
       },
       assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
       childNode: props.childNode,
-      createTaskListener: {
+      taskCreateListener: {
+        enable: false
+      },
+      taskAssignListener: {
+        enable: false
+      },
+      taskCompleteListener: {
         enable: false
       }
     }

+ 18 - 4
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -98,7 +98,11 @@ export interface SimpleFlowNode {
   // 审批节点的审批人与发起人相同时,对应的处理类型
   assignStartUserHandlerType?: number
   // 创建任务监听器
-  createTaskListener?: ListenerHandler
+  taskCreateListener?: ListenerHandler
+  // 创建任务监听器
+  taskAssignListener?: ListenerHandler
+  // 创建任务监听器
+  taskCompleteListener?: ListenerHandler
   // 条件类型
   conditionType?: ConditionType
   // 条件表达式
@@ -236,15 +240,25 @@ export type AssignEmptyHandler = {
  */
 export type ListenerHandler = {
   enable: boolean
-  path: string
-  header: ListenerMap[]
-  body: ListenerMap[]
+  path?: string
+  header?: ListenerMap[]
+  body?: ListenerMap[]
 }
 export type ListenerMap = {
   key: string
   type: number
   value: string
 }
+export enum ListenerMapTypeEnum {
+  /**
+   * 固定值
+   */
+  FIXED_VALUE = 1,
+  /**
+   * 表单
+   */
+  FROM_FORM = 2
+}
 export const LISTENER_MAP_TYPES = [
   {
     value: 1,

+ 14 - 1
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -14,7 +14,8 @@ import {
   NODE_DEFAULT_NAME,
   AssignStartUserHandlerType,
   AssignEmptyHandlerType,
-  FieldPermissionType
+  FieldPermissionType,
+  ListenerMap
 } from './consts'
 import { parseFormFields } from '@/components/FormCreate/src/utils/index'
 export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
@@ -136,6 +137,18 @@ export type UserTaskFormType = {
   timeDuration?: number
   maxRemindCount?: number
   buttonsSetting: any[]
+  taskCreateListenerEnable?: boolean
+  taskCreateListenerPath?: string
+  taskCreateListenerHeader?: ListenerMap[]
+  taskCreateListenerBody?: ListenerMap[]
+  taskAssignListenerEnable?: boolean
+  taskAssignListenerPath?: string
+  taskAssignListenerHeader?: ListenerMap[]
+  taskAssignListenerBody?: ListenerMap[]
+  taskCompleteListenerEnable?: boolean
+  taskCompleteListenerPath?: string
+  taskCompleteListenerHeader?: ListenerMap[]
+  taskCompleteListenerBody?: ListenerMap[]
 }
 
 export type CopyTaskFormType = {

+ 190 - 129
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -436,133 +436,153 @@
         </div>
       </el-tab-pane>
       <el-tab-pane label="监听器" name="listener">
-        <div>
+        <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
           <el-form label-position="top">
-            <el-divider content-position="left">
-              <el-text tag="b" size="large">创建任务</el-text>
-            </el-divider>
-            <!-- TODO @lesan:createTaskListenerEnable、createTaskListenerPath 等 idea 红色的告警! -->
-            <el-form-item prop="createTaskListenerEnable">
-              <el-switch
-                v-model="configForm.createTaskListenerEnable"
-                active-text="开启"
-                inactive-text="关闭"
-              />
-            </el-form-item>
-            <div v-if="configForm.createTaskListenerEnable">
+            <div>
+              <el-divider content-position="left">
+                <el-text tag="b" size="large">{{ listener.name }}</el-text>
+              </el-divider>
               <el-form-item>
-                <el-alert
-                  title="仅支持 POST 请求,以请求体方式接收参数"
-                  type="warning"
-                  show-icon
-                  :closable="false"
+                <el-switch
+                  v-model="configForm[`task${listener.type}ListenerEnable`]"
+                  active-text="开启"
+                  inactive-text="关闭"
                 />
               </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 @lesan:css 尽量用 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"
+              <div v-if="configForm[`task${listener.type}ListenerEnable`]">
+                <el-form-item>
+                  <el-alert
+                    title="仅支持 POST 请求,以请求体方式接收参数"
+                    type="warning"
+                    show-icon
+                    :closable="false"
+                  />
+                </el-form-item>
+                <el-form-item label="请求地址">
+                  <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-input class="w-160px" v-model="item.key" />
+                    </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-input
+                        v-if="item.type === ListenerMapTypeEnum.FIXED_VALUE"
+                        class="w-160px"
+                        v-model="item.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
+                        v-if="item.type === ListenerMapTypeEnum.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>
+                    </div>
+                    <div class="mr-1 flex items-center">
+                      <Icon
+                        icon="ep:delete"
+                        :size="18"
+                        @click="
+                          deleteTaskListenerMap(
+                            configForm[`task${listener.type}ListenerHeader`],
+                            index
+                          )
+                        "
                       />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-input v-model="item.value" style="width: 160px" />
+                    </div>
                   </div>
-                  <div class="mr-1 flex items-center">
-                    <Icon
-                      icon="ep:delete"
-                      :size="18"
-                      @click="deleteTaskListenerMap(configForm.createTaskListenerBody, index)"
-                    />
+                  <el-button
+                    type="primary"
+                    text
+                    @click="addTaskListenerMap(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-input class="w-160px" v-model="item.key" />
+                    </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-input
+                        v-if="item.type === ListenerMapTypeEnum.FIXED_VALUE"
+                        class="w-160px"
+                        v-model="item.value"
+                      />
+                      <el-select
+                        v-if="item.type === ListenerMapTypeEnum.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>
+                    </div>
+                    <div class="mr-1 flex items-center">
+                      <Icon
+                        icon="ep:delete"
+                        :size="18"
+                        @click="
+                          deleteTaskListenerMap(
+                            configForm[`task${listener.type}ListenerBody`],
+                            index
+                          )
+                        "
+                      />
+                    </div>
                   </div>
-                </div>
-                <el-button
-                  type="primary"
-                  text
-                  @click="addTaskListenerMap(configForm.createTaskListenerBody)"
-                >
-                  <Icon icon="ep:plus" class="mr-5px" />添加一行
-                </el-button>
-              </el-form-item>
+                  <el-button
+                    type="primary"
+                    text
+                    @click="addTaskListenerMap(configForm[`task${listener.type}ListenerBody`])"
+                  >
+                    <Icon icon="ep:plus" class="mr-5px" />添加一行
+                  </el-button>
+                </el-form-item>
+              </div>
             </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>
@@ -602,7 +622,8 @@ import {
   AssignEmptyHandlerType,
   FieldPermissionType,
   ProcessVariableEnum,
-  LISTENER_MAP_TYPES
+  LISTENER_MAP_TYPES,
+  ListenerMapTypeEnum
 } from '../consts'
 
 import {
@@ -693,6 +714,21 @@ const formRules = reactive({
   assignEmptyHandlerUserIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
   assignStartUserHandlerType: [{ required: true }]
 })
+// 监听器数组
+const taskListener = ref([
+  {
+    name: '创建任务',
+    type: 'Create'
+  },
+  {
+    name: '指派任务执行人员',
+    type: 'Assign'
+  },
+  {
+    name: '完成任务',
+    type: 'Complete'
+  }
+])
 
 const {
   configForm: tempConfigForm,
@@ -796,11 +832,25 @@ const saveConfig = async () => {
   // 设置按钮权限
   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.taskCreateListener = {
+    enable: configForm.value.taskCreateListenerEnable ?? false,
+    path: configForm.value.taskCreateListenerPath,
+    header: configForm.value.taskCreateListenerHeader,
+    body: configForm.value.taskCreateListenerBody
+  }
+  // 指派任务监听器
+  currentNode.value.taskAssignListener = {
+    enable: configForm.value.taskAssignListenerEnable ?? false,
+    path: configForm.value.taskAssignListenerPath,
+    header: configForm.value.taskAssignListenerHeader,
+    body: configForm.value.taskAssignListenerBody
+  }
+  // 完成任务监听器
+  currentNode.value.taskCompleteListener = {
+    enable: configForm.value.taskCompleteListenerEnable ?? false,
+    path: configForm.value.taskCompleteListenerPath,
+    header: configForm.value.taskCompleteListenerHeader,
+    body: configForm.value.taskCompleteListenerBody
   }
 
   currentNode.value.showText = showText
@@ -853,11 +903,22 @@ 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 ?? []
+  // 5. 监听器
+  // 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 ?? []
+  // 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.taskCompleteListenerEnable = node.taskCompleteListener!.enable
+  configForm.value.taskCompleteListenerPath = node.taskCompleteListener!.path
+  configForm.value.taskCompleteListenerHeader = node.taskCompleteListener?.header ?? []
+  configForm.value.taskCompleteListenerBody = node.taskCompleteListener?.body ?? []
 }
 
 defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件