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

组织机构选择部门时的岗位选择

ZGC4846 6 часов назад
Родитель
Сommit
74072e1308

+ 0 - 10
.claude/settings.local.json

@@ -1,10 +0,0 @@
-{
-  "permissions": {
-    "allow": [
-      "Bash(git checkout *)",
-      "Bash(python *)",
-      "Bash(node *)",
-      "Bash(xargs grep *)"
-    ]
-  }
-}

+ 2 - 1
.gitignore

@@ -23,4 +23,5 @@ yarn-error.log*
 *.sw?
 *.sw?
 *.vue.config.js
 *.vue.config.js
 package-lock.json
 package-lock.json
-vue.config.js
+vue.config.js
+.claude

+ 64 - 0
src/api/system/organization/index.js

@@ -268,3 +268,67 @@ export async function getHrPositionById(id) {
   }
   }
   return Promise.reject(new Error(res.data.message));
   return Promise.reject(new Error(res.data.message));
 }
 }
+
+/**
+ * 获取岗位序列列表
+ */
+export async function getPositionSequenceList() {
+  const res = await request.get('/hr/positionSequence/getPositionSequenceList');
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 获取岗位性质字典
+ * 归一化为 [{ id, name }],兼容后端返回数组/对象及 dictLabel/dictValue 等字段名
+ * id 用于提交(position_type),name 用于展示(position_type_name)
+ */
+export async function getPositionNatureDict() {
+  const res = await request.get('/system/dict/getByCode/position_nature', {});
+  if (res.data.code != 0) {
+    return Promise.reject(new Error(res.data.message));
+  }
+  const data = res.data.data;
+  // 后端可能返回对象 { general_positions: "一般岗" },统一转成数组
+  const list = Array.isArray(data)
+    ? data
+    : data && typeof data === 'object'
+    ? Object.entries(data).map(([k, v]) =>
+        v && typeof v === 'object' && !Array.isArray(v)
+          ? v
+          : { key: k, value: v }
+      )
+    : [];
+  return list
+    .map((item) => {
+      if (item === null || typeof item !== 'object') {
+        const text = String(item ?? '');
+        return { id: text, name: text };
+      }
+      // 标准字段:dictValue/dictCode 等
+      const name =
+        item.dictValue ??
+        item.value ??
+        item.dictLabel ??
+        item.label ??
+        item.name;
+      let id = item.dictCode ?? item.code ?? item.key;
+      if (id === 0 || id === '0') id = undefined;
+      if (name !== undefined || id !== undefined) {
+        const nameText = String(name ?? id ?? '');
+        return { id: String(id ?? nameText), name: nameText };
+      }
+      // 单键值对对象 { "1": "全职" }:key 为编码(id),value 为文案(name)
+      const entries = Object.entries(item).filter(
+        ([, v]) => v !== undefined && v !== null && v !== ''
+      );
+      if (entries.length >= 1) {
+        const [mapKey, mapVal] = entries[0];
+        return { id: String(mapKey), name: String(mapVal) };
+      }
+      return null;
+    })
+    .filter((item) => item && item.name);
+}

+ 223 - 26
src/views/enterpriseModel/dept/components/org-edit.vue

@@ -1,7 +1,7 @@
 <!-- 机构编辑弹窗 -->
 <!-- 机构编辑弹窗 -->
 <template>
 <template>
   <ele-modal
   <ele-modal
-    width="880px"
+    width="1080px"
     :visible="visible"
     :visible="visible"
     :close-on-click-modal="false"
     :close-on-click-modal="false"
     custom-class="ele-dialog-form"
     custom-class="ele-dialog-form"
@@ -241,6 +241,27 @@
       class="dept-position-table"
       class="dept-position-table"
     >
     >
       <el-table-column type="index" label="序号" width="80" align="center" />
       <el-table-column type="index" label="序号" width="80" align="center" />
+      <el-table-column label="岗位序列" min-width="140" align="center">
+        <template slot-scope="{ row }">
+          <el-select
+            v-if="!row.id"
+            v-model="row.positionSequenceId"
+            filterable
+            clearable
+            placeholder="请选择"
+            style="width: 100%"
+            @change="onDeptPositionSequenceChange(row)"
+          >
+            <el-option
+              v-for="item in positionSequenceList"
+              :key="item.id"
+              :label="item.sequence"
+              :value="item.id"
+            />
+          </el-select>
+          <span v-else>{{ row.positionSequence }}</span>
+        </template>
+      </el-table-column>
       <el-table-column label="岗位名称" min-width="160" align="center">
       <el-table-column label="岗位名称" min-width="160" align="center">
         <template slot-scope="{ row }">
         <template slot-scope="{ row }">
           <el-select
           <el-select
@@ -250,12 +271,13 @@
             clearable
             clearable
             placeholder="请选择"
             placeholder="请选择"
             style="width: 100%"
             style="width: 100%"
+            :disabled="!row.positionSequenceId"
             @change="onDeptPositionNameChange(row)"
             @change="onDeptPositionNameChange(row)"
           >
           >
             <el-option
             <el-option
-              v-for="item in positionList"
+              v-for="item in getPositionsBySequence(row.positionSequenceId)"
               :key="item.id"
               :key="item.id"
-              :label="item.positionName"
+              :label="formatPositionLabel(item)"
               :value="item.id"
               :value="item.id"
             />
             />
           </el-select>
           </el-select>
@@ -284,6 +306,27 @@
           <span v-else>{{ row.positionLevel }}</span>
           <span v-else>{{ row.positionLevel }}</span>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
+      <el-table-column label="岗位性质" min-width="140" align="center">
+        <template slot-scope="{ row }">
+          <el-select
+            v-if="!row.id"
+            v-model="row.positionType"
+            filterable
+            clearable
+            placeholder="请选择"
+            style="width: 100%"
+            @change="onDeptPositionTypeChange(row)"
+          >
+            <el-option
+              v-for="item in positionNatureOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+          <span v-else>{{ row.positionTypeName }}</span>
+        </template>
+      </el-table-column>
       <el-table-column label="编制人数" min-width="120" align="center">
       <el-table-column label="编制人数" min-width="120" align="center">
         <template slot-scope="{ row }">
         <template slot-scope="{ row }">
           <el-input-number
           <el-input-number
@@ -465,7 +508,9 @@
     saveDepartmentPositionBatch,
     saveDepartmentPositionBatch,
     deleteDepartmentPosition,
     deleteDepartmentPosition,
     getHrPositionPage,
     getHrPositionPage,
-    getHrPositionById
+    getHrPositionById,
+    getPositionSequenceList,
+    getPositionNatureDict
   } from '@/api/system/organization';
   } from '@/api/system/organization';
   import { basicAreaPageAPI } from '@/api/regionalManage';
   import { basicAreaPageAPI } from '@/api/regionalManage';
   import { cityDataLabel } from 'ele-admin/packages/utils/regions';
   import { cityDataLabel } from 'ele-admin/packages/utils/regions';
@@ -544,7 +589,9 @@
         // 是否是修改
         // 是否是修改
         isUpdate: false,
         isUpdate: false,
         departmentPositionList: [],
         departmentPositionList: [],
-        positionList: []
+        positionList: [],
+        positionSequenceList: [],
+        positionNatureOptions: []
       };
       };
     },
     },
     computed: {
     computed: {
@@ -615,6 +662,10 @@
           id: null,
           id: null,
           positionId: '',
           positionId: '',
           positionName: '',
           positionName: '',
+          positionSequenceId: '',
+          positionSequence: '',
+          positionType: '',
+          positionTypeName: '',
           positionLevel: '',
           positionLevel: '',
           positionLevelId: '',
           positionLevelId: '',
           establishmentCount: undefined,
           establishmentCount: undefined,
@@ -656,6 +707,10 @@
             deptId: this.form.id,
             deptId: this.form.id,
             positionId: row.positionId,
             positionId: row.positionId,
             positionName: row.positionName,
             positionName: row.positionName,
+            positionSequenceId: row.positionSequenceId,
+            positionSequence: row.positionSequence,
+            positionType: row.positionType,
+            positionTypeName: row.positionTypeName,
             positionLevelId: row.positionLevelId,
             positionLevelId: row.positionLevelId,
             positionLevel: row.positionLevel,
             positionLevel: row.positionLevel,
             establishmentCount: Number(row.establishmentCount)
             establishmentCount: Number(row.establishmentCount)
@@ -703,35 +758,157 @@
         } catch (e) {}
         } catch (e) {}
       },
       },
       async ensurePositionOptions() {
       async ensurePositionOptions() {
-        if (this.positionList.length) {
-          return;
+        // 防重:并发调用复用同一 Promise,避免接口被请求两次
+        if (this._ensurePositionPromise) {
+          return this._ensurePositionPromise;
         }
         }
-        const res = await getHrPositionPage({
-          pageNum: 1,
-          size: 9999,
-          status: 1
-        });
-        this.positionList = (res && res.list) || [];
+        this._ensurePositionPromise = (async () => {
+          try {
+            if (!this.positionList.length) {
+              const res = await getHrPositionPage({
+                pageNum: 1,
+                size: 9999,
+                status: 1
+              });
+              this.positionList = (res && res.list) || [];
+            }
+            if (!this.positionSequenceList.length) {
+              try {
+                const res = await getPositionSequenceList();
+                this.positionSequenceList = (Array.isArray(res)
+                  ? res
+                  : []
+                ).filter((item) => Number(item.status) === 1);
+              } catch (e) {
+                this.positionSequenceList = [];
+              }
+            }
+            // 加载岗位性质字典(无静态兜底)
+            if (!this.positionNatureOptions.length) {
+              try {
+                const res = await getPositionNatureDict();
+                this.positionNatureOptions = Array.isArray(res) ? res : [];
+              } catch (e) {
+                this.positionNatureOptions = [];
+              }
+            }
+          } finally {
+            this._ensurePositionPromise = null;
+          }
+        })();
+        return this._ensurePositionPromise;
       },
       },
       async onDeptPositionNameChange(row) {
       async onDeptPositionNameChange(row) {
         row.positionLevel = '';
         row.positionLevel = '';
         row.positionLevelId = '';
         row.positionLevelId = '';
         row.levelOptions = [];
         row.levelOptions = [];
+        row.positionSequenceId = '';
+        row.positionSequence = '';
+        row.positionType = '';
+        row.positionTypeName = '';
         const position = this.positionList.find(
         const position = this.positionList.find(
           (item) => item.id === row.positionId
           (item) => item.id === row.positionId
         );
         );
         row.positionName = position ? position.positionName : '';
         row.positionName = position ? position.positionName : '';
+        // 自动带出岗位序列 id+名称
+        if (position && (position.positionSequenceId || position.sequenceId)) {
+          row.positionSequenceId = position.positionSequenceId || position.sequenceId;
+          row.positionSequence =
+            position.positionSequence || position.sequenceName || '';
+        }
         if (!row.positionId) {
         if (!row.positionId) {
           return;
           return;
         }
         }
         const detail = await getHrPositionById(row.positionId);
         const detail = await getHrPositionById(row.positionId);
+        // levelConfigs 项层级id可能是 positionLevelId 或 id,层级名可能是 positionLevel 或 level
         const configs = (detail && detail.levelConfigs) || [];
         const configs = (detail && detail.levelConfigs) || [];
         row.levelOptions = configs
         row.levelOptions = configs
-          .filter((item) => item.positionLevelId)
+          .filter(
+            (item) =>
+              (item.positionLevelId ?? item.id) &&
+              (item.positionLevel || item.level)
+          )
           .map((item) => ({
           .map((item) => ({
-            positionLevelId: item.positionLevelId,
-            positionLevel: item.positionLevel
+            positionLevelId: item.positionLevelId ?? item.id,
+            positionLevel: item.positionLevel || item.level || ''
           }));
           }));
+        // 详情接口的岗位序列覆盖列表值
+        if (detail && (detail.positionSequenceId || detail.sequenceId)) {
+          row.positionSequenceId = detail.positionSequenceId || detail.sequenceId;
+          row.positionSequence =
+            detail.positionSequence || detail.sequenceName || '';
+        }
+        // 详情/列表的岗位性质可能是 id 或名称,通过字典反查统一为 id+名称
+        const typeRaw =
+          (detail && detail.positionType) ||
+          (position && position.positionType) ||
+          '';
+        if (typeRaw) {
+          if (!this.positionNatureOptions.length) {
+            await this.ensurePositionOptions();
+          }
+          const matched = this.positionNatureOptions.find(
+            (item) =>
+              String(item.id) === String(typeRaw) || item.name === typeRaw
+          );
+          if (matched) {
+            row.positionType = matched.id;
+            row.positionTypeName = matched.name;
+          } else {
+            row.positionType = typeRaw;
+            row.positionTypeName =
+              (detail && detail.positionTypeName) ||
+              (position && position.positionTypeName) ||
+              '';
+          }
+        }
+      },
+      onDeptPositionSequenceChange(row) {
+        // 选中序列时同步名称
+        const seq = this.positionSequenceList.find(
+          (item) => item.id === row.positionSequenceId
+        );
+        row.positionSequence = seq ? seq.sequence : '';
+        // 切换序列时清空岗位名称、层级、性质
+        row.positionId = '';
+        row.positionName = '';
+        row.positionLevel = '';
+        row.positionLevelId = '';
+        row.positionType = '';
+        row.positionTypeName = '';
+        row.levelOptions = [];
+      },
+      onDeptPositionTypeChange(row) {
+        // 选中岗位性质时同步名称
+        const type = this.positionNatureOptions.find(
+          (item) => item.id === row.positionType
+        );
+        row.positionTypeName = type ? type.name : '';
+      },
+      // 按岗位序列过滤岗位名称选项(未选序列时返回全部)
+      getPositionsBySequence(sequenceId) {
+        if (!sequenceId) return this.positionList;
+        return this.positionList.filter(
+          (item) =>
+            String(item.positionSequenceId ?? item.sequenceId ?? '') ===
+            String(sequenceId)
+        );
+      },
+      // 岗位名称拼接岗位性质:测试人员(全职)
+      formatPositionLabel(item) {
+        if (!item) return '';
+        let type = item.positionTypeName || '';
+        if (!type && item.positionType) {
+          const matched = this.positionNatureOptions.find(
+            (m) =>
+              String(m.id) === String(item.positionType) ||
+              m.name === item.positionType
+          );
+          type = matched ? matched.name : '';
+        }
+        return type
+          ? `${item.positionName}(${type})`
+          : item.positionName;
       },
       },
       onDeptPositionLevelChange(row) {
       onDeptPositionLevelChange(row) {
         const level = (row.levelOptions || []).find(
         const level = (row.levelOptions || []).find(
@@ -744,20 +921,40 @@
           this.departmentPositionList = [];
           this.departmentPositionList = [];
           return;
           return;
         }
         }
-        await this.ensurePositionOptions();
-        const res = await getDepartmentPositionPage({
-          deptId: this.form.id,
-          pageNum: 1,
-          size: 9999
-        });
-        this.departmentPositionList = ((res && res.list) || []).map((item) => ({
-          ...item,
-          levelOptions: []
-        }));
+        // 防重:visible 与 form.type watch 会并发触发,复用同一请求
+        const deptId = this.form.id;
+        if (
+          this._loadDeptPromise &&
+          this._loadDeptId === deptId
+        ) {
+          return this._loadDeptPromise;
+        }
+        this._loadDeptId = deptId;
+        this._loadDeptPromise = (async () => {
+          try {
+            await this.ensurePositionOptions();
+            const res = await getDepartmentPositionPage({
+              deptId,
+              pageNum: 1,
+              size: 9999
+            });
+            this.departmentPositionList = ((res && res.list) || []).map(
+              (item) => ({
+                ...item,
+                levelOptions: []
+              })
+            );
+          } finally {
+            this._loadDeptPromise = null;
+          }
+        })();
+        return this._loadDeptPromise;
       },
       },
       resetDepartmentPosition() {
       resetDepartmentPosition() {
         this.departmentPositionList = [];
         this.departmentPositionList = [];
         this.positionList = [];
         this.positionList = [];
+        this.positionSequenceList = [];
+        this.positionNatureOptions = [];
       },
       },
       formValidate() {
       formValidate() {
         return new Promise((resolve, reject) => {
         return new Promise((resolve, reject) => {