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