Explorar o código

岗位档案推荐培训改按字典 id 收集与回显并去掉手输(api/hr、PositionEditDialog);工种列表名称列前置且点名称开编辑、工时费空值兜底 -;入职办理二维码邀请隐藏入职资料表单并跳过校验、去掉有效天数

lihaiting hai 4 días
pai
achega
bb1ca99866

+ 13 - 12
src/api/hr/index.js

@@ -219,10 +219,11 @@ function adaptLevelConfig(cfg = {}) {
       category: item.certificateType || "",
       certificateId: item.certificateId,
     })),
-    // 推荐培训:字典项 value 作为课程标识,回写/回显统一走 trainingName 字段
+    // 推荐培训:接口按字典 id(trainingId)回显,与下拉选项 value 对齐
     trainings: (cfg.trainings || [])
-      .map((item) => item.trainingName)
-      .filter(Boolean),
+      .map((item) => item?.trainingId ?? item?.id ?? null)
+      .filter((value) => value !== null && value !== undefined && value !== "")
+      .map((value) => String(value)),
     promote: {
       paths: paths.length ? paths : [{ up: "", down: "", lateral: "" }],
       conditions,
@@ -345,9 +346,11 @@ export function adaptPosition(raw = {}) {
       name: item.certificateName || "",
       category: item.certificateType || "",
     })),
+    // 推荐培训:接口按字典 id(trainingId)回显,字符串化以匹配下拉选项 value
     trainings: (raw.trainings || [])
-      .map((item) => item.trainingName)
-      .filter(Boolean),
+      .map((item) => item?.trainingId ?? item?.id ?? null)
+      .filter((value) => value !== null && value !== undefined && value !== "")
+      .map((value) => String(value)),
     promote: { paths: pathsForForm, up, down, lateral, conditions },
     promotions,
     employees: (raw.activeEmployees || []).map((item) => ({
@@ -480,16 +483,14 @@ function buildLevelConfig(level = {}, positionList = [], positionSequence = "")
         sortNo: index + 1,
       }),
     );
-  // 推荐培训:表单值是字典项 value(字符串),接口按 trainingName 回写;
-  // 兼容对象形态(含 trainingName 的历史数据),否则重进编辑会丢失已选课程
+  // 推荐培训:提交字典 id(trainingId),不再提交名称
   const trainings = (level.trainings || [])
     .map((item) =>
-      typeof item === "string"
-        ? item.trim()
-        : String(item?.trainingName ?? item?.name ?? "").trim(),
+      item && typeof item === "object" ? (item.trainingId ?? item.id) : item,
     )
-    .filter(Boolean)
-    .map((trainingName) => ({ trainingName }));
+    .map((value) => keepBigIntId(value))
+    .filter((value) => value !== undefined && value !== null && value !== "")
+    .map((trainingId) => ({ trainingId }));
 
   const sequenceText = String(
     positionSequence || level.positionSequence || "",

+ 25 - 10
src/views/hrManagement/jobManagement/index.vue

@@ -30,6 +30,11 @@
             }}</el-link>
           <span v-else>{{ row.code }}</span>
         </template>
+        <template v-slot:name="{ row }">
+          <el-link v-if="row.name" type="primary" :underline="false" @click.stop="openEdit(row, true)">{{ row.name
+            }}</el-link>
+          <span v-else>{{ row.name }}</span>
+        </template>
 
         <!-- 操作列 -->
         <template v-slot:action="{ row }">
@@ -47,6 +52,16 @@
             </template>
           </el-popconfirm>
         </template>
+
+        <!-- 标准工时费:未填写时不显示 undefined,统一兜底为 '-' -->
+        <template v-slot:hourCost="{ row }">
+          <span>{{ row.hourCost != null && row.hourCost !== '' ? row.hourCost : '-' }}</span>
+        </template>
+
+        <!-- 工时费单位:未填写时兜底为 '-' -->
+        <template v-slot:unit="{ row }">
+          <span>{{ row.unit || '-' }}</span>
+        </template>
       </ele-pro-table>
     </el-card>
     <!-- 编辑弹窗 -->
@@ -116,14 +131,12 @@ export default {
           align: 'center'
         },
         {
-          slot: 'type',
-          label: '类型',
+          slot: 'name',
+          prop: 'name',
+          label: '名称',
           showOverflowTooltip: true,
           align: 'left',
-          minWidth: 110,
-          formatter: (_row) => {
-            return this.getDictValue('工种类型', _row.type) || '-';
-          }
+          minWidth: 110
         },
         {
           align: 'left',
@@ -134,12 +147,14 @@ export default {
           minWidth: 110
         },
         {
-          slot: 'name',
-          prop: 'name',
-          label: '名称',
+          slot: 'type',
+          label: '类型',
           showOverflowTooltip: true,
           align: 'left',
-          minWidth: 110
+          minWidth: 110,
+          formatter: (_row) => {
+            return this.getDictValue('工种类型', _row.type) || '-';
+          }
         },
         {
           slot: 'level',

+ 4 - 7
src/views/hrManagement/onboardingHandling/index.vue

@@ -131,12 +131,9 @@
                 <el-radio label="invite">二维码邀请</el-radio>
               </el-radio-group>
             </el-form-item>
-            <el-form-item v-if="editing.createMode === 'invite'" label="有效天数">
-              <el-input-number v-model="editing.validDays" :min="1" :max="90" controls-position="right" />
-            </el-form-item>
           </div>
 
-          <!-- 二维码邀请:M02 无邀请码接口,仍需填写入职资料;有链接时展示二维码 -->
+          <!-- 二维码邀请:不填入职资料,只展示邀请二维码(二维码图待后端就绪后接入,暂留空位) -->
           <div v-if="editing.createMode === 'invite'" class="invite-qr-panel">
             <div v-if="qrImageUrl" class="invite-qr-card">
               <img :src="qrImageUrl" alt="入职邀请二维码" />
@@ -145,10 +142,9 @@
                 <el-button slot="append" icon="el-icon-document-copy" @click="copyInviteLink">复制</el-button>
               </el-input>
             </div>
-            <el-empty v-else description="点击「保存」后生成入职邀请二维码" :image-size="80" />
           </div>
 
-          <div class="form-grid">
+          <div v-else class="form-grid">
             <el-form-item label="姓名" prop="name">
               <el-input v-model.trim="editing.name" placeholder="输入姓名后自动带出录用信息" @blur="onNameBlur"
                 @keyup.enter.native="searchHiringByName">
@@ -474,7 +470,6 @@ const empty = () => ({
   age: "",
   probationFlag: 0,
   probationMonths: undefined,
-  validDays: 7,
   createMode: "manual",
   status: "待办理",
   flow: "待补充档案",
@@ -600,6 +595,8 @@ export default {
     },
     rules() {
       if (this.currentStep !== "profile") return {};
+      /* 二维码邀请:入职资料表单整体隐藏,隐藏字段不参与校验,否则「保存」永远过不去 */
+      if (this.editing.createMode === "invite") return {};
       return {
         name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
         hireDate: [

+ 24 - 3
src/views/position/positionManagement/components/PositionEditDialog.vue

@@ -302,8 +302,9 @@
                   </el-table>
 
                   <div class="form-subtitle">推荐培训</div>
-                  <el-select v-model="form.trainings" multiple filterable allow-create default-first-option
-                    class="full-width" placeholder="选择或录入关联培训课程">
+                  <!-- 接口按 trainingId(字典主键)收/回显,选项 value 用字典 id,不再支持手输 -->
+                  <el-select v-model="form.trainings" multiple filterable class="full-width"
+                    placeholder="请选择关联培训课程">
                     <el-option v-for="(item, optIdx) in trainingOptions" :key="'training-' + optIdx + '-' + item.value"
                       :label="item.label" :value="item.value" />
                   </el-select>
@@ -735,9 +736,29 @@ export default {
         // 晋升路径暂时下线,晋升条件字典不再拉取;恢复时取消注释
         // load("Promotion_Conditions", "conditionOptions"),
         load("Required_Skills", "skillLevelOptions"),
-        load("Recommended_Training", "trainingOptions"),
+        this.loadTrainingOptions(),
       ]);
     },
+    /**
+     * 推荐培训:接口按 trainingId(字典主键)收/回显,
+     * 因此选项 value 用字典 id,label 用字典文案(与岗位性质 position_nature 同一口径)
+     */
+    async loadTrainingOptions() {
+      try {
+        const items = await getDictByCode("Recommended_Training");
+        this.trainingOptions = items
+          .map((item) => {
+            const id = String(item.id ?? "").trim();
+            const label = String(item.label ?? "").trim();
+            if (!id || !label) return null;
+            return { label, value: id };
+          })
+          .filter(Boolean);
+      } catch (error) {
+        this.trainingOptions = [];
+        this.$message.error(error.message || "Recommended_Training 字典加载失败");
+      }
+    },
     // 资质证书类别:/system/dict/id_type
     async loadIdTypeOptions() {
       try {