approval.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import {
  2. adaptBpmTask,
  3. adaptCcProcess,
  4. approveTask,
  5. getCcProcessPage,
  6. getDoneTaskPage,
  7. getProcessInstance,
  8. getTodoTaskPage,
  9. rejectTask,
  10. } from "@/api/hr/bpm";
  11. import { loadResignationDetail } from "@/api/hr/resign";
  12. export const APPROVAL_TAB_TODO = "待我审批";
  13. export const APPROVAL_TAB_DONE = "我已审批";
  14. export const APPROVAL_TAB_CC = "抄送我的";
  15. export const APPROVAL_TABS = [APPROVAL_TAB_TODO, APPROVAL_TAB_DONE, APPROVAL_TAB_CC];
  16. export const RESIGN_BUSINESS_TYPE = "离职申请";
  17. // 缓存最近一次列表结果,详情页按 id 查找时优先命中,避免重复请求
  18. const listCache = new Map();
  19. function isResignType(text) {
  20. return String(text || "").includes("离职");
  21. }
  22. // 服务端任务不一定回填 businessType,这里把流程名 / 移动端路由一起作为兜底判断
  23. function isResignItem(item = {}) {
  24. return [item.type, item.businessName, item.processName, item.viewRouter].some(
  25. (text) => isResignType(text) || String(text || "").toLowerCase().includes("resign")
  26. );
  27. }
  28. function withStatus(item) {
  29. const status = (item.result || {}).status || "待审批";
  30. return {
  31. ...item,
  32. statusLabel: status,
  33. statusClass:
  34. status === "已同意"
  35. ? "status-approved"
  36. : status === "待审批" || status === "已抄送"
  37. ? "status-pending"
  38. : "status-rejected",
  39. };
  40. }
  41. // 任务上没带业务主键时,回流程实例里找(PC 端发起流程时写入 businessId / 流程变量)
  42. async function resolveBusiness(item) {
  43. if (item.businessId && item.applicant !== "—") return item;
  44. if (!item.processInstanceId) return item;
  45. try {
  46. const instance = (await getProcessInstance(item.processInstanceId, false)) || {};
  47. const variables = instance.variables || {};
  48. const businessId = item.businessId || instance.businessId || variables.businessId || "";
  49. const applicant =
  50. item.applicant && item.applicant !== "—"
  51. ? item.applicant
  52. : instance.startUserNickname || variables.businessName || "—";
  53. return {
  54. ...item,
  55. businessId,
  56. processName:
  57. item.processName || instance.name || variables.businessName || "",
  58. businessCode: item.businessCode || instance.businessCode || variables.businessCode || "",
  59. applicant,
  60. avatar: String(applicant || "审").charAt(0),
  61. dept: item.dept || variables.departmentName || "",
  62. position: item.position || variables.positionName || "",
  63. type: isResignType(item.type) || !variables.businessType ? item.type : variables.businessType,
  64. };
  65. } catch (error) {
  66. return item;
  67. }
  68. }
  69. // 离职类审批:补全离职单信息,便于审批人在手机上直接看单据
  70. async function withResignation(item) {
  71. if (!isResignItem(item) || !item.businessId) return item;
  72. try {
  73. const resignation = await loadResignationDetail(item.businessId);
  74. if (!resignation) return item;
  75. const applicant =
  76. item.applicant && item.applicant !== "—" ? item.applicant : resignation.name || "—";
  77. return {
  78. ...item,
  79. resignation,
  80. applicant,
  81. avatar: String(applicant || "离").charAt(0),
  82. dept: item.dept || resignation.department || "",
  83. position: item.position || resignation.position || "",
  84. businessCode: resignation.resignationNo || item.businessCode || "",
  85. processName: item.processName || resignation.type || RESIGN_BUSINESS_TYPE,
  86. summary: `${resignation.type || RESIGN_BUSINESS_TYPE} · 预计离职 ${
  87. resignation.expectedLeaveDate || "—"
  88. }`,
  89. reason: resignation.reason || item.reason,
  90. days: item.days || `${(resignation.handover && resignation.handover.items ? resignation.handover.items.length : 0)} 项交接`,
  91. };
  92. } catch (error) {
  93. return item;
  94. }
  95. }
  96. async function toServerItem(vo, done) {
  97. const task = adaptBpmTask(vo);
  98. const base = {
  99. id: task.taskId,
  100. source: "server",
  101. taskId: task.taskId,
  102. processInstanceId: task.processInstanceId,
  103. processName: task.processName,
  104. businessId: task.businessId,
  105. businessCode: task.businessCode,
  106. businessName: task.businessName,
  107. type: task.businessType || task.processName,
  108. applicant: task.applicant || "—",
  109. avatar: String(task.applicant || "审").charAt(0),
  110. dept: task.department,
  111. position: task.position,
  112. taskName: task.name,
  113. viewRouter: task.viewRouter,
  114. handleRouter: task.handleRouter,
  115. summary: [task.businessType, task.businessCode || task.businessName].filter(Boolean).join(" · "),
  116. reason: "",
  117. days: "",
  118. color: "#157a2c",
  119. time: task.createTime,
  120. result: done
  121. ? {
  122. status: task.resultLabel || "已同意",
  123. comment: task.reason || "",
  124. handledAt: task.handledAt,
  125. }
  126. : null,
  127. raw: vo,
  128. };
  129. return withStatus(await withResignation(await resolveBusiness(base)));
  130. }
  131. // 抄送我的是只读列表,不带审批动作
  132. function toCcItem(vo) {
  133. const cc = adaptCcProcess(vo);
  134. return withStatus({
  135. id: `cc-${cc.id}`,
  136. source: "server",
  137. ccId: cc.id,
  138. processInstanceId: cc.processInstanceId,
  139. processName: cc.processName,
  140. businessId: "",
  141. businessCode: "",
  142. type: cc.processName || "流程抄送",
  143. applicant: cc.applicant || cc.creatorName || "—",
  144. avatar: String(cc.applicant || cc.creatorName || "抄").charAt(0),
  145. dept: "",
  146. position: "",
  147. taskName: cc.taskName,
  148. summary: [cc.processName, cc.taskName].filter(Boolean).join(" · "),
  149. reason: cc.reason,
  150. days: "",
  151. color: "#157a2c",
  152. time: cc.createTime,
  153. readonly: true,
  154. result: { status: "已抄送", comment: cc.reason || "", handledAt: cc.createTime },
  155. raw: vo,
  156. });
  157. }
  158. async function serverApprovalItems(tab) {
  159. if (tab === APPROVAL_TAB_CC) {
  160. const page = await getCcProcessPage();
  161. return (page.list || []).map((vo) => toCcItem(vo));
  162. }
  163. const page = tab === APPROVAL_TAB_TODO ? await getTodoTaskPage() : await getDoneTaskPage();
  164. const done = tab !== APPROVAL_TAB_TODO;
  165. return Promise.all((page.list || []).map((vo) => toServerItem(vo, done)));
  166. }
  167. /**
  168. * 审批列表统一入口
  169. */
  170. export async function loadApprovalList(tab = APPROVAL_TAB_TODO) {
  171. const list = await serverApprovalItems(tab);
  172. listCache.set(tab, list);
  173. return { list, source: "api" };
  174. }
  175. /**
  176. * 审批详情:优先命中缓存,未命中则待办 / 已办各拉一次再匹配
  177. */
  178. export async function loadApprovalDetail(id) {
  179. if (!id) return null;
  180. const cachedList = Array.from(listCache.values());
  181. for (let i = 0; i < cachedList.length; i++) {
  182. const hit = (cachedList[i] || []).find((x) => String(x.id) === String(id));
  183. if (hit) return hit;
  184. }
  185. const results = await Promise.all([
  186. serverApprovalItems(APPROVAL_TAB_TODO),
  187. serverApprovalItems(APPROVAL_TAB_DONE),
  188. ]);
  189. const all = results[0].concat(results[1]);
  190. return all.find((x) => String(x.id) === String(id)) || null;
  191. }
  192. /**
  193. * 审批通过
  194. */
  195. export async function actApproveApproval(item, comment = "") {
  196. if (!item || !item.id) throw new Error("缺少审批主键");
  197. const reason = String(comment || "").trim() || "同意";
  198. await approveTask({ id: item.taskId || item.id, reason });
  199. return true;
  200. }
  201. /**
  202. * 审批驳回
  203. */
  204. export async function actRejectApproval(item, comment = "") {
  205. if (!item || !item.id) throw new Error("缺少审批主键");
  206. const reason = String(comment || "").trim() || "申请信息不完整";
  207. await rejectTask({ id: item.taskId || item.id, reason });
  208. return true;
  209. }
  210. /** 待办数量(首页 / 工作台 / TabBar 角标) */
  211. export async function fetchPendingApprovalCount() {
  212. try {
  213. const page = await getTodoTaskPage({ pageSize: 1 }, false);
  214. return page.total || 0;
  215. } catch (error) {
  216. return 0;
  217. }
  218. }