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

feat(考勤审批): 新增补卡/请假/外出/加班/出差审批详情页,接入 ESS 统一申请详情与考勤明细两步取数

详情取数顺序:ESS 管理端统一申请详情(申请人/组织/编号)→ 考勤申请详情(来源业务主键 businessId)→ 明细缺失时回退流程实例 formVariables;并补全 handleTask 的流程组件映射
lihaiting 1 неделя назад
Родитель
Сommit
7092e27d85

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

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

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

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

+ 49 - 29
src/views/bpm/handleTask/index.vue

@@ -340,6 +340,54 @@
 
   // 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_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,35 +545,7 @@
             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'
-              }
-            : businessKey === 'hr_contract_termination_approval'
-            ? {
-                pcView:
-                  '/bpm/handleTask/components/hrRemoveContractManagement/hrRemoveContractDetail.vue',
-                pcHandle:
-                  '/bpm/handleTask/components/hrRemoveContractManagement/submit.vue'
-              }
-            : {};
+        const formFallback = FORM_FALLBACK_MAP[businessKey] || {};
         return {
           view:
             fromTodo.view ||