提交代码
This commit is contained in:
1 parent
4c23ea16c0
commit
e5d91e7ac1
11 files changed
+534
-35
No files matched your search
+5
-2
@@ -1,6 +1,5 @@
|
||||
package org.dromara.workflow.domain.bo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
@@ -12,7 +11,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* 驳回参数请求
|
||||
*
|
||||
@@ -50,6 +48,11 @@ public class BackProcessBo implements Serializable {
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 手写签名(Base64编码的图片数据)
|
||||
*/
|
||||
private String signature;
|
||||
|
||||
/**
|
||||
* 通知
|
||||
*/
|
||||
|
||||
+6
-1
@@ -25,7 +25,7 @@ public class CompleteTaskBo implements Serializable {
|
||||
/**
|
||||
* 任务id
|
||||
*/
|
||||
@NotNull(message = "任务id不能为空", groups = {AddGroup.class})
|
||||
@NotNull(message = "任务id不能为空", groups = { AddGroup.class })
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
@@ -48,6 +48,11 @@ public class CompleteTaskBo implements Serializable {
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 手写签名(Base64编码的图片数据)
|
||||
*/
|
||||
private String signature;
|
||||
|
||||
/**
|
||||
* 消息通知
|
||||
*/
|
||||
|
||||
+5
@@ -151,6 +151,11 @@ public class FlowHisTaskVo implements Serializable {
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 手写签名(Base64编码的图片数据)
|
||||
*/
|
||||
private String signature;
|
||||
|
||||
/**
|
||||
* 业务详情 存业务类的json
|
||||
*/
|
||||
|
||||
+48
@@ -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
@@ -42,6 +42,7 @@ import org.dromara.workflow.domain.vo.FlowHisTaskVo;
|
||||
import org.dromara.workflow.domain.vo.FlowInstanceVo;
|
||||
import org.dromara.workflow.handler.FlowProcessEventHandler;
|
||||
import org.dromara.workflow.mapper.FlwCategoryMapper;
|
||||
import org.dromara.workflow.mapper.FlwHisTaskExtMapper;
|
||||
import org.dromara.workflow.mapper.FlwInstanceMapper;
|
||||
import org.dromara.workflow.service.IFlwInstanceService;
|
||||
import org.dromara.workflow.service.IFlwTaskService;
|
||||
@@ -71,6 +72,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
private final IFlwTaskService flwTaskService;
|
||||
private final FlwInstanceMapper flwInstanceMapper;
|
||||
private final FlwCategoryMapper flwCategoryMapper;
|
||||
private final FlwHisTaskExtMapper flwHisTaskExtMapper;
|
||||
|
||||
/**
|
||||
* 分页查询正在运行的流程实例
|
||||
@@ -128,15 +130,21 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
*/
|
||||
private QueryWrapper<FlowInstanceBo> buildQueryWrapper(FlowInstanceBo flowInstanceBo) {
|
||||
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())) {
|
||||
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.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.orderByDesc("fi.create_time");
|
||||
return queryWrapper;
|
||||
@@ -149,7 +157,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
*/
|
||||
@Override
|
||||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
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)) {
|
||||
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
|
||||
return false;
|
||||
@@ -204,8 +214,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
}
|
||||
// 获取定义信息
|
||||
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 -> {
|
||||
@@ -242,11 +252,11 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
String userIdStr = LoginHelper.getUserIdStr();
|
||||
BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
log.error("撤销失败: {}", e.getMessage(), e);
|
||||
@@ -281,7 +291,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
|
||||
}
|
||||
Long instanceId = flowInstance.getId();
|
||||
//运行中的任务
|
||||
// 运行中的任务
|
||||
List<FlowHisTaskVo> list = new ArrayList<>();
|
||||
List<FlowTask> flowTaskList = flwTaskService.selectByInstId(instanceId);
|
||||
if (CollUtil.isNotEmpty(flowTaskList)) {
|
||||
@@ -302,16 +312,13 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
}
|
||||
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
|
||||
public Map<String, Object> instanceVariable(Long 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()
|
||||
.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());
|
||||
}
|
||||
|
||||
@@ -416,10 +424,10 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
|
||||
}
|
||||
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);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
+14
@@ -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.FlowTaskMapper;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.mapper.FlwHisTaskExtMapper;
|
||||
import org.dromara.workflow.common.constant.FlowConstant;
|
||||
import org.dromara.workflow.common.enums.TaskAssigneeType;
|
||||
import org.dromara.workflow.common.enums.TaskStatusEnum;
|
||||
@@ -86,6 +87,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
private final IFlwTaskAssigneeService flwTaskAssigneeService;
|
||||
private final IFlwCommonService flwCommonService;
|
||||
private final IFlwNodeExtService flwNodeExtService;
|
||||
private final FlwHisTaskExtMapper flwHisTaskExtMapper;
|
||||
|
||||
/**
|
||||
* 启动任务
|
||||
@@ -185,6 +187,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
.hisTaskExt(completeTaskBo.getFileId());
|
||||
// 执行任务跳转,并根据返回的处理人设置下一步处理人
|
||||
taskService.skip(taskId, flowParams);
|
||||
|
||||
// 保存手写签名到历史任务表
|
||||
if (StringUtils.isNotBlank(completeTaskBo.getSignature())) {
|
||||
flwHisTaskExtMapper.updateSignatureByTaskId(taskId, completeTaskBo.getSignature());
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
@@ -409,6 +417,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
.hisStatus(TaskStatusEnum.BACK.getStatus())
|
||||
.hisTaskExt(bo.getFileId());
|
||||
taskService.skip(task.getId(), flowParams);
|
||||
|
||||
// 保存手写签名到历史任务表(驳回场景)
|
||||
if (StringUtils.isNotBlank(bo.getSignature())) {
|
||||
flwHisTaskExtMapper.updateSignatureByTaskId(taskId, bo.getSignature());
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
|
||||
+66
@@ -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
@@ -59,6 +59,7 @@
|
||||
a.instance_id,
|
||||
a.flow_status flow_task_status,
|
||||
a.message,
|
||||
a.signature,
|
||||
a.ext,
|
||||
a.create_time,
|
||||
a.update_time,
|
||||
|
||||
@@ -33,6 +33,21 @@
|
||||
</template>
|
||||
</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="updateTime" label="结束时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
|
||||
<el-table-column
|
||||
@@ -68,6 +83,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, getCurrentInstance, toRefs, type ComponentInternalInstance } from 'vue';
|
||||
import { flowHisTaskList } from '@/api/workflow/instance';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { listByIds } from '@/api/system/oss';
|
||||
@@ -198,4 +214,48 @@ defineExpose({
|
||||
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>
|
||||
@@ -31,6 +31,9 @@
|
||||
<el-form-item v-if="task.flowStatus === 'waiting'" label="审批意见">
|
||||
<el-input v-model="form.message" type="textarea" resize="none" />
|
||||
</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>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@@ -107,6 +110,9 @@
|
||||
<el-form-item label="审批意见">
|
||||
<el-input v-model="backForm.message" type="textarea" resize="none" />
|
||||
</el-form-item>
|
||||
<el-form-item label="">
|
||||
<SignaturePad v-model="backForm.signature" :width="450" :height="180" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer" style="float: right; padding-bottom: 20px">
|
||||
@@ -147,6 +153,7 @@ import {
|
||||
getNextNodeList
|
||||
} from '@/api/workflow/task';
|
||||
import UserSelect from '@/components/UserSelect';
|
||||
import SignaturePad from '@/components/SignaturePad/index.vue';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
@@ -228,6 +235,7 @@ const deleteSignatureVisible = ref(false);
|
||||
const form = ref<Record<string, any>>({
|
||||
taskId: undefined,
|
||||
message: undefined,
|
||||
signature: undefined,
|
||||
assigneeMap: {},
|
||||
variables: {},
|
||||
messageType: ['1'],
|
||||
@@ -237,6 +245,7 @@ const backForm = ref<Record<string, any>>({
|
||||
taskId: undefined,
|
||||
nodeCode: undefined,
|
||||
message: undefined,
|
||||
signature: undefined,
|
||||
variables: {},
|
||||
messageType: ['1']
|
||||
});
|
||||
@@ -248,6 +257,7 @@ const openDialog = async (id?: string) => {
|
||||
form.value.fileId = undefined;
|
||||
taskId.value = id;
|
||||
form.value.message = undefined;
|
||||
form.value.signature = undefined;
|
||||
dialog.visible = true;
|
||||
loading.value = true;
|
||||
buttonDisabled.value = true;
|
||||
@@ -322,6 +332,7 @@ const handleCompleteTask = async () => {
|
||||
const handleBackProcessOpen = async () => {
|
||||
backForm.value = {};
|
||||
backForm.value.messageType = ['1'];
|
||||
backForm.value.signature = undefined;
|
||||
backVisible.value = true;
|
||||
backLoading.value = true;
|
||||
backButtonDisabled.value = true;
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user