|
|
@@ -0,0 +1,317 @@
|
|
|
+import { getServerConfig } from '@/utils/auth'
|
|
|
+import { approvals as mockApprovals } from '@/data/mock'
|
|
|
+import {
|
|
|
+ getApprovalResults,
|
|
|
+ setApprovalResult,
|
|
|
+ setResignLocalState,
|
|
|
+ updateCreatedResignation,
|
|
|
+} from '@/utils/storage'
|
|
|
+import {
|
|
|
+ adaptBpmTask,
|
|
|
+ adaptCcProcess,
|
|
|
+ approveTask,
|
|
|
+ getCcProcessPage,
|
|
|
+ getDoneTaskPage,
|
|
|
+ getProcessInstance,
|
|
|
+ getTodoTaskPage,
|
|
|
+ rejectTask,
|
|
|
+} from '@/api/bpm'
|
|
|
+import { loadResignationDetail } from '@/api/resign'
|
|
|
+
|
|
|
+export const APPROVAL_TAB_TODO = '待我审批'
|
|
|
+export const APPROVAL_TAB_DONE = '我已审批'
|
|
|
+export const APPROVAL_TAB_CC = '抄送我的'
|
|
|
+export const APPROVAL_TABS = [APPROVAL_TAB_TODO, APPROVAL_TAB_DONE, APPROVAL_TAB_CC]
|
|
|
+export const RESIGN_BUSINESS_TYPE = '离职申请'
|
|
|
+
|
|
|
+// 缓存最近一次列表结果,详情页按 id 查找时优先命中,避免重复请求
|
|
|
+const listCache = new Map()
|
|
|
+
|
|
|
+function isServerMode() {
|
|
|
+ return getServerConfig().mode === 'server'
|
|
|
+}
|
|
|
+
|
|
|
+function isResignType(text) {
|
|
|
+ return String(text || '').includes('离职')
|
|
|
+}
|
|
|
+
|
|
|
+// 服务端任务不一定回填 businessType,这里把流程名 / 移动端路由一起作为兜底判断
|
|
|
+function isResignItem(item = {}) {
|
|
|
+ return [item.type, item.businessName, item.processName, item.viewRouter].some(
|
|
|
+ (text) => isResignType(text) || String(text || '').toLowerCase().includes('resign'),
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function withStatus(item) {
|
|
|
+ const status = item.result?.status || '待审批'
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ statusLabel: status,
|
|
|
+ statusClass:
|
|
|
+ status === '已同意'
|
|
|
+ ? 'status-approved'
|
|
|
+ : status === '待审批' || status === '已抄送'
|
|
|
+ ? 'status-pending'
|
|
|
+ : 'status-rejected',
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 演示模式 ============
|
|
|
+
|
|
|
+async function demoApprovalItems() {
|
|
|
+ const results = getApprovalResults()
|
|
|
+ const base = mockApprovals.map((x) => {
|
|
|
+ const result = results[x.id] || null
|
|
|
+ return {
|
|
|
+ id: x.id,
|
|
|
+ source: 'demo',
|
|
|
+ type: x.type,
|
|
|
+ applicant: x.applicant,
|
|
|
+ avatar: x.avatar,
|
|
|
+ dept: x.dept,
|
|
|
+ position: '',
|
|
|
+ summary: x.summary,
|
|
|
+ reason: x.reason || '',
|
|
|
+ days: x.days || '',
|
|
|
+ color: x.color || '#5b8ff9',
|
|
|
+ time: x.time || '',
|
|
|
+ businessId: x.businessId || '',
|
|
|
+ processInstanceId: '',
|
|
|
+ result: result
|
|
|
+ ? {
|
|
|
+ status: result.status,
|
|
|
+ comment: result.comment || '',
|
|
|
+ handledAt: result.handledAt || '',
|
|
|
+ }
|
|
|
+ : null,
|
|
|
+ raw: x,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const enriched = await Promise.all(base.map(withResignation))
|
|
|
+ return enriched.map(withStatus)
|
|
|
+}
|
|
|
+
|
|
|
+function filterByTab(list, tab) {
|
|
|
+ if (tab === APPROVAL_TAB_TODO) return list.filter((x) => !x.result)
|
|
|
+ if (tab === APPROVAL_TAB_DONE) return list.filter((x) => x.result)
|
|
|
+ return []
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 服务器模式 ============
|
|
|
+
|
|
|
+// 任务上没带业务主键时,回流程实例里找(PC 端发起流程时写入 businessId / 流程变量)
|
|
|
+async function resolveBusiness(item) {
|
|
|
+ if (item.businessId && item.applicant !== '—') return item
|
|
|
+ if (!item.processInstanceId) return item
|
|
|
+ try {
|
|
|
+ const instance = await getProcessInstance(item.processInstanceId)
|
|
|
+ const variables = instance?.variables || {}
|
|
|
+ const businessId = item.businessId || instance?.businessId || variables.businessId || ''
|
|
|
+ const applicant =
|
|
|
+ item.applicant && item.applicant !== '—'
|
|
|
+ ? item.applicant
|
|
|
+ : instance?.startUserNickname || variables.businessName || '—'
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ businessId,
|
|
|
+ processName: item.processName || instance?.name || variables.businessName || '',
|
|
|
+ businessCode: item.businessCode || instance?.businessCode || variables.businessCode || '',
|
|
|
+ applicant,
|
|
|
+ avatar: String(applicant || '审').charAt(0),
|
|
|
+ dept: item.dept || variables.departmentName || '',
|
|
|
+ position: item.position || variables.positionName || '',
|
|
|
+ type: isResignType(item.type) || !variables.businessType ? item.type : variables.businessType,
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ return item
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 离职类审批:补全离职单信息,便于审批人在手机上直接看单据
|
|
|
+async function withResignation(item) {
|
|
|
+ if (!isResignItem(item) || !item.businessId) return item
|
|
|
+ try {
|
|
|
+ const resignation = await loadResignationDetail(item.businessId)
|
|
|
+ if (!resignation) return item
|
|
|
+ const applicant =
|
|
|
+ item.applicant && item.applicant !== '—' ? item.applicant : resignation.name || '—'
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ resignation,
|
|
|
+ applicant,
|
|
|
+ avatar: String(applicant || '离').charAt(0),
|
|
|
+ dept: item.dept || resignation.department || '',
|
|
|
+ position: item.position || resignation.position || '',
|
|
|
+ businessCode: resignation.resignationNo || item.businessCode || '',
|
|
|
+ processName: item.processName || resignation.type || RESIGN_BUSINESS_TYPE,
|
|
|
+ summary: `${resignation.type || RESIGN_BUSINESS_TYPE} · 预计离职 ${
|
|
|
+ resignation.expectedLeaveDate || '—'
|
|
|
+ }`,
|
|
|
+ reason: resignation.reason || item.reason,
|
|
|
+ days: item.days || `${resignation.handover?.items?.length || 0} 项交接`,
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ return item
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toServerItem(vo, done) {
|
|
|
+ const task = adaptBpmTask(vo)
|
|
|
+ const base = {
|
|
|
+ id: task.taskId,
|
|
|
+ source: 'server',
|
|
|
+ taskId: task.taskId,
|
|
|
+ processInstanceId: task.processInstanceId,
|
|
|
+ processName: task.processName,
|
|
|
+ businessId: task.businessId,
|
|
|
+ businessCode: task.businessCode,
|
|
|
+ businessName: task.businessName,
|
|
|
+ type: task.businessType || task.processName,
|
|
|
+ applicant: task.applicant || '—',
|
|
|
+ avatar: String(task.applicant || '审').charAt(0),
|
|
|
+ dept: task.department,
|
|
|
+ position: task.position,
|
|
|
+ taskName: task.name,
|
|
|
+ viewRouter: task.viewRouter,
|
|
|
+ handleRouter: task.handleRouter,
|
|
|
+ summary: [task.businessType, task.businessCode || task.businessName]
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(' · '),
|
|
|
+ reason: '',
|
|
|
+ days: '',
|
|
|
+ color: '#5b8ff9',
|
|
|
+ time: task.createTime,
|
|
|
+ result: done
|
|
|
+ ? {
|
|
|
+ status: task.resultLabel || '已同意',
|
|
|
+ comment: task.reason || '',
|
|
|
+ handledAt: task.handledAt,
|
|
|
+ }
|
|
|
+ : null,
|
|
|
+ raw: vo,
|
|
|
+ }
|
|
|
+ return withStatus(await withResignation(await resolveBusiness(base)))
|
|
|
+}
|
|
|
+
|
|
|
+// 抄送我的是只读列表,不带审批动作
|
|
|
+function toCcItem(vo) {
|
|
|
+ const cc = adaptCcProcess(vo)
|
|
|
+ return withStatus({
|
|
|
+ id: `cc-${cc.id}`,
|
|
|
+ source: 'server',
|
|
|
+ ccId: cc.id,
|
|
|
+ processInstanceId: cc.processInstanceId,
|
|
|
+ processName: cc.processName,
|
|
|
+ businessId: '',
|
|
|
+ businessCode: '',
|
|
|
+ type: cc.processName || '流程抄送',
|
|
|
+ applicant: cc.applicant || cc.creatorName || '—',
|
|
|
+ avatar: String(cc.applicant || cc.creatorName || '抄').charAt(0),
|
|
|
+ dept: '',
|
|
|
+ position: '',
|
|
|
+ taskName: cc.taskName,
|
|
|
+ summary: [cc.processName, cc.taskName].filter(Boolean).join(' · '),
|
|
|
+ reason: cc.reason,
|
|
|
+ days: '',
|
|
|
+ color: '#5b8ff9',
|
|
|
+ time: cc.createTime,
|
|
|
+ readonly: true,
|
|
|
+ result: { status: '已抄送', comment: cc.reason || '', handledAt: cc.createTime },
|
|
|
+ raw: vo,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+async function serverApprovalItems(tab) {
|
|
|
+ if (tab === APPROVAL_TAB_CC) {
|
|
|
+ const page = await getCcProcessPage()
|
|
|
+ return page.list.map(toCcItem)
|
|
|
+ }
|
|
|
+ const page = tab === APPROVAL_TAB_TODO ? await getTodoTaskPage() : await getDoneTaskPage()
|
|
|
+ const done = tab !== APPROVAL_TAB_TODO
|
|
|
+ return Promise.all(page.list.map((vo) => toServerItem(vo, done)))
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 对外统一入口 ============
|
|
|
+
|
|
|
+export async function loadApprovalList(tab = APPROVAL_TAB_TODO) {
|
|
|
+ if (!isServerMode()) {
|
|
|
+ const list = filterByTab(await demoApprovalItems(), tab)
|
|
|
+ listCache.set(tab, list)
|
|
|
+ return { list, source: 'demo' }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ const list = await serverApprovalItems(tab)
|
|
|
+ listCache.set(tab, list)
|
|
|
+ return { list, source: 'api' }
|
|
|
+}
|
|
|
+
|
|
|
+export async function loadApprovalDetail(id) {
|
|
|
+ if (!id) return null
|
|
|
+ for (const cached of listCache.values()) {
|
|
|
+ const hit = cached.find((x) => String(x.id) === String(id))
|
|
|
+ if (hit) return hit
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isServerMode()) {
|
|
|
+ return (await demoApprovalItems()).find((x) => String(x.id) === String(id)) || null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 缓存未命中(例如从消息直达):待办 / 已办各拉一次再匹配
|
|
|
+ const [todo, done] = await Promise.all([
|
|
|
+ serverApprovalItems(APPROVAL_TAB_TODO),
|
|
|
+ serverApprovalItems(APPROVAL_TAB_DONE),
|
|
|
+ ])
|
|
|
+ return [...todo, ...done].find((x) => String(x.id) === String(id)) || null
|
|
|
+}
|
|
|
+
|
|
|
+export async function actApproveApproval(item, comment = '') {
|
|
|
+ if (!item?.id) throw new Error('缺少审批主键')
|
|
|
+ const reason = String(comment || '').trim() || '同意'
|
|
|
+ if (isServerMode()) {
|
|
|
+ await approveTask({ id: item.taskId || item.id, reason })
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ setApprovalResult(item.id, '已同意', reason)
|
|
|
+ await syncDemoResignation(item, true, reason)
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+export async function actRejectApproval(item, comment = '') {
|
|
|
+ if (!item?.id) throw new Error('缺少审批主键')
|
|
|
+ const reason = String(comment || '').trim() || '申请信息不完整'
|
|
|
+ if (isServerMode()) {
|
|
|
+ await rejectTask({ id: item.taskId || item.id, reason })
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ setApprovalResult(item.id, '已驳回', reason)
|
|
|
+ await syncDemoResignation(item, false, reason)
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// 演示模式:把审批结果回写到离职单,使离职流程能继续往下走(生成交接单 / 归档)
|
|
|
+async function syncDemoResignation(item, approved, comment) {
|
|
|
+ const id = item.businessId
|
|
|
+ if (!id) return
|
|
|
+ // 本地不存在对应离职单时(例如纯 mock 待办)不回写,避免产生孤立数据
|
|
|
+ const existing = await loadResignationDetail(id)
|
|
|
+ if (!existing) return
|
|
|
+ const patch = approved
|
|
|
+ ? { status: 'approved', stage: 'handover', approvalStatus: 'APPROVED', hrComment: comment }
|
|
|
+ : { status: 'rejected', stage: 'submit', approvalStatus: 'REJECTED', hrComment: comment }
|
|
|
+ setResignLocalState(id, patch)
|
|
|
+ updateCreatedResignation(id, patch)
|
|
|
+}
|
|
|
+
|
|
|
+/** 待办数量(首页 / 工作台 / TabBar 角标) */
|
|
|
+export async function fetchPendingApprovalCount() {
|
|
|
+ if (!isServerMode()) {
|
|
|
+ return (await demoApprovalItems()).filter((x) => !x.result).length
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const page = await getTodoTaskPage({ pageSize: 1 })
|
|
|
+ return page.total
|
|
|
+ } catch (error) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+}
|