Explorar el Código

feat(profile): 添加岗位信息加载与提交逻辑,优化用户编辑体验

xieyong hace 6 días
padre
commit
c2994373b8
Se han modificado 4 ficheros con 674 adiciones y 6 borrados
  1. 369 0
      src/api/position.js
  2. 225 0
      src/api/positionSequence.js
  3. 4 4
      src/api/user.js
  4. 76 2
      src/pages/profile/edit.vue

+ 369 - 0
src/api/position.js

@@ -0,0 +1,369 @@
+import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
+
+// ============ 模式判断 ============
+function isServerMode() {
+  return getServerConfig().mode === 'server'
+}
+
+function serverPath() {
+  const base = getApiBaseUrl()
+  if (!base) throw new Error('缺少服务器地址,请先在登录页右上角配置')
+  return base
+}
+
+// ============ 枚举 ============
+
+// 岗位状态:0-停用 / 1-启用 / 2-待撤销(与后端一致)
+export const POSITION_STATUS = Object.freeze({
+  DISABLED: 0,
+  ENABLED: 1,
+  PENDING_REVOKE: 2,
+})
+export const POSITION_STATUS_LABEL = {
+  0: '停用',
+  1: '启用',
+  2: '待撤销',
+}
+
+// 员工资质达标状态(与后端 qualificationStatus 枚举一致)
+export const QUALIFICATION_STATUS_LABEL = {
+  UNCONFIGURED: '未配置可匹配证书',
+  NO_EMPLOYEE: '无在岗员工',
+  QUALIFIED: '全部达标',
+  PARTIAL: '部分达标',
+  UNQUALIFIED: '无人达标',
+}
+
+// 排序方式(与后端 orderBy 枚举一致)
+export const ORDER_BY = {
+  ASC: 'ascending',
+  DESC: 'descending',
+}
+
+// 常见的 sortName:默认 createTime
+export const DEFAULT_SORT_NAME = 'createTime'
+
+// ============ Demo 数据 ============
+// demo 模式内存 mock,覆盖 4 类序列 + 多种状态,便于页面演示。
+let demoIdCounter = 100
+const DEMO_POSITIONS = [
+  {
+    id: 1,
+    positionCode: 'P-MGR-001',
+    positionName: '研发总监',
+    positionSequence: '管理',
+    positionLevel: 'M3',
+    positionLevels: ['M1', 'M2', 'M3'],
+    positionType: 'MGMT',
+    positionTypeName: '管理',
+    deptId: 101,
+    deptName: '研发中心',
+    organizationId: 1,
+    organizationName: '中盈智能科技股份有限公司',
+    status: 1,
+    contractTemplateId: 1,
+    establishmentCount: 1,
+    occupancyCount: 1,
+    plannedRecruitmentCount: 0,
+    occupancyRate: 100,
+    salaryMin: 25000,
+    salaryMax: 40000,
+    requiredCertificateCount: 0,
+    qualifiedEmployeeCount: 1,
+    evaluatedEmployeeCount: 1,
+    qualificationRate: 100,
+    qualificationStatus: 'UNCONFIGURED',
+    sourceApplicationId: null,
+    revokeDate: null,
+    createTime: '2026-01-15T09:00:00',
+    createUserId: 1,
+    lastChangeTime: '2026-08-20T14:30:00',
+  },
+  {
+    id: 2,
+    positionCode: 'P-PRO-101',
+    positionName: '高级前端工程师',
+    positionSequence: '专业',
+    positionLevel: 'P3',
+    positionLevels: ['P1', 'P2', 'P3'],
+    positionType: 'PROF',
+    positionTypeName: '专业',
+    deptId: 101,
+    deptName: '研发中心',
+    organizationId: 1,
+    organizationName: '中盈智能科技股份有限公司',
+    status: 1,
+    contractTemplateId: 1,
+    establishmentCount: 5,
+    occupancyCount: 4,
+    plannedRecruitmentCount: 1,
+    occupancyRate: 80,
+    salaryMin: 18000,
+    salaryMax: 30000,
+    requiredCertificateCount: 2,
+    qualifiedEmployeeCount: 3,
+    evaluatedEmployeeCount: 4,
+    qualificationRate: 75,
+    qualificationStatus: 'PARTIAL',
+    sourceApplicationId: null,
+    revokeDate: null,
+    createTime: '2026-02-10T10:00:00',
+    createUserId: 1,
+    lastChangeTime: '2026-09-05T11:15:00',
+  },
+  {
+    id: 3,
+    positionCode: 'P-TEC-201',
+    positionName: '设备运维工程师',
+    positionSequence: '技术',
+    positionLevel: 'T2',
+    positionLevels: ['T1', 'T2'],
+    positionType: 'TECH',
+    positionTypeName: '技术',
+    deptId: 102,
+    deptName: '生产运营部',
+    organizationId: 1,
+    organizationName: '中盈智能科技股份有限公司',
+    status: 1,
+    contractTemplateId: 1,
+    establishmentCount: 8,
+    occupancyCount: 8,
+    plannedRecruitmentCount: 0,
+    occupancyRate: 100,
+    salaryMin: 8000,
+    salaryMax: 14000,
+    requiredCertificateCount: 3,
+    qualifiedEmployeeCount: 8,
+    evaluatedEmployeeCount: 8,
+    qualificationRate: 100,
+    qualificationStatus: 'QUALIFIED',
+    sourceApplicationId: null,
+    revokeDate: null,
+    createTime: '2026-03-01T08:30:00',
+    createUserId: 1,
+    lastChangeTime: '2026-07-12T16:45:00',
+  },
+  {
+    id: 4,
+    positionCode: 'P-OPS-301',
+    positionName: '产线操作员',
+    positionSequence: '操作',
+    positionLevel: 'O1',
+    positionLevels: ['O1', 'O2'],
+    positionType: 'OPS',
+    positionTypeName: '操作',
+    deptId: 103,
+    deptName: '生产车间',
+    organizationId: 1,
+    organizationName: '中盈智能科技股份有限公司',
+    status: 0,
+    contractTemplateId: null,
+    establishmentCount: 20,
+    occupancyCount: 0,
+    plannedRecruitmentCount: 0,
+    occupancyRate: 0,
+    salaryMin: 4000,
+    salaryMax: 6000,
+    requiredCertificateCount: 1,
+    qualifiedEmployeeCount: 0,
+    evaluatedEmployeeCount: 0,
+    qualificationRate: null,
+    qualificationStatus: 'NO_EMPLOYEE',
+    sourceApplicationId: null,
+    revokeDate: null,
+    createTime: '2026-04-05T13:20:00',
+    createUserId: 1,
+    lastChangeTime: '2026-08-30T09:00:00',
+  },
+  {
+    id: 5,
+    positionCode: 'P-PRO-102',
+    positionName: 'HRBP 专员',
+    positionSequence: '专业',
+    positionLevel: 'P2',
+    positionLevels: ['P1', 'P2'],
+    positionType: 'PROF',
+    positionTypeName: '专业',
+    deptId: 104,
+    deptName: '人力资源部',
+    organizationId: 1,
+    organizationName: '中盈智能科技股份有限公司',
+    status: 2,
+    contractTemplateId: 1,
+    establishmentCount: 2,
+    occupancyCount: 2,
+    plannedRecruitmentCount: 0,
+    occupancyRate: 100,
+    salaryMin: 10000,
+    salaryMax: 16000,
+    requiredCertificateCount: 0,
+    qualifiedEmployeeCount: 2,
+    evaluatedEmployeeCount: 2,
+    qualificationRate: null,
+    qualificationStatus: 'UNCONFIGURED',
+    sourceApplicationId: 88,
+    revokeDate: '2026-12-31',
+    createTime: '2026-05-18T10:30:00',
+    createUserId: 1,
+    lastChangeTime: '2026-09-01T14:00:00',
+  },
+]
+
+function matchDemoPosition(query) {
+  const keyword = (query.keyword || '').trim()
+  const sequence = (query.positionSequence || '').trim()
+  const level = (query.positionLevel || '').trim()
+  return DEMO_POSITIONS.filter((p) => {
+    if (query.deptId !== undefined && query.deptId !== null && query.deptId !== '' && Number(p.deptId) !== Number(query.deptId)) {
+      return false
+    }
+    if (query.organizationId !== undefined && query.organizationId !== null && query.organizationId !== '' && Number(p.organizationId) !== Number(query.organizationId)) {
+      return false
+    }
+    if (query.status !== undefined && query.status !== null && query.status !== '' && Number(p.status) !== Number(query.status)) {
+      return false
+    }
+    if (sequence && String(p.positionSequence) !== sequence) return false
+    if (level && !(Array.isArray(p.positionLevels) && p.positionLevels.includes(level)) && p.positionLevel !== level) {
+      return false
+    }
+    if (keyword) {
+      const code = String(p.positionCode || '')
+      const name = String(p.positionName || '')
+      if (!code.includes(keyword) && !name.includes(keyword)) return false
+    }
+    return true
+  })
+}
+
+function paginate(list, pageNum, size) {
+  const safePage = Math.max(1, Number(pageNum) || 1)
+  const safeSize = Math.max(1, Number(size) || 20)
+  const start = (safePage - 1) * safeSize
+  return list.slice(start, start + safeSize)
+}
+
+function sortDemoPositions(list, sortName, orderBy) {
+  if (!sortName) return list
+  const desc = String(orderBy || 'ascending').toLowerCase() === 'descending'
+  const key = String(sortName)
+  const sorted = [...list].sort((a, b) => {
+    const av = a[key]
+    const bv = b[key]
+    if (av === bv) return 0
+    if (av === undefined || av === null) return 1
+    if (bv === undefined || bv === null) return -1
+    if (typeof av === 'number' && typeof bv === 'number') return av - bv
+    return av > bv ? 1 : -1
+  })
+  return desc ? sorted.reverse() : sorted
+}
+
+// ============ 对外 API ============
+
+/**
+ * 岗位分页查询
+ * POST /hr/position/page
+ *
+ * 请求参数(与后端约定):
+ *   - deptId            所属部门ID
+ *   - keyword           岗位编码或岗位名称模糊关键字
+ *   - offset            偏移量(非必填,由后端按 pageNum/size 计算)
+ *   - orderBy           排序方式 ascending | descending
+ *   - organizationId    所属法人ID
+ *   - pageNum           当前页(从 1 开始)
+ *   - positionLevel     岗位序列层级
+ *   - positionSequence  岗位序列
+ *   - size              每页显示数
+ *   - sortName          排序字段(默认 createTime)
+ *   - status            岗位状态:0-停用、1-启用、2-待撤销
+ *
+ * 响应:{ code, data: Pagination, message }
+ *   Pagination.list 每项为 岗位基本信息(含资质达标率、编制使用率等扩展字段)
+ *
+ * @param {object} query 详见 JSDoc
+ * @returns {Promise<{ count: number, list: Array, pageNum: number, size: number, current: number, currentUserId?: number, deptId?: number, extInfo?: object }>}
+ */
+export async function pagePosition(query = {}) {
+  const body = {
+    pageNum: Number(query.pageNum) || 1,
+    size: Number(query.size) || 20,
+    sortName: query.sortName || DEFAULT_SORT_NAME,
+    orderBy: query.orderBy || ORDER_BY.ASC,
+  }
+  if (query.deptId !== undefined && query.deptId !== null && query.deptId !== '') {
+    body.deptId = Number(query.deptId) || 0
+  }
+  if (query.organizationId !== undefined && query.organizationId !== null && query.organizationId !== '') {
+    body.organizationId = Number(query.organizationId) || 0
+  }
+  if (query.keyword !== undefined && query.keyword !== null) {
+    body.keyword = String(query.keyword)
+  }
+  if (query.positionSequence !== undefined && query.positionSequence !== null) {
+    body.positionSequence = String(query.positionSequence)
+  }
+  if (query.positionLevel !== undefined && query.positionLevel !== null) {
+    body.positionLevel = String(query.positionLevel)
+  }
+  if (query.status !== undefined && query.status !== null && query.status !== '') {
+    body.status = Number(query.status)
+  }
+  if (query.offset !== undefined && query.offset !== null) {
+    body.offset = Number(query.offset) || 0
+  }
+
+  if (!isServerMode()) {
+    await new Promise((r) => setTimeout(r, 200))
+    const filtered = matchDemoPosition(body)
+    const sorted = sortDemoPositions(filtered, body.sortName, body.orderBy)
+    const list = paginate(sorted, body.pageNum, body.size)
+    return {
+      count: sorted.length,
+      current: body.pageNum,
+      currentUserId: 0,
+      deptId: body.deptId || 0,
+      extInfo: {},
+      list,
+      pageNum: body.pageNum,
+      size: body.size,
+    }
+  }
+
+  const path = `${serverPath()}/hr/position/page`
+  const res = await request({ url: path, method: 'POST', data: body })
+  return res.data || {
+    count: 0,
+    current: body.pageNum,
+    list: [],
+    pageNum: body.pageNum,
+    size: body.size,
+  }
+}
+
+/**
+ * 把后端 status(0|1|2)翻译成中文标签
+ */
+export function translatePositionStatus(status) {
+  if (status === undefined || status === null || status === '') return ''
+  return POSITION_STATUS_LABEL[status] || String(status)
+}
+
+/**
+ * 把后端 qualificationStatus 翻译成中文标签
+ */
+export function translateQualificationStatus(status) {
+  if (!status) return ''
+  return QUALIFICATION_STATUS_LABEL[status] || String(status)
+}
+
+/**
+ * 计算编制使用率(兜底:编制为 0 时返回 0)。
+ * 后端 occupancyRate 已处理此场景,这里主要给页面侧展示用。
+ */
+export function computeOccupancyRate(occupancyCount, establishmentCount) {
+  const occ = Number(occupancyCount) || 0
+  const est = Number(establishmentCount) || 0
+  if (est <= 0) return 0
+  return Math.round((occ / est) * 10000) / 100
+}

