Переглянути джерело

!659 BPM:签名功能完善
Merge pull request !659 from Lesan/feature/bpm-n

芋道源码 7 місяців тому
батько
коміт
e8e357b8a2

+ 1 - 1
src/api/bpm/processInstance/index.ts

@@ -36,7 +36,7 @@ export type ApprovalTaskInfo = {
   assigneeUser: User
   status: number
   reason: string
-  sign: string // TODO @lesan:字段改成 signPicUrl 签名照片。只有 sign 感觉是签名文本哈。
+  signPicUrl: string
 }
 
 // 审批节点信息

+ 12 - 8
src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue

@@ -86,7 +86,7 @@ const currentNode = useWatchNode(props)
 // 节点名称
 const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTER_BRANCH_NODE)
 const routerGroups = ref<RouterCondition[]>([])
-const nodeOptions = ref()
+const nodeOptions = ref<any>([])
 const conditionRef = ref([])
 
 /** 保存配置 */
@@ -94,7 +94,7 @@ const saveConfig = async () => {
   // 校验表单
   let valid = true
   for (const item of conditionRef.value) {
-    if (!(await item.validate())) {
+    if (item && !(await item.validate())) {
       valid = false
     }
   }
@@ -109,7 +109,7 @@ const saveConfig = async () => {
 }
 // 显示路由分支节点配置, 由父组件传过来
 const showRouteNodeConfig = (node: SimpleFlowNode) => {
-  getRouterNode()
+  getRouterNode(processNodeTree?.value)
   routerGroups.value = []
   nodeName.value = node.name
   if (node.routerGroups) {
@@ -172,15 +172,14 @@ const deleteRouterGroup = (index: number) => {
   routerGroups.value.splice(index, 1)
 }
 
-const getRouterNode = () => {
-  // TODO @lesan 还需要满足以下要求
+// 递归获取所有节点
+const getRouterNode = (node) => {
+  // TODO 最好还需要满足以下要求
   // 并行分支、包容分支内部节点不能跳转到外部节点
   // 条件分支节点可以向上跳转到外部节点
-  let node = processNodeTree?.value
-  nodeOptions.value = []
   while (true) {
     if (!node) break
-    if (node.type !== NodeType.ROUTER_BRANCH_NODE) {
+    if (node.type !== NodeType.ROUTER_BRANCH_NODE && node.type !== NodeType.CONDITION_NODE) {
       nodeOptions.value.push({
         label: node.name,
         value: node.id
@@ -189,6 +188,11 @@ const getRouterNode = () => {
     if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
       break
     }
+    if (node.conditionNodes && node.conditionNodes.length) {
+      node.conditionNodes.forEach((item) => {
+        getRouterNode(item)
+      })
+    }
     node = node.childNode
   }
 }

+ 6 - 242
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -440,217 +440,8 @@
           </div>
         </div>
       </el-tab-pane>
-      <!-- TODO @lesan:要不抽成 Listener 小组件?类似 Condition.vue -->
       <el-tab-pane label="监听器" name="listener">
-        <el-form ref="listenerFormRef" :model="configForm" label-position="top">
-          <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
-            <el-divider content-position="left">
-              <el-text tag="b" size="large">{{ listener.name }}</el-text>
-            </el-divider>
-            <el-form-item>
-              <el-switch
-                v-model="configForm[`task${listener.type}ListenerEnable`]"
-                active-text="开启"
-                inactive-text="关闭"
-              />
-            </el-form-item>
-            <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="请求地址"
-                :prop="`task${listener.type}ListenerPath`"
-                :rules="{
-                  required: true,
-                  message: '请求地址不能为空',
-                  trigger: 'blur'
-                }"
-              >
-                <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>
-            </div>
-          </div>
-        </el-form>
+        <UserTaskListener ref="userTaskListenerRef" v-model="configForm" :form-field-options="formFieldOptions" />
       </el-tab-pane>
     </el-tabs>
     <template #footer>
@@ -687,9 +478,7 @@ import {
   ASSIGN_EMPTY_HANDLER_TYPES,
   AssignEmptyHandlerType,
   FieldPermissionType,
-  ProcessVariableEnum,
-  LISTENER_MAP_TYPES,
-  ListenerParamTypeEnum
+  ProcessVariableEnum
 } from '../consts'
 
 import {
@@ -703,6 +492,7 @@ import {
 import { defaultProps } from '@/utils/tree'
 import { cloneDeep } from 'lodash-es'
 import { convertTimeUnit, getApproveTypeText } from '../utils'
+import UserTaskListener from './components/UserTaskListener.vue'
 defineOptions({
   name: 'UserTaskNodeConfig'
 })
@@ -780,21 +570,6 @@ 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,
@@ -843,7 +618,7 @@ const {
   cTimeoutMaxRemindCount
 } = useTimeoutHandler()
 
-const listenerFormRef = ref()
+const userTaskListenerRef = ref()
 
 // 保存配置
 const saveConfig = async () => {
@@ -860,8 +635,8 @@ const saveConfig = async () => {
   }
 
   if (!formRef) return false
-  if (!listenerFormRef) return false
-  const valid = (await formRef.value.validate()) && (await listenerFormRef.value.validate())
+  if (!userTaskListenerRef) return false
+  const valid = (await formRef.value.validate()) && (await userTaskListenerRef.value.validate())
   if (!valid) return false
   const showText = getShowText()
   if (!showText) return false
@@ -1104,17 +879,6 @@ function useTimeoutHandler() {
     cTimeoutMaxRemindCount
   }
 }
-
-const addTaskListenerParam = (arr) => {
-  arr.push({
-    key: '',
-    type: 1,
-    value: ''
-  })
-}
-const deleteTaskListenerParam = (arr, index) => {
-  arr.splice(index, 1)
-}
 </script>
 
 <style lang="scss" scoped>

+ 261 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue

@@ -0,0 +1,261 @@
+<template>
+  <el-form ref="listenerFormRef" :model="configForm" label-position="top">
+    <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
+      <el-divider content-position="left">
+        <el-text tag="b" size="large">{{ listener.name }}</el-text>
+      </el-divider>
+      <el-form-item>
+        <el-switch
+          v-model="configForm[`task${listener.type}ListenerEnable`]"
+          active-text="开启"
+          inactive-text="关闭"
+        />
+      </el-form-item>
+      <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="请求地址"
+          :prop="`task${listener.type}ListenerPath`"
+          :rules="{
+            required: true,
+            message: '请求地址不能为空',
+            trigger: 'blur'
+          }"
+        >
+          <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>
+      </div>
+    </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { LISTENER_MAP_TYPES, ListenerParamTypeEnum } from '../../consts'
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  },
+  formFieldOptions: {
+    type: Object,
+    required: true
+  }
+})
+const emit = defineEmits(['update:modelValue'])
+const listenerFormRef = ref()
+const configForm = computed({
+  get() {
+    return props.modelValue
+  },
+  set(newValue) {
+    emit('update:modelValue', newValue)
+  }
+})
+const taskListener = ref([
+  {
+    name: '创建任务',
+    type: 'Create'
+  },
+  {
+    name: '指派任务执行人员',
+    type: 'Assign'
+  },
+  {
+    name: '完成任务',
+    type: 'Complete'
+  }
+])
+
+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()
+}
+
+defineExpose({ validate })
+</script>

+ 14 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json

@@ -1438,6 +1438,20 @@
           "isBody": true
         }
       ]
+    },
+    {
+      "name": "SignEnable",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Boolean",
+          "isBody": true
+        }
+      ]
     }
   ],
   "emumerations": []

+ 1 - 1
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="process-panel__container" :style="{ width: `${width}px` }">
+  <div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '600px' }">
     <el-collapse v-model="activeTab" v-if="isReady">
       <el-collapse-item name="base">
         <!-- class="panel-tab__title" -->

+ 16 - 1
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue

@@ -5,6 +5,7 @@
      4. 操作按钮
      5. 字段权限
      6. 审批类型
+     7. 是否需要签名
 -->
 <template>
   <div>
@@ -161,6 +162,11 @@
         </el-radio-group>
       </div>
     </div>
+
+    <el-divider content-position="left">是否需要签名</el-divider>
+    <el-form-item prop="signEnable">
+      <el-switch v-model="signEnable.value" active-text="是" inactive-text="否" />
+    </el-form-item>
   </div>
 </template>
 
@@ -218,6 +224,9 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFie
 // 审批类型
 const approveType = ref({ value: ApproveType.USER })
 
+// 是否需要签名
+const signEnable = ref({ value: false })
+
 const elExtensionElements = ref()
 const otherExtensions = ref()
 const bpmnElement = ref()
@@ -325,6 +334,11 @@ const resetCustomConfigList = () => {
         ex.$type !== `${prefix}:ApproveType`
     ) ?? []
 
+  // 是否需要签名
+  signEnable.value =
+    elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:SignEnable`)?.[0] ||
+    bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false })
+
   // 更新元素扩展属性,避免后续报错
   updateElementExtensions()
 }
@@ -373,7 +387,8 @@ const updateElementExtensions = () => {
       assignEmptyUserIdsEl.value,
       approveType.value,
       ...buttonsSettingEl.value,
-      ...fieldsPermissionEl.value
+      ...fieldsPermissionEl.value,
+      signEnable.value
     ]
   })
   bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {

+ 27 - 0
src/utils/download.ts

@@ -65,6 +65,33 @@ const download = {
       a.download = 'image.png'
       a.click()
     }
+  },
+  base64ToFile: (base64, fileName) => {
+    // 将base64按照 , 进行分割 将前缀  与后续内容分隔开
+    const data = base64.split(',')
+    // 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
+    const type = data[0].match(/:(.*?);/)[1]
+    // 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
+    const suffix = type.split('/')[1]
+    // 使用atob()对base64数据进行解码  结果是一个文件数据流 以字符串的格式输出
+    const bstr = window.atob(data[1])
+    // 获取解码结果字符串的长度
+    let n = bstr.length
+    // 根据解码结果字符串的长度创建一个等长的整形数字数组
+    // 但在创建时 所有元素初始值都为 0
+    const u8arr = new Uint8Array(n)
+    // 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
+    while (n--) {
+      // charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
+      u8arr[n] = bstr.charCodeAt(n)
+    }
+    // 利用构造函数创建File文件对象
+    // new File(bits, name, options)
+    const file = new File([u8arr], `${fileName}.${suffix}`, {
+      type: type
+    })
+    // 将File文件对象返回给方法的调用者
+    return file
   }
 }
 

+ 8 - 8
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -47,15 +47,15 @@
           <el-form-item
             v-if="runningTask.signEnable"
             label="签名"
-            prop="sign"
+            prop="signPicUrl"
             ref="approveSignFormRef"
           >
             <el-button @click="signRef.open()">点击签名</el-button>
             <el-image
               class="w-90px h-40px ml-5px"
-              v-if="approveReasonForm.sign"
-              :src="approveReasonForm.sign"
-              :preview-src-list="[approveReasonForm.sign]"
+              v-if="approveReasonForm.signPicUrl"
+              :src="approveReasonForm.signPicUrl"
+              :preview-src-list="[approveReasonForm.signPicUrl]"
             />
           </el-form-item>
           <el-form-item>
@@ -553,11 +553,11 @@ const signRef = ref()
 const approveSignFormRef = ref()
 const approveReasonForm = reactive({
   reason: '',
-  sign: ''
+  signPicUrl: ''
 })
 const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({
   reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
-  sign: [{ required: true, message: '签名不能为空', trigger: 'change' }]
+  signPicUrl: [{ required: true, message: '签名不能为空', trigger: 'change' }]
 })
 // 拒绝表单
 const rejectFormRef = ref<FormInstance>()
@@ -705,7 +705,7 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
       }
       // 签名
       if (runningTask.value.signEnable) {
-        data.sign = approveReasonForm.sign
+        data.signPicUrl = approveReasonForm.signPicUrl
       }
       // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
       // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
@@ -1002,7 +1002,7 @@ const getUpdatedProcessInstanceVariables = () => {
 
 /** 处理签名完成 */
 const handleSignFinish = (url: string) => {
-  approveReasonForm.sign = url
+  approveReasonForm.signPicUrl = url
   approveSignFormRef.value.validate('change')
 }
 

+ 3 - 3
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -124,14 +124,14 @@
                 审批意见:{{ task.reason }}
               </div>
               <div
-                v-if="task.sign && activity.nodeType === NodeType.USER_TASK_NODE"
+                v-if="task.signPicUrl && activity.nodeType === NodeType.USER_TASK_NODE"
                 class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
               >
                 签名:
                 <el-image
                   class="w-90px h-40px ml-5px"
-                  :src="task.sign"
-                  :preview-src-list="[task.sign]"
+                  :src="task.signPicUrl"
+                  :preview-src-list="[task.signPicUrl]"
                 />
               </div>
             </teleport>

+ 3 - 32
src/views/bpm/processInstance/detail/SignDialog.vue

@@ -2,9 +2,8 @@
   <el-dialog v-model="signDialogVisible" title="签名" width="935">
     <div class="position-relative">
       <Vue3Signature class="b b-solid b-gray" ref="signature" w="900px" h="400px" />
-      <!-- @lesan:建议改成 unocss 哈 -->
       <el-button
-        style="position: absolute; bottom: 20px; right: 10px"
+        class="pos-absolute bottom-20px right-10px"
         type="primary"
         text
         size="small"
@@ -26,6 +25,7 @@
 <script setup lang="ts">
 import Vue3Signature from 'vue3-signature'
 import * as FileApi from '@/api/infra/file'
+import download from '@/utils/download'
 
 const message = useMessage() // 消息弹窗
 const signDialogVisible = ref(false)
@@ -40,40 +40,11 @@ const emits = defineEmits(['success'])
 const submit = async () => {
   message.success('签名上传中请稍等。。。')
   const res = await FileApi.updateFile({
-    file: base64ToFile(signature.value.save('image/png'), '签名')
+    file: download.base64ToFile(signature.value.save('image/png'), '签名')
   })
   emits('success', res.data)
   signDialogVisible.value = false
 }
-
-// TODO @lesan:这个要不抽到 download.js 里,让这个组件更简洁干净?
-const base64ToFile = (base64, fileName) => {
-  // 将base64按照 , 进行分割 将前缀  与后续内容分隔开
-  let data = base64.split(',')
-  // 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
-  let type = data[0].match(/:(.*?);/)[1]
-  // 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
-  let suffix = type.split('/')[1]
-  // 使用atob()对base64数据进行解码  结果是一个文件数据流 以字符串的格式输出
-  const bstr = window.atob(data[1])
-  // 获取解码结果字符串的长度
-  let n = bstr.length
-  // 根据解码结果字符串的长度创建一个等长的整形数字数组
-  // 但在创建时 所有元素初始值都为 0
-  const u8arr = new Uint8Array(n)
-  // 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
-  while (n--) {
-    // charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
-    u8arr[n] = bstr.charCodeAt(n)
-  }
-  // 利用构造函数创建File文件对象
-  // new File(bits, name, options)
-  const file = new File([u8arr], `${fileName}.${suffix}`, {
-    type: type
-  })
-  // 将File文件对象返回给方法的调用者
-  return file
-}
 </script>
 
 <style scoped></style>