“出国申请审批流程已实现10.29”
This commit is contained in:
1 parent
32b9822146
commit
4277e2fca8
15 files changed
+1040
-321
No files matched your search
+10
-1
@@ -330,7 +330,7 @@ public class RsOverseasApplicationController extends BaseController {
|
||||
return toAjax(applicationService.approveApplication(
|
||||
request.getApplicationId(), approverId, approverName,
|
||||
request.getApprovalStatus(), request.getApprovalOpinion(),
|
||||
request.getTaskId()));
|
||||
request.getApprovalSignature(), request.getTaskId()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,6 +376,7 @@ public class RsOverseasApplicationController extends BaseController {
|
||||
private Long applicationId;
|
||||
private String approvalStatus;
|
||||
private String approvalOpinion;
|
||||
private String approvalSignature; // 审批签字(base64图片)
|
||||
private String taskId; // 🔴 新增:工作流任务ID,支持标准工作流模式
|
||||
|
||||
public Long getApplicationId() {
|
||||
@@ -402,6 +403,14 @@ public class RsOverseasApplicationController extends BaseController {
|
||||
this.approvalOpinion = approvalOpinion;
|
||||
}
|
||||
|
||||
public String getApprovalSignature() {
|
||||
return approvalSignature;
|
||||
}
|
||||
|
||||
public void setApprovalSignature(String approvalSignature) {
|
||||
this.approvalSignature = approvalSignature;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
+34
-36
@@ -6,20 +6,14 @@ import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.rs.domain.vo.RsOverseasApprovalMonitorVo;
|
||||
import org.dromara.rs.domain.vo.RsOverseasApprovalVo;
|
||||
import org.dromara.rs.domain.query.RsOverseasApprovalMonitorQuery;
|
||||
import org.dromara.rs.service.IRsOverseasApprovalMonitorService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 出国申请审批监控Controller
|
||||
* <p>
|
||||
@@ -40,53 +34,40 @@ public class RsOverseasApprovalMonitorController extends BaseController {
|
||||
private final IRsOverseasApprovalMonitorService monitorService;
|
||||
|
||||
/**
|
||||
* 查询审批中的申请列表(实时监控)
|
||||
* 查询待审批的申请列表(出国申请进度查询)
|
||||
* <p>
|
||||
* 显示所有状态为"待审核"的申请,包含:
|
||||
* - 申请人信息
|
||||
* - 出国目的地和时间
|
||||
* - 当前审批环节(部门主管/分管领导/校领导)
|
||||
* - 当前审批人姓名
|
||||
* - 审批开始时间
|
||||
* 🔴 功能说明:查询所有待审批申请(不限制申请人)
|
||||
* - 拥有权限的用户可以查看所有待审批申请
|
||||
* - 显示当前审批环节和审批人
|
||||
* - 可以查看审批历史记录
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 审批中的申请列表
|
||||
* @return 所有待审批申请列表
|
||||
*/
|
||||
// 🔴 标准化改造:删除自定义待审批查询接口
|
||||
// 原因:出国申请工作流应使用标准工作流模式
|
||||
// 用户应通过工作流API pageByTaskWait() 获取待办任务
|
||||
// 而不是调用自定义的业务API
|
||||
//
|
||||
// 标准工作流模式:
|
||||
// 1. 用户访问标准待办页面:/workflow/task/taskWaiting.vue
|
||||
// 2. 调用工作流API:pageByTaskWait() 获取待办任务
|
||||
// 3. 点击"办理"跳转:/rs/overseas/application/edit?taskId=xxx
|
||||
// 4. 在业务表单页面进行审批操作
|
||||
|
||||
@SaCheckPermission("rs:overseas:list")
|
||||
@GetMapping("/pending/list")
|
||||
public R<String> pendingList(RsOverseasApprovalMonitorQuery query) {
|
||||
// 提示用户使用标准工作流API
|
||||
return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务");
|
||||
public TableDataInfo<RsOverseasApprovalMonitorVo> pendingList(RsOverseasApprovalMonitorQuery query) {
|
||||
log.info("查询所有待审批申请列表");
|
||||
// 只查询待审核状态(status = 0)
|
||||
query.setStatus("0");
|
||||
return monitorService.queryPendingList(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询已完成审批的申请列表
|
||||
* <p>
|
||||
* 显示所有审批流程完成的申请(已通过),包含:
|
||||
* - 申请人信息
|
||||
* - 出国目的地和时间
|
||||
* - 最终审批结果
|
||||
* - 完成时间
|
||||
* - 所有审批环节的历史记录
|
||||
* 🔴 功能说明:查询所有已完成审批申请(不限制申请人)
|
||||
* - 拥有权限的用户可以查看所有已完成审批申请
|
||||
* - 显示最终审批结果和完成时间
|
||||
* - 可以查看所有审批环节的历史记录
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 已完成审批的申请列表
|
||||
* @return 所有已完成审批的申请列表
|
||||
*/
|
||||
@SaCheckPermission("rs:overseas:list")
|
||||
@GetMapping("/completed/list")
|
||||
public TableDataInfo<RsOverseasApprovalMonitorVo> completedList(RsOverseasApprovalMonitorQuery query) {
|
||||
log.info("查询已完成审批的申请列表,查询条件:{}", query);
|
||||
log.info("查询所有已完成审批的申请列表,查询条件:{}", query);
|
||||
// 只查询已通过状态(status = 1)
|
||||
query.setStatus("1");
|
||||
return monitorService.queryCompletedList(query);
|
||||
@@ -128,4 +109,21 @@ public class RsOverseasApprovalMonitorController extends BaseController {
|
||||
log.info("查询审批进度统计");
|
||||
return R.ok(monitorService.getApprovalStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载出国申请审批表PDF
|
||||
* <p>
|
||||
* 生成并下载"杭州第一技师学院登记报备人员因私事出国(境)审批表"PDF文件
|
||||
*
|
||||
* @param applicationId 申请ID
|
||||
* @param response HTTP响应对象
|
||||
*/
|
||||
@SaCheckPermission("rs:overseas:query")
|
||||
@GetMapping("/downloadApprovalPdf/{applicationId}")
|
||||
public void downloadApprovalPdf(
|
||||
@PathVariable @NotNull(message = "申请ID不能为空") Long applicationId,
|
||||
HttpServletResponse response) {
|
||||
log.info("下载出国申请审批表PDF,申请ID:{}", applicationId);
|
||||
monitorService.downloadApprovalPdf(applicationId, response);
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -54,7 +54,7 @@ public class RsOverseasApproval extends BaseEntity {
|
||||
private String approverName;
|
||||
|
||||
/**
|
||||
* 审批级别:1-部门主管,2-分管领导,3-校领导
|
||||
* 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
|
||||
*/
|
||||
@ExcelProperty(value = "审批级别")
|
||||
private String approvalLevel;
|
||||
@@ -71,6 +71,12 @@ public class RsOverseasApproval extends BaseEntity {
|
||||
@ExcelProperty(value = "审批意见")
|
||||
private String approvalOpinion;
|
||||
|
||||
/**
|
||||
* 审批签字(base64图片数据)
|
||||
*/
|
||||
@ExcelIgnore
|
||||
private String approvalSignature;
|
||||
|
||||
/**
|
||||
* 审批时间
|
||||
*/
|
||||
@@ -118,5 +124,4 @@ public class RsOverseasApproval extends BaseEntity {
|
||||
@ExcelIgnore
|
||||
private Date updateTime;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -72,4 +72,9 @@ public class RsOverseasApprovalBo extends BaseEntity {
|
||||
* 审批意见
|
||||
*/
|
||||
private String approvalOpinion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批签字(base64图片数据)
|
||||
*/
|
||||
private String approvalSignature;
|
||||
}
|
||||
+15
-10
@@ -67,6 +67,11 @@ public class RsOverseasApprovalVo implements Serializable {
|
||||
*/
|
||||
private String approvalOpinion;
|
||||
|
||||
/**
|
||||
* 审批签字(base64图片数据)
|
||||
*/
|
||||
private String approvalSignature;
|
||||
|
||||
/**
|
||||
* 审批时间
|
||||
*/
|
||||
@@ -74,49 +79,49 @@ public class RsOverseasApprovalVo implements Serializable {
|
||||
private Date approvalTime;
|
||||
|
||||
// ==================== 申请信息相关字段 ====================
|
||||
|
||||
|
||||
/**
|
||||
* 申请人ID
|
||||
*/
|
||||
private Long applicantId;
|
||||
|
||||
|
||||
/**
|
||||
* 申请人姓名
|
||||
*/
|
||||
private String applicantName;
|
||||
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
private String department;
|
||||
|
||||
|
||||
/**
|
||||
* 出国目的
|
||||
*/
|
||||
private String purpose;
|
||||
|
||||
|
||||
/**
|
||||
* 出国目的文本
|
||||
*/
|
||||
private String purposeText;
|
||||
|
||||
|
||||
/**
|
||||
* 目的地
|
||||
*/
|
||||
private String destination;
|
||||
|
||||
|
||||
/**
|
||||
* 出国开始日期
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date startDate;
|
||||
|
||||
|
||||
/**
|
||||
* 出国结束日期
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date endDate;
|
||||
|
||||
|
||||
/**
|
||||
* 出国天数
|
||||
*/
|
||||
@@ -158,4 +163,4 @@ public class RsOverseasApprovalVo implements Serializable {
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
}
|
||||
+9
-8
@@ -132,21 +132,22 @@ public interface IRsOverseasApplicationService {
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean approveApplication(Long applicationId, Long approverId, String approverName,
|
||||
String approvalStatus, String approvalOpinion);
|
||||
String approvalStatus, String approvalOpinion, String approvalSignature);
|
||||
|
||||
/**
|
||||
* 审批出国申请(支持标准工作流模式)
|
||||
*
|
||||
* @param applicationId 申请ID
|
||||
* @param approverId 审批人ID
|
||||
* @param approverName 审批人姓名
|
||||
* @param approvalStatus 审批结果:1-通过,2-拒绝
|
||||
* @param approvalOpinion 审批意见
|
||||
* @param taskId 工作流任务ID(可选,用于标准工作流模式)
|
||||
* @param applicationId 申请ID
|
||||
* @param approverId 审批人ID
|
||||
* @param approverName 审批人姓名
|
||||
* @param approvalStatus 审批结果:1-通过,2-拒绝
|
||||
* @param approvalOpinion 审批意见
|
||||
* @param approvalSignature 审批签字(base64图片)
|
||||
* @param taskId 工作流任务ID(可选,用于标准工作流模式)
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean approveApplication(Long applicationId, Long approverId, String approverName,
|
||||
String approvalStatus, String approvalOpinion, String taskId);
|
||||
String approvalStatus, String approvalOpinion, String approvalSignature, String taskId);
|
||||
|
||||
/**
|
||||
* 校验删除数据
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
package org.dromara.rs.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.rs.domain.vo.RsOverseasApprovalMonitorVo;
|
||||
import org.dromara.rs.domain.query.RsOverseasApprovalMonitorQuery;
|
||||
@@ -44,4 +45,12 @@ public interface IRsOverseasApprovalMonitorService {
|
||||
* @return 统计数据
|
||||
*/
|
||||
Map<String, Object> getApprovalStatistics();
|
||||
|
||||
/**
|
||||
* 下载出国申请审批表PDF
|
||||
*
|
||||
* @param applicationId 申请ID
|
||||
* @param response HTTP响应对象
|
||||
*/
|
||||
void downloadApprovalPdf(Long applicationId, HttpServletResponse response);
|
||||
}
|
||||
+13
-4
@@ -558,7 +558,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
|
||||
@Override
|
||||
@Transactional
|
||||
public Boolean approveApplication(Long applicationId, Long approverId, String approverName,
|
||||
String approvalStatus, String approvalOpinion) {
|
||||
String approvalStatus, String approvalOpinion, String approvalSignature) {
|
||||
RsOverseasApplication application = baseMapper.selectById(applicationId);
|
||||
if (application == null) {
|
||||
throw new RuntimeException("申请不存在");
|
||||
@@ -584,8 +584,16 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
|
||||
approval.setApprovalLevel(application.getApprovalLevel());
|
||||
approval.setApprovalStatus(approvalStatus);
|
||||
approval.setApprovalOpinion(approvalOpinion);
|
||||
approval.setApprovalSignature(approvalSignature); // 🔴 保存审批签字
|
||||
approval.setApprovalTime(new Date());
|
||||
|
||||
// 日志记录签字数据
|
||||
if (approvalSignature != null && !approvalSignature.trim().isEmpty()) {
|
||||
log.info("✅ 保存审批签字数据,长度: {} 字符", approvalSignature.length());
|
||||
} else {
|
||||
log.info("ℹ️ 未提供审批签字");
|
||||
}
|
||||
|
||||
// 手动设置多租户和删除标志字段
|
||||
if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) {
|
||||
approval.setTenantId(LoginHelper.getTenantId());
|
||||
@@ -652,7 +660,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
|
||||
@Override
|
||||
@Transactional
|
||||
public Boolean approveApplication(Long applicationId, Long approverId, String approverName,
|
||||
String approvalStatus, String approvalOpinion, String taskId) {
|
||||
String approvalStatus, String approvalOpinion, String approvalSignature, String taskId) {
|
||||
|
||||
// 🔴 标准化改造:如果提供了taskId,说明是标准工作流模式
|
||||
if (taskId != null && !taskId.trim().isEmpty()) {
|
||||
@@ -666,8 +674,9 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
|
||||
log.info("⚠️ [兼容模式] 暂时使用原有审批逻辑,等待集成工作流引擎API");
|
||||
}
|
||||
|
||||
// 调用原有的审批方法
|
||||
return approveApplication(applicationId, approverId, approverName, approvalStatus, approvalOpinion);
|
||||
// 调用原有的审批方法(增加签字参数)
|
||||
return approveApplication(applicationId, approverId, approverName, approvalStatus, approvalOpinion,
|
||||
approvalSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+618
-16
@@ -1,11 +1,25 @@
|
||||
package org.dromara.rs.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.itextpdf.text.Chunk;
|
||||
import com.itextpdf.text.Document;
|
||||
import com.itextpdf.text.DocumentException;
|
||||
import com.itextpdf.text.Element;
|
||||
import com.itextpdf.text.Font;
|
||||
import com.itextpdf.text.Image;
|
||||
import com.itextpdf.text.PageSize;
|
||||
import com.itextpdf.text.Paragraph;
|
||||
import com.itextpdf.text.Phrase;
|
||||
import com.itextpdf.text.pdf.BaseFont;
|
||||
import com.itextpdf.text.pdf.PdfPCell;
|
||||
import com.itextpdf.text.pdf.PdfPTable;
|
||||
import com.itextpdf.text.pdf.PdfWriter;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
@@ -22,10 +36,18 @@ import org.dromara.rs.service.IRsOverseasApprovalMonitorService;
|
||||
import org.dromara.rs.service.IRsOverseasApprovalService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -46,6 +68,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
|
||||
/**
|
||||
* 查询审批中的申请列表(待审核状态)
|
||||
* 🔴 查询所有待审批申请(不限制申请人)
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<RsOverseasApprovalMonitorVo> queryPendingList(RsOverseasApprovalMonitorQuery query) {
|
||||
@@ -54,6 +77,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
// 构建查询条件
|
||||
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
|
||||
wrapper.eq("status", "0"); // 只查询待审核状态
|
||||
// 🔴 不再限制申请人,拥有权限的用户可以看到所有数据
|
||||
|
||||
// 分页查询
|
||||
Page<RsOverseasApplication> page = applicationMapper.selectPage(
|
||||
@@ -70,11 +94,13 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
resultPage.setRecords(voList);
|
||||
resultPage.setTotal(page.getTotal());
|
||||
|
||||
log.info("✅ 查询到 {} 条待审批申请", voList.size());
|
||||
return TableDataInfo.build(resultPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询已完成审批的申请列表(已通过状态)
|
||||
* 🔴 查询所有已完成审批申请(不限制申请人)
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<RsOverseasApprovalMonitorVo> queryCompletedList(RsOverseasApprovalMonitorQuery query) {
|
||||
@@ -83,6 +109,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
// 构建查询条件
|
||||
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
|
||||
wrapper.eq("status", "1"); // 只查询已通过状态
|
||||
// 🔴 不再限制申请人,拥有权限的用户可以看到所有数据
|
||||
|
||||
// 分页查询
|
||||
Page<RsOverseasApplication> page = applicationMapper.selectPage(
|
||||
@@ -99,6 +126,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
resultPage.setRecords(voList);
|
||||
resultPage.setTotal(page.getTotal());
|
||||
|
||||
log.info("✅ 查询到 {} 条已完成审批申请", voList.size());
|
||||
return TableDataInfo.build(resultPage);
|
||||
}
|
||||
|
||||
@@ -143,11 +171,13 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
.eq("tenant_id", tenantId));
|
||||
statistics.put("pendingCount", pendingCount);
|
||||
|
||||
// 2. 各审批级别的待审批数量
|
||||
// 2. 各审批级别的待审批数量(五级审批制度)
|
||||
Map<String, Long> pendingByLevel = new HashMap<>();
|
||||
pendingByLevel.put("level1", countByStatusAndLevel("0", "1", tenantId)); // 部门主管
|
||||
pendingByLevel.put("level2", countByStatusAndLevel("0", "2", tenantId)); // 分管领导
|
||||
pendingByLevel.put("level3", countByStatusAndLevel("0", "3", tenantId)); // 校领导
|
||||
pendingByLevel.put("level1", countByStatusAndLevel("0", "1", tenantId)); // 部门负责人
|
||||
pendingByLevel.put("level2", countByStatusAndLevel("0", "2", tenantId)); // 人事处
|
||||
pendingByLevel.put("level3", countByStatusAndLevel("0", "3", tenantId)); // 纪委
|
||||
pendingByLevel.put("level4", countByStatusAndLevel("0", "4", tenantId)); // 学院分管领导
|
||||
pendingByLevel.put("level5", countByStatusAndLevel("0", "5", tenantId)); // 学院党委书记
|
||||
statistics.put("pendingByLevel", pendingByLevel);
|
||||
|
||||
// 3. 已完成审批总数
|
||||
@@ -285,9 +315,20 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
// 审批状态信息
|
||||
vo.setStatus(application.getStatus());
|
||||
vo.setStatusText(getStatusText(application.getStatus()));
|
||||
vo.setCurrentApprover(application.getCurrentApprover());
|
||||
vo.setApprovalLevel(application.getApprovalLevel());
|
||||
vo.setApprovalLevelText(getApprovalLevelText(application.getApprovalLevel()));
|
||||
|
||||
// 根据申请状态设置当前审批人和审批级别
|
||||
if ("1".equals(application.getStatus()) || "2".equals(application.getStatus())) {
|
||||
// 审批已完成(通过或拒绝)
|
||||
vo.setCurrentApprover("审批已完成");
|
||||
vo.setApprovalLevel(application.getApprovalLevel());
|
||||
vo.setApprovalLevelText("审批已完成");
|
||||
} else {
|
||||
// 待审核状态,显示当前审批信息
|
||||
vo.setCurrentApprover(application.getCurrentApprover());
|
||||
vo.setApprovalLevel(application.getApprovalLevel());
|
||||
vo.setApprovalLevelText(getApprovalLevelText(application.getApprovalLevel()));
|
||||
}
|
||||
|
||||
vo.setApplyTime(application.getApplyTime());
|
||||
vo.setApprovalTime(application.getApprovalTime());
|
||||
|
||||
@@ -336,8 +377,8 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
.count();
|
||||
vo.setApprovedLevels((int) approvedCount);
|
||||
|
||||
// 总审批级别数(假设最多3级:部门主管、分管领导、校领导)
|
||||
vo.setTotalLevels(3);
|
||||
// 总审批级别数(五级审批制度:部门负责人、人事处、纪委、学院分管领导、学院党委书记)
|
||||
vo.setTotalLevels(5);
|
||||
|
||||
// 审批进度文本
|
||||
vo.setApprovalProgress(approvedCount + "/" + vo.getTotalLevels());
|
||||
@@ -350,8 +391,8 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
} else {
|
||||
// 未开始审批
|
||||
vo.setApprovedLevels(0);
|
||||
vo.setTotalLevels(3);
|
||||
vo.setApprovalProgress("0/3");
|
||||
vo.setTotalLevels(5);
|
||||
vo.setApprovalProgress("0/5");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,6 +421,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
|
||||
/**
|
||||
* 获取审批级别文本
|
||||
* 五级审批制度:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
|
||||
*/
|
||||
private String getApprovalLevelText(String level) {
|
||||
if (level == null) {
|
||||
@@ -387,11 +429,15 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
}
|
||||
switch (level) {
|
||||
case "1":
|
||||
return "部门主管审批";
|
||||
return "部门负责人审批";
|
||||
case "2":
|
||||
return "分管领导审批";
|
||||
return "人事处审批";
|
||||
case "3":
|
||||
return "校领导审批";
|
||||
return "纪委审批";
|
||||
case "4":
|
||||
return "学院分管领导审批";
|
||||
case "5":
|
||||
return "学院党委书记审批";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
@@ -495,4 +541,560 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
||||
return politicalStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载出国申请审批表PDF
|
||||
*/
|
||||
@Override
|
||||
public void downloadApprovalPdf(Long applicationId, HttpServletResponse response) {
|
||||
log.info("开始生成出国申请审批表PDF,申请ID:{}", applicationId);
|
||||
|
||||
try {
|
||||
// 查询申请信息
|
||||
RsOverseasApplication application = applicationMapper.selectById(applicationId);
|
||||
if (application == null) {
|
||||
throw new ServiceException("未找到申请信息");
|
||||
}
|
||||
|
||||
// 查询审批历史
|
||||
List<RsOverseasApprovalVo> approvalHistory = approvalService.queryByApplicationId(applicationId);
|
||||
|
||||
// 调试日志:输出审批历史数据
|
||||
log.info("📋 查询到审批历史记录数量: {}", approvalHistory != null ? approvalHistory.size() : 0);
|
||||
if (approvalHistory != null && !approvalHistory.isEmpty()) {
|
||||
for (RsOverseasApprovalVo approval : approvalHistory) {
|
||||
log.info(" - 级别{}: 审批人={}, 意见={}, 签字数据长度={}",
|
||||
approval.getApprovalLevel(),
|
||||
approval.getApproverName(),
|
||||
approval.getApprovalOpinion(),
|
||||
approval.getApprovalSignature() != null ? approval.getApprovalSignature().length() : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
String fileName = "出国申请审批表_" + application.getApplicantName() + "_"
|
||||
+ new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".pdf";
|
||||
response.setContentType("application/pdf");
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
|
||||
|
||||
// 生成PDF
|
||||
generateApprovalPdf(application, approvalHistory, response.getOutputStream());
|
||||
|
||||
log.info("✅ 出国申请审批表PDF生成成功");
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 生成出国申请审批表PDF失败:{}", e.getMessage(), e);
|
||||
throw new ServiceException("生成PDF失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成审批表PDF文档
|
||||
*/
|
||||
private void generateApprovalPdf(RsOverseasApplication application,
|
||||
List<RsOverseasApprovalVo> approvalHistory, OutputStream outputStream)
|
||||
throws DocumentException, IOException {
|
||||
|
||||
Document document = new Document(PageSize.A4, 40, 40, 40, 40);
|
||||
PdfWriter.getInstance(document, outputStream);
|
||||
document.open();
|
||||
|
||||
try {
|
||||
// 创建中文字体
|
||||
BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
|
||||
Font titleFont = new Font(baseFont, 16, Font.BOLD);
|
||||
Font headerFont = new Font(baseFont, 14, Font.BOLD);
|
||||
Font normalFont = new Font(baseFont, 10, Font.NORMAL);
|
||||
Font smallFont = new Font(baseFont, 9, Font.NORMAL);
|
||||
|
||||
// 1. 标题
|
||||
Paragraph title1 = new Paragraph("杭州第一技师学院", titleFont);
|
||||
title1.setAlignment(Element.ALIGN_CENTER);
|
||||
title1.setSpacingAfter(5);
|
||||
document.add(title1);
|
||||
|
||||
Paragraph title2 = new Paragraph("登记报备人员因私事出国(境)审批表", headerFont);
|
||||
title2.setAlignment(Element.ALIGN_CENTER);
|
||||
title2.setSpacingAfter(15);
|
||||
document.add(title2);
|
||||
|
||||
// 2. 个人信息表格(优化列宽,使垂直线对齐)
|
||||
PdfPTable infoTable = new PdfPTable(12); // 12列
|
||||
infoTable.setWidthPercentage(100);
|
||||
// 列宽:使关键分界线对齐
|
||||
infoTable.setWidths(new float[] { 6f, 9f, 5f, 7f, 8f, 9f, 8f, 9f, 6f, 9f, 8f, 10f });
|
||||
|
||||
// 第一行:姓名、性别、出生年月、政治面貌
|
||||
addCell(infoTable, "姓名", normalFont, true, 1); // 列1: 6f
|
||||
addCell(infoTable, application.getApplicantName(), normalFont, false, 2); // 列2-3: 9f+5f=14f
|
||||
addCell(infoTable, "性别", normalFont, true, 1); // 列4: 7f
|
||||
addCell(infoTable, translateGender(application.getGender()), normalFont, false, 1); // 列5: 8f
|
||||
addCell(infoTable, "出生年月", normalFont, true, 1); // 列6: 9f
|
||||
addCell(infoTable, formatBirthDate(application.getBirthDate()), normalFont, false, 2); // 列7-8: 8f+9f=17f
|
||||
addCell(infoTable, "政治面貌", normalFont, true, 1); // 列9: 6f
|
||||
addCell(infoTable, translatePoliticalStatus(application.getPoliticalStatus()), normalFont, false, 3); // 列10-12:
|
||||
// 9f+8f+10f=27f
|
||||
|
||||
// 第二行:现工作部门、职称、职务
|
||||
addCell(infoTable, "现工作部门\n(教学系)", normalFont, true, 3); // 列1-3: 6f+9f+5f=20f
|
||||
addCell(infoTable, application.getDepartment(), normalFont, false, 2); // 列4-5: 7f+8f=15f
|
||||
addCell(infoTable, "职称", normalFont, true, 1); // 列6: 9f
|
||||
addCell(infoTable, application.getProfessionalTitle(), normalFont, false, 2); // 列7-8: 8f+9f=17f
|
||||
addCell(infoTable, "职务", normalFont, true, 1); // 列9: 6f
|
||||
addCell(infoTable, application.getPosition(), normalFont, false, 3); // 列10-12: 9f+8f+10f=27f
|
||||
|
||||
// 第三行:申领证件名称、证件号码
|
||||
addCell(infoTable, "申领证件名称", normalFont, true, 1); // 列1: 6f
|
||||
addCell(infoTable, application.getDocumentName(), normalFont, false, 2); // 列2-3: 9f+5f=14f
|
||||
addCell(infoTable, "证件号码", normalFont, true, 2); // 列4-5: 7f+8f=15f
|
||||
addCell(infoTable, application.getDocumentNumber(), normalFont, false, 7); // 列6-12:
|
||||
// 9f+8f+9f+6f+9f+8f+10f=59f
|
||||
|
||||
// 第四行:前往国家或地区(按照附件2标准格式)
|
||||
addCell(infoTable, "前往国家\n或地区", normalFont, true, 1); // 列1: 6f
|
||||
|
||||
PdfPCell destinationCell = new PdfPCell();
|
||||
destinationCell.setColspan(11); // 列2-12: 占据剩余11列
|
||||
destinationCell.setMinimumHeight(60);
|
||||
destinationCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||
destinationCell.setPadding(0);
|
||||
|
||||
// 创建嵌套表格:5列布局(国家名称 | 出境事由 | 旅游/探亲 | 探望对象关系标签 | 探望对象关系内容)
|
||||
// 列宽精确对齐外层表格的垂直线位置
|
||||
PdfPTable innerTable = new PdfPTable(5);
|
||||
innerTable.setWidthPercentage(100);
|
||||
// 对齐到外层列3|列5|列8|列10|列12的位置: 14f, 15f, 26f, 15f, 18f (总计88f)
|
||||
innerTable.setWidths(new float[] { 14f, 15f, 26f, 15f, 18f });
|
||||
|
||||
// 第一列:国家名称(合并3行)
|
||||
String destination = StringUtils.isNotBlank(application.getDestinationCountry())
|
||||
? application.getDestinationCountry()
|
||||
: "";
|
||||
PdfPCell countryCell = new PdfPCell(new Phrase(destination, smallFont));
|
||||
countryCell.setRowspan(3);
|
||||
countryCell.setMinimumHeight(60);
|
||||
countryCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
countryCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
countryCell.setPadding(3);
|
||||
innerTable.addCell(countryCell);
|
||||
|
||||
// 第一行:出境事由(合并2行)| 旅游选项(占据右侧3列)
|
||||
// 出境事由标签(合并2行)
|
||||
PdfPCell reasonLabelCell = new PdfPCell(new Phrase("出境事由", smallFont));
|
||||
reasonLabelCell.setRowspan(2);
|
||||
reasonLabelCell.setMinimumHeight(40);
|
||||
reasonLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
reasonLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
innerTable.addCell(reasonLabelCell);
|
||||
|
||||
// 旅游选项框(占据3列)
|
||||
String travelMark = "旅游".equals(application.getExitReason()) ? "√" : "";
|
||||
PdfPCell travelCell = new PdfPCell(new Phrase("旅游( " + travelMark + " )", smallFont));
|
||||
travelCell.setColspan(3);
|
||||
travelCell.setMinimumHeight(20);
|
||||
travelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
travelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
innerTable.addCell(travelCell);
|
||||
|
||||
// 第二行:探亲选项 | 探望对象关系标签 | 探望对象关系内容
|
||||
// 探亲选项框
|
||||
String visitMark = "探亲".equals(application.getExitReason()) ? "√" : "";
|
||||
PdfPCell visitCell = new PdfPCell(new Phrase("探亲( " + visitMark + " )", smallFont));
|
||||
visitCell.setMinimumHeight(20);
|
||||
visitCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
visitCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
innerTable.addCell(visitCell);
|
||||
|
||||
// 探望对象关系标签(独立单元格)
|
||||
PdfPCell relationLabelCell = new PdfPCell(new Phrase("探望对象姓名及\n与申请者关系", smallFont));
|
||||
relationLabelCell.setMinimumHeight(20);
|
||||
relationLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
relationLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
relationLabelCell.setPadding(3);
|
||||
innerTable.addCell(relationLabelCell);
|
||||
|
||||
// 探望对象关系内容(独立单元格)
|
||||
String relationship = StringUtils.isNotBlank(application.getVisitRelationship())
|
||||
? application.getVisitRelationship()
|
||||
: "";
|
||||
PdfPCell relationContentCell = new PdfPCell(new Phrase(relationship, smallFont));
|
||||
relationContentCell.setMinimumHeight(20);
|
||||
relationContentCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||
relationContentCell.setPadding(3);
|
||||
innerTable.addCell(relationContentCell);
|
||||
|
||||
// 第三行:备注 | 备注内容(国家名称已合并,从第2列开始,备注内容占据3列)
|
||||
PdfPCell remarkLabelCell = new PdfPCell(new Phrase("备注", smallFont));
|
||||
remarkLabelCell.setMinimumHeight(20);
|
||||
remarkLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
remarkLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
remarkLabelCell.setPadding(3);
|
||||
innerTable.addCell(remarkLabelCell);
|
||||
|
||||
String remarks = StringUtils.isNotBlank(application.getRemarks())
|
||||
? application.getRemarks()
|
||||
: "";
|
||||
PdfPCell remarkValueCell = new PdfPCell(new Phrase(remarks, smallFont));
|
||||
remarkValueCell.setColspan(3); // 占据第3、4、5列
|
||||
remarkValueCell.setMinimumHeight(20);
|
||||
remarkValueCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||
remarkValueCell.setPadding(3);
|
||||
innerTable.addCell(remarkValueCell);
|
||||
|
||||
destinationCell.addElement(innerTable);
|
||||
infoTable.addCell(destinationCell);
|
||||
|
||||
// 第五行:出国时间
|
||||
addCell(infoTable, "出过(境)时间", normalFont, true, 2); // 列1-2
|
||||
PdfPCell timeCell = new PdfPCell();
|
||||
timeCell.setColspan(10); // 列3-12: 占据剩余10列
|
||||
timeCell.setMinimumHeight(30);
|
||||
timeCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
|
||||
String timeText = String.format("拟于 %s 至 %s 出国(境),为期 %d 天。",
|
||||
formatDateChinese(application.getStartDate()),
|
||||
formatDateChinese(application.getEndDate()),
|
||||
application.getDuration() != null ? application.getDuration() : 0);
|
||||
Paragraph timePara = new Paragraph(timeText, smallFont);
|
||||
timeCell.addElement(timePara);
|
||||
infoTable.addCell(timeCell);
|
||||
|
||||
// 第六行:承诺声明(修复断层问题,使用单一段落)
|
||||
PdfPCell promiseCell = new PdfPCell();
|
||||
promiseCell.setColspan(12); // 占据全部12列
|
||||
promiseCell.setMinimumHeight(70);
|
||||
promiseCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||
promiseCell.setPadding(5);
|
||||
|
||||
// 使用单一段落避免断层,通过Chunk控制对齐
|
||||
Paragraph promisePara = new Paragraph();
|
||||
promisePara.setSpacingAfter(0);
|
||||
promisePara.setSpacingBefore(0);
|
||||
|
||||
// 承诺内容 - 居中部分
|
||||
Chunk promiseText1 = new Chunk("本人承诺,在出国(境)期间遵守当地法律法规和中国的法律法规,结束后如期回国并到学校", smallFont);
|
||||
Chunk promiseText2 = new Chunk("\n报到,如逾期不归,同意作自动离职处理。\n", smallFont);
|
||||
promisePara.add(promiseText1);
|
||||
promisePara.add(promiseText2);
|
||||
promisePara.setAlignment(Element.ALIGN_CENTER);
|
||||
promiseCell.addElement(promisePara);
|
||||
|
||||
// 联系信息和签字 - 左对齐部分
|
||||
Paragraph contactPara = new Paragraph();
|
||||
contactPara.setSpacingAfter(0);
|
||||
contactPara.setSpacingBefore(2);
|
||||
contactPara.setAlignment(Element.ALIGN_LEFT);
|
||||
contactPara.setIndentationLeft(10);
|
||||
contactPara.add(new Chunk("手写签字:", smallFont));
|
||||
|
||||
// 如果有签字,插入签字图片
|
||||
if (StringUtils.isNotBlank(application.getSignature())) {
|
||||
try {
|
||||
Image signatureImage = getImageFromBase64(application.getSignature());
|
||||
if (signatureImage != null) {
|
||||
signatureImage.scaleToFit(60, 20);
|
||||
contactPara.add(new Chunk(signatureImage, 0, -5));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("插入申请人签字图片失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
contactPara.add(new Chunk(" 联系电话:", smallFont));
|
||||
if (StringUtils.isNotBlank(application.getMobilePhone())) {
|
||||
contactPara.add(new Chunk(application.getMobilePhone(), smallFont));
|
||||
} else {
|
||||
contactPara.add(new Chunk("13333333333", smallFont));
|
||||
}
|
||||
contactPara.add(new Chunk(" 应急联系人及联系电话:", smallFont));
|
||||
if (StringUtils.isNotBlank(application.getEmergencyContact())) {
|
||||
contactPara.add(new Chunk(application.getEmergencyContact(), smallFont));
|
||||
} else {
|
||||
contactPara.add(new Chunk("13333333333", smallFont));
|
||||
}
|
||||
promiseCell.addElement(contactPara);
|
||||
|
||||
// 日期 - 靠右对齐部分(使用申请日期)
|
||||
Paragraph datePara = new Paragraph();
|
||||
datePara.setSpacingAfter(0);
|
||||
datePara.setSpacingBefore(2);
|
||||
datePara.setAlignment(Element.ALIGN_RIGHT);
|
||||
datePara.setIndentationRight(20);
|
||||
// 使用申请时间作为申请人签名日期(优先使用applyTime,其次使用createTime)
|
||||
Date applicationDateTime = application.getApplyTime() != null
|
||||
? application.getApplyTime()
|
||||
: application.getCreateTime();
|
||||
String applicationDate = applicationDateTime != null
|
||||
? formatDateChinese(applicationDateTime)
|
||||
: "年 月 日";
|
||||
datePara.add(new Chunk(applicationDate, smallFont));
|
||||
promiseCell.addElement(datePara);
|
||||
|
||||
infoTable.addCell(promiseCell);
|
||||
|
||||
document.add(infoTable);
|
||||
|
||||
// 3. 审批意见表格(紧贴个人信息表格,无间隔)
|
||||
PdfPTable approvalTable = new PdfPTable(2);
|
||||
approvalTable.setWidthPercentage(100);
|
||||
approvalTable.setWidths(new float[] { 20f, 80f });
|
||||
approvalTable.setSpacingBefore(0); // 设置为0,与上方表格紧密连接
|
||||
|
||||
// 部门(教学系)意见
|
||||
addApprovalRowWithSignature(approvalTable, "部门\n(教学\n系)\n意见",
|
||||
approvalHistory, "1", normalFont, smallFont);
|
||||
|
||||
// 人事(组织)处审核意见
|
||||
addApprovalRowWithSignature(approvalTable, "人事\n(组\n织)处\n审核\n意见",
|
||||
approvalHistory, "2", normalFont, smallFont);
|
||||
|
||||
// 纪委意见
|
||||
addApprovalRowWithSignature(approvalTable, "纪委\n意见",
|
||||
approvalHistory, "3", normalFont, smallFont);
|
||||
|
||||
// 学院分管领导意见(独立行)
|
||||
addApprovalRowWithLeaderSignature(approvalTable, "学院分\n管领导\n意见",
|
||||
approvalHistory, "4", normalFont, smallFont);
|
||||
|
||||
// 学院党委书记意见(独立行)
|
||||
addApprovalRowWithSignature(approvalTable, "学院党\n委书记\n意见",
|
||||
approvalHistory, "5", normalFont, smallFont);
|
||||
|
||||
document.add(approvalTable);
|
||||
|
||||
// 4. 备注说明(移除空段落,直接连接)
|
||||
Paragraph note = new Paragraph();
|
||||
note.setSpacingBefore(5); // 只留很小的间距
|
||||
note.add(new Chunk("备注:1. 因私出境时间,除特殊原因外,原则上应为假期;\n", smallFont));
|
||||
note.add(new Chunk(" 2. 新申办证件名,本表证件号码一栏不填。", smallFont));
|
||||
document.add(note);
|
||||
|
||||
} finally {
|
||||
document.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加表格单元格
|
||||
*/
|
||||
private void addCell(PdfPTable table, String text, Font font, boolean isHeader, int colspan) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(text, font));
|
||||
cell.setColspan(colspan);
|
||||
cell.setMinimumHeight(25);
|
||||
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
// 移除灰色背景,所有单元格统一白色背景
|
||||
// if (isHeader) {
|
||||
// cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
|
||||
// }
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加审批意见行(带签字显示)
|
||||
*/
|
||||
private void addApprovalRowWithSignature(PdfPTable table, String label,
|
||||
List<RsOverseasApprovalVo> approvalHistory, String level, Font labelFont, Font contentFont) {
|
||||
PdfPCell labelCell = new PdfPCell(new Phrase(label, labelFont));
|
||||
labelCell.setMinimumHeight(50);
|
||||
labelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
labelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
table.addCell(labelCell);
|
||||
|
||||
PdfPCell opinionCell = new PdfPCell();
|
||||
opinionCell.setMinimumHeight(50);
|
||||
opinionCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||
opinionCell.setPadding(5);
|
||||
|
||||
Paragraph para = new Paragraph();
|
||||
String opinion = getApprovalOpinion(approvalHistory, level);
|
||||
para.add(new Chunk(opinion, contentFont));
|
||||
para.add(new Chunk("\n\n负责人签字:", contentFont));
|
||||
|
||||
// 添加签字图片
|
||||
String signature = getApprovalSignature(approvalHistory, level);
|
||||
if (StringUtils.isNotBlank(signature)) {
|
||||
try {
|
||||
Image signImage = getImageFromBase64(signature);
|
||||
if (signImage != null) {
|
||||
signImage.scaleToFit(60, 20);
|
||||
para.add(new Chunk(signImage, 0, -5));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("插入审批人签字图片失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 日期靠右(使用审批日期)
|
||||
opinionCell.addElement(para);
|
||||
Paragraph datePara = new Paragraph();
|
||||
datePara.setAlignment(Element.ALIGN_RIGHT);
|
||||
datePara.setIndentationRight(10);
|
||||
// 获取审批日期
|
||||
Date approvalDate = getApprovalDate(approvalHistory, level);
|
||||
String dateText = approvalDate != null ? formatDateChinese(approvalDate) : "年 月 日";
|
||||
datePara.add(new Chunk(dateText, contentFont));
|
||||
opinionCell.addElement(datePara);
|
||||
|
||||
table.addCell(opinionCell);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加学院分管领导审批行(使用"领导签字"并包含"学院"文字)
|
||||
*/
|
||||
private void addApprovalRowWithLeaderSignature(PdfPTable table, String label,
|
||||
List<RsOverseasApprovalVo> approvalHistory, String level, Font labelFont, Font contentFont) {
|
||||
PdfPCell labelCell = new PdfPCell(new Phrase(label, labelFont));
|
||||
labelCell.setMinimumHeight(50);
|
||||
labelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||
labelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
table.addCell(labelCell);
|
||||
|
||||
PdfPCell opinionCell = new PdfPCell();
|
||||
opinionCell.setMinimumHeight(50);
|
||||
opinionCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||
opinionCell.setPadding(5);
|
||||
|
||||
Paragraph para = new Paragraph();
|
||||
String opinion = getApprovalOpinion(approvalHistory, level);
|
||||
para.add(new Chunk(opinion, contentFont));
|
||||
para.add(new Chunk("\n\n领导签字:", contentFont));
|
||||
|
||||
// 添加签字图片
|
||||
String signature = getApprovalSignature(approvalHistory, level);
|
||||
if (StringUtils.isNotBlank(signature)) {
|
||||
try {
|
||||
Image signImage = getImageFromBase64(signature);
|
||||
if (signImage != null) {
|
||||
signImage.scaleToFit(60, 20);
|
||||
para.add(new Chunk(signImage, 0, -5));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("插入分管领导签字图片失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 日期靠右(使用审批日期)
|
||||
opinionCell.addElement(para);
|
||||
Paragraph datePara = new Paragraph();
|
||||
datePara.setAlignment(Element.ALIGN_RIGHT);
|
||||
datePara.setIndentationRight(10);
|
||||
// 获取审批日期
|
||||
Date approvalDate = getApprovalDate(approvalHistory, level);
|
||||
String dateText = approvalDate != null ? formatDateChinese(approvalDate) : "年 月 日";
|
||||
datePara.add(new Chunk(dateText, contentFont));
|
||||
opinionCell.addElement(datePara);
|
||||
|
||||
table.addCell(opinionCell);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定级别的审批意见
|
||||
*/
|
||||
private String getApprovalOpinion(List<RsOverseasApprovalVo> approvalHistory, String level) {
|
||||
if (approvalHistory == null || approvalHistory.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return approvalHistory.stream()
|
||||
.filter(a -> level.equals(a.getApprovalLevel()))
|
||||
.findFirst()
|
||||
.map(a -> {
|
||||
String opinion = StringUtils.isNotBlank(a.getApprovalOpinion())
|
||||
? a.getApprovalOpinion()
|
||||
: ("1".equals(a.getApprovalStatus()) ? "同意" : "");
|
||||
return opinion;
|
||||
})
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
private String formatDate(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
return sdf.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期为中文格式(yyyy 年 MM 月 dd 日)
|
||||
*/
|
||||
private String formatDateChinese(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy 年 MM 月 dd 日");
|
||||
return sdf.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化出生日期(只显示年月)
|
||||
*/
|
||||
private String formatBirthDate(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月");
|
||||
return sdf.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定级别的审批签字
|
||||
*/
|
||||
private String getApprovalSignature(List<RsOverseasApprovalVo> approvalHistory, String level) {
|
||||
if (approvalHistory == null || approvalHistory.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return approvalHistory.stream()
|
||||
.filter(a -> level.equals(a.getApprovalLevel()))
|
||||
.findFirst()
|
||||
.map(RsOverseasApprovalVo::getApprovalSignature)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定级别的审批日期
|
||||
*/
|
||||
private Date getApprovalDate(List<RsOverseasApprovalVo> approvalHistory, String level) {
|
||||
if (approvalHistory == null || approvalHistory.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return approvalHistory.stream()
|
||||
.filter(a -> level.equals(a.getApprovalLevel()))
|
||||
.findFirst()
|
||||
.map(RsOverseasApprovalVo::getApprovalTime)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Base64字符串创建Image对象
|
||||
*/
|
||||
private Image getImageFromBase64(String base64String) {
|
||||
if (StringUtils.isBlank(base64String)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理data URL格式:data:image/png;base64,xxxxx
|
||||
String base64Data = base64String;
|
||||
if (base64String.contains(",")) {
|
||||
base64Data = base64String.substring(base64String.indexOf(",") + 1);
|
||||
}
|
||||
|
||||
// Base64解码
|
||||
byte[] imageBytes = java.util.Base64.getDecoder().decode(base64Data);
|
||||
|
||||
// 创建Image对象
|
||||
Image image = Image.getInstance(imageBytes);
|
||||
return image;
|
||||
} catch (Exception e) {
|
||||
log.error("从Base64创建图片失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -121,9 +121,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
ELSE oa.purpose
|
||||
END as purpose_text,
|
||||
CASE oa.approval_level
|
||||
WHEN '1' THEN '部门主管'
|
||||
WHEN '2' THEN '分管领导'
|
||||
WHEN '3' THEN '校领导'
|
||||
WHEN '1' THEN '部门负责人'
|
||||
WHEN '2' THEN '人事处'
|
||||
WHEN '3' THEN '纪委'
|
||||
WHEN '4' THEN '学院分管领导'
|
||||
WHEN '5' THEN '学院党委书记'
|
||||
ELSE '未知级别'
|
||||
END as approval_level_text,
|
||||
CASE oa.gender
|
||||
|
||||
+12
-7
@@ -12,6 +12,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="approvalLevel" column="approval_level"/>
|
||||
<result property="approvalStatus" column="approval_status"/>
|
||||
<result property="approvalOpinion" column="approval_opinion"/>
|
||||
<result property="approvalSignature" column="approval_signature"/>
|
||||
<result property="approvalTime" column="approval_time"/>
|
||||
<result property="tenantId" column="tenant_id"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
@@ -39,16 +40,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<sql id="selectOverseasApprovalVo">
|
||||
select oa.approval_id, oa.application_id, oa.approver_id, oa.approver_name, oa.approval_level,
|
||||
oa.approval_status, oa.approval_opinion, oa.approval_time, oa.tenant_id,
|
||||
oa.approval_status, oa.approval_opinion, oa.approval_signature, oa.approval_time, oa.tenant_id,
|
||||
oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted,
|
||||
<!-- 申请信息字段 -->
|
||||
app.applicant_id, app.applicant_name, app.department, app.purpose, app.destination,
|
||||
app.start_date, app.end_date, app.duration,
|
||||
<!-- 文本转换 -->
|
||||
CASE oa.approval_level
|
||||
WHEN '1' THEN '部门主管'
|
||||
WHEN '2' THEN '分管领导'
|
||||
WHEN '3' THEN '校领导'
|
||||
WHEN '1' THEN '部门负责人'
|
||||
WHEN '2' THEN '人事处'
|
||||
WHEN '3' THEN '纪委'
|
||||
WHEN '4' THEN '学院分管领导'
|
||||
WHEN '5' THEN '学院党委书记'
|
||||
ELSE '未知级别'
|
||||
END as approval_level_text,
|
||||
CASE oa.approval_status
|
||||
@@ -169,9 +172,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
app.duration,
|
||||
-- 文本转换
|
||||
CASE oa.approval_level
|
||||
WHEN '1' THEN '部门主管'
|
||||
WHEN '2' THEN '分管领导'
|
||||
WHEN '3' THEN '校领导'
|
||||
WHEN '1' THEN '部门负责人'
|
||||
WHEN '2' THEN '人事处'
|
||||
WHEN '3' THEN '纪委'
|
||||
WHEN '4' THEN '学院分管领导'
|
||||
WHEN '5' THEN '学院党委书记'
|
||||
ELSE '未知级别'
|
||||
END as approval_level_text,
|
||||
CASE oa.approval_status
|
||||
|
||||
@@ -157,6 +157,7 @@ export interface ApprovalRequest {
|
||||
applicationId: number | string;
|
||||
approvalStatus: string;
|
||||
approvalOpinion?: string;
|
||||
approvalSignature?: string; // 审批签字(base64图片)
|
||||
taskId?: string; // 🔴 新增:工作流任务ID,支持标准工作流模式
|
||||
}
|
||||
|
||||
@@ -885,6 +886,7 @@ export interface ItineraryChangeApprovalRequest {
|
||||
changeId: number | string;
|
||||
approved: boolean; // true=通过,false=拒绝
|
||||
opinion?: string; // 审批意见
|
||||
signature?: string; // 审批签字(base64图片)
|
||||
}
|
||||
|
||||
// 查询行程变更列表
|
||||
@@ -1030,3 +1032,54 @@ export function getHrApprovers(applicantId: string | number): Promise<{ data: nu
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 出国申请进度查询(监控)相关API ====================
|
||||
|
||||
// 审批监控查询参数类型
|
||||
export interface OverseasMonitorQuery {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
applicantName?: string;
|
||||
department?: string;
|
||||
destination?: string;
|
||||
approvalLevel?: string;
|
||||
currentApprover?: string;
|
||||
status?: string;
|
||||
applyTimeStart?: string;
|
||||
applyTimeEnd?: string;
|
||||
}
|
||||
|
||||
// 查询待审批列表(监控)
|
||||
export function getPendingList(query: OverseasMonitorQuery) {
|
||||
return request({
|
||||
url: '/rs/overseas/monitor/pending/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
}
|
||||
|
||||
// 查询已完成审批列表(监控)
|
||||
export function getCompletedList(query: OverseasMonitorQuery) {
|
||||
return request({
|
||||
url: '/rs/overseas/monitor/completed/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
}
|
||||
|
||||
// 查询申请详情(监控)
|
||||
export function getDetail(applicationId: string | number) {
|
||||
return request({
|
||||
url: `/rs/overseas/monitor/detail/${applicationId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
// 下载审批表PDF
|
||||
export function downloadApprovalPdf(applicationId: string | number) {
|
||||
return request({
|
||||
url: `/rs/overseas/monitor/downloadApprovalPdf/${applicationId}`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
@@ -1,278 +1,247 @@
|
||||
<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">
|
||||
<div class="signature-pad-container">
|
||||
<div class="signature-canvas-wrapper">
|
||||
<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>
|
||||
:width="width"
|
||||
:height="height"
|
||||
@mousedown="startDrawing"
|
||||
@mousemove="draw"
|
||||
@mouseup="stopDrawing"
|
||||
@mouseleave="stopDrawing"
|
||||
@touchstart="handleTouch"
|
||||
@touchmove="handleTouch"
|
||||
@touchend="stopDrawing"
|
||||
/>
|
||||
</div>
|
||||
<div class="signature-buttons">
|
||||
<el-button size="small" @click="clearSignature">清除</el-button>
|
||||
<el-button v-if="showSaveButton" size="small" type="primary" @click="saveSignature">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
modelValue?: string; // Base64签名数据
|
||||
modelValue?: string; // base64图片数据
|
||||
width?: number;
|
||||
height?: number;
|
||||
lineWidth?: number;
|
||||
lineColor?: string;
|
||||
showSaveButton?: boolean; // 是否显示保存按钮
|
||||
autoSave?: boolean; // 是否自动保存(绘制时实时保存)
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
width: 500,
|
||||
height: 200,
|
||||
lineWidth: 2,
|
||||
lineColor: '#000000'
|
||||
width: 400,
|
||||
height: 150,
|
||||
showSaveButton: true,
|
||||
autoSave: true
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
'update:modelValue': [value: string];
|
||||
'save': [value: string];
|
||||
}>();
|
||||
|
||||
// Refs
|
||||
const canvasRef = ref<HTMLCanvasElement>();
|
||||
const hasSignature = ref(false);
|
||||
const isDrawing = ref(false);
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
const lastPoint = ref({ x: 0, y: 0 });
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas();
|
||||
// 如果有初始值,加载到画布上
|
||||
if (props.modelValue) {
|
||||
loadSignature(props.modelValue);
|
||||
// Methods
|
||||
const startDrawing = (event: MouseEvent) => {
|
||||
if (!canvasRef.value) return;
|
||||
|
||||
isDrawing.value = true;
|
||||
const rect = canvasRef.value.getBoundingClientRect();
|
||||
lastPoint.value = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
};
|
||||
|
||||
const draw = (event: MouseEvent) => {
|
||||
if (!isDrawing.value || !canvasRef.value) return;
|
||||
|
||||
const ctx = canvasRef.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const rect = canvasRef.value.getBoundingClientRect();
|
||||
const currentPoint = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
|
||||
// 绘制线条
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lastPoint.value.x, lastPoint.value.y);
|
||||
ctx.lineTo(currentPoint.x, currentPoint.y);
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.stroke();
|
||||
|
||||
lastPoint.value = currentPoint;
|
||||
|
||||
// 自动保存
|
||||
if (props.autoSave) {
|
||||
saveToModel();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听容器大小变化
|
||||
if (canvasRef.value?.parentElement) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
resizeObserver.observe(canvasRef.value.parentElement);
|
||||
const stopDrawing = () => {
|
||||
isDrawing.value = false;
|
||||
|
||||
// 如果不是自动保存,在停止绘制时保存
|
||||
if (!props.autoSave) {
|
||||
saveToModel();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
// 触摸事件处理
|
||||
const handleTouch = (event: TouchEvent) => {
|
||||
event.preventDefault();
|
||||
const touch = event.touches[0];
|
||||
if (!touch || !canvasRef.value) return;
|
||||
|
||||
const rect = canvasRef.value.getBoundingClientRect();
|
||||
const mouseEvent = new MouseEvent(
|
||||
event.type === 'touchstart' ? 'mousedown' : 'mousemove',
|
||||
{
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
}
|
||||
);
|
||||
|
||||
if (event.type === 'touchstart') {
|
||||
startDrawing(mouseEvent);
|
||||
} else if (event.type === 'touchmove') {
|
||||
draw(mouseEvent);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 监听外部值变化
|
||||
// 清除签名
|
||||
const clearSignature = () => {
|
||||
if (!canvasRef.value) return;
|
||||
|
||||
const ctx = canvasRef.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
|
||||
emit('update:modelValue', '');
|
||||
};
|
||||
|
||||
// 保存到 v-model
|
||||
const saveToModel = () => {
|
||||
if (!canvasRef.value) return;
|
||||
|
||||
try {
|
||||
const dataURL = canvasRef.value.toDataURL('image/png');
|
||||
emit('update:modelValue', dataURL);
|
||||
} catch (error) {
|
||||
console.error('保存签名失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 手动保存(触发save事件)
|
||||
const saveSignature = () => {
|
||||
if (!canvasRef.value) return;
|
||||
|
||||
try {
|
||||
const dataURL = canvasRef.value.toDataURL('image/png');
|
||||
emit('update:modelValue', dataURL);
|
||||
emit('save', dataURL);
|
||||
ElMessage.success('签名已保存');
|
||||
} catch (error) {
|
||||
console.error('保存签名失败:', error);
|
||||
ElMessage.error('保存签名失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 从base64恢复签名
|
||||
const restoreSignature = (base64Image: string) => {
|
||||
if (!canvasRef.value || !base64Image) return;
|
||||
|
||||
const ctx = canvasRef.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (!canvasRef.value) return;
|
||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
};
|
||||
img.src = base64Image;
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化,恢复签名
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal && !hasSignature.value) {
|
||||
loadSignature(newVal);
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
restoreSignature(newValue);
|
||||
} else {
|
||||
clearSignature();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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';
|
||||
// 组件挂载时恢复签名
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
restoreSignature(props.modelValue);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 处理容器大小变化
|
||||
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
|
||||
clearSignature,
|
||||
saveSignature
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.signature-pad-wrapper {
|
||||
.signature-pad-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
.signature-canvas-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fafafa;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.signature-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.signature-canvas {
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
cursor: crosshair;
|
||||
background-color: #fff;
|
||||
touch-action: none; /* 防止触摸时页面滚动 */
|
||||
}
|
||||
|
||||
.signature-canvas-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
.signature-canvas:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
.signature-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -325,11 +325,23 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审批对话框 -->
|
||||
<el-dialog :title="approvalDialog.title" v-model="approvalDialog.visible" width="600px" append-to-body>
|
||||
<el-dialog :title="approvalDialog.title" v-model="approvalDialog.visible" width="650px" append-to-body>
|
||||
<el-form ref="approvalFormRef" :model="approvalForm" :rules="approvalRules" label-width="100px">
|
||||
<el-form-item label="审批意见" prop="approvalOpinion">
|
||||
<el-input v-model="approvalForm.approvalOpinion" type="textarea" :rows="4" placeholder="请输入审批意见" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 手写签字 -->
|
||||
<el-form-item label="审批签字" prop="approvalSignature">
|
||||
<SignaturePad
|
||||
v-model="approvalForm.approvalSignature"
|
||||
:width="450"
|
||||
:height="120"
|
||||
:auto-save="true"
|
||||
:show-save-button="false"
|
||||
/>
|
||||
<div class="signature-tip">请在上方签字板中手写签名</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -360,6 +372,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Document, Warning, Paperclip, View, Download } from '@element-plus/icons-vue';
|
||||
import animate from '@/animate';
|
||||
import request from '@/utils/request';
|
||||
import SignaturePad from '@/components/SignaturePad/index.vue';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
// 响应式数据
|
||||
@@ -400,7 +413,8 @@ const state = reactive({
|
||||
approvalForm: {
|
||||
applicationId: '',
|
||||
approvalStatus: '1',
|
||||
approvalOpinion: ''
|
||||
approvalOpinion: '',
|
||||
approvalSignature: '' // 审批签字
|
||||
} as ApprovalRequest,
|
||||
approvalRules: {
|
||||
approvalOpinion: [{ required: true, message: '请输入审批意见', trigger: 'blur' }]
|
||||
@@ -550,6 +564,7 @@ const handleApprovalPass = (row: OverseasApplicationVO) => {
|
||||
state.approvalForm.applicationId = row.applicationId;
|
||||
state.approvalForm.approvalStatus = '1';
|
||||
state.approvalForm.approvalOpinion = '';
|
||||
state.approvalForm.approvalSignature = ''; // 清空签字
|
||||
state.approvalDialog.title = '通过';
|
||||
state.approvalDialog.visible = true;
|
||||
};
|
||||
@@ -559,6 +574,7 @@ const handleApprovalReject = (row: OverseasApplicationVO) => {
|
||||
state.approvalForm.applicationId = row.applicationId;
|
||||
state.approvalForm.approvalStatus = '2';
|
||||
state.approvalForm.approvalOpinion = '';
|
||||
state.approvalForm.approvalSignature = ''; // 清空签字
|
||||
state.approvalDialog.title = '拒绝';
|
||||
state.approvalDialog.visible = true;
|
||||
};
|
||||
@@ -586,7 +602,8 @@ const cancelApproval = () => {
|
||||
state.approvalForm = {
|
||||
applicationId: '',
|
||||
approvalStatus: '1',
|
||||
approvalOpinion: ''
|
||||
approvalOpinion: '',
|
||||
approvalSignature: ''
|
||||
};
|
||||
};
|
||||
|
||||
@@ -913,4 +930,12 @@ const handlePreviewStampedFile = async (data: OverseasApplicationVO) => {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 签字样式 */
|
||||
.signature-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -257,7 +257,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审批对话框 -->
|
||||
<el-dialog :title="approvalDialog.title" v-model="approvalDialog.visible" width="600px" append-to-body :close-on-click-modal="false">
|
||||
<el-dialog :title="approvalDialog.title" v-model="approvalDialog.visible" width="650px" append-to-body :close-on-click-modal="false">
|
||||
<el-form ref="approvalFormRef" :model="approvalForm" :rules="approvalRules" label-width="100px">
|
||||
<el-form-item label="审批结果" prop="approved">
|
||||
<el-radio-group v-model="approvalForm.approved">
|
||||
@@ -268,6 +268,18 @@
|
||||
<el-form-item label="审批意见" prop="opinion">
|
||||
<el-input v-model="approvalForm.opinion" type="textarea" :rows="4" placeholder="请输入审批意见" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 手写签字 -->
|
||||
<el-form-item label="审批签字">
|
||||
<SignaturePad
|
||||
v-model="approvalForm.signature"
|
||||
:width="450"
|
||||
:height="120"
|
||||
:auto-save="true"
|
||||
:show-save-button="false"
|
||||
/>
|
||||
<div class="signature-tip">请在上方签字板中手写签名</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -295,6 +307,7 @@ import {
|
||||
type ItineraryChangeVO,
|
||||
type ItineraryChangeApprovalRequest
|
||||
} from '@/api/rs/overseas';
|
||||
import SignaturePad from '@/components/SignaturePad/index.vue';
|
||||
|
||||
// ==================== 响应式数据 ====================
|
||||
|
||||
@@ -346,7 +359,8 @@ const state = reactive({
|
||||
approvalForm: {
|
||||
changeId: 0,
|
||||
approved: true, // true=通过,false=拒绝
|
||||
opinion: ''
|
||||
opinion: '',
|
||||
signature: '' // 审批签字
|
||||
} as ItineraryChangeApprovalRequest,
|
||||
approvalLoading: false,
|
||||
|
||||
@@ -549,4 +563,12 @@ const getStatusTagType = (status: string): 'primary' | 'success' | 'info' | 'war
|
||||
.tab-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* 签字样式 */
|
||||
.signature-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user