ソースを参照

调整机构部门口径与考勤表单,iOS 更新限制

lihaiting 4 日 前
親
コミット
0a2a185a59

+ 40 - 6
api/hr/attendanceApply.js

@@ -42,26 +42,41 @@ export const ATTENDANCE_APPROVAL_BUSINESS_KEYS = {
 
 export const ATTENDANCE_APPLY_TYPE_KEYS = Object.keys(ATTENDANCE_APPLY_TYPE_MAP);
 
-// 表单 leaveType 中文标签 → 接口 leaveType 枚举
+// 请假类型中文标签 → 接口 leaveType 枚举
+// 注:请假类型候选已改由 GET /hr/ess/attendance/leave-rules 提供,
+// 此表仅用于兼容历史草稿/历史单据里的中文标签。
 export const LEAVE_TYPE_MAP = {
   年假: "ANNUAL",
   事假: "PERSONAL",
   病假: "SICK",
   调休假: "COMPENSATORY",
+  调休: "COMPENSATORY",
   婚假: "MARRIAGE",
   产假: "MATERNITY",
   陪产假: "PATERNITY",
   丧假: "BEREAVEMENT",
-  工伤假: "WORK_INJURY",
+  工伤假: "INJURY",
   其他: "OTHER",
 };
 
-// 接口 leaveType 枚举 → 中文标签(详情回填用,与 LEAVE_TYPE_MAP 互为逆映射)
+// 接口 leaveType 枚举 → 中文标签(详情回显用,与 LEAVE_TYPE_MAP 互为逆映射)
 export const LEAVE_TYPE_LABEL = Object.keys(LEAVE_TYPE_MAP).reduce((acc, key) => {
   acc[LEAVE_TYPE_MAP[key]] = key;
   return acc;
 }, {});
 
+/**
+ * 请假类型归一化为接口编码:
+ *   接口/详情回传的 ANNUAL、SICK 等编码原样转大写;
+ *   历史草稿/历史单据里的中文标签(如「调休假」)按 LEAVE_TYPE_MAP 转一次。
+ */
+export function normalizeLeaveTypeCode(value) {
+  const raw = String(value == null ? "" : value).trim();
+  if (!raw) return "";
+  if (LEAVE_TYPE_MAP[raw]) return LEAVE_TYPE_MAP[raw];
+  return raw.toUpperCase();
+}
+
 // 补卡类型枚举(对应后端 repairType 字段),仅 CARD_REPAIR 申请使用
 export const REPAIR_TYPE_LABEL = {
   MISSING_IN: "上班漏打卡",
@@ -156,8 +171,8 @@ export function hydrateAttendanceForm(source = {}, type = "leave") {
     endTime: end.time || DEFAULT_END_TIME,
   });
   if (type === "leave") {
-    const code = String(src.leaveType || "").toUpperCase();
-    form.leaveType = code ? LEAVE_TYPE_LABEL[code] || "" : "";
+    // form.leaveType 统一存接口编码(ANNUAL/SICK…),老数据的「年假」等中文标签转一次
+    form.leaveType = normalizeLeaveTypeCode(src.leaveType);
   }
   return form;
 }
