Pārlūkot izejas kodu

feat(hr): 重构岗位晋升路径并接入合同模板与招聘需求接口

yusheng 1 nedēļu atpakaļ
vecāks
revīzija
b51867e248

+ 8 - 18
package-lock.json

@@ -11,6 +11,7 @@
         "axios": "^0.27.2",
         "core-js": "^3.25.0",
         "cropperjs": "^1.5.12",
+        "crypto": "^1.0.1",
         "dayjs": "^1.11.9",
         "echarts": "^5.6.0",
         "ele-admin": "^1.11.2",
@@ -1981,9 +1982,6 @@
         "arm"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2005,9 +2003,6 @@
         "arm"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2029,9 +2024,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2053,9 +2045,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2077,9 +2066,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2101,9 +2087,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -4706,6 +4689,13 @@
         "semver": "bin/semver"
       }
     },
+    "node_modules/crypto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz",
+      "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
+      "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
+      "license": "ISC"
+    },
     "node_modules/css-declaration-sorter": {
       "version": "6.4.1",
       "resolved": "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz",

+ 1 - 0
package.json

@@ -12,6 +12,7 @@
     "axios": "^0.27.2",
     "core-js": "^3.25.0",
     "cropperjs": "^1.5.12",
+    "crypto": "^1.0.1",
     "dayjs": "^1.11.9",
     "echarts": "^5.6.0",
     "ele-admin": "^1.11.2",

+ 98 - 39
src/api/hr/index.js

@@ -134,20 +134,34 @@ export function adaptPosition(raw = {}) {
   const relation = raw.departmentPosition || {};
   const seq = sequenceCode(raw.positionSequence || raw.sequenceName);
   const promotions = raw.promotions || [];
-  const up = promotions
-    .map((item) => item.upwardPromotionPositionName)
-    .filter(Boolean);
-  const down = promotions
-    .map((item) => item.downwardDemotionPositionName)
-    .filter(Boolean);
-  const lateral = promotions
-    .map((item) => item.lateralRotationPositionName)
-    .filter(Boolean);
+  // 晋升路径改为多条:每条包含向上/向下/平级岗位
+  const paths = promotions
+    .map((item) => ({
+      up: item.upwardPromotionPositionName || "",
+      down: item.downwardDemotionPositionName || "",
+      lateral: item.lateralRotationPositionName || "",
+    }))
+    .filter((path) => path.up || path.down || path.lateral);
+  // 编辑时至少保留一条空路径
+  const pathsForForm = paths.length ? paths : [{ up: "", down: "", lateral: "" }];
+  const up = paths.map((item) => item.up).filter(Boolean);
+  const down = paths.map((item) => item.down).filter(Boolean);
+  const lateral = paths.map((item) => item.lateral).filter(Boolean);
+  // 公共条件:优先取顶层 conditions,其次兼容旧的 promotions[].conditions
   const conditions = [
     ...new Set(
-      promotions
-        .flatMap((item) =>
-          (item.conditions || []).map((condition) => condition.conditionName),
+      (Array.isArray(raw.conditions) ? raw.conditions : [])
+        .map((condition) =>
+          typeof condition === "string"
+            ? condition
+            : condition.conditionName,
+        )
+        .concat(
+          promotions.flatMap((item) =>
+            (item.conditions || []).map(
+              (condition) => condition.conditionName,
+            ),
+          ),
         )
         .filter(Boolean),
     ),
@@ -163,7 +177,7 @@ export function adaptPosition(raw = {}) {
     deptId: raw.deptId,
     deptName: raw.deptName || "",
     seq,
-    level: levelNumber(raw.positionLevel, seq),
+    level: apiLevel(raw.positionLevel, seq),
     sequenceId: raw.sequenceId || relation.sequenceId,
     type: TYPE_FROM_API[raw.positionType] || 3,
     staffing: Number(
@@ -212,7 +226,7 @@ export function adaptPosition(raw = {}) {
     trainings: (raw.trainings || [])
       .map((item) => item.trainingName)
       .filter(Boolean),
-    promote: { up, down, lateral, conditions },
+    promote: { paths: pathsForForm, up, down, lateral, conditions },
     promotions,
     employees: (raw.activeEmployees || []).map((item) => ({
       ...item,
@@ -232,34 +246,37 @@ export function adaptPosition(raw = {}) {
 
 export function serializePosition(form, positionList = []) {
   const targetId = (name) => positionTarget(positionList, name)?.id || null;
-  const count = Math.max(
-    form.promote?.up?.length || 0,
-    form.promote?.down?.length || 0,
-    form.promote?.lateral?.length || 0,
-  );
-  const promotions = Array.from({ length: count }, (_, index) => {
-    const upName = form.promote.up[index];
-    const downName = form.promote.down[index];
-    const lateralName = form.promote.lateral[index];
-    return compact({
-      upwardPromotionPositionId: targetId(upName),
-      upwardPromotionPositionName: upName,
-      downwardDemotionPositionId: targetId(downName),
-      downwardDemotionPositionName: downName,
-      lateralRotationPositionId: targetId(lateralName),
-      lateralRotationPositionName: lateralName,
-      conditions: (form.promote.conditions || []).map((conditionName) => ({
-        conditionName,
-      })),
-    });
-  });
+  // 公共条件:三类岗位流动共用,顶层提交(旧后端也兼容每条路径携带)
+  const conditions = (form.promote?.conditions || []).map((conditionName) => ({
+    conditionName,
+  }));
+  // 晋升路径改为多条:过滤三项均为空的路径,每条包含向上/向下/平级岗位
+  const promotions = (form.promote?.paths || [])
+    .map((path) =>
+      compact({
+        upwardPromotionPositionId: targetId(path?.up),
+        upwardPromotionPositionName: path?.up,
+        downwardDemotionPositionId: targetId(path?.down),
+        downwardDemotionPositionName: path?.down,
+        lateralRotationPositionId: targetId(path?.lateral),
+        lateralRotationPositionName: path?.lateral,
+        conditions,
+      }),
+    )
+    .filter(
+      (item) =>
+        item.upwardPromotionPositionName ||
+        item.downwardDemotionPositionName ||
+        item.lateralRotationPositionName,
+    );
   const payload = {
     deptId: form.deptId,
     organizationId: form.legalId,
     positionCode: form.code,
+    // positionName 为用户输入的岗位名称、positionSequence 取序列 label、positionLevel 取层级 label
     positionName: form.name,
-    positionSequence: sequenceName(form.seq),
-    positionLevel: apiLevel(form.level, form.seq),
+    positionSequence: form.positionSequence || sequenceName(form.seq),
+    positionLevel: form.positionLevel ?? apiLevel(form.level, form.seq),
     positionType: TYPE_TO_API[form.type] || TYPE_TO_API[3],
     status: STATUS_TO_API[form.status] ?? 0,
     revokeDate: form.status === 3 ? form.revokeDate : undefined,
@@ -309,6 +326,7 @@ export function serializePosition(form, positionList = []) {
       .filter(Boolean)
       .map((trainingName) => ({ trainingName })),
     promotions,
+    conditions,
   };
   if (form.id) payload.id = form.id;
   return payload;
@@ -519,8 +537,23 @@ export async function importPromotions(file) {
 }
 
 // 岗位序列
-export const getPositionSequenceList = async () =>
-  unwrap(await request.get("/hr/positionSequence/getPositionSequenceList"));
+export const getPositionSequenceList = async () => {
+  const list = unwrap(
+    await request.get("/hr/positionSequence/getPositionSequencePullList"),
+  );
+  // 后端返回 [{ sequence, positionName, positionSequence, levelArrayList: [{ id, level, positionLevel }] }]
+  // 映射:positionName → 序列 value(seq)、positionSequence → 序列 label、positionLevel → 层级 value
+  return (Array.isArray(list) ? list : []).map((item) => ({
+    sequence: item.sequence,
+    positionName: item.positionName ?? item.sequence,
+    positionSequence: item.positionSequence ?? item.sequence,
+    children: (item.levelArrayList || []).map((level) => ({
+      id: level.id,
+      label: level.level,
+      value: level.positionLevel ?? level.id,
+    })),
+  }));
+};
 export const getPositionSequenceNames = async () =>
   unwrap(await request.get("/hr/positionSequence/sequences"));
 export const getPositionSequenceLevels = async (sequence) =>
@@ -538,3 +571,29 @@ export const updatePositionSequence = async (data) =>
   unwrap(await request.put("/hr/positionSequence/update", data));
 export const isPositionSequenceInUse = async (id) =>
   unwrap(await request.get(`/hr/positionSequence/${id}/in-use`));
+
+// 劳动合同模板
+export const getContractTemplatePage = async (data = {}) =>
+  unwrap(await request.get("/hr/contracts/templates", compact(data)));
+export const getContractTemplateById = async (id) =>
+  unwrap(await request.get(`/hr/contracts/templates/${id}`));
+export const saveContractTemplate = async (data) =>
+  unwrap(await request.post("/hr/contracts/templates", data));
+export const updateContractTemplate = async (data) =>
+  unwrap(await request.put(`/hr/contracts/templates/${data.id}`, data));
+export const deleteContractTemplate = async (id) =>
+  unwrap(await request.get(`/hr/contracts/templates/delete/${id}`));
+export const disableContractTemplate = async (id) =>
+  unwrap(await request.post(`/hr/contracts/templates/${id}/disable`));
+export const enableContractTemplate = async (id) =>
+  unwrap(await request.post(`/hr/contracts/templates/${id}/enable`));
+
+// 招聘需求
+export const getRecruitDemandPage = async (data = {}) =>
+  unwrap(await request.get("/recruit/demands", { params: compact(data) }));
+export const submitRecruitDemand = async (data) =>
+  unwrap(await request.post("/recruit/demands", compact(data)));
+export const getRecruitDemandById = async (id) =>
+  unwrap(await request.get(`/recruit/demands/${id}`));
+export const changeRecruitDemandStatus = async (id, data) =>
+  unwrap(await request.post(`/recruit/demands/${id}/action`, compact(data)));

+ 27 - 0
src/styles/views/positionManagement/components/PositionEditDialog.scss

@@ -177,6 +177,33 @@
     background: #e9f8f3;
   }
 
+  .promote-path-card {
+    margin-bottom: 14px;
+    padding: 12px 16px 0;
+    border: 1px solid #e3eaf4;
+    border-radius: 10px;
+    background: linear-gradient(135deg, #fbfdff, #f7faff);
+  }
+
+  .promote-path-head {
+    margin-bottom: 2px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .promote-path-index {
+    color: #1768e5;
+    font-size: 13px;
+    font-weight: 700;
+  }
+
+  .promote-conditions {
+    margin-top: 6px;
+    padding-top: 14px;
+    border-top: 1px dashed #e3eaf4;
+  }
+
   .age-field {
     position: relative;
   }

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 102 - 10
src/views/onboardingContractTemplate/index.vue


+ 0 - 6
src/views/onboardingContractTemplate/mock.js

@@ -1,6 +0,0 @@
-const rows=[
- {id:1,code:'CT-STD-001',name:'标准劳动合同(固定期限)',type:'劳动合同',legal:'中盈工业互联网有限公司',enabled:true,version:'V3.2',remark:'适用于正式员工入职',audit:'已发布',updatedAt:'2026-08-10 15:20',fields:[{id:'term',label:'合同期限',required:true},{id:'position',label:'工作岗位',required:true},{id:'location',label:'工作地点',required:true},{id:'probation',label:'试用期',required:false},{id:'salary',label:'薪酬标准',required:true},{id:'confidential',label:'保密义务',required:true}]},
- {id:2,code:'CT-TECH-002',name:'技术岗位劳动合同',type:'劳动合同',legal:'中盈工业互联网有限公司',enabled:true,version:'V2.1',remark:'含保密及知识产权条款',audit:'已发布',updatedAt:'2026-08-06 11:08',fields:[{id:'term',label:'合同期限',required:true},{id:'position',label:'工作岗位',required:true},{id:'location',label:'工作地点',required:true},{id:'salary',label:'薪酬标准',required:true},{id:'confidential',label:'保密义务',required:true},{id:'ip',label:'知识产权',required:true}]},
- {id:3,code:'CT-PROJ-003',name:'项目制用工合同',type:'项目合同',legal:'中盈智能制造有限公司',enabled:false,version:'V1.0',remark:'适用于项目周期内用工',audit:'草稿',updatedAt:'2026-08-02 09:36',fields:[{id:'term',label:'项目周期',required:true},{id:'position',label:'项目岗位',required:true},{id:'location',label:'工作地点',required:true},{id:'salary',label:'项目报酬',required:true}]}
-];
-export function getContractTemplates(){return new Promise(r=>window.setTimeout(()=>r(JSON.parse(JSON.stringify(rows))),160))}

+ 158 - 37
src/views/positionManagement/components/MyPromotionDialog.vue

@@ -1,82 +1,203 @@
 <template>
-  <ele-modal title="我的晋升" :visible="visible" width="820px" append-to-body @update:visible="$emit('update:visible', $event)">
+  <ele-modal
+    title="我的晋升"
+    :visible="visible"
+    width="820px"
+    append-to-body
+    @update:visible="$emit('update:visible', $event)"
+  >
     <div v-loading="loading">
       <template v-if="currentApplication">
         <div class="employee-card">
-          <el-avatar :size="54">{{ initials(currentApplication.employee) }}</el-avatar>
-          <div><strong>{{ currentApplication.employee }}</strong><span>当前岗位:{{ currentApplication.from || '-' }}</span></div>
-          <el-steps :active="activeStep" simple finish-status="success"><el-step title="已提交" /><el-step title="待评审" /><el-step title="评审中" /><el-step title="结果通知" /></el-steps>
+          <el-avatar :size="54">{{
+            initials(currentApplication.employee)
+          }}</el-avatar>
+          <div>
+            <strong>{{ currentApplication.employee }}</strong
+            ><span>当前岗位:{{ currentApplication.from || "-" }}</span>
+          </div>
+          <el-steps :active="activeStep" simple finish-status="success"
+            ><el-step title="已提交" /><el-step title="待评审" /><el-step
+              title="评审中" /><el-step title="结果通知"
+          /></el-steps>
         </div>
         <div class="target-card">
-          <div><small>申请目标</small><h3>{{ currentApplication.to || '-' }}</h3><p>{{ currentApplication.no }} · {{ currentApplication.date }}</p></div>
-          <div class="target-rate"><strong>{{ currentApplication.status }}</strong><span>当前状态</span></div>
+          <div>
+            <small>申请目标</small>
+            <h3>{{ currentApplication.to || "-" }}</h3>
+            <p>{{ currentApplication.no }} · {{ currentApplication.date }}</p>
+          </div>
+          <div class="target-rate">
+            <strong>{{ currentApplication.status }}</strong
+            ><span>当前状态</span>
+          </div>
+        </div>
+        <div class="gap-list">
+          <strong>申请说明</strong>
+          <p>{{ currentApplication.applyReason || "未填写申请说明" }}</p>
         </div>
-        <div class="gap-list"><strong>申请说明</strong><p>{{ currentApplication.applyReason || '未填写申请说明' }}</p></div>
       </template>
       <el-empty v-else description="暂无晋升申请" />
 
       <div v-if="targets.length" class="target-card">
-        <div><small>可晋升目标</small><h3>{{ selectedTarget ? selectedTarget.targetPositionName : '-' }}</h3><p>{{ selectedTarget ? (selectedTarget.conditions || []).join(';') : '' }}</p></div>
-        <el-select v-model="targetPositionId" placeholder="选择晋升目标" @change="selectTarget">
-          <el-option v-for="item in targets" :key="item.targetPositionId" :label="item.targetPositionName" :value="item.targetPositionId" />
+        <div>
+          <small>可晋升目标</small>
+          <h3>
+            {{ selectedTarget ? selectedTarget.targetPositionName : "-" }}
+          </h3>
+          <p>
+            {{
+              selectedTarget ? (selectedTarget.conditions || []).join(";") : ""
+            }}
+          </p>
+        </div>
+        <el-select
+          v-model="targetPositionId"
+          placeholder="选择晋升目标"
+          @change="selectTarget"
+        >
+          <el-option
+            v-for="item in targets"
+            :key="item.targetPositionId"
+            :label="item.targetPositionName"
+            :value="item.targetPositionId"
+          />
         </el-select>
       </div>
-      <el-alert v-else title="暂时无法确定当前岗位的可晋升目标,请先维护员工当前岗位和向上晋升通道。" type="info" :closable="false" show-icon />
-      <el-form label-position="top"><el-form-item label="晋升申请理由"><el-input v-model.trim="reason" type="textarea" :rows="3" maxlength="2000" show-word-limit placeholder="说明工作成果、能力成长与晋升意愿" /></el-form-item></el-form>
+      <el-alert
+        v-else
+        title="暂时无法确定当前岗位的可晋升目标,请先维护员工当前岗位和向上晋升通道。"
+        type="info"
+        :closable="false"
+        show-icon
+      />
+      <el-form label-position="top"
+        ><el-form-item label="晋升申请理由"
+          ><el-input
+            v-model.trim="reason"
+            type="textarea"
+            :rows="3"
+            maxlength="2000"
+            show-word-limit
+            placeholder="说明工作成果、能力成长与晋升意愿" /></el-form-item
+      ></el-form>
     </div>
-    <template v-slot:footer><el-button @click="$emit('update:visible', false)">关闭</el-button><el-button v-if="targets.length" type="primary" :loading="submitting" :disabled="!targetPositionId" @click="submit">申请晋升</el-button></template>
+    <template v-slot:footer
+      ><el-button @click="$emit('update:visible', false)">关闭</el-button
+      ><el-button
+        v-if="targets.length"
+        type="primary"
+        :loading="submitting"
+        :disabled="!targetPositionId"
+        @click="submit"
+        >申请晋升</el-button
+      ></template
+    >
   </ele-modal>
 </template>
 <script>
-import { adaptPromotion, createPromotion, getMyPromotionPage, getPromotionTargets } from '@/api/hr';
+import {
+  adaptPromotion,
+  createPromotion,
+  getMyPromotionPage,
+  getPromotionTargets,
+} from "@/api/hr";
 
 export default {
-  name: 'MyPromotionDialog',
+  name: "MyPromotionDialog",
   props: { visible: Boolean },
   data() {
-    return { loading: false, submitting: false, reason: '', applications: [], targets: [], targetPositionId: null, selectedTarget: null };
+    return {
+      loading: false,
+      submitting: false,
+      reason: "",
+      applications: [],
+      targets: [],
+      targetPositionId: null,
+      selectedTarget: null,
+    };
   },
   computed: {
-    currentApplication() { return this.applications[0] || null; },
-    activeStep() { return { PENDING: 1, REVIEWING: 2, APPROVED: 4, REJECTED: 4, CANCELLED: 4 }[this.currentApplication?.rawStatus] || 0; }
+    currentApplication() {
+      return this.applications[0] || null;
+    },
+    activeStep() {
+      return (
+        { PENDING: 1, REVIEWING: 2, APPROVED: 4, REJECTED: 4, CANCELLED: 4 }[
+          this.currentApplication?.rawStatus
+        ] || 0
+      );
+    },
+  },
+  watch: {
+    visible(value) {
+      if (value) this.loadData();
+    },
   },
-  watch: { visible(value) { if (value) this.loadData(); } },
   methods: {
-    initials(name = '') { return String(name).slice(-2); },
+    initials(name = "") {
+      return String(name).slice(-2);
+    },
     async loadData() {
       this.loading = true;
       try {
-        const page = await getMyPromotionPage({ pageNum: 1, size: 20, sortName: 'createTime', orderBy: 'descending' });
+        const page = await getMyPromotionPage({
+          pageNum: 1,
+          size: 20,
+          sortName: "createTime",
+          orderBy: "descending",
+        });
         this.applications = (page?.list || []).map(adaptPromotion);
         const sourcePositionId = page?.list?.[0]?.sourcePositionId || null;
-        const targetPositions = sourcePositionId ? (await getPromotionTargets(sourcePositionId, 'upward')) || [] : [];
+        const targetPositions = sourcePositionId
+          ? (await getPromotionTargets(sourcePositionId, "upward")) || []
+          : [];
         this.targets = targetPositions.map((item) => ({
           ...item,
           targetPositionId: item.targetPositionId || item.id,
           targetPositionName: item.targetPositionName || item.positionName,
-          conditions: item.conditions || []
+          conditions: item.conditions || [],
         }));
         this.targetPositionId = this.targets[0]?.targetPositionId || null;
         this.selectTarget(this.targetPositionId);
       } catch (error) {
-        this.applications = []; this.targets = [];
-        this.$message.error(error.message || '晋升信息加载失败');
-      } finally { this.loading = false; }
+        this.applications = [];
+        this.targets = [];
+        this.$message.error(error.message || "晋升信息加载失败");
+      } finally {
+        this.loading = false;
+      }
+    },
+    selectTarget(id) {
+      this.selectedTarget =
+        this.targets.find(
+          (item) => String(item.targetPositionId) === String(id),
+        ) || null;
     },
-    selectTarget(id) { this.selectedTarget = this.targets.find((item) => String(item.targetPositionId) === String(id)) || null; },
     async submit() {
-      if (!this.targetPositionId) return this.$message.warning('请选择晋升目标');
+      if (!this.targetPositionId)
+        return this.$message.warning("请选择晋升目标");
       this.submitting = true;
       try {
-        await createPromotion({ targetPositionId: this.targetPositionId, applyReason: this.reason });
-        this.$message.success('晋升申请已提交');
-        this.reason = '';
+        await createPromotion({
+          targetPositionId: this.targetPositionId,
+          applyReason: this.reason,
+        });
+        this.$message.success("晋升申请已提交");
+        this.reason = "";
         await this.loadData();
-        this.$emit('submitted');
-      } catch (error) { this.$message.error(error.message || '晋升申请提交失败'); }
-      finally { this.submitting = false; }
-    }
-  }
+        this.$emit("submitted");
+      } catch (error) {
+        this.$message.error(error.message || "晋升申请提交失败");
+      } finally {
+        this.submitting = false;
+      }
+    },
+  },
 };
 </script>
-<style src="../../../styles/views/positionManagement/components/MyPromotionDialog.scss" lang="scss" scoped></style>
+<style
+  src="../../../styles/views/positionManagement/components/MyPromotionDialog.scss"
+  lang="scss"
+  scoped
+></style>

+ 907 - 334
src/views/positionManagement/components/PositionEditDialog.vue

@@ -13,8 +13,12 @@
     <div class="dialog-intro">
       <div class="intro-icon"><i class="el-icon-collection-tag"></i></div>
       <div>
-        <strong>{{ isUpdate ? '维护岗位说明书与联动规则' : '创建标准化岗位' }}</strong>
-        <p>岗位档案将承载定岗、定薪、定责、技能资质与晋升路径,保存后同步到人力资源岗位主档。</p>
+        <strong>{{
+          isUpdate ? "维护岗位说明书与联动规则" : "创建标准化岗位"
+        }}</strong>
+        <p>
+          岗位档案将承载定岗、定薪、定责、技能资质与晋升路径,保存后同步到人力资源岗位主档。
+        </p>
       </div>
     </div>
 
@@ -24,45 +28,101 @@
           <el-row :gutter="18">
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="岗位编码" prop="code">
-                <el-input v-model.trim="form.code" maxlength="32" placeholder="自动生成或手动输入">
+                <el-input
+                  v-model.trim="form.code"
+                  maxlength="32"
+                  placeholder="自动生成或手动输入"
+                >
                   <i slot="prefix" class="el-icon-cpu"></i>
                 </el-input>
               </el-form-item>
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="岗位名称" prop="name">
-                <el-input v-model.trim="form.name" maxlength="64" placeholder="如:数控车床操作工">
+                <el-input
+                  v-model.trim="form.name"
+                  maxlength="64"
+                  placeholder="如:数控车床操作工"
+                >
                   <i slot="prefix" class="el-icon-suitcase"></i>
                 </el-input>
               </el-form-item>
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="所属法人" prop="legalId">
-                <el-select v-model="form.legalId" class="full-width" placeholder="请选择法人" @change="handleLegalChange">
-                  <el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" />
+                <el-select
+                  v-model="form.legalId"
+                  class="full-width"
+                  placeholder="请选择法人"
+                  @change="handleLegalChange"
+                >
+                  <el-option
+                    v-for="item in legalEntities"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                  />
                 </el-select>
               </el-form-item>
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="所属部门" prop="deptId">
-                <el-select v-model="form.deptId" filterable class="full-width" placeholder="请选择部门">
-                  <el-option v-for="item in departmentOptions" :key="item.id" :label="item.name" :value="item.id" />
+                <el-select
+                  v-model="form.deptId"
+                  filterable
+                  class="full-width"
+                  placeholder="请选择部门"
+                >
+                  <el-option
+                    v-for="item in departmentOptions"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                  />
                 </el-select>
               </el-form-item>
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="岗位序列" prop="seq">
-                <el-radio-group v-model="form.seq" class="seq-radio" @change="handleSequenceChange">
-                  <el-radio-button v-for="item in sequenceOptions" :key="item.id" :label="item.id">{{ item.label.replace('序列', '') }}</el-radio-button>
+                <el-radio-group
+                  v-model="form.seq"
+                  class="seq-radio"
+                  @change="handleSequenceChange"
+                >
+                  <el-radio-button
+                    v-for="item in sequenceOptions"
+                    :key="item.id"
+                    :label="item.id"
+                    >{{ item.label.replace("序列", "") }}</el-radio-button
+                  >
                 </el-radio-group>
               </el-form-item>
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="岗位层级" prop="level">
-                <el-select v-if="configuredLevelOptions.length" v-model="form.level" class="full-width" placeholder="请选择岗位层级" @change="refreshCode">
-                  <el-option v-for="item in configuredLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
+                <el-select
+                  v-if="configuredLevelOptions.length"
+                  v-model="form.level"
+                  class="full-width"
+                  placeholder="请选择岗位层级"
+                  @change="handleLevelChange"
+                >
+                  <el-option
+                    v-for="item in configuredLevelOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
                 </el-select>
-                <el-input-number v-else v-model="form.level" :min="1" :max="20" controls-position="right" class="full-width" @change="refreshCode" />
+                <el-input-number
+                  v-else
+                  v-model="form.level"
+                  :min="1"
+                  :max="20"
+                  controls-position="right"
+                  class="full-width"
+                  @change="handleLevelChange"
+                />
               </el-form-item>
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
@@ -77,12 +137,25 @@
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="岗位编制" prop="staffing">
-                <el-input-number v-model="form.staffing" :min="0" :max="999" controls-position="right" class="full-width" />
+                <el-input-number
+                  v-model="form.staffing"
+                  :min="0"
+                  :max="999"
+                  controls-position="right"
+                  class="full-width"
+                />
               </el-form-item>
             </el-col>
             <el-col :xs="24" :sm="12" :md="8">
               <el-form-item label="在岗人数">
-                <el-input-number v-model="form.actual" :min="0" :max="999" controls-position="right" class="full-width" disabled />
+                <el-input-number
+                  v-model="form.actual"
+                  :min="0"
+                  :max="999"
+                  controls-position="right"
+                  class="full-width"
+                  disabled
+                />
                 <div class="field-hint">由员工入职、调岗和离职记录实时统计</div>
               </el-form-item>
             </el-col>
@@ -97,7 +170,13 @@
             </el-col>
             <el-col v-if="form.status === 3" :xs="24" :sm="12" :md="8">
               <el-form-item label="计划撤销日期">
-                <el-date-picker v-model="form.revokeDate" type="date" value-format="yyyy-MM-dd" :picker-options="revokePickerOptions" class="full-width" />
+                <el-date-picker
+                  v-model="form.revokeDate"
+                  type="date"
+                  value-format="yyyy-MM-dd"
+                  :picker-options="revokePickerOptions"
+                  class="full-width"
+                />
               </el-form-item>
             </el-col>
           </el-row>
@@ -107,50 +186,272 @@
           <div class="duty-layout">
             <section class="form-section salary-section">
               <div class="section-heading">
-                <div><i class="el-icon-coin"></i><span><strong>定薪标准</strong><small>设置岗位薪酬区间及绩效联动规则</small></span></div>
+                <div>
+                  <i class="el-icon-coin"></i
+                  ><span
+                    ><strong>定薪标准</strong
+                    ><small>设置岗位薪酬区间及绩效联动规则</small></span
+                  >
+                </div>
                 <span class="salary-summary">{{ salarySummary }}</span>
               </div>
               <el-row :gutter="16">
-                <el-col :xs="24" :sm="8"><el-form-item label="薪资下限" prop="salaryMin"><el-input-number v-model="form.salaryMin" :min="0" :step="500" controls-position="right" class="full-width" @change="syncSalaryMid" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="8"><el-form-item label="薪资中位值"><el-input-number v-model="form.salaryMid" :min="0" :step="500" controls-position="right" class="full-width" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="8"><el-form-item label="薪资上限" prop="salaryMax"><el-input-number v-model="form.salaryMax" :min="0" :step="500" controls-position="right" class="full-width" @change="syncSalaryMid" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="薪资模板"><el-select v-model="form.salaryTemplateId" filterable allow-create default-first-option class="full-width" placeholder="关联薪资模板名称或编码"><el-option v-for="item in availableSalaryTemplates" :key="item.id" :label="item.name" :value="item.name" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="绩效方案"><el-select v-model="form.perfSchemeId" filterable allow-create default-first-option class="full-width" placeholder="关联绩效方案名称或编码"><el-option v-for="item in performanceSchemes" :key="item.id" :label="item.name" :value="item.name" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="8"><el-form-item label="绩效系数基准"><el-input-number v-model="form.perfBaseCoef" :min="0" :max="3" :step="0.05" controls-position="right" class="full-width" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="8"><el-form-item label="考勤规则组"><el-select v-model="form.attendRuleGroup" class="full-width" placeholder="关联考勤规则"><el-option label="标准白班规则" value="1" /><el-option label="生产倒班规则" value="2" /><el-option label="弹性工时规则" value="3" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="8"><el-form-item label="工作班制"><el-select v-model="form.workShift" class="full-width"><el-option label="白班" value="白班" /><el-option label="两班倒" value="两班倒" /><el-option label="三班倒" value="三班倒" /><el-option label="弹性" value="弹性" /><el-option label="跟班制" value="跟班制" /></el-select></el-form-item></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="薪资下限" prop="salaryMin"
+                    ><el-input-number
+                      v-model="form.salaryMin"
+                      :min="0"
+                      :step="500"
+                      controls-position="right"
+                      class="full-width"
+                      @change="syncSalaryMid" /></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="薪资中位值"
+                    ><el-input-number
+                      v-model="form.salaryMid"
+                      :min="0"
+                      :step="500"
+                      controls-position="right"
+                      class="full-width" /></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="薪资上限" prop="salaryMax"
+                    ><el-input-number
+                      v-model="form.salaryMax"
+                      :min="0"
+                      :step="500"
+                      controls-position="right"
+                      class="full-width"
+                      @change="syncSalaryMid" /></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="12"
+                  ><el-form-item label="薪资模板"
+                    ><el-select
+                      v-model="form.salaryTemplateId"
+                      filterable
+                      allow-create
+                      default-first-option
+                      class="full-width"
+                      placeholder="关联薪资模板名称或编码"
+                      ><el-option
+                        v-for="item in availableSalaryTemplates"
+                        :key="item.id"
+                        :label="item.name"
+                        :value="item.name" /></el-select></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="12"
+                  ><el-form-item label="绩效方案"
+                    ><el-select
+                      v-model="form.perfSchemeId"
+                      filterable
+                      allow-create
+                      default-first-option
+                      class="full-width"
+                      placeholder="关联绩效方案名称或编码"
+                      ><el-option
+                        v-for="item in performanceSchemes"
+                        :key="item.id"
+                        :label="item.name"
+                        :value="item.name" /></el-select></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="绩效系数基准"
+                    ><el-input-number
+                      v-model="form.perfBaseCoef"
+                      :min="0"
+                      :max="3"
+                      :step="0.05"
+                      controls-position="right"
+                      class="full-width" /></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="考勤规则组"
+                    ><el-select
+                      v-model="form.attendRuleGroup"
+                      class="full-width"
+                      placeholder="关联考勤规则"
+                      ><el-option label="标准白班规则" value="1" /><el-option
+                        label="生产倒班规则"
+                        value="2" /><el-option
+                        label="弹性工时规则"
+                        value="3" /></el-select></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="工作班制"
+                    ><el-select v-model="form.workShift" class="full-width"
+                      ><el-option label="白班" value="白班" /><el-option
+                        label="两班倒"
+                        value="两班倒" /><el-option
+                        label="三班倒"
+                        value="三班倒" /><el-option
+                        label="弹性"
+                        value="弹性" /><el-option
+                        label="跟班制"
+                        value="跟班制" /></el-select></el-form-item
+                ></el-col>
               </el-row>
             </section>
 
             <section class="form-section qualification-section">
-              <div class="section-heading"><div><i class="el-icon-user"></i><span><strong>任职条件</strong><small>明确岗位基本资格、工作关系与生效时间</small></span></div></div>
+              <div class="section-heading">
+                <div>
+                  <i class="el-icon-user"></i
+                  ><span
+                    ><strong>任职条件</strong
+                    ><small>明确岗位基本资格、工作关系与生效时间</small></span
+                  >
+                </div>
+              </div>
               <el-row :gutter="16" class="qualification-basics">
-                <el-col :xs="24" :sm="8"><el-form-item label="学历要求"><el-select v-model="form.eduReq" class="full-width"><el-option label="初中及以上" value="初中及以上" /><el-option label="中专及以上" value="中专及以上" /><el-option label="大专及以上" value="大专及以上" /><el-option label="本科及以上" value="本科及以上" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="8"><el-form-item label="性别要求"><el-select v-model="form.genderReq" class="full-width"><el-option label="不限" value="不限" /><el-option label="男" value="男" /><el-option label="女" value="女" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="8"><el-form-item label="经验要求(年)"><el-input-number v-model="form.expReq" :min="0" :max="30" controls-position="right" class="full-width" /></el-form-item></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="学历要求"
+                    ><el-select v-model="form.eduReq" class="full-width"
+                      ><el-option
+                        label="初中及以上"
+                        value="初中及以上" /><el-option
+                        label="中专及以上"
+                        value="中专及以上" /><el-option
+                        label="大专及以上"
+                        value="大专及以上" /><el-option
+                        label="本科及以上"
+                        value="本科及以上" /></el-select></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="性别要求"
+                    ><el-select v-model="form.genderReq" class="full-width"
+                      ><el-option label="不限" value="不限" /><el-option
+                        label="男"
+                        value="男" /><el-option
+                        label="女"
+                        value="女" /></el-select></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="8"
+                  ><el-form-item label="经验要求(年)"
+                    ><el-input-number
+                      v-model="form.expReq"
+                      :min="0"
+                      :max="30"
+                      controls-position="right"
+                      class="full-width" /></el-form-item
+                ></el-col>
               </el-row>
               <el-row :gutter="16" class="qualification-timing">
-                <el-col :xs="24" :sm="12"><div class="age-panel"><el-form-item label="年龄范围" class="age-field"><div class="age-value"><i class="el-icon-time"></i>{{ form.ageRange[0] }} - {{ form.ageRange[1] }} 岁</div><el-slider v-model="form.ageRange" range :min="18" :max="65" /><div class="age-scale"><span>18 岁</span><span>65 岁</span></div></el-form-item></div></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="说明书生效日期" class="date-panel"><el-date-picker v-model="form.effectDate" value-format="yyyy-MM-dd" type="date" class="full-width" /></el-form-item></el-col>
+                <el-col :xs="24" :sm="12"
+                  ><div class="age-panel">
+                    <el-form-item label="年龄范围" class="age-field"
+                      ><div class="age-value">
+                        <i class="el-icon-time"></i>{{ form.ageRange[0] }} -
+                        {{ form.ageRange[1] }} 岁
+                      </div>
+                      <el-slider
+                        v-model="form.ageRange"
+                        range
+                        :min="18"
+                        :max="65"
+                      />
+                      <div class="age-scale">
+                        <span>18 岁</span><span>65 岁</span>
+                      </div></el-form-item
+                    >
+                  </div></el-col
+                >
+                <el-col :xs="24" :sm="12"
+                  ><el-form-item label="说明书生效日期" class="date-panel"
+                    ><el-date-picker
+                      v-model="form.effectDate"
+                      value-format="yyyy-MM-dd"
+                      type="date"
+                      class="full-width" /></el-form-item
+                ></el-col>
               </el-row>
               <el-row :gutter="16" class="qualification-relations">
-                <el-col :xs="24" :sm="12"><el-form-item label="汇报对象"><el-select v-model="form.reportToId" filterable clearable class="full-width" placeholder="请选择上级岗位"><el-option v-for="item in positionOptions" :key="item.id" :label="item.name + ' · ' + item.code" :value="item.id" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="协作部门"><el-select v-model="form.cooperationDepartmentId" filterable clearable class="full-width" placeholder="请选择协作部门"><el-option v-for="item in departments" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
+                <el-col :xs="24" :sm="12"
+                  ><el-form-item label="汇报对象"
+                    ><el-select
+                      v-model="form.reportToId"
+                      filterable
+                      clearable
+                      class="full-width"
+                      placeholder="请选择上级岗位"
+                      ><el-option
+                        v-for="item in positionOptions"
+                        :key="item.id"
+                        :label="item.name + ' · ' + item.code"
+                        :value="item.id" /></el-select></el-form-item
+                ></el-col>
+                <el-col :xs="24" :sm="12"
+                  ><el-form-item label="协作部门"
+                    ><el-select
+                      v-model="form.cooperationDepartmentId"
+                      filterable
+                      clearable
+                      class="full-width"
+                      placeholder="请选择协作部门"
+                      ><el-option
+                        v-for="item in departments"
+                        :key="item.id"
+                        :label="item.name"
+                        :value="item.id" /></el-select></el-form-item
+                ></el-col>
               </el-row>
-              <el-form-item label="工作环境" class="work-environment-field"><el-input v-model.trim="form.workEnv" placeholder="如:车间、办公室、高温或粉尘环境等"><i slot="prefix" class="el-icon-office-building"></i></el-input></el-form-item>
+              <el-form-item label="工作环境" class="work-environment-field"
+                ><el-input
+                  v-model.trim="form.workEnv"
+                  placeholder="如:车间、办公室、高温或粉尘环境等"
+                  ><i
+                    slot="prefix"
+                    class="el-icon-office-building"
+                  ></i></el-input
+              ></el-form-item>
             </section>
 
             <section class="form-section responsibility-section">
-              <div class="section-heading"><div><i class="el-icon-document-checked"></i><span><strong>岗位职责与权限</strong><small>职责建议逐行填写 5-10 条,权限支持输入后回车新增</small></span></div><span class="duty-counter">已填写 {{ dutyCount }} 条</span></div>
-              <el-form-item label="岗位职责"><el-input v-model="dutyText" type="textarea" :rows="6" placeholder="1. 负责……&#10;2. 落实……&#10;3. 协同……" /></el-form-item>
-              <el-form-item label="工作权限"><el-select v-model="form.authorities" multiple filterable allow-create default-first-option class="full-width" placeholder="选择权限,或输入后按回车新增"><el-option v-for="item in authorityOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
+              <div class="section-heading">
+                <div>
+                  <i class="el-icon-document-checked"></i
+                  ><span
+                    ><strong>岗位职责与权限</strong
+                    ><small
+                      >职责建议逐行填写 5-10 条,权限支持输入后回车新增</small
+                    ></span
+                  >
+                </div>
+                <span class="duty-counter">已填写 {{ dutyCount }} 条</span>
+              </div>
+              <el-form-item label="岗位职责"
+                ><el-input
+                  v-model="dutyText"
+                  type="textarea"
+                  :rows="6"
+                  placeholder="1. 负责……&#10;2. 落实……&#10;3. 协同……"
+              /></el-form-item>
+              <el-form-item label="工作权限"
+                ><el-select
+                  v-model="form.authorities"
+                  multiple
+                  filterable
+                  allow-create
+                  default-first-option
+                  class="full-width"
+                  placeholder="选择权限,或输入后按回车新增"
+                  ><el-option
+                    v-for="item in authorityOptions"
+                    :key="item"
+                    :label="item"
+                    :value="item" /></el-select
+              ></el-form-item>
             </section>
           </div>
         </el-tab-pane>
 
         <el-tab-pane label="技能资质" name="skill">
           <div class="form-subtitle">必备技能</div>
-          <div v-for="(skill, index) in form.skills" :key="index" class="dynamic-row">
+          <div
+            v-for="(skill, index) in form.skills"
+            :key="index"
+            class="dynamic-row"
+          >
             <el-input v-model.trim="skill.name" placeholder="技能名称" />
             <el-select v-model="skill.level" placeholder="等级">
               <el-option label="初级" value="初级" />
@@ -162,23 +463,45 @@
             <el-switch v-model="skill.required" active-text="必备" />
             <el-button icon="el-icon-delete" @click="removeSkill(index)" />
           </div>
-          <el-button class="add-line" icon="el-icon-plus" @click="addSkill">新增技能</el-button>
+          <el-button class="add-line" icon="el-icon-plus" @click="addSkill"
+            >新增技能</el-button
+          >
 
           <div class="form-subtitle">资质证书</div>
-          <div v-for="(cert, index) in form.certificates" :key="cert.name + index" class="dynamic-row cert-row">
+          <div
+            v-for="(cert, index) in form.certificates"
+            :key="cert.name + index"
+            class="dynamic-row cert-row"
+          >
             <el-input v-model.trim="cert.name" placeholder="证书名称" />
             <el-select v-model="cert.category" placeholder="类别">
               <el-option label="准入类" value="准入类" />
               <el-option label="评价类" value="评价类" />
               <el-option label="管理类" value="管理类" />
             </el-select>
-            <el-input-number v-model="cert.validMonth" :min="0" :max="120" controls-position="right" placeholder="有效期" />
+            <el-input-number
+              v-model="cert.validMonth"
+              :min="0"
+              :max="120"
+              controls-position="right"
+              placeholder="有效期"
+            />
             <el-button icon="el-icon-delete" @click="removeCert(index)" />
           </div>
-          <el-button class="add-line" icon="el-icon-plus" @click="addCert">新增证书</el-button>
+          <el-button class="add-line" icon="el-icon-plus" @click="addCert"
+            >新增证书</el-button
+          >
 
           <div class="form-subtitle">推荐培训</div>
-          <el-select v-model="form.trainings" multiple filterable allow-create default-first-option class="full-width" placeholder="选择或录入关联培训课程">
+          <el-select
+            v-model="form.trainings"
+            multiple
+            filterable
+            allow-create
+            default-first-option
+            class="full-width"
+            placeholder="选择或录入关联培训课程"
+          >
             <el-option label="岗位安全上岗培训" value="岗位安全上岗培训" />
             <el-option label="质量意识与问题分析" value="质量意识与问题分析" />
             <el-option label="班组管理训练营" value="班组管理训练营" />
@@ -186,325 +509,575 @@
         </el-tab-pane>
 
         <el-tab-pane label="晋升路径" name="promote">
-          <el-row :gutter="18">
-            <el-col :xs="24" :md="8">
-              <el-form-item label="向上晋升岗位">
-                <el-select v-model="form.promote.up" multiple filterable allow-create class="full-width">
-                  <el-option v-for="item in positionOptions" :key="item.id" :label="item.name + '(' + item.seq + item.level + ')'" :value="item.name" :disabled="item.level <= form.level" />
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :xs="24" :md="8">
-              <el-form-item label="向下降级岗位">
-                <el-select v-model="form.promote.down" multiple filterable allow-create class="full-width">
-                  <el-option v-for="item in positionOptions" :key="item.id" :label="item.name + '(' + item.seq + item.level + ')'" :value="item.name" />
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :xs="24" :md="8">
-              <el-form-item label="平级轮岗岗位">
-                <el-select v-model="form.promote.lateral" multiple filterable allow-create class="full-width">
-                  <el-option v-for="item in positionOptions" :key="item.id" :label="item.name + '(' + item.seq + item.level + ')'" :value="item.name" />
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="24">
-              <el-form-item label="晋升条件">
-                <el-select v-model="form.promote.conditions" multiple filterable allow-create default-first-option class="full-width" placeholder="输入条件后回车,如:绩效≥B">
-                  <el-option v-for="item in conditionOptions" :key="item" :label="item" :value="item" />
-                </el-select>
-              </el-form-item>
-            </el-col>
-          </el-row>
+          <div class="form-section qualification-section promote-section">
+            <div class="section-heading">
+              <div>
+                <i class="el-icon-s-opportunity"></i>
+                <div>
+                  <strong>晋升路径</strong>
+                  <small
+                    >可配置多条路径,每条包含向上晋升、向下降级、平级轮岗岗位</small
+                  >
+                </div>
+              </div>
+              <el-button
+                type="primary"
+                plain
+                size="small"
+                icon="el-icon-plus"
+                @click="addPromotePath"
+              >
+                新增路径
+              </el-button>
+            </div>
+
+            <div
+              v-for="(path, index) in form.promote.paths"
+              :key="index"
+              class="promote-path-card"
+            >
+              <div class="promote-path-head">
+                <span class="promote-path-index">路径 {{ index + 1 }}</span>
+                <el-button
+                  type="danger"
+                  plain
+                  size="mini"
+                  icon="el-icon-delete"
+                  :disabled="form.promote.paths.length <= 1"
+                  @click="removePromotePath(index)"
+                >
+                  删除
+                </el-button>
+              </div>
+              <el-row :gutter="18">
+                <el-col :xs="24" :md="8">
+                  <el-form-item label="向上晋升岗位">
+                    <el-select
+                      v-model="path.up"
+                      filterable
+                      allow-create
+                      class="full-width"
+                      placeholder="选择或录入晋升岗位"
+                    >
+                      <el-option
+                        v-for="item in positionOptions"
+                        :key="item.id"
+                        :label="item.name + '(' + item.seq + item.level + ')'"
+                        :value="item.name"
+                        :disabled="item.level <= form.level"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+                <el-col :xs="24" :md="8">
+                  <el-form-item label="向下降级岗位">
+                    <el-select
+                      v-model="path.down"
+                      filterable
+                      allow-create
+                      class="full-width"
+                      placeholder="选择或录入降级岗位"
+                    >
+                      <el-option
+                        v-for="item in positionOptions"
+                        :key="item.id"
+                        :label="item.name + '(' + item.seq + item.level + ')'"
+                        :value="item.name"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+                <el-col :xs="24" :md="8">
+                  <el-form-item label="平级轮岗岗位">
+                    <el-select
+                      v-model="path.lateral"
+                      filterable
+                      allow-create
+                      class="full-width"
+                      placeholder="选择或录入轮岗岗位"
+                    >
+                      <el-option
+                        v-for="item in positionOptions"
+                        :key="item.id"
+                        :label="item.name + '(' + item.seq + item.level + ')'"
+                        :value="item.name"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+
+            <el-form-item label="晋升条件" class="promote-conditions">
+              <el-select
+                v-model="form.promote.conditions"
+                multiple
+                filterable
+                allow-create
+                default-first-option
+                class="full-width"
+                placeholder="公共条件,输入条件后回车,如:绩效≥B"
+              >
+                <el-option
+                  v-for="item in conditionOptions"
+                  :key="item"
+                  :label="item"
+                  :value="item"
+                />
+              </el-select>
+            </el-form-item>
+          </div>
         </el-tab-pane>
       </el-tabs>
     </el-form>
 
     <template v-slot:footer>
       <el-button @click="updateVisible(false)">取消</el-button>
-      <el-button icon="el-icon-document-checked" @click="previewRules">校验规则</el-button>
+      <el-button icon="el-icon-document-checked" @click="previewRules"
+        >校验规则</el-button
+      >
       <el-button type="primary" :loading="saving" @click="save">
-        {{ saving ? '正在保存' : '保存岗位' }}
+        {{ saving ? "正在保存" : "保存岗位" }}
       </el-button>
     </template>
   </ele-modal>
 </template>
 
 <script>
-  import { performanceSchemes, salaryTemplates } from '../mock';
+import { performanceSchemes, salaryTemplates } from "../mock";
 
-  const emptyForm = () => ({
-    id: null,
-    code: '',
-    name: '',
-    legalId: 1,
-    deptId: null,
-    seq: 'O',
-    level: 1,
-    type: 3,
-    staffing: 0,
-    actual: 0,
-    salaryMin: 0,
-    salaryMid: 0,
-    salaryMax: 0,
-    salaryTemplateId: 1,
-    perfSchemeId: 1,
-    perfBaseCoef: 1,
-    status: 1,
-    revokeDate: '',
-    attendRuleGroup: '1',
-    workShift: '白班',
-    reportTo: '',
-    reportToId: null,
-    cooperateWith: '',
-    cooperationDepartmentId: null,
-    workEnv: '',
-    ageRange: [18, 60],
-    genderReq: '不限',
-    eduReq: '中专及以上',
-    expReq: 0,
-    effectDate: new Date().toISOString().slice(0, 10),
-    version: 1,
-    duties: [],
-    authorities: [],
-    skills: [],
-    certificates: [],
-    trainings: [],
-    promote: { up: [], down: [], lateral: [], conditions: [] },
-    employees: [],
-    history: [],
-    updatedAt: ''
-  });
+const emptyForm = () => ({
+  id: null,
+  code: "",
+  name: "",
+  legalId: 1,
+  deptId: null,
+  seq: "O",
+  level: 1,
+  positionName: "",
+  positionSequence: "",
+  positionLevel: "",
+  type: 3,
+  staffing: 0,
+  actual: 0,
+  salaryMin: 0,
+  salaryMid: 0,
+  salaryMax: 0,
+  salaryTemplateId: 1,
+  perfSchemeId: 1,
+  perfBaseCoef: 1,
+  status: 1,
+  revokeDate: "",
+  attendRuleGroup: "1",
+  workShift: "白班",
+  reportTo: "",
+  reportToId: null,
+  cooperateWith: "",
+  cooperationDepartmentId: null,
+  workEnv: "",
+  ageRange: [18, 60],
+  genderReq: "不限",
+  eduReq: "中专及以上",
+  expReq: 0,
+  effectDate: new Date().toISOString().slice(0, 10),
+  version: 1,
+  duties: [],
+  authorities: [],
+  skills: [],
+  certificates: [],
+  trainings: [],
+  promote: { paths: [{ up: "", down: "", lateral: "" }], conditions: [] },
+  employees: [],
+  history: [],
+  updatedAt: "",
+});
 
-  export default {
-    name: 'PositionEditDialog',
-    props: {
-      visible: Boolean,
-      data: Object,
-      nextIndex: {
-        type: Number,
-        default: 1
-      },
-      positionList: {
-        type: Array,
-        default: () => []
-      },
-      legalEntities: {
-        type: Array,
-        default: () => []
+export default {
+  name: "PositionEditDialog",
+  props: {
+    visible: Boolean,
+    data: Object,
+    nextIndex: {
+      type: Number,
+      default: 1,
+    },
+    positionList: {
+      type: Array,
+      default: () => [],
+    },
+    legalEntities: {
+      type: Array,
+      default: () => [],
+    },
+    departments: {
+      type: Array,
+      default: () => [],
+    },
+    sequenceOptions: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      activeTab: "base",
+      form: emptyForm(),
+      dutyText: "",
+      saving: false,
+      salaryTemplates,
+      performanceSchemes,
+      authorityOptions: [
+        "生产报工",
+        "设备点检",
+        "绩效初评",
+        "班组排班",
+        "体系文件维护",
+        "维修工单派发",
+      ],
+      conditionOptions: [
+        "在岗满 24 月",
+        "绩效≥B",
+        "技能达标",
+        "资质齐全",
+        "培训完成率 100%",
+        "直接上级推荐",
+      ],
+      revokePickerOptions: {
+        disabledDate: (date) =>
+          date.getTime() < new Date().setHours(0, 0, 0, 0),
       },
-      departments: {
-        type: Array,
-        default: () => []
+      rules: {
+        code: [{ required: true, message: "请输入岗位编码", trigger: "blur" }],
+        name: [{ required: true, message: "请输入岗位名称", trigger: "blur" }],
+        legalId: [{ required: true, message: "请选择法人", trigger: "change" }],
+        deptId: [{ required: true, message: "请选择部门", trigger: "change" }],
+        seq: [{ required: true, message: "请选择岗位序列", trigger: "change" }],
+        level: [
+          { required: true, message: "请输入岗位层级", trigger: "change" },
+        ],
+        staffing: [
+          { required: true, message: "请输入编制人数", trigger: "change" },
+        ],
+        salaryMin: [
+          { required: true, message: "请输入薪资下限", trigger: "change" },
+        ],
+        salaryMax: [
+          { required: true, message: "请输入薪资上限", trigger: "change" },
+        ],
       },
-      sequenceOptions: {
-        type: Array,
-        default: () => []
+    };
+  },
+  computed: {
+    isUpdate() {
+      return Boolean(this.data?.id);
+    },
+    departmentOptions() {
+      return this.departments.filter(
+        (item) => item.legalId === this.form.legalId,
+      );
+    },
+    positionOptions() {
+      return this.positionList.filter(
+        (item) => item.id !== this.form.id && item.status < 4,
+      );
+    },
+    availableSalaryTemplates() {
+      return this.salaryTemplates.filter((item) =>
+        item.legalIds.includes(this.form.legalId),
+      );
+    },
+    configuredLevelOptions() {
+      const sequence = this.sequenceOptions.find(
+        (item) => item.id === this.form.seq,
+      );
+      return (sequence?.children || [])
+        .map((item) => ({
+          // 优先取后端 positionLevel,缺失时按原逻辑从 id/label 提取数字
+          value:
+            item.value ??
+            Number(String(item.id || item.label).match(/\d+/)?.[0] || 0),
+          label: item.label,
+        }))
+        .filter((item) => item.label);
+    },
+    salarySummary() {
+      const min = Number(this.form.salaryMin || 0).toLocaleString("zh-CN");
+      const max = Number(this.form.salaryMax || 0).toLocaleString("zh-CN");
+      return `¥${min} - ¥${max} / 月`;
+    },
+    dutyCount() {
+      return this.dutyText
+        .split("\n")
+        .map((item) => item.trim())
+        .filter(Boolean).length;
+    },
+  },
+  watch: {
+    visible(value) {
+      if (value) {
+        this.initialize();
       }
     },
-    data() {
-      return {
-        activeTab: 'base',
-        form: emptyForm(),
-        dutyText: '',
-        saving: false,
-        salaryTemplates,
-        performanceSchemes,
-        authorityOptions: ['生产报工', '设备点检', '绩效初评', '班组排班', '体系文件维护', '维修工单派发'],
-        conditionOptions: ['在岗满 24 月', '绩效≥B', '技能达标', '资质齐全', '培训完成率 100%', '直接上级推荐'],
-        revokePickerOptions: { disabledDate: (date) => date.getTime() < new Date().setHours(0, 0, 0, 0) },
-        rules: {
-          code: [{ required: true, message: '请输入岗位编码', trigger: 'blur' }],
-          name: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
-          legalId: [{ required: true, message: '请选择法人', trigger: 'change' }],
-          deptId: [{ required: true, message: '请选择部门', trigger: 'change' }],
-          seq: [{ required: true, message: '请选择岗位序列', trigger: 'change' }],
-          level: [{ required: true, message: '请输入岗位层级', trigger: 'change' }],
-          staffing: [{ required: true, message: '请输入编制人数', trigger: 'change' }],
-          salaryMin: [{ required: true, message: '请输入薪资下限', trigger: 'change' }],
-          salaryMax: [{ required: true, message: '请输入薪资上限', trigger: 'change' }]
-        }
+  },
+  methods: {
+    initialize() {
+      const source = this.data
+        ? JSON.parse(JSON.stringify(this.data))
+        : emptyForm();
+      this.form = {
+        ...emptyForm(),
+        ...source,
+        promote: this.normalizePromote(source.promote),
       };
+      if (!this.isUpdate) {
+        this.form.legalId = this.legalEntities[0]?.id || null;
+        this.form.seq = this.sequenceOptions[0]?.id || this.form.seq;
+        this.form.level = this.configuredLevelOptions[0]?.value || 1;
+        this.syncSequenceFields();
+        this.refreshCode();
+        this.form.deptId = this.departmentOptions[0]?.id || null;
+        this.form.salaryTemplateId =
+          this.availableSalaryTemplates[0]?.name || "";
+      }
+      this.syncSequenceFields();
+      this.dutyText = (this.form.duties || []).join("\n");
+      this.activeTab = "base";
     },
-    computed: {
-      isUpdate() {
-        return Boolean(this.data?.id);
-      },
-      departmentOptions() {
-        return this.departments.filter((item) => item.legalId === this.form.legalId);
-      },
-      positionOptions() {
-        return this.positionList.filter((item) => item.id !== this.form.id && item.status < 4);
-      },
-      availableSalaryTemplates() {
-        return this.salaryTemplates.filter((item) => item.legalIds.includes(this.form.legalId));
-      },
-      configuredLevelOptions() {
-        const sequence = this.sequenceOptions.find((item) => item.id === this.form.seq);
-        return (sequence?.children || []).map((item) => ({
-          value: Number(String(item.id || item.label).match(/\d+/)?.[0] || 0),
-          label: item.label,
-        })).filter((item) => item.value > 0);
-      },
-      salarySummary() {
-        const min = Number(this.form.salaryMin || 0).toLocaleString('zh-CN');
-        const max = Number(this.form.salaryMax || 0).toLocaleString('zh-CN');
-        return `¥${min} - ¥${max} / 月`;
-      },
-      dutyCount() {
-        return this.dutyText
-          .split('\n')
-          .map((item) => item.trim())
-          .filter(Boolean).length;
+    normalizePromote(source) {
+      const promote = { ...emptyForm().promote, ...(source || {}) };
+      const hasPath = (list) =>
+        Array.isArray(list) &&
+        list.some((path) => path?.up || path?.down || path?.lateral);
+      if (!Array.isArray(promote.paths) || !hasPath(promote.paths)) {
+        // 兼容旧数据结构(up/down/lateral 数组):迁移为首条路径,最少保留一条
+        promote.paths =
+          promote.up?.length || promote.down?.length || promote.lateral?.length
+            ? [
+                {
+                  up: promote.up?.[0] || "",
+                  down: promote.down?.[0] || "",
+                  lateral: promote.lateral?.[0] || "",
+                },
+              ]
+            : [{ up: "", down: "", lateral: "" }];
       }
+      return promote;
     },
-    watch: {
-      visible(value) {
-        if (value) {
-          this.initialize();
-        }
+    addPromotePath() {
+      this.form.promote.paths.push({ up: "", down: "", lateral: "" });
+    },
+    removePromotePath(index) {
+      if (this.form.promote.paths.length <= 1) return;
+      this.form.promote.paths.splice(index, 1);
+    },
+    handleLegalChange() {
+      this.form.deptId = this.departmentOptions[0]?.id || null;
+      if (
+        !this.availableSalaryTemplates.some(
+          (item) => item.id === this.form.salaryTemplateId,
+        )
+      ) {
+        this.form.salaryTemplateId =
+          this.availableSalaryTemplates[0]?.name || "";
       }
+      this.refreshCode();
     },
-    methods: {
-      initialize() {
-        const source = this.data ? JSON.parse(JSON.stringify(this.data)) : emptyForm();
-        this.form = {
-          ...emptyForm(),
-          ...source,
-          promote: { ...emptyForm().promote, ...(source.promote || {}) }
-        };
-        if (!this.isUpdate) {
-          this.form.legalId = this.legalEntities[0]?.id || null;
-          this.form.seq = this.sequenceOptions[0]?.id || this.form.seq;
-          this.form.level = this.configuredLevelOptions[0]?.value || 1;
-          this.refreshCode();
-          this.form.deptId = this.departmentOptions[0]?.id || null;
-          this.form.salaryTemplateId = this.availableSalaryTemplates[0]?.name || '';
-        }
-        this.dutyText = (this.form.duties || []).join('\n');
-        this.activeTab = 'base';
-      },
-      handleLegalChange() {
-        this.form.deptId = this.departmentOptions[0]?.id || null;
-        if (!this.availableSalaryTemplates.some((item) => item.id === this.form.salaryTemplateId)) {
-          this.form.salaryTemplateId = this.availableSalaryTemplates[0]?.name || '';
-        }
-        this.refreshCode();
-      },
-      handleSequenceChange() {
-        if (this.configuredLevelOptions.length) {
-          this.form.level = this.configuredLevelOptions[0].value;
-        }
-        this.refreshCode();
-      },
-      refreshCode() {
-        if (this.isUpdate) {
+    handleSequenceChange() {
+      if (this.configuredLevelOptions.length) {
+        this.form.level = this.configuredLevelOptions[0].value;
+      }
+      this.syncSequenceFields();
+      this.refreshCode();
+    },
+    handleLevelChange() {
+      this.syncSequenceFields();
+      this.refreshCode();
+    },
+    syncSequenceFields() {
+      // positionSequence 取选中序列的 label
+      // positionLevel 取选中层级的 label
+      // sequenceId 取选中层级的 value(用于 departmentPosition.sequenceId)
+      const sequence = this.sequenceOptions.find(
+        (item) => item.id === this.form.seq,
+      );
+      const level = (sequence?.children || []).find(
+        (item) => String(item.value) === String(this.form.level),
+      );
+      if (sequence?.label) this.form.positionSequence = sequence.label;
+      if (level?.label) this.form.positionLevel = level.label;
+      if (level?.value !== undefined && level?.value !== null) {
+        this.form.sequenceId = level.value;
+      }
+    },
+    refreshCode() {
+      if (this.isUpdate) {
+        return;
+      }
+      const legal = this.legalEntities.find(
+        (item) => item.id === this.form.legalId,
+      );
+      const serial = String(this.nextIndex || 1).padStart(4, "0");
+      // 层级部分用 positionLevel(层级 label,如 "pq"),不再用层级的长 id
+      this.form.code = `${this.form.seq}${
+        this.form.positionLevel || this.form.level
+      }-${
+        legal?.code || legal?.groupCode || "ZY"
+      }-${serial}`;
+    },
+    syncSalaryMid() {
+      if (this.form.salaryMax >= this.form.salaryMin) {
+        this.form.salaryMid = Math.round(
+          (Number(this.form.salaryMax) + Number(this.form.salaryMin)) / 2,
+        );
+      }
+    },
+    addSkill() {
+      this.form.skills.push({
+        name: "",
+        level: "中级",
+        required: true,
+        matchRate: 0,
+      });
+    },
+    removeSkill(index) {
+      this.form.skills.splice(index, 1);
+    },
+    addCert() {
+      this.form.certificates.push({
+        name: "",
+        category: "准入类",
+        validMonth: 36,
+        alertDays: 30,
+      });
+    },
+    removeCert(index) {
+      this.form.certificates.splice(index, 1);
+    },
+    previewRules() {
+      const warnings = this.collectRuleErrors();
+      if (this.form.actual > this.form.staffing) {
+        warnings.push("在岗人数超过编制,将触发超编预警");
+      }
+      if (!warnings.length) {
+        this.$message.success("规则校验通过,可参与编制、薪资、绩效和培训联动");
+        return;
+      }
+      this.$alert(warnings.join(";"), "规则校验提示", { type: "warning" });
+    },
+    save() {
+      this.$refs.form.validate((valid) => {
+        if (!valid || this.saving) {
           return;
         }
-        const legal = this.legalEntities.find((item) => item.id === this.form.legalId);
-        const serial = String(this.nextIndex || 1).padStart(4, '0');
-        this.form.code = `${this.form.seq}${this.form.level}-${legal?.code || legal?.groupCode || 'ZY'}-${serial}`;
-      },
-      syncSalaryMid() {
-        if (this.form.salaryMax >= this.form.salaryMin) {
-          this.form.salaryMid = Math.round((Number(this.form.salaryMax) + Number(this.form.salaryMin)) / 2);
-        }
-      },
-      addSkill() {
-        this.form.skills.push({ name: '', level: '中级', required: true, matchRate: 0 });
-      },
-      removeSkill(index) {
-        this.form.skills.splice(index, 1);
-      },
-      addCert() {
-        this.form.certificates.push({ name: '', category: '准入类', validMonth: 36, alertDays: 30 });
-      },
-      removeCert(index) {
-        this.form.certificates.splice(index, 1);
-      },
-      previewRules() {
-        const warnings = this.collectRuleErrors();
-        if (this.form.actual > this.form.staffing) {
-          warnings.push('在岗人数超过编制,将触发超编预警');
-        }
-        if (!warnings.length) {
-          this.$message.success('规则校验通过,可参与编制、薪资、绩效和培训联动');
+        const errors = this.collectRuleErrors();
+        if (errors.length) {
+          this.activeTab = this.resolveErrorTab(errors[0]);
+          this.$alert(errors.join(";"), "岗位规则校验未通过", {
+            type: "error",
+          });
           return;
         }
-        this.$alert(warnings.join(';'), '规则校验提示', { type: 'warning' });
-      },
-      save() {
-        this.$refs.form.validate((valid) => {
-          if (!valid || this.saving) {
-            return;
-          }
-          const errors = this.collectRuleErrors();
-          if (errors.length) {
-            this.activeTab = this.resolveErrorTab(errors[0]);
-            this.$alert(errors.join(';'), '岗位规则校验未通过', { type: 'error' });
-            return;
-          }
-          this.saving = true;
-          const payload = {
-            ...this.form,
-            version: this.isUpdate ? Number(this.form.version || 1) + 1 : 1,
-            duties: this.dutyText
-              .split('\n')
-              .map((item) => item.trim())
-              .filter(Boolean),
-            updatedAt: new Date().toISOString().slice(0, 10),
-            history: [
-              {
-                time: new Date().toLocaleString('zh-CN', { hour12: false }),
-                title: this.isUpdate ? '编辑岗位档案' : '新增岗位档案',
-                content: '岗位档案已提交至人力资源服务。'
-              },
-              ...(this.form.history || [])
-            ]
-          };
-          this.$emit('save', payload);
-          this.saving = false;
-        });
-      },
-      reset() {
-        this.form = emptyForm();
-        this.dutyText = '';
-        this.$refs.form?.clearValidate();
-      },
-      updateVisible(value) {
-        this.$emit('update:visible', value);
-      },
-      collectRuleErrors() {
-        const errors = [];
-        const duplicate = this.positionList.some((item) => item.code === this.form.code && item.id !== this.form.id);
-        const department = this.departments.find((item) => item.id === this.form.deptId);
-        const template = this.salaryTemplates.find((item) => item.name === this.form.salaryTemplateId || item.id === this.form.salaryTemplateId);
-        const duties = this.dutyText.split('\n').map((item) => item.trim()).filter(Boolean);
-        const skills = this.form.skills.filter((item) => item.name.trim());
-        const targets = this.form.promote.up.map((name) => this.positionList.find((item) => item.name === name)).filter(Boolean);
-        if (duplicate) errors.push('岗位编码已存在');
-        if (!department || department.legalId !== this.form.legalId) errors.push('所属部门必须存在且属于当前法人');
-        if (this.form.level < 1) errors.push('岗位层级必须大于 0');
-        if (this.configuredLevelOptions.length && !this.configuredLevelOptions.some((item) => item.value === this.form.level)) errors.push('岗位层级必须来自当前序列的启用层级');
-        if (this.form.salaryMax < this.form.salaryMin) errors.push('薪资上限不能小于下限');
-        if (template && !template.legalIds.includes(this.form.legalId)) errors.push('薪资模板与所属法人不匹配');
-        else if (template && (this.form.salaryMin < template.range[0] || this.form.salaryMax > template.range[1])) errors.push(`薪资区间须位于模板 ${template.range[0]}-${template.range[1]} 元范围内`);
-        if (this.form.status === 3 && !this.form.revokeDate) errors.push('待撤销岗位必须填写计划撤销日期');
-        if (!duties.length) errors.push('岗位职责不能为空');
-        if (!(this.form.authorities || []).length) errors.push('岗位权限不能为空');
-        if (this.form.skills.some((item) => !item.name.trim())) errors.push('技能名称不能为空');
-        if (this.form.certificates.some((item) => !item.name.trim())) errors.push('证书名称不能为空');
-        if (targets.some((item) => item.level <= this.form.level)) errors.push('向上晋升目标岗位层级必须高于当前岗位');
-        return errors;
-      },
-      resolveErrorTab(message) {
-        if (/编码|部门|层级/.test(message)) return 'base';
-        if (/薪资|职责/.test(message)) return 'duty';
-        if (/技能|证书/.test(message)) return 'skill';
-        return 'promote';
-      }
-    }
-  };
+        this.saving = true;
+        const payload = {
+          ...this.form,
+          version: this.isUpdate ? Number(this.form.version || 1) + 1 : 1,
+          duties: this.dutyText
+            .split("\n")
+            .map((item) => item.trim())
+            .filter(Boolean),
+          updatedAt: new Date().toISOString().slice(0, 10),
+          history: [
+            {
+              time: new Date().toLocaleString("zh-CN", { hour12: false }),
+              title: this.isUpdate ? "编辑岗位档案" : "新增岗位档案",
+              content: "岗位档案已提交至人力资源服务。",
+            },
+            ...(this.form.history || []),
+          ],
+        };
+        this.$emit("save", payload);
+        this.saving = false;
+      });
+    },
+    reset() {
+      this.form = emptyForm();
+      this.dutyText = "";
+      this.$refs.form?.clearValidate();
+    },
+    updateVisible(value) {
+      this.$emit("update:visible", value);
+    },
+    collectRuleErrors() {
+      const errors = [];
+      const duplicate = this.positionList.some(
+        (item) => item.code === this.form.code && item.id !== this.form.id,
+      );
+      const department = this.departments.find(
+        (item) => item.id === this.form.deptId,
+      );
+      const template = this.salaryTemplates.find(
+        (item) =>
+          item.name === this.form.salaryTemplateId ||
+          item.id === this.form.salaryTemplateId,
+      );
+      const duties = this.dutyText
+        .split("\n")
+        .map((item) => item.trim())
+        .filter(Boolean);
+      const skills = this.form.skills.filter((item) => item.name.trim());
+      const targets = (this.form.promote.paths || [])
+        .map((path) =>
+          this.positionList.find((item) => item.name === path.up),
+        )
+        .filter(Boolean);
+      if (duplicate) errors.push("岗位编码已存在");
+      if (!department || department.legalId !== this.form.legalId)
+        errors.push("所属部门必须存在且属于当前法人");
+      if (this.form.level < 1) errors.push("岗位层级必须大于 0");
+      if (
+        this.configuredLevelOptions.length &&
+        !this.configuredLevelOptions.some(
+          (item) => item.value === this.form.level,
+        )
+      )
+        errors.push("岗位层级必须来自当前序列的启用层级");
+      if (this.form.salaryMax < this.form.salaryMin)
+        errors.push("薪资上限不能小于下限");
+      if (template && !template.legalIds.includes(this.form.legalId))
+        errors.push("薪资模板与所属法人不匹配");
+      else if (
+        template &&
+        (this.form.salaryMin < template.range[0] ||
+          this.form.salaryMax > template.range[1])
+      )
+        errors.push(
+          `薪资区间须位于模板 ${template.range[0]}-${template.range[1]} 元范围内`,
+        );
+      if (this.form.status === 3 && !this.form.revokeDate)
+        errors.push("待撤销岗位必须填写计划撤销日期");
+      if (!duties.length) errors.push("岗位职责不能为空");
+      if (!(this.form.authorities || []).length)
+        errors.push("岗位权限不能为空");
+      if (this.form.skills.some((item) => !item.name.trim()))
+        errors.push("技能名称不能为空");
+      if (this.form.certificates.some((item) => !item.name.trim()))
+        errors.push("证书名称不能为空");
+      if (targets.some((item) => item.level <= this.form.level))
+        errors.push("向上晋升目标岗位层级必须高于当前岗位");
+      return errors;
+    },
+    resolveErrorTab(message) {
+      if (/编码|部门|层级/.test(message)) return "base";
+      if (/薪资|职责/.test(message)) return "duty";
+      if (/技能|证书/.test(message)) return "skill";
+      return "promote";
+    },
+  },
+};
 </script>
 
-<style src="../../../styles/views/positionManagement/components/PositionEditDialog.scss" lang="scss" scoped></style>
+<style
+  src="../../../styles/views/positionManagement/components/PositionEditDialog.scss"
+  lang="scss"
+  scoped
+></style>

+ 113 - 30
src/views/positionManagement/components/PositionHistoryDialog.vue

@@ -18,9 +18,18 @@
         </div>
       </div>
       <div class="history-stats">
-        <div><strong>{{ events.length }}</strong><span>变更记录</span></div>
-        <div><strong>{{ positionCount }}</strong><span>涉及岗位</span></div>
-        <div class="warning-stat"><strong>{{ warningCount }}</strong><span>风险变更</span></div>
+        <div>
+          <strong>{{ events.length }}</strong
+          ><span>变更记录</span>
+        </div>
+        <div>
+          <strong>{{ positionCount }}</strong
+          ><span>涉及岗位</span>
+        </div>
+        <div class="warning-stat">
+          <strong>{{ warningCount }}</strong
+          ><span>风险变更</span>
+        </div>
       </div>
     </div>
 
@@ -40,7 +49,12 @@
         />
       </el-select>
       <el-select v-model="monthFilter" clearable placeholder="全部时间">
-        <el-option v-for="month in months" :key="month" :label="formatMonth(month)" :value="month" />
+        <el-option
+          v-for="month in months"
+          :key="month"
+          :label="formatMonth(month)"
+          :value="month"
+        />
       </el-select>
       <button v-if="hasFilters" class="reset-button" @click="clearFilters">
         <i class="el-icon-refresh-left"></i> 重置
@@ -48,7 +62,9 @@
     </div>
 
     <div class="result-caption">
-      <span>共找到 <strong>{{ filteredEvents.length }}</strong> 条记录</span>
+      <span
+        >共找到 <strong>{{ filteredEvents.length }}</strong> 条记录</span
+      >
       <span v-if="latestTime">最近更新于 {{ latestTime }}</span>
     </div>
 
@@ -62,16 +78,26 @@
             :class="[{ active: item.id === activeId }, 'type-' + item.type]"
             @click="activeId = item.id"
           >
-            <span class="timeline-rail"><i :class="typeMeta(item.type).icon"></i></span>
+            <span class="timeline-rail"
+              ><i :class="typeMeta(item.type).icon"></i
+            ></span>
             <span class="history-card">
               <span class="card-topline">
-                <span class="history-time"><i class="el-icon-time"></i>{{ item.time }}</span>
+                <span class="history-time"
+                  ><i class="el-icon-time"></i>{{ item.time }}</span
+                >
                 <span class="type-tag">{{ typeMeta(item.type).label }}</span>
               </span>
               <strong class="event-title">{{ item.title }}</strong>
-              <span class="position-line"><i class="el-icon-suitcase"></i>{{ item.position }}</span>
-              <span class="event-preview">{{ item.content || '暂无补充说明' }}</span>
-              <span class="card-action">查看详情 <i class="el-icon-arrow-right"></i></span>
+              <span class="position-line"
+                ><i class="el-icon-suitcase"></i>{{ item.position }}</span
+              >
+              <span class="event-preview">{{
+                item.content || "暂无补充说明"
+              }}</span>
+              <span class="card-action"
+                >查看详情 <i class="el-icon-arrow-right"></i
+              ></span>
             </span>
           </button>
         </template>
@@ -94,7 +120,12 @@
         <div class="detail-section">
           <span class="detail-label">变更岗位</span>
           <strong>{{ activeEvent.position }}</strong>
-          <small>{{ activeEvent.code }}<template v-if="activeEvent.department"> · {{ activeEvent.department }}</template></small>
+          <small
+            >{{ activeEvent.code
+            }}<template v-if="activeEvent.department">
+              · {{ activeEvent.department }}</template
+            ></small
+          >
         </div>
         <div class="detail-section">
           <span class="detail-label">发生时间</span>
@@ -102,17 +133,30 @@
         </div>
         <div class="detail-section">
           <span class="detail-label">变更说明</span>
-          <p class="detail-content">{{ activeEvent.content || '暂无补充说明。' }}</p>
+          <p class="detail-content">
+            {{ activeEvent.content || "暂无补充说明。" }}
+          </p>
         </div>
         <div class="detail-section">
           <span class="detail-label">可能影响</span>
           <div class="impact-tags">
-            <span v-for="impact in activeEvent.impacts" :key="impact">{{ impact }}</span>
+            <span v-for="impact in activeEvent.impacts" :key="impact">{{
+              impact
+            }}</span>
           </div>
         </div>
         <div class="detail-actions">
-          <el-button type="primary" icon="el-icon-view" @click="viewPosition(activeEvent)">查看岗位档案</el-button>
-          <el-button icon="el-icon-document-copy" @click="copyEvent(activeEvent)">复制摘要</el-button>
+          <el-button
+            type="primary"
+            icon="el-icon-view"
+            @click="viewPosition(activeEvent)"
+            >查看岗位档案</el-button
+          >
+          <el-button
+            icon="el-icon-document-copy"
+            @click="copyEvent(activeEvent)"
+            >复制摘要</el-button
+          >
         </div>
       </aside>
       <aside v-else class="history-detail detail-placeholder">
@@ -123,7 +167,9 @@
     </div>
 
     <template v-slot:footer>
-      <span class="footer-tip"><i class="el-icon-info"></i> 历史记录按时间倒序展示</span>
+      <span class="footer-tip"
+        ><i class="el-icon-info"></i> 历史记录按时间倒序展示</span
+      >
       <el-button @click="updateVisible(false)">关闭</el-button>
     </template>
   </ele-modal>
@@ -173,14 +219,26 @@ export default {
     filteredEvents() {
       const keyword = this.keyword.toLowerCase();
       return this.events.filter((item) => {
-        const text = [item.position, item.code, item.title, item.content, item.department].join(" ").toLowerCase();
-        return (!keyword || text.includes(keyword)) &&
+        const text = [
+          item.position,
+          item.code,
+          item.title,
+          item.content,
+          item.department,
+        ]
+          .join(" ")
+          .toLowerCase();
+        return (
+          (!keyword || text.includes(keyword)) &&
           (!this.typeFilter || item.type === this.typeFilter) &&
-          (!this.monthFilter || item.month === this.monthFilter);
+          (!this.monthFilter || item.month === this.monthFilter)
+        );
       });
     },
     activeEvent() {
-      return this.filteredEvents.find((item) => item.id === this.activeId) || null;
+      return (
+        this.filteredEvents.find((item) => item.id === this.activeId) || null
+      );
     },
     positionCount() {
       return new Set(this.events.map((item) => item.positionId)).size;
@@ -189,7 +247,9 @@ export default {
       return this.events.filter((item) => item.type === "warning").length;
     },
     months() {
-      return [...new Set(this.events.map((item) => item.month).filter(Boolean))];
+      return [
+        ...new Set(this.events.map((item) => item.month).filter(Boolean)),
+      ];
     },
     availableTypes() {
       const types = new Set(this.events.map((item) => item.type));
@@ -227,15 +287,24 @@ export default {
       this.activeId = this.filteredEvents[0]?.id || "";
     },
     typeMeta(type) {
-      return TYPE_OPTIONS.find((item) => item.value === type) || TYPE_OPTIONS[5];
+      return (
+        TYPE_OPTIONS.find((item) => item.value === type) || TYPE_OPTIONS[5]
+      );
     },
     eventType(item) {
       const changeTypeMap = {
-        CREATE: "organization", UPDATE: "general", STATUS: "warning",
-        REVOKE: "warning", DELETE: "warning", OCCUPANCY: "staffing",
-        MERGE: "organization", MERGE_TARGET: "organization", DOCUMENT: "general",
+        CREATE: "organization",
+        UPDATE: "general",
+        STATUS: "warning",
+        REVOKE: "warning",
+        DELETE: "warning",
+        OCCUPANCY: "staffing",
+        MERGE: "organization",
+        MERGE_TARGET: "organization",
+        DOCUMENT: "general",
       };
-      if (changeTypeMap[item.changeType || item.type]) return changeTypeMap[item.changeType || item.type];
+      if (changeTypeMap[item.changeType || item.type])
+        return changeTypeMap[item.changeType || item.type];
       const text = (item.title + " " + (item.content || "")).toLowerCase();
       if (/撤销|停用|归档|风险|预警/.test(text)) return "warning";
       if (/薪资|薪酬|定薪|调薪|绩效/.test(text)) return "salary";
@@ -245,7 +314,8 @@ export default {
       return "general";
     },
     eventImpacts(item, type) {
-      if (Array.isArray(item.impacts) && item.impacts.length) return item.impacts.slice(0, 4);
+      if (Array.isArray(item.impacts) && item.impacts.length)
+        return item.impacts.slice(0, 4);
       const impactMap = {
         organization: ["组织架构", "岗位说明书"],
         staffing: ["人员档案", "编制台账"],
@@ -263,14 +333,23 @@ export default {
     },
     formatMonth(month) {
       const parts = month.split("-");
-      return parts.length === 2 ? parts[0] + "年" + Number(parts[1]) + "月" : month;
+      return parts.length === 2
+        ? parts[0] + "年" + Number(parts[1]) + "月"
+        : month;
     },
     viewPosition(item) {
       this.$emit("view-position", item.positionId);
       this.updateVisible(false);
     },
     copyEvent(item) {
-      const text = [item.time, item.position + "(" + item.code + ")", item.title, item.content].filter(Boolean).join(" | " );
+      const text = [
+        item.time,
+        item.position + "(" + item.code + ")",
+        item.title,
+        item.content,
+      ]
+        .filter(Boolean)
+        .join(" | ");
       const textarea = document.createElement("textarea");
       textarea.value = text;
       textarea.setAttribute("readonly", "readonly");
@@ -286,4 +365,8 @@ export default {
 };
 </script>
 
-<style src="../../../styles/views/positionManagement/components/PositionHistoryDialog.scss" lang="scss" scoped></style>
+<style
+  src="../../../styles/views/positionManagement/components/PositionHistoryDialog.scss"
+  lang="scss"
+  scoped
+></style>

+ 96 - 24
src/views/positionManagement/components/PositionLifecycleDialog.vue

@@ -1,15 +1,50 @@
 <template>
-  <ele-modal :title="title" :visible="visible" width="640px" :close-on-click-modal="false" append-to-body @update:visible="updateVisible" @closed="reset">
-    <el-alert :title="impactText" type="warning" :closable="false" show-icon class="impact-alert" />
+  <ele-modal
+    :title="title"
+    :visible="visible"
+    width="640px"
+    :close-on-click-modal="false"
+    append-to-body
+    @update:visible="updateVisible"
+    @closed="reset"
+  >
+    <el-alert
+      :title="impactText"
+      type="warning"
+      :closable="false"
+      show-icon
+      class="impact-alert"
+    />
     <el-form ref="form" :model="form" :rules="rules" label-position="top">
-      <el-form-item v-if="mode === 'merge'" label="合并目标岗位" prop="targetId">
-        <el-select v-model="form.targetId" filterable class="full-width" placeholder="请选择目标岗位">
-          <el-option v-for="item in targetOptions" :key="item.id" :label="item.name + ' · ' + item.code" :value="item.id" />
+      <el-form-item
+        v-if="mode === 'merge'"
+        label="合并目标岗位"
+        prop="targetId"
+      >
+        <el-select
+          v-model="form.targetId"
+          filterable
+          class="full-width"
+          placeholder="请选择目标岗位"
+        >
+          <el-option
+            v-for="item in targetOptions"
+            :key="item.id"
+            :label="item.name + ' · ' + item.code"
+            :value="item.id"
+          />
         </el-select>
       </el-form-item>
       <el-form-item v-else label="计划撤销日期" prop="effectDate">
-        <el-date-picker v-model="form.effectDate" type="date" value-format="yyyy-MM-dd" class="full-width" />
-        <div class="field-hint">当前接口仅登记岗位待撤销状态和计划日期;人员安置需在人员异动流程中另行办理。</div>
+        <el-date-picker
+          v-model="form.effectDate"
+          type="date"
+          value-format="yyyy-MM-dd"
+          class="full-width"
+        />
+        <div class="field-hint">
+          当前接口仅登记岗位待撤销状态和计划日期;人员安置需在人员异动流程中另行办理。
+        </div>
       </el-form-item>
     </el-form>
     <template v-slot:footer>
@@ -21,41 +56,78 @@
 <script>
 const today = () => new Date().toISOString().slice(0, 10);
 export default {
-  name: 'PositionLifecycleDialog',
-  props: { visible: Boolean, mode: String, position: Object, positionList: { type: Array, default: () => [] } },
+  name: "PositionLifecycleDialog",
+  props: {
+    visible: Boolean,
+    mode: String,
+    position: Object,
+    positionList: { type: Array, default: () => [] },
+  },
   data() {
     return {
       form: { targetId: null, effectDate: today() },
       rules: {
-        targetId: [{ required: true, message: '请选择目标岗位', trigger: 'change' }],
-        effectDate: [{ required: true, message: '请选择计划撤销日期', trigger: 'change' }]
-      }
+        targetId: [
+          { required: true, message: "请选择目标岗位", trigger: "change" },
+        ],
+        effectDate: [
+          { required: true, message: "请选择计划撤销日期", trigger: "change" },
+        ],
+      },
     };
   },
   computed: {
-    title() { return this.mode === 'merge' ? '岗位合并' : '岗位撤销计划'; },
+    title() {
+      return this.mode === "merge" ? "岗位合并" : "岗位撤销计划";
+    },
     impactText() {
       const position = this.position || {};
-      return this.mode === 'merge'
-        ? `源岗位“${position.name || '-'}”将合并到目标岗位,请确认目标岗位无误。`
-        : `岗位“${position.name || '-'}”将设置为待撤销,当前在岗 ${position.actual || 0} 人。`;
+      return this.mode === "merge"
+        ? `源岗位“${
+            position.name || "-"
+          }”将合并到目标岗位,请确认目标岗位无误。`
+        : `岗位“${position.name || "-"}”将设置为待撤销,当前在岗 ${
+            position.actual || 0
+          } 人。`;
     },
     targetOptions() {
-      return this.positionList.filter((item) => item.id !== this.position?.id && item.status === 1 && item.legalId === this.position?.legalId);
-    }
+      return this.positionList.filter(
+        (item) =>
+          item.id !== this.position?.id &&
+          item.status === 1 &&
+          item.legalId === this.position?.legalId,
+      );
+    },
+  },
+  watch: {
+    visible(value) {
+      if (value) this.form = { targetId: null, effectDate: today() };
+    },
   },
-  watch: { visible(value) { if (value) this.form = { targetId: null, effectDate: today() }; } },
   methods: {
     submit() {
       this.$refs.form.validate((valid) => {
         if (!valid) return;
-        this.$emit('confirm', { mode: this.mode, positionId: this.position.id, ...this.form });
+        this.$emit("confirm", {
+          mode: this.mode,
+          positionId: this.position.id,
+          ...this.form,
+        });
         this.updateVisible(false);
       });
     },
-    reset() { this.form = { targetId: null, effectDate: today() }; this.$refs.form?.clearValidate(); },
-    updateVisible(value) { this.$emit('update:visible', value); }
-  }
+    reset() {
+      this.form = { targetId: null, effectDate: today() };
+      this.$refs.form?.clearValidate();
+    },
+    updateVisible(value) {
+      this.$emit("update:visible", value);
+    },
+  },
 };
 </script>
-<style src="../../../styles/views/positionManagement/components/PositionLifecycleDialog.scss" lang="scss" scoped></style>
+<style
+  src="../../../styles/views/positionManagement/components/PositionLifecycleDialog.scss"
+  lang="scss"
+  scoped
+></style>

+ 45 - 14
src/views/positionManagement/components/PromotionReviewDialog.vue

@@ -1,11 +1,22 @@
 <template>
-  <ele-modal title="晋升申请审核" :visible="visible" width="680px" append-to-body @update:visible="$emit('update:visible', $event)">
+  <ele-modal
+    title="晋升申请审核"
+    :visible="visible"
+    width="680px"
+    append-to-body
+    @update:visible="$emit('update:visible', $event)"
+  >
     <div v-if="application" class="review-header">
       <strong>{{ application.employee }}</strong>
-      <span>{{ application.from || '-' }} → {{ application.to || '-' }}</span>
+      <span>{{ application.from || "-" }} → {{ application.to || "-" }}</span>
       <el-tag size="small">{{ application.status }}</el-tag>
     </div>
-    <el-alert title="审核结果将直接写入晋升申请状态" type="info" :closable="false" show-icon />
+    <el-alert
+      title="审核结果将直接写入晋升申请状态"
+      type="info"
+      :closable="false"
+      show-icon
+    />
     <el-form label-position="top">
       <el-form-item label="审核决定">
         <el-radio-group v-model="decision">
@@ -14,7 +25,14 @@
         </el-radio-group>
       </el-form-item>
       <el-form-item label="审核意见" required>
-        <el-input v-model.trim="comment" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入审核意见" />
+        <el-input
+          v-model.trim="comment"
+          type="textarea"
+          :rows="4"
+          maxlength="500"
+          show-word-limit
+          placeholder="请输入审核意见"
+        />
       </el-form-item>
     </el-form>
     <template v-slot:footer>
@@ -25,21 +43,34 @@
 </template>
 <script>
 export default {
-  name: 'PromotionReviewDialog',
+  name: "PromotionReviewDialog",
   props: { visible: Boolean, application: Object },
-  data() { return { decision: 'APPROVED', comment: '' }; },
+  data() {
+    return { decision: "APPROVED", comment: "" };
+  },
   watch: {
     visible(value) {
-      if (value) { this.decision = 'APPROVED'; this.comment = ''; }
-    }
+      if (value) {
+        this.decision = "APPROVED";
+        this.comment = "";
+      }
+    },
   },
   methods: {
     submit() {
-      if (!this.comment) return this.$message.warning('请填写审核意见');
-      this.$emit('submit', { id: this.application.id, decision: this.decision, comment: this.comment });
-      this.$emit('update:visible', false);
-    }
-  }
+      if (!this.comment) return this.$message.warning("请填写审核意见");
+      this.$emit("submit", {
+        id: this.application.id,
+        decision: this.decision,
+        comment: this.comment,
+      });
+      this.$emit("update:visible", false);
+    },
+  },
 };
 </script>
-<style src="../../../styles/views/positionManagement/components/PromotionReviewDialog.scss" lang="scss" scoped></style>
+<style
+  src="../../../styles/views/positionManagement/components/PromotionReviewDialog.scss"
+  lang="scss"
+  scoped
+></style>

+ 282 - 85
src/views/positionManagement/index.vue

@@ -125,11 +125,11 @@
           <button
             v-for="level in selectedSequenceNode.children"
             :key="level.id"
-            :class="{ active: selectedLevel === levelValue(level) }"
-            @click="selectedLevel = levelValue(level)"
+            :class="{ active: selectedLevel === level.label }"
+            @click="selectedLevel = level.label"
           >
             <span>{{ level.label }}</span>
-            <strong>{{ countByLevel(level.id) }}</strong>
+            <strong>{{ countByLevel(level.label) }}</strong>
           </button>
         </div>
       </aside>
@@ -200,7 +200,7 @@
             type="success"
             closable
             @close="selectedLevel = null"
-            >{{ selectedSequence }}{{ selectedLevel }}</el-tag
+            >{{ selectedLevel }}</el-tag
           >
           <el-tag v-if="keyword" type="info" closable @close="keyword = ''"
             >关键词:{{ keyword }}</el-tag
@@ -874,7 +874,10 @@
       :application="reviewApplication"
       @submit="submitPromotionReview"
     />
-    <my-promotion-dialog :visible.sync="myPromotionVisible" @submitted="loadData" />
+    <my-promotion-dialog
+      :visible.sync="myPromotionVisible"
+      @submitted="loadData"
+    />
     <position-history-dialog
       :visible.sync="historyVisible"
       :positions="historyPositions"
@@ -1036,7 +1039,9 @@ export default {
           actual: Number(this.dashboardMetrics.occupancyCount || 0),
           risk: Number(this.dashboardMetrics.warningCount || 0),
           sequenceCount: Number(this.dashboardMetrics.sequenceCount || 0),
-          staffingRate: Math.round(Number(this.dashboardMetrics.occupancyRate || 0)),
+          staffingRate: Math.round(
+            Number(this.dashboardMetrics.occupancyRate || 0),
+          ),
         };
       }
       const total = this.localPositions.length;
@@ -1110,7 +1115,6 @@ export default {
         return (
           this.sequenceName(this.selectedSequence) +
           " · " +
-          this.selectedSequence +
           this.selectedLevel
         );
       }
@@ -1228,27 +1232,44 @@ export default {
     async loadData() {
       this.loading = true;
       try {
-        const [positionPage, dashboard, navigation, configuredSequences, promotionDashboard, organizations, promotionChannels] =
-          await Promise.all([
-            getPositionPage({ pageNum: 1, size: 1000, sortName: "createTime", orderBy: "descending" }),
-            getPositionDashboard(),
-            getPositionNavigation().catch(() => []),
-            getPositionSequenceList().catch(() => []),
-            getPromotionReviewDashboard(),
-            listOrganizations(),
-            getPromotionChannels().catch(() => []),
-          ]);
+        const promotionDashboard = [];
+        const [
+          positionPage,
+          dashboard,
+          navigation,
+          configuredSequences,
+          // promotionDashboard,
+          organizations,
+          promotionChannels,
+        ] = await Promise.all([
+          getPositionPage({
+            pageNum: 1,
+            size: 1000,
+            sortName: "createTime",
+            orderBy: "descending",
+          }),
+          getPositionDashboard(),
+          getPositionNavigation().catch(() => []),
+          getPositionSequenceList().catch(() => []),
+          // getPromotionReviewDashboard(),
+          listOrganizations(),
+          getPromotionChannels().catch(() => []),
+        ]);
         this.dashboardMetrics = dashboard || null;
         this.localPositions = (positionPage?.list || []).map(adaptPosition);
         this.buildOrganizations(organizations || []);
         this.buildSequenceTree(configuredSequences || [], navigation || []);
         this.promotionChannels = promotionChannels || [];
-        this.promoteApplications = (promotionDashboard?.reviewApplications || []).map(adaptPromotion);
-        this.promoteStats = (promotionDashboard?.channelStatistics || []).map((item) => ({
-          label: `${item.sequenceName || item.level || "其他"}通道`,
-          value: Number(item.applicationCount || 0),
-          rate: Math.round(Number(item.applicationRate || 0)),
-        }));
+        this.promoteApplications = (
+          promotionDashboard?.reviewApplications || []
+        ).map(adaptPromotion);
+        this.promoteStats = (promotionDashboard?.channelStatistics || []).map(
+          (item) => ({
+            label: `${item.sequenceName || item.level || "其他"}通道`,
+            value: Number(item.applicationCount || 0),
+            rate: Math.round(Number(item.applicationRate || 0)),
+          }),
+        );
         this.lastSyncTime = new Date().toLocaleTimeString("zh-CN", {
           hour: "2-digit",
           minute: "2-digit",
@@ -1266,20 +1287,60 @@ export default {
       const parents = new Map(records.map((item) => [String(item.id), item]));
       this.legalEntities = records
         .filter((item) => !parents.has(String(item.parentId || 0)))
-        .map((item) => ({ id: item.id, name: item.name, code: item.groupCode || item.code || "ZY" }));
+        .map((item) => ({
+          id: item.id,
+          name: item.name,
+          code: item.groupCode || item.code || "ZY",
+        }));
       if (!this.legalEntities.length) {
-        this.legalEntities = records.map((item) => ({ id: item.id, name: item.name, code: item.groupCode || item.code || "ZY" }));
+        this.legalEntities = records.map((item) => ({
+          id: item.id,
+          name: item.name,
+          code: item.groupCode || item.code || "ZY",
+        }));
       }
       this.departments = records.map((item) => {
         let current = item;
-        while (parents.has(String(current.parentId))) current = parents.get(String(current.parentId));
-        return { id: item.id, name: item.name, code: item.groupCode || item.code || "", legalId: current.id };
+        while (parents.has(String(current.parentId)))
+          current = parents.get(String(current.parentId));
+        return {
+          id: item.id,
+          name: item.name,
+          code: item.groupCode || item.code || "",
+          legalId: current.id,
+        };
       });
       if (!this.legalEntities.length) {
-        this.legalEntities = [...new Map(this.localPositions.filter((item) => item.legalId).map((item) => [String(item.legalId), { id: item.legalId, name: item.legalName || `组织${item.legalId}`, code: "ZY" }])).values()];
+        this.legalEntities = [
+          ...new Map(
+            this.localPositions
+              .filter((item) => item.legalId)
+              .map((item) => [
+                String(item.legalId),
+                {
+                  id: item.legalId,
+                  name: item.legalName || `组织${item.legalId}`,
+                  code: "ZY",
+                },
+              ]),
+          ).values(),
+        ];
       }
       if (!this.departments.length) {
-        this.departments = [...new Map(this.localPositions.filter((item) => item.deptId).map((item) => [String(item.deptId), { id: item.deptId, name: item.deptName || `部门${item.deptId}`, legalId: item.legalId }])).values()];
+        this.departments = [
+          ...new Map(
+            this.localPositions
+              .filter((item) => item.deptId)
+              .map((item) => [
+                String(item.deptId),
+                {
+                  id: item.deptId,
+                  name: item.deptName || `部门${item.deptId}`,
+                  legalId: item.legalId,
+                },
+              ]),
+          ).values(),
+        ];
       }
     },
     buildSequenceTree(configuredSequences, navigation) {
@@ -1288,15 +1349,17 @@ export default {
         sequenceTree.map((item) => [item.id, JSON.parse(JSON.stringify(item))]),
       );
       const base = new Map();
-      const ensureSequence = (name, description = "") => {
-        const cleanName = String(name || "").trim().replace(/序列$/, "");
+      const ensureSequence = (name, description = "", displayName) => {
+        const cleanName = String(name || "")
+          .trim()
+          .replace(/序列$/, "");
         if (!cleanName) return null;
         const id = sequenceCode(cleanName);
         if (!base.has(id)) {
           const preset = presets.get(id);
           base.set(id, {
             id,
-            label: `${cleanName}序列`,
+            label: displayName || `${cleanName}`,
             subtitle: description || preset?.subtitle || "",
             color: preset?.color || colors[base.size % colors.length],
             children: [],
@@ -1305,18 +1368,40 @@ export default {
         return base.get(id);
       };
       const addLevel = (node, rawLevel) => {
-        if (!node || rawLevel === null || rawLevel === undefined || rawLevel === "") return;
+        if (!node || rawLevel === null || rawLevel === undefined) return;
+        if (typeof rawLevel === "object") {
+          const label = String(rawLevel.label ?? "").trim();
+          if (!label) return;
+          // 按 id 或 label 去重,避免配置序列与导航层级(同层级名、不同 id)重复
+          if (
+            !node.children.some(
+              (item) =>
+                String(item.id) === String(rawLevel.id) ||
+                item.label === label,
+            )
+          ) {
+            node.children.push({
+              id: rawLevel.id,
+              label,
+              ...(rawLevel.value !== undefined && rawLevel.value !== null
+                ? { value: rawLevel.value }
+                : {}),
+            });
+          }
+          return;
+        }
         const text = String(rawLevel).trim();
-        const number = Number(text.match(/\d+/)?.[0]);
-        const levelId = number ? `${node.id}${number}` : text;
-        if (!node.children.some((item) => item.id === levelId)) {
+        if (!text) return;
+        if (!node.children.some((item) => item.label === text)) {
           node.children.push({
-            id: levelId,
-            label: number ? `${number}级` : text,
+            id: text,
+            label: text,
           });
         }
       };
       const parseLevels = (item) => {
+        // 接口适配后:children 为 [{ id, label }]
+        if (Array.isArray(item.children)) return item.children;
         if (Array.isArray(item.levelList)) return item.levelList;
         const levelList = String(item.levelList || "").trim();
         if (levelList) {
@@ -1326,17 +1411,24 @@ export default {
           } catch (error) {
             // 后端也可能使用逗号、顿号或分号拼接层级。
           }
-          return levelList.split(/[、,,;;\s]+/).filter(Boolean);
+          return levelList.split(/[、,,;;/\\\s]+/).filter(Boolean);
         }
         const count = Number(item.level);
         if (count > 0 && !/[A-Za-z]/.test(String(item.level))) {
-          return Array.from({ length: count }, (_, index) => `${sequenceCode(item.sequence)}${index + 1}`);
+          return Array.from(
+            { length: count },
+            (_, index) => `${sequenceCode(item.sequence)}${index + 1}`,
+          );
         }
         return item.level ? [item.level] : [];
       };
 
       configuredSequences.forEach((item) => {
-        const node = ensureSequence(item.sequence, item.description);
+        // 用序列名(positionSequence/sequence)生成序列节点,与 navigation 的 sequenceName 保持一致
+        const node = ensureSequence(
+          item.positionSequence || item.sequence,
+          item.description,
+        );
         parseLevels(item).forEach((level) => addLevel(node, level));
       });
 
@@ -1368,6 +1460,7 @@ export default {
           return left - right;
         }),
       }));
+      console.log(this.sequenceTree,'this.sequenceTree')
     },
     selectSequence(seq) {
       this.selectedSequence = this.selectedSequence === seq ? "" : seq;
@@ -1382,24 +1475,25 @@ export default {
     },
     countByLevel(levelId) {
       return this.localPositions.filter(
-        (item) => item.seq + item.level === levelId,
+        (item) => item.level === levelId,
       ).length;
     },
-    levelValue(level) {
-      return Number(String(level?.id || level?.label || "").match(/\d+/)?.[0] || 0);
-    },
     sequenceName(seq) {
       return this.sequenceTree.find((item) => item.id === seq)?.label || seq;
     },
     sequenceShortName(seq) {
-      return { M: "管", P: "专", T: "技", O: "操" }[seq] || seq;
+      console.log(seq,'seq')
+      return { M: "管", P: "专", T: "技", O: "操" }[seq] || seq.split('')[0];
     },
     async selectPosition(row) {
       this.loading = true;
       try {
         this.selectedPosition = await getPositionById(row.id);
-        const index = this.localPositions.findIndex((item) => String(item.id) === String(row.id));
-        if (index >= 0) this.$set(this.localPositions, index, this.selectedPosition);
+        const index = this.localPositions.findIndex(
+          (item) => String(item.id) === String(row.id),
+        );
+        if (index >= 0)
+          this.$set(this.localPositions, index, this.selectedPosition);
         this.detailTab = "base";
         this.detailVisible = true;
       } catch (error) {
@@ -1428,7 +1522,9 @@ export default {
       try {
         if (payload.id) await updatePosition(payload, this.localPositions);
         else await createPosition(payload, this.localPositions);
-        this.$message.success(payload.id ? "岗位档案保存成功" : "岗位档案新增成功");
+        this.$message.success(
+          payload.id ? "岗位档案保存成功" : "岗位档案新增成功",
+        );
         this.editVisible = false;
         await this.loadData();
       } catch (error) {
@@ -1460,20 +1556,37 @@ export default {
       if (!source) return;
       if (change.mode === "merge") {
         try {
-          await mergePositions({ sourcePositionIds: [source.id], targetPositionId: change.targetId });
-          this.$notify({ title: "岗位合并成功", message: "员工岗位与源岗位状态已由服务端完成联动。", type: "success" });
+          await mergePositions({
+            sourcePositionIds: [source.id],
+            targetPositionId: change.targetId,
+          });
+          this.$notify({
+            title: "岗位合并成功",
+            message: "员工岗位与源岗位状态已由服务端完成联动。",
+            type: "success",
+          });
           await this.loadData();
         } catch (error) {
           this.$message.error(error.message || "岗位合并失败");
         }
         return;
       } else if (change.mode === "split") {
-        this.$message.warning("当前接口文档未提供岗位拆分接口,暂不能提交该操作");
+        this.$message.warning(
+          "当前接口文档未提供岗位拆分接口,暂不能提交该操作",
+        );
         return;
       } else {
         try {
-          await updatePositionStatus({ id: source.id, status: 2, revokeDate: change.effectDate });
-          this.$notify({ title: "岗位已设为待撤销", message: `计划撤销日期:${change.effectDate}`, type: "success" });
+          await updatePositionStatus({
+            id: source.id,
+            status: 2,
+            revokeDate: change.effectDate,
+          });
+          this.$notify({
+            title: "岗位已设为待撤销",
+            message: `计划撤销日期:${change.effectDate}`,
+            type: "success",
+          });
           await this.loadData();
         } catch (error) {
           this.$message.error(error.message || "岗位撤销计划提交失败");
@@ -1496,9 +1609,16 @@ export default {
     },
     async submitPromotionReview(review) {
       try {
-        await reviewPromotion(review.id, { decision: review.decision, reviewComment: review.comment });
+        await reviewPromotion(review.id, {
+          decision: review.decision,
+          reviewComment: review.comment,
+        });
         const approved = review.decision === "APPROVED";
-        this.$notify({ title: "审核结果已提交", message: approved ? "晋升申请已通过。" : "晋升申请已驳回。", type: approved ? "success" : "warning" });
+        this.$notify({
+          title: "审核结果已提交",
+          message: approved ? "晋升申请已通过。" : "晋升申请已驳回。",
+          type: approved ? "success" : "warning",
+        });
         await this.loadData();
       } catch (error) {
         this.$message.error(error.message || "评审提交失败");
@@ -1514,19 +1634,44 @@ export default {
         this.loading = true;
         try {
           const result = await importPositions(file);
-          const errors = (result.errors || []).slice(0, 3).map((item) => `第${item.rowNum}行:${item.message || item.errorMessage}`).join(";");
-          this.$alert(`总计 ${result.totalCount || 0} 条,成功 ${result.successCount || 0} 条,失败 ${result.failCount || 0} 条${errors ? `;${errors}` : ""}`, "岗位导入结果", { type: result.failCount ? "warning" : "success" });
+          const errors = (result.errors || [])
+            .slice(0, 3)
+            .map(
+              (item) =>
+                `第${item.rowNum}行:${item.message || item.errorMessage}`,
+            )
+            .join(";");
+          this.$alert(
+            `总计 ${result.totalCount || 0} 条,成功 ${
+              result.successCount || 0
+            } 条,失败 ${result.failCount || 0} 条${
+              errors ? `;${errors}` : ""
+            }`,
+            "岗位导入结果",
+            { type: result.failCount ? "warning" : "success" },
+          );
           await this.loadData();
-        } catch (error) { this.$message.error(error.message || "岗位导入失败"); }
-        finally { this.loading = false; }
+        } catch (error) {
+          this.$message.error(error.message || "岗位导入失败");
+        } finally {
+          this.loading = false;
+        }
       };
       input.click();
     },
     async exportManual() {
-      if (!this.selectedPosition?.id) return this.$message.warning("请先选择需要生成说明书的岗位");
+      if (!this.selectedPosition?.id)
+        return this.$message.warning("请先选择需要生成说明书的岗位");
       try {
-        const document = await generatePositionDocument(this.selectedPosition.id);
-        const documentId = document?.documentId || document?.id || (typeof document === "string" || typeof document === "number" ? document : null);
+        const document = await generatePositionDocument(
+          this.selectedPosition.id,
+        );
+        const documentId =
+          document?.documentId ||
+          document?.id ||
+          (typeof document === "string" || typeof document === "number"
+            ? document
+            : null);
         if (documentId) {
           await downloadPositionDocument(documentId);
           this.$message.success("岗位说明书已生成并开始下载");
@@ -1534,37 +1679,60 @@ export default {
           this.$message.success("岗位说明书已生成,可在岗位说明书版本中下载");
         }
         this.selectedPosition = await getPositionById(this.selectedPosition.id);
-      } catch (error) { this.$message.error(error.message || "导出失败"); }
+      } catch (error) {
+        this.$message.error(error.message || "导出失败");
+      }
     },
     async downloadImportTemplate() {
-      try { await downloadPositionImportTemplate(); }
-      catch (error) { this.$message.error(error.message || "模板下载失败"); }
+      try {
+        await downloadPositionImportTemplate();
+      } catch (error) {
+        this.$message.error(error.message || "模板下载失败");
+      }
     },
     async exportChannels() {
-      try { await exportPromotionChannels(); }
-      catch (error) { this.$message.error(error.message || "晋升通道导出失败"); }
+      try {
+        await exportPromotionChannels();
+      } catch (error) {
+        this.$message.error(error.message || "晋升通道导出失败");
+      }
     },
     async showAllHistory() {
       this.loading = true;
       try {
-        const page = await getPositionChangePage({ pageNum: 1, size: 1000, sortName: "createTime", orderBy: "descending" });
+        const page = await getPositionChangePage({
+          pageNum: 1,
+          size: 1000,
+          sortName: "createTime",
+          orderBy: "descending",
+        });
         const groups = new Map();
         (page?.list || []).forEach((item) => {
           const key = String(item.positionId);
-          if (!groups.has(key)) groups.set(key, {
-            id: item.positionId, name: item.positionName, code: item.positionCode, deptId: item.deptId,
-            history: [],
-          });
+          if (!groups.has(key))
+            groups.set(key, {
+              id: item.positionId,
+              name: item.positionName,
+              code: item.positionCode,
+              deptId: item.deptId,
+              history: [],
+            });
           groups.get(key).history.push({
-            ...item, time: item.createTime, title: item.changeTitle || item.changeTypeName,
-            content: item.changeSummary, type: item.changeType, impacts: item.affectedScopes || [],
+            ...item,
+            time: item.createTime,
+            title: item.changeTitle || item.changeTypeName,
+            content: item.changeSummary,
+            type: item.changeType,
+            impacts: item.affectedScopes || [],
           });
         });
         this.historyPositions = [...groups.values()];
         this.historyVisible = true;
       } catch (error) {
         this.$message.error(error.message || "岗位变更历史加载失败");
-      } finally { this.loading = false; }
+      } finally {
+        this.loading = false;
+      }
     },
     viewHistoryPosition(positionId) {
       const position = this.localPositions.find(
@@ -1619,20 +1787,36 @@ export default {
     },
     positionsBySeq(seq) {
       const routes = this.promotionChannels
-        .filter((group) => sequenceCode(group.sequenceName || group.sequence) === seq)
+        .filter(
+          (group) => sequenceCode(group.sequenceName || group.sequence) === seq,
+        )
         .flatMap((group) => group.routes || []);
-      const sourceIds = new Set(routes.map((route) => String(route.sourcePositionId)).filter(Boolean));
+      const sourceIds = new Set(
+        routes.map((route) => String(route.sourcePositionId)).filter(Boolean),
+      );
       return this.localPositions
         .filter((item) => sourceIds.has(String(item.id)))
         .map((item) => {
-          const itemRoutes = routes.filter((route) => String(route.sourcePositionId) === String(item.id));
+          const itemRoutes = routes.filter(
+            (route) => String(route.sourcePositionId) === String(item.id),
+          );
           return {
             ...item,
             promote: {
               ...item.promote,
-              up: [...new Set(itemRoutes.map((route) => route.targetPositionName).filter(Boolean))],
-              conditions: [...new Set(itemRoutes.flatMap((route) => route.conditions || []))]
-            }
+              up: [
+                ...new Set(
+                  itemRoutes
+                    .map((route) => route.targetPositionName)
+                    .filter(Boolean),
+                ),
+              ],
+              conditions: [
+                ...new Set(
+                  itemRoutes.flatMap((route) => route.conditions || []),
+                ),
+              ],
+            },
           };
         })
         .sort((a, b) => a.level - b.level);
@@ -1719,11 +1903,20 @@ export default {
       );
     },
     salaryTemplateName(id) {
-      return this.salaryTemplates.find((item) => item.id === id || item.name === id)?.name || id || "-";
+      return (
+        this.salaryTemplates.find((item) => item.id === id || item.name === id)
+          ?.name ||
+        id ||
+        "-"
+      );
     },
     performanceSchemeName(id) {
       return (
-        this.performanceSchemes.find((item) => item.id === id || item.name === id)?.name || id || "-"
+        this.performanceSchemes.find(
+          (item) => item.id === id || item.name === id,
+        )?.name ||
+        id ||
+        "-"
       );
     },
     attendRuleName(id) {
@@ -1749,4 +1942,8 @@ export default {
 };
 </script>
 
-<style src="../../styles/views/positionManagement/index.scss" lang="scss" scoped></style>
+<style
+  src="../../styles/views/positionManagement/index.scss"
+  lang="scss"
+  scoped
+></style>

+ 20 - 9
src/views/positionSequence/index.vue

@@ -243,7 +243,8 @@
       <div class="drawer-body">
         <template v-if="readOnly">
           <section class="detail-overview">
-            <span v-if="false"
+            <span
+              v-if="false"
               class="detail-category"
               :class="'category-' + form.category"
               >{{ form.category }}</span
@@ -562,7 +563,10 @@ export default {
         active,
         inactive: total - active,
         categories: new Set(this.sequences.map((item) => item.category)).size,
-        levels: this.sequences.reduce((sum, item) => sum + item.levels.length, 0),
+        levels: this.sequences.reduce(
+          (sum, item) => sum + item.levels.length,
+          0,
+        ),
         activeRate: total ? Math.round((active / total) * 100) : 0,
       };
     },
@@ -570,8 +574,7 @@ export default {
       const keyword = this.filters.keyword.toLowerCase();
       return this.sequences.filter((item) => {
         const keywordMatch =
-          !keyword ||
-          item.name.toLowerCase().includes(keyword);
+          !keyword || item.name.toLowerCase().includes(keyword);
         const categoryMatch = true;
         const statusMatch =
           this.filters.status === "" || item.status === this.filters.status;
@@ -680,8 +683,8 @@ export default {
         if (!valid) return;
         const duplicate = this.sequences.some(
           (item) =>
-            item.name.trim().toLowerCase() === this.form.name.trim().toLowerCase() &&
-            item.id !== this.form.id,
+            item.name.trim().toLowerCase() ===
+              this.form.name.trim().toLowerCase() && item.id !== this.form.id,
         );
         if (duplicate) {
           this.$message.warning("岗位序列名称已存在,请更换后重试");
@@ -691,7 +694,10 @@ export default {
         try {
           const originalRecords = this.form.levelRecords || [];
           const originalByLevel = new Map(
-            originalRecords.map((item) => [String(item.level).toUpperCase(), item]),
+            originalRecords.map((item) => [
+              String(item.level).toUpperCase(),
+              item,
+            ]),
           );
           if (this.mode === "edit") {
             const removed = originalRecords.filter(
@@ -718,7 +724,8 @@ export default {
             if (record) {
               const inUse = await isPositionSequenceInUse(record.id);
               const nameChanged = record.sequence !== payload.sequence;
-              const levelChanged = String(record.level) !== String(payload.level);
+              const levelChanged =
+                String(record.level) !== String(payload.level);
               if (inUse && (nameChanged || levelChanged)) {
                 throw new Error(
                   `层级 ${record.level} 已被岗位使用,只允许修改启停状态`,
@@ -777,4 +784,8 @@ export default {
 };
 </script>
 
-<style lang="scss" scoped src="../../styles/views/positionSequence/index.scss"></style>
+<style
+  lang="scss"
+  scoped
+  src="../../styles/views/positionSequence/index.scss"
+></style>

+ 256 - 98
src/views/recruitmentPlan/index.vue

@@ -627,9 +627,16 @@
 
 <script>
 import {
-  getRecruitmentPlans,
-  departmentOptions,
-  positionOptions,
+  adaptPosition,
+  getPositionPage,
+  getRecruitDemandById,
+  getRecruitDemandPage,
+  submitRecruitDemand,
+} from "@/api/hr";
+import { listOrganizations } from "@/api/organization";
+import {
+  departmentOptions as mockDepartmentOptions,
+  positionOptions as mockPositionOptions,
   channelOptions,
   skillOptions,
   approvalStatusOptions,
@@ -647,6 +654,50 @@ const SectionTitle = {
   },
 };
 
+const REASON_LABELS = {
+  NEW_HEADCOUNT: "新增编制",
+  REPLACEMENT: "人员补充",
+  BUSINESS_EXPANSION: "业务扩张",
+  SEASONAL: "季节性需求",
+  TURNOVER_SUPPLEMENT: "流失补充",
+  TEMPORARY: "临时需求",
+  OTHER: "其他",
+};
+const REASON_CODES = Object.fromEntries(
+  Object.entries(REASON_LABELS).map(([code, label]) => [label, code]),
+);
+const URGENCY_LABELS = {
+  LOW: "普通",
+  NORMAL: "普通",
+  HIGH: "紧急",
+  URGENT: "特急",
+};
+const URGENCY_CODES = { 普通: "NORMAL", 紧急: "HIGH", 特急: "URGENT" };
+const RECRUIT_TYPE_LABELS = {
+  SOCIAL: "社会招聘",
+  CAMPUS: "校园招聘",
+  TECHNICAL: "技术岗位",
+  PRODUCTION: "生产岗位",
+  SEASONAL: "季节性用工",
+  DISPATCH: "劳务派遣",
+  AGENCY: "中介招聘",
+  INTERNAL_REFERRAL: "内部推荐",
+  REHIRE: "返聘",
+  URGENT: "紧急招聘",
+  BATCH: "批量招聘",
+};
+const BUSINESS_LABELS = {
+  ACTIVE: "招聘中",
+  OPEN: "招聘中",
+  IN_PROGRESS: "招聘中",
+  RECRUITING: "招聘中",
+  SUSPENDED: "已暂停",
+  PAUSED: "已暂停",
+  CLOSED: "已关闭",
+  CANCELLED: "已取消",
+  CANCELED: "已取消",
+};
+
 const emptyFilters = () => ({
   keyword: "",
   cycle: [],
@@ -685,27 +736,63 @@ const emptyForm = () => ({
   onboardedCount: 0,
   owner: "当前用户",
   updatedAt: "",
+  // 后端原始字段,用于提交时还原枚举值
+  reasonRaw: "",
+  urgencyRaw: "",
+  employmentRaw: "",
+  recruitTypeRaw: "",
+  departmentId: null,
+  positionId: null,
+  specialRequirementJson: "",
 });
 
+function normApproval(value) {
+  const key = String(value || "").toUpperCase();
+  if (key === "DRAFT") return "draft";
+  if (
+    ["PENDING", "PENDING_APPROVAL", "APPROVING", "SUBMITTED", "PROCESSING"].includes(key)
+  )
+    return "deptPending";
+  if (["APPROVED", "PASS", "PASSED"].includes(key)) return "approved";
+  if (["REJECTED", "RETURNED", "FAIL"].includes(key)) return "rejected";
+  return key ? key.toLowerCase() : "draft";
+}
+
+function parseSpecial(raw) {
+  try {
+    const data = JSON.parse(raw?.specialRequirementJson || "{}");
+    return (
+      data.responsibilities ||
+      data.requirements ||
+      data.requirement ||
+      data.content ||
+      ""
+    );
+  } catch {
+    return raw?.specialRequirementJson || "";
+  }
+}
+
 export default {
   name: "RecruitmentPlan",
   components: { SectionTitle },
   data() {
-    const salaryValidator = (rule, value, callback) => {
+    const salaryValidator = (_rule, _value, callback) => {
       if (this.form.salaryMax < this.form.salaryMin)
         callback(new Error("最高月薪不能低于最低月薪"));
       else callback();
     };
     return {
       loading: false,
+      saving: false,
       plans: [],
-      departmentOptions,
-      positionOptions,
+      departmentOptions: mockDepartmentOptions,
+      positionOptions: mockPositionOptions,
       channelOptions,
       skillOptions,
       approvalStatusOptions,
       progressOptions,
-      reasonOptions: ["新增编制", "人员补充", "业务扩张"],
+      reasonOptions: Object.values(REASON_LABELS),
       genderOptions: ["不限", "男", "女"],
       educationOptions: [
         "不限",
@@ -716,6 +803,11 @@ export default {
         "硕士及以上",
       ],
       experienceOptions: ["不限", "1年以上", "2年以上", "3年以上", "5年以上"],
+      // 真实下拉数据源
+      departments: [],
+      positions: [],
+      deptIdMap: {},
+      positionIdMap: {},
       cacheKeyUrl: "oa-pc-recruitment-plan-v1",
       draftFilters: emptyFilters(),
       filters: emptyFilters(),
@@ -836,7 +928,7 @@ export default {
       ).length;
       const pending = this.plans.filter((item) => this.isPending(item)).length;
       const active = this.plans.filter(
-        (item) => !["待发布", "已入职"].includes(item.progress),
+        (item) => item.progress === "招聘中",
       ).length;
       const onboarded = this.plans.reduce(
         (sum, item) => sum + item.onboardedCount,
@@ -868,7 +960,7 @@ export default {
           label: "招聘进行中",
           value: active,
           unit: "项",
-          note: "渠道已发布并在推进",
+          note: "业务状态为招聘中",
           icon: "el-icon-data-analysis",
           tone: "cyan",
           filter: { active: true },
@@ -888,7 +980,7 @@ export default {
           label: "累计已入职",
           value: onboarded,
           unit: "人",
-          note: "模拟计划转化结果",
+          note: "已入职人数汇总",
           icon: "el-icon-user",
           tone: "purple",
           filter: { progress: "已入职" },
@@ -937,14 +1029,95 @@ export default {
     },
   },
   created() {
+    this.loadOptions();
     this.loadData();
   },
   methods: {
+    // 加载部门/岗位下拉(真实数据)
+    async loadOptions() {
+      try {
+        const [organizations, positionPage] = await Promise.all([
+          listOrganizations().catch(() => []),
+          getPositionPage({ pageNum: 1, size: 1000 }).catch(() => ({ list: [] })),
+        ]);
+        const orgs = Array.isArray(organizations) ? organizations : [];
+        const positions = (positionPage?.list || []).map(adaptPosition);
+        this.departments = orgs;
+        this.positions = positions;
+        this.departmentOptions = orgs.map((item) => item.name);
+        this.positionOptions = positions.map((item) => item.name);
+        this.deptIdMap = Object.fromEntries(
+          orgs.map((item) => [item.name, item.id]),
+        );
+        this.positionIdMap = Object.fromEntries(
+          positions.map((item) => [item.name, item.id]),
+        );
+      } catch (error) {
+        // 加载失败时回退到 mock 静态选项
+      }
+    },
     async loadData(showMessage = false) {
       this.loading = true;
-      this.plans = await getRecruitmentPlans();
-      this.loading = false;
-      if (showMessage) this.$message.success("招聘计划数据已刷新");
+      try {
+        const data = await getRecruitDemandPage({ pageNum: 1, size: 1000 });
+        this.plans = (data?.list || []).map((item) => this.adaptDemand(item));
+        if (showMessage) this.$message.success("招聘计划数据已刷新");
+      } finally {
+        this.loading = false;
+      }
+    },
+    // 后端行 → 页面原有字段结构
+    adaptDemand(raw = {}) {
+      const business = String(raw.businessStatus || "").toUpperCase();
+      const start = raw.applyDate || String(raw.createTime || "").slice(0, 10);
+      const end = raw.expectedArrivalDate || start || "";
+      const dept = this.departments.find(
+        (item) => String(item.id) === String(raw.departmentId),
+      );
+      const recruitType = RECRUIT_TYPE_LABELS[raw.recruitType];
+      return this.cloneRow({
+        id: raw.id,
+        code: raw.demandNo || "-",
+        planName: raw.positionName || "未命名招聘需求",
+        department: dept?.name || raw.departmentName || "-",
+        position: raw.positionName || "-",
+        headcount: Number(raw.recruitCount ?? 0),
+        urgency: URGENCY_LABELS[raw.urgencyLevel] || raw.urgencyLevel || "普通",
+        reason: REASON_LABELS[raw.demandReason] || raw.demandReason || "-",
+        arrivalDate: raw.expectedArrivalDate || "",
+        salaryMin: 0,
+        salaryMax: 0,
+        responsibilities: parseSpecial(raw) || "暂无岗位职责说明",
+        ageRange: "不限",
+        gender: "不限",
+        education: "不限",
+        experience: "不限",
+        skills: [],
+        certificates: "",
+        qualifications: "",
+        cycle: [start || "", end || ""],
+        channels: recruitType ? [recruitType] : [],
+        channelBudget: 0,
+        progress: BUSINESS_LABELS[business] || raw.businessStatus || "待发布",
+        approvalStatus: normApproval(raw.approvalStatus),
+        resumeCount: 0,
+        interviewCount: 0,
+        offerCount: Number(raw.offerCount ?? 0),
+        onboardedCount: Number(raw.onboardCount ?? 0),
+        owner: raw.applicantUserId || "-",
+        updatedAt: raw.createTime || "",
+        // 保留后端原始字段,用于提交时还原
+        reasonRaw: raw.demandReason,
+        urgencyRaw: raw.urgencyLevel,
+        employmentRaw: raw.employmentType,
+        recruitTypeRaw: raw.recruitType,
+        departmentId: raw.departmentId,
+        positionId: raw.positionId,
+        remainingCount: Number(raw.remainingCount ?? 0),
+        offerAcceptedCount: Number(raw.offerAcceptedCount ?? 0),
+        pendingOnboardCount: Number(raw.pendingOnboardCount ?? 0),
+        specialRequirementJson: raw.specialRequirementJson,
+      });
     },
     refreshData() {
       this.loadData(true);
@@ -1004,10 +1177,15 @@ export default {
       this.drawerVisible = true;
       this.$nextTick(() => this.$refs.recruitmentForm?.clearValidate());
     },
-    openView(row) {
+    async openView(row) {
       this.mode = "view";
-      this.form = this.cloneRow(row);
       this.drawerVisible = true;
+      try {
+        const detail = await getRecruitDemandById(row.id);
+        this.form = this.adaptDemand(detail);
+      } catch (error) {
+        this.form = this.cloneRow(row);
+      }
     },
     openEdit(row) {
       if (!this.canEdit(row)) return;
@@ -1024,112 +1202,86 @@ export default {
       this.drawerVisible = false;
       if (typeof done === "function") done();
     },
+    // 表单 → 后端提交参数
+    buildPayload(form) {
+      const departmentId =
+        form.departmentId || this.deptIdMap[form.department];
+      const positionId = form.positionId || this.positionIdMap[form.position];
+      if (!departmentId) {
+        this.$message.warning("请从下拉列表中选择申请部门");
+        return null;
+      }
+      if (!positionId) {
+        this.$message.warning("请从下拉列表中选择申请岗位");
+        return null;
+      }
+      const specialRequirementJson = JSON.stringify({
+        responsibilities: form.responsibilities,
+        qualifications: form.qualifications,
+        certificates: form.certificates,
+        skills: form.skills,
+        ageRange: form.ageRange,
+        gender: form.gender,
+        education: form.education,
+        experience: form.experience,
+        channels: form.channels,
+      });
+      return {
+        id: form.id || undefined,
+        demandReason: form.reasonRaw || REASON_CODES[form.reason],
+        departmentId,
+        positionId,
+        recruitCount: Number(form.headcount),
+        urgencyLevel: form.urgencyRaw || URGENCY_CODES[form.urgency],
+        employmentType: form.employmentRaw || undefined,
+        recruitType: form.recruitTypeRaw || undefined,
+        expectedArrivalDate: form.arrivalDate || undefined,
+        specialRequirementJson,
+      };
+    },
     savePlan(submit) {
       if (!submit && !this.form.planName.trim()) {
         this.$message.warning("保存草稿前至少填写计划名称");
         return;
       }
       const persist = () => {
-        if (this.form.salaryMax < this.form.salaryMin) {
-          this.$message.warning("最高月薪不能低于最低月薪");
-          return;
-        }
-        const payload = this.cloneRow({
-          ...this.form,
-          code: this.form.code || this.generateCode(),
-          approvalStatus: submit ? "deptPending" : "draft",
-          updatedAt: "2026-08-12 11:30",
-        });
-        if (this.mode === "create") {
-          payload.id = Math.max(0, ...this.plans.map((item) => item.id)) + 1;
-          this.plans.unshift(payload);
-        } else {
-          const index = this.plans.findIndex((item) => item.id === payload.id);
-          this.$set(this.plans, index, payload);
-        }
-        this.form = this.cloneRow(payload);
-        this.mode = "view";
-        this.$message.success(
-          submit ? "招聘计划已提交部门负责人审批" : "招聘计划草稿已保存",
-        );
-        this.reloadTable();
+        const payload = this.buildPayload(this.form);
+        if (!payload) return;
+        this.saving = true;
+        submitRecruitDemand(payload)
+          .then(() => {
+            this.$message.success(
+              submit ? "招聘需求已提交审批" : "招聘需求草稿已保存",
+            );
+            this.drawerVisible = false;
+            this.loadData();
+          })
+          .catch(() => {})
+          .finally(() => {
+            this.saving = false;
+          });
       };
       if (submit)
         this.$refs.recruitmentForm.validate((valid) => valid && persist());
       else persist();
     },
-    generateCode() {
-      const serial = String(
-        Math.max(0, ...this.plans.map((item) => Number(item.code.slice(-3)))) +
-          1,
-      ).padStart(3, "0");
-      return "RD202608" + serial;
-    },
     handleCommand(command, row) {
       if (command === "approve") this.advanceApproval(row);
       if (command === "publish") this.publishPlan(row);
       if (command === "progress") this.advanceProgress(row);
       if (command === "delete") this.deletePlan(row);
     },
-    mutateRow(row, patch) {
-      const index = this.plans.findIndex((item) => item.id === row.id);
-      const next = this.cloneRow({
-        ...this.plans[index],
-        ...patch,
-        updatedAt: "2026-08-12 11:30",
-      });
-      this.$set(this.plans, index, next);
-      if (this.drawerVisible && this.form.id === row.id)
-        this.form = this.cloneRow(next);
-      this.reloadTable();
-    },
     advanceApproval(row) {
-      const nextMap = {
-        deptPending: "hrPending",
-        hrPending: "managerPending",
-        managerPending: "approved",
-      };
-      const next = nextMap[row.approvalStatus];
-      if (!next) return;
-      this.mutateRow(row, { approvalStatus: next });
-      this.$message.success(
-        next === "approved"
-          ? "三级审批已完成,可发布招聘计划"
-          : "审批已通过,流转至下一节点",
-      );
+      this.$message.info(`【${row.planName}】后端暂未提供审批操作接口`);
     },
     publishPlan(row) {
-      this.mutateRow(row, { progress: "已发布" });
-      this.$message.success("招聘计划已发布,开始接收候选人简历");
+      this.$message.info(`【${row.planName}】后端暂未提供发布操作接口`);
     },
     advanceProgress(row) {
-      const steps = ["已发布", "已收简历", "已面试", "已Offer", "已入职"];
-      const next = steps[steps.indexOf(row.progress) + 1];
-      if (!next) return;
-      const patch = { progress: next };
-      if (next === "已收简历")
-        patch.resumeCount = Math.max(row.resumeCount, row.headcount * 3);
-      if (next === "已面试")
-        patch.interviewCount = Math.max(row.interviewCount, row.headcount * 2);
-      if (next === "已Offer")
-        patch.offerCount = Math.max(row.offerCount, row.headcount);
-      if (next === "已入职")
-        patch.onboardedCount = Math.max(row.onboardedCount, row.headcount);
-      this.mutateRow(row, patch);
-      this.$message.success("计划进度已推进至“" + next + "”");
+      this.$message.info(`【${row.planName}】后端暂未提供进度推进接口`);
     },
     deletePlan(row) {
-      this.$confirm("删除后将无法恢复该模拟计划,是否继续?", "删除招聘计划", {
-        type: "warning",
-      })
-        .then(() => {
-          this.plans = this.plans.filter((item) => item.id !== row.id);
-          if (this.drawerVisible && this.form.id === row.id)
-            this.drawerVisible = false;
-          this.$message.success("招聘计划已删除");
-          this.reloadTable();
-        })
-        .catch(() => {});
+      this.$message.info(`【${row.planName}】后端暂未提供删除操作接口`);
     },
     approvalLabel(status) {
       return (
@@ -1147,7 +1299,13 @@ export default {
         : "warning";
     },
     progressClass(progress) {
-      return "progress-" + progress;
+      const colorMap = {
+        招聘中: "progress-已发布",
+        已暂停: "progress-已面试",
+        已关闭: "progress-已取消",
+        已取消: "progress-已取消",
+      };
+      return colorMap[progress] || "progress-" + progress;
     },
     isPending(row) {
       return ["deptPending", "hrPending", "managerPending"].includes(

+ 214 - 173
yarn.lock

@@ -25,7 +25,7 @@
   resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz"
   integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==
 
-"@babel/core@^7.12.16":
+"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.12.0", "@babel/core@^7.12.16", "@babel/core@^7.13.0", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0":
   version "7.29.7"
   resolved "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz"
   integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==
@@ -936,7 +936,7 @@
     "@nodelib/fs.stat" "2.0.5"
     run-parallel "^1.1.9"
 
-"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
   version "2.0.5"
   resolved "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -949,61 +949,6 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
-"@parcel/watcher-android-arm64@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz#99aaa3223d43807c9340af439cad7e9b6d26ada6"
-  integrity sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==
-
-"@parcel/watcher-darwin-arm64@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz#024496e586b4744f09ce532bbe89fe38ef02a64e"
-  integrity sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==
-
-"@parcel/watcher-darwin-x64@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz#a4621df1359a93d39a332d9bab5ff09016a0608f"
-  integrity sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==
-
-"@parcel/watcher-freebsd-x64@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz#7f565ed1a5b3a5e604e6a4799121518265d62a3d"
-  integrity sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==
-
-"@parcel/watcher-linux-arm-glibc@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz#ad7d3825e67b81999165da42593022045abc0889"
-  integrity sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==
-
-"@parcel/watcher-linux-arm-musl@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz#fe7d1cccb2c483215c090e938cf5cf404d2f9a8c"
-  integrity sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==
-
-"@parcel/watcher-linux-arm64-glibc@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz#7e239dcb4646c4c79f006a7131a48238249530da"
-  integrity sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==
-
-"@parcel/watcher-linux-arm64-musl@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz#c58b8d9c6d8d81594be00dd83aab741c1aaf7e0e"
-  integrity sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==
-
-"@parcel/watcher-linux-x64-glibc@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz#5184fa9a770478d86e56875f4ee163a0abdc8791"
-  integrity sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==
-
-"@parcel/watcher-linux-x64-musl@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz#2d1c55aa7246cbc7670e2612058a8a542c9cf246"
-  integrity sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==
-
-"@parcel/watcher-win32-arm64@2.6.0":
-  version "2.6.0"
-  resolved "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz#15e09432040fee9e2213aa9c10ed589012526def"
-  integrity sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==
-
 "@parcel/watcher-win32-x64@2.6.0":
   version "2.6.0"
   resolved "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz"
@@ -1412,7 +1357,7 @@
   resolved "https://registry.npmmirror.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-5.0.9.tgz"
   integrity sha512-AQhgGNFVd4Pu2crvS0a+hRckgrJv07gzOASdbLd3I72wkT43dd01MLRp8IBRRsu92t3MXenW86AZUCbQBz3//A==
 
-"@vue/cli-service@^5.0.8":
+"@vue/cli-service@^3.0.0 || ^4.0.0 || ^5.0.0-0", "@vue/cli-service@^5.0.8":
   version "5.0.9"
   resolved "https://registry.npmmirror.com/@vue/cli-service/-/cli-service-5.0.9.tgz"
   integrity sha512-yTX7GVyM19tEbd+y5/gA6MkVKA6K61nVYHYAivD61Hx6odVFmQsaC3/R3cWAHM1P5oVKCevBbumPljbT+tFG2w==
@@ -1510,17 +1455,6 @@
     "@vue/compiler-core" "3.5.41"
     "@vue/shared" "3.5.41"
 
-"@vue/compiler-sfc@2.7.16":
-  version "2.7.16"
-  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz"
-  integrity sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==
-  dependencies:
-    "@babel/parser" "^7.23.5"
-    postcss "^8.4.14"
-    source-map "^0.6.1"
-  optionalDependencies:
-    prettier "^1.18.2 || ^2.0.0"
-
 "@vue/compiler-sfc@^3.5.18":
   version "3.5.41"
   resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz"
@@ -1536,6 +1470,17 @@
     postcss "^8.5.19"
     source-map-js "^1.2.1"
 
+"@vue/compiler-sfc@2.7.16":
+  version "2.7.16"
+  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz"
+  integrity sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==
+  dependencies:
+    "@babel/parser" "^7.23.5"
+    postcss "^8.4.14"
+    source-map "^0.6.1"
+  optionalDependencies:
+    prettier "^1.18.2 || ^2.0.0"
+
 "@vue/compiler-ssr@3.5.41":
   version "3.5.41"
   resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz"
@@ -1560,7 +1505,7 @@
   optionalDependencies:
     prettier "^1.18.2 || ^2.0.0"
 
-"@vue/shared@3.5.41", "@vue/shared@^3.5.18":
+"@vue/shared@^3.5.18", "@vue/shared@3.5.41":
   version "3.5.41"
   resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz"
   integrity sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==
@@ -1581,7 +1526,7 @@
   resolved "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz"
   integrity sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==
 
-"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
+"@webassemblyjs/ast@^1.14.1", "@webassemblyjs/ast@1.14.1":
   version "1.14.1"
   resolved "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz"
   integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==
@@ -1682,7 +1627,7 @@
     "@webassemblyjs/wasm-gen" "1.14.1"
     "@webassemblyjs/wasm-parser" "1.14.1"
 
-"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1":
+"@webassemblyjs/wasm-parser@^1.14.1", "@webassemblyjs/wasm-parser@1.14.1":
   version "1.14.1"
   resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz"
   integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==
@@ -1756,7 +1701,7 @@ ajv-keywords@^5.1.0:
   dependencies:
     fast-deep-equal "^3.1.3"
 
-ajv@^6.12.4, ajv@^6.12.5:
+ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1:
   version "6.15.0"
   resolved "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz"
   integrity sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==
@@ -1766,7 +1711,17 @@ ajv@^6.12.4, ajv@^6.12.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^8.0.0, ajv@^8.9.0:
+ajv@^8.0.0:
+  version "8.20.0"
+  resolved "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz"
+  integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    fast-uri "^3.0.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+
+ajv@^8.8.2, ajv@^8.9.0:
   version "8.20.0"
   resolved "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz"
   integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==
@@ -2036,7 +1991,7 @@ braces@^3.0.3, braces@~3.0.2:
   dependencies:
     fill-range "^7.1.1"
 
-browserslist@^4.0.0, browserslist@^4.16.3, browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.6:
+browserslist@^4.0.0, browserslist@^4.16.3, browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.6, "browserslist@>= 4.21.0":
   version "4.28.7"
   resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz"
   integrity sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==
@@ -2060,7 +2015,7 @@ buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
-bytes@3.1.2, bytes@~3.1.2:
+bytes@~3.1.2, bytes@3.1.2:
   version "3.1.2"
   resolved "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz"
   integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
@@ -2146,7 +2101,23 @@ chalk@^3.0.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
+chalk@^4.0.0:
+  version "4.1.2"
+  resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chalk@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chalk@^4.1.2:
   version "4.1.2"
   resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz"
   integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -2274,16 +2245,16 @@ color-convert@^2.0.1:
   dependencies:
     color-name "~1.1.4"
 
-color-name@1.1.3:
-  version "1.1.3"
-  resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz"
-  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-
 color-name@~1.1.4:
   version "1.1.4"
   resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz"
+  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
 colord@^2.9.1:
   version "2.9.3"
   resolved "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz"
@@ -2463,12 +2434,17 @@ cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+crypto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz"
+  integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
+
 css-declaration-sorter@^6.3.1:
   version "6.4.1"
   resolved "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz"
   integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==
 
-css-loader@^6.5.0:
+css-loader@*, css-loader@^6.5.0:
   version "6.11.0"
   resolved "https://registry.npmmirror.com/css-loader/-/css-loader-6.11.0.tgz"
   integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==
@@ -2599,13 +2575,6 @@ debounce@^1.2.1:
   resolved "https://registry.npmmirror.com/debounce/-/debounce-1.2.1.tgz"
   integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
 
-debug@2.6.9:
-  version "2.6.9"
-  resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz"
-  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
-  dependencies:
-    ms "2.0.0"
-
 debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.6, debug@^4.4.3:
   version "4.4.3"
   resolved "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz"
@@ -2613,6 +2582,13 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.6, debug@^4.4.3:
   dependencies:
     ms "^2.1.3"
 
+debug@2.6.9:
+  version "2.6.9"
+  resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
 deepmerge@^1.2.0, deepmerge@^1.5.2:
   version "1.5.2"
   resolved "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz"
@@ -2670,17 +2646,17 @@ delegate@^3.1.2:
   resolved "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz"
   integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
 
-depd@2.0.0, depd@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz"
-  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
-
 depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/depd/-/depd-1.1.2.tgz"
   integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
 
-destroy@1.2.0, destroy@~1.2.0:
+depd@~2.0.0, depd@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@~1.2.0, destroy@1.2.0:
   version "1.2.0"
   resolved "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz"
   integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
@@ -2806,7 +2782,7 @@ electron-to-chromium@^1.5.393:
   resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz"
   integrity sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==
 
-element-ui@2.15.7:
+element-ui@>=2.15.4, element-ui@2.15.7:
   version "2.15.7"
   resolved "https://registry.npmmirror.com/element-ui/-/element-ui-2.15.7.tgz"
   integrity sha512-+J6rnXajxzLwV6w8Q6bf7Yqzk1FO1ewbIrCy/4B5alnd7tj8WEpfQoAvISirVaUGVGy77d9Ji3o2bF4f0AsJLQ==
@@ -3184,11 +3160,6 @@ fs.realpath@^1.0.0:
   resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz"
   integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
 
-fsevents@~2.3.2:
-  version "2.3.3"
-  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
-  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
-
 function-bind@^1.1.2:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz"
@@ -3240,7 +3211,7 @@ get-stream@^6.0.0:
   resolved "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz"
   integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
 
-glob-parent@^5.1.2, glob-parent@~5.1.2:
+glob-parent@^5.1.2:
   version "5.1.2"
   resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -3254,6 +3225,13 @@ glob-parent@^6.0.1:
   dependencies:
     is-glob "^4.0.3"
 
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob@^7.1.3:
   version "7.2.3"
   resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz"
@@ -3527,21 +3505,21 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
+inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4, inherits@2, inherits@2.0.4:
   version "2.0.4"
   resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
-ipaddr.js@1.9.1:
-  version "1.9.1"
-  resolved "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
-  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
-
 ipaddr.js@^2.0.1:
   version "2.5.0"
   resolved "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.5.0.tgz"
   integrity sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==
 
+ipaddr.js@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz"
@@ -3791,7 +3769,16 @@ loader-utils@^1.0.2, loader-utils@^1.1.0:
     emojis-list "^3.0.0"
     json5 "^1.0.1"
 
-loader-utils@^2.0.0, loader-utils@^2.0.4:
+loader-utils@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz"
+  integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^2.1.2"
+
+loader-utils@^2.0.4:
   version "2.0.4"
   resolved "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz"
   integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
@@ -3959,16 +3946,16 @@ micromatch@^4.0.2, micromatch@^4.0.8:
     braces "^3.0.3"
     picomatch "^2.3.1"
 
+mime-db@^1.54.0, "mime-db@>= 1.43.0 < 2":
+  version "1.54.0"
+  resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz"
+  integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
+
 mime-db@1.52.0:
   version "1.52.0"
   resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz"
   integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
 
-"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0:
-  version "1.54.0"
-  resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz"
-  integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
-
 mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34, mime-types@~2.1.35:
   version "2.1.35"
   resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
@@ -4043,16 +4030,16 @@ mrmime@^2.0.0:
   resolved "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz"
   integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
 
+ms@^2.1.3, ms@2.1.3:
+  version "2.1.3"
+  resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz"
   integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
 
-ms@2.1.3, ms@^2.1.3:
-  version "2.1.3"
-  resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz"
-  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
 multicast-dns@^7.2.5:
   version "7.2.5"
   resolved "https://registry.npmmirror.com/multicast-dns/-/multicast-dns-7.2.5.tgz"
@@ -4075,16 +4062,16 @@ nanoid@^3.3.16:
   resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.17.tgz"
   integrity sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==
 
-negotiator@0.6.3:
-  version "0.6.3"
-  resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz"
-  integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
-
 negotiator@~0.6.4:
   version "0.6.4"
   resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz"
   integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
 
+negotiator@0.6.3:
+  version "0.6.3"
+  resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz"
+  integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
 neo-async@^2.6.2:
   version "2.6.2"
   resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz"
@@ -4682,15 +4669,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
   resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
 
-postcss@^7.0.36:
-  version "7.0.39"
-  resolved "https://registry.npmmirror.com/postcss/-/postcss-7.0.39.tgz"
-  integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
-  dependencies:
-    picocolors "^0.2.1"
-    source-map "^0.6.1"
-
-postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.33, postcss@^8.5.19:
+"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.33, postcss@^8.5.19:
   version "8.5.25"
   resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.25.tgz"
   integrity sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==
@@ -4699,6 +4678,14 @@ postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.33, postcss@^8.5.1
     picocolors "^1.1.1"
     source-map-js "^1.2.1"
 
+postcss@^7.0.36:
+  version "7.0.39"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-7.0.39.tgz"
+  integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
+  dependencies:
+    picocolors "^0.2.1"
+    source-map "^0.6.1"
+
 "prettier@^1.18.2 || ^2.0.0":
   version "2.8.8"
   resolved "https://registry.npmmirror.com/prettier/-/prettier-2.8.8.tgz"
@@ -4972,7 +4959,7 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
-safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
+safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1:
   version "5.2.1"
   resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -4994,7 +4981,7 @@ sass-loader@^13.0.2:
   dependencies:
     neo-async "^2.6.2"
 
-sass@^1.54.8:
+sass@^1.3.0, sass@^1.54.8:
   version "1.102.0"
   resolved "https://registry.npmmirror.com/sass/-/sass-1.102.0.tgz"
   integrity sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==
@@ -5019,7 +5006,7 @@ schema-utils@^2.6.5:
     ajv "^6.12.4"
     ajv-keywords "^3.5.2"
 
-schema-utils@^3.0.0, schema-utils@^3.1.1:
+schema-utils@^3.0.0:
   version "3.3.0"
   resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz"
   integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
@@ -5028,7 +5015,46 @@ schema-utils@^3.0.0, schema-utils@^3.1.1:
     ajv "^6.12.5"
     ajv-keywords "^3.5.2"
 
-schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3:
+schema-utils@^3.1.1:
+  version "3.3.0"
+  resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz"
+  integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
+  dependencies:
+    "@types/json-schema" "^7.0.8"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+schema-utils@^4.0.0:
+  version "4.3.3"
+  resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz"
+  integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==
+  dependencies:
+    "@types/json-schema" "^7.0.9"
+    ajv "^8.9.0"
+    ajv-formats "^2.1.1"
+    ajv-keywords "^5.1.0"
+
+schema-utils@^4.2.0:
+  version "4.3.3"
+  resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz"
+  integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==
+  dependencies:
+    "@types/json-schema" "^7.0.9"
+    ajv "^8.9.0"
+    ajv-formats "^2.1.1"
+    ajv-keywords "^5.1.0"
+
+schema-utils@^4.3.0:
+  version "4.3.3"
+  resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz"
+  integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==
+  dependencies:
+    "@types/json-schema" "^7.0.9"
+    ajv "^8.9.0"
+    ajv-formats "^2.1.1"
+    ajv-keywords "^5.1.0"
+
+schema-utils@^4.3.3:
   version "4.3.3"
   resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz"
   integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==
@@ -5056,7 +5082,7 @@ selfsigned@^2.1.1:
     "@types/node-forge" "^1.3.0"
     node-forge "^1"
 
-"semver@2 || 3 || 4 || 5", semver@^5.5.0:
+semver@^5.5.0:
   version "5.7.2"
   resolved "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz"
   integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
@@ -5066,11 +5092,26 @@ semver@^6.0.0, semver@^6.3.1:
   resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz"
   integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
 
-semver@^7.3.4, semver@^7.3.5, semver@^7.5.4:
+semver@^7.3.4:
   version "7.8.5"
   resolved "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz"
   integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==
 
+semver@^7.3.5:
+  version "7.8.5"
+  resolved "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz"
+  integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==
+
+semver@^7.5.4:
+  version "7.8.5"
+  resolved "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz"
+  integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==
+
+"semver@2 || 3 || 4 || 5":
+  version "5.7.2"
+  resolved "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz"
+  integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
+
 send@~0.19.0, send@~0.19.1:
   version "0.19.2"
   resolved "https://registry.npmmirror.com/send/-/send-0.19.2.tgz"
@@ -5137,7 +5178,7 @@ set-function-length@^1.2.2:
     gopd "^1.0.1"
     has-property-descriptors "^1.0.2"
 
-setprototypeof@1.2.0, setprototypeof@~1.2.0:
+setprototypeof@~1.2.0, setprototypeof@1.2.0:
   version "1.2.0"
   resolved "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz"
   integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
@@ -5251,17 +5292,17 @@ sockjs@^0.3.24:
     uuid "^8.3.2"
     websocket-driver "^0.7.4"
 
-sortablejs@1.10.2:
-  version "1.10.2"
-  resolved "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.10.2.tgz"
-  integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
-
 sortablejs@^1.15.0:
   version "1.15.7"
   resolved "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz"
   integrity sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==
 
-"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
+sortablejs@1.10.2:
+  version "1.10.2"
+  resolved "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.10.2.tgz"
+  integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
+
+source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
   version "1.2.1"
   resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz"
   integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@@ -5355,6 +5396,20 @@ statuses@~2.0.1, statuses@~2.0.2:
   resolved "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz"
   integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
 
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
 string-width@^2.1.1:
   version "2.1.1"
   resolved "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz"
@@ -5372,20 +5427,6 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.1"
 
-string_decoder@^1.1.1:
-  version "1.3.0"
-  resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz"
-  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
-  dependencies:
-    safe-buffer "~5.2.0"
-
-string_decoder@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz"
-  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
-  dependencies:
-    safe-buffer "~5.1.0"
-
 strip-ansi@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-4.0.0.tgz"
@@ -5534,7 +5575,7 @@ to-regex-range@^5.0.1:
   dependencies:
     is-number "^7.0.0"
 
-toidentifier@1.0.1, toidentifier@~1.0.1:
+toidentifier@~1.0.1, toidentifier@1.0.1:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
@@ -5549,16 +5590,16 @@ tr46@~0.0.3:
   resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz"
   integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
 
-tslib@2.3.0:
-  version "2.3.0"
-  resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz"
-  integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
-
 tslib@^2.0.3:
   version "2.8.1"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz"
   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
 
+tslib@2.3.0:
+  version "2.3.0"
+  resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz"
+  integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
+
 type-fest@^0.6.0:
   version "0.6.0"
   resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.6.0.tgz"
@@ -5702,7 +5743,7 @@ vue-style-loader@^4.1.0, vue-style-loader@^4.1.3:
     hash-sum "^1.0.2"
     loader-utils "^1.0.2"
 
-vue-template-compiler@^2.7.10:
+vue-template-compiler@^2.0.0, vue-template-compiler@^2.7.10:
   version "2.7.16"
   resolved "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz"
   integrity sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==
@@ -5715,7 +5756,7 @@ vue-template-es2015-compiler@^1.9.0:
   resolved "https://registry.npmmirror.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz"
   integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
 
-vue@^2.7.10:
+vue@*, "vue@^2 || ^3.2.13", vue@^2.0.0, vue@^2.5.17, vue@^2.7.10, "vue@>=2.6.0 <3.0.0":
   version "2.7.16"
   resolved "https://registry.npmmirror.com/vue/-/vue-2.7.16.tgz"
   integrity sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==
@@ -5738,7 +5779,7 @@ vuex-persistedstate@^4.1.0:
     deepmerge "^4.2.2"
     shvl "^2.0.3"
 
-vuex@^3.6.2:
+"vuex@^3.0 || ^4.0.0-rc", vuex@^3.6.2:
   version "3.6.2"
   resolved "https://registry.npmmirror.com/vuex/-/vuex-3.6.2.tgz"
   integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
@@ -5851,7 +5892,7 @@ webpack-merge@^5.7.3:
     flat "^5.0.2"
     wildcard "^2.0.0"
 
-webpack-sources@^3.5.1:
+webpack-sources@*, webpack-sources@^3.5.1:
   version "3.5.1"
   resolved "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.5.1.tgz"
   integrity sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==
@@ -5861,7 +5902,7 @@ webpack-virtual-modules@^0.4.2:
   resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz"
   integrity sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==
 
-webpack@^5.54.0, webpack@^5.74.0:
+"webpack@^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "webpack@^3.0.0 || ^4.1.0 || ^5.0.0-0", "webpack@^4.0.0 || ^5.0.0", "webpack@^4.1.0 || ^5.0.0-0", "webpack@^4.27.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.54.0, webpack@^5.74.0, webpack@>=2:
   version "5.109.2"
   resolved "https://registry.npmmirror.com/webpack/-/webpack-5.109.2.tgz"
   integrity sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==
@@ -5887,7 +5928,7 @@ webpack@^5.54.0, webpack@^5.74.0:
     watchpack "^2.5.2"
     webpack-sources "^3.5.1"
 
-websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
+websocket-driver@^0.7.4, websocket-driver@>=0.5.1:
   version "0.7.5"
   resolved "https://registry.npmmirror.com/websocket-driver/-/websocket-driver-0.7.5.tgz"
   integrity sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels