Browse Source

Merge branch 'dev' of http://110.41.163.243:9980/kd-aiot/kd-aiot-frontend-wt into dev

xieyong 6 days ago
parent
commit
da8e066cfb
28 changed files with 2848 additions and 97 deletions
  1. 2 1
      .gitignore
  2. 2 0
      src/api/bpm/components/offerApproval/index.js
  3. 71 0
      src/api/hr/attendanceApply.js
  4. 25 0
      src/api/hr/essApplication.js
  5. 42 0
      src/utils/attachment.js
  6. 5 6
      src/views/bpm/handleTask/components/hiringApproval/hiringApprovalDetail.vue
  7. 180 0
      src/views/bpm/handleTask/components/hrCardRepairManagement/hrCardRepairDetail.vue
  8. 139 0
      src/views/bpm/handleTask/components/hrCardRepairManagement/submit.vue
  9. 331 0
      src/views/bpm/handleTask/components/hrChangeContractManagement/hrChangeContractDetail.vue
  10. 139 0
      src/views/bpm/handleTask/components/hrChangeContractManagement/submit.vue
  11. 15 15
      src/views/bpm/handleTask/components/hrContractManagement/hrContractDetail.vue
  12. 200 0
      src/views/bpm/handleTask/components/hrLeaveManagement/hrLeaveDetail.vue
  13. 139 0
      src/views/bpm/handleTask/components/hrLeaveManagement/submit.vue
  14. 186 0
      src/views/bpm/handleTask/components/hrOutingManagement/hrOutingDetail.vue
  15. 139 0
      src/views/bpm/handleTask/components/hrOutingManagement/submit.vue
  16. 193 0
      src/views/bpm/handleTask/components/hrOvertimeManagement/hrOvertimeDetail.vue
  17. 139 0
      src/views/bpm/handleTask/components/hrOvertimeManagement/submit.vue
  18. 221 0
      src/views/bpm/handleTask/components/hrRemoveContractManagement/hrRemoveContractDetail.vue
  19. 139 0
      src/views/bpm/handleTask/components/hrRemoveContractManagement/submit.vue
  20. 173 0
      src/views/bpm/handleTask/components/hrTripManagement/hrTripDetail.vue
  21. 139 0
      src/views/bpm/handleTask/components/hrTripManagement/submit.vue
  22. 9 12
      src/views/bpm/handleTask/components/offerApproval/offerApprovalDetail.vue
  23. 95 33
      src/views/bpm/handleTask/components/positionTransfer/positionTransferDetail.vue
  24. 3 3
      src/views/bpm/handleTask/components/regularizationHandling/regularizationHandlingDetail.vue
  25. 26 1
      src/views/bpm/handleTask/components/resignationApplication/resignationApplicationDetail.vue
  26. 21 1
      src/views/bpm/handleTask/components/resignationHandoverOrder/resignationHandoverOrderDetail.vue
  27. 73 23
      src/views/bpm/handleTask/index.vue
  28. 2 2
      vue.config.js

+ 2 - 1
.gitignore

@@ -23,4 +23,5 @@ yarn-error.log*
 *.sw?
 vue.config.js
 package-lock.json
-.history
+.history
+.workbuddy

+ 2 - 0
src/api/bpm/components/offerApproval/index.js

@@ -1,4 +1,5 @@
 import request from '@/utils/request';
+import { parseAttachmentIds } from '@/utils/attachment';
 
 function dateOnly(value) {
   if (!value) return '';
@@ -76,6 +77,7 @@ export function adaptHiringApproval(raw = {}) {
     status: HIRING_STATUS_LABELS[statusCode] || raw.approvalStatus || '-',
     remark: raw.remark || raw.approvalComment || '',
     attachments,
+    attachmentFileIds: parseAttachmentIds(raw),
     attachmentNames: attachments
       .map((item) => item.fileName || item.name)
       .filter(Boolean),

+ 71 - 0
src/api/hr/attendanceApply.js

@@ -0,0 +1,71 @@
+import request from '@/utils/request';
+import { getProcessInstance } from '@/api/bpm/processInstance';
+import { getAdminApplicationDetail } from '@/api/hr/essApplication';
+
+function unwrap(response) {
+  const result = response?.data || {};
+  if (Number(result.code) === 0) return result.data;
+  const error = new Error(result.message || '服务调用失败');
+  if (response?.__toastShown) error.__toastShown = true;
+  return Promise.reject(error);
+}
+
+/**
+ * 考勤申请详情(来源业务主键),补卡 / 请假 / 外出 / 加班 / 出差五类共用
+ * GET /hr/attendance/applications/{id}
+ */
+export async function getAttendanceApplicationDetail(id) {
+  if (id === undefined || id === null || id === '') return null;
+  const response = await request({
+    url: `/hr/attendance/applications/${encodeURIComponent(String(id))}`,
+    method: 'get'
+  });
+  return unwrap(response);
+}
+
+/**
+ * 考勤审批详情取数(审批页用)
+ *
+ * 1. 按待办传入的 M09 索引主键取 ESS 统一申请详情:申请人 / 所属组织 / 申请编号;
+ * 2. 用 ESS 返回的来源业务主键 businessId 取考勤申请明细;
+ * 3. 明细缺失时回退流程实例的 formVariables(发起审批时透传的载荷与明细 VO 同构)。
+ */
+export async function loadAttendanceApprovalDetail(
+  essId,
+  processInstanceId = ''
+) {
+  let ess = {};
+  let detail = {};
+
+  if (essId !== undefined && essId !== null && essId !== '') {
+    try {
+      ess = (await getAdminApplicationDetail(essId)) || {};
+    } catch (error) {
+      ess = {};
+    }
+    // 优先用 ESS 返回的来源业务主键;未返回时兼容待办直接传考勤申请主键
+    const sourceId = ess.businessId || essId;
+    try {
+      detail = (await getAttendanceApplicationDetail(sourceId)) || {};
+    } catch (error) {
+      detail = {};
+    }
+  }
+
+  if (!Object.keys(detail).length && processInstanceId) {
+    try {
+      const instance = await getProcessInstance(processInstanceId);
+      detail = (instance && instance.formVariables) || {};
+      if (!ess.applicantName && instance && instance.startUser) {
+        ess = Object.assign({}, ess, {
+          applicantName: instance.startUser.nickname,
+          organizationName: instance.startUser.deptName
+        });
+      }
+    } catch (error) {
+      detail = {};
+    }
+  }
+
+  return { ess, detail };
+}

+ 25 - 0
src/api/hr/essApplication.js

@@ -0,0 +1,25 @@
+import request from '@/utils/request';
+
+function unwrap(response) {
+  const result = response?.data || {};
+  if (Number(result.code) === 0) return result.data;
+  const error = new Error(result.message || '服务调用失败');
+  if (response?.__toastShown) error.__toastShown = true;
+  return Promise.reject(error);
+}
+
+/**
+ * 管理端:统一申请详情(M09 员工自助申请索引主键)
+ * GET /hr/ess/admin/applications/{id}
+ *
+ * 该视图只返回「来源引用 + 脱敏摘要 + 状态 + 当前节点」,
+ * 业务明细需要按返回的 businessId 走对应来源业务接口。
+ */
+export async function getAdminApplicationDetail(id) {
+  if (id === undefined || id === null || id === '') return null;
+  const response = await request({
+    url: `/hr/ess/admin/applications/${encodeURIComponent(String(id))}`,
+    method: 'get'
+  });
+  return unwrap(response);
+}

+ 42 - 0
src/utils/attachment.js

@@ -0,0 +1,42 @@
+/**
+ * 从审批 / 业务详情的原始接口数据中,兼容多种附件字段来源,
+ * 解析出文件 ID 数组(去重、字符串化)。
+ *
+ * 兼容的字段来源(按常见命名):
+ *  - attachmentIds            字符串(逗号分隔)或字符串数组
+ *  - attachmentFileIds        字符串(逗号分隔)或字符串数组
+ *  - approvalDocumentFileId   单个 ID 或逗号分隔的字符串或数组
+ *  - attachmentFileId         同上
+ *  - resumeAttachmentFileIds  同上
+ *  - files / attachments      对象数组,元素含 fileId / id,或本身就是 ID 字符串
+ *
+ * 这样无论后端接口用哪种字段返回附件,前端都能正确拿到可预览的文件 ID 列表。
+ */
+export function parseAttachmentIds(raw) {
+  raw = raw || {};
+  const ids = [];
+  const collect = (value) => {
+    if (value == null || value === '') return;
+    const list = Array.isArray(value)
+      ? value
+      : String(value)
+          .split(',')
+          .map((s) => s.trim());
+    list.forEach((item) => {
+      if (item == null || item === '') return;
+      const id =
+        typeof item === 'object' ? item.fileId || item.id : String(item).trim();
+      if (id && !ids.includes(String(id))) ids.push(String(id));
+    });
+  };
+
+  collect(raw.attachmentIds);
+  collect(raw.attachmentFileIds);
+  collect(raw.approvalDocumentFileId);
+  collect(raw.attachmentFileId);
+  collect(raw.resumeAttachmentFileIds);
+  if (Array.isArray(raw.files)) collect(raw.files);
+  if (Array.isArray(raw.attachments)) collect(raw.attachments);
+
+  return ids;
+}

+ 5 - 6
src/views/bpm/handleTask/components/hiringApproval/hiringApprovalDetail.vue

@@ -209,7 +209,7 @@
           <div class="attach-row">
             <label>简历</label>
             <div class="attach-content">
-              <fileMain v-model="resumeFileIds" type="view" size="small" />
+              <fileMain v-model="resumeFileIds" type="view" size="small" :view-disabled="!resumeFileIds.length" />
             </div>
           </div>
         </section>
@@ -221,11 +221,11 @@
 <script>
 import { getPositionById, getPositionPage } from '@/api/hr';
 import {
-  getHiringApprovalById,
-  parseResumeAttachmentFileIds
+  getHiringApprovalById
 } from '@/api/hr/hiringApproval';
 import { listOrganizations } from '@/api/organization';
 import { getDictByCode } from '@/api/sys';
+import { parseAttachmentIds } from '@/utils/attachment';
 
 const emptyForm = () => ({
   id: null,
@@ -425,9 +425,8 @@ export default {
         specialTerms: detail.specialTerms || detail.remark || '',
         specialApprovalReason: detail.specialApprovalReason || ''
       };
-      this.resumeFileIds = parseResumeAttachmentFileIds(
-        detail.resumeAttachmentFileIds
-      );
+      // 兼容多种附件字段来源(attachmentIds / resumeAttachmentFileIds / files 等)
+      this.resumeFileIds = parseAttachmentIds(detail);
     },
     async loadDictOptions() {
       try {

+ 180 - 0
src/views/bpm/handleTask/components/hrCardRepairManagement/hrCardRepairDetail.vue

@@ -0,0 +1,180 @@
+<template>
+  <main class="cm-detail-host" v-loading="loading">
+    <div v-if="hasData" class="cm-create-page cm-detail-page">
+      <div class="cm-create-employee-card cm-detail-employee">
+        <span class="cm-employee-avatar">{{ avatarText }}</span>
+        <div class="cm-employee-info">
+          <b
+            >申请人:{{ applicantName || '-'
+            }}<span class="cm-employee-no"
+              >所属组织:{{ organizationName }}</span
+            ><span v-if="ess.applicationNo" class="cm-employee-no"
+              >申请编号:{{ ess.applicationNo }}</span
+            ></b
+          >
+        </div>
+      </div>
+
+      <div class="cm-create-section">
+        <div class="cm-create-section-title">补卡申请信息</div>
+        <div class="cm-create-grid">
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">补卡类型:</span
+            ><span class="cm-compare-value">{{ repairTypeLabel }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">补卡日期:</span
+            ><span class="cm-compare-value">{{ datePart(punchTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">补卡时间:</span
+            ><span class="cm-compare-value">{{ timePart(punchTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">附件:</span
+            ><span class="cm-compare-value">
+              <fileMain
+                v-if="attachmentIds.length"
+                :value="attachmentIds"
+                type="view"
+                size="small"
+              />
+              <span v-else class="cm-detail-muted">暂无附件</span>
+            </span>
+          </div>
+        </div>
+        <div v-if="form.reason" class="cm-detail-remark">
+          <strong>申请事由:</strong>{{ form.reason }}
+        </div>
+      </div>
+    </div>
+    <el-empty v-else-if="!loading" description="暂无补卡申请详情" />
+  </main>
+</template>
+
+<script>
+import { loadAttendanceApprovalDetail } from '@/api/hr/attendanceApply';
+import { parseAttachmentIds } from '@/utils/attachment';
+
+/** 补卡类型枚举(与 /hr/attendance/applications 的 repairType 口径一致) */
+const REPAIR_TYPE_LABELS = {
+  MISSING_IN: '上班漏打卡',
+  MISSING_OUT: '下班漏打卡',
+  LATE: '迟到补卡',
+  EARLY: '早退补卡',
+  ABSENT: '缺勤补卡',
+  NORMAL: '正常补卡',
+  OTHER: '其他补卡'
+};
+
+function datePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  return text.split('T')[0].split(' ')[0];
+}
+
+function timePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  const time = text.split('T')[1] || text.split(' ')[1] || '';
+  return time ? time.slice(0, 5) : '-';
+}
+
+export default {
+  name: 'HrCardRepairDetail',
+  props: {
+    // 待办传入的 M09 员工自助申请索引主键
+    businessId: {
+      default: ''
+    },
+    // 流程实例主键,ESS 明细缺失时回退流程表单变量
+    id: {
+      default: ''
+    },
+    deptName: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      ess: {},
+      form: {}
+    };
+  },
+  computed: {
+    hasData() {
+      return Object.keys(this.form).length > 0;
+    },
+    applicantName() {
+      return (
+        this.ess.applicantName ||
+        this.form.userName ||
+        this.form.applicantName ||
+        ''
+      );
+    },
+    organizationName() {
+      return (
+        this.ess.organizationName ||
+        this.form.deptName ||
+        this.form.departmentName ||
+        this.deptName ||
+        '-'
+      );
+    },
+    avatarText() {
+      return this.applicantName ? this.applicantName.slice(0, 1) : '?';
+    },
+    repairTypeLabel() {
+      const code = this.form.repairType;
+      if (!code) return '-';
+      return REPAIR_TYPE_LABELS[code] || code;
+    },
+    // 补卡以补卡打卡时间为主,缺失时退回开始时间
+    punchTime() {
+      return this.form.repairPunchTime || this.form.startTime || '';
+    },
+    attachmentIds() {
+      return parseAttachmentIds(this.form);
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler() {
+        this.getInfo();
+      }
+    },
+    id() {
+      if (!this.businessId) this.getInfo();
+    }
+  },
+  methods: {
+    datePart,
+    timePart,
+    async getInfo() {
+      // 审批详情 = ESS 统一申请详情(申请人/组织/编号)+ 考勤申请明细,
+      // 明细缺失时回退流程实例表单变量
+      if (!this.businessId && !this.id) return;
+      this.loading = true;
+      try {
+        const { ess, detail } = await loadAttendanceApprovalDetail(
+          this.businessId,
+          this.id
+        );
+        this.ess = ess || {};
+        this.form = detail || {};
+      } catch (e) {
+        this.ess = {};
+        this.form = {};
+        this.$message.error(e.message || '补卡申请详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hrContractManagement/index.scss"></style>

+ 139 - 0
src/views/bpm/handleTask/components/hrCardRepairManagement/submit.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批意见"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <ApprovalOpinion ref="approvalOpinion" v-model="form.reason" />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+
+  export default {
+    name: 'HrCardRepairSubmit',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        isLoading: false,
+        form: {
+          reason: '同意'
+        }
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        if (!this.form.reason && !!status) {
+          this.$message.warning('请填写审批意见');
+          return;
+        }
+        await this._approveTaskWithVariables(status);
+      },
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        const API = !!status ? approveTaskWithVariables : rejectTask;
+        try {
+          this.isLoading = true;
+          const res = await API({
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: {
+              pass: !!status
+            }
+          });
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        } finally {
+          this.isLoading = false;
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 331 - 0
src/views/bpm/handleTask/components/hrChangeContractManagement/hrChangeContractDetail.vue

@@ -0,0 +1,331 @@
+<template>
+  <main class="cm-detail-host" v-loading="loading">
+    <div v-if="detailRow" class="cm-create-page cm-detail-page">
+      <div class="cm-create-employee-card cm-detail-employee">
+        <span class="cm-employee-avatar">{{ avatarText }}</span>
+        <div class="cm-employee-info">
+          <b>员工姓名:{{ detailRow.employeeName || '-'
+          }}<span class="cm-employee-no">合同编号:{{ detailRow.contractNo || '-' }}</span></b>
+        </div>
+      </div>
+
+      <!-- 变更前后对比:原合同信息 / 发起后信息 -->
+      <div class="cm-change-columns">
+        <div class="cm-create-section cm-change-col">
+          <div class="cm-create-section-title">原合同信息</div>
+          <div class="cm-create-grid">
+            <div v-for="field in detailRow.beforeFields" :key="'before-' + field.key" class="cm-compare-item">
+              <span class="cm-compare-label">{{ field.label }}:</span><span class="cm-compare-value">{{ field.value ||
+                '-' }}</span>
+            </div>
+            <div class="cm-compare-item is-span-2">
+              <span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
+                <fileMain v-if="detailRow.attachmentFileIds.length" :value="detailRow.attachmentFileIds" type="view"
+                  size="small" />
+                <span v-else class="cm-detail-muted">暂无附件</span>
+              </span>
+            </div>
+          </div>
+        </div>
+
+        <div class="cm-create-section cm-change-col">
+          <div class="cm-create-section-title">变更后信息</div>
+          <div class="cm-create-grid">
+            <div v-for="field in detailRow.afterFields" :key="'after-' + field.key" class="cm-compare-item">
+              <span class="cm-compare-label">{{ field.label }}:</span><span class="cm-compare-value"
+                :class="{ 'is-highlight': field.changed }">{{ field.value || '-' }}</span>
+            </div>
+            <div class="cm-compare-item is-span-2">
+              <span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
+                <fileMain v-if="detailRow.attachmentFileIds.length" :value="detailRow.attachmentFileIds" type="view"
+                  size="small" />
+                <span v-else class="cm-detail-muted">暂无附件</span>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 变更信息 -->
+      <div class="cm-create-section">
+        <div class="cm-create-section-title">变更信息</div>
+        <div class="cm-create-grid">
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">变更单号:</span><span class="cm-compare-value">{{ detailRow.changeNo || '-'
+              }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">变更类型:</span><span class="cm-compare-value">{{
+              detailRow.changeTypeText
+              }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">审批状态:</span><span class="cm-compare-value">{{
+              detailRow.changeStatusText
+              }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">生效日期:</span><span class="cm-compare-value">{{
+              detailRow.effectiveDate || '-'
+              }}</span>
+          </div>
+        </div>
+        <div v-if="detailRow.changeReason" class="cm-detail-remark">
+          <strong>变更原因:</strong>{{ detailRow.changeReason }}
+        </div>
+      </div>
+    </div>
+    <el-empty v-else-if="!loading" description="暂无合同变更详情" />
+  </main>
+</template>
+
+<script>
+import { getContractById } from '@/api/hr';
+import { getProcessInstance } from '@/api/bpm/processInstance';
+import { parseAttachmentIds } from '@/utils/attachment';
+
+/** 签署方式:ONLINE-审批待办附件盖章签字;OFFLINE-线下签署 */
+const SIGNING_METHOD_LABELS = {
+  ONLINE: '线上',
+  OFFLINE: '线下'
+};
+
+/** 对比字段:快照字段名与合同顶层字段同名,缺失时逐层回退 */
+const FIELD_DEFS = [
+  { key: 'employeeName', label: '签订员工' },
+  { key: 'contractNo', label: '合同编号' },
+  { key: 'contractTypeName', label: '合同类型' },
+  { key: 'salaryText', label: '拟定薪资' },
+  { key: 'legalEntityName', label: '所属机构' },
+  { key: 'departmentName', label: '所属部门' },
+  { key: 'positionName', label: '岗位名称' },
+  { key: 'positionSequence', label: '岗位序列' },
+  { key: 'positionLevel', label: '岗位层级' },
+  { key: 'positionTypeName', label: '岗位性质' },
+  { key: 'probationText', label: '试用期' },
+  { key: 'startDate', label: '合同起始' },
+  { key: 'endDate', label: '合同终止' },
+  { key: 'signingMethodText', label: '签署方式' }
+];
+
+function parseSnapshot(raw) {
+  if (!raw) return {};
+  if (typeof raw === 'object') return raw;
+  try {
+    return JSON.parse(raw) || {};
+  } catch (e) {
+    return {};
+  }
+}
+
+function formatDate(value) {
+  if (!value) return '';
+  return String(value).slice(0, 10);
+}
+
+function formatSalary(value) {
+  if (value == null || value === '') return '';
+  const num = Number(value);
+  if (Number.isNaN(num)) return String(value);
+  return `¥ ${num.toLocaleString('zh-CN')}/月`;
+}
+
+function formatProbation(value) {
+  if (value == null || value === '') return '';
+  const num = Number(value);
+  if (Number.isNaN(num)) return String(value);
+  return `${num}个月`;
+}
+
+export default {
+  name: 'HrChangeContractDetail',
+  props: {
+    businessId: {
+      default: ''
+    },
+    /** 流程实例 id:用于从流程变量里补取合同 id */
+    id: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      detailRow: null
+    };
+  },
+  computed: {
+    avatarText() {
+      return this.detailRow?.employeeName
+        ? this.detailRow.employeeName.slice(0, 1)
+        : '?';
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler(id) {
+        if (id) this.getInfo(id);
+      }
+    }
+  },
+  methods: {
+    async getInfo(businessId) {
+      if (!businessId) return;
+      this.loading = true;
+      try {
+        let contract = await this.fetchContract(businessId);
+        // businessId 可能是变更单 id(不是合同 id):拿到的不对时从流程变量补取合同 id
+        if (!contract || String(contract.id) !== String(businessId)) {
+          const contractId = await this.resolveContractIdFromProcess();
+          if (contractId) contract = await this.fetchContract(contractId);
+        }
+        if (!contract) throw new Error('未获取到合同信息');
+        this.detailRow = this.adaptChange(contract, businessId);
+      } catch (e) {
+        this.detailRow = null;
+        this.$message.error(e.message || '合同变更详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    },
+    async fetchContract(id) {
+      try {
+        return (await getContractById(id)) || null;
+      } catch (e) {
+        return null;
+      }
+    },
+    /**
+     * 从流程变量补取合同 id(项目内既有做法,见 api/hr/attendanceApply.js)。
+     * 兼容常见变量命名,并打印变量内容便于确认后端实际下发的字段名。
+     */
+    async resolveContractIdFromProcess() {
+      if (!this.id) return '';
+      try {
+        const instance = await getProcessInstance(this.id);
+        const raw = instance && instance.formVariables;
+        const vars = typeof raw === 'string' ? parseSnapshot(raw) : raw || {};
+        // eslint-disable-next-line no-console
+        console.log('[HrChangeContractDetail] 流程变量 formVariables:', vars);
+        const candidates = [
+          vars.contractId,
+          vars.contract_id,
+          vars.contractPrimaryKey,
+          vars.businessContractId,
+          vars.contract && vars.contract.id
+        ];
+        const hit = candidates.find((item) => item != null && item !== '');
+        return hit != null ? String(hit) : '';
+      } catch (e) {
+        return '';
+      }
+    },
+    /**
+     * 合同详情接口返回该合同全部变更记录(changes),定位本条审批对应的变更:
+     * ① 传入的 businessId(后端流程 businessKey 实际存的是变更单 id)
+     * ② contract.changeId
+     * ③ 审批中/签署中的那条
+     * ④ 最新一条
+     */
+    adaptChange(contract = {}, changeIdHint = '') {
+      const list = Array.isArray(contract.changes) ? contract.changes : [];
+      const pendingStatuses = ['PENDING_APPROVAL', 'SIGNING'];
+      const record =
+        list.find((item) => String(item.id) === String(changeIdHint)) ||
+        list.find((item) => String(item.id) === String(contract.changeId)) ||
+        list.find((item) =>
+          pendingStatuses.includes(
+            String(item.changeStatus || '').toUpperCase()
+          )
+        ) ||
+        list[0] ||
+        {};
+      const before = {
+        ...this.contractSnapshot(contract),
+        ...parseSnapshot(record.beforeSnapshot)
+      };
+      const after = { ...before, ...parseSnapshot(record.afterSnapshot) };
+      const normalize = (snap) => ({
+        employeeName: snap.employeeName || '',
+        contractNo: snap.contractNo || '',
+        contractTypeName: snap.contractTypeName || snap.contractType || '',
+        salaryText: formatSalary(snap.salaryStandard),
+        legalEntityName: snap.legalEntityName || '',
+        departmentName: snap.departmentName || snap.deptName || '',
+        positionName: snap.positionName || '',
+        positionSequence: snap.positionSequence || '',
+        positionLevel: snap.positionLevel || '',
+        positionTypeName: snap.positionTypeName || snap.positionType || '',
+        probationText: formatProbation(snap.probationMonths),
+        startDate: formatDate(snap.startDate),
+        endDate: formatDate(snap.endDate),
+        signingMethodText:
+          SIGNING_METHOD_LABELS[
+          String(snap.signingMethod || '').toUpperCase()
+          ] ||
+          snap.signingMethodName ||
+          snap.signingMethod ||
+          ''
+      });
+      const beforeView = normalize(before);
+      const afterView = normalize(after);
+      const buildFields = (view, other) =>
+        FIELD_DEFS.map((def) => ({
+          key: def.key,
+          label: def.label,
+          value: view[def.key],
+          changed: Boolean(other && view[def.key] !== other[def.key])
+        }));
+      return {
+        employeeName: contract.employeeName || beforeView.employeeName,
+        contractNo: contract.contractNo || beforeView.contractNo,
+        beforeFields: buildFields(beforeView, null),
+        afterFields: buildFields(afterView, beforeView),
+        attachmentFileIds: parseAttachmentIds(contract),
+        changeNo: record.changeNo || '',
+        changeTypeText: record.changeTypeName || record.changeType || '-',
+        changeStatusText: record.changeStatusName || record.changeStatus || '-',
+        effectiveDate: formatDate(record.effectiveDate),
+        changeReason: record.changeReason || ''
+      };
+    },
+    /** 合同顶层快照字段:作为变更前快照的兜底来源 */
+    contractSnapshot(contract) {
+      return {
+        employeeName: contract.employeeName,
+        contractNo: contract.contractNo,
+        contractTypeName: contract.contractTypeName || contract.contractType,
+        salaryStandard: contract.salaryStandard,
+        legalEntityName: contract.legalEntityName,
+        departmentName: contract.departmentName,
+        positionName: contract.positionName,
+        positionSequence: contract.positionSequence,
+        positionLevel: contract.positionLevel,
+        positionTypeName: contract.positionTypeName || contract.positionType,
+        probationMonths: contract.probationMonths,
+        startDate: contract.startDate,
+        endDate: contract.endDate,
+        signingMethod: contract.signingMethod,
+        signingMethodName: contract.signingMethodName
+      };
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hrContractManagement/index.scss"></style>
+<style lang="scss" scoped>
+.cm-change-columns {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 14px;
+  align-items: start;
+}
+
+@media (max-width: 900px) {
+  .cm-change-columns {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 139 - 0
src/views/bpm/handleTask/components/hrChangeContractManagement/submit.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批意见"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <ApprovalOpinion ref="approvalOpinion" v-model="form.reason" />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+
+  export default {
+    name: 'HrChangeContractSubmit',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        isLoading: false,
+        form: {
+          reason: '同意'
+        }
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        if (!this.form.reason && !!status) {
+          this.$message.warning('请填写审批意见');
+          return;
+        }
+        await this._approveTaskWithVariables(status);
+      },
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        const API = !!status ? approveTaskWithVariables : rejectTask;
+        try {
+          this.isLoading = true;
+          const res = await API({
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: {
+              pass: !!status
+            }
+          });
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        } finally {
+          this.isLoading = false;
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 15 - 15
src/views/bpm/handleTask/components/hrContractManagement/hrContractDetail.vue

@@ -83,10 +83,8 @@
             }}</span>
           </div>
           <div class="cm-compare-item">
-            <span class="cm-compare-label">试用期至:</span
-            ><span class="cm-compare-value">{{
-              detailRow.probationEnd || '无试用期'
-            }}</span>
+            <span class="cm-compare-label">试用期:</span
+            ><span class="cm-compare-value">{{ probationText }}</span>
           </div>
         </div>
       </div>
@@ -180,6 +178,7 @@ import {
   contractFlowSignStatusLabel,
   pickFlowSignStatus
 } from '@/api/hr';
+import { parseAttachmentIds } from '@/utils/attachment';
 
 const CONTRACT_TYPES = [
   { value: 'FIXED', label: '固定期限劳动合同' },
@@ -213,16 +212,7 @@ function signMethodLabel(value) {
   return value || '';
 }
 
-function collectAttachmentFileIds(files) {
-  const list = (Array.isArray(files) ? files : []).filter(Boolean);
-  const attach = list.filter((item) =>
-    ['ATTACHMENT', 'SCAN', 'ARCHIVE'].includes(item.filePurpose)
-  );
-  const source = attach.length ? attach : list;
-  return source
-    .map((item) => item.fileId || item.id)
-    .filter((id) => id != null && id !== '');
-}
+// 附件 ID 解析已统一由 src/utils/attachment.js 的 parseAttachmentIds 处理
 
 function adaptContract(item = {}) {
   const files = item.files || [];
@@ -263,7 +253,7 @@ function adaptContract(item = {}) {
     probationMonths: item.probationMonths,
     createdAt: formatDateTime(item.createTime),
     probationEnd: formatDate(item.probationEndDate),
-    attachmentFileIds: collectAttachmentFileIds(files),
+    attachmentFileIds: parseAttachmentIds(item),
     level: item.positionLevel || item.level || '',
     manager: item.managerName || item.manager || '',
     archived:
@@ -286,6 +276,16 @@ export default {
       detailRow: null
     };
   },
+  computed: {
+    /**
+     * 试用期只认后端 probationMonths(月数):>0 显示「N个月」,0/空显示「无试用期」。
+     */
+    probationText() {
+      const months = Number(this.detailRow?.probationMonths);
+      if (Number.isFinite(months) && months > 0) return `${months}个月`;
+      return '无试用期';
+    }
+  },
   watch: {
     businessId: {
       immediate: true,

+ 200 - 0
src/views/bpm/handleTask/components/hrLeaveManagement/hrLeaveDetail.vue

@@ -0,0 +1,200 @@
+<template>
+  <main class="cm-detail-host" v-loading="loading">
+    <div v-if="hasData" class="cm-create-page cm-detail-page">
+      <div class="cm-create-employee-card cm-detail-employee">
+        <span class="cm-employee-avatar">{{ avatarText }}</span>
+        <div class="cm-employee-info">
+          <b
+            >申请人:{{ applicantName || '-'
+            }}<span class="cm-employee-no"
+              >所属组织:{{ organizationName }}</span
+            ><span v-if="ess.applicationNo" class="cm-employee-no"
+              >申请编号:{{ ess.applicationNo }}</span
+            ></b
+          >
+        </div>
+      </div>
+
+      <div class="cm-create-section">
+        <div class="cm-create-section-title">请假申请信息</div>
+        <div class="cm-create-grid">
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">请假类型:</span
+            ><span class="cm-compare-value">{{ leaveTypeLabel }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">申请时长:</span
+            ><span class="cm-compare-value">{{ durationLabel }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始日期:</span
+            ><span class="cm-compare-value">{{ datePart(form.startTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始时间:</span
+            ><span class="cm-compare-value">{{ timePart(form.startTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束日期:</span
+            ><span class="cm-compare-value">{{ datePart(form.endTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束时间:</span
+            ><span class="cm-compare-value">{{ timePart(form.endTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">附件:</span
+            ><span class="cm-compare-value">
+              <fileMain
+                v-if="attachmentIds.length"
+                :value="attachmentIds"
+                type="view"
+                size="small"
+              />
+              <span v-else class="cm-detail-muted">暂无附件</span>
+            </span>
+          </div>
+        </div>
+        <div v-if="form.reason" class="cm-detail-remark">
+          <strong>申请事由:</strong>{{ form.reason }}
+        </div>
+      </div>
+    </div>
+    <el-empty v-else-if="!loading" description="暂无请假申请详情" />
+  </main>
+</template>
+
+<script>
+import { loadAttendanceApprovalDetail } from '@/api/hr/attendanceApply';
+import { parseAttachmentIds } from '@/utils/attachment';
+
+/** 请假类型枚举(与 /hr/attendance/applications 的 leaveType 口径一致) */
+const LEAVE_TYPE_LABELS = {
+  ANNUAL: '年假',
+  PERSONAL: '事假',
+  SICK: '病假',
+  COMPENSATORY: '调休假',
+  MARRIAGE: '婚假',
+  MATERNITY: '产假',
+  PATERNITY: '陪产假',
+  BEREAVEMENT: '丧假',
+  WORK_INJURY: '工伤假',
+  OTHER: '其他'
+};
+
+function datePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  return text.split('T')[0].split(' ')[0];
+}
+
+function timePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  const time = text.split('T')[1] || text.split(' ')[1] || '';
+  return time ? time.slice(0, 5) : '-';
+}
+
+export default {
+  name: 'HrLeaveDetail',
+  props: {
+    // 待办传入的 M09 员工自助申请索引主键
+    businessId: {
+      default: ''
+    },
+    // 流程实例主键,ESS 明细缺失时回退流程表单变量
+    id: {
+      default: ''
+    },
+    deptName: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      ess: {},
+      form: {}
+    };
+  },
+  computed: {
+    hasData() {
+      return Object.keys(this.form).length > 0;
+    },
+    applicantName() {
+      return (
+        this.ess.applicantName ||
+        this.form.userName ||
+        this.form.applicantName ||
+        ''
+      );
+    },
+    organizationName() {
+      return (
+        this.ess.organizationName ||
+        this.form.deptName ||
+        this.form.departmentName ||
+        this.deptName ||
+        '-'
+      );
+    },
+    avatarText() {
+      return this.applicantName ? this.applicantName.slice(0, 1) : '?';
+    },
+    leaveTypeLabel() {
+      const code = this.form.leaveType;
+      if (!code) return '-';
+      return LEAVE_TYPE_LABELS[code] || code;
+    },
+    durationLabel() {
+      const minutes = Number(this.form.durationMinutes);
+      if (!minutes || isNaN(minutes) || minutes <= 0) return '-';
+      const hours = Math.floor(minutes / 60);
+      const mins = minutes % 60;
+      if (!hours) return `${mins} 分钟`;
+      if (!mins) return `${hours} 小时`;
+      return `${hours} 小时 ${mins} 分钟`;
+    },
+    attachmentIds() {
+      return parseAttachmentIds(this.form);
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler() {
+        this.getInfo();
+      }
+    },
+    id() {
+      if (!this.businessId) this.getInfo();
+    }
+  },
+  methods: {
+    datePart,
+    timePart,
+    async getInfo() {
+      // 审批详情 = ESS 统一申请详情(申请人/组织/编号)+ 考勤申请明细,
+      // 明细缺失时回退流程实例表单变量
+      if (!this.businessId && !this.id) return;
+      this.loading = true;
+      try {
+        const { ess, detail } = await loadAttendanceApprovalDetail(
+          this.businessId,
+          this.id
+        );
+        this.ess = ess || {};
+        this.form = detail || {};
+      } catch (e) {
+        this.ess = {};
+        this.form = {};
+        this.$message.error(e.message || '请假申请详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hrContractManagement/index.scss"></style>

+ 139 - 0
src/views/bpm/handleTask/components/hrLeaveManagement/submit.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批意见"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <ApprovalOpinion ref="approvalOpinion" v-model="form.reason" />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+
+  export default {
+    name: 'HrLeaveSubmit',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        isLoading: false,
+        form: {
+          reason: '同意'
+        }
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        if (!this.form.reason && !!status) {
+          this.$message.warning('请填写审批意见');
+          return;
+        }
+        await this._approveTaskWithVariables(status);
+      },
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        const API = !!status ? approveTaskWithVariables : rejectTask;
+        try {
+          this.isLoading = true;
+          const res = await API({
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: {
+              pass: !!status
+            }
+          });
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        } finally {
+          this.isLoading = false;
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 186 - 0
src/views/bpm/handleTask/components/hrOutingManagement/hrOutingDetail.vue

@@ -0,0 +1,186 @@
+<template>
+  <main class="cm-detail-host" v-loading="loading">
+    <div v-if="hasData" class="cm-create-page cm-detail-page">
+      <div class="cm-create-employee-card cm-detail-employee">
+        <span class="cm-employee-avatar">{{ avatarText }}</span>
+        <div class="cm-employee-info">
+          <b
+            >申请人:{{ applicantName || '-'
+            }}<span class="cm-employee-no"
+              >所属组织:{{ organizationName }}</span
+            ><span v-if="ess.applicationNo" class="cm-employee-no"
+              >申请编号:{{ ess.applicationNo }}</span
+            ></b
+          >
+        </div>
+      </div>
+
+      <div class="cm-create-section">
+        <div class="cm-create-section-title">外出申请信息</div>
+        <div class="cm-create-grid">
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始日期:</span
+            ><span class="cm-compare-value">{{ datePart(form.startTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始时间:</span
+            ><span class="cm-compare-value">{{ timePart(form.startTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束日期:</span
+            ><span class="cm-compare-value">{{ datePart(form.endTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束时间:</span
+            ><span class="cm-compare-value">{{ timePart(form.endTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">外勤时长:</span
+            ><span class="cm-compare-value">{{ durationLabel }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">同步打卡:</span
+            ><span class="cm-compare-value">{{ yesNo(form.syncAsPunch) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">附件:</span
+            ><span class="cm-compare-value">
+              <fileMain
+                v-if="attachmentIds.length"
+                :value="attachmentIds"
+                type="view"
+                size="small"
+              />
+              <span v-else class="cm-detail-muted">暂无附件</span>
+            </span>
+          </div>
+        </div>
+        <div v-if="form.reason" class="cm-detail-remark">
+          <strong>申请事由:</strong>{{ form.reason }}
+        </div>
+      </div>
+    </div>
+    <el-empty v-else-if="!loading" description="暂无外出申请详情" />
+  </main>
+</template>
+
+<script>
+import { loadAttendanceApprovalDetail } from '@/api/hr/attendanceApply';
+import { parseAttachmentIds } from '@/utils/attachment';
+
+function datePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  return text.split('T')[0].split(' ')[0];
+}
+
+function timePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  const time = text.split('T')[1] || text.split(' ')[1] || '';
+  return time ? time.slice(0, 5) : '-';
+}
+
+export default {
+  name: 'HrOutingDetail',
+  props: {
+    // 待办传入的 M09 员工自助申请索引主键
+    businessId: {
+      default: ''
+    },
+    // 流程实例主键,ESS 明细缺失时回退流程表单变量
+    id: {
+      default: ''
+    },
+    deptName: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      ess: {},
+      form: {}
+    };
+  },
+  computed: {
+    hasData() {
+      return Object.keys(this.form).length > 0;
+    },
+    applicantName() {
+      return (
+        this.ess.applicantName ||
+        this.form.userName ||
+        this.form.applicantName ||
+        ''
+      );
+    },
+    organizationName() {
+      return (
+        this.ess.organizationName ||
+        this.form.deptName ||
+        this.form.departmentName ||
+        this.deptName ||
+        '-'
+      );
+    },
+    avatarText() {
+      return this.applicantName ? this.applicantName.slice(0, 1) : '?';
+    },
+    durationLabel() {
+      const minutes = Number(this.form.durationMinutes);
+      if (!minutes || isNaN(minutes) || minutes <= 0) return '-';
+      const hours = Math.floor(minutes / 60);
+      const mins = minutes % 60;
+      if (!hours) return `${mins} 分钟`;
+      if (!mins) return `${hours} 小时`;
+      return `${hours} 小时 ${mins} 分钟`;
+    },
+    attachmentIds() {
+      return parseAttachmentIds(this.form);
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler() {
+        this.getInfo();
+      }
+    },
+    id() {
+      if (!this.businessId) this.getInfo();
+    }
+  },
+  methods: {
+    datePart,
+    timePart,
+    yesNo(val) {
+      if (val === 0 || val === '0') return '否';
+      if (val === 1 || val === '1') return '是';
+      return '-';
+    },
+    async getInfo() {
+      // 审批详情 = ESS 统一申请详情(申请人/组织/编号)+ 考勤申请明细,
+      // 明细缺失时回退流程实例表单变量
+      if (!this.businessId && !this.id) return;
+      this.loading = true;
+      try {
+        const { ess, detail } = await loadAttendanceApprovalDetail(
+          this.businessId,
+          this.id
+        );
+        this.ess = ess || {};
+        this.form = detail || {};
+      } catch (e) {
+        this.ess = {};
+        this.form = {};
+        this.$message.error(e.message || '外出申请详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hrContractManagement/index.scss"></style>

+ 139 - 0
src/views/bpm/handleTask/components/hrOutingManagement/submit.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批意见"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <ApprovalOpinion ref="approvalOpinion" v-model="form.reason" />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+
+  export default {
+    name: 'HrOutingSubmit',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        isLoading: false,
+        form: {
+          reason: '同意'
+        }
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        if (!this.form.reason && !!status) {
+          this.$message.warning('请填写审批意见');
+          return;
+        }
+        await this._approveTaskWithVariables(status);
+      },
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        const API = !!status ? approveTaskWithVariables : rejectTask;
+        try {
+          this.isLoading = true;
+          const res = await API({
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: {
+              pass: !!status
+            }
+          });
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        } finally {
+          this.isLoading = false;
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 193 - 0
src/views/bpm/handleTask/components/hrOvertimeManagement/hrOvertimeDetail.vue

@@ -0,0 +1,193 @@
+<template>
+  <main class="cm-detail-host" v-loading="loading">
+    <div v-if="hasData" class="cm-create-page cm-detail-page">
+      <div class="cm-create-employee-card cm-detail-employee">
+        <span class="cm-employee-avatar">{{ avatarText }}</span>
+        <div class="cm-employee-info">
+          <b
+            >申请人:{{ applicantName || '-'
+            }}<span class="cm-employee-no"
+              >所属组织:{{ organizationName }}</span
+            ><span v-if="ess.applicationNo" class="cm-employee-no"
+              >申请编号:{{ ess.applicationNo }}</span
+            ></b
+          >
+        </div>
+      </div>
+
+      <div class="cm-create-section">
+        <div class="cm-create-section-title">加班申请信息</div>
+        <div class="cm-create-grid">
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">加班类型:</span
+            ><span class="cm-compare-value">{{ overtimeTypeLabel }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">加班时长:</span
+            ><span class="cm-compare-value">{{ durationLabel }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始日期:</span
+            ><span class="cm-compare-value">{{ datePart(form.startTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始时间:</span
+            ><span class="cm-compare-value">{{ timePart(form.startTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束日期:</span
+            ><span class="cm-compare-value">{{ datePart(form.endTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束时间:</span
+            ><span class="cm-compare-value">{{ timePart(form.endTime) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">附件:</span
+            ><span class="cm-compare-value">
+              <fileMain
+                v-if="attachmentIds.length"
+                :value="attachmentIds"
+                type="view"
+                size="small"
+              />
+              <span v-else class="cm-detail-muted">暂无附件</span>
+            </span>
+          </div>
+        </div>
+        <div v-if="form.reason" class="cm-detail-remark">
+          <strong>申请事由:</strong>{{ form.reason }}
+        </div>
+      </div>
+    </div>
+    <el-empty v-else-if="!loading" description="暂无加班申请详情" />
+  </main>
+</template>
+
+<script>
+import { loadAttendanceApprovalDetail } from '@/api/hr/attendanceApply';
+import { parseAttachmentIds } from '@/utils/attachment';
+
+/** 加班类型枚举(与 /hr/attendance/applications 的 overtimeType 口径一致) */
+const OVERTIME_TYPE_LABELS = {
+  WORKDAY: '工作日加班',
+  RESTDAY: '休息日加班',
+  LEGAL_HOLIDAY: '法定节假日加班'
+};
+
+function datePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  return text.split('T')[0].split(' ')[0];
+}
+
+function timePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  const time = text.split('T')[1] || text.split(' ')[1] || '';
+  return time ? time.slice(0, 5) : '-';
+}
+
+export default {
+  name: 'HrOvertimeDetail',
+  props: {
+    // 待办传入的 M09 员工自助申请索引主键
+    businessId: {
+      default: ''
+    },
+    // 流程实例主键,ESS 明细缺失时回退流程表单变量
+    id: {
+      default: ''
+    },
+    deptName: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      ess: {},
+      form: {}
+    };
+  },
+  computed: {
+    hasData() {
+      return Object.keys(this.form).length > 0;
+    },
+    applicantName() {
+      return (
+        this.ess.applicantName ||
+        this.form.userName ||
+        this.form.applicantName ||
+        ''
+      );
+    },
+    organizationName() {
+      return (
+        this.ess.organizationName ||
+        this.form.deptName ||
+        this.form.departmentName ||
+        this.deptName ||
+        '-'
+      );
+    },
+    avatarText() {
+      return this.applicantName ? this.applicantName.slice(0, 1) : '?';
+    },
+    overtimeTypeLabel() {
+      const code = this.form.overtimeType;
+      if (!code) return '-';
+      return OVERTIME_TYPE_LABELS[code] || code;
+    },
+    durationLabel() {
+      const minutes = Number(this.form.durationMinutes);
+      if (!minutes || isNaN(minutes) || minutes <= 0) return '-';
+      const hours = Math.floor(minutes / 60);
+      const mins = minutes % 60;
+      if (!hours) return `${mins} 分钟`;
+      if (!mins) return `${hours} 小时`;
+      return `${hours} 小时 ${mins} 分钟`;
+    },
+    attachmentIds() {
+      return parseAttachmentIds(this.form);
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler() {
+        this.getInfo();
+      }
+    },
+    id() {
+      if (!this.businessId) this.getInfo();
+    }
+  },
+  methods: {
+    datePart,
+    timePart,
+    async getInfo() {
+      // 审批详情 = ESS 统一申请详情(申请人/组织/编号)+ 考勤申请明细,
+      // 明细缺失时回退流程实例表单变量
+      if (!this.businessId && !this.id) return;
+      this.loading = true;
+      try {
+        const { ess, detail } = await loadAttendanceApprovalDetail(
+          this.businessId,
+          this.id
+        );
+        this.ess = ess || {};
+        this.form = detail || {};
+      } catch (e) {
+        this.ess = {};
+        this.form = {};
+        this.$message.error(e.message || '加班申请详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hrContractManagement/index.scss"></style>

+ 139 - 0
src/views/bpm/handleTask/components/hrOvertimeManagement/submit.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批意见"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <ApprovalOpinion ref="approvalOpinion" v-model="form.reason" />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+
+  export default {
+    name: 'HrOvertimeSubmit',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        isLoading: false,
+        form: {
+          reason: '同意'
+        }
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        if (!this.form.reason && !!status) {
+          this.$message.warning('请填写审批意见');
+          return;
+        }
+        await this._approveTaskWithVariables(status);
+      },
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        const API = !!status ? approveTaskWithVariables : rejectTask;
+        try {
+          this.isLoading = true;
+          const res = await API({
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: {
+              pass: !!status
+            }
+          });
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        } finally {
+          this.isLoading = false;
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 221 - 0
src/views/bpm/handleTask/components/hrRemoveContractManagement/hrRemoveContractDetail.vue

@@ -0,0 +1,221 @@
+<template>
+  <main class="cm-detail-host" v-loading="loading">
+    <div v-if="detailRow" class="cm-create-page cm-detail-page">
+      <div class="cm-create-employee-card cm-detail-employee">
+        <span class="cm-employee-avatar">{{ avatarText }}</span>
+        <div class="cm-employee-info">
+          <b
+            >员工姓名:{{ detailRow.employeeName || '-'
+            }}<span class="cm-employee-no"
+              >合同编号:{{ detailRow.contractNo || '-' }}</span
+            ></b
+          >
+        </div>
+      </div>
+
+      <div class="cm-create-section">
+        <div class="cm-create-section-title">解除 / 终止信息</div>
+        <div class="cm-create-grid">
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">业务类型:</span
+            ><span class="cm-compare-value">{{
+              detailRow.terminationTypeText
+            }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">签署方式:</span
+            ><span class="cm-compare-value">{{
+              detailRow.signingMethodText
+            }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">原因编码:</span
+            ><span class="cm-compare-value">{{ detailRow.reasonCodeText }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">生效日期:</span
+            ><span class="cm-compare-value">{{
+              detailRow.effectiveDate || '-'
+            }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">最后工作日期:</span
+            ><span class="cm-compare-value">{{
+              detailRow.lastWorkingDate || '-'
+            }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">离职证明附件:</span
+            ><span class="cm-compare-value">
+              <fileMain
+                v-if="
+                  detailRow.attachmentFileIds &&
+                  detailRow.attachmentFileIds.length
+                "
+                :value="detailRow.attachmentFileIds"
+                type="view"
+                size="small"
+              />
+              <span v-else class="cm-detail-muted">暂无附件</span>
+            </span>
+          </div>
+        </div>
+        <div v-if="detailRow.reason" class="cm-detail-remark">
+          <strong>原因说明:</strong>{{ detailRow.reason }}
+        </div>
+      </div>
+    </div>
+    <el-empty v-else-if="!loading" description="暂无解除 / 终止详情" />
+  </main>
+</template>
+
+<script>
+import { getContractById } from '@/api/hr';
+import { getByCode } from '@/api/system/dictionary-data';
+import { parseAttachmentIds } from '@/utils/attachment';
+
+/** 业务类型:接口参数为 TERMINATE / END(见劳动合同解除终止接口) */
+const TERMINATION_TYPE_LABELS = {
+  TERMINATE: '解除',
+  END: '终止'
+};
+
+/** 签署方式:ONLINE-审批待办附件盖章签字;OFFLINE-线下签署 */
+const SIGNING_METHOD_LABELS = {
+  ONLINE: '线上',
+  OFFLINE: '线下'
+};
+
+/** 原因编码兜底项:字典 hr_contract_terminate 拉取失败时使用 */
+const TERMINATE_REASON_FALLBACK = [
+  { value: 'MUTUAL', label: '协商一致解除' },
+  { value: 'EMPLOYEE', label: '员工提出解除' },
+  { value: 'MISCONDUCT', label: '严重违纪' },
+  { value: 'CONTRACT_EXPIRE', label: '合同到期终止' },
+  { value: 'OTHER', label: '其他' }
+];
+
+function formatDate(value) {
+  if (!value) return '';
+  return String(value).slice(0, 10);
+}
+
+/**
+ * 解除/终止单的附件:文档里 terminations[] 给的是 attachmentIds;
+ * 线下签署时创建入参传的是 offlineFileId,两种都兼容,避免附件丢失。
+ */
+function pickTerminationFileIds(record = {}) {
+  const ids = parseAttachmentIds(record);
+  if (!ids.length && record.offlineFileId) ids.push(String(record.offlineFileId));
+  return ids;
+}
+
+export default {
+  name: 'HrRemoveContractDetail',
+  props: {
+    businessId: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      detailRow: null,
+      terminationReasons: TERMINATE_REASON_FALLBACK
+    };
+  },
+  computed: {
+    avatarText() {
+      return this.detailRow?.employeeName
+        ? this.detailRow.employeeName.slice(0, 1)
+        : '?';
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler(id) {
+        if (id) this.getInfo(id);
+      }
+    }
+  },
+  methods: {
+    async getInfo(id) {
+      if (!id) return;
+      this.loading = true;
+      // 原因编码字典单独拉取,失败不影响详情展示
+      this.loadTerminationReasons();
+      try {
+        const detail = await getContractById(id);
+        this.detailRow = this.adaptTermination(detail || {});
+      } catch (e) {
+        this.detailRow = null;
+        this.$message.error(e.message || '解除/终止详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    },
+    /**
+     * 合同详情接口返回该合同全部解除/终止记录(按创建时间倒序)。
+     * 审批单对应的是「最近一次解除终止单」terminationId,取不到时退回最新一条。
+     */
+    adaptTermination(contract = {}) {
+      const list = Array.isArray(contract.terminations)
+        ? contract.terminations
+        : [];
+      const record =
+        list.find(
+          (item) => String(item.id) === String(contract.terminationId)
+        ) ||
+        list[0] ||
+        {};
+      return {
+        id: record.id ?? '',
+        employeeName: contract.employeeName || '',
+        contractNo: contract.contractNo || '',
+        terminationType: record.terminationType || '',
+        terminationTypeText:
+          TERMINATION_TYPE_LABELS[record.terminationType] ||
+          record.terminationType ||
+          '-',
+        signingMethod: record.signingMethod || '',
+        signingMethodText:
+          SIGNING_METHOD_LABELS[
+            String(record.signingMethod || '').toUpperCase()
+          ] ||
+          record.signingMethodName ||
+          record.signingMethod ||
+          '-',
+        reasonCode: record.reasonCode || '',
+        reasonCodeText: this.reasonLabel(record.reasonCode),
+        effectiveDate: formatDate(record.effectiveDate),
+        lastWorkingDate: formatDate(record.lastWorkingDate),
+        reason: record.reason || '',
+        attachmentFileIds: pickTerminationFileIds(record)
+      };
+    },
+    reasonLabel(code) {
+      if (!code) return '-';
+      const hit = this.terminationReasons.find((item) => item.value === code);
+      return hit ? hit.label : code;
+    },
+    /** 原因编码字典:与 HR 侧同源 hr_contract_terminate */
+    async loadTerminationReasons() {
+      try {
+        const res = await getByCode('hr_contract_terminate');
+        const list = (res?.data || [])
+          .map((item) => {
+            const entry = Object.entries(item || {})[0] || [];
+            return { value: entry[0], label: entry[1] };
+          })
+          .filter((item) => item.value);
+        if (list.length) this.terminationReasons = list;
+      } catch (e) {
+        this.terminationReasons = TERMINATE_REASON_FALLBACK;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hrContractManagement/index.scss"></style>

+ 139 - 0
src/views/bpm/handleTask/components/hrRemoveContractManagement/submit.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批意见"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <ApprovalOpinion ref="approvalOpinion" v-model="form.reason" />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+
+  export default {
+    name: 'HrContractSubmit',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        isLoading: false,
+        form: {
+          reason: '同意'
+        }
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        if (!this.form.reason && !!status) {
+          this.$message.warning('请填写审批意见');
+          return;
+        }
+        await this._approveTaskWithVariables(status);
+      },
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        const API = !!status ? approveTaskWithVariables : rejectTask;
+        try {
+          this.isLoading = true;
+          const res = await API({
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: {
+              pass: !!status
+            }
+          });
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        } finally {
+          this.isLoading = false;
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 173 - 0
src/views/bpm/handleTask/components/hrTripManagement/hrTripDetail.vue

@@ -0,0 +1,173 @@
+<template>
+  <main class="cm-detail-host" v-loading="loading">
+    <div v-if="hasData" class="cm-create-page cm-detail-page">
+      <div class="cm-create-employee-card cm-detail-employee">
+        <span class="cm-employee-avatar">{{ avatarText }}</span>
+        <div class="cm-employee-info">
+          <b>申请人:{{ applicantName || '-'
+          }}<span class="cm-employee-no">所属组织:{{ organizationName }}</span><span v-if="ess.applicationNo"
+              class="cm-employee-no">申请编号:{{ ess.applicationNo }}</span></b>
+        </div>
+      </div>
+
+      <div class="cm-create-section">
+        <div class="cm-create-section-title">出差申请信息</div>
+        <div class="cm-create-grid">
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始日期:</span><span class="cm-compare-value">{{ datePart(form.startTime)
+              }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">开始时间:</span><span class="cm-compare-value">{{ timePart(form.startTime)
+              }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束日期:</span><span class="cm-compare-value">{{ datePart(form.endTime)
+              }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">结束时间:</span><span class="cm-compare-value">{{ timePart(form.endTime)
+              }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">出差时长:</span><span class="cm-compare-value">{{ durationLabel }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">异地打卡:</span><span class="cm-compare-value">{{ yesNo(form.needPunch) }}</span>
+          </div>
+          <div class="cm-compare-item">
+            <span class="cm-compare-label">附件:</span><span class="cm-compare-value">
+              <fileMain v-if="attachmentIds.length" :value="attachmentIds" type="view" size="small" />
+              <span v-else class="cm-detail-muted">暂无附件</span>
+            </span>
+          </div>
+        </div>
+        <div v-if="form.reason" class="cm-detail-remark">
+          <strong>申请事由:</strong>{{ form.reason }}
+        </div>
+      </div>
+    </div>
+    <el-empty v-else-if="!loading" description="暂无出差申请详情" />
+  </main>
+</template>
+
+<script>
+import { loadAttendanceApprovalDetail } from '@/api/hr/attendanceApply';
+import { parseAttachmentIds } from '@/utils/attachment';
+
+function datePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  return text.split('T')[0].split(' ')[0];
+}
+
+function timePart(val) {
+  const text = String(val || '');
+  if (!text) return '-';
+  const time = text.split('T')[1] || text.split(' ')[1] || '';
+  return time ? time.slice(0, 5) : '-';
+}
+
+export default {
+  name: 'HrTripDetail',
+  props: {
+    // 待办传入的 M09 员工自助申请索引主键
+    businessId: {
+      default: ''
+    },
+    // 流程实例主键,ESS 明细缺失时回退流程表单变量
+    id: {
+      default: ''
+    },
+    deptName: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      ess: {},
+      form: {}
+    };
+  },
+  computed: {
+    hasData() {
+      return Object.keys(this.form).length > 0;
+    },
+    applicantName() {
+      return (
+        this.ess.applicantName ||
+        this.form.userName ||
+        this.form.applicantName ||
+        ''
+      );
+    },
+    organizationName() {
+      return (
+        this.ess.organizationName ||
+        this.form.deptName ||
+        this.form.departmentName ||
+        this.deptName ||
+        '-'
+      );
+    },
+    avatarText() {
+      return this.applicantName ? this.applicantName.slice(0, 1) : '?';
+    },
+    durationLabel() {
+      const minutes = Number(this.form.durationMinutes);
+      if (!minutes || isNaN(minutes) || minutes <= 0) return '-';
+      const hours = Math.floor(minutes / 60);
+      const mins = minutes % 60;
+      if (!hours) return `${mins} 分钟`;
+      if (!mins) return `${hours} 小时`;
+      return `${hours} 小时 ${mins} 分钟`;
+    },
+    attachmentIds() {
+      return parseAttachmentIds(this.form);
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler() {
+        this.getInfo();
+      }
+    },
+    id() {
+      if (!this.businessId) this.getInfo();
+    }
+  },
+  methods: {
+    datePart,
+    timePart,
+    yesNo(val) {
+      if (val === 0 || val === '0') return '否';
+      if (val === 1 || val === '1') return '是';
+      return '-';
+    },
+    async getInfo() {
+      // 审批详情 = ESS 统一申请详情(申请人/组织/编号)+ 考勤申请明细,
+      // 明细缺失时回退流程实例表单变量
+      if (!this.businessId && !this.id) return;
+      this.loading = true;
+      try {
+        const { ess, detail } = await loadAttendanceApprovalDetail(
+          this.businessId,
+          this.id
+        );
+        this.ess = ess || {};
+        this.form = detail || {};
+      } catch (e) {
+        this.ess = {};
+        this.form = {};
+        this.$message.error(e.message || '出差申请详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hrContractManagement/index.scss"></style>

+ 139 - 0
src/views/bpm/handleTask/components/hrTripManagement/submit.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批意见"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <ApprovalOpinion ref="approvalOpinion" v-model="form.reason" />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+
+  export default {
+    name: 'HrTripSubmit',
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        isLoading: false,
+        form: {
+          reason: '同意'
+        }
+      };
+    },
+    methods: {
+      async handleAudit(status) {
+        if (!this.form.reason && !!status) {
+          this.$message.warning('请填写审批意见');
+          return;
+        }
+        await this._approveTaskWithVariables(status);
+      },
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        const API = !!status ? approveTaskWithVariables : rejectTask;
+        try {
+          this.isLoading = true;
+          const res = await API({
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: {
+              pass: !!status
+            }
+          });
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        } finally {
+          this.isLoading = false;
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 9 - 12
src/views/bpm/handleTask/components/offerApproval/offerApprovalDetail.vue

@@ -107,17 +107,12 @@
       </div>
       <div class="detail-block">
         <span>附件</span>
-        <div
-          class="file-tags"
-          v-if="form.attachmentNames && form.attachmentNames.length"
-        >
-          <em
-            v-for="(name, index) in form.attachmentNames"
-            :key="name + index"
-            class="file-tag"
-            >{{ name }}</em
-          >
-        </div>
+        <fileMain
+          v-if="attachmentFileIds.length"
+          v-model="attachmentFileIds"
+          type="view"
+          size="small"
+        />
         <p v-else class="detail-muted">暂无附件</p>
       </div>
       <div class="detail-block" v-if="form.remark">
@@ -163,7 +158,8 @@ export default {
   data() {
     return {
       loading: false,
-      form: emptyForm()
+      form: emptyForm(),
+      attachmentFileIds: []
     };
   },
   watch: {
@@ -183,6 +179,7 @@ export default {
           ...emptyForm(),
           ...(await getHiringApprovalById(id))
         };
+        this.attachmentFileIds = this.form.attachmentFileIds || [];
       } catch (error) {
         this.form = emptyForm();
         this.$message.error(error.message || '录用审批详情加载失败');

+ 95 - 33
src/views/bpm/handleTask/components/positionTransfer/positionTransferDetail.vue

@@ -77,8 +77,9 @@
 
 <script>
 import { getUserChangeById } from '@/api/main';
-import { listOrganizations, adaptEmployee, getEmployeeById } from '@/api/organization';
+import { listOrganizations, getOrganization, adaptEmployee, getEmployeeById } from '@/api/organization';
 import { getDictByCode } from '@/api/sys';
+import { parseAttachmentIds } from '@/utils/attachment';
 
 /** 异动类型(与接口文档 changeType 枚举对齐) */
 const CHANGE_TYPE_LABELS = {
@@ -135,6 +136,7 @@ const empty = () => ({
   contractPeriod: '',
   // 变更信息
   targetLegalId: null,
+  targetLegalName: '',
   targetDeptId: null,
   targetDept: '',
   positionSequence: '',
@@ -259,22 +261,28 @@ export default {
       this.loading = true;
       try {
         const record = await getUserChangeById(id);
-        // 异动单只带 userId:并行补查员工档案与组织列表
-        const [employee] = await Promise.all([
-          this.fetchEmployee(record),
-          this.loadOrganizations(),
-          this.loadPositionNatureOptions()
-        ]);
-        this.form = this.mapForm(record, employee);
-        const fileId = this.form.approvalDocumentFileId;
-        this.approvalFileIds = fileId ? [fileId] : [];
+        // 主数据先行渲染,补查(员工/组织/字典)失败不阻断、不清空
+        this.form = this.mapForm(record, null);
+        // 兼容多种附件字段来源(attachmentIds / approvalDocumentFileId 等)
+        this.approvalFileIds = parseAttachmentIds(record);
+        try {
+          // 异动单只带 userId:并行补查员工档案与组织列表
+          const [employee] = await Promise.all([
+            this.fetchEmployee(record),
+            this.loadOrganizations(),
+            this.loadPositionNatureOptions()
+          ]);
+          this.form = this.mapForm(record, employee);
+        } catch (error) {
+          // 补查失败:保留已解析的主数据继续展示
+        }
+        // 组织树查不到机构名时,按 id 单查补全
+        this.ensureLegalName(this.form.targetLegalId);
       } catch (error) {
         this.form = empty();
         this.approvalFileIds = [];
-        if (process.env.NODE_ENV !== 'production') {
-          // eslint-disable-next-line no-console
-          console.warn('[PositionTransferDetail] 加载岗位变更单失败:', error);
-        }
+        // eslint-disable-next-line no-console
+        console.warn('[PositionTransferDetail] 加载岗位变更单失败:', error);
       } finally {
         this.loading = false;
       }
@@ -295,21 +303,28 @@ export default {
     async loadOrganizations() {
       try {
         const res = await listOrganizations();
-        const list = Array.isArray(res)
+        const raw = Array.isArray(res)
           ? res
           : (res && (res.list || res.records || res.rows)) || [];
-        this.orgList = list
-          .filter(item => item && item.id != null && item.id !== '')
-          .map(item => ({
-            id: String(item.id),
-            name: item.name || '',
-            parentId:
-              item.parentId != null && item.parentId !== ''
-                ? String(item.parentId)
-                : '',
-            type: item.type
-          }))
-          .filter(item => item.name);
+        // getGroupList 返回树形(含 children),需递归展开为平铺列表
+        const flat = [];
+        const walk = nodes => {
+          (Array.isArray(nodes) ? nodes : []).forEach(item => {
+            if (!item || item.id == null || item.id === '') return;
+            flat.push({
+              id: String(item.id),
+              name: item.name || '',
+              parentId:
+                item.parentId != null && item.parentId !== ''
+                  ? String(item.parentId)
+                  : '',
+              type: item.type
+            });
+            if (item.children && item.children.length) walk(item.children);
+          });
+        };
+        walk(raw);
+        this.orgList = flat.filter(item => item.name);
       } catch (error) {
         this.orgList = [];
       }
@@ -408,9 +423,29 @@ export default {
           .filter(Boolean)
           .map(toDateStr)
           .join(' 至 '),
-        // 变更信息
-        targetLegalId: first(record.targetLegalId) || null,
-        targetDeptId: first(record.targetDeptId) || null,
+        // 变更信息:机构来自 beforeSnapshot / afterSnapshot(接口无顶层机构字段)
+        targetLegalId: first(
+          record.targetLegalId,
+          record.targetOrganizationId,
+          record.toOrganizationId,
+          record.toLegalId,
+          record.organizationId,
+          record.legalEntityId,
+          after.legalId,
+          before.legalId
+        ) || null,
+        // 机构名:接口/快照直接返回的名字优先,避免依赖组织树回显
+        targetLegalName: first(
+          record.targetLegalName,
+          record.targetOrganizationName,
+          record.toLegalName,
+          record.toOrganizationName,
+          record.organizationName,
+          record.legalEntityName,
+          after.legalName,
+          before.legalName
+        ) || '',
+        targetDeptId: first(record.targetDeptId, after.deptId, before.deptId) || null,
         targetDept: first(
           record.targetDeptName,
           record.targetDept,
@@ -466,13 +501,15 @@ export default {
       if (explicit) return explicit;
       return onlineDocumentTemplateId ? 'online' : 'offline';
     },
-    /** 员工所属机构:档案机构 → 部门上溯顶层机构 */
+    /** 员工所属机构:档案机构 → 快照机构 → 部门上溯顶层机构 */
     resolveOriginLegal(record, employee) {
       const emp = employee || {};
+      const before = parseSnapshot(record && record.beforeSnapshot);
       const explicit = first(
         emp.company,
         emp.legalEntity,
-        emp.raw && emp.raw.groupName
+        emp.raw && emp.raw.groupName,
+        before.legalName
       );
       if (explicit) return explicit;
       const legalId = this.findLegalIdByDept(
@@ -500,13 +537,38 @@ export default {
       );
       return (legal && legal.id) || node.id;
     },
-    /** 机构名称:组织列表优先,缺失时按部门上溯,兜底原始 id */
+    /** 机构名称:接口直接返回的机构名优先,其次组织树查询,兜底原始 id */
     legalName(id) {
+      if (this.form.targetLegalName) return this.form.targetLegalName;
       const target =
         id == null || id === '' ? this.findLegalIdByDept(this.form.targetDeptId) : id;
       if (target == null || target === '') return '-';
       const node = this.orgList.find(item => String(item.id) === String(target));
       return (node && node.name) || String(target);
+    },
+    /** 组织树查不到机构名时,按 id 单查补全(不阻塞渲染) */
+    async ensureLegalName(id) {
+      if (id == null || id === '' || this.form.targetLegalName) return;
+      if (this.orgList.some(item => String(item.id) === String(id))) return;
+      try {
+        const node = await getOrganization(id);
+        if (node && node.name) {
+          this.orgList = [
+            ...this.orgList,
+            {
+              id: String(node.id != null ? node.id : id),
+              name: node.name,
+              parentId:
+                node.parentId != null && node.parentId !== ''
+                  ? String(node.parentId)
+                  : '',
+              type: node.type
+            }
+          ];
+        }
+      } catch (error) {
+        // 单查失败不影响其它字段展示
+      }
     }
   }
 };

+ 3 - 3
src/views/bpm/handleTask/components/regularizationHandling/regularizationHandlingDetail.vue

@@ -175,6 +175,7 @@
 <script>
 import { getRegularizationById } from '@/api/main';
 import { getEmployeeById } from '@/api/organization';
+import { parseAttachmentIds } from '@/utils/attachment';
 
 const empty = () => ({
   id: '',
@@ -391,9 +392,8 @@ export default {
         }
         this.employeeInfo = this.mapEmployeeInfo(regularization, employee);
 
-        // 转正单 swagger 只有单个 approvalDocumentFileId,统一转成数组给 fileMain
-        const fileId = this.approvalDocumentFileId;
-        this.approvalFileIds = fileId ? [fileId] : [];
+        // 兼容多种附件字段来源(attachmentIds / approvalDocumentFileId 等)
+        this.approvalFileIds = parseAttachmentIds(regularization);
       } catch (error) {
         this.form = empty();
         this.employeeInfo = {};

+ 26 - 1
src/views/bpm/handleTask/components/resignationApplication/resignationApplicationDetail.vue

@@ -75,12 +75,33 @@
         </div>
       </div>
     </section>
+
+    <!-- 附件 -->
+    <section class="detail-section">
+      <div class="info-card info-card--body">
+        <div class="kv-grid">
+          <div class="kv-item kv-item--full">
+            <span class="kv-item__label">附件</span>
+            <span class="kv-item__value">
+              <fileMain
+                v-if="approvalFileIds.length"
+                v-model="approvalFileIds"
+                type="view"
+                size="small"
+              />
+              <span v-else style="color: #909399">暂无附件</span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </section>
   </main>
 </template>
 
 <script>
 import { getResignationById } from '@/api/main';
 import { getEmployeeById } from '@/api/organization';
+import { parseAttachmentIds } from '@/utils/attachment';
 
 const empty = () => ({
   id: '',
@@ -162,7 +183,8 @@ export default {
     return {
       loading: false,
       form: empty(),
-      employeeInfo: {}
+      employeeInfo: {},
+      approvalFileIds: []
     };
   },
   computed: {
@@ -287,9 +309,12 @@ export default {
           }
         }
         this.employeeInfo = this.mapEmployeeInfo(resignation, employee);
+        // 兼容多种附件字段来源(attachmentIds / approvalDocumentFileId 等)
+        this.approvalFileIds = parseAttachmentIds(resignation);
       } catch (error) {
         this.form = empty();
         this.employeeInfo = {};
+        this.approvalFileIds = [];
         if (process.env.NODE_ENV !== 'production') {
           console.warn('[ResignationDetail] 加载离职单失败:', error);
         }

+ 21 - 1
src/views/bpm/handleTask/components/resignationHandoverOrder/resignationHandoverOrderDetail.vue

@@ -181,6 +181,20 @@
         </div>
         <p class="detail-description">{{ form.remark }}</p>
       </div>
+
+      <!-- 附件 -->
+      <div class="detail-section-card detail-section-card--gap">
+        <div class="detail-section-head">
+          <h3>附件</h3>
+        </div>
+        <fileMain
+          v-if="approvalFileIds.length"
+          v-model="approvalFileIds"
+          type="view"
+          size="small"
+        />
+        <p v-else class="detail-description">暂无附件</p>
+      </div>
     </div>
   </main>
 </template>
@@ -188,6 +202,7 @@
 <script>
 import { getHandoverById, getLatestItemReceipt } from '@/api/main';
 import { getEmployeeById } from '@/api/organization';
+import { parseAttachmentIds } from '@/utils/attachment';
 
 /** 总体交接状态 */
 const HANDOVER_STATUS_LABELS = {
@@ -307,7 +322,9 @@ export default {
       /** 该员工最新入职物品领用单号 */
       receiptCode: '',
       /** 员工缓存:接收人 / 交接人姓名回显 */
-      employeeMap: {}
+      employeeMap: {},
+      /** 附件文件 ID 列表 */
+      approvalFileIds: []
     };
   },
   computed: {
@@ -352,6 +369,8 @@ export default {
         const detail = await getHandoverById(id);
         this.form = this.mapForm(detail);
         this.items = this.mapItems(detail);
+        // 兼容多种附件字段来源(attachmentIds / approvalDocumentFileId / attachmentFileId 等)
+        this.approvalFileIds = parseAttachmentIds(detail);
         await Promise.all([
           this.loadEmployee(this.form.userId),
           this.loadReceivers(detail),
@@ -363,6 +382,7 @@ export default {
         this.assetRows = [];
         this.receiptCode = '';
         this.employeeMap = {};
+        this.approvalFileIds = [];
         if (process.env.NODE_ENV !== 'production') {
           console.warn('[ResignationHandoverOrderDetail] 加载交接单失败:', error);
         }

+ 73 - 23
src/views/bpm/handleTask/index.vue

@@ -340,6 +340,60 @@
 
   // import { getDate } from '@/utils/dateUtils';
 
+  // 已有审批组件的流程未配路由时,使用的本项目约定路径(按流程标识匹配)
+  const FORM_FALLBACK_MAP = {
+    contact_approve: {
+      pcView: '/bpm/handleTask/components/contact/contactDetailDialog.vue',
+      pcHandle: '/bpm/handleTask/components/contact/submit.vue'
+    },
+    hr_recruit_hiring_approval: {
+      pcView:
+        '/bpm/handleTask/components/hiringApproval/hiringApprovalDetail.vue',
+      pcHandle: '/bpm/handleTask/components/hiringApproval/submit.vue'
+    },
+    hr_contract_approval: {
+      pcView:
+        '/bpm/handleTask/components/hrContractManagement/hrContractDetail.vue',
+      pcHandle: '/bpm/handleTask/components/hrContractManagement/submit.vue'
+    },
+    hr_contract_termination_approval: {
+      pcView:
+        '/bpm/handleTask/components/hrRemoveContractManagement/hrRemoveContractDetail.vue',
+      pcHandle:
+        '/bpm/handleTask/components/hrRemoveContractManagement/submit.vue'
+    },
+    hr_contract_change_approval: {
+      pcView:
+        '/bpm/handleTask/components/hrChangeContractManagement/hrChangeContractDetail.vue',
+      pcHandle:
+        '/bpm/handleTask/components/hrChangeContractManagement/submit.vue'
+    },
+    // 考勤申请:补卡 / 请假 / 外出 / 加班 / 出差
+    hr_attendance_card_repair_apply: {
+      pcView:
+        '/bpm/handleTask/components/hrCardRepairManagement/hrCardRepairDetail.vue',
+      pcHandle: '/bpm/handleTask/components/hrCardRepairManagement/submit.vue'
+    },
+    hr_attendance_leave_apply: {
+      pcView: '/bpm/handleTask/components/hrLeaveManagement/hrLeaveDetail.vue',
+      pcHandle: '/bpm/handleTask/components/hrLeaveManagement/submit.vue'
+    },
+    hr_attendance_outing_apply: {
+      pcView:
+        '/bpm/handleTask/components/hrOutingManagement/hrOutingDetail.vue',
+      pcHandle: '/bpm/handleTask/components/hrOutingManagement/submit.vue'
+    },
+    hr_attendance_overtime_apply: {
+      pcView:
+        '/bpm/handleTask/components/hrOvertimeManagement/hrOvertimeDetail.vue',
+      pcHandle: '/bpm/handleTask/components/hrOvertimeManagement/submit.vue'
+    },
+    hr_attendance_trip_apply: {
+      pcView: '/bpm/handleTask/components/hrTripManagement/hrTripDetail.vue',
+      pcHandle: '/bpm/handleTask/components/hrTripManagement/submit.vue'
+    }
+  };
+
   // 流程实例的详情页,可用于审批
   export default {
     name: 'ProcessInstanceDetail',
@@ -497,29 +551,8 @@
             this.listData.processDefinitionKey ||
             ''
         );
-        const formFallback =
-          businessKey === 'contact_approve'
-            ? {
-                pcView:
-                  '/bpm/handleTask/components/contact/contactDetailDialog.vue',
-                pcHandle: '/bpm/handleTask/components/contact/submit.vue'
-              }
-            : businessKey === 'hr_recruit_hiring_approval'
-            ? {
-                pcView:
-                  '/bpm/handleTask/components/hiringApproval/hiringApprovalDetail.vue',
-                pcHandle:
-                  '/bpm/handleTask/components/hiringApproval/submit.vue'
-              }
-            : businessKey === 'hr_contract_approval'
-            ? {
-                pcView:
-                  '/bpm/handleTask/components/hrContractManagement/hrContractDetail.vue',
-                pcHandle:
-                  '/bpm/handleTask/components/hrContractManagement/submit.vue'
-              }
-            : {};
-        return {
+        const formFallback = FORM_FALLBACK_MAP[businessKey] || {};
+        const resolved = {
           view:
             fromTodo.view ||
             this.normalizeViewPath(fromDef.pcView || fromDef.pcViewRouter) ||
@@ -531,6 +564,23 @@
             ) ||
             this.normalizeViewPath(formFallback.pcHandle)
         };
+        // 排查路由串台:打印三级来源,确认最终组件路径是谁给的
+        if (process.env.NODE_ENV !== 'production') {
+          // eslint-disable-next-line no-console
+          console.log('[流程表单路由解析]', {
+            businessKey,
+            '①待办数据': fromTodo,
+            '②流程定义原始': {
+              key: def.key,
+              formCustomCreatePath: def.formCustomCreatePath,
+              formKey: def.formKey
+            },
+            '②流程定义解析': fromDef,
+            '③前端兜底': formFallback,
+            最终: resolved
+          });
+        }
+        return resolved;
       },
       getBusinessComponentRouter(type) {
         const routers = this.resolveFormRouters();

+ 2 - 2
vue.config.js

@@ -44,9 +44,9 @@ module.exports = {
         // target: 'http://192.168.1.251:18186',
         // target: 'http://192.168.1.251:18086',
         // target: 'http://192.168.1.3:18086',
-        target: 'http://aiot.zoomwin.com.cn:51001/api',
+        // target: 'http://aiot.zoomwin.com.cn:51001/api',
         // target: 'http://192.168.1.3:18086',
-        // target: 'http://192.168.1.147:18086', // 陈兵
+        target: 'http://192.168.1.147:18086', // 陈兵
         // target: 'http://192.168.1.116:18086', // 赵沙金
         // target: 'http://192.168.1.113:18086', // 郭丹