Przeglądaj źródła

feat(position): 添加岗位申请状态及相关逻辑,优化岗位选择功能

xieyong 4 dni temu
rodzic
commit
60bc762552

+ 229 - 16
src/api/position.js

@@ -1,4 +1,5 @@
 import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
+import { positionApplications } from '@/data/mock'
 
 // ============ 模式判断 ============
 function isServerMode() {
@@ -34,6 +35,20 @@ export const QUALIFICATION_STATUS_LABEL = {
   UNQUALIFIED: '无人达标',
 }
 
+// 岗位申请状态(与后端一致):草稿 / 审批中 / 已通过 / 已驳回
+export const POSITION_APPLICATION_STATUS = Object.freeze({
+  DRAFT: 'DRAFT',
+  PENDING: 'PENDING',
+  APPROVED: 'APPROVED',
+  REJECTED: 'REJECTED',
+})
+export const POSITION_APPLICATION_STATUS_LABEL = {
+  DRAFT: '草稿',
+  PENDING: '审批中',
+  APPROVED: '已通过',
+  REJECTED: '已驳回',
+}
+
 // 排序方式(与后端 orderBy 枚举一致)
 export const ORDER_BY = {
   ASC: 'ascending',
@@ -56,9 +71,9 @@ const DEMO_POSITIONS = [
     positionLevels: ['M1', 'M2', 'M3'],
     positionType: 'MGMT',
     positionTypeName: '管理',
-    deptId: 101,
-    deptName: '研发中心',
-    organizationId: 1,
+    deptId: 104,
+    deptName: '产品研发部',
+    organizationId: 10,
     organizationName: '中盈智能科技股份有限公司',
     status: 1,
     contractTemplateId: 1,
@@ -88,9 +103,9 @@ const DEMO_POSITIONS = [
     positionLevels: ['P1', 'P2', 'P3'],
     positionType: 'PROF',
     positionTypeName: '专业',
-    deptId: 101,
-    deptName: '研发中心',
-    organizationId: 1,
+    deptId: 201,
+    deptName: '前端组',
+    organizationId: 10,
     organizationName: '中盈智能科技股份有限公司',
     status: 1,
     contractTemplateId: 1,
@@ -120,9 +135,9 @@ const DEMO_POSITIONS = [
     positionLevels: ['T1', 'T2'],
     positionType: 'TECH',
     positionTypeName: '技术',
-    deptId: 102,
-    deptName: '生产运营部',
-    organizationId: 1,
+    deptId: 206,
+    deptName: '设备管理部',
+    organizationId: 10,
     organizationName: '中盈智能科技股份有限公司',
     status: 1,
     contractTemplateId: 1,
@@ -152,9 +167,9 @@ const DEMO_POSITIONS = [
     positionLevels: ['O1', 'O2'],
     positionType: 'OPS',
     positionTypeName: '操作',
-    deptId: 103,
-    deptName: '生产车间',
-    organizationId: 1,
+    deptId: 204,
+    deptName: '生产一部',
+    organizationId: 10,
     organizationName: '中盈智能科技股份有限公司',
     status: 0,
     contractTemplateId: null,
@@ -184,9 +199,9 @@ const DEMO_POSITIONS = [
     positionLevels: ['P1', 'P2'],
     positionType: 'PROF',
     positionTypeName: '专业',
-    deptId: 104,
+    deptId: 102,
     deptName: '人力资源部',
-    organizationId: 1,
+    organizationId: 10,
     organizationName: '中盈智能科技股份有限公司',
     status: 2,
     contractTemplateId: 1,
@@ -207,6 +222,38 @@ const DEMO_POSITIONS = [
     createUserId: 1,
     lastChangeTime: '2026-09-01T14:00:00',
   },
+  {
+    id: 6,
+    positionCode: 'P-PRO-103',
+    positionName: '人力资源专员',
+    positionSequence: '专业',
+    positionLevel: 'P1',
+    positionLevels: ['P1', 'P2'],
+    positionType: 'PROF',
+    positionTypeName: '专业',
+    deptId: 102,
+    deptName: '人力资源部',
+    organizationId: 10,
+    organizationName: '中盈智能科技股份有限公司',
+    status: 1,
+    contractTemplateId: 1,
+    establishmentCount: 3,
+    occupancyCount: 1,
+    plannedRecruitmentCount: 2,
+    occupancyRate: 33.33,
+    salaryMin: 7000,
+    salaryMax: 11000,
+    requiredCertificateCount: 0,
+    qualifiedEmployeeCount: 1,
+    evaluatedEmployeeCount: 1,
+    qualificationRate: 100,
+    qualificationStatus: 'UNCONFIGURED',
+    sourceApplicationId: 9003,
+    revokeDate: null,
+    createTime: '2026-06-02T09:10:00',
+    createUserId: 1,
+    lastChangeTime: '2026-09-06T10:20:00',
+  },
 ]
 
 function matchDemoPosition(query) {
@@ -292,10 +339,10 @@ export async function pagePosition(query = {}) {
     orderBy: query.orderBy || ORDER_BY.ASC,
   }
   if (query.deptId !== undefined && query.deptId !== null && query.deptId !== '') {
-    body.deptId = Number(query.deptId) || 0
+    body.deptId = String(query.deptId || 0)
   }
   if (query.organizationId !== undefined && query.organizationId !== null && query.organizationId !== '') {
-    body.organizationId = Number(query.organizationId) || 0
+    body.organizationId = String(query.organizationId) || 0
   }
   if (query.keyword !== undefined && query.keyword !== null) {
     body.keyword = String(query.keyword)
@@ -341,6 +388,172 @@ export async function pagePosition(query = {}) {
   }
 }
 
+/** 只保留正整数主键,避免把空值/非法值当条件发给后端 */
+function positiveId(value) {
+  const num = Number(value)
+  return Number.isFinite(num) && num > 0 ? num : undefined
+}
+
+/** 岗位申请列表项 → 页面形状(只保留选岗需要的字段) */
+export function adaptPositionApplication(raw = {}) {
+  return {
+    id: raw.id ?? raw.applicationId ?? '',
+    positionName: raw.positionName || '',
+    deptId: raw.deptId ?? '',
+    deptName: raw.deptName || '',
+    organizationId: raw.organizationId ?? '',
+    status: String(raw.status || '').toUpperCase(),
+    effectivePositionId: raw.effectivePositionId ?? '',
+    positionSequence: raw.positionSequence || '',
+    positionLevel: raw.positionLevel || '',
+  }
+}
+
+/**
+ * 岗位申请分页查询
+ * POST /hr/position/apply/page
+ *
+ * 请求参数(与后端约定,仅传非空项):
+ *   - deptId          所属部门主键
+ *   - organizationId  所属机构主键
+ *   - keyword         岗位编码 / 岗位名称 / 申请人姓名
+ *   - status          申请状态:DRAFT | PENDING | APPROVED | REJECTED
+ */
+export async function pagePositionApplication(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,
+  }
+  const deptId = positiveId(query.deptId)
+  if (deptId) body.deptId = String(deptId)
+  const organizationId = positiveId(query.organizationId)
+  if (organizationId) body.organizationId = organizationId
+  if (query.status) body.status = String(query.status).toUpperCase()
+  if (query.keyword !== undefined && query.keyword !== null && query.keyword !== '') {
+    body.keyword = String(query.keyword)
+  }
+
+  if (!isServerMode()) {
+    await new Promise((r) => setTimeout(r, 160))
+    const rows = positionApplications
+      .filter((item) => !body.deptId || Number(item.deptId) === Number(body.deptId))
+      .filter((item) => !body.organizationId || Number(item.organizationId) === Number(body.organizationId))
+      .filter((item) => !body.status || String(item.status).toUpperCase() === body.status)
+      .map(adaptPositionApplication)
+    return {
+      count: rows.length,
+      list: paginate(rows, body.pageNum, body.size),
+      pageNum: body.pageNum,
+      size: body.size,
+    }
+  }
+
+  const res = await request({
+    url: `${serverPath()}/hr/position/apply/page`,
+    method: 'POST',
+    data: body,
+  })
+  return res.data || { count: 0, list: [], pageNum: body.pageNum, size: body.size }
+}
+
+/** 岗位基本信息 → 选岗选项(页面只依赖这几个字段) */
+function positionOption(raw = {}) {
+  return {
+    id: String(raw.id ?? raw.positionId ?? ''),
+    name: raw.positionName || raw.name || '',
+    code: raw.positionCode || '',
+    deptId: raw.deptId ?? '',
+    deptName: raw.deptName || '',
+    level: raw.positionLevel || '',
+    levels: Array.isArray(raw.positionLevels) ? raw.positionLevels : [],
+    sequence: raw.positionSequence || '',
+  }
+}
+
+/** 演示模式下,该部门「已通过岗位申请」沉淀出的正式岗位主键集合 */
+function demoApprovedPositionIds(deptId) {
+  return new Set(
+    positionApplications
+      .filter((item) => String(item.status).toUpperCase() === POSITION_APPLICATION_STATUS.APPROVED)
+      .filter((item) => Number(item.deptId) === Number(deptId))
+      .map((item) => String(item.effectivePositionId ?? ''))
+      .filter(Boolean),
+  )
+}
+
+/**
+ * 入职可选岗位:口径与 PC 端「员工档案」选岗一致。
+ *   1) 未选部门时返回空列表(不支持全局选岗);
+ *   2) 只取该部门下启用状态(status=1)的岗位;
+ *   3) 优先只保留「该部门已通过岗位申请」沉淀出的正式岗位;
+ *      该部门没有已通过申请时,回落到部门全部启用岗位,并把 restricted 置为 false;
+ *   4) 岗位分页拿不到数据但存在已通过申请时,用申请里的岗位信息兜底。
+ *
+ * @returns {Promise<{list: Array, restricted: boolean}>} restricted=true 表示当前是严格口径
+ */
+export async function loadSelectablePositions({ deptId, keyword = '' } = {}) {
+  const deptKey = positiveId(deptId)
+  const text = String(keyword || '').trim().toLowerCase()
+  if (!deptKey) return { list: [], restricted: false }
+  const sameDept = (item) =>
+    item.deptId == null || item.deptId === '' || Number(item.deptId) === deptKey
+  const byKeyword = (rows) =>
+    text ? rows.filter((row) => `${row.code || ''}${row.name || ''}`.toLowerCase().includes(text)) : rows
+
+  if (!isServerMode()) {
+    await new Promise((r) => setTimeout(r, 200))
+    const deptPositions = matchDemoPosition({
+      deptId: deptKey,
+      status: POSITION_STATUS.ENABLED,
+    }).map(positionOption)
+    const approvedIds = demoApprovedPositionIds(deptKey)
+    const approved = deptPositions.filter((item) => approvedIds.has(item.id))
+    const list = approved.length ? approved : deptPositions
+    return { list: byKeyword(list), restricted: approved.length > 0 }
+  }
+
+  const [page, applications] = await Promise.all([
+    pagePosition({ pageNum: 1, size: 200, status: POSITION_STATUS.ENABLED, deptId: deptKey }).catch(
+      () => ({ list: [] }),
+    ),
+    pagePositionApplication({
+      pageNum: 1,
+      size: 200,
+      status: POSITION_APPLICATION_STATUS.APPROVED,
+      deptId: deptKey,
+    }).catch(() => ({ list: [] })),
+  ])
+  const positions = (page?.list || []).filter(sameDept).map(positionOption)
+  const apps = (applications?.list || []).filter(sameDept)
+  const approvedIds = new Set(
+    apps.map((item) => String(item.effectivePositionId || '')).filter(Boolean),
+  )
+  const approvedNames = new Set(
+    apps.map((item) => String(item.positionName || '').trim()).filter(Boolean),
+  )
+  const approved = positions.filter(
+    (item) => approvedIds.has(String(item.id)) || (item.name && approvedNames.has(item.name)),
+  )
+  let list = approved.length ? approved : positions
+  if (!list.length && apps.length) {
+    list = apps
+      .filter((item) => item.positionName)
+      .map((item) =>
+        positionOption({
+          id: item.effectivePositionId || item.id,
+          positionName: item.positionName,
+          deptId: item.deptId,
+          deptName: item.deptName,
+          positionSequence: item.positionSequence,
+          positionLevel: item.positionLevel,
+        }),
+      )
+  }
+  return { list: byKeyword(list), restricted: approved.length > 0 }
+}
+
 /**
  * 把后端 status(0|1|2)翻译成中文标签
  */

+ 10 - 0
src/data/mock.js

@@ -127,6 +127,16 @@ export const onboardingDepartments = [
   { id: 302, name: '装配班组',       type: 'dept', parentId: 205, sort: 1 },
 ]
 
+// 岗位申请(对应 POST /hr/position/apply/page)
+// 演示模式用它复刻后端「岗位按部门收敛」的口径:只有审批通过(APPROVED)的申请沉淀出的正式岗位,
+// 才允许用于入职邀约;effectivePositionId 指向正式岗位主键(见 api/position.js 的演示岗位)。
+export const positionApplications = [
+  { id: 9001, positionName: '研发总监',       deptId: 104, deptName: '产品研发部', organizationId: 10, status: 'APPROVED', effectivePositionId: 1, positionSequence: '管理', positionLevel: 'M3' },
+  { id: 9002, positionName: '高级前端工程师', deptId: 201, deptName: '前端组',     organizationId: 10, status: 'APPROVED', effectivePositionId: 2, positionSequence: '专业', positionLevel: 'P3' },
+  { id: 9003, positionName: '人力资源专员',   deptId: 102, deptName: '人力资源部', organizationId: 10, status: 'APPROVED', effectivePositionId: 6, positionSequence: '专业', positionLevel: 'P1' },
+  { id: 9004, positionName: '设备运维工程师', deptId: 206, deptName: '设备管理部', organizationId: 10, status: 'PENDING' },
+]
+
 export const employmentTypes = [
   { value: 'FULL_TIME', label: '全职' },
   { value: 'INTERN',    label: '实习' },

+ 56 - 28
src/pages/manage/onboard-create.vue

@@ -67,6 +67,7 @@
           form.positionName || '请选择'
           }}</text><text class="arrow">›</text>
       </view>
+      <view class="field-note"><text class="muted">{{ positionTip }}</text></view>
       <view class="divider"></view>
       <view class="field press" @click="chooseEmployment"><text class="label">用工类型</text><text class="value">{{
           employmentLabel }}</text><text class="arrow">›</text></view>
@@ -154,7 +155,7 @@ import {
 } from '@/utils/storage';
 import { createOnboardingManual, createOnboardingInvitation } from '@/api/onboarding';
 import { listOrganizations } from '@/api/organization';
-import { pagePosition } from '@/api/position';
+import { loadSelectablePositions } from '@/api/position';
 import {
   employmentTypes,
   probationMonthOptions,
@@ -206,9 +207,10 @@ const deptColumns = computed(() => {
   return [l1, l2, l3];
 });
 
-const positionKeyword = ref('');
 const positionResults = ref([]);
 const positionLoading = ref(false);
+// 岗位是否收敛到「该部门已通过岗位申请」的严格口径
+const positionRestricted = ref(false);
 const submitting = ref(false);
 const inviteResult = ref(null);
 const showInviteCard = ref(false);
@@ -231,6 +233,15 @@ const employmentLabel = computed(() => {
   return hit ? hit.label : form.employmentType;
 });
 
+// 岗位栏下的口径提示,让用户知道为什么只看到少数岗位
+const positionTip = computed(() => {
+  if (!form.deptId) return '请先选择目标部门,岗位按部门收敛';
+  if (positionLoading.value) return '正在加载该部门岗位…';
+  if (positionRestricted.value) return '仅显示该部门已通过岗位申请沉淀的岗位';
+  if (positionResults.value.length) return '该部门暂无已通过的岗位申请,已展示部门全部启用岗位';
+  return '该部门暂无可选岗位,请更换目标部门';
+});
+
 async function refreshCompanies() {
   try {
     // 一次拉全量(与 PC 端 onboardingHandling 一致),客户端拆分法人 + 部门树
@@ -272,6 +283,7 @@ function chooseLegal() {
       form.deptId = null;
       form.deptPath = '';
       deptIndex.value = [0, 0, 0];
+      clearPosition();
       refreshDeptTree(c.id);
     },
   });
@@ -311,6 +323,9 @@ function onDeptPick(e) {
     .map((n) => n.name)
     .filter(Boolean)
     .join(' / ');
+  // 部门变了,岗位跟着收敛,已选岗位作废
+  clearPosition();
+  searchPositions();
 }
 
 function chooseEmployment() {
@@ -334,46 +349,54 @@ function chooseValidDays() {
   });
 }
 
-let positionDebounce = null;
-async function searchPositions(keyword) {
+/** 岗位随目标部门收敛(与 PC 员工档案口径一致),切部门时清空已选岗位 */
+async function searchPositions() {
+  if (!form.deptId) {
+    positionResults.value = [];
+    positionRestricted.value = false;
+    return;
+  }
   positionLoading.value = true;
   try {
-    const data = await pagePosition({
-      pageNum: 1,
-      size: 50,
-      keyword: keyword || undefined,
-      positionName: keyword || undefined,
-    });
-    positionResults.value = (data?.list || []).map((p) => ({
-      id: p.id,
-      name: p.positionName,
-      deptName: p.deptName,
-    }));
+    const result = await loadSelectablePositions({ deptId: form.deptId });
+    positionResults.value = result.list || [];
+    positionRestricted.value = Boolean(result.restricted);
+  } catch (error) {
+    positionResults.value = [];
+    positionRestricted.value = false;
   } finally {
     positionLoading.value = false;
   }
 }
 
-function onPositionInput(e) {
-  positionKeyword.value = e.detail.value;
-  clearTimeout(positionDebounce);
-  positionDebounce = setTimeout(() => searchPositions(positionKeyword.value), 300);
+function clearPosition() {
+  form.positionId = null;
+  form.positionName = '';
+  positionResults.value = [];
+  positionRestricted.value = false;
 }
 
-function choosePosition() {
-  // 进入页面已经预拉过;这里只在第一次打开且空结果时补一次兜底
+async function choosePosition() {
+  if (!form.deptId) {
+    uni.showToast({ title: '请先选择目标部门', icon: 'none' });
+    return;
+  }
+  if (positionLoading.value) return;
   if (!positionResults.value.length) {
-    searchPositions(positionKeyword.value);
-    uni.showToast({ title: '暂无岗位数据', icon: 'none' });
+    await searchPositions();
+  }
+  if (!positionResults.value.length) {
+    uni.showToast({ title: '该部门暂无可用岗位', icon: 'none' });
     return;
   }
   uni.showActionSheet({
-    itemList: positionResults.value.map((p) => p.name),
+    // 同部门内用层级区分同名岗位,便于确认
+    itemList: positionResults.value.map((p) => (p.level ? `${p.name}(${p.level})` : p.name)),
     success: (r) => {
       const p = positionResults.value[r.tapIndex];
+      if (!p) return;
       form.positionId = p.id;
       form.positionName = p.name;
-      positionKeyword.value = p.name;
     },
   });
 }
@@ -553,7 +576,7 @@ function onCancel() {
 onLoad(() => {
   uni.setNavigationBarTitle({ title: '新建入职流程' });
   refreshCompanies();
-  searchPositions(''); // 进入即拉岗位列表,避免点开岗位栏才加载的等待感
+  clearPosition(); // 未选部门前不拉岗位(岗位按部门收敛)
   clearOnboardingDraft();
 });
 
@@ -584,8 +607,7 @@ function resetForm() {
     probationMonths: 3,
     remark: '',
   });
-  positionKeyword.value = '';
-  positionResults.value = [];
+  clearPosition();
   inviteResult.value = null;
   showInviteCard.value = false;
   orgList.value = [];
@@ -722,6 +744,12 @@ onUnload(() => {
   color: #b5bac3;
 }
 
+.field-note {
+  padding: 4rpx 0 14rpx;
+  font-size: 22rpx;
+  line-height: 1.6;
+}
+
 .value-input {
   text-align: right;
   height: 96rpx;

+ 51 - 15
src/pages/manage/transfer-create.vue

@@ -42,6 +42,7 @@
         <text :class="form.targetPositionName ? 'value' : 'placeholder'">{{ form.targetPositionName || '请选择' }}</text>
         <text class="arrow">›</text>
       </view>
+      <view class="field-note"><text class="muted">{{ positionTip }}</text></view>
       <template v-if="form.targetLevel">
         <view class="divider"></view>
         <view class="field"><text class="label">目标职级</text><text class="value">{{ form.targetLevel }}</text></view>
@@ -88,7 +89,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard'
 import { useCurrentUser } from '@/hooks/useCurrentUser'
 import { applicationTypes } from '@/data/mock'
 import { listOrganizations } from '@/api/organization'
-import { pagePosition } from '@/api/position'
+import { loadSelectablePositions } from '@/api/position'
 import { buildOrganizationTree } from '@/utils/organization-tree'
 import { addApplication } from '@/utils/storage'
 import { getServerConfig } from '@/utils/auth'
@@ -131,6 +132,8 @@ const submitting = ref(false)
 const deptOptions = ref([])
 const positionOptions = ref([])
 const positionLoading = ref(false)
+// 岗位是否收敛到「该部门已通过岗位申请」的严格口径
+const positionRestricted = ref(false)
 
 const changeTypeText = computed(() => (form.changeType ? changeTypeLabel(form.changeType) : '请选择'))
 
@@ -148,6 +151,15 @@ const footerTip = computed(() =>
     : '演示模式:申请保存在本机,提交后可在「调岗申请」与「我的申请」中查看进度。',
 )
 
+// 目标岗位按目标部门收敛,这里说明当前口径
+const positionTip = computed(() => {
+  if (!form.targetDeptName) return '请先选择目标部门,岗位按部门收敛'
+  if (positionLoading.value) return '正在加载该部门岗位…'
+  if (positionRestricted.value) return '仅显示该部门已通过岗位申请沉淀的岗位'
+  if (positionOptions.value.length) return '该部门暂无已通过的岗位申请,已展示部门全部启用岗位'
+  return '该部门暂无可选岗位,请更换目标部门'
+})
+
 // 法人节点只作为路径前缀,不作为可选的调动目标
 const LEGAL_TYPES = new Set(['10', '20', 'group', 'company'])
 
@@ -174,23 +186,33 @@ async function loadDepartments() {
 }
 
 async function loadPositions() {
+  if (!form.targetDeptId) {
+    positionOptions.value = []
+    positionRestricted.value = false
+    return
+  }
   positionLoading.value = true
   try {
-    const data = await pagePosition({ pageNum: 1, size: 50 })
-    positionOptions.value = (data?.list || []).map((p) => ({
-      id: p.id,
-      name: p.positionName,
-      deptId: p.deptId,
-      deptName: p.deptName,
-      level: p.positionLevel || '',
-    }))
+    const result = await loadSelectablePositions({ deptId: form.targetDeptId })
+    positionOptions.value = result.list || []
+    positionRestricted.value = Boolean(result.restricted)
   } catch (error) {
     positionOptions.value = []
+    positionRestricted.value = false
   } finally {
     positionLoading.value = false
   }
 }
 
+/** 目标部门变化:已选岗位作废并重新按部门拉岗位 */
+function clearTargetPosition() {
+  form.targetPositionId = null
+  form.targetPositionName = ''
+  form.targetLevel = ''
+  positionOptions.value = []
+  positionRestricted.value = false
+}
+
 function syncOrigin() {
   const user = currentUser || {}
   form.userId = form.userId || user.id || null
@@ -224,21 +246,29 @@ function chooseTargetDept() {
       if (!dept) return
       form.targetDeptId = dept.id
       form.targetDeptName = dept.name
+      // 部门变了,岗位跟着收敛,已选岗位作废
+      clearTargetPosition()
+      loadPositions()
     },
   })
 }
 
 async function chooseTargetPosition() {
   if (positionLoading.value) return
+  if (!form.targetDeptId) {
+    uni.showToast({ title: '请先选择目标部门', icon: 'none' })
+    return
+  }
   if (!positionOptions.value.length) {
     await loadPositions()
   }
   if (!positionOptions.value.length) {
-    uni.showToast({ title: '暂无岗位数据', icon: 'none' })
+    uni.showToast({ title: '该部门暂无可选岗位', icon: 'none' })
     return
   }
   uni.showActionSheet({
-    itemList: positionOptions.value.map((item) => (item.deptName ? `${item.name}(${item.deptName})` : item.name)),
+    // 同部门内用层级区分同名岗位
+    itemList: positionOptions.value.map((item) => (item.level ? `${item.name}(${item.level})` : item.name)),
     success: (r) => {
       const position = positionOptions.value[r.tapIndex]
       if (!position) return
@@ -246,10 +276,10 @@ async function chooseTargetPosition() {
       form.targetPositionName = position.name
       // 目标岗位自带岗位层级时同步展示;与当前职级一致则不产生 GRADE 变动
       form.targetLevel = position.level || ''
-      // 岗位归属部门即调动目标部门,避免部门与岗位不一致
-      if (position.deptId != null && position.deptName) {
+      // 岗位已按目标部门收敛,这里只做一次兜底对齐,避免部门与岗位不一致
+      if (position.deptId != null && position.deptId !== '' && String(position.deptId) !== String(form.targetDeptId)) {
         form.targetDeptId = position.deptId
-        form.targetDeptName = position.deptName
+        form.targetDeptName = position.deptName || form.targetDeptName
       }
     },
   })
@@ -349,6 +379,8 @@ async function hydrateForm(id) {
       reason: row.reason,
       remark: row.remark,
     })
+    // 回填目标部门后按部门拉岗位,避免编辑草稿时岗位列表为空
+    loadPositions()
   } catch (e) {
     uni.hideLoading()
     uni.showToast({ title: e.message || '加载失败', icon: 'none' })
@@ -361,7 +393,6 @@ async function hydrateForm(id) {
 onLoad((options) => {
   uni.setNavigationBarTitle({ title: '调岗申请' })
   loadDepartments()
-  loadPositions()
   const id = options?.id || ''
   if (id) {
     hydrateForm(id)
@@ -441,6 +472,11 @@ onLoad((options) => {
   display: flex;
   align-items: center;
 }
+.field-note {
+  padding: 0 28rpx 20rpx;
+  font-size: 22rpx;
+  line-height: 1.6;
+}
 .label {
   width: 180rpx;
   flex-shrink: 0;