@@ -186,7 +201,7 @@ export function buildAttendancePayload(form, type, userRaw, attachmentFileIds) {
   switch (type) {
     case "leave":
       return Object.assign({}, base, {
-        leaveType: LEAVE_TYPE_MAP[form.leaveType] || "OTHER",
+        leaveType: normalizeLeaveTypeCode(form.leaveType) || "OTHER",
         startTime: buildRangeTime(form.startDate, form.startTime || DEFAULT_START_TIME),
         endTime: buildRangeTime(form.endDate, form.endTime || DEFAULT_END_TIME),
         durationMinutes: computeDurationHours(
@@ -298,6 +313,25 @@ export async function getAttendanceApplicationDetail(id, loding = false) {
   return Promise.reject(new Error(res.message || "查询考勤申请详情失败"));
 }
 
+/**
+ * 请假类型列表
+ * GET /hr/ess/attendance/leave-rules
+ * 返回结构尚未确认(先用原样 data 暴露给调用方,页面侧打印联调日志)
+ */
+export async function queryLeaveRules(loding = false) {
+  const res = await withTimeout(
+    get(Vue.prototype.apiUrl + `/hr/ess/attendance/leave-rules`, {}, loding),
+    REQUEST_TIMEOUT,
+    "请假类型查询超时(12s 无响应)"
+  );
+  if (!(res && res.code == 0)) {
+    throw new Error(
+      (res && res.message) || `请假类型查询失败(code=${res ? res.code : "无响应"})`
+    );
+  }
+  return res.data;
+}
+
 /**
  * 修改被驳回的考勤申请
  * PUT /hr/attendance/applications/{id}

+ 5 - 8
manifest.json

@@ -2,22 +2,20 @@
     "name" : "AiMill工业互联网平台",
     "appid" : "__UNI__45B3907",
     "description" : "",
-    "versionName" : "V1.0.4.51",
-    "versionCode" : "100",
+    "versionName" : "1.0.1",
+    "versionCode" : 104,
     "transformPx" : false,
     "h5" : {
         "devServer" : {
             "proxy" : {
                 "/api" : {
-                    // "target" : "http://192.168.1.110:18086/",
+                    "target" : "http://192.168.1.113:18086/",
                     // "target" : "http://123.249.79.125/api/",
                     // "target" : "http://114.116.248.196:86/api/",
                     // "target": "http://116.63.185.248:80/api",
                     // "target" : "http://192.168.1.251:18086/",sss
                     // "target" : "http://192.168.1.251:18086/",
-                    "target" : "http://192.168.1.147:18086/",
-                    // "target" : "http://192.168.1.251:18086/",
-                    // "target" : "http://192.168.1.102:18086/",
+                    // "target" : "http://192.168.1.10:18086/",
                     // "target" : "http://aiot.zoomwin.com.cn:51001/api",
                     "changeOrigin" : true,
                     "secure" : false,
@@ -45,7 +43,6 @@
         "modules" : {
             "Camera" : {},
             "Barcode" : {},
-            "Push" : {},
             "Geolocation" : {
                 "system" : {
                     "__platform__" : [ "ios", "android" ]
@@ -81,6 +78,7 @@
             /* ios打包配置 */
             "ios" : {
                 "dSYMs" : false,
+                "idfa" : false,
                 "privacyDescription" : {
                     "NSLocationWhenInUseUsageDescription" : "用于考勤打卡时获取您的当前位置",
                     "NSCameraUsageDescription" : "用于拍照上传附件、扫码",
@@ -89,7 +87,6 @@
             },
             /* SDK配置 */
             "sdkConfigs" : {
-                "ad" : {},
                 "push" : {
                     "unipush" : {}
                 },

+ 10 - 3
pages/doc/index.vue

@@ -7,9 +7,11 @@
 			</u-badge>
 		</view>
 
-		<u-button :plain="true" :hairline="true" v-if="type != 'view'" size='mini' text="上传"
-			@click="handleUpload"></u-button>
-		<u-button :plain="true" :hairline="true" v-else size='mini' text="查看" @click="handleUpload"></u-button>
+		<view class="upload-row">
+			<u-button :plain="true" :hairline="true" v-if="type != 'view'" size='mini' text="上传"
+				@click="handleUpload"></u-button>
+			<u-button :plain="true" :hairline="true" v-else size='mini' text="查看" @click="handleUpload"></u-button>
+		</view>
 		<docList ref="docListRef" @getFiles="getFiles"></docList>
 	</div>
 </template>
@@ -80,6 +82,11 @@
 	.doc {
 		position: relative;
 
+		.upload-row {
+			display: flex;
+			justify-content: flex-end;
+		}
+
 		.box {
 			position: absolute;
 			right: -20rpx;

+ 57 - 4
pages/hr/apply/form.vue

@@ -247,7 +247,7 @@
           label="请假类型"
           required
           :value="form.leaveType"
-          :display="form.leaveType"
+          :display="leaveTypeLabel"
           :options="leaveTypeOptions"
           :disabled="isReadonly"
           @change="onLeaveTypeChange"
@@ -355,8 +355,10 @@ import {
   buildAttendancePayload,
   computeDurationMinutes,
   hydrateAttendanceForm,
+  LEAVE_TYPE_LABEL,
   loadAttendanceApplicationForEdit,
   pickApplicationId,
+  queryLeaveRules,
   startAttendanceApproval,
   submitAttendanceApplication,
   updateAttendanceApplication,
@@ -389,7 +391,7 @@ const REDIRECT_ROUTES = {
 // 转发时透传的参数
 const CARRIED_KEYS = ["mode", "id", "businessId", "businessType", "processInstanceId", "admin"];
 
-const LEAVE_TYPE_ITEMS = ["年假", "事假", "病假", "调休假", "婚假"];
+// 请假类型不再前端写死,改由 GET /hr/ess/attendance/leave-rules 提供
 
 // 时长字段各类型的标签文案(请假 / 加班 / 出差 / 外勤,补卡与离职无时长)
 const DURATION_LABELS = {
@@ -468,6 +470,9 @@ export default {
       approvalError: "",
       sourceLoaded: false,
       profileLoaded: false,
+      // 请假类型(GET /hr/ess/attendance/leave-rules):结构待确认,先原样打印
+      leaveRules: [],
+      leaveRulesLoaded: false,
     };
   },
   computed: {
@@ -522,9 +527,17 @@ export default {
     applicantNo() {
       return this.form.employeeNo || this.currentUser.employeeNo || "";
     },
-    /** 请假类型候选(统一选择器用) */
+    /** 请假类型候选(来源 GET /hr/ess/attendance/leave-rules:label=policyName,value=leaveType 编码) */
     leaveTypeOptions() {
-      return LEAVE_TYPE_ITEMS.map((item) => ({ label: item, value: item }));
+      return this.leaveRules.map((item) => ({ label: item.name, value: item.code }));
+    },
+    /** 请假类型显示名:优先接口规则里的 policyName,其次内置枚举标签,最后原样显示编码 */
+    leaveTypeLabel() {
+      const code = String(this.form.leaveType || "").trim();
+      if (!code) return "";
+      const hit = this.leaveRules.find((item) => item.code === code);
+      if (hit) return hit.name;
+      return LEAVE_TYPE_LABEL[code] || code;
     },
     /** 补卡类型候选 */
     repairTypeOptions() {
@@ -599,6 +612,8 @@ export default {
     }
 
     this.type = applicationTypes[incomingType] ? incomingType : "leave";
+    // 请假类型候选来自接口:新建与详情(回显名称)都需要
+    if (this.type === "leave") this.loadLeaveRules();
     // 本页不直接承接 mode=edit(编辑由详情页内「编辑」按钮进入)
     if (this.appMode === APPLICATION_MODE.EDIT) this.appMode = APPLICATION_MODE.DETAIL;
     this.loadCurrentUser();
@@ -661,12 +676,50 @@ export default {
       if (!this.form.userId) this.form.userId = this.currentUser.id || "";
       if (!this.form.name) this.form.name = name;
       if (!this.form.employeeNo) this.form.employeeNo = this.currentUser.employeeNo || "";
+      // 登录缓存不含部门时,调档案接口补真实部门(对齐 transfer-create.loadProfile 的取数方式);
+      // 档案也没返回时申请人行保持「—」,如实显示未取到,不做兜底拼值
+      if (!this.currentUser.department && this.currentUser.id) {
+        this.loadCurrentUserDepartment();
+      }
+    },
+    /**
+     * 请假类型列表:GET /hr/ess/attendance/leave-rules
+     * 返回体 data.leaveRules[]:policyName=中文名,leaveType=接口编码(ANNUAL/SICK…)。
+     */
+    async loadLeaveRules() {
+      if (this.leaveRulesLoaded) return;
+      this.leaveRulesLoaded = true;
+      try {
+        const data = await queryLeaveRules(false);
+        const list = data && Array.isArray(data.leaveRules) ? data.leaveRules : [];
+        this.leaveRules = list
+          .filter((item) => item && item.leaveType && item.policyName)
+          .map((item) => ({
+            id: String(item.policyId == null ? "" : item.policyId),
+            code: String(item.leaveType),
+            name: String(item.policyName),
+          }));
+        console.log("[请假类型] leave-rules 返回:", JSON.stringify(data));
+      } catch (error) {
+        this.leaveRules = [];
+        console.log("[请假类型] leave-rules 失败:", (error && error.message) || error);
+      }
+    },
+    async loadCurrentUserDepartment() {
+      try {
+        const raw = await fetchUserProfile(this.currentUser.id, false);
+        const p = buildProfileForm(raw || {});
+        if (p.deptName) this.currentUser.department = p.deptName;
+      } catch (error) {
+        // 档案拉取失败:保持「—」,提示接口未返回,不阻塞申请主流程
+      }
     },
     syncType(options) {
       const next = (options && options.type) || "leave";
       if (this.type !== next) this.resetForm();
       this.type = next;
       uni.setNavigationBarTitle({ title: this.navTitle });
+      if (next === "leave") this.loadLeaveRules();
       const draft = getDraft(next);
       if (draft) Object.assign(this.form, draft);
       // 草稿兼容:老草稿缺时间字段时兜底默认值,避免校验误报

+ 4 - 4
pages/hr/manage/onboard-create.vue

@@ -147,7 +147,7 @@ import {
   getDeptPositionLevels,
 } from "@/api/hr/position";
 import { buildDeptTreeUnderLegal } from "@/utils/hr/organization-tree";
-import { getLoginGroupOptions } from "@/utils/hr/current-org";
+import { getOrgOptionsWithFallback } from "@/utils/hr/current-org";
 import fileMain from "@/pages/doc/index.vue";
 import HrPickerRow from "@/components/hr/HrPickerRow.vue";
 
@@ -228,11 +228,11 @@ export default {
     backToWorkbench() {
       switchMain("/pages/home/home");
     },
-    /** 机构选项:只列当前账号所属公司,默认选中第一条(与 PC「默认取缓存第一条」约定一致) */
+    /** 机构选项:取登录缓存 orgVO(所属机构),默认选中它 */
     refreshCompanies() {
-      this.companies = getLoginGroupOptions();
+      this.companies = getOrgOptionsWithFallback(this.orgList, this.form.legalId, this.form.legalName);
       if (!this.companies.length) {
-        uni.showToast({ title: "当前账号未配置所属公司,请联系管理员", icon: "none" });
+        uni.showToast({ title: "登录信息缺少所属机构(orgVO),请联系管理员", icon: "none" });
         return;
       }
       const first = this.companies[0];

+ 10 - 1
pages/hr/manage/transfer-create.vue

@@ -72,6 +72,11 @@
 				<text class="value">{{ profile.level || '—' }}</text>
 			</view>
 			<view class="divider"></view>
+			<view class="field">
+				<text class="label">岗位性质</text>
+				<text class="value">{{ profile.positionTypeName || '—' }}</text>
+			</view>
+			<view class="divider"></view>
 			<view class="field">
 				<text class="label required">手机号</text>
 				<input
@@ -154,7 +159,7 @@
 			<view class="divider"></view>
 			<view class="field">
 				<text class="label">工作地址</text>
-				<input class="field-input" v-model="form.workLocation" :disabled="formDisabled" maxlength="60" placeholder="调动后工作地点" />
+				<input class="field-input" v-model="form.workLocation" :disabled="formDisabled" maxlength="60" placeholder="请输入" />
 			</view>
 			<view class="divider"></view>
 			<view class="field press">
@@ -274,6 +279,7 @@ function todayIso() {
 					dept: "",
 					position: "",
 					level: "",
+					positionTypeName: "",
 					contractTerm: "",
 					contractPeriod: "",
 					phone: "",
@@ -505,6 +511,9 @@ function todayIso() {
 					const levelName =
 						(primary && (primary.positionLevelName || primary.positionLevel)) || "";
 					if (levelName) this.profile.level = levelName;
+					// 岗位性质:主岗位快照的 positionTypeName(原合同信息展示用,非提交字段)
+					const typeName = (primary && primary.positionTypeName) || "";
+					if (typeName) this.profile.positionTypeName = typeName;
 				} catch (error) {
 					// 档案聚合接口不可用时保留 getById 的结果
 				}

+ 18 - 13
pages/login/login.vue

@@ -188,9 +188,13 @@
 				return 'h5_platform'; // H5场景直接返回
 				// #endif
 
-				// #ifdef APP-PLUS
-				try {
-					const versionInfo = await getLatestVersion()
+				// #			// #ifdef APP-PLUS
+			try {
+				// iOS 禁止应用内自更新(App Store 审核条款 2.5.2),版本检查仅安卓执行
+				if (uni.getSystemInfoSync().platform !== 'android') {
+					return 'latest_version'
+				}
+				const versionInfo = await getLatestVersion()
 					const { appVersion } = uni.getAppBaseInfo()
 
 					// 已是最新版本
@@ -353,18 +357,19 @@
 
 					// 保存用户信息
 					const data = res.data
-					// 与 PC setCurrentUser 对齐:登录后把「当前机构 / 当前角色」落到缓存里。
-					// 接口未回传时取 loginChangeGroupVOList 第一条(用户所属多个机构默认第一个),
-					// 供 utils/hr/current-org.js 的 getCurrentOrgId() 兜底使用。
-					const firstGroup = Array.isArray(data.loginChangeGroupVOList) ? data.loginChangeGroupVOList[0] : null
-					const firstRole = firstGroup && Array.isArray(firstGroup.loginChangeRoleVOList)
-						? firstGroup.loginChangeRoleVOList[0]
+					// 与 PC setCurrentUser 对齐:登录后把「当前部门 / 当前角色」落到缓存里。
+					// loginChangeGroupVOList 是部门列表(groupType=30),isMainGroup == 1 才是主所属部门;
+					// 所属机构另见 orgVO,不要从这里取。
+					const groupList = Array.isArray(data.loginChangeGroupVOList) ? data.loginChangeGroupVOList : []
+					const mainGroup = groupList.find((item) => Number(item.isMainGroup) === 1) || null
+					const mainRole = mainGroup && Array.isArray(mainGroup.loginChangeRoleVOList)
+						? mainGroup.loginChangeRoleVOList[0]
 						: null
-					if (data.currentGroupId == null && firstGroup && firstGroup.groupId != null) {
-						data.currentGroupId = firstGroup.groupId
+					if (data.currentGroupId == null && mainGroup && mainGroup.groupId != null) {
+						data.currentGroupId = mainGroup.groupId
 					}
-					if (data.currentRoleId == null && firstRole && firstRole.roleId != null) {
-						data.currentRoleId = firstRole.roleId
+					if (data.currentRoleId == null && mainRole && mainRole.roleId != null) {
+						data.currentRoleId = mainRole.roleId
 					}
 					uni.setStorageSync('token', data.token)
 					uni.setStorageSync('userInfo', data)

+ 10 - 2
pages/systemSettings/index.vue

@@ -193,8 +193,16 @@
 				// 	}
 				// })
 
-				// #ifdef APP-PLUS
-				getLatestVersion().then(res => {
+			// #ifdef APP-PLUS
+			// iOS 禁止应用内自更新(App Store 审核条款 2.5.2),更新仅安卓执行
+			if (uni.getSystemInfoSync().platform !== 'android') {
+				uni.showToast({
+					title: 'iOS 端请通过 App Store 更新',
+					icon: 'none'
+				})
+				return
+			}
+			getLatestVersion().then(res => {
 					console.log('res:', res)
 					const {
 						appVersion

+ 24 - 1
readme.md

@@ -2,4 +2,27 @@
 证书别名 aimillApp
 证书私钥密码 aimill666
 包名 com.zhongying.aimill
-证书文件路径 aiot-app/aimill.keystore
+证书文件路径 aiot-app/aimill.keystore
+
+
+ios配置
+[隐私政策](http://aiot.zoomwin.com.cn:51001/AiMillApp-privacy-policy.html)
+版权信息:2024 湖南中赢智能科技有限公司
+应用名称:AiMill工业互联网平台
+
+应用介绍:
+AI Mill 是一款面向制造企业的一站式移动数字化工作台,让工厂管理装进口袋。
+生产制造方面,覆盖生产工单、派工管理、PDA 扫码报工、巡检点检与产品追溯,车间动态实时掌握;设备管理方面,提供设备台账、维修报修、保养计划、模具与备件管理,助力设备高效运转;品质与合规方面,内置 QMS 质量管理与 EHS 安全环保模块,问题闭环更省心;供应链方面,支持销售、采购、出入库、库存盘点与发票确认协同,单据流转一部手机搞定;能源管理(EMS)实时监测能耗,为降本增效提供数据支撑。
+同时内置人事考勤能力:GPS 定位打卡、排班考勤、请假加班等自助申请与企业审批流,员工办事不跑腿,管理者审批不过夜。
+数据实时同步、流程移动审批、扫码快捷作业,AI Mill 让工厂更智能,让管理更简单。
+
+一句话简介:口袋里的智慧工厂
+
+新版本特性:
+AI Mill工业互联网平台移动端首次发布,主要特性:
+生产管理:工单处理、派工管理、PDA 扫码报工、巡检点检;
+设备管理:设备台账、维修报修、保养计划、模具与备件管理,MTTR/MTBF 数据可视化;
+办公协同:消息通知、企业文档、流程审批随时随地处理;
+人事考勤:GPS 定位打卡、排班考勤,请假、加班、补卡、出差等自助申请,审批进度实时可查;
+支持扫一扫单据识别。
+优化企业移动办公体验,欢迎反馈使用建议。

+ 47 - 64
utils/hr/current-org.js

@@ -1,94 +1,77 @@
 /**
- * 当前登录用户所属机构(与 PC 端 src/utils/current-org.js 同一口径):
- * 「所属机构」下拉选项一律取登录缓存 loginChangeGroupVOList,
- * 不再调 /main/group/getGroupList 全量接口——普通账号不该看到其它公司。
+ * 当前登录用户所属机构 / 部门(2026-11-03 按用户口述校准):
+ *   - 「所属机构」唯一来源:登录缓存 userInfo.orgVO,形如 { id, name, code, type };
+ *   - 「所属部门」来源:loginChangeGroupVOList(groupType=30 的部门列表),
+ *     其中 isMainGroup == 1 的那条为主所属部门。
  *
- * 移动端登录后把 /main/user/login 的返回整体存在 storage 的 userInfo 里,
- * 与 PC vuex 里 user.info 同源,故直接读 loginChangeGroupVOList。
+ * 历史口径错误:早期把 loginChangeGroupVOList 当成「所属机构」候选,
+ * 于是「研发部」被当成所属机构显示在调岗申请 / 入职登记 / 档案编辑三处。
  */
 import { pickLegalEntities } from "@/utils/hr/organization-tree";
 
-/** 是否有效机构 id(0 视为无效) */
-function isValidId(value) {
-  return value !== undefined && value !== null && value !== "" && Number(value) !== 0;
-}
-
-/** 登录缓存中的可选机构列表 → [{ id, name }](即当前账号所属公司) */
-export function getLoginGroupOptions() {
+/** 登录缓存的业务字段层(兼容 { raw: {...} } 包装的历史写法) */
+function cacheSource() {
   const info = uni.getStorageSync("userInfo") || {};
-  const raw = info.raw || {};
-  const list = info.loginChangeGroupVOList || raw.loginChangeGroupVOList || [];
-  return (Array.isArray(list) ? list : [])
-    .filter((item) => item && isValidId(item.groupId))
-    .map((item) => ({
-      id: item.groupId,
-      name: item.groupName || item.name || item.organName || item.orgName || "",
-    }));
+  return info.raw && typeof info.raw === "object" ? info.raw : info;
 }
 
 /**
- * 当前登录用户所属机构 id:
- * 优先取登录缓存机构列表第一条(用户所属多个机构时默认第一个),
- * 缓存缺失时回落 userInfo.currentGroupId(登录/切换机构时写入)。
+ * 所属机构:登录缓存 orgVO → { id, name }。
+ * 接口未返回 orgVO 时返回 null,由调用方如实提示,不拿部门或组织树顶替。
  */
+export function getCurrentOrg() {
+  const org = cacheSource().orgVO;
+  if (!org || org.id == null || org.id === "") return null;
+  return { id: String(org.id), name: org.name || "" };
+}
+
+/** 所属机构 id(无 orgVO 时 null) */
 export function getCurrentOrgId() {
-  const options = getLoginGroupOptions();
-  if (options.length) return options[0].id;
-  const info = uni.getStorageSync("userInfo") || {};
-  const raw = info.raw || {};
-  const current = info.currentGroupId != null ? info.currentGroupId : raw.currentGroupId;
-  return isValidId(current) ? current : null;
+  const org = getCurrentOrg();
+  return org ? org.id : null;
 }
 
-/** 当前登录用户所属机构 { id, name } */
-export function getCurrentOrg() {
-  const id = getCurrentOrgId();
-  const matched = getLoginGroupOptions().find((item) => String(item.id) === String(id));
-  return matched || { id, name: "" };
+/**
+ * 登录缓存里的部门列表 loginChangeGroupVOList → [{ id, name, isMainGroup }]。
+ * 注意:这是「部门」列表(groupType=30),不是机构列表;isMainGroup==1 是主所属部门。
+ */
+export function getLoginGroupOptions() {
+  const list = cacheSource().loginChangeGroupVOList;
+  return (Array.isArray(list) ? list : [])
+    .filter((item) => item && item.groupId != null && item.groupId !== "")
+    .map((item) => ({
+      id: String(item.groupId),
+      name: item.groupName || "",
+      isMainGroup: Number(item.isMainGroup) === 1,
+    }));
+}
+
+/** 主所属部门(isMainGroup == 1);缓存里没有该标识时返回 null,不猜第一条 */
+export function getMainDept() {
+  return getLoginGroupOptions().find((item) => item.isMainGroup) || null;
 }
 
 /**
- * 「所属机构」下拉选项(口径对齐 PC EmployeeArchiveForm.legalEntities):
- *   ① 主源:登录缓存 loginChangeGroupVOList(当前账号所属公司),**不再用全量机构树**;
- *   ② 缓存里只有 id 没有名称时(老缓存/接口缺字段),才回落到组织树里的集团/公司节点,
- *      保证下拉可读可选,而不是显示成一串 id;
- *   ③ 当前值不在候选中时(编辑老档案,原机构已不在账号所属范围)补一条兜底项,
- *      避免下拉只显示 ID、且不会因此改掉已保存的值。
- * @param {Array} orgList 组织树全量列表(仅用于 ②③ 兜底,可传空)
+ * 「所属机构」下拉选项:唯一主源是登录缓存 orgVO(普通账号只属于自己的公司)。
+ * orgVO 缺名称时按 id 从组织树补名(只补名称,不扩大候选范围);
+ * currentId 不在候选里时(看他人档案 / 历史单据)补一条,保证原值能显示、能保存。
+ * @param {Array} orgList 组织树全量列表(仅用于补名称,可传空)
  * @param {string|number} currentId 当前编辑中的机构 id
- * @param {string} currentName 当前编辑中的机构名称(组织树里也找不到时兜底显示)
+ * @param {string} currentName 当前编辑中的机构名称
  * @returns {Array<{id:string, name:string}>}
  */
 export function getOrgOptionsWithFallback(orgList = [], currentId = "", currentName = "") {
   const tree = Array.isArray(orgList) ? orgList : [];
-  const companiesFromTree = () =>
-    pickLegalEntities(tree).map((item) => ({ id: String(item.id), name: item.name || "" }));
   const nameFromTree = (id) => {
-    const hit = companiesFromTree().find((item) => String(item.id) === String(id));
-    return hit ? hit.name : "";
+    const hit = pickLegalEntities(tree).find((item) => String(item.id) === String(id));
+    return hit ? hit.name || "" : "";
   };
 
-  let list;
-  const cached = getLoginGroupOptions();
-  if (cached.length) {
-    // 机构范围锁在登录缓存里;只是名称缺失(老缓存/接口少字段)时按 id 从组织树补名,
-    // 不因为“没名称”就把候选扩大到全部公司(这点比 PC 更严一点)
-    list = cached.map((item) => ({
-      id: String(item.id),
-      name: item.name || nameFromTree(item.id),
-    }));
-  } else {
-    // 完全没有登录缓存(半登录态/老缓存)才回落组织树,否则下拉会是空的
-    list = companiesFromTree();
-    console.log(
-      "[current-org] 登录缓存无机构列表,回落组织树:",
-      list.map((item) => item.id).join(",") || "-"
-    );
-  }
+  const org = getCurrentOrg();
+  let list = org ? [{ id: org.id, name: org.name || nameFromTree(org.id) }] : [];
 
   const id = currentId == null || currentId === "" ? "" : String(currentId);
   if (id && !list.some((item) => String(item.id) === id)) {
-    // 编辑老档案:原机构已不在账号所属范围,补一条兜底项保证能显示、能保存原值
     list = [{ id, name: nameFromTree(id) || currentName || "" }].concat(list);
   }
   return list;

+ 5 - 1
utils/hr/position-cascade.js

@@ -380,7 +380,11 @@ export const positionCascadeMixin = {
       try {
         const data = await getDeptPositionType({ deptId, positionId, positionSequenceId }, false);
         const first = cascadeList(data)[0] || null;
-        const rawType = String((first && first.positionType) || "");
+        // 岗位性质取值顺序:positionType/list 首条 → 表单已有值(离职/编辑态由档案
+        // 预填 employmentType)。positionType/list 未返回时不能丢:compactQuery 会把
+        // 空串从 query 里删掉,positionLevel/list 请求就完全没有 positionType 字段了。
+        // 手工重选岗位时 clearCascadeBelow 已清掉 typeId,不会拿旧值顶替。
+        const rawType = String((first && first.positionType) || form[f.typeId] || "");
         // 名称:接口 positionTypeName 优先,取不到时用岗位性质字典按编码反查(对齐 PC),
         // 不能拿编码当名称兜底,否则只读框会显示成 "1"
         let typeName = String((first && first.positionTypeName) || "").trim();