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

feat(合同变更审批): 新增合同变更审批详情页,接入 hr_contract_change_approval 流程组件映射

详情页按变更前后两列对比展示;同时补充流程表单路由解析的开发环境日志,便于排查组件串台
lihaiting 1 неделя назад
Родитель
Сommit
71644b5521

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

+ 24 - 1
src/views/bpm/handleTask/index.vue

@@ -362,6 +362,12 @@
       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:
@@ -546,7 +552,7 @@
             ''
         );
         const formFallback = FORM_FALLBACK_MAP[businessKey] || {};
-        return {
+        const resolved = {
           view:
             fromTodo.view ||
             this.normalizeViewPath(fromDef.pcView || fromDef.pcViewRouter) ||
@@ -558,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();