From 9af67076a10a91da11d92982b9688bb073ba6530 Mon Sep 17 00:00:00 2001 From: weizhong Date: Mon, 3 Nov 2025 15:32:19 +0800 Subject: [PATCH] =?UTF-8?q?=E2=80=9C=E5=AE=8C=E6=88=90=E5=87=BA=E5=9B=BD?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E5=AE=A1=E6=89=B9=E6=B5=81=E7=A8=8B11.3?= =?UTF-8?q?=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RsOverseasApplicationController.java | 73 + .../RsOverseasRecordController.java | 15 + .../rs/domain/RsOverseasApplication.java | 6 + .../rs/domain/bo/RsOverseasApplicationBo.java | 5 + .../query/RsOverseasApplicationQuery.java | 5 + .../rs/domain/vo/RsOverseasApplicationVo.java | 5 + .../IRsOverseasApplicationService.java | 14 + .../RsOverseasApplicationServiceImpl.java | 17 + .../mapper/rs/RsOverseasApplicationMapper.xml | 16 +- web/src/api/rs/overseas.ts | 6 +- web/src/views/rs/overseas/approval.vue | 73 +- web/src/views/rs/overseas/index.vue | 2082 +++++++++-------- 12 files changed, 1338 insertions(+), 979 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java index 93e4606f..a02dff6d 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java @@ -550,6 +550,79 @@ public class RsOverseasApplicationController extends BaseController { } } + // ==================== 党籍保留附件上传接口 ==================== + + /** + * 上传党籍保留附件 + */ + @SaCheckPermission("rs:overseas:add") + @PostMapping("/uploadPartyRetentionFile") + @Log(title = "上传党籍保留附件", businessType = BusinessType.OTHER) + public R> uploadPartyRetentionFile(@RequestParam("file") MultipartFile file, + @RequestParam("applicationId") Long applicationId) { + try { + // 验证文件格式 + String contentType = file.getContentType(); + if (!isValidFileType(contentType)) { + return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件"); + } + + // 验证文件大小(10MB) + long maxSize = 10 * 1024 * 1024; // 10MB + if (file.getSize() > maxSize) { + return R.fail("文件大小超过10MB限制"); + } + + // 使用系统OSS服务上传文件(自动保存到sys_oss表) + SysOssVo ossResult = ossService.upload(file); + + // 更新申请记录 - 存储OSS ID和文件路径 + applicationService.updatePartyRetentionFileInfo( + applicationId, + ossResult.getUrl(), // 文件URL + ossResult.getOriginalName(), // 原始文件名 + new Date(), // 上传时间 + "1", // 上传状态:已上传 + ossResult.getOssId() // OSS文件ID + ); + + // 返回结果 + Map result = new HashMap<>(); + result.put("filePath", ossResult.getUrl()); + result.put("fileName", ossResult.getOriginalName()); + result.put("uploadTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + result.put("ossId", ossResult.getOssId()); + + return R.ok("党籍保留附件上传成功", result); + + } catch (Exception e) { + e.printStackTrace(); + return R.fail("系统异常:" + e.getMessage()); + } + } + + /** + * 下载党籍保留附件(安全下载) + */ + @SaCheckPermission("rs:overseas:query") + @GetMapping("/downloadPartyRetentionFile/{applicationId}") + public void downloadPartyRetentionFile(@PathVariable Long applicationId, HttpServletResponse response) { + try { + RsOverseasApplicationVo application = applicationService.queryById(applicationId); + if (application == null || application.getPartyRetentionFileOssId() == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}"); + return; + } + + // 使用系统OSS服务进行安全下载 + ossService.secureDownload(application.getPartyRetentionFileOssId(), response); + } catch (Exception e) { + e.printStackTrace(); + } + } + /** * 验证文件类型 */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java index 6dc50fe1..3d414703 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java @@ -30,6 +30,7 @@ import org.dromara.rs.domain.bo.RsOverseasRecordBo; import org.dromara.rs.domain.vo.RsOverseasRecordVo; import org.dromara.rs.domain.query.RsOverseasRecordQuery; import org.dromara.rs.service.IRsOverseasRecordService; +import org.dromara.rs.service.IRegistrationFormService; import org.dromara.rs.util.OverseasReceiptPdfUtil; import java.io.IOException; @@ -50,6 +51,9 @@ public class RsOverseasRecordController extends BaseController { @Autowired private IRsOverseasRecordService recordService; + @Autowired + private IRegistrationFormService registrationFormService; + /** * 查询出国记录列表 */ @@ -100,6 +104,17 @@ public class RsOverseasRecordController extends BaseController { ExcelUtil.exportExcel(list, "出国记录数据", RsOverseasRecordVo.class, response); } + /** + * 下载党籍保留申请PDF模板 + * 注意:此接口必须放在 /{recordId} 之前,避免路由冲突 + */ + @SaCheckPermission("rs:overseas:query") + @Log(title = "下载党籍保留申请模板", businessType = BusinessType.EXPORT) + @GetMapping("/downloadPartyRetention") + public void downloadPartyRetention(HttpServletResponse response) { + registrationFormService.downloadPartyMembershipTemplate(response); + } + /** * 获取出国记录详细信息 */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java index d6e47f3a..55d544e1 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java @@ -315,6 +315,12 @@ public class RsOverseasApplication extends BaseEntity { @ExcelProperty(value = "应急联系人及电话") private String emergencyContact; + /** + * 附件OSS ID列表(逗号分隔) + */ + @ExcelProperty(value = "附件ID列表") + private String attachmentIds; + /** * 手写签字(base64格式) */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java index 213a6d3d..6f97b7d7 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java @@ -251,6 +251,11 @@ public class RsOverseasApplicationBo extends BaseEntity { */ private String emergencyContact; + /** + * 附件OSS ID列表(逗号分隔) + */ + private String attachmentIds; + /** * 手写签字(base64格式) */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java index 2476f2f1..9efec295 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/query/RsOverseasApplicationQuery.java @@ -33,6 +33,11 @@ public class RsOverseasApplicationQuery extends PageQuery { */ private String purpose; + /** + * 出境事由 + */ + private String exitReason; + /** * 目的地国家 */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java index ad716e09..8e722a56 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java @@ -297,6 +297,11 @@ public class RsOverseasApplicationVo implements Serializable { */ private String emergencyContact; + /** + * 附件OSS ID列表(逗号分隔) + */ + private String attachmentIds; + /** * 手写签字(base64格式) */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java index 605890ee..cbdc7557 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java @@ -195,6 +195,20 @@ public interface IRsOverseasApplicationService { Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName, java.util.Date uploadTime, String uploadStatus, Long ossId); + /** + * 更新党籍保留附件信息 + * + * @param applicationId 申请ID + * @param filePath 文件路径 + * @param fileName 文件名 + * @param uploadTime 上传时间 + * @param uploadStatus 上传状态 + * @param ossId OSS文件ID + * @return 是否成功 + */ + Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName, + java.util.Date uploadTime, String uploadStatus, Long ossId); + /** * 校验出国登记前置条件 * 检查近3个月内是否有已通过的出国登记记录 diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java index 6e885517..e2c957eb 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java @@ -983,6 +983,23 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl 0; } + /** + * 更新党籍保留附件信息 + */ + @Override + @Transactional + public Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName, + java.util.Date uploadTime, String uploadStatus, Long ossId) { + return baseMapper.update(null, + new LambdaUpdateWrapper() + .eq(RsOverseasApplication::getApplicationId, applicationId) + .set(RsOverseasApplication::getPartyRetentionFilePath, filePath) + .set(RsOverseasApplication::getPartyRetentionFileName, fileName) + .set(RsOverseasApplication::getPartyRetentionUploadTime, uploadTime) + .set(RsOverseasApplication::getPartyRetentionUploadStatus, uploadStatus) + .set(RsOverseasApplication::getPartyRetentionFileOssId, ossId)) > 0; + } + /** * 校验出国登记前置条件 * 检查当前自然年内是否有已通过的出国登记记录 diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml index fe8e0e65..7770ed92 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml @@ -52,12 +52,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + @@ -89,9 +95,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" oa.start_date, oa.end_date, oa.duration, oa.invitation, oa.itinerary, oa.accommodation_plan, oa.transportation_plan, oa.status, oa.approval_status, oa.current_approver, oa.approval_level, oa.apply_time, oa.approval_time, oa.admin_remark, oa.applicant_remark, - oa.emergency_contact, oa.signature, + oa.emergency_contact, oa.attachment_ids, oa.signature, oa.stamped_registration_file_path, oa.stamped_registration_file_name, oa.stamped_file_upload_time, oa.stamped_file_upload_status, oa.stamped_file_oss_id, + oa.party_retention_file_path, oa.party_retention_file_name, + oa.party_retention_upload_time, oa.party_retention_upload_status, oa.party_retention_file_oss_id, oa.tenant_id, oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted, CASE WHEN oa.status IS NULL OR oa.status = '' THEN '草稿' @@ -199,6 +207,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND oa.purpose = #{purpose} + + AND oa.exit_reason = #{exitReason} + AND oa.destination like concat('%', #{destination}, '%') @@ -257,6 +268,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND oa.purpose = #{purpose} + + AND oa.exit_reason = #{exitReason} + AND oa.destination like concat('%', #{destination}, '%') diff --git a/web/src/api/rs/overseas.ts b/web/src/api/rs/overseas.ts index f6f2be5d..d89aff46 100644 --- a/web/src/api/rs/overseas.ts +++ b/web/src/api/rs/overseas.ts @@ -6,7 +6,7 @@ export interface OverseasApplicationQuery { pageSize: number; applicantName?: string; department?: string; - purpose?: string; + exitReason?: string; destination?: string; status?: string; approvalLevel?: string; @@ -96,6 +96,8 @@ export interface OverseasApplicationVO { approvalStatus?: string; // 工作流审批状态:draft/waiting/finish/back/cancel approvalStatusText?: string; // 工作流审批状态文本 taskId?: string; // 当前待办任务ID(用于审批跳转) + // 附件相关字段 + attachmentIds?: string; // 附件OSS ID列表(逗号分隔) } // 出国申请表单类型 @@ -150,6 +152,8 @@ export interface OverseasApplicationForm { applicantRemark?: string; // 工作流相关字段 approvalStatus?: string; // 工作流审批状态 + // 附件相关字段 + attachmentIds?: string; // 附件OSS ID列表(逗号分隔) } // 审批请求类型(支持标准工作流模式) diff --git a/web/src/views/rs/overseas/approval.vue b/web/src/views/rs/overseas/approval.vue index 0d13724a..b8171c2c 100644 --- a/web/src/views/rs/overseas/approval.vue +++ b/web/src/views/rs/overseas/approval.vue @@ -7,39 +7,20 @@ - - - - - - - - - - - - + + - - - - - - - - + + + + @@ -79,8 +60,14 @@ > - - + + + + + + @@ -1365,6 +1442,11 @@ const attachmentRules = { file: [{ required: true, message: '请选择要上传的文件', trigger: 'change' }] }; +// 申请附件上传相关(新增 - 参考培训申请模式) +const applicationAttachmentFileList = ref([]); +const baseApi = import.meta.env.VITE_APP_BASE_API || '/dev-api'; +const clientId = import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'; + const state = reactive({ // 选项卡相关 activeTab: 'application', @@ -1378,8 +1460,7 @@ const state = reactive({ pageNum: 1, pageSize: 10, destination: '', - status: '', - purpose: '' + exitReason: '' } as OverseasApplicationQuery, // 记录列表相关 @@ -1390,15 +1471,16 @@ const state = reactive({ recordQueryParams: { pageNum: 1, pageSize: 10, - applicantName: '', destination: '', + exitReason: '', completionStatus: '' } as OverseasRecordQuery, // 表单相关 applicationDialog: { visible: false, - title: '' + title: '', + key: 0 // 添加key用于强制重新渲染弹窗 }, applicationForm: {} as OverseasApplicationForm, isResubmitMode: false, // 标记是否为重新提交模式 @@ -1448,7 +1530,7 @@ const state = reactive({ changeHistoryLoading: false, changeHistoryList: [] as any[], - // 文件通知阅读弹窗 + // 文件通知阅读弹窗(第一步:学院文件) noticeDialog: { visible: false, title: '杭州第一技师学院文件' @@ -1457,6 +1539,15 @@ const state = reactive({ countdownActive: false, // 倒计时是否激活 readCompleted: false, // 是否已阅读完成 + // 承诺书阅读弹窗(第二步:承诺书) + commitmentDialog: { + visible: false, + title: '教职工因私出国(境)遵守党纪条规承诺书' + }, + commitmentCountdown: 5, // 承诺书倒计时秒数 + commitmentCountdownActive: false, // 承诺书倒计时是否激活 + commitmentReadCompleted: false, // 承诺书是否已阅读完成 + // 字典数据 statusOptions: [ { label: '待审核', value: '0' }, @@ -1474,6 +1565,11 @@ const state = reactive({ { label: '其他', value: 'other' } ], +exitReasonOptions: [ + { label: '旅游', value: 'tourism' }, + { label: '探亲', value: 'visiting_relatives' } +], + completionStatusOptions: [ { label: '进行中', value: '0' }, { label: '已完成', value: '1' }, @@ -1508,6 +1604,7 @@ const { currentApplication, statusOptions, purposeOptions, + exitReasonOptions, completionStatusOptions } = toRefs(state); @@ -1519,6 +1616,15 @@ const uploadHeaders = computed(() => { }; }); +// 获取当前日期(年月日格式) +const getCurrentDate = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}年${month}月${day}日`; +}; + // 计算出国天数(确保至少为1天,符合后端验证要求) const calculateDuration = () => { if (state.applicationForm.startDate && state.applicationForm.endDate) { @@ -1653,8 +1759,7 @@ const resetApplicationQuery = () => { pageNum: 1, pageSize: 10, destination: '', - status: '', - purpose: '', + exitReason: '', params: {} }); getApplicationList(); @@ -1667,8 +1772,8 @@ const resetRecordQuery = () => { Object.assign(state.recordQueryParams, { pageNum: 1, pageSize: 10, - applicantName: '', destination: '', + exitReason: '', completionStatus: '', params: {} }); @@ -1698,14 +1803,20 @@ const startCountdown = () => { }, 1000); }; -// 关闭文件通知弹窗并打开申请表单 +// 关闭第一个文件通知弹窗并打开第二个承诺书弹窗 const handleReadComplete = () => { state.noticeDialog.visible = false; - // 重置申请表单并打开申请对话框 - resetApplicationForm(); - state.applicationDialog.title = '新增因私出国(境)申请登记报备表'; - state.pageType = 'add'; // 设置页面类型为新增 - state.applicationDialog.visible = true; + // 重置第一个对话框状态 + state.countdown = 5; + state.countdownActive = false; + state.readCompleted = false; + + // 打开第二个承诺书对话框 + state.commitmentDialog.visible = true; + state.commitmentCountdown = 5; + state.commitmentCountdownActive = true; + state.commitmentReadCompleted = false; + startCommitmentCountdown(); }; // 关闭文件通知弹窗 @@ -1717,6 +1828,46 @@ const handleCloseNotice = () => { state.readCompleted = false; }; +// 承诺书倒计时 +const startCommitmentCountdown = () => { + const timer = setInterval(() => { + if (state.commitmentCountdown > 0) { + state.commitmentCountdown--; + } else { + clearInterval(timer); + state.commitmentCountdownActive = false; + state.commitmentReadCompleted = true; + } + }, 1000); +}; + +// 承诺书阅读完成,打开申请表单 +const handleCommitmentReadComplete = () => { + state.commitmentDialog.visible = false; + // 重置承诺书对话框状态 + state.commitmentCountdown = 5; + state.commitmentCountdownActive = false; + state.commitmentReadCompleted = false; + + // 重置申请表单并打开申请对话框 + resetApplicationForm(); + state.applicationDialog.title = '新增因私出国(境)申请登记报备表'; + state.pageType = 'add'; // 设置页面类型为新增 + // 更新弹窗key,确保每次打开都是全新的组件 + state.applicationDialog.key = Date.now(); + console.log('🆕 [新增申请] 打开弹窗,更新key:', state.applicationDialog.key); + state.applicationDialog.visible = true; +}; + +// 关闭承诺书弹窗 +const handleCloseCommitment = () => { + state.commitmentDialog.visible = false; + // 重置状态 + state.commitmentCountdown = 5; + state.commitmentCountdownActive = false; + state.commitmentReadCompleted = false; +}; + // 编辑申请 const handleUpdateApplication = async (row: OverseasApplicationVO) => { try { @@ -1725,6 +1876,9 @@ const handleUpdateApplication = async (row: OverseasApplicationVO) => { state.applicationForm = response.data; state.applicationDialog.title = '编辑因私出国(境)申请登记报备表'; state.pageType = 'edit'; + // 更新弹窗key,确保每次打开都是全新的组件 + state.applicationDialog.key = Date.now(); + console.log('✏️ [编辑申请] 打开弹窗,更新key:', state.applicationDialog.key); state.applicationDialog.visible = true; } catch (error) { console.error('加载申请详情失败:', error); @@ -1738,28 +1892,28 @@ const handleViewApplication = async (row: OverseasApplicationVO) => { try { const response = await getOverseasApplication(row.applicationId); console.log('✅ [查看详情] 申请详情获取成功:', response.data); - + // 添加调试日志:检查党籍保留附件字段 console.log('🔍 [党籍附件] 检查党籍保留附件字段:'); console.log(' - partyRetentionUploadStatus:', response.data.partyRetentionUploadStatus); console.log(' - partyRetentionFileName:', response.data.partyRetentionFileName); console.log(' - partyRetentionFileOssId:', response.data.partyRetentionFileOssId); console.log(' - partyRetentionUploadTime:', response.data.partyRetentionUploadTime); - + // 添加调试日志:检查盖章文件字段 console.log('🔍 [盖章文件] 检查盖章文件字段:'); console.log(' - stampedFileUploadStatus:', response.data.stampedFileUploadStatus); console.log(' - stampedRegistrationFileName:', response.data.stampedRegistrationFileName); console.log(' - stampedFileOssId:', response.data.stampedFileOssId); console.log(' - stampedFileUploadTime:', response.data.stampedFileUploadTime); - + state.viewData = response.data; state.viewDialog.title = '出国申请详情'; state.viewDialog.visible = true; - + console.log('🔄 [查看详情] 开始加载附件列表...'); - // 加载附件列表 - await loadViewAttachmentList(row.applicationId); + // 加载附件列表(传入attachmentIds而不是applicationId) + await loadViewAttachmentList(response.data.attachmentIds); console.log('✅ [查看详情] 附件列表加载完成'); } catch (error) { console.error('❌ [查看详情] 获取申请详情失败:', error); @@ -1799,6 +1953,9 @@ const handleUpdateAndResubmitApplication = async (row: OverseasApplicationVO) => state.applicationDialog.title = '修改并重新提交因私出国(境)申请'; state.pageType = 'resubmit'; state.isResubmitMode = true; // 标记为重新提交模式 + // 更新弹窗key,确保每次打开都是全新的组件 + state.applicationDialog.key = Date.now(); + console.log('🔄 [重新提交] 打开弹窗,更新key:', state.applicationDialog.key); state.applicationDialog.visible = true; } catch (error) { console.error('加载申请详情失败:', error); @@ -1949,12 +2106,12 @@ const getOldApprovalStatusTagType = (status: string): 'success' | 'warning' | 'd */ const getApprovalStatusTagType = (status: string): 'success' | 'warning' | 'danger' | 'info' | 'primary' => { const statusMap: Record = { - 'cancel': 'info', // 已撤销 - 灰色 - 'draft': 'info', // 草稿 - 灰色 - 'waiting': 'warning', // 待审核 - 橙色 - 'finish': 'success', // 已完成 - 绿色 - 'invalid': 'danger', // 已作废 - 红色 - 'back': 'danger', // 已退回 - 红色 + 'cancel': 'info', // 已撤销 - 灰色 + 'draft': 'info', // 草稿 - 灰色 + 'waiting': 'warning', // 待审核 - 橙色 + 'finish': 'success', // 已完成 - 绿色 + 'invalid': 'danger', // 已作废 - 红色 + 'back': 'danger', // 已退回 - 红色 'termination': 'primary' // 已终止 - 蓝色 }; return statusMap[status] || 'info'; @@ -1965,19 +2122,24 @@ const getApprovalStatusTagType = (status: string): 'success' | 'warning' | 'dang */ const handleApproval = (row: OverseasApplicationVO) => { console.log('📋 [审批] 打开审批页面,申请ID:', row.applicationId, '任务ID:', row.taskId); - + // 设置页面类型为审批模式 state.pageType = 'approval'; state.applicationDialog.title = '出国申请审批'; - + // 加载申请详情 - getOverseasApplication(row.applicationId).then((response: any) => { - state.applicationForm = response.data; - state.applicationDialog.visible = true; - }).catch((error: any) => { - console.error('❌ [审批] 加载申请详情失败:', error); - ElMessage.error('加载申请详情失败'); - }); + getOverseasApplication(row.applicationId) + .then((response: any) => { + state.applicationForm = response.data; + // 更新弹窗key,确保每次打开都是全新的组件 + state.applicationDialog.key = Date.now(); + console.log('👍 [审批] 打开弹窗,更新key:', state.applicationDialog.key); + state.applicationDialog.visible = true; + }) + .catch((error: any) => { + console.error('❌ [审批] 加载申请详情失败:', error); + ElMessage.error('加载申请详情失败'); + }); }; /** @@ -1985,7 +2147,7 @@ const handleApproval = (row: OverseasApplicationVO) => { */ const handleViewProcess = (row: OverseasApplicationVO) => { console.log('📊 [查看流程] 打开流程图,申请ID:', row.applicationId); - + if (approvalRecordRef.value && row.applicationId) { // 使用标准工作流组件查看审批记录 approvalRecordRef.value.open(row.applicationId, 'dyjs-cgsq'); @@ -2000,23 +2162,23 @@ const handleViewProcess = (row: OverseasApplicationVO) => { */ const submitForm = async (type: string) => { console.log('📝 [提交表单] 类型:', type); - + // 验证表单 if (!applicationFormRef.value) { ElMessage.error('表单引用不存在'); return; } - + const valid = await applicationFormRef.value.validate().catch(() => false); if (!valid) { ElMessage.warning('请正确填写所有必填项'); return; } - + try { // 1. 保存业务数据 let applicationId = state.applicationForm.applicationId; - + if (applicationId) { // 更新 await updateOverseasApplication(state.applicationForm); @@ -2026,12 +2188,12 @@ const submitForm = async (type: string) => { applicationId = response.data; state.applicationForm.applicationId = applicationId; } - + // 2. 如果是提交审批,使用标准工作流API if (type === 'submit') { // 动态获取审批人信息 let departmentApproverId: number | null = null; - + try { // 获取部门审批人ID const deptApproverResponse = await getDepartmentApproverId(state.applicationForm.applicantId || userStore.userId); @@ -2048,13 +2210,13 @@ const submitForm = async (type: string) => { ElMessage.error('获取部门审批人信息失败,请稍后重试'); return; } - + // 注意:人事处审批通过角色配置,不需要动态获取审批人 console.log('人事处审批人通过hr_dept_role角色配置,无需动态获取'); - + // 标准工作流启动参数 const workflowData = { - flowCode: 'dyjs-cgsq', // 出国申请工作流标识KEY + flowCode: 'dyjs-cgsq', // 出国申请工作流标识KEY businessId: String(applicationId), variables: { // dyjs-cgsq 使用的流程变量 @@ -2064,18 +2226,18 @@ const submitForm = async (type: string) => { duration: state.applicationForm.duration, budget: state.applicationForm.budget, // 动态审批人变量 - bmfzrsp: departmentApproverId // 部门负责人审批(对应工作流中的入库主键) + bmfzrsp: departmentApproverId // 部门负责人审批(对应工作流中的入库主键) } }; - + // 启动工作流 const workflowResponse = await startWorkFlow(workflowData); - + // 打开审批确认对话框 if (submitVerifyRef.value && workflowResponse.data?.taskId) { submitVerifyRef.value.openDialog(workflowResponse.data.taskId); } - + ElMessage.success('申请已提交,进入审批流程'); state.applicationDialog.visible = false; getApplicationList(); @@ -2096,7 +2258,7 @@ const submitForm = async (type: string) => { */ const approvalVerifyOpen = (taskId: string) => { console.log('🔍 [审批确认] 打开审批对话框, taskId:', taskId); - + if (submitVerifyRef.value && taskId) { submitVerifyRef.value.openDialog(taskId); } else { @@ -2109,7 +2271,7 @@ const approvalVerifyOpen = (taskId: string) => { */ const handleApprovalRecord = () => { console.log('📜 [审批记录] 打开审批记录'); - + if (approvalRecordRef.value && state.applicationForm.applicationId) { approvalRecordRef.value.open(state.applicationForm.applicationId, 'dyjs-cgsq'); } else { @@ -2132,7 +2294,7 @@ const resetApplicationForm = () => { applicationFormRef.value?.resetFields(); state.applicationForm = { applicantId: userStore.userId as number, - applicantName: userStore.name, + applicantName: '', // 修改为手动填写,不再自动填充 department: userStore.deptName, contactPhone: userStore.phonenumber, mobilePhone: '', // 需要用户手动输入 @@ -2167,10 +2329,19 @@ const resetApplicationForm = () => { stampedRegistrationFileName: '', stampedFileUploadTime: '', stampedFileUploadStatus: '0', + // 党籍保留附件相关字段(重要:取消时必须清空) + partyRetentionFilePath: '', + partyRetentionFileName: '', + partyRetentionUploadTime: '', + partyRetentionUploadStatus: '0', // budget 字段已删除,但保留默认值避免类型错误 budget: 0 }; - + + // ✅ 清空党籍保留附件列表(关键修复!) + state.partyRetentionFileList = []; + console.log('🧹 [重置表单] 已清空党籍保留附件列表'); + // 重置签名画布 nextTick(() => { if (signatureCanvas.value) { @@ -2185,7 +2356,7 @@ const submitApplicationForm = () => { // 党籍保留附件上传检查已移除(不再作为必填项) // const isPartyMember = state.applicationForm.politicalStatus === '1' || state.applicationForm.politicalStatus === '中共党员'; // const needRetention = isPartyMember && state.applicationForm.duration && state.applicationForm.duration > 30; - // + // // if (needRetention) { // if (!state.applicationForm.partyRetentionUploadStatus || state.applicationForm.partyRetentionUploadStatus === '0') { // ElMessage.warning('您是中共党员且出国天数超过30天,必须上传党籍保留盖章附件才能提交申请!'); @@ -2195,18 +2366,15 @@ const submitApplicationForm = () => { // 前端调用后端校验接口(已禁用出国登记前置条件校验) try { - const validation = await validateApplicationPrerequisites( - state.applicationForm.politicalStatus, - state.applicationForm.duration - ); - + const validation = await validateApplicationPrerequisites(state.applicationForm.politicalStatus, state.applicationForm.duration); + // 检查出国登记前置条件(已禁用 - 允许直接提交申请) // const registrationValidation = validation.data.registrationValidation; // if (!registrationValidation.isValid) { // ElMessage.error(registrationValidation.message); // return; // } - + // 检查党籍保留前置条件(如果有返回结果) const retentionValidation = validation.data.retentionValidation; if (retentionValidation && !retentionValidation.isValid) { @@ -2224,13 +2392,13 @@ const submitApplicationForm = () => { console.log('原始documentNumber:', state.applicationForm.documentNumber); console.log('原始idNumber:', state.applicationForm.idNumber); console.log('原始politicalStatus:', state.applicationForm.politicalStatus); - + // 1. 将destinationCountry的值同步到destination字段 if (state.applicationForm.destinationCountry) { state.applicationForm.destination = state.applicationForm.destinationCountry; console.log('✅ destination已同步:', state.applicationForm.destination); } - + // 2. 处理身份证号:从证件号码映射(如果证件是身份证) if (!state.applicationForm.idNumber) { // 如果idNumber为空,需要处理 @@ -2252,22 +2420,17 @@ const submitApplicationForm = () => { } else { console.log('✅ idNumber已存在,无需映射:', state.applicationForm.idNumber); } - + // 3. 处理户籍所在地:如果为空,使用部门信息作为默认值 if (!state.applicationForm.registeredAddress) { state.applicationForm.registeredAddress = state.applicationForm.department || '待完善'; console.log('✅ registeredAddress已映射:', state.applicationForm.registeredAddress); } - // 4. 处理行程安排:如果为空,自动生成默认内容 + // 4. 处理行程安排:保持用户原始输入,不再自动生成(用户要求:未填写则显示"未填写") if (!state.applicationForm.itinerary || state.applicationForm.itinerary.trim() === '') { - const startDate = state.applicationForm.startDate || '待定'; - const endDate = state.applicationForm.endDate || '待定'; - const destination = state.applicationForm.destination || state.applicationForm.destinationCountry || '待定'; - const exitReason = state.applicationForm.exitReason || '因公出国'; - - state.applicationForm.itinerary = `${exitReason},计划于${startDate}至${endDate}期间前往${destination},具体行程安排待确定。`; - console.log('✅ itinerary已自动生成:', state.applicationForm.itinerary); + state.applicationForm.itinerary = '未填写'; + console.log('✅ itinerary未填写,设置为"未填写"'); } else { console.log('✅ itinerary已存在:', state.applicationForm.itinerary); } @@ -2279,26 +2442,25 @@ const submitApplicationForm = () => { } else { console.log('✅ duration已存在:', state.applicationForm.duration); } - + // 7. 处理人员类别代码:根据政治面貌判断,默认为普通工作人员 if (!state.applicationForm.personnelCategory) { // 中共党员可能属于备案人员,其他为普通人员 - const isPartyMember = state.applicationForm.politicalStatus === '1' || - state.applicationForm.politicalStatus === '中共党员'; + const isPartyMember = state.applicationForm.politicalStatus === '1' || state.applicationForm.politicalStatus === '中共党员'; state.applicationForm.personnelCategory = isPartyMember ? '01' : '99'; // 01=党员干部,99=其他人员 console.log('✅ personnelCategory已映射:', state.applicationForm.personnelCategory, '(政治面貌:', state.applicationForm.politicalStatus, ')'); } - + // unitContact 字段已删除 - + // 5. 清理空的身份证号字段(避免后端格式校验) if (!state.applicationForm.idNumber || state.applicationForm.idNumber.trim() === '') { delete state.applicationForm.idNumber; console.log('⚠️ idNumber字段已删除(为空或无效)'); } - + // budget 和 budgetSource 字段已删除,无需设置 - + // 打印最终提交的数据(关键字段) console.log('=== 最终提交数据(关键字段) ==='); console.log({ @@ -2308,7 +2470,7 @@ const submitApplicationForm = () => { itinerary: state.applicationForm.itinerary, duration: state.applicationForm.duration, personnelCategory: state.applicationForm.personnelCategory, - emergencyContact: state.applicationForm.emergencyContact // 添加应急联系人字段 + emergencyContact: state.applicationForm.emergencyContact // 添加应急联系人字段 }); // 权限检查 @@ -2366,36 +2528,47 @@ const submitApplicationForm = () => { }; const cancelApplicationForm = () => { - state.applicationDialog.visible = false; - state.isResubmitMode = false; // 重置重新提交模式 + console.log('🚫 [取消表单] 关闭弹窗并重置表单'); + + // 1. 先重置表单数据 resetApplicationForm(); + + // 2. 延迟关闭弹窗,确保数据重置完成后再关闭 + nextTick(() => { + state.applicationDialog.visible = false; + state.isResubmitMode = false; // 重置重新提交模式 + + // 3. 更新弹窗key,强制Vue重新创建弹窗组件(解决附件缓存问题) + state.applicationDialog.key = Date.now(); + console.log('🔄 [取消表单] 更新弹窗key,强制组件重新渲染:', state.applicationDialog.key); + }); }; // 计算属性:是否显示保留党籍附件按钮 const showPartyMembershipButton = computed(() => { // 首先检查是否为中共党员 const isPartyMember = state.applicationForm.politicalStatus === '1' || state.applicationForm.politicalStatus === '中共党员'; // '1' 对应 "中共党员" - + // 如果不是党员,直接返回false if (!isPartyMember) { return false; } - + // 检查出国天数条件 const startDate = state.applicationForm.startDate; const endDate = state.applicationForm.endDate; - + // 如果开始日期或结束日期不存在,不显示按钮 if (!startDate || !endDate) { return false; } - + // 计算出国天数 const start = new Date(startDate); const end = new Date(endDate); const diffTime = end.getTime() - start.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); // 向上取整 - + // 出国天数大于30天且小于180天才显示保留党籍附件按钮 return diffDays > 30 && diffDays < 180; }); @@ -2403,19 +2576,19 @@ const showPartyMembershipButton = computed(() => { // 计算属性:是否显示党籍保留上传组件(用于控制上传区域显示) const showPartyRetentionUpload = computed(() => { // 首先检查是否为中共党员 - const isPartyMember = state.applicationForm.politicalStatus === '1' || state.applicationForm.politicalStatus === '中共党员'; - + const isPartyMember = state.applicationForm.politicalStatus === '1' || state.applicationForm.politicalStatus === '中共党员'; + // 如果不是党员,直接返回false if (!isPartyMember) { return false; } - + // 检查出国天数 const duration = state.applicationForm.duration; if (!duration || typeof duration === 'string') { return false; } - + // 出国天数大于30天才显示 return Number(duration) > 30; }); @@ -2435,14 +2608,14 @@ watch( const handlePartyMembershipAttachment = async () => { try { ElMessage.info('正在下载保留党籍申请模板...'); - + // 调用API下载模板 const response = await downloadPartyMembershipTemplate(); - + console.log('下载响应完整对象:', response); console.log('响应类型:', typeof response); console.log('响应是否为Blob:', response instanceof Blob); - + // 根据响应拦截器逻辑,blob响应应该直接返回blob数据 // 但由于TypeScript类型定义,我们需要检查实际的数据结构 let blobData; @@ -2462,12 +2635,12 @@ const handlePartyMembershipAttachment = async () => { blobData = response; console.log('直接使用响应数据,类型:', typeof blobData); } - + // 检查数据 if (!blobData) { throw new Error('服务器返回空数据'); } - + // 确保是blob类型 let blob; if (blobData instanceof Blob) { @@ -2478,7 +2651,7 @@ const handlePartyMembershipAttachment = async () => { blob = new Blob([blobData], { type: 'application/pdf' }); console.log('手动创建Blob,大小:', blob.size, 'bytes'); } - + // 检查blob大小 if (blob.size < 100) { console.warn('文件大小异常小:', blob.size, 'bytes'); @@ -2487,7 +2660,7 @@ const handlePartyMembershipAttachment = async () => { console.log('小文件内容:', text); throw new Error(`下载的文件异常小(${blob.size}字节),可能是错误响应: ${text.substring(0, 100)}`); } - + // 创建下载链接 const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); @@ -2497,7 +2670,7 @@ const handlePartyMembershipAttachment = async () => { link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); - + ElMessage.success(`保留党籍申请模板下载成功(${blob.size}字节)`); } catch (error: any) { console.error('下载保留党籍申请模板失败:', error); @@ -2509,14 +2682,14 @@ const handlePartyMembershipAttachment = async () => { const handleRegistrationAttachment = async () => { try { ElMessage.info('正在下载登记备案表模板...'); - + // 调用API下载模板 const response = await downloadRegistrationTemplate(); - + console.log('下载响应完整对象:', response); console.log('响应类型:', typeof response); console.log('响应是否为Blob:', response instanceof Blob); - + // 根据响应拦截器逻辑,blob响应应该直接返回blob数据 // 但由于TypeScript类型定义,我们需要检查实际的数据结构 let blobData; @@ -2536,12 +2709,12 @@ const handleRegistrationAttachment = async () => { blobData = response; console.log('直接使用响应数据,类型:', typeof blobData); } - + // 检查数据 if (!blobData) { throw new Error('服务器返回空数据'); } - + // 确保是blob类型 let blob; if (blobData instanceof Blob) { @@ -2552,7 +2725,7 @@ const handleRegistrationAttachment = async () => { blob = new Blob([blobData], { type: 'application/pdf' }); console.log('手动创建Blob,大小:', blob.size, 'bytes'); } - + // 检查blob大小 if (blob.size < 100) { console.warn('文件大小异常小:', blob.size, 'bytes'); @@ -2561,7 +2734,7 @@ const handleRegistrationAttachment = async () => { console.log('小文件内容:', text); throw new Error(`下载的文件异常小(${blob.size}字节),可能是错误响应: ${text.substring(0, 100)}`); } - + // 创建下载链接 const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); @@ -2571,7 +2744,7 @@ const handleRegistrationAttachment = async () => { link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); - + ElMessage.success(`登记备案表模板下载成功(${blob.size}字节)`); } catch (error: any) { console.error('下载登记备案表模板失败:', error); @@ -2586,7 +2759,7 @@ const lastPoint = ref({ x: 0, y: 0 }); const startDrawing = (event: MouseEvent) => { if (!signatureCanvas.value) return; - + isDrawing.value = true; const rect = signatureCanvas.value.getBoundingClientRect(); lastPoint.value = { @@ -2597,16 +2770,16 @@ const startDrawing = (event: MouseEvent) => { const draw = (event: MouseEvent) => { if (!isDrawing.value || !signatureCanvas.value) return; - + const ctx = signatureCanvas.value.getContext('2d'); if (!ctx) return; - + const rect = signatureCanvas.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); @@ -2615,9 +2788,9 @@ const draw = (event: MouseEvent) => { ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.stroke(); - + lastPoint.value = currentPoint; - + // 保存签名到表单数据 saveSignature(); }; @@ -2630,13 +2803,13 @@ const handleTouch = (event: TouchEvent) => { event.preventDefault(); const touch = event.touches[0]; if (!touch || !signatureCanvas.value) return; - + const rect = signatureCanvas.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') { @@ -2646,17 +2819,17 @@ const handleTouch = (event: TouchEvent) => { const clearSignature = () => { if (!signatureCanvas.value) return; - + const ctx = signatureCanvas.value.getContext('2d'); if (!ctx) return; - + ctx.clearRect(0, 0, signatureCanvas.value.width, signatureCanvas.value.height); state.applicationForm.signature = ''; }; const saveSignature = () => { if (!signatureCanvas.value) return; - + try { const dataURL = signatureCanvas.value.toDataURL('image/png'); state.applicationForm.signature = dataURL; @@ -2667,10 +2840,10 @@ const saveSignature = () => { const restoreSignature = (base64Image: string) => { if (!signatureCanvas.value || !base64Image) return; - + const ctx = signatureCanvas.value.getContext('2d'); if (!ctx) return; - + const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, signatureCanvas.value!.width, signatureCanvas.value!.height); @@ -2679,8 +2852,6 @@ const restoreSignature = (base64Image: string) => { img.src = base64Image; }; - - // 工具方法 const formatDate = (date: string) => { if (!date) return ''; @@ -2751,9 +2922,9 @@ const handleViewRecord = async (row: OverseasRecordVO) => { state.viewData = row; state.viewDialog.visible = true; state.viewDialog.title = '出国记录详情'; - + // 加载该记录的变更历史 - await loadChangeHistory(row.recordId); + await loadChangeHistory(Number(row.recordId)); }; // 加载变更历史记录 @@ -2763,7 +2934,7 @@ const loadChangeHistory = async (recordId: number) => { state.changeHistoryList = []; return; } - + state.changeHistoryLoading = true; try { console.log('🔄 开始加载变更历史记录, recordId:', recordId); @@ -2794,7 +2965,7 @@ const handleDownloadReceipt = async (row: OverseasRecordVO) => { } ElMessage.info('正在生成PDF,请稍候...'); - + // 调用后端API下载PDF const response = await fetch(`${import.meta.env.VITE_APP_BASE_API}/rs/overseas/record/downloadReceipt/${recordId}`, { method: 'GET', @@ -2839,16 +3010,15 @@ const handleDownloadReceipt = async (row: OverseasRecordVO) => { // 判断是否应该显示党籍保留申请下载按钮 const shouldShowPartyRetentionDownload = (application: OverseasApplicationVO) => { if (!application) return false; - + // 检查是否为中共党员 - const isPartyMember = application.politicalStatus === '1' || - application.politicalStatus === '中共党员' || - application.politicalStatusText === '中共党员'; - + const isPartyMember = + application.politicalStatus === '1' || application.politicalStatus === '中共党员' || application.politicalStatusText === '中共党员'; + // 检查出国天数是否大于30天 const duration = Number(application.duration) || 0; const needRetention = duration > 30; - + return isPartyMember && needRetention; }; @@ -2856,7 +3026,7 @@ const shouldShowPartyRetentionDownload = (application: OverseasApplicationVO) => const handleDownloadPartyRetention = async () => { try { ElMessage.info('正在生成PDF,请稍候...'); - + // 使用request工具发送请求,自动包含认证头 const response = await request({ url: '/rs/overseas/record/downloadPartyRetention', @@ -2865,17 +3035,16 @@ const handleDownloadPartyRetention = async () => { }); // 创建blob并触发下载(response已经是blob对象) - const blob = response instanceof Blob - ? new Blob([response], { type: 'application/pdf' }) - : new Blob([response.data], { type: 'application/pdf' }); - + const blob = + response instanceof Blob ? new Blob([response], { type: 'application/pdf' }) : new Blob([response.data], { type: 'application/pdf' }); + console.log('📥 生成的Blob大小:', blob.size, 'bytes'); console.log('📥 Blob类型:', blob.type); - + if (blob.size === 0) { throw new Error('下载的文件为空'); } - + const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; @@ -2913,88 +3082,17 @@ const beforePartyRetentionFileUpload = async (file: File) => { ElMessage.error('只支持PDF、JPG、PNG格式的文件!'); return false; } - + // 检查文件大小(10MB) const isValidSize = file.size / 1024 / 1024 < 10; if (!isValidSize) { ElMessage.error('文件大小不能超过10MB!'); return false; } - - // 🔴 关键修复:如果是新增模式(没有applicationId),先保存申请记录 - if (!state.applicationForm.applicationId) { - try { - ElMessage.info('正在保存申请记录...'); - - // 执行必要的字段映射(日期格式转换) - if (state.applicationForm.startDate) { - state.applicationForm.startDate = dayjs(state.applicationForm.startDate).format('YYYY-MM-DD'); - } - if (state.applicationForm.endDate) { - state.applicationForm.endDate = dayjs(state.applicationForm.endDate).format('YYYY-MM-DD'); - } - if (state.applicationForm.birthDate) { - state.applicationForm.birthDate = dayjs(state.applicationForm.birthDate).format('YYYY-MM-DD'); - } - // actualStartDate 和 actualEndDate 字段不属于申请表单,已删除 - - // 🔴 关键修复:字段映射(参考submitApplicationForm的逻辑) - // 1. 同步destination字段 - if (state.applicationForm.destinationCountry) { - state.applicationForm.destination = state.applicationForm.destinationCountry; - } - - // 2. 处理身份证号(如果证件是身份证) - if (!state.applicationForm.idNumber && state.applicationForm.documentNumber) { - state.applicationForm.idNumber = state.applicationForm.documentNumber; - } - - // 3. 处理户籍所在地(默认使用部门名称) - if (!state.applicationForm.registeredAddress) { - state.applicationForm.registeredAddress = state.applicationForm.department || '待填写'; - } - - // 4. 处理行程安排(自动生成) - if (!state.applicationForm.itinerary || state.applicationForm.itinerary.trim() === '') { - const startDate = state.applicationForm.startDate || '待定'; - const endDate = state.applicationForm.endDate || '待定'; - const destination = state.applicationForm.destination || state.applicationForm.destinationCountry || '待定'; - const exitReason = state.applicationForm.exitReason || '因公出国'; - state.applicationForm.itinerary = `${exitReason},计划于${startDate}至${endDate}期间前往${destination},具体行程安排待确定。`; - } - - // 5. 处理出国天数 - if (!state.applicationForm.duration || state.applicationForm.duration < 1) { - state.applicationForm.duration = 1; - } - - // 6. 处理人员类别代码 - if (!state.applicationForm.personnelCategory) { - const isPartyMember = state.applicationForm.politicalStatus === '1' || - state.applicationForm.politicalStatus === '中共党员'; - state.applicationForm.personnelCategory = isPartyMember ? '01' : '99'; - } - - // 保存申请记录(草稿状态) - const response = await addOverseasApplication(state.applicationForm); - - // 🔴 重要:从响应中获取新创建的applicationId - if (response.data && response.data.applicationId) { - state.applicationForm.applicationId = response.data.applicationId; - ElMessage.success('申请记录已保存,现在可以上传党籍保留附件了'); - } else { - ElMessage.error('保存申请记录失败,无法获取申请ID'); - return false; - } - - return true; - } catch (error) { - console.error('保存申请记录失败:', error); - ElMessage.error('保存申请记录失败,请先手动保存'); - return false; - } - } - + + // 允许在新增模式下上传附件 + // 附件先上传到服务器,只有点击"提交"按钮时才会保存申请记录 + // 点击"取消"按钮时,申请记录不会被保存 return true; }; @@ -3006,7 +3104,7 @@ const handlePartyRetentionFileUploadSuccess = (response: any, file: any) => { state.applicationForm.partyRetentionFileName = response.data.fileName; state.applicationForm.partyRetentionUploadTime = response.data.uploadTime; state.applicationForm.partyRetentionUploadStatus = '1'; - + ElMessage.success('党籍保留附件上传成功!'); } else { ElMessage.error(response.msg || '上传失败'); @@ -3026,10 +3124,10 @@ const handlePartyRetentionFileRemove = () => { state.applicationForm.partyRetentionFileName = ''; state.applicationForm.partyRetentionUploadTime = ''; state.applicationForm.partyRetentionUploadStatus = '0'; - + // 清空文件列表(重要:解决删除后文件仍显示的问题) state.partyRetentionFileList = []; - + ElMessage.info('已移除党籍保留附件'); }; @@ -3109,10 +3207,8 @@ const handleDownloadPartyAttachment = async (file: any) => { if (file.raw) { console.log('下载刚上传的文件(使用raw对象)'); try { - const fileBlob = fileName.toLowerCase().endsWith('.pdf') - ? new Blob([file.raw], { type: 'application/pdf' }) - : file.raw; - + const fileBlob = fileName.toLowerCase().endsWith('.pdf') ? new Blob([file.raw], { type: 'application/pdf' }) : file.raw; + const url = URL.createObjectURL(fileBlob); const link = document.createElement('a'); link.href = url; @@ -3121,11 +3217,11 @@ const handleDownloadPartyAttachment = async (file: any) => { document.body.appendChild(link); link.click(); document.body.removeChild(link); - + setTimeout(() => { URL.revokeObjectURL(url); }, 100); - + ElMessage.success('文件下载成功'); return; } catch (error) { @@ -3144,15 +3240,18 @@ const handleDownloadPartyAttachment = async (file: any) => { // 从服务器下载已保存的文件 try { console.log('从服务器下载已保存的文件,applicationId:', state.applicationForm.applicationId); - - const response = await fetch(import.meta.env.VITE_APP_BASE_API + '/rs/overseas/application/downloadPartyRetentionFile/' + state.applicationForm.applicationId, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${getToken() || ''}`, - 'clientid': import.meta.env.VITE_APP_CLIENT_ID || '', - 'referer': window.location.origin, + + const response = await fetch( + import.meta.env.VITE_APP_BASE_API + '/rs/overseas/application/downloadPartyRetentionFile/' + state.applicationForm.applicationId, + { + method: 'GET', + headers: { + 'Authorization': `Bearer ${getToken() || ''}`, + 'clientid': import.meta.env.VITE_APP_CLIENT_ID || '', + 'referer': window.location.origin + } } - }); + ); if (!response.ok) { const errorText = await response.text(); @@ -3165,17 +3264,15 @@ const handleDownloadPartyAttachment = async (file: any) => { const blob = await response.blob(); console.log('下载文件Blob大小:', blob.size, 'bytes'); - + // 获取服务器返回的文件名 if (state.applicationForm.partyRetentionFileName) { fileName = state.applicationForm.partyRetentionFileName; } - + // 如果是PDF文件,设置正确的MIME类型 - const fileBlob = fileName.toLowerCase().endsWith('.pdf') - ? new Blob([blob], { type: 'application/pdf' }) - : blob; - + const fileBlob = fileName.toLowerCase().endsWith('.pdf') ? new Blob([blob], { type: 'application/pdf' }) : blob; + const url = window.URL.createObjectURL(fileBlob); const link = document.createElement('a'); link.href = url; @@ -3184,11 +3281,11 @@ const handleDownloadPartyAttachment = async (file: any) => { document.body.appendChild(link); link.click(); document.body.removeChild(link); - + setTimeout(() => { window.URL.revokeObjectURL(url); }, 100); - + ElMessage.success('文件下载成功'); } catch (error) { console.error('下载文件失败:', error); @@ -3205,7 +3302,6 @@ const formatFileSize = (bytes: number): string => { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; - // ==================== 附件管理相关方法 ==================== // 查询申请的附件列表 @@ -3219,40 +3315,82 @@ const loadAttachmentList = async (applicationId: number | string) => { } }; -// 查询详情的附件列表 -const loadViewAttachmentList = async (applicationId: number | string) => { - console.log('🔍 [附件加载] 开始加载附件列表,applicationId:', applicationId); +// 查询详情的附件列表(参考培训申请模式 - 从OSS接口获取) +const loadViewAttachmentList = async (attachmentIds: string | null | undefined) => { + console.log('🔍 [申请附件] 开始加载附件列表,attachmentIds:', attachmentIds); try { - const response = await listOverseasAttachments(applicationId); - console.log('✅ [附件加载] API响应成功:', response); - console.log('📊 [附件加载] 响应数据:', response.data); - console.log('🔢 [附件加载] 附件数量:', response.data?.length || 0); - - viewAttachmentList.value = response.data || []; - - if (!response.data || response.data.length === 0) { - console.warn('⚠️ [附件加载] 该申请暂无附件数据'); - console.warn('💡 [附件加载] 请在编辑页面点击"添加附件"按钮上传附件'); + // 检查是否有附件ID + if (!attachmentIds || !attachmentIds.toString().trim()) { + console.log('⚠️ [申请附件] 该申请暂无附件数据'); + viewAttachmentList.value = []; + return; + } + + // 解析OSS ID列表 + const ossIdsArray = attachmentIds + .toString() + .split(',') + .map((id) => id.trim()) + .filter((id) => id); + + if (ossIdsArray.length === 0) { + console.warn('⚠️ [申请附件] OSS ID列表为空'); + viewAttachmentList.value = []; + return; + } + + console.log('📋 [申请附件] OSS ID列表:', ossIdsArray); + + // 调用OSS接口获取文件信息 + const response = await request({ + url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`, + method: 'get' + }); + + if (response.code === 200 && response.data) { + console.log('✅ [申请附件] 获取OSS文件信息成功:', response.data); + + // 转换数据格式 + const attachmentFiles = response.data.map((item: any, index: number) => ({ + uid: `attachment_${index}_${Date.now()}`, + name: item.originalName || `附件${index + 1}`, + url: item.url, + ossId: item.ossId, + status: 'success', + fileSize: item.fileSize || 0, + uploadTime: item.createTime, + response: { + code: 200, + data: { + url: item.url, + ossId: item.ossId + } + } + })); + + viewAttachmentList.value = attachmentFiles; + console.log('✨ [申请附件] 附件数据处理完成,共', attachmentFiles.length, '个附件'); } else { - console.log('✨ [附件加载] 附件列表:', viewAttachmentList.value); + console.warn('⚠️ [申请附件] OSS接口返回数据为空'); + viewAttachmentList.value = []; } } catch (error: any) { - console.error('❌ [附件加载] 加载附件列表失败:', error); - console.error('📋 [附件加载] 错误详情:', { + console.error('❌ [申请附件] 加载附件列表失败:', error); + console.error('📋 [申请附件] 错误详情:', { message: error.message, response: error.response, status: error.response?.status, data: error.response?.data }); - + // 根据错误类型给出提示 if (error.response?.status === 403) { - console.error('🔒 [附件加载] 权限不足!请检查是否有 rs:overseas:query 权限'); + console.error('🔒 [申请附件] 权限不足!请检查是否有 resource:oss:list 权限'); ElMessage.warning('您没有查询附件的权限,请联系管理员'); } else if (error.response?.status === 404) { - console.error('🔍 [附件加载] 接口不存在!请检查后端服务是否正常'); + console.error('🔍 [申请附件] OSS接口不存在!请检查后端服务是否正常'); } - + viewAttachmentList.value = []; } }; @@ -3263,13 +3401,13 @@ const handleAddAttachment = () => { ElMessage.warning('请先保存申请表单,然后再上传附件'); return; } - + // 重置表单 attachmentForm.title = ''; attachmentForm.content = ''; attachmentForm.file = null; attachmentForm.fileList = []; - + attachmentDialog.visible = true; }; @@ -3287,18 +3425,18 @@ const handleAttachmentFileExceed = () => { // 提交附件上传 const submitAttachment = async () => { if (!attachmentFormRef.value) return; - + await attachmentFormRef.value.validate(async (valid: boolean) => { if (!valid) return; - + if (!attachmentForm.file) { ElMessage.warning('请选择要上传的文件'); return; } - + try { attachmentDialog.uploading = true; - + // 构建FormData const formData = new FormData(); formData.append('applicationId', state.applicationForm.applicationId!.toString()); @@ -3307,13 +3445,13 @@ const submitAttachment = async () => { formData.append('content', attachmentForm.content); } formData.append('file', attachmentForm.file); - + // 上传附件 await uploadOverseasAttachment(formData); - + ElMessage.success('附件上传成功'); attachmentDialog.visible = false; - + // 重新加载附件列表 await loadAttachmentList(state.applicationForm.applicationId!); } catch (error: any) { @@ -3339,10 +3477,10 @@ const handleDeleteAttachment = async (row: OverseasAttachmentVO) => { cancelButtonText: '取消', type: 'warning' }); - + await deleteOverseasAttachment(row.attachmentId); ElMessage.success('删除成功'); - + // 重新加载附件列表 await loadAttachmentList(state.applicationForm.applicationId!); } catch (error: any) { @@ -3354,26 +3492,34 @@ const handleDeleteAttachment = async (row: OverseasAttachmentVO) => { }; // 下载附件(从详情页) -const handleDownloadAttachment = (row: OverseasAttachmentVO) => { - // 直接通过链接下载 - const link = document.createElement('a'); - link.href = row.filePath; - link.download = row.fileOriginalName; - link.target = '_blank'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); +// 下载附件(兼容转换后的数据格式) +const handleDownloadAttachment = (row: any) => { + try { + // 直接通过链接下载 + const link = document.createElement('a'); + link.href = row.url || row.filePath; // 兼容新旧格式 + link.download = row.name || row.fileOriginalName; // 兼容新旧格式 + link.target = '_blank'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + console.log('📥 下载附件成功:', row.name || row.fileOriginalName); + } catch (error) { + console.error('❌ 下载附件失败:', error); + ElMessage.error('下载附件失败,请稍后重试'); + } }; // 下载出国境登记表盖章文件 - 参考培训申请的实现 const handleDownloadStampedFile = async (data: OverseasApplicationVO) => { console.log('📥 下载出国境登记表盖章文件:', data.stampedRegistrationFileName); - + if (!data.stampedFileOssId) { ElMessage.warning('文件OSS ID不存在,无法下载'); return; } - + try { // 使用request工具发送请求,自动包含认证头 const response = await request({ @@ -3386,23 +3532,21 @@ const handleDownloadStampedFile = async (data: OverseasApplicationVO) => { const fileName = data.stampedRegistrationFileName || '出国境登记表盖章文件.pdf'; const fileExt = fileName.split('.').pop()?.toLowerCase(); const mimeType = fileExt === 'pdf' ? 'application/pdf' : 'application/octet-stream'; - - const blob = response instanceof Blob - ? new Blob([response], { type: mimeType }) - : new Blob([response.data], { type: mimeType }); + + const blob = response instanceof Blob ? new Blob([response], { type: mimeType }) : new Blob([response.data], { type: mimeType }); const url = window.URL.createObjectURL(blob); - + const link = document.createElement('a'); link.href = url; link.download = fileName; - + document.body.appendChild(link); link.click(); - + // 清理资源 document.body.removeChild(link); window.URL.revokeObjectURL(url); - + ElMessage.success('文件下载成功'); } catch (error) { console.error('下载出国境登记表盖章文件失败:', error); @@ -3413,12 +3557,12 @@ const handleDownloadStampedFile = async (data: OverseasApplicationVO) => { // 下载党籍保留附件 - 参考培训申请的实现 const handleDownloadPartyRetentionFile = async (data: OverseasApplicationVO) => { console.log('📥 下载党籍保留附件:', data.partyRetentionFileName); - + if (!data.partyRetentionFileOssId) { ElMessage.warning('文件OSS ID不存在,无法下载'); return; } - + try { // 使用request工具发送请求,自动包含认证头 const response = await request({ @@ -3431,23 +3575,21 @@ const handleDownloadPartyRetentionFile = async (data: OverseasApplicationVO) => const fileName = data.partyRetentionFileName || '党籍保留附件.pdf'; const fileExt = fileName.split('.').pop()?.toLowerCase(); const mimeType = fileExt === 'pdf' ? 'application/pdf' : 'application/octet-stream'; - - const blob = response instanceof Blob - ? new Blob([response], { type: mimeType }) - : new Blob([response.data], { type: mimeType }); + + const blob = response instanceof Blob ? new Blob([response], { type: mimeType }) : new Blob([response.data], { type: mimeType }); const url = window.URL.createObjectURL(blob); - + const link = document.createElement('a'); link.href = url; link.download = fileName; - + document.body.appendChild(link); link.click(); - + // 清理资源 document.body.removeChild(link); window.URL.revokeObjectURL(url); - + ElMessage.success('文件下载成功'); } catch (error) { console.error('下载党籍保留附件失败:', error); @@ -3458,34 +3600,35 @@ const handleDownloadPartyRetentionFile = async (data: OverseasApplicationVO) => // 预览党籍保留附件 const handlePreviewPartyRetentionFile = async (data: OverseasApplicationVO) => { console.log('👁️ 预览党籍保留附件:', data.partyRetentionFileName); - + if (!data.partyRetentionFileOssId) { ElMessage.warning('文件OSS ID不存在,无法预览'); return; } - + try { // 判断文件类型 const fileName = data.partyRetentionFileName || ''; const fileExt = fileName.split('.').pop()?.toLowerCase(); - + // 图片类型直接在新窗口预览 if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExt || '')) { // 使用OSS下载接口获取图片 const imageUrl = `/resource/oss/download/${data.partyRetentionFileOssId}`; - + // 获取blob URL用于预览 const response = await request({ url: imageUrl, method: 'get', responseType: 'blob' }); - - const blob = response instanceof Blob - ? new Blob([response], { type: response.type || 'image/jpeg' }) - : new Blob([response.data], { type: response.data.type || 'image/jpeg' }); + + const blob = + response instanceof Blob + ? new Blob([response], { type: response.type || 'image/jpeg' }) + : new Blob([response.data], { type: response.data.type || 'image/jpeg' }); const blobUrl = window.URL.createObjectURL(blob); - + // 在新窗口打开图片 const previewWindow = window.open('', '_blank'); if (previewWindow) { @@ -3506,7 +3649,7 @@ const handlePreviewPartyRetentionFile = async (data: OverseasApplicationVO) => { `); previewWindow.document.close(); } - } + } // PDF类型在新窗口预览 else if (fileExt === 'pdf') { // 获取PDF文件 @@ -3515,28 +3658,27 @@ const handlePreviewPartyRetentionFile = async (data: OverseasApplicationVO) => { method: 'get', responseType: 'blob' }); - + // 正确处理Blob创建,确保包含正确的MIME类型 - const blob = response instanceof Blob - ? new Blob([response], { type: 'application/pdf' }) - : new Blob([response.data], { type: 'application/pdf' }); - + const blob = + response instanceof Blob ? new Blob([response], { type: 'application/pdf' }) : new Blob([response.data], { type: 'application/pdf' }); + console.log('📄 [PDF预览] 党籍附件Blob创建成功:', { size: blob.size, type: blob.type, fileName: fileName }); - + const blobUrl = window.URL.createObjectURL(blob); - + // 在新窗口打开PDF window.open(blobUrl, '_blank'); - + // 延迟清理,确保新窗口已加载 setTimeout(() => { window.URL.revokeObjectURL(blobUrl); }, 60000); // 60秒后清理 - } + } // 其他文件类型提示下载 else { ElMessage.info('该文件类型不支持在线预览,请下载后查看'); @@ -3551,34 +3693,35 @@ const handlePreviewPartyRetentionFile = async (data: OverseasApplicationVO) => { // 预览出国境登记表盖章文件 const handlePreviewStampedFile = async (data: OverseasApplicationVO) => { console.log('👁️ 预览出国境登记表盖章文件:', data.stampedRegistrationFileName); - + if (!data.stampedFileOssId) { ElMessage.warning('文件OSS ID不存在,无法预览'); return; } - + try { // 判断文件类型 const fileName = data.stampedRegistrationFileName || ''; const fileExt = fileName.split('.').pop()?.toLowerCase(); - + // 图片类型直接在新窗口预览 if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExt || '')) { // 使用OSS下载接口获取图片 const imageUrl = `/resource/oss/download/${data.stampedFileOssId}`; - + // 获取blob URL用于预览 const response = await request({ url: imageUrl, method: 'get', responseType: 'blob' }); - - const blob = response instanceof Blob - ? new Blob([response], { type: response.type || 'image/jpeg' }) - : new Blob([response.data], { type: response.data.type || 'image/jpeg' }); + + const blob = + response instanceof Blob + ? new Blob([response], { type: response.type || 'image/jpeg' }) + : new Blob([response.data], { type: response.data.type || 'image/jpeg' }); const blobUrl = window.URL.createObjectURL(blob); - + // 在新窗口打开图片 const previewWindow = window.open('', '_blank'); if (previewWindow) { @@ -3599,7 +3742,7 @@ const handlePreviewStampedFile = async (data: OverseasApplicationVO) => { `); previewWindow.document.close(); } - } + } // PDF类型在新窗口预览 else if (fileExt === 'pdf') { // 获取PDF文件 @@ -3608,28 +3751,27 @@ const handlePreviewStampedFile = async (data: OverseasApplicationVO) => { method: 'get', responseType: 'blob' }); - + // 正确处理Blob创建,确保包含正确的MIME类型 - const blob = response instanceof Blob - ? new Blob([response], { type: 'application/pdf' }) - : new Blob([response.data], { type: 'application/pdf' }); - + const blob = + response instanceof Blob ? new Blob([response], { type: 'application/pdf' }) : new Blob([response.data], { type: 'application/pdf' }); + console.log('📄 [PDF预览] 盖章文件Blob创建成功:', { size: blob.size, type: blob.type, fileName: fileName }); - + const blobUrl = window.URL.createObjectURL(blob); - + // 在新窗口打开PDF window.open(blobUrl, '_blank'); - + // 延迟清理,确保新窗口已加载 setTimeout(() => { window.URL.revokeObjectURL(blobUrl); }, 60000); // 60秒后清理 - } + } // 其他文件类型提示下载 else { ElMessage.info('该文件类型不支持在线预览,请下载后查看'); @@ -3640,6 +3782,90 @@ const handlePreviewStampedFile = async (data: OverseasApplicationVO) => { ElMessage.error('预览文件失败,请检查文件权限或稍后重试'); } }; + +// ==================== 申请附件上传相关方法(新增 - 参考培训申请模式) ==================== + +/** 申请附件上传前检查 */ +function beforeApplicationAttachmentUpload(file: File) { + const isValidType = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'image/jpeg', + 'image/png' + ].includes(file.type); + const isLt20M = file.size / 1024 / 1024 < 20; + + if (!isValidType) { + proxy?.$modal.msgError('上传附件文件只能是PDF/Word/Excel/图片格式!'); + return false; + } + + if (!isLt20M) { + proxy?.$modal.msgError('上传附件文件大小不能超过20MB!'); + return false; + } + + return true; +} + +/** 申请附件上传成功处理 */ +function handleApplicationAttachmentSuccess(response: any, file: any) { + if (response.code === 200 && response.data) { + file.url = response.data.url; + file.ossId = response.data.ossId; + + proxy?.$modal.msgSuccess('附件上传成功!'); + console.log('✅ [申请附件] 上传成功:', response.data); + + // 更新文件状态为成功 + file.status = 'success'; + file.response = response; + + // 更新 applicationForm 中的 attachmentIds 字段 + updateApplicationFormAttachmentIds(); + } +} + +/** 申请附件上传失败处理 */ +function handleApplicationAttachmentError(error: any) { + console.error('❌ [申请附件] 上传失败:', error); + let errorMessage = '附件上传失败,请重试!'; + + // 尝试从错误响应中获取具体错误信息 + if (error && error.response && error.response.data) { + if (error.response.data.msg) { + errorMessage = error.response.data.msg; + } else if (error.response.data.message) { + errorMessage = error.response.data.message; + } + } + + proxy?.$modal.msgError(errorMessage); +} + +/** 更新 applicationForm 中的 attachmentIds 字段 */ +function updateApplicationFormAttachmentIds() { + // 提取所有成功上传的 ossId + const attachmentOssIds = applicationAttachmentFileList.value + .filter((file) => file.status === 'success' && file.ossId) + .map((file) => file.ossId); + + // 更新 applicationForm 的 attachmentIds 字段(逗号分隔) + (state.applicationForm as any).attachmentIds = attachmentOssIds.join(','); + + console.log('📋 [申请附件] 更新 attachmentIds:', attachmentOssIds); + console.log('📋 [申请附件] applicationForm.attachmentIds:', (state.applicationForm as any).attachmentIds); +} + +/** 移除申请附件 */ +function handleRemoveApplicationAttachment(file: any) { + console.log('🗑️ [申请附件] 移除附件:', file.name); + // 更新 attachmentIds + updateApplicationFormAttachmentIds(); +}