user.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  1. import Vue from "vue";
  2. import { get, postJ, putJ, deleteApi } from "@/utils/request";
  3. import { formatDate } from "@/utils/hr/format";
  4. /* =========================================================================
  5. * 字典 / 选项(与 PC 端 kd-aiot-frontend-hr 员工档案表单口径一致)
  6. * ========================================================================= */
  7. /**
  8. * 员工状态:1 正式在职 / 6 试用期 / 7 待岗 / 8 已离职
  9. * 口径来源:hr_user_status 字典实测(与 PC kd-aiot-frontend-hr 的
  10. * utils/employee-status-dict.js 一致;swagger 注释「1全职…6离职」与实际数据不符)。
  11. * 运行时以字典为准,这里只作字典拉取失败时的兜底,避免静默串码。
  12. */
  13. export const EMPLOYEE_STATUS_OPTIONS = [
  14. { value: 1, label: "正式在职" },
  15. { value: 6, label: "试用期" },
  16. { value: 7, label: "待岗" },
  17. { value: 8, label: "已离职" },
  18. ];
  19. export const EMPLOYEE_STATUS_LABEL = EMPLOYEE_STATUS_OPTIONS.reduce((acc, item) => {
  20. acc[item.value] = item.label;
  21. return acc;
  22. }, {});
  23. export const SEX_CODE = { 男: 1, 女: 2, 未知: 0 };
  24. export const SEX_OPTIONS = ["男", "女", "未知"];
  25. export function sexLabel(value) {
  26. const map = { 1: "男", 2: "女", 0: "未知" };
  27. return map[value] != null ? map[value] : value == null ? "" : String(value);
  28. }
  29. export const MARITAL_OPTIONS = ["未婚", "已婚", "离异", "丧偶"];
  30. export const EDUCATION_OPTIONS = ["小学", "初中", "高中", "中专", "大专", "本科", "硕士", "博士"];
  31. export const DEGREE_OPTIONS = ["无", "学士", "硕士", "博士"];
  32. export const ETHNIC_OPTIONS = ["汉族", "土家族", "苗族", "侗族", "回族", "瑶族", "壮族", "满族", "其他"];
  33. export const SOCIAL_SECURITY_OPTIONS = ["四险", "四险一金", "五险", "五险一金", "六险一金"];
  34. export const STOP_REASON_OPTIONS = ["离职", "调转", "其他"];
  35. /* 业务记录枚举:后端原始码 ↔ 中文(与 PC 端 userChange/regularization/resignation/salarySlip 映射一致) */
  36. export const CHANGE_TYPE_LABELS = {
  37. DEPARTMENT_TRANSFER: "部门调动",
  38. POSITION_TRANSFER: "岗位调动",
  39. GRADE_CHANGE: "职级变更",
  40. PROMOTION: "晋升",
  41. DEMOTION: "降职",
  42. SALARY_CHANGE: "薪资变更",
  43. MANAGER_CHANGE: "上级变更",
  44. WORK_LOCATION_CHANGE: "工作地点变更",
  45. FACTORY_TRANSFER: "工厂调动",
  46. LEGAL_ENTITY_TRANSFER: "法人实体转移",
  47. EMPLOYMENT_TYPE_CHANGE: "雇佣类型变更",
  48. COMPOSITE_CHANGE: "复合变更",
  49. };
  50. export const APPROVAL_STATUS_LABELS = {
  51. DRAFT: "草稿",
  52. PENDING: "审批中",
  53. APPROVING: "审批中",
  54. APPROVED: "已通过",
  55. REJECTED: "已驳回",
  56. WITHDRAWN: "已撤回",
  57. CANCELLED: "已取消",
  58. };
  59. export const EFFECTIVE_STATUS_LABELS = {
  60. PENDING: "待生效",
  61. PENDING_EFFECTIVE: "待生效",
  62. SCHEDULED: "已排期",
  63. EFFECTIVE: "已生效",
  64. INVALID: "已失效",
  65. FAILED: "生效失败",
  66. CANCELLED: "已取消",
  67. };
  68. export const REGULARIZATION_TYPE_LABELS = {
  69. NORMAL: "按期转正",
  70. EARLY: "提前转正",
  71. EXTENDED: "延期转正",
  72. };
  73. export const REGULARIZATION_RESULT_LABELS = {
  74. NORMAL: "按期转正",
  75. EARLY: "提前转正",
  76. EXTENDED: "延期",
  77. FAILED: "不通过",
  78. };
  79. export const RESIGNATION_TYPE_LABELS = {
  80. VOLUNTARY: "主动离职",
  81. NEGOTIATED: "协商解除",
  82. PROBATION_FAILED: "试用不合格",
  83. PERFORMANCE_FAILED: "绩效不合格",
  84. DISMISSED: "辞退",
  85. CONTRACT_EXPIRED: "合同到期",
  86. RETIREMENT: "退休",
  87. OTHER: "其他",
  88. };
  89. export const PAYSLIP_STATUS_LABELS = {
  90. PUBLISHED: "已发布",
  91. VIEWED: "已查看",
  92. CONFIRMED: "已确认",
  93. APPEALED: "已申诉",
  94. CORRECTED: "已更正",
  95. CLOSED: "已关闭",
  96. };
  97. /** 合同状态 / 签署状态枚举(对齐 PC src/api/hr/contractStatus.js,避免详情里显示英文码) */
  98. export const CONTRACT_STATUS_LABELS = {
  99. DRAFT: "草稿",
  100. PENDING_APPROVAL: "审批中",
  101. PENDING_SIGN: "待签署",
  102. SIGNED_WAIT_EFFECTIVE: "已签待生效",
  103. EFFECTIVE: "生效",
  104. EXPIRING: "临期",
  105. EXPIRED: "已到期",
  106. RENEWED: "已续签",
  107. CHANGED: "已变更",
  108. TERMINATED: "已解除",
  109. ENDED: "已终止",
  110. CANCELLED: "已作废",
  111. SIGNING: "签署中",
  112. PENDING_EFFECTIVE: "已签待生效",
  113. VOIDED: "已作废",
  114. ARCHIVED: "已归档",
  115. };
  116. export const CONTRACT_SIGNING_STATUS_LABELS = {
  117. NOT_STARTED: "未签署",
  118. COMPLETED: "已签署",
  119. REJECTED: "已拒签",
  120. EXPIRED: "已过期",
  121. CANCELLED: "已取消",
  122. UNSIGNED: "未签署",
  123. PENDING: "待签署",
  124. PENDING_SIGN: "待签署",
  125. SIGNING: "签署中",
  126. SIGNED: "已签署",
  127. };
  128. /** 由出生日期算年龄(接口不返回 age 时兜底,对齐 PC 的 calculateAge) */
  129. export function calcAgeByBirthday(birthday) {
  130. const text = String(birthday == null ? "" : birthday).replace(/-/g, "/").slice(0, 10);
  131. if (!text) return "";
  132. const date = new Date(text);
  133. if (isNaN(date.getTime())) return "";
  134. const now = new Date();
  135. let age = now.getFullYear() - date.getFullYear();
  136. const monthDiff = now.getMonth() - date.getMonth();
  137. if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < date.getDate())) age -= 1;
  138. return age >= 0 && age < 130 ? String(age) : "";
  139. }
  140. /** 证照类型:名称 ↔ 后端编码 */
  141. export const CERT_TYPE_CODE = {
  142. 身份证: 1,
  143. 驾驶证: 2,
  144. 军官证: 3,
  145. 护照: 4,
  146. 特种作业证: 5,
  147. 职称证: 6,
  148. 职业资格证: 7,
  149. 特种设备操作证: 8,
  150. 其他: 9,
  151. };
  152. export const CERT_TYPE_OPTIONS = Object.keys(CERT_TYPE_CODE);
  153. const CERT_LEVEL_CODE = { 初级: "1", 中级: "2", 高级: "3" };
  154. const CERT_LEVEL_LABEL = { 1: "初级", 2: "中级", 3: "高级", 初级: "初级", 中级: "中级", 高级: "高级" };
  155. /** 岗位津贴单位:字典码 ↔ 展示名 */
  156. export const ALLOWANCE_UNIT_OPTIONS = [
  157. { value: "month", label: "元/月" },
  158. { value: "quarter", label: "元/季度" },
  159. { value: "year", label: "元/年" },
  160. ];
  161. const ALLOWANCE_UNIT_LABEL = ALLOWANCE_UNIT_OPTIONS.reduce((acc, item) => {
  162. acc[item.value] = item.label;
  163. return acc;
  164. }, {});
  165. export function allowanceUnitLabel(value) {
  166. if (value == null || value === "") return "";
  167. return ALLOWANCE_UNIT_LABEL[String(value)] || String(value);
  168. }
  169. /** 业务记录枚举翻译:命中映射取中文,否则原样返回(空值返回空串) */
  170. export function enumLabel(map, code) {
  171. if (code == null || code === "") return "";
  172. const key = String(code).trim();
  173. return map[key] || map[key.toUpperCase()] || key;
  174. }
  175. /* =========================================================================
  176. * 字典归一(/system/dict/getByCode/{code} 原始返回结构有多种)
  177. * ========================================================================= */
  178. /** 字典返回体 → 数组(兼容 list / records / rows / data 包装与纯数组) */
  179. function dictList(raw) {
  180. if (Array.isArray(raw)) return raw;
  181. if (raw && typeof raw === "object") {
  182. const inner = raw.list || raw.records || raw.rows || raw.data;
  183. if (Array.isArray(inner)) return inner;
  184. }
  185. return [];
  186. }
  187. /**
  188. * 字典项归一,兼容三类结构:
  189. * ① 标准字段:{ label / value / name / dictLabel / dictValue / code }
  190. * ② 单键值对:[{6:"试用期"}, {1:"正式在职"}] —— 数字码在 key 上、文案在 value 上
  191. * ③ 纯字符串数组
  192. * @returns {Array<{label:string, value:any, key:string, id:any}>}
  193. */
  194. export function normalizeDictItems(raw) {
  195. const out = [];
  196. dictList(raw).forEach((item) => {
  197. if (item === null || item === undefined) return;
  198. if (typeof item !== "object") {
  199. const text = String(item).trim();
  200. if (text) out.push({ label: text, value: text, key: "", id: undefined });
  201. return;
  202. }
  203. const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== "");
  204. const hasStd =
  205. item.label != null ||
  206. item.dictLabel != null ||
  207. item.name != null ||
  208. item.value != null ||
  209. item.dictValue != null ||
  210. item.code != null;
  211. if (hasStd) {
  212. const rawLabel =
  213. item.label != null
  214. ? item.label
  215. : item.dictLabel != null
  216. ? item.dictLabel
  217. : item.name != null
  218. ? item.name
  219. : item.value != null
  220. ? item.value
  221. : item.dictValue != null
  222. ? item.dictValue
  223. : "";
  224. const label = String(rawLabel).trim();
  225. if (!label) return;
  226. const id = item.id != null ? item.id : item.dictId;
  227. const key =
  228. item.key != null
  229. ? String(item.key)
  230. : item.dictCode != null
  231. ? String(item.dictCode)
  232. : id != null
  233. ? String(id)
  234. : "";
  235. const value =
  236. item.value != null
  237. ? item.value
  238. : item.dictValue != null
  239. ? item.dictValue
  240. : item.code != null
  241. ? item.code
  242. : key;
  243. out.push({ label, value, key, id });
  244. return;
  245. }
  246. // 单键值对:{6:"试用期"}
  247. if (entries.length === 1) {
  248. const [k, v] = entries[0];
  249. const label = String(v).trim();
  250. if (!label) return;
  251. out.push({ label, value: k, key: String(k), id: undefined });
  252. }
  253. });
  254. return out;
  255. }
  256. /** 员工状态字典 → 下拉项(value 一律用数字状态码,与接口 status 对齐) */
  257. export function dictToStatusOptions(raw) {
  258. return normalizeDictItems(raw)
  259. .map((item) => {
  260. const key = String(item.key);
  261. const value = String(item.value);
  262. const code = /^\d+$/.test(key) ? Number(key) : /^\d+$/.test(value) ? Number(value) : NaN;
  263. return Number.isFinite(code) ? { label: item.label, value: code } : null;
  264. })
  265. .filter(Boolean);
  266. }
  267. /** 岗位性质字典 → 下拉项(value 取字典主键,与 PC 表单一致:提交的 positionType 就是它) */
  268. export function dictToPositionNatureOptions(raw) {
  269. return normalizeDictItems(raw)
  270. .map((item) => {
  271. const value = item.id != null ? String(item.id) : item.key ? String(item.key) : String(item.value || "");
  272. return value ? { label: item.label, value } : null;
  273. })
  274. .filter(Boolean);
  275. }
  276. /* =========================================================================
  277. * 基础工具
  278. * ========================================================================= */
  279. function txt(value) {
  280. return value == null ? "" : String(value);
  281. }
  282. function trim(value) {
  283. return txt(value).trim();
  284. }
  285. function date10(value) {
  286. return value ? formatDate(value) : "";
  287. }
  288. /**
  289. * 主键一律按**字符串**回传。
  290. * 接口 GET 回来的 id 都是字符串(雪花 id 19 位,超出 Number.MAX_SAFE_INTEGER,
  291. * 转成 number 末尾几位会被四舍五入),提交时必须原样送字符串,否则会出现
  292. * 「查回来是 2096754981489143810、存进去变成 2096754981489143800」。
  293. * 原先这里用的是 num() = Number(value),把 id / 机构 / 部门 / 主管 / 工厂全转成了数字。
  294. */
  295. export function keepIdString(value) {
  296. if (value === null || value === undefined || value === "") return undefined;
  297. const text = String(value).trim();
  298. return text || undefined;
  299. }
  300. /** 去掉 undefined / null / 空串(保留 0 / false),对齐 PC 端 compact */
  301. function compact(obj = {}) {
  302. const out = {};
  303. Object.keys(obj).forEach((key) => {
  304. const value = obj[key];
  305. if (value === undefined || value === null || value === "") return;
  306. out[key] = value;
  307. });
  308. return out;
  309. }
  310. function currentUserRaw() {
  311. return uni.getStorageSync("userInfo") || {};
  312. }
  313. /* =========================================================================
  314. * 读取接口
  315. * ========================================================================= */
  316. /**
  317. * 拉取当前登录人的扩展资料(部门、岗位、角色等)
  318. * GET /system/account/getLoginUser
  319. */
  320. export async function fetchCurrentUser(params = {}, loding = false) {
  321. const res = await get(Vue.prototype.apiUrl + "/system/account/getLoginUser", params, loding);
  322. if (res.code == 0) return res.data || null;
  323. return Promise.reject(new Error(res.message));
  324. }
  325. /** 把 profile.postId("1111,2222")解析成岗位名称字符串 */
  326. async function resolvePostIds(postIdString) {
  327. if (!postIdString) return "";
  328. const ids = String(postIdString)
  329. .split(",")
  330. .map((s) => s.trim())
  331. .filter(Boolean);
  332. if (!ids.length) return "";
  333. const res = await postJ(Vue.prototype.apiUrl + "/hr/position/page", { pageNum: 1, size: 1000, status: 1 }, false);
  334. if (Number(res.code) !== 0) return "";
  335. const pageData = res.data || {};
  336. const records = Array.isArray(pageData.list) ? pageData.list : Array.isArray(pageData) ? pageData : [];
  337. return ids
  338. .map((id) => {
  339. const match = records.find((r) => String(r.id) === id);
  340. return match ? String(match.positionName || match.name || "") : "";
  341. })
  342. .filter(Boolean)
  343. .join("、");
  344. }
  345. /**
  346. * 员工档案详情(把 user 子对象摊平到顶层,并回填岗位名称)
  347. * GET /main/users/:userId/profile
  348. */
  349. export async function fetchUserDetail(userId, loding = true) {
  350. if (!userId && userId !== 0) throw new Error("缺少用户ID");
  351. const res = await get(Vue.prototype.apiUrl + `/main/users/${userId}/profile`, {}, loding);
  352. if (Number(res.code) !== 0) return Promise.reject(new Error(res.message));
  353. const data = res.data;
  354. if (!data) return null;
  355. const profile = data.user && typeof data.user === "object" ? { ...data, ...data.user } : data;
  356. try {
  357. const names = await resolvePostIds(profile.postId);
  358. if (names) profile.position = names;
  359. } catch (error) {
  360. // 岗位解析失败不影响其他字段
  361. }
  362. return profile;
  363. }
  364. /** 拉取完整档案聚合视图(基础档案 + 履历 + 业务摘要) */
  365. export async function fetchUserProfile(userId, loding = true) {
  366. if (!userId && userId !== 0) throw new Error("缺少用户ID");
  367. const res = await get(Vue.prototype.apiUrl + `/main/users/${userId}/profile`, {}, loding);
  368. if (Number(res.code) !== 0) return Promise.reject(new Error(res.message));
  369. const data = res.data;
  370. if (!data) return null;
  371. const profile = data.user && typeof data.user === "object" ? { ...data, ...data.user } : data;
  372. return { ...data, ...profile };
  373. }
  374. /** 拉取主管档案并把姓名回填到 vo.managerName */
  375. export async function resolveManagerName(vo) {
  376. if (!vo || typeof vo !== "object") return vo;
  377. const raw = vo.user && typeof vo.user === "object" ? vo.user : vo;
  378. const managerId = raw.managerUserId != null ? raw.managerUserId : vo.managerUserId;
  379. if (managerId === undefined || managerId === null || managerId === "") return vo;
  380. try {
  381. const managerVo = await fetchUserDetail(managerId, false);
  382. if (managerVo) {
  383. const name = (managerVo.user && managerVo.user.name) || managerVo.name || "";
  384. raw.managerName = String(name || "");
  385. }
  386. } catch (error) {
  387. // 主管档案解析失败不阻塞主流程
  388. }
  389. return vo;
  390. }
  391. /** 修改档案成功后,重新拉一次当前用户的 profile 接口并写回本地缓存 */
  392. export async function refetchMyProfile() {
  393. const login = currentUserRaw();
  394. const userId = login.userId || login.id;
  395. if (!userId && userId !== 0) throw new Error("缺少用户ID,无法刷新档案");
  396. const vo = await fetchUserDetail(userId);
  397. if (vo) {
  398. await resolveManagerName(vo);
  399. uni.setStorageSync("userInfo", { ...login, ...vo });
  400. }
  401. return vo;
  402. }
  403. /** 当前登录人 userId */
  404. export function currentUserId() {
  405. const login = currentUserRaw();
  406. return login.userId || login.id || "";
  407. }
  408. /** 工厂(厂区)列表,用于「所属工厂」下拉 */
  409. export async function listFactories(loding = false) {
  410. try {
  411. const res = await get(
  412. Vue.prototype.apiUrl + "/main/factoryarea/page",
  413. { pageNum: 1, size: 999, type: 1 },
  414. loding
  415. );
  416. if (Number(res.code) !== 0) return [];
  417. const data = res.data || {};
  418. const rows = Array.isArray(data.list) ? data.list : Array.isArray(data) ? data : [];
  419. return rows.map((item) => ({
  420. id: item.id != null ? item.id : item.factoryId,
  421. name: item.name || item.factoryName || item.areaName || "",
  422. }));
  423. } catch (error) {
  424. return [];
  425. }
  426. }
  427. /* =========================================================================
  428. * 工作履历(独立接口 /main/users/{userId}/work-experiences)
  429. * ========================================================================= */
  430. export function serializeWorkExperience(item = {}) {
  431. return compact({
  432. formerCompanyName: trim(item.company),
  433. formerDepartment: trim(item.department),
  434. formerPosition: trim(item.position),
  435. resignationReason: trim(item.leaveReason),
  436. workDescription: trim(item.description),
  437. workStartDate: item.startDate || "",
  438. workEndDate: item.endDate || "",
  439. });
  440. }
  441. export async function listWorkExperiences(userId, loding = false) {
  442. const uid = userId || currentUserId();
  443. if (!uid) return [];
  444. const res = await get(Vue.prototype.apiUrl + `/main/users/${uid}/work-experiences`, {}, loding);
  445. if (Number(res.code) !== 0) return [];
  446. return Array.isArray(res.data) ? res.data : [];
  447. }
  448. export async function saveWorkExperience(userId, payload = {}) {
  449. const uid = userId || currentUserId();
  450. const res = await postJ(Vue.prototype.apiUrl + `/main/users/${uid}/work-experiences`, payload, false);
  451. if (res.code == 0) return res.data;
  452. return Promise.reject(new Error(res.message));
  453. }
  454. export async function updateWorkExperience(userId, workExperienceId, payload = {}) {
  455. const uid = userId || currentUserId();
  456. const res = await putJ(
  457. Vue.prototype.apiUrl + `/main/users/${uid}/work-experiences/${workExperienceId}`,
  458. payload,
  459. false
  460. );
  461. if (res.code == 0) return res.data;
  462. return Promise.reject(new Error(res.message));
  463. }
  464. export async function removeWorkExperience(userId, workExperienceId) {
  465. const uid = userId || currentUserId();
  466. const res = await deleteApi(
  467. Vue.prototype.apiUrl + `/main/users/${uid}/work-experiences/${workExperienceId}`,
  468. {},
  469. false
  470. );
  471. if (res.code == 0) return res.data;
  472. return Promise.reject(new Error(res.message));
  473. }
  474. /**
  475. * 以「页面当前列表」为准同步工作履历:缺 id 的新增、有 id 的更新、页面已删除的移除。
  476. * 返回同步后的最新列表。
  477. */
  478. export async function syncWorkExperiences(userId, jobs = [], existing = []) {
  479. const uid = userId || currentUserId();
  480. if (!uid) return jobs;
  481. const next = (jobs || []).filter((item) => item.company && item.startDate && item.endDate);
  482. const keptIds = new Set(next.map((item) => item.id).filter(Boolean).map(String));
  483. const removals = (existing || [])
  484. .filter((item) => item.id && !keptIds.has(String(item.id)))
  485. .map((item) => removeWorkExperience(uid, item.id));
  486. await Promise.all(removals);
  487. const saved = await Promise.all(
  488. next.map((item) => {
  489. const payload = serializeWorkExperience(item);
  490. if (item.id) return updateWorkExperience(uid, item.id, payload).then(() => ({ ...item }));
  491. return saveWorkExperience(uid, payload).then((id) => ({ ...item, id: id || item.id }));
  492. })
  493. );
  494. return saved;
  495. }
  496. /* =========================================================================
  497. * 档案 VO → 页面表单模型
  498. * ========================================================================= */
  499. function adaptBankCards(raw = {}) {
  500. if (Array.isArray(raw.bankCardList) && raw.bankCardList.length) {
  501. return raw.bankCardList.map((item, index) => ({
  502. key: `bank_${item.id != null ? item.id : index}`,
  503. id: item.id,
  504. bankName: txt(item.openingBank),
  505. bankBranch: txt(item.bankBranch),
  506. bankAccount: txt(item.cardNumber),
  507. bankAccountName: txt(item.accountName || raw.name),
  508. }));
  509. }
  510. if (raw.openingBank || raw.cardNumber) {
  511. return [
  512. {
  513. key: "bank_legacy",
  514. id: undefined,
  515. bankName: txt(raw.openingBank),
  516. bankBranch: txt(raw.bankBranch),
  517. bankAccount: txt(raw.cardNumber),
  518. bankAccountName: txt(raw.name),
  519. },
  520. ];
  521. }
  522. return [];
  523. }
  524. function adaptSocialSecurity(raw = {}) {
  525. if (!Array.isArray(raw.socialSecurityList)) return [];
  526. return raw.socialSecurityList.map((item, index) => ({
  527. key: `social_${item.id != null ? item.id : index}`,
  528. id: item.id,
  529. type: txt(item.socialType),
  530. base: item.socialBase === null || item.socialBase === undefined || item.socialBase === "" ? "" : String(item.socialBase),
  531. insuredDate: date10(item.insuredDate),
  532. stopDate: date10(item.stopDate),
  533. stopReason: txt(item.stopRecord),
  534. isEnabled: Number(item.isEnabled) === 1 ? 1 : 0,
  535. }));
  536. }
  537. function adaptCertificates(raw = {}) {
  538. const levelLabel = { 1: "初级", 2: "中级", 3: "高级" };
  539. const typeLabel = Object.keys(CERT_TYPE_CODE).reduce((acc, name) => {
  540. acc[String(CERT_TYPE_CODE[name])] = name;
  541. return acc;
  542. }, {});
  543. if (!Array.isArray(raw.identityPhotos)) return [];
  544. return raw.identityPhotos.map((item, index) => ({
  545. key: `cert_${item.id != null ? item.id : index}`,
  546. id: item.id,
  547. hrCertificateId: item.hrCertificateId,
  548. type: typeLabel[String(item.type)] || "其他",
  549. number: txt(item.code),
  550. level: item.level == null || item.level === "" ? "" : levelLabel[item.level] || String(item.level),
  551. status: Number(item.status) === 0 ? "失效" : "有效",
  552. startDate: date10(item.validityStartTime),
  553. endDate: date10(item.validityEndTime),
  554. issueDate: date10(item.enactorTime),
  555. attachment: item.fileObj || item.fileIds || item.files || item.attachment || [],
  556. remark: txt(item.remark),
  557. holder: txt(item.holder || raw.name),
  558. holderId: item.holderId != null ? item.holderId : raw.id,
  559. holderType: item.holderType || "1",
  560. hasPositionAllowance: Number(item.hasPositionAllowance) === 1 ? 1 : 0,
  561. positionAllowanceAmount:
  562. item.positionAllowanceAmount === "" || item.positionAllowanceAmount == null
  563. ? ""
  564. : String(item.positionAllowanceAmount),
  565. positionAllowanceUnit:
  566. item.positionAllowancePaymentUnit == null ? "" : String(item.positionAllowancePaymentUnit),
  567. }));
  568. }
  569. function adaptWorkExperiences(list = []) {
  570. return (list || []).map((item, index) => ({
  571. key: `job_${item.id != null ? item.id : index}`,
  572. id: item.id,
  573. startDate: date10(item.workStartDate),
  574. endDate: date10(item.workEndDate),
  575. company: txt(item.formerCompanyName),
  576. position: txt(item.formerPosition),
  577. department: txt(item.formerDepartment),
  578. leaveReason: txt(item.resignationReason),
  579. description: txt(item.workDescription),
  580. }));
  581. }
  582. function adaptContract(raw = {}) {
  583. const first = Array.isArray(raw.contracts) ? raw.contracts[0] : null;
  584. if (!first) {
  585. return { category: "", name: "", signDate: "", startDate: "", endDate: "", status: "", remark: "" };
  586. }
  587. return {
  588. category: txt(first.contractType),
  589. name: txt(first.contractNo),
  590. signDate: date10(first.effectiveDate || first.startDate),
  591. startDate: date10(first.startDate),
  592. endDate: date10(first.endDate),
  593. // 状态:优先接口给的中文名,其次按枚举码翻译,最后才原样透出
  594. status:
  595. txt(first.contractStatusName) ||
  596. enumLabel(CONTRACT_STATUS_LABELS, first.contractStatus) ||
  597. enumLabel(CONTRACT_SIGNING_STATUS_LABELS, first.signingStatus),
  598. remark: txt(first.remark),
  599. };
  600. }
  601. /** 主岗位(primaryFlag 优先,其次第一条) */
  602. export function pickPrimaryPosition(raw = {}) {
  603. const list = Array.isArray(raw.positionList) ? raw.positionList : [];
  604. if (!list.length) return null;
  605. return list.find((item) => Number(item.primaryFlag) === 1) || list[0];
  606. }
  607. /**
  608. * 把档案聚合 VO 摊平成页面表单模型(字段命名对齐 PC 表单,便于复用载荷构造)。
  609. * @param {object} raw 已摊平 user 字段的档案对象
  610. * @param {object} extras { workExperiences }
  611. */
  612. export function buildProfileForm(raw = {}, extras = {}) {
  613. const primary = pickPrimaryPosition(raw);
  614. const positionIds = txt(raw.postId)
  615. .split(",")
  616. .map((s) => s.trim())
  617. .filter(Boolean);
  618. const deptIds = Array.isArray(raw.deptIds) ? raw.deptIds.filter((id) => id != null && id !== "") : [];
  619. const positionId = primary && primary.positionId != null ? String(primary.positionId) : positionIds[0] || "";
  620. const birthDate = date10(raw.birthday);
  621. // 接口不给 age 时按出生日期兜底(PC 表单同步口径)
  622. const age =
  623. raw.age == null || raw.age === "" ? calcAgeByBirthday(birthDate) : String(raw.age);
  624. const statusCode = raw.status == null || raw.status === "" ? NaN : Number(raw.status);
  625. return {
  626. id: raw.id != null ? raw.id : raw.userId,
  627. accountId: raw.accountId,
  628. account: txt(raw.loginName || raw.account),
  629. employeeNo: txt(raw.jobNumber),
  630. name: txt(raw.name || raw.userName),
  631. gender: sexLabel(raw.sex),
  632. birthDate,
  633. age,
  634. idCard: txt(raw.idCard || raw.idNumber),
  635. ethnic: txt(raw.ethnic),
  636. politicalStatus: txt(raw.politicsStatus),
  637. maritalStatus: txt(raw.maritalStatus),
  638. nativePlace: txt(raw.birthplace),
  639. education: txt(raw.educationBackground),
  640. degree: txt(raw.degree),
  641. school: txt(raw.school),
  642. major: txt(raw.major),
  643. graduationDate: date10(raw.graduateDate),
  644. censusAddress: txt(raw.censusAddress),
  645. currentAddress: txt(raw.currentAddress),
  646. workAddress: txt(raw.address),
  647. phone: txt(raw.phone),
  648. email: txt(raw.email),
  649. wechat: txt(raw.wxId),
  650. emergencyContact: txt(raw.secondLinkName),
  651. emergencyRelation: txt(raw.secondLinkRelation),
  652. emergencyPhone: txt(raw.secondLinkPhone),
  653. groupId: raw.groupId != null && raw.groupId !== "" ? String(raw.groupId) : "",
  654. company: txt(raw.groupName),
  655. factoryId: raw.factoryId != null && raw.factoryId !== "" ? String(raw.factoryId) : raw.factoriesId != null ? String(raw.factoriesId) : "",
  656. factory: txt(raw.factoryName),
  657. deptIds,
  658. deptId: deptIds.length ? String(deptIds[deptIds.length - 1]) : "",
  659. deptName: txt(raw.deptName || raw.deptNames),
  660. positionSequenceId:
  661. primary && primary.positionSequenceId != null ? String(primary.positionSequenceId) : "",
  662. positionSequenceName: txt(primary && (primary.positionSequenceName || primary.sequenceName)),
  663. positionId,
  664. // 岗位名称:优先取岗位明细(positionList)里的名称,与 PC adaptPositionBindings 口径一致
  665. positionName: txt(
  666. (Array.isArray(raw.positionList)
  667. ? raw.positionList
  668. .map((item) => (item && (item.positionName || item.name)) || "")
  669. .filter(Boolean)
  670. .join("、")
  671. : "") ||
  672. raw.postName ||
  673. raw.position
  674. ),
  675. // 岗位性质:码与名称分开存(码取字典主键,名称缺失时由页面按字典翻译),对齐 PC 口径
  676. employmentType: txt(
  677. (primary && (primary.positionType != null ? primary.positionType : "")) ||
  678. raw.positionType ||
  679. raw.employmentType ||
  680. raw.staffingType ||
  681. ""
  682. ),
  683. employmentTypeName: txt(
  684. (primary && (primary.positionTypeName || "")) ||
  685. raw.positionTypeName ||
  686. raw.employmentTypeName ||
  687. raw.staffingTypeName ||
  688. ""
  689. ),
  690. staffingLevel: txt((primary && (primary.positionLevelName || primary.positionLevel)) || ""),
  691. managerId: raw.managerUserId != null && raw.managerUserId !== "" ? String(raw.managerUserId) : "",
  692. manager: txt(raw.managerUserName || raw.managerName),
  693. hireDate: date10(raw.joinDate || raw.hireDate),
  694. regularDate: date10(raw.positiveDate),
  695. // 员工状态:只认接口原始状态码,取不到就留空(不编造默认值,避免「已离职」被回显成「正式在职」)
  696. status: Number.isFinite(statusCode) && statusCode > 0 ? statusCode : "",
  697. statusName: txt(raw.statusName),
  698. enabled: Number(raw.isEnabled) !== 0,
  699. keyProcess: txt(
  700. raw.criticalTaskIds ||
  701. (Array.isArray(raw.criticalTaskList)
  702. ? raw.criticalTaskList.map((item) => item.taskName || item.taskId).filter(Boolean).join(",")
  703. : "")
  704. ),
  705. bankAccounts: adaptBankCards(raw),
  706. socialSecurityRecords: adaptSocialSecurity(raw),
  707. certificates: adaptCertificates(raw),
  708. previousJobs: adaptWorkExperiences(extras.workExperiences || []),
  709. remarks: txt(raw.remark),
  710. contract: adaptContract(raw),
  711. };
  712. }
  713. /* =========================================================================
  714. * 表单模型 → /main/user/saveNew 载荷
  715. * ========================================================================= */
  716. /** 关键工序字符串 → CriticalTaskDTO 列表(数字视为已存在的 taskId) */
  717. function buildCriticalTaskList(text, rawList = []) {
  718. const value = trim(text);
  719. if (!value) return [];
  720. const rows = Array.isArray(rawList) ? rawList : [];
  721. return value
  722. .split(/[,,]/)
  723. .map((item) => trim(item))
  724. .filter(Boolean)
  725. .map((item) => {
  726. if (/^\d+$/.test(item)) return { taskId: item, taskName: item };
  727. const matched = rows.find((row) => String(row.taskName) === item || String(row.taskId) === item);
  728. if (matched) return compact({ taskId: matched.taskId, taskName: matched.taskName || item });
  729. return { taskName: item };
  730. });
  731. }
  732. /**
  733. * 页面表单 → 保存参数(对齐 PC 端 serializeEmployee 的字段口径)。
  734. * @param {object} form 表单模型
  735. * @param {object} options { raw, positionList }
  736. */
  737. export function buildUserPayload(form = {}, options = {}) {
  738. const raw = options.raw || {};
  739. const positionList = Array.isArray(options.positionList) ? options.positionList : [];
  740. const bankCardList = (form.bankAccounts || [])
  741. .filter((item) => item.bankName || item.bankBranch || item.bankAccount)
  742. .map((item) => ({
  743. openingBank: trim(item.bankName),
  744. bankBranch: trim(item.bankBranch),
  745. cardNumber: trim(item.bankAccount),
  746. accountName: trim(item.bankAccountName || form.name),
  747. }))
  748. .filter((item) => item.accountName);
  749. const socialSecurityList = (form.socialSecurityRecords || [])
  750. .filter((item) => trim(item.type))
  751. .map((item) =>
  752. compact({
  753. socialType: trim(item.type),
  754. socialBase: item.base === "" || item.base == null ? undefined : Number(item.base),
  755. insuredDate: item.insuredDate || "",
  756. stopDate: item.stopDate || "",
  757. stopRecord: trim(item.stopReason),
  758. isEnabled: Number(item.isEnabled) === 1 ? 1 : 0,
  759. })
  760. );
  761. const identityPhotos = (form.certificates || [])
  762. .filter((item) => trim(item.type) || trim(item.number))
  763. .map((item) =>
  764. compact({
  765. id: keepIdString(item.id),
  766. type: CERT_TYPE_CODE[trim(item.type)] != null ? CERT_TYPE_CODE[trim(item.type)] : CERT_TYPE_CODE.其他,
  767. code: trim(item.number),
  768. level: CERT_LEVEL_CODE[trim(item.level)] || trim(item.level),
  769. status: item.status === "失效" ? 0 : 1,
  770. validityStartTime: item.startDate || "",
  771. validityEndTime: item.endDate || "",
  772. enactorTime: item.issueDate || "",
  773. remark: trim(item.remark),
  774. holderType: item.holderType || "1",
  775. holder: trim(item.holder || form.name),
  776. holderId: keepIdString(item.holderId != null ? item.holderId : form.id),
  777. hasPositionAllowance: Number(item.hasPositionAllowance) === 1 ? 1 : 0,
  778. positionAllowanceAmount:
  779. Number(item.hasPositionAllowance) === 1 && item.positionAllowanceAmount !== "" && item.positionAllowanceAmount != null
  780. ? Number(item.positionAllowanceAmount)
  781. : undefined,
  782. positionAllowancePaymentUnit:
  783. Number(item.hasPositionAllowance) === 1 && item.positionAllowanceUnit
  784. ? String(item.positionAllowanceUnit)
  785. : undefined,
  786. fileObj: Array.isArray(item.attachment) ? item.attachment : undefined,
  787. hrCertificateId: keepIdString(item.hrCertificateId),
  788. })
  789. );
  790. const deptIds = (Array.isArray(form.deptIds) && form.deptIds.length ? form.deptIds : form.deptId ? [form.deptId] : [])
  791. .map((id) => keepIdString(id))
  792. .filter((id) => id != null);
  793. const payload = compact({
  794. id: keepIdString(form.id),
  795. accountId: keepIdString(form.accountId),
  796. name: trim(form.name),
  797. jobNumber: trim(form.employeeNo),
  798. sex: SEX_CODE[form.gender] != null ? SEX_CODE[form.gender] : undefined,
  799. birthday: form.birthDate || "",
  800. age: form.age === "" || form.age == null ? undefined : Number(form.age),
  801. idCard: trim(form.idCard),
  802. idNumber: trim(form.idCard),
  803. ethnic: trim(form.ethnic),
  804. politicsStatus: trim(form.politicalStatus),
  805. maritalStatus: trim(form.maritalStatus),
  806. birthplace: trim(form.nativePlace),
  807. censusAddress: trim(form.censusAddress),
  808. currentAddress: trim(form.currentAddress),
  809. address: trim(form.workAddress),
  810. educationBackground: trim(form.education),
  811. degree: trim(form.degree),
  812. school: trim(form.school),
  813. major: trim(form.major),
  814. graduateDate: form.graduationDate || "",
  815. phone: trim(form.phone),
  816. email: trim(form.email),
  817. wxId: trim(form.wechat),
  818. // 员工状态只提交数字状态码;statusName 仅用于页面兜底显示,不回传避免写入过期快照
  819. status: Number(form.status) > 0 ? Number(form.status) : undefined,
  820. joinDate: form.hireDate || "",
  821. positiveDate: form.regularDate || "",
  822. leaveDate: raw.leaveDate || "",
  823. isEnabled: form.enabled === false ? 0 : 1,
  824. bankCardList,
  825. secondLinkName: trim(form.emergencyContact),
  826. secondLinkPhone: trim(form.emergencyPhone),
  827. secondLinkRelation: trim(form.emergencyRelation),
  828. professionId: raw.professionId,
  829. workTypeId: raw.workTypeId,
  830. remark: trim(form.remarks),
  831. groupId: keepIdString(form.groupId),
  832. deptIds,
  833. deptName: trim(form.deptName),
  834. managerUserId: keepIdString(form.managerId),
  835. postId: keepIdString(form.positionId),
  836. postName: trim(form.positionName),
  837. positionList,
  838. factoryId: keepIdString(form.factoryId),
  839. criticalTaskList: buildCriticalTaskList(form.keyProcess, raw.criticalTaskList),
  840. socialSecurityList,
  841. identityPhotos,
  842. socialSecurityAccount: raw.socialSecurityAccount,
  843. socialSecurityAddress: raw.socialSecurityAddress,
  844. socialSecurityAddressId: raw.socialSecurityAddressId,
  845. accumulationFundAccount: raw.accumulationFundAccount,
  846. });
  847. return payload;
  848. }
  849. /**
  850. * 新增 / 修改员工档案
  851. * POST /main/user/saveNew
  852. */
  853. export async function saveEmployee(payload = {}) {
  854. const res = await postJ(Vue.prototype.apiUrl + "/main/user/saveNew", payload, true);
  855. if (res.code == 0) return res.data;
  856. return Promise.reject(new Error(res.message));
  857. }
  858. export { compact as compactPayload };