+ 225 - 0
src/api/positionSequence.js

@@ -0,0 +1,225 @@
+import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
+
+// ============ 模式判断 ============
+function isServerMode() {
+  return getServerConfig().mode === 'server'
+}
+
+function serverPath() {
+  const base = getApiBaseUrl()
+  if (!base) throw new Error('缺少服务器地址,请先在登录页右上角配置')
+  return base
+}
+
+// ============ 枚举 ============
+
+// 岗位序列状态:1-启用 / 0-停用(与后端约定)
+export const SEQUENCE_STATUS_LABEL = {
+  1: '启用',
+  0: '停用',
+  '1': '启用',
+  '0': '停用',
+}
+
+// 排序方式(与后端 orderBy 枚举一致)
+export const ORDER_BY = {
+  ASC: 'ascending',
+  DESC: 'descending',
+}
+
+// 常见的 sortName:默认 createTime
+export const DEFAULT_SORT_NAME = 'createTime'
+
+// ============ Demo 数据 ============
+// demo 模式内存 mock,避免页面硬编码常量。
+// 序列覆盖 管理/专业/技术/操作 4 大类,每类 2-3 个层级。
+let demoIdCounter = 1000
+const DEMO_SEQUENCES = [
+  {
+    id: 1,
+    sequence: '管理',
+    status: '1',
+    levelList: [
+      { id: 11, level: 'M1', levelGrade: 1, sequenceId: 1, status: '1' },
+      { id: 12, level: 'M2', levelGrade: 2, sequenceId: 1, status: '1' },
+      { id: 13, level: 'M3', levelGrade: 3, sequenceId: 1, status: '1' },
+    ],
+  },
+  {
+    id: 2,
+    sequence: '专业',
+    status: '1',
+    levelList: [
+      { id: 21, level: 'P1', levelGrade: 1, sequenceId: 2, status: '1' },
+      { id: 22, level: 'P2', levelGrade: 2, sequenceId: 2, status: '1' },
+      { id: 23, level: 'P3', levelGrade: 3, sequenceId: 2, status: '0' },
+    ],
+  },
+  {
+    id: 3,
+    sequence: '技术',
+    status: '1',
+    levelList: [
+      { id: 31, level: 'T1', levelGrade: 1, sequenceId: 3, status: '1' },
+      { id: 32, level: 'T2', levelGrade: 2, sequenceId: 3, status: '1' },
+      { id: 33, level: 'T3', levelGrade: 3, sequenceId: 3, status: '1' },
+      { id: 34, level: 'T4', levelGrade: 4, sequenceId: 3, status: '1' },
+    ],
+  },
+  {
+    id: 4,
+    sequence: '操作',
+    status: '0',
+    levelList: [
+      { id: 41, level: 'O1', levelGrade: 1, sequenceId: 4, status: '0' },
+      { id: 42, level: 'O2', levelGrade: 2, sequenceId: 4, status: '0' },
+    ],
+  },
+]
+
+function matchDemoSequence(query) {
+  const sequence = (query.sequence || '').trim()
+  const status = query.status === undefined || query.status === null ? '' : String(query.status)
+  return DEMO_SEQUENCES.filter((s) => {
+    if (sequence && !String(s.sequence).includes(sequence)) return false
+    if (status && String(s.status) !== status) return false
+    return true
+  })
+}
+
+function paginate(list, pageNum, size) {
+  const safePage = Math.max(1, Number(pageNum) || 1)
+  const safeSize = Math.max(1, Number(size) || 20)
+  const start = (safePage - 1) * safeSize
+  return list.slice(start, start + safeSize)
+}
+
+function sortDemoSequences(list, sortName, orderBy) {
+  if (!sortName) return list
+  const desc = String(orderBy || 'ascending').toLowerCase() === 'descending'
+  const key = String(sortName)
+  const sorted = [...list].sort((a, b) => {
+    const av = a[key]
+    const bv = b[key]
+    if (av === bv) return 0
+    if (av === undefined || av === null) return 1
+    if (bv === undefined || bv === null) return -1
+    return av > bv ? 1 : -1
+  })
+  return desc ? sorted.reverse() : sorted
+}
+
+// ============ 对外 API ============
+
+/**
+ * 分页查询岗位序列
+ * POST /hr/positionSequence/page
+ *
+ * 请求参数(与后端约定):
+ *   - deptId        机构 id(非必填)
+ *   - offset        偏移量(非必填,由后端按 pageNum/size 计算)
+ *   - orderBy       排序方式 ascending | descending
+ *   - pageNum       当前页(从 1 开始)
+ *   - sequence      岗位序列名称(模糊匹配)
+ *   - size          每页显示数
+ *   - sortName      排序字段(默认 createTime)
+ *   - status        状态 1:启用、0:停用
+ *
+ * 响应:{ code, data: Pagination, message }
+ *   Pagination.list 每项为 岗位序列列表:{ id, sequence, status, levelList: [{id, level, levelGrade, sequenceId, status}] }
+ *
+ * @param {object} query 详见 JSDoc
+ * @returns {Promise<{ count: number, list: Array, pageNum: number, size: number, current: number, currentUserId?: number, deptId?: number, extInfo?: object }>}
+ */
+export async function pagePositionSequence(query = {}) {
+  // 归一化入参:status / orderBy / sortName 给后端的默认值
+  const body = {
+    pageNum: Number(query.pageNum) || 1,
+    size: Number(query.size) || 20,
+    sortName: query.sortName || DEFAULT_SORT_NAME,
+    orderBy: query.orderBy || ORDER_BY.ASC,
+  }
+  if (query.deptId !== undefined && query.deptId !== null && query.deptId !== '') {
+    body.deptId = Number(query.deptId) || 0
+  }
+  if (query.sequence !== undefined && query.sequence !== null) {
+    body.sequence = String(query.sequence)
+  }
+  if (query.status !== undefined && query.status !== null && query.status !== '') {
+    body.status = String(query.status)
+  }
+  if (query.offset !== undefined && query.offset !== null) {
+    body.offset = Number(query.offset) || 0
+  }
+
+  if (!isServerMode()) {
+    await new Promise((r) => setTimeout(r, 200))
+    const filtered = matchDemoSequence(body)
+    const sorted = sortDemoSequences(filtered, body.sortName, body.orderBy)
+    const list = paginate(sorted, body.pageNum, body.size)
+    return {
+      count: sorted.length,
+      current: body.pageNum,
+      currentUserId: 0,
+      deptId: body.deptId || 0,
+      extInfo: {},
+      list,
+      pageNum: body.pageNum,
+      size: body.size,
+    }
+  }
+
+  const path = `${serverPath()}/hr/positionSequence/page`
+  const res = await request({ url: path, method: 'POST', data: body })
+  return res.data || {
+    count: 0,
+    current: body.pageNum,
+    list: [],
+    pageNum: body.pageNum,
+    size: body.size,
+  }
+}
+
+/**
+ * 查询岗位序列列表(仅启用)
+ * GET /hr/positionSequence/getPositionSequenceList
+ *
+ * 接口描述:返回启用的岗位序列及其启用层级集合;层级包含文本、数值级别和主键,
+ * 供岗位管理展示或级联选择。
+ *
+ * 响应:{ code, data: Array<岗位序列列表>, message }
+ *
+ * @returns {Promise<Array<{ id: number, sequence: string, status: string, levelList: Array<{id, level, levelGrade, sequenceId, status}> }>>}
+ */
+export async function getPositionSequenceList() {
+  if (!isServerMode()) {
+    await new Promise((r) => setTimeout(r, 150))
+    return DEMO_SEQUENCES
+      .filter((s) => String(s.status) === '1')
+      .map((s) => ({
+        ...s,
+        levelList: (s.levelList || []).filter((lv) => String(lv.status) === '1'),
+      }))
+  }
+
+  const path = `${serverPath()}/hr/positionSequence/getPositionSequenceList`
+  const res = await request({ url: path, method: 'GET' })
+  return Array.isArray(res.data) ? res.data : []
+}
+
+/**
+ * 把后端 status('0' | '1' | 0 | 1)翻译成中文标签
+ */
+export function translateSequenceStatus(status) {
+  if (status === undefined || status === null || status === '') return ''
+  return SEQUENCE_STATUS_LABEL[status] || String(status)
+}
+
+/**
+ * 把后端层级数组按 levelGrade 升序排序(数值越大层级越高 → 排序时大的在后,
+ * 调用方若要"高层在前"自行 reverse)。
+ */
+export function sortLevelsByGrade(levels) {
+  if (!Array.isArray(levels)) return []
+  return [...levels].sort((a, b) => (a.levelGrade || 0) - (b.levelGrade || 0))
+}

+ 4 - 4
src/api/user.js

@@ -73,9 +73,6 @@ export async function saveEmployee(payload = {}) {
   // 用 delete 把 raw 里的 bankCardList / 顶层兼容 cardNumber / openingBank 删掉,
   // 避免修改其它字段时把旧的银行卡一并带上去(后端会按"非空整单替换"覆盖)
   const body = { ...raw };
-  delete body.bankCardList;
-  delete body.cardNumber;
-  delete body.openingBank;
 
   if ("phone" in payload) body.phone = payload.phone;
   if ("maritalStatus" in payload) body.maritalStatus = payload.maritalStatus;
@@ -85,7 +82,10 @@ export async function saveEmployee(payload = {}) {
   if ("secondLinkRelation" in payload)
     body.secondLinkRelation = payload.secondLinkRelation;
   if (Array.isArray(payload.bankCardList)) {
-    body.bankCardList = mapBankCardList(payload.bankCardList, accountName);
+    body.bankCardList = [];
+  }
+  if(Array.isArray(payload.positionList) && payload.positionList.length > 0) {
+    body.positionList = payload.positionList;
   }
 
   if (!isServerMode()) {

+ 76 - 2
src/pages/profile/edit.vue

@@ -45,13 +45,77 @@
   </view>
 </template>
 <script setup>
-import { computed, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
 import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { useCurrentUser } from "@/hooks/useCurrentUser";
 import { refetchMyProfile, saveEmployee } from "@/api/user";
+import { getPositionSequenceList } from '@/api/positionSequence'
+import { pagePosition } from '@/api/position'
 const currentUser = useCurrentUser();
 useAuthGuard();
 const saving = ref(false);
+
+const positionList = ref([]);
+// 初始化状态:硬约束——只有 ready=true 才允许 commitChange 提交
+// - ready=false 且 error='':仍在加载中(用户操作会被拦截并 toast 提示)
+// - ready=false 且 error 非空:初始化失败,需要用户重试或离开页面
+const positionListReady = ref(false);
+const positionListError = ref('');
+// ============ 初始化:把当前用户的 postId 解析成 saveEmployee 所需的 positionList ============
+// 后端约定:postId 是 "1,2,3" 形式的逗号分隔串;positionLevelId / positionSequenceId
+// 要从岗位序列接口查表得到,按 level 名称匹配(避免按数组 index 取值导致的错位)。
+async function loadPositionList() {
+  positionListReady.value = false;
+  positionListError.value = '';
+  try {
+    const [{ list = [] }, sequences] = await Promise.all([
+      pagePosition({ pageNum: 1, size: 1000 }),
+      getPositionSequenceList(),
+    ]);
+
+    // postId 兜底:undefined / null / "" 都按空数组处理
+    const rawPostId = currentUser && currentUser.raw && currentUser.raw.postId;
+    const postIdList = String(rawPostId || '')
+      .split(',')
+      .map((s) => s.trim())
+      .filter(Boolean);
+
+    // level → levelVo 索引;同名 level 重复时后者覆盖前者(demo 数据里 level 唯一)
+    const levelByName = new Map();
+    for (const seq of sequences || []) {
+      for (const lv of seq.levelList || []) {
+        if (!lv || lv.level == null) continue;
+        levelByName.set(String(lv.level), lv);
+      }
+    }
+
+    // positionId → positionVo 索引
+    const positionsById = new Map(
+      (list || []).map((it) => [String(it.id), it]),
+    );
+
+    positionList.value = postIdList
+      .map((id, idx) => {
+        const pos = positionsById.get(String(id));
+        const lv = pos ? levelByName.get(String(pos.positionLevel || '')) : null;
+        if (!pos || !lv) return null;
+        return {
+          positionId: pos.id,
+          positionLevelId: lv.id,
+          positionSequenceId: lv.sequenceId,
+          primaryFlag: idx === 0 ? 1 : 0,
+        };
+      })
+      .filter(Boolean);
+    positionListReady.value = true;
+  } catch (error) {
+    positionListReady.value = false;
+    positionListError.value = error?.message || String(error || '岗位信息加载失败');
+    console.warn('[profile/edit] 初始化 positionList 失败:', error?.message || error);
+  }
+}
+onMounted(loadPositionList);
+
 // 银行卡:本地可编辑数组,先克隆接口数据,避免直接改 reactive 引用
 const bankCardList = ref(
   (currentUser.bankCardList || []).map((c) => ({ ...c })),
@@ -214,10 +278,20 @@ function applyOptimistic(payload) {
 // 统一提交入口:手机号 / 婚姻 / 紧急联系人 / 银行卡(增删改)都走这里
 async function commitChange(payload, { toast = "已保存" } = {}) {
   if (saving.value) return
+  // 硬约束:positionList 未就绪不允许提交。区分两种状态给出可读的提示,
+  // 避免把空 / 残缺的 positionList 写回后端,也避免用户在加载中误以为卡死。
+  if (!positionListReady.value) {
+    return uni.showToast({
+      title: positionListError.value
+        ? '岗位信息加载失败,请重试'
+        : '岗位信息加载中,请稍候',
+      icon: 'none',
+    })
+  }
   saving.value = true
   uni.showLoading({ title: "保存中..." })
   try {
-    payload = { ...payload, id: payload.userId }
+    payload = { ...payload, id: payload.userId, positionList: positionList.value }
     await saveEmployee(payload)
     // 先用本地 payload 做乐观合并(saveNew 通常只返回 {id},不会回完整 VO)
     applyOptimistic(payload)