Просмотр исходного кода

“出国申请审批流程已实现10.29”

weizhong 1 год назад
Родитель
Сommit
4277e2fca8
15 измененных файлов с 1025 добавлено и 306 удалено
  1. 10 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java
  2. 34 36
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java
  3. 8 3
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApproval.java
  4. 6 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApprovalBo.java
  5. 15 10
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalVo.java
  6. 9 8
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java
  7. 9 0
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApprovalMonitorService.java
  8. 13 4
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java
  9. 618 16
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java
  10. 5 3
      backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml
  11. 12 7
      backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApprovalMapper.xml
  12. 53 0
      web/src/api/rs/overseas.ts
  13. 181 212
      web/src/components/SignaturePad/index.vue
  14. 28 3
      web/src/views/rs/overseas/approval.vue
  15. 24 2
      web/src/views/rs/overseas/itinerary-change-approval.vue

+ 10 - 1
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApproval.java

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApprovalBo.java

@@ -72,4 +72,9 @@ public class RsOverseasApprovalBo extends BaseEntity {
      * 审批意见
      */
     private String approvalOpinion;
-} 
+
+    /**
+     * 审批签字(base64图片数据)
+     */
+    private String approvalSignature;
+}

+ 15 - 10
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApprovalVo.java

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java

@@ -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 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApprovalMonitorService.java

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml

@@ -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
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApprovalMapper.xml

@@ -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 

+ 53 - 0
web/src/api/rs/overseas.ts

@@ -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'
+  });
+}

+ 181 - 212
web/src/components/SignaturePad/index.vue

@@ -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);
-  }
-
-  // 监听容器大小变化
-  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 = () => {
+// Methods
+const startDrawing = (event: MouseEvent) => {
   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);
+  lastPoint.value = {
+    x: event.clientX - rect.left,
+    y: event.clientY - rect.top
+  };
 };
 
-const draw = (x: number, y: number) => {
-  if (!isDrawing.value || !ctx || !canvasRef.value) return;
-
+const draw = (event: MouseEvent) => {
+  if (!isDrawing.value || !canvasRef.value) return;
+  
+  const ctx = canvasRef.value.getContext('2d');
+  if (!ctx) return;
+  
   const rect = canvasRef.value.getBoundingClientRect();
-  ctx.lineTo(x - rect.left, y - rect.top);
+  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();
-  hasSignature.value = true;
+  
+  lastPoint.value = currentPoint;
+  
+  // 自动保存
+  if (props.autoSave) {
+    saveToModel();
+  }
 };
 
 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);
+  
+  // 如果不是自动保存,在停止绘制时保存
+  if (!props.autoSave) {
+    saveToModel();
   }
 };
 
-// 鼠标事件处理
-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 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 handleTouchMove = (e: TouchEvent) => {
-  e.preventDefault();
-  const touch = e.touches[0];
-  draw(touch.clientX, touch.clientY);
+// 清除签名
+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', '');
 };
 
-const handleTouchEnd = (e: TouchEvent) => {
-  e.preventDefault();
-  stopDrawing();
+// 保存到 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);
+  }
 };
 
-// 清空签名
-const handleClear = () => {
-  if (!canvasRef.value || !ctx) return;
-
-  ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
-  hasSignature.value = false;
-  emit('update:modelValue', '');
+// 手动保存(触发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('保存签名失败');
+  }
 };
 
-// 加载签名
-const loadSignature = (base64Data: string) => {
-  if (!canvasRef.value || !ctx) return;
-
+// 从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 = () => {
-    ctx!.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
-    ctx!.drawImage(img, 0, 0);
-    hasSignature.value = true;
+    if (!canvasRef.value) return;
+    ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
+    ctx.drawImage(img, 0, 0);
   };
-  img.src = base64Data;
+  img.src = base64Image;
 };
 
-// 获取签名数据
-const getSignature = (): string => {
-  if (!canvasRef.value || !hasSignature.value) return '';
-  return canvasRef.value.toDataURL('image/png');
-};
+// 监听 modelValue 变化,恢复签名
+watch(
+  () => props.modelValue,
+  (newValue) => {
+    if (newValue) {
+      restoreSignature(newValue);
+    } else {
+      clearSignature();
+    }
+  }
+);
 
-// 对外暴露方法
+// 组件挂载时恢复签名
+onMounted(() => {
+  if (props.modelValue) {
+    restoreSignature(props.modelValue);
+  }
+});
+
+// 暴露方法供父组件调用
 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-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;
+.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;
+}
 
-    &:hover {
-      border-color: #c0c4cc;
-    }
+.signature-canvas {
+  border: 1px dashed #dcdfe6;
+  border-radius: 4px;
+  cursor: crosshair;
+  background-color: #fff;
+  touch-action: none; /* 防止触摸时页面滚动 */
+}
 
-    .signature-canvas {
-      display: block;
-      width: 100%;
-      cursor: crosshair;
-      touch-action: none;
-    }
+.signature-canvas:hover {
+  border-color: #409eff;
+}
 
-    .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>

+ 28 - 3
web/src/views/rs/overseas/approval.vue

@@ -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>

+ 24 - 2
web/src/views/rs/overseas/itinerary-change-approval.vue

@@ -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>