Jelajahi Sumber

提交代码

zlwu 1 tahun lalu
induk
melakukan
e5d91e7ac1

+ 5 - 2
backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/BackProcessBo.java

@@ -1,6 +1,5 @@
 package org.dromara.workflow.domain.bo;
 package org.dromara.workflow.domain.bo;
 
 
-import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import lombok.Data;
 import org.dromara.common.core.validate.AddGroup;
 import org.dromara.common.core.validate.AddGroup;
@@ -12,7 +11,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
 
 
-
 /**
 /**
  * 驳回参数请求
  * 驳回参数请求
  *
  *
@@ -50,6 +48,11 @@ public class BackProcessBo implements Serializable {
      */
      */
     private String message;
     private String message;
 
 
+    /**
+     * 手写签名(Base64编码的图片数据)
+     */
+    private String signature;
+
     /**
     /**
      * 通知
      * 通知
      */
      */

+ 6 - 1
backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/CompleteTaskBo.java

@@ -25,7 +25,7 @@ public class CompleteTaskBo implements Serializable {
     /**
     /**
      * 任务id
      * 任务id
      */
      */
-    @NotNull(message = "任务id不能为空", groups = {AddGroup.class})
+    @NotNull(message = "任务id不能为空", groups = { AddGroup.class })
     private Long taskId;
     private Long taskId;
 
 
     /**
     /**
@@ -48,6 +48,11 @@ public class CompleteTaskBo implements Serializable {
      */
      */
     private String message;
     private String message;
 
 
+    /**
+     * 手写签名(Base64编码的图片数据)
+     */
+    private String signature;
+
     /**
     /**
      * 消息通知
      * 消息通知
      */
      */

+ 5 - 0
backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowHisTaskVo.java

@@ -151,6 +151,11 @@ public class FlowHisTaskVo implements Serializable {
      */
      */
     private String message;
     private String message;
 
 
+    /**
+     * 手写签名(Base64编码的图片数据)
+     */
+    private String signature;
+
     /**
     /**
      * 业务详情 存业务类的json
      * 业务详情 存业务类的json
      */
      */

+ 48 - 0
backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskExtMapper.java

@@ -0,0 +1,48 @@
+package org.dromara.workflow.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
+import org.dromara.workflow.domain.vo.FlowHisTaskVo;
+
+import java.util.List;
+
+/**
+ * 历史任务扩展Mapper
+ * 用于处理Warm-Flow框架不支持的自定义字段
+ *
+ * @author System
+ */
+@Mapper
+public interface FlwHisTaskExtMapper {
+
+    /**
+     * 更新历史任务的签名字段
+     *
+     * @param taskId    任务ID
+     * @param signature 签名数据(Base64)
+     * @return 影响行数
+     */
+    @Update("UPDATE flow_his_task SET signature = #{signature} WHERE task_id = #{taskId}")
+    int updateSignatureByTaskId(@Param("taskId") Long taskId, @Param("signature") String signature);
+
+    /**
+     * 根据实例ID更新签名字段(批量更新场景)
+     *
+     * @param instanceId 流程实例ID
+     * @param signature  签名数据(Base64)
+     * @return 影响行数
+     */
+    @Update("UPDATE flow_his_task SET signature = #{signature} WHERE instance_id = #{instanceId}")
+    int updateSignatureByInstanceId(@Param("instanceId") Long instanceId, @Param("signature") String signature);
+
+    /**
+     * 根据实例ID查询历史任务列表(包含签名字段)
+     *
+     * @param instanceId 流程实例ID
+     * @param nodeType   节点类型
+     * @return 历史任务列表
+     */
+    List<FlowHisTaskVo> selectHisTaskListWithSignature(@Param("instanceId") Long instanceId,
+            @Param("nodeType") Integer nodeType);
+}

+ 40 - 32
backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java

@@ -42,6 +42,7 @@ import org.dromara.workflow.domain.vo.FlowHisTaskVo;
 import org.dromara.workflow.domain.vo.FlowInstanceVo;
 import org.dromara.workflow.domain.vo.FlowInstanceVo;
 import org.dromara.workflow.handler.FlowProcessEventHandler;
 import org.dromara.workflow.handler.FlowProcessEventHandler;
 import org.dromara.workflow.mapper.FlwCategoryMapper;
 import org.dromara.workflow.mapper.FlwCategoryMapper;
+import org.dromara.workflow.mapper.FlwHisTaskExtMapper;
 import org.dromara.workflow.mapper.FlwInstanceMapper;
 import org.dromara.workflow.mapper.FlwInstanceMapper;
 import org.dromara.workflow.service.IFlwInstanceService;
 import org.dromara.workflow.service.IFlwInstanceService;
 import org.dromara.workflow.service.IFlwTaskService;
 import org.dromara.workflow.service.IFlwTaskService;
@@ -71,6 +72,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
     private final IFlwTaskService flwTaskService;
     private final IFlwTaskService flwTaskService;
     private final FlwInstanceMapper flwInstanceMapper;
     private final FlwInstanceMapper flwInstanceMapper;
     private final FlwCategoryMapper flwCategoryMapper;
     private final FlwCategoryMapper flwCategoryMapper;
+    private final FlwHisTaskExtMapper flwHisTaskExtMapper;
 
 
     /**
     /**
      * 分页查询正在运行的流程实例
      * 分页查询正在运行的流程实例
@@ -128,15 +130,21 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
      */
      */
     private QueryWrapper<FlowInstanceBo> buildQueryWrapper(FlowInstanceBo flowInstanceBo) {
     private QueryWrapper<FlowInstanceBo> buildQueryWrapper(FlowInstanceBo flowInstanceBo) {
         QueryWrapper<FlowInstanceBo> queryWrapper = Wrappers.query();
         QueryWrapper<FlowInstanceBo> queryWrapper = Wrappers.query();
-        queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getNodeName()), "fi.node_name", flowInstanceBo.getNodeName());
-        queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowName()), "fd.flow_name", flowInstanceBo.getFlowName());
-        queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowCode()), "fd.flow_code", flowInstanceBo.getFlowCode());
+        queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getNodeName()), "fi.node_name",
+                flowInstanceBo.getNodeName());
+        queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowName()), "fd.flow_name",
+                flowInstanceBo.getFlowName());
+        queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowCode()), "fd.flow_code",
+                flowInstanceBo.getFlowCode());
         if (StringUtils.isNotBlank(flowInstanceBo.getCategory())) {
         if (StringUtils.isNotBlank(flowInstanceBo.getCategory())) {
-            List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowInstanceBo.getCategory()));
+            List<Long> categoryIds = flwCategoryMapper
+                    .selectCategoryIdsByParentId(Convert.toLong(flowInstanceBo.getCategory()));
             queryWrapper.in("fd.category", StreamUtils.toList(categoryIds, Convert::toStr));
             queryWrapper.in("fd.category", StreamUtils.toList(categoryIds, Convert::toStr));
         }
         }
-        queryWrapper.eq(StringUtils.isNotBlank(flowInstanceBo.getBusinessId()), "fi.business_id", flowInstanceBo.getBusinessId());
-        queryWrapper.in(CollUtil.isNotEmpty(flowInstanceBo.getCreateByIds()), "fi.create_by", flowInstanceBo.getCreateByIds());
+        queryWrapper.eq(StringUtils.isNotBlank(flowInstanceBo.getBusinessId()), "fi.business_id",
+                flowInstanceBo.getBusinessId());
+        queryWrapper.in(CollUtil.isNotEmpty(flowInstanceBo.getCreateByIds()), "fi.create_by",
+                flowInstanceBo.getCreateByIds());
         queryWrapper.eq("fi.del_flag", "0");
         queryWrapper.eq("fi.del_flag", "0");
         queryWrapper.orderByDesc("fi.create_time");
         queryWrapper.orderByDesc("fi.create_time");
         return queryWrapper;
         return queryWrapper;
@@ -149,7 +157,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
      */
      */
     @Override
     @Override
     public FlowInstance selectInstByBusinessId(String businessId) {
     public FlowInstance selectInstByBusinessId(String businessId) {
-        return flowInstanceMapper.selectOne(new LambdaQueryWrapper<FlowInstance>().eq(FlowInstance::getBusinessId, businessId));
+        return flowInstanceMapper
+                .selectOne(new LambdaQueryWrapper<FlowInstance>().eq(FlowInstance::getBusinessId, businessId));
     }
     }
 
 
     /**
     /**
@@ -180,7 +189,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
     public boolean deleteByBusinessIds(List<Long> businessIds) {
     public boolean deleteByBusinessIds(List<Long> businessIds) {
-        List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>().in(FlowInstance::getBusinessId, StreamUtils.toList(businessIds, Convert::toStr)));
+        List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>()
+                .in(FlowInstance::getBusinessId, StreamUtils.toList(businessIds, Convert::toStr)));
         if (CollUtil.isEmpty(flowInstances)) {
         if (CollUtil.isEmpty(flowInstances)) {
             log.warn("未找到对应的流程实例信息,无法执行删除操作。");
             log.warn("未找到对应的流程实例信息,无法执行删除操作。");
             return false;
             return false;
@@ -204,8 +214,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
         }
         }
         // 获取定义信息
         // 获取定义信息
         Map<Long, Definition> definitionMap = defService.getByIds(
         Map<Long, Definition> definitionMap = defService.getByIds(
-            StreamUtils.toList(instances, Instance::getDefinitionId)
-        ).stream().collect(Collectors.toMap(Definition::getId, definition -> definition));
+                StreamUtils.toList(instances, Instance::getDefinitionId)).stream()
+                .collect(Collectors.toMap(Definition::getId, definition -> definition));
 
 
         // 逐一触发删除事件
         // 逐一触发删除事件
         instances.forEach(instance -> {
         instances.forEach(instance -> {
@@ -242,11 +252,11 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
             String userIdStr = LoginHelper.getUserIdStr();
             String userIdStr = LoginHelper.getUserIdStr();
             BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
             BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
             FlowParams flowParams = FlowParams.build()
             FlowParams flowParams = FlowParams.build()
-                .message(message)
-                .flowStatus(BusinessStatusEnum.CANCEL.getStatus())
-                .hisStatus(BusinessStatusEnum.CANCEL.getStatus())
-                .handler(userIdStr)
-                .ignore(true);
+                    .message(message)
+                    .flowStatus(BusinessStatusEnum.CANCEL.getStatus())
+                    .hisStatus(BusinessStatusEnum.CANCEL.getStatus())
+                    .handler(userIdStr)
+                    .ignore(true);
             taskService.revoke(instance.getId(), flowParams);
             taskService.revoke(instance.getId(), flowParams);
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("撤销失败: {}", e.getMessage(), e);
             log.error("撤销失败: {}", e.getMessage(), e);
@@ -281,7 +291,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
             throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
             throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
         }
         }
         Long instanceId = flowInstance.getId();
         Long instanceId = flowInstance.getId();
-        //运行中的任务
+        // 运行中的任务
         List<FlowHisTaskVo> list = new ArrayList<>();
         List<FlowHisTaskVo> list = new ArrayList<>();
         List<FlowTask> flowTaskList = flwTaskService.selectByInstId(instanceId);
         List<FlowTask> flowTaskList = flwTaskService.selectByInstId(instanceId);
         if (CollUtil.isNotEmpty(flowTaskList)) {
         if (CollUtil.isNotEmpty(flowTaskList)) {
@@ -302,16 +312,13 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
             }
             }
             list.addAll(flowHisTaskVos);
             list.addAll(flowHisTaskVos);
         }
         }
-        //历史任务
-        LambdaQueryWrapper<FlowHisTask> wrapper = Wrappers.lambdaQuery();
-        wrapper.eq(FlowHisTask::getInstanceId, instanceId);
-        wrapper.eq(FlowHisTask::getNodeType, NodeType.BETWEEN.getKey());
-        wrapper.orderByDesc(FlowHisTask::getCreateTime).orderByDesc(FlowHisTask::getUpdateTime);
-        List<FlowHisTask> flowHisTasks = flowHisTaskMapper.selectList(wrapper);
-        if (CollUtil.isNotEmpty(flowHisTasks)) {
-            list.addAll(BeanUtil.copyToList(flowHisTasks, FlowHisTaskVo.class));
+        // 历史任务(使用自定义Mapper查询,包含签名字段)
+        List<FlowHisTaskVo> flowHisTaskVos = flwHisTaskExtMapper.selectHisTaskListWithSignature(instanceId,
+                NodeType.BETWEEN.getKey());
+        if (CollUtil.isNotEmpty(flowHisTaskVos)) {
+            list.addAll(flowHisTaskVos);
         }
         }
-        return Map.of("list", list,"instanceId",instanceId);
+        return Map.of("list", list, "instanceId", instanceId);
     }
     }
 
 
     /**
     /**
@@ -336,10 +343,11 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
     @Override
     @Override
     public Map<String, Object> instanceVariable(Long instanceId) {
     public Map<String, Object> instanceVariable(Long instanceId) {
         FlowInstance flowInstance = flowInstanceMapper.selectById(instanceId);
         FlowInstance flowInstance = flowInstanceMapper.selectById(instanceId);
-        Map<String, Object> variableMap = Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap());
+        Map<String, Object> variableMap = Optional.ofNullable(flowInstance.getVariableMap())
+                .orElse(Collections.emptyMap());
         List<Map<String, Object>> variableList = variableMap.entrySet().stream()
         List<Map<String, Object>> variableList = variableMap.entrySet().stream()
-            .map(entry -> Map.of("key", entry.getKey(), "value", entry.getValue()))
-            .toList();
+                .map(entry -> Map.of("key", entry.getKey(), "value", entry.getValue()))
+                .toList();
         return Map.of("variableList", variableList, "variable", flowInstance.getVariable());
         return Map.of("variableList", variableList, "variable", flowInstance.getVariable());
     }
     }
 
 
@@ -416,10 +424,10 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
                 BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
                 BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
             }
             }
             FlowParams flowParams = FlowParams.build()
             FlowParams flowParams = FlowParams.build()
-                .message(bo.getComment())
-                .flowStatus(BusinessStatusEnum.INVALID.getStatus())
-                .hisStatus(TaskStatusEnum.INVALID.getStatus())
-                .ignore(true);
+                    .message(bo.getComment())
+                    .flowStatus(BusinessStatusEnum.INVALID.getStatus())
+                    .hisStatus(TaskStatusEnum.INVALID.getStatus())
+                    .ignore(true);
             taskService.terminationByInsId(bo.getId(), flowParams);
             taskService.terminationByInsId(bo.getId(), flowParams);
             return true;
             return true;
         } catch (Exception e) {
         } catch (Exception e) {

+ 14 - 0
backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java

@@ -38,6 +38,7 @@ import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
 import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
 import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
 import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
 import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
 import org.dromara.workflow.common.ConditionalOnEnable;
 import org.dromara.workflow.common.ConditionalOnEnable;
+import org.dromara.workflow.mapper.FlwHisTaskExtMapper;
 import org.dromara.workflow.common.constant.FlowConstant;
 import org.dromara.workflow.common.constant.FlowConstant;
 import org.dromara.workflow.common.enums.TaskAssigneeType;
 import org.dromara.workflow.common.enums.TaskAssigneeType;
 import org.dromara.workflow.common.enums.TaskStatusEnum;
 import org.dromara.workflow.common.enums.TaskStatusEnum;
@@ -86,6 +87,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
     private final IFlwTaskAssigneeService flwTaskAssigneeService;
     private final IFlwTaskAssigneeService flwTaskAssigneeService;
     private final IFlwCommonService flwCommonService;
     private final IFlwCommonService flwCommonService;
     private final IFlwNodeExtService flwNodeExtService;
     private final IFlwNodeExtService flwNodeExtService;
+    private final FlwHisTaskExtMapper flwHisTaskExtMapper;
 
 
     /**
     /**
      * 启动任务
      * 启动任务
@@ -185,6 +187,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
                     .hisTaskExt(completeTaskBo.getFileId());
                     .hisTaskExt(completeTaskBo.getFileId());
             // 执行任务跳转,并根据返回的处理人设置下一步处理人
             // 执行任务跳转,并根据返回的处理人设置下一步处理人
             taskService.skip(taskId, flowParams);
             taskService.skip(taskId, flowParams);
+
+            // 保存手写签名到历史任务表
+            if (StringUtils.isNotBlank(completeTaskBo.getSignature())) {
+                flwHisTaskExtMapper.updateSignatureByTaskId(taskId, completeTaskBo.getSignature());
+            }
+
             return true;
             return true;
         } catch (Exception e) {
         } catch (Exception e) {
             log.error(e.getMessage(), e);
             log.error(e.getMessage(), e);
@@ -409,6 +417,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
                     .hisStatus(TaskStatusEnum.BACK.getStatus())
                     .hisStatus(TaskStatusEnum.BACK.getStatus())
                     .hisTaskExt(bo.getFileId());
                     .hisTaskExt(bo.getFileId());
             taskService.skip(task.getId(), flowParams);
             taskService.skip(task.getId(), flowParams);
+
+            // 保存手写签名到历史任务表(驳回场景)
+            if (StringUtils.isNotBlank(bo.getSignature())) {
+                flwHisTaskExtMapper.updateSignatureByTaskId(taskId, bo.getSignature());
+            }
+
             return true;
             return true;
         } catch (Exception e) {
         } catch (Exception e) {
             log.error(e.getMessage(), e);
             log.error(e.getMessage(), e);

+ 66 - 0
backend/ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/FlwHisTaskExtMapper.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.workflow.mapper.FlwHisTaskExtMapper">
+
+    <!-- FlowHisTaskVo结果映射 -->
+    <resultMap id="FlowHisTaskVoResult" type="org.dromara.workflow.domain.vo.FlowHisTaskVo">
+        <id property="id" column="id"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="tenantId" column="tenant_id"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="definitionId" column="definition_id"/>
+        <result property="instanceId" column="instance_id"/>
+        <result property="taskId" column="task_id"/>
+        <result property="cooperateType" column="cooperate_type"/>
+        <result property="nodeCode" column="node_code"/>
+        <result property="nodeName" column="node_name"/>
+        <result property="nodeType" column="node_type"/>
+        <result property="targetNodeCode" column="target_node_code"/>
+        <result property="targetNodeName" column="target_node_name"/>
+        <result property="approver" column="approver"/>
+        <result property="collaborator" column="collaborator"/>
+        <result property="skipType" column="skip_type"/>
+        <result property="flowStatus" column="flow_status"/>
+        <result property="message" column="message"/>
+        <result property="signature" column="signature"/>
+        <result property="ext" column="ext"/>
+        <result property="formCustom" column="form_custom"/>
+        <result property="formPath" column="form_path"/>
+    </resultMap>
+
+    <!-- 根据实例ID查询历史任务列表(包含签名字段) -->
+    <select id="selectHisTaskListWithSignature" resultMap="FlowHisTaskVoResult">
+        SELECT
+            id,
+            create_time,
+            update_time,
+            tenant_id,
+            del_flag,
+            definition_id,
+            instance_id,
+            task_id,
+            cooperate_type,
+            node_code,
+            node_name,
+            node_type,
+            target_node_code,
+            target_node_name,
+            approver,
+            collaborator,
+            skip_type,
+            flow_status,
+            message,
+            signature,
+            ext,
+            form_custom,
+            form_path
+        FROM flow_his_task
+        WHERE instance_id = #{instanceId}
+          AND node_type = #{nodeType}
+          AND del_flag = '0'
+        ORDER BY create_time DESC, update_time DESC
+    </select>
+
+</mapper>
+

+ 1 - 0
backend/ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/FlwTaskMapper.xml

@@ -59,6 +59,7 @@
                 a.instance_id,
                 a.instance_id,
                 a.flow_status flow_task_status,
                 a.flow_status flow_task_status,
                 a.message,
                 a.message,
+                a.signature,
                 a.ext,
                 a.ext,
                 a.create_time,
                 a.create_time,
                 a.update_time,
                 a.update_time,

+ 60 - 0
web/src/components/Process/approvalRecord.vue

@@ -33,6 +33,21 @@
                 </template>
                 </template>
               </el-table-column>
               </el-table-column>
               <el-table-column prop="message" label="审批意见" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
               <el-table-column prop="message" label="审批意见" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
+              <el-table-column prop="signature" label="手写签名" width="150" align="center">
+                <template #default="scope">
+                  <el-popover v-if="scope.row.signature" placement="left" :width="350" trigger="hover">
+                    <template #reference>
+                      <div class="signature-thumbnail">
+                        <img :src="scope.row.signature" alt="签名" class="signature-thumb-image" />
+                      </div>
+                    </template>
+                    <div class="signature-preview-large">
+                      <img :src="scope.row.signature" alt="签名" class="signature-large-image" />
+                    </div>
+                  </el-popover>
+                  <span v-else style="color: #909399; font-size: 12px">未签名</span>
+                </template>
+              </el-table-column>
               <el-table-column prop="createTime" label="开始时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
               <el-table-column prop="createTime" label="开始时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
               <el-table-column prop="updateTime" label="结束时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
               <el-table-column prop="updateTime" label="结束时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
               <el-table-column
               <el-table-column
@@ -68,6 +83,7 @@
   </div>
   </div>
 </template>
 </template>
 <script setup lang="ts">
 <script setup lang="ts">
+import { ref, getCurrentInstance, toRefs, type ComponentInternalInstance } from 'vue';
 import { flowHisTaskList } from '@/api/workflow/instance';
 import { flowHisTaskList } from '@/api/workflow/instance';
 import { propTypes } from '@/utils/propTypes';
 import { propTypes } from '@/utils/propTypes';
 import { listByIds } from '@/api/system/oss';
 import { listByIds } from '@/api/system/oss';
@@ -198,4 +214,48 @@ defineExpose({
     min-height: calc(100vh - 170px) !important;
     min-height: calc(100vh - 170px) !important;
   }
   }
 }
 }
+
+// 列表中的签名缩略图容器
+.signature-thumbnail {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 4px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: scale(1.05);
+  }
+
+  .signature-thumb-image {
+    max-width: 120px;
+    max-height: 50px;
+    border: 1px solid #e4e7ed;
+    border-radius: 4px;
+    background: #ffffff;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+    object-fit: contain;
+  }
+}
+
+// 鼠标悬停时的大图预览
+.signature-preview-large {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 16px;
+  background: #f5f7fa;
+  border-radius: 6px;
+
+  .signature-large-image {
+    max-width: 100%;
+    max-height: 200px;
+    border: 2px solid #dcdfe6;
+    border-radius: 4px;
+    background: #ffffff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    object-fit: contain;
+  }
+}
 </style>
 </style>

+ 11 - 0
web/src/components/Process/submitVerify.vue

@@ -31,6 +31,9 @@
       <el-form-item v-if="task.flowStatus === 'waiting'" label="审批意见">
       <el-form-item v-if="task.flowStatus === 'waiting'" label="审批意见">
         <el-input v-model="form.message" type="textarea" resize="none" />
         <el-input v-model="form.message" type="textarea" resize="none" />
       </el-form-item>
       </el-form-item>
+      <el-form-item v-if="task.flowStatus === 'waiting'" label="">
+        <SignaturePad v-model="form.signature" :width="480" :height="180" />
+      </el-form-item>
     </el-form>
     </el-form>
     <template #footer>
     <template #footer>
       <span class="dialog-footer">
       <span class="dialog-footer">
@@ -107,6 +110,9 @@
         <el-form-item label="审批意见">
         <el-form-item label="审批意见">
           <el-input v-model="backForm.message" type="textarea" resize="none" />
           <el-input v-model="backForm.message" type="textarea" resize="none" />
         </el-form-item>
         </el-form-item>
+        <el-form-item label="">
+          <SignaturePad v-model="backForm.signature" :width="450" :height="180" />
+        </el-form-item>
       </el-form>
       </el-form>
       <template #footer>
       <template #footer>
         <div class="dialog-footer" style="float: right; padding-bottom: 20px">
         <div class="dialog-footer" style="float: right; padding-bottom: 20px">
@@ -147,6 +153,7 @@ import {
   getNextNodeList
   getNextNodeList
 } from '@/api/workflow/task';
 } from '@/api/workflow/task';
 import UserSelect from '@/components/UserSelect';
 import UserSelect from '@/components/UserSelect';
+import SignaturePad from '@/components/SignaturePad/index.vue';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 import { UserVO } from '@/api/system/user/types';
 import { UserVO } from '@/api/system/user/types';
@@ -228,6 +235,7 @@ const deleteSignatureVisible = ref(false);
 const form = ref<Record<string, any>>({
 const form = ref<Record<string, any>>({
   taskId: undefined,
   taskId: undefined,
   message: undefined,
   message: undefined,
+  signature: undefined,
   assigneeMap: {},
   assigneeMap: {},
   variables: {},
   variables: {},
   messageType: ['1'],
   messageType: ['1'],
@@ -237,6 +245,7 @@ const backForm = ref<Record<string, any>>({
   taskId: undefined,
   taskId: undefined,
   nodeCode: undefined,
   nodeCode: undefined,
   message: undefined,
   message: undefined,
+  signature: undefined,
   variables: {},
   variables: {},
   messageType: ['1']
   messageType: ['1']
 });
 });
@@ -248,6 +257,7 @@ const openDialog = async (id?: string) => {
   form.value.fileId = undefined;
   form.value.fileId = undefined;
   taskId.value = id;
   taskId.value = id;
   form.value.message = undefined;
   form.value.message = undefined;
+  form.value.signature = undefined;
   dialog.visible = true;
   dialog.visible = true;
   loading.value = true;
   loading.value = true;
   buttonDisabled.value = true;
   buttonDisabled.value = true;
@@ -322,6 +332,7 @@ const handleCompleteTask = async () => {
 const handleBackProcessOpen = async () => {
 const handleBackProcessOpen = async () => {
   backForm.value = {};
   backForm.value = {};
   backForm.value.messageType = ['1'];
   backForm.value.messageType = ['1'];
+  backForm.value.signature = undefined;
   backVisible.value = true;
   backVisible.value = true;
   backLoading.value = true;
   backLoading.value = true;
   backButtonDisabled.value = true;
   backButtonDisabled.value = true;

+ 278 - 0
web/src/components/SignaturePad/index.vue

@@ -0,0 +1,278 @@
+<template>
+  <div class="signature-pad-wrapper">
+    <div class="signature-header">
+      <span class="signature-title">手写签名</span>
+      <el-button v-if="hasSignature" size="small" type="danger" :icon="Delete" @click="handleClear">清空</el-button>
+    </div>
+    <div class="signature-canvas-container">
+      <canvas
+        ref="canvasRef"
+        class="signature-canvas"
+        @mousedown="handleMouseDown"
+        @mousemove="handleMouseMove"
+        @mouseup="handleMouseUp"
+        @mouseleave="handleMouseUp"
+        @touchstart="handleTouchStart"
+        @touchmove="handleTouchMove"
+        @touchend="handleTouchEnd"
+      ></canvas>
+      <div v-if="!hasSignature" class="signature-placeholder">请在此处签名</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
+import { Delete } from '@element-plus/icons-vue';
+
+interface Props {
+  modelValue?: string; // Base64签名数据
+  width?: number;
+  height?: number;
+  lineWidth?: number;
+  lineColor?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: '',
+  width: 500,
+  height: 200,
+  lineWidth: 2,
+  lineColor: '#000000'
+});
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string): void;
+}>();
+
+const canvasRef = ref<HTMLCanvasElement>();
+const hasSignature = ref(false);
+const isDrawing = ref(false);
+let ctx: CanvasRenderingContext2D | null = null;
+let resizeObserver: ResizeObserver | null = null;
+
+onMounted(() => {
+  initCanvas();
+  // 如果有初始值,加载到画布上
+  if (props.modelValue) {
+    loadSignature(props.modelValue);
+  }
+
+  // 监听容器大小变化
+  if (canvasRef.value?.parentElement) {
+    resizeObserver = new ResizeObserver(() => {
+      handleResize();
+    });
+    resizeObserver.observe(canvasRef.value.parentElement);
+  }
+});
+
+onUnmounted(() => {
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+  }
+});
+
+// 监听外部值变化
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal && !hasSignature.value) {
+      loadSignature(newVal);
+    }
+  }
+);
+
+const initCanvas = () => {
+  if (!canvasRef.value) return;
+
+  const canvas = canvasRef.value;
+  const container = canvas.parentElement;
+
+  // 获取容器的实际宽度
+  const containerWidth = container?.clientWidth || props.width;
+  
+  // 设置画布的物理尺寸(实际像素)
+  canvas.width = containerWidth;
+  canvas.height = props.height;
+
+  // 同时设置CSS样式,确保显示尺寸与物理尺寸一致
+  canvas.style.width = `${containerWidth}px`;
+  canvas.style.height = `${props.height}px`;
+
+  ctx = canvas.getContext('2d');
+  if (ctx) {
+    ctx.strokeStyle = props.lineColor;
+    ctx.lineWidth = props.lineWidth;
+    ctx.lineCap = 'round';
+    ctx.lineJoin = 'round';
+  }
+};
+
+// 处理容器大小变化
+const handleResize = () => {
+  if (!canvasRef.value) return;
+
+  // 保存当前签名
+  const currentSignature = hasSignature.value ? canvasRef.value.toDataURL('image/png') : '';
+
+  // 重新初始化画布
+  initCanvas();
+
+  // 恢复签名
+  if (currentSignature && hasSignature.value) {
+    nextTick(() => {
+      loadSignature(currentSignature);
+    });
+  }
+};
+
+const startDrawing = (x: number, y: number) => {
+  if (!ctx || !canvasRef.value) return;
+
+  isDrawing.value = true;
+  const rect = canvasRef.value.getBoundingClientRect();
+  ctx.beginPath();
+  ctx.moveTo(x - rect.left, y - rect.top);
+};
+
+const draw = (x: number, y: number) => {
+  if (!isDrawing.value || !ctx || !canvasRef.value) return;
+
+  const rect = canvasRef.value.getBoundingClientRect();
+  ctx.lineTo(x - rect.left, y - rect.top);
+  ctx.stroke();
+  hasSignature.value = true;
+};
+
+const stopDrawing = () => {
+  if (!isDrawing.value) return;
+
+  isDrawing.value = false;
+  if (ctx) {
+    ctx.closePath();
+  }
+
+  // 导出签名数据
+  if (hasSignature.value && canvasRef.value) {
+    const signatureData = canvasRef.value.toDataURL('image/png');
+    emit('update:modelValue', signatureData);
+  }
+};
+
+// 鼠标事件处理
+const handleMouseDown = (e: MouseEvent) => {
+  startDrawing(e.clientX, e.clientY);
+};
+
+const handleMouseMove = (e: MouseEvent) => {
+  draw(e.clientX, e.clientY);
+};
+
+const handleMouseUp = () => {
+  stopDrawing();
+};
+
+// 触摸事件处理(移动端支持)
+const handleTouchStart = (e: TouchEvent) => {
+  e.preventDefault();
+  const touch = e.touches[0];
+  startDrawing(touch.clientX, touch.clientY);
+};
+
+const handleTouchMove = (e: TouchEvent) => {
+  e.preventDefault();
+  const touch = e.touches[0];
+  draw(touch.clientX, touch.clientY);
+};
+
+const handleTouchEnd = (e: TouchEvent) => {
+  e.preventDefault();
+  stopDrawing();
+};
+
+// 清空签名
+const handleClear = () => {
+  if (!canvasRef.value || !ctx) return;
+
+  ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
+  hasSignature.value = false;
+  emit('update:modelValue', '');
+};
+
+// 加载签名
+const loadSignature = (base64Data: string) => {
+  if (!canvasRef.value || !ctx) return;
+
+  const img = new Image();
+  img.onload = () => {
+    ctx!.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
+    ctx!.drawImage(img, 0, 0);
+    hasSignature.value = true;
+  };
+  img.src = base64Data;
+};
+
+// 获取签名数据
+const getSignature = (): string => {
+  if (!canvasRef.value || !hasSignature.value) return '';
+  return canvasRef.value.toDataURL('image/png');
+};
+
+// 对外暴露方法
+defineExpose({
+  clear: handleClear,
+  getSignature,
+  hasSignature: () => hasSignature.value
+});
+</script>
+
+<style scoped lang="scss">
+.signature-pad-wrapper {
+  width: 100%;
+
+  .signature-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+
+    .signature-title {
+      font-size: 14px;
+      color: #606266;
+      font-weight: 500;
+    }
+  }
+
+  .signature-canvas-container {
+    position: relative;
+    width: 100%;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    background-color: #ffffff;
+    overflow: hidden;
+
+    &:hover {
+      border-color: #c0c4cc;
+    }
+
+    .signature-canvas {
+      display: block;
+      width: 100%;
+      cursor: crosshair;
+      touch-action: none;
+    }
+
+    .signature-placeholder {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      color: #c0c4cc;
+      font-size: 14px;
+      pointer-events: none;
+      user-select: none;
+    }
+  }
+}
+</style>