Forráskód Böngészése

合同管理新增作废三类未提交草稿:按 2026-10-20 契约走专用路径(作废变更 /hr/contracts/{id}/changes/{changeId}/void、作废解除 /terminations/{terminationId}/void 均无请求体,作废续签 /renewals/{id}/void 收 reason),不再用 void 接口传草稿主键顶替合同主键,作废原因仅合同/续签草稿可见必填;解除草稿原地修改入参统一为 terminationId;驳回态放开操作并可查看合同/续签/解除各自驳回原因,驳回配色统一 danger,变更与解除的审批中改排除法判定(非草稿非终态即审批中);员工档案导入模板删除与第 3 列重复的 *工号列并新增用户账号列(取 loginName)同步样例行;社保方案编辑弹窗校验失败自动滚动到第一个报错项

lihaiting 2 napja
szülő
commit
e44d9e131e

+ 10 - 0
src/api/hr/index.js

@@ -974,6 +974,16 @@ export const archiveContractOffline = async (id, data) =>
   unwrap(await request.post(`/hr/contracts/${id}/offline/archive`, data));
 export const voidContract = async (id, data) =>
   unwrap(await request.post(`/hr/contracts/${id}/void`, data));
+/* 作废三类「未提交审批」的单据草稿(2026-10-20 契约确认):
+   变更草稿 / 续签草稿 / 解除终止草稿各自挂在原合同下,id 是原合同主键,第二个路径参数是草稿主键。
+   ★ 变更、解除的作废接口只有路径参数、无请求体;续签作废收「劳动合同作废参数」{ reason }。
+   ★ 不要再用 /hr/contracts/{id}/void 传草稿主键顶替合同主键。 */
+export const voidContractChange = async (id, changeId) =>
+  unwrap(await request.post(`/hr/contracts/${id}/changes/${changeId}/void`));
+export const voidContractRenewal = async (id, renewalContractId, data) =>
+  unwrap(await request.post(`/hr/contracts/${id}/renewals/${renewalContractId}/void`, data));
+export const voidContractTermination = async (id, terminationId) =>
+  unwrap(await request.post(`/hr/contracts/${id}/terminations/${terminationId}/void`));
 
 export const getContractUnrenewedReport = async (data = {}) =>
   unwrap(await request.get("/hr/contracts/reports/unrenewed", { params: compact(data) }));

+ 10 - 6
src/utils/employee-template.js

@@ -3,10 +3,10 @@ import * as XLSX from "xlsx";
 /**
  * 员工档案「导入/导出模板」唯一来源。
  *
- * 表头文案、顺序、工作表名、样例行 100% 对齐用户拍板的《员工档案导入模板.xlsx》(28 列,含「微信号」),
+ * 表头文案、顺序、工作表名、样例行 100% 对齐用户拍板的《员工档案导入模板.xlsx》(28 列,含「微信号」「用户账号」),
  * 「导出当前结果 / 导出已选」与「下载导入模板」都从这里生成,两处口径不允许再各写一份。
- * 两处重复表头(第 3 列与第 14 列都叫「*工号」、第 11 列与第 16 列都叫「岗位」)是模板
- * 自身的写法,这里原样保留,不做「优化」,否则回导对不上列。
+ * 2026-10-20 用户拍板:删掉原文模板里第 14 列重复的「*工号」(与第 3 列同名同值)。
+ * 剩余 1 处重复表头(第 11 列「岗位」与第 15 列「*岗位」,原文模板自带)原样保留,不做「优化」,否则回导对不上列。
  *
  * 取值来源说明(均为 GET /main/user/getUserPage 列表行实测字段,无需逐人查详情):
  * - 所属机构名称  ← 组织列表 groupId 对应节点名,回落 groupName
@@ -14,6 +14,7 @@ import * as XLSX from "xlsx";
  * - 所属班组      ← 人员 teamId(逗号串)经「班组」字典翻译
  * - 负责部门编码  ← 人员 deptIds 经组织列表节点 groupCode 翻译
  * - 岗位序列/层级/性质 ← 人员 positionList 主岗位(positionSequenceName / positionLevelName / positionTypeName)
+ * - 用户账号    ← 人员 loginName(getUserPage 行内返回,同员工档案「用户账号」)
  * - 员工状态      ← status 数字码中文(1全职 2兼职 3实习 4正式 5试用 6离职)
  */
 
@@ -94,8 +95,7 @@ export const EMPLOYEE_TEMPLATE_COLUMNS = [
         .join(","),
   },
   { header: "*身份证号", value: (row) => textOf(row.idCard) },
-  /* 与第 3 列同名的模板列,原样保留 */
-  { header: "*工号", value: (row) => textOf(row.employeeNo) },
+  /* 原文模板此处还有 1 列重复的「*工号」(与第 3 列同名同值),2026-10-20 用户拍板删除 */
   { header: "*岗位序列", value: (row) => textOf(primaryBinding(row)?.positionSequenceName) },
   { header: "*岗位", value: (row) => textOf(row.position) },
   {
@@ -119,6 +119,8 @@ export const EMPLOYEE_TEMPLATE_COLUMNS = [
   { header: "毕业院校", value: (row) => textOf(row.school) },
   { header: "生日", value: (row) => toExcelDate(row.birthDate) },
   { header: "微信号", value: (row) => textOf(row.wechat) },
+  /* 「用户账号」= 登录账号,adaptEmployee 里取自后端 loginName,与员工档案「用户账号」同源 */
+  { header: "用户账号", value: (row) => textOf(row.account) },
   { header: "备注:标*号为必填项", value: () => "" },
 ];
 
@@ -150,7 +152,7 @@ export function buildEmployeeTemplateSampleRow() {
     "总经理",
     "G0018,G0019",
     "430904198506061566",
-    null,
+    /* 原第 14 列重复「*工号」样例(null)已随列删除 */
     null,
     null,
     null,
@@ -166,6 +168,8 @@ export function buildEmployeeTemplateSampleRow() {
     /* 生日:原文件为日期序列号 25569 = 1970-01-01 */
     new Date(1970, 0, 1),
     "test001",
+    /* 账号:登录账号示例 */
+    "test001",
     null,
   ];
 }

+ 147 - 46
src/views/contractManagement/index.vue

@@ -117,7 +117,7 @@
                 提交变更
               </el-link>
               <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoidDraft(row, 'CHANGE')">
-                作废
+                作废变更
               </el-link>
             </template>
             <template v-if="isRenewDraftStatus(row)">
@@ -128,7 +128,7 @@
                 提交
               </el-link>
               <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoidDraft(row, 'RENEW')">
-                作废
+                作废续签
               </el-link>
             </template>
             <template v-if="isTerminateDraftStatus(row)">
@@ -140,26 +140,26 @@
               </el-link>
               <el-link type="primary" :underline="false" icon="el-icon-close"
                 @click="openVoidDraft(row, 'TERMINATION')">
-                作废
+                作废解除
               </el-link>
             </template>
           </template>
           <!-- 变更审批中:详情统一从「合同编号 / 员工姓名」点开,这里只留变更审批记录 -->
           <template v-else-if="isChangeApprovingStatus(row)">
             <el-link type="warning" :underline="false" icon="el-icon-tickets" @click="openChangeApprovalRecord(row)">
-              审批记录
+              变更审批
             </el-link>
           </template>
           <!-- 续签审批中 / 签署中:同上,只留续签审批记录 -->
           <template v-else-if="isRenewApprovingStatus(row)">
             <el-link type="warning" :underline="false" icon="el-icon-tickets" @click="openRenewApprovalRecord(row)">
-              审批记录
+              续签审批
             </el-link>
           </template>
           <!-- 解除审批中:任何变更 / 续签 / 解除操作都不允许,只留解除审批记录 -->
           <template v-else-if="isTerminateApprovingStatus(row)">
             <el-link type="warning" :underline="false" icon="el-icon-tickets" @click="openTerminateApprovalRecord(row)">
-              审批记录
+              解除审批
             </el-link>
           </template>
           <template v-else>
@@ -168,7 +168,7 @@
                 编辑
               </el-link>
               <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoid(row)">
-                作废
+                作废合同
               </el-link>
             </template>
             <template v-if="isEffective(row)">
@@ -194,12 +194,34 @@
             <!-- 续签已签署待生效:审批已走完,操作不锁死,保留刚走完的续签审批记录入口 -->
             <el-link v-if="isRenewSignedWaitEffective(row)" type="warning" :underline="false"
               icon="el-icon-tickets" @click="openRenewApprovalRecord(row)">
-              审批记录
+              续签审批
+            </el-link>
+            <!-- 驳回态:流程已结束,操作不锁死,但要能查看驳回原因(合同自身 / 续签 / 解除) -->
+            <!-- 合同审批被驳回:可编辑后再次提交审批(与签订/续签同一个 hr_contract_approval,businessId = 合同主键) -->
+            <el-link v-if="isContractRejected(row)" type="primary" :underline="false" icon="el-icon-edit"
+              @click="openEdit(row)">
+              编辑
+            </el-link>
+            <el-link v-if="isContractRejected(row)" type="primary" :underline="false"
+              icon="el-icon-s-promotion" @click="launchApproval(row)">
+              提交
+            </el-link>
+            <el-link v-if="isContractRejected(row)" type="warning" :underline="false"
+              icon="el-icon-tickets" @click="openApprovalRecord(row)">
+              合同审批
+            </el-link>
+            <el-link v-if="isRenewRejected(row)" type="warning" :underline="false"
+              icon="el-icon-tickets" @click="openRenewApprovalRecord(row)">
+              续签审批
+            </el-link>
+            <el-link v-if="isTerminateRejected(row)" type="warning" :underline="false"
+              icon="el-icon-tickets" @click="openTerminateApprovalRecord(row)">
+              解除审批
             </el-link>
           </template>
           <el-link v-if="isApproving(row)" type="warning" :underline="false" icon="el-icon-tickets"
             @click="openApprovalRecord(row)">
-            审批记录
+            合同审批
           </el-link>
         </template>
         <template slot="empty">
@@ -793,10 +815,12 @@
     <!-- 作废劳动合同 / 草稿弹窗 -->
     <ele-modal :visible.sync="voidVisible" width="480px" :title="voidDialogTitle" append-to-body>
       <el-form ref="voidFormRef" :model="voidForm" :rules="voidRules" label-position="top">
-        <el-form-item label="作废原因" prop="reason">
+        <!-- 作废原因:仅「合同草稿 / 续签草稿」接口收 reason;变更、解除作废接口无请求体 -->
+        <el-form-item v-if="voidNeedsReason" label="作废原因" prop="reason" :required="voidNeedsReason">
           <el-input v-model="voidForm.reason" type="textarea" :rows="4" maxlength="200" show-word-limit
             :placeholder="voidForm.targetType === 'CONTRACT' ? '已签署且生效的合同不能作废' : '请填写作废原因'" />
         </el-form-item>
+        <p v-else class="cm-void-tip">作废后该合同可重新发起变更、续签或解除终止,变更历史仍保留该记录。</p>
       </el-form>
       <span slot="footer">
         <el-button @click="voidVisible = false">取消</el-button>
@@ -845,6 +869,9 @@ import {
   terminateContract,
   archiveContractOffline,
   voidContract,
+  voidContractChange,
+  voidContractRenewal,
+  voidContractTermination,
   getContractTemplatePage,
   getContractTemplateVersions,
   exportContractLedger,
@@ -1144,8 +1171,9 @@ function blankTerminateForm() {
 }
 
 function blankVoidForm() {
-  /* targetType:CONTRACT=作废合同草稿 / CHANGE=作废变更草稿 / RENEW=作废续签草稿 / TERMINATION=作废解除草稿 */
-  return { id: null, reason: '', targetType: 'CONTRACT' };
+  /* targetType:CONTRACT=作废合同草稿 / CHANGE=作废变更草稿 / RENEW=作废续签草稿 / TERMINATION=作废解除草稿
+     contractId = 原合同主键(接口路径的 {id});draftId = 对应草稿单据主键(变更/续签/解除的第二个路径参数) */
+  return { contractId: null, draftId: null, reason: '', targetType: 'CONTRACT' };
 }
 
 function blankArchiveForm() {
@@ -1259,9 +1287,9 @@ export default {
       createVisible: false,
       createMode: 'create',
       changeSnapshot: blankChangeSnapshot(),
-      /* 编辑中的变更草稿主键:保存时带上 = 原地修改(2026-10-19 后端支持);空 = 新建 */
+      /* 编辑中的变更草稿主键:保存时穿透 changeId = 原地修改(不传 = 新建) */
       editingChangeId: '',
-      /* 编辑中的续签草稿主键(= 续签生成的新合同主键):保存时带上 = 原地修改;空 = 新建 */
+      /* 编辑中的续签草稿主键(= 续签生成的新合同主键):保存时穿透 renewalContractId = 原地修改 */
       editingRenewContractId: '',
       createForm: blankCreateForm(),
       createRules: {
@@ -1364,7 +1392,17 @@ export default {
       voidSubmitting: false,
       voidForm: blankVoidForm(),
       voidRules: {
-        reason: [{ required: true, message: '请填写作废原因', trigger: 'blur' }],
+        reason: [
+          {
+            /* 只有收 reason 的接口(合同作废 / 续签草稿作废)才必填 */
+            validator: (_rule, value, callback) => {
+              if (!this.voidNeedsReason) return callback();
+              if (String(value || '').trim()) return callback();
+              callback(new Error('请填写作废原因'));
+            },
+            trigger: 'blur',
+          },
+        ],
       },
       archiveVisible: false,
       archiveSubmitting: false,
@@ -1471,6 +1509,10 @@ export default {
       };
       return map[this.voidForm.targetType] || '作废劳动合同';
     },
+    /** 作废原因只对「合同作废 / 续签草稿作废」可见必填:变更、解除作废接口没有请求体 */
+    voidNeedsReason() {
+      return ['CONTRACT', 'RENEW'].includes(this.voidForm.targetType);
+    },
     isCompareMode() {
       return this.isChangeMode || this.isRenewMode;
     },
@@ -1651,6 +1693,8 @@ export default {
         EXPIRING: 'expiring',
         CHANGED: 'changed',
         RENEWED: 'active',
+        /* 驳回 = 红色(danger),与列表续签/解除状态的驳回配色一致(2026-10-20 用户要求) */
+        REJECTED: 'terminated',
         TERMINATED: 'terminated',
         ENDED: 'terminated',
         CANCELLED: 'expired',
@@ -2983,9 +3027,9 @@ export default {
            随后用该主键 + hr_contract_change_approval 发起审批,终态通过后才把字段应用到合同上。
            所以这里必须把返回的变更单主键带出去,不能用合同 id 顶替。 */
         const changeId = await changeContract(form.id, compact({
-          /* ★ 2026-10-19 后端支持:携带变更草稿主键 = 原地修改该草稿(不传 = 新建);
+          /* ★ 2026-10-20 用户确认:原地修改变更草稿的入参字段名是 changeId(不是 id);
              编辑已有草稿时必须带上,否则会撞「已有草稿」校验 */
-          id: this.editingChangeId || undefined,
+          changeId: this.editingChangeId || undefined,
           /* ★ 变更类型仍是接口枚举必填项,前端目前只有「其他」一种业务口径,固定 OTHER;
              变更原因改为用户在表单里填写(changeReason),不再写死「合同变更」 */
           changeType: 'OTHER',
@@ -3030,9 +3074,10 @@ export default {
             ...payload,
             renewal: true,
             probationMonths: 0,
-            /* ★ 2026-10-19 后端支持:携带续签草稿(新合同)主键 = 原地修改该草稿 */
-            id: this.editingRenewContractId || undefined,
           }),
+          /* ★ 2026-10-20 契约确认:续签草稿主键在 body 顶层(不是 contract 里),字段名 renewalContractId;
+               传入同一原合同下尚未发起审批的续签合同主键时原地更新,不传则新建 */
+          renewalContractId: this.editingRenewContractId || undefined,
         });
         /* 拿不到新合同主键时**不能**退回原合同 id —— 那会把审批发到旧合同上 */
         return { id: this.pickCreatedId(created), payload };
@@ -3060,7 +3105,7 @@ export default {
             : this.isChangeMode
               ? '合同变更已保存'
               : this.isEditMode
-                ? '合同草稿已修改'
+                ? '合同已修改'
                 : '合同已保存',
         );
         this.closeCreate();
@@ -3267,13 +3312,15 @@ export default {
         this.isTerminateDraftStatus(row)
       );
     },
-    /** 变更审批中:变更单已提交、流程未到终态(DRAFT/REJECTED/CANCELLED 之外,且带流程特征) */
+    /**
+     * 变更审批中:有变更状态编码、且不是草稿/终态(REJECTED/CANCELLED/EFFECTIVE)即视为审批中。
+     * ★ 2026-10-20 截图实测:后端会下发枚举注释之外的「审批中」编码(tag 文案「变更审批中」),
+     *   用 /PENDING|APPROV|SIGN/ 白名单猜会漏 → 改成排除法,未知编码按审批中处理。
+     */
     isChangeApprovingStatus(row = {}) {
       const code = String(row.changeStatusCode || '').toUpperCase();
-      if (!code || code === 'DRAFT' || code === 'REJECTED' || code === 'CANCELLED') {
-        return false;
-      }
-      return /PENDING|APPROV|SIGN/.test(code);
+      if (!code) return false;
+      return !['DRAFT', 'REJECTED', 'CANCELLED', 'EFFECTIVE'].includes(code);
     },
     /**
      * 续签审批中 / 签署中:PENDING_APPROVAL、PENDING_SIGN、SIGNING。
@@ -3304,13 +3351,23 @@ export default {
     isRenewSignedWaitEffective(row = {}) {
       return String(row.renewalStatusCode || '').toUpperCase() === 'SIGNED_WAIT_EFFECTIVE';
     },
-    /** 解除审批中:解除单已提交、流程未到终态(口径与变更一致) */
+    /** 解除审批中:口径与变更一致 —— 排除法,非草稿/终态即审批中 */
     isTerminateApprovingStatus(row = {}) {
       const code = String(row.terminationStatusCode || '').toUpperCase();
-      if (!code || code === 'DRAFT' || code === 'REJECTED' || code === 'CANCELLED') {
-        return false;
-      }
-      return /PENDING|APPROV|SIGN/.test(code);
+      if (!code) return false;
+      return !['DRAFT', 'REJECTED', 'CANCELLED', 'EFFECTIVE'].includes(code);
+    },
+    /** 合同自身审批被驳回(签订 / 续签审批未过):流程已结束,但要能看驳回原因 */
+    isContractRejected(row = {}) {
+      return String(row.statusCode || '').toUpperCase() === 'REJECTED';
+    },
+    /** 最近一次续签审批被驳回 */
+    isRenewRejected(row = {}) {
+      return String(row.renewalStatusCode || '').toUpperCase() === 'REJECTED';
+    },
+    /** 最近一次解除审批被驳回 */
+    isTerminateRejected(row = {}) {
+      return String(row.terminationStatusCode || '').toUpperCase() === 'REJECTED';
     },
     /**
      * 解除审批记录:流程实例挂在解除单上(「劳动合同解除终止信息」的 processInstanceId)。
@@ -3358,13 +3415,19 @@ export default {
     /** 变更审批记录:用变更审批的流程实例(changeProcessInstanceId),不是合同自身的审批 */
     async openChangeApprovalRecord(row = {}) {
       let pid = row.changeProcessInstanceId;
-      if (!pid && row.changeId && row.id) {
+      if (!pid && row.id) {
         try {
-          const list = await getContractChanges(row.id);
-          const hit = (Array.isArray(list) ? list : []).find(
-            (item) => String(item.id) === String(row.changeId),
-          );
-          pid = hit?.processInstanceId || hit?.changeProcessInstanceId || '';
+          /* 列表行没带 changeProcessInstanceId 时查变更台账:
+             优先按 changeId 精确取,行没带 changeId 时按变更状态编码匹配同一状态的那份 */
+          const list = this.toCascadeList(await getContractChanges(row.id));
+          const code = String(row.changeStatusCode || '').toUpperCase();
+          const hit =
+            (row.changeId
+              ? list.find((item) => String(item.id) === String(row.changeId))
+              : null) ||
+            list.find((item) => String(item.changeStatus || '').toUpperCase() === code) ||
+            null;
+          pid = hit?.processInstanceId || '';
         } catch (e) {
           /* 走下方统一提示 */
         }
@@ -3614,8 +3677,8 @@ export default {
         this.$message.warning('缺少合同主键,无法编辑');
         return;
       }
-      if (!isDraft(row)) {
-        this.$message.warning('仅未提交审批的合同草稿可以修改');
+      if (!isDraft(row) && !this.isContractRejected(row)) {
+        this.$message.warning('仅草稿或被驳回的合同可以修改');
         return;
       }
       this.createMode = 'edit';
@@ -3675,7 +3738,7 @@ export default {
       this.createMode = 'renew';
       this.createForm = blankCreateForm();
       this.changeSnapshot = blankChangeSnapshot();
-      /* 续签草稿主键(= 续签生成的新合同主键):保存时带上 = 原地修改该草稿 */
+      /* 续签草稿主键(= 续签生成的新合同主键):保存时穿透 renewalContractId 原地修改该草稿 */
       this.editingRenewContractId = draftContractId || '';
       this.createVisible = true;
       this.loading = true;
@@ -3795,8 +3858,9 @@ export default {
       this.terminateSubmitting = true;
       try {
         const result = await terminateContract(form.id, compact({
-          /* ★ 2026-10-19 后端支持:携带解除草稿主键 = 原地修改该草稿(不传 = 新建) */
-          id: this.terminateDraftId || undefined,
+          /* ★ 2026-10-20 契约确认:原地修改解除草稿的入参字段名是 terminationId(不是 id);
+             传入同一原合同下未发起审批的草稿主键时原地更新,不传则新建草稿 */
+          terminationId: this.terminateDraftId || undefined,
           terminationType: form.terminationType,
           reason: form.reason,
           reasonCode: form.reasonCode,
@@ -3842,7 +3906,7 @@ export default {
         this.$message.warning('仅未提交审批的合同草稿可以作废');
         return;
       }
-      this.voidForm = { id: row.id, reason: '', targetType: 'CONTRACT' };
+      this.voidForm = { contractId: row.id, draftId: row.id, reason: '', targetType: 'CONTRACT' };
       this.voidVisible = true;
       this.$nextTick(() => this.$refs.voidFormRef?.clearValidate());
     },
@@ -3871,7 +3935,7 @@ export default {
         );
         return;
       }
-      this.voidForm = { id, reason: '', targetType: flow };
+      this.voidForm = { contractId: row.id, draftId: id, reason: '', targetType: flow };
       this.voidVisible = true;
       this.$nextTick(() => this.$refs.voidFormRef?.clearValidate());
     },
@@ -3885,7 +3949,7 @@ export default {
           );
           return;
         }
-        this.voidForm = { id: draft.id, reason: '', targetType: 'TERMINATION' };
+        this.voidForm = { contractId: row.id, draftId: draft.id, reason: '', targetType: 'TERMINATION' };
         this.voidVisible = true;
         this.$nextTick(() => this.$refs.voidFormRef?.clearValidate());
       } catch (e) {
@@ -3905,8 +3969,37 @@ export default {
           RENEW: '续签草稿已作废',
           TERMINATION: '解除草稿已作废',
         };
-        await voidContract(this.voidForm.id, { reason: this.voidForm.reason });
-        this.$message.success(msgMap[this.voidForm.targetType] || '作废成功');
+        const { contractId, draftId, targetType } = this.voidForm;
+        const body = { reason: this.voidForm.reason };
+        if (!contractId) {
+          this.$message.warning('缺少原合同主键,无法作废');
+          return;
+        }
+        if (targetType === 'CONTRACT') {
+          await voidContract(contractId, body);
+        } else if (targetType === 'RENEW') {
+          /* 续签草稿作废:唯一收「作废参数」的草稿接口 */
+          if (!draftId) {
+            this.$message.warning('缺少续签草稿主键,无法作废');
+            return;
+          }
+          await voidContractRenewal(contractId, draftId, body);
+        } else if (targetType === 'CHANGE' || targetType === 'TERMINATION') {
+          /* 变更 / 解除作废接口只有路径参数,无请求体 */
+          if (!draftId) {
+            this.$message.warning('缺少草稿主键,无法作废');
+            return;
+          }
+          if (targetType === 'CHANGE') {
+            await voidContractChange(contractId, draftId);
+          } else {
+            await voidContractTermination(contractId, draftId);
+          }
+        } else {
+          this.$message.warning('未知的作废对象类型,无法作废');
+          return;
+        }
+        this.$message.success(msgMap[targetType] || '作废成功');
         this.voidVisible = false;
         this.applyFilters();
       } catch (e) {
@@ -3968,4 +4061,12 @@ export default {
 .cm-change-none {
   color: #c0c4cc;
 }
+
+/* 作废弹框:变更/解除草稿作废无需填原因时的说明文案 */
+.cm-void-tip {
+  margin: 0;
+  font-size: 13px;
+  line-height: 1.7;
+  color: #909399;
+}
 </style>

+ 10 - 0
src/views/salary/socialScheme/SchemeEditDialog.vue

@@ -690,6 +690,14 @@ export default {
     revalidateRange(prop) {
       if (this.$refs.form) this.$refs.form.validateField(prop);
     },
+    /** 校验失败时滚到第一个报错的表单项,避免「提示弹了但看不到哪里红」 */
+    scrollToFirstError() {
+      this.$nextTick(() => {
+        const modal = document.querySelector('.social-scheme-modal');
+        const first = modal && modal.querySelector('.el-form-item.is-error');
+        if (first) first.scrollIntoView({ behavior: 'smooth', block: 'center' });
+      });
+    },
     /**
      * 基数区间整体校验(复测 P1-13)
      * 用 await 保证校验结果 synchronous-usable:validateField 的自定义校验器可能异步回调。
@@ -715,6 +723,8 @@ export default {
       }
       if (!valid) {
         this.$message.warning('请先补全表单中的必填项');
+        /* 弹窗内容可滚动:报错字段可能在视口外(如顶部基础信息区),主动滚过去让用户看到红框 */
+        this.scrollToFirstError();
         return;
       }
       if (!(await this.validateBaseRanges())) return;