import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth' import { userChangeStatus, userChangeStages, userChanges } from '@/data/mock' import { addCreatedUserChange, formatNow, getCreatedUserChanges, getCurrentUser, getLoginUser, getUserChangeIds, getUserChangeStates, rememberUserChangeId, setUserChangeLocalState, updateCreatedUserChange, } from '@/utils/storage' // ============ 模式判断 ============ function isServerMode() { return getServerConfig().mode === 'server' } function serverPath() { const base = getApiBaseUrl() if (!base) throw new Error('缺少服务器地址,请先在登录页右上角配置') return base } function delay(ms = 200) { return new Promise((resolve) => setTimeout(resolve, ms)) } // ============ 工具 ============ function compact(object = {}) { return Object.keys(object).reduce((result, key) => { const value = object[key] if (value !== '' && value !== null && value !== undefined) result[key] = value return result }, {}) } function dateOnly(value) { if (!value) return '' return String(value).slice(0, 10) } function labelOf(map, raw, fallback = '') { const text = String(raw || '').trim() if (!text) return fallback return map[text] || map[text.toUpperCase()] || text } function asList(data) { if (Array.isArray(data)) return data if (Array.isArray(data?.list)) return data.list if (Array.isArray(data?.records)) return data.records if (data && typeof data === 'object' && data.id != null) return [data] return [] } function keepBigIntId(value) { if (value === undefined || value === null || value === '') return undefined const text = String(value).trim() if (!/^-?\d+$/.test(text)) return value if (!Number.isSafeInteger(Number(text))) return text return Number(text) } function extractId(result) { if (result == null) return null if (typeof result === 'object') return result.id ?? result.changeId ?? null return result } function parseSnapshot(raw) { if (!raw) return {} if (typeof raw === 'object') return raw try { return JSON.parse(raw) || {} } catch (error) { return {} } } function detailMap(details = []) { const map = {} const list = Array.isArray(details) ? details : [] list.forEach((item) => { const field = String(item?.changeField || '').toUpperCase() if (field) map[field] = item }) return map } // ============ 枚举(与 PC 端 src/api/hr/userChange.js 对齐)============ /** 异动类型 */ export const CHANGE_TYPE_LABELS = { POSITION_TRANSFER: '调岗', DEPARTMENT_TRANSFER: '调部门', GRADE_CHANGE: '职级调整', PROMOTION: '晋升', DEMOTION: '降级', SALARY_CHANGE: '调薪', MANAGER_CHANGE: '调整上级', WORK_LOCATION_CHANGE: '工作地点变更', FACTORY_TRANSFER: '调工厂', LEGAL_ENTITY_TRANSFER: '跨法人调动', EMPLOYMENT_TYPE_CHANGE: '用工类型变更', COMPOSITE_CHANGE: '综合异动', } /** 员工端可自助提交的异动类型,其余由 HR 在 PC 端办理 */ export const ESS_CHANGE_TYPE_KEYS = [ 'POSITION_TRANSFER', 'DEPARTMENT_TRANSFER', 'PROMOTION', 'GRADE_CHANGE', 'COMPOSITE_CHANGE', ] export const ESS_CHANGE_TYPE_OPTIONS = ESS_CHANGE_TYPE_KEYS.map((value) => ({ value, label: CHANGE_TYPE_LABELS[value], })) export function changeTypeLabel(value) { return labelOf(CHANGE_TYPE_LABELS, value, value || '调岗') } export const APPROVAL_STATUS_LABELS = { DRAFT: '草稿', PENDING: '审批中', APPROVING: '审批中', APPROVED: '已通过', REJECTED: '已驳回', WITHDRAWN: '已撤回', CANCELLED: '已取消', } export const EFFECTIVE_STATUS_LABELS = { PENDING: '待生效', SCHEDULED: '已排期', EFFECTIVE: '已生效', CANCELLED: '已取消', } /** 页面展示态 → 标签 / 样式(枚举定义在 data/mock.js) */ export const TRANSFER_DISPLAY_STATUS = userChangeStatus export const TRANSFER_STATUS_FILTERS = [ { value: 'all', label: '全部状态' }, { value: 'draft', label: '草稿' }, { value: 'pending', label: '审批中' }, { value: 'approved', label: '已通过' }, { value: 'rejected', label: '已驳回' }, ] // ============ 状态机 ============ export function deriveDocStatus(raw = {}) { const approval = String(raw.approvalStatus || '').toUpperCase() const effective = String(raw.effectiveStatus || '').toUpperCase() if (approval === 'CANCELLED' || effective === 'CANCELLED') return 'cancelled' if (approval === 'APPROVED' || effective === 'EFFECTIVE') return 'approved' if (approval === 'REJECTED') return 'rejected' if (approval === 'PENDING' || approval === 'APPROVING') return 'pending' if (approval === 'WITHDRAWN') return 'withdrawn' return 'draft' } export function canEditUserChange(row = {}) { const status = String(row.approvalStatus || '').toUpperCase() return status === 'DRAFT' || status === 'REJECTED' || status === '' } export function canSubmitUserChange(row = {}) { if (!row?.id) return false return ['draft', 'rejected'].includes(row.displayStatus) } export function canWithdrawUserChange(row = {}) { const status = String(row.approvalStatus || '').toUpperCase() return status === 'PENDING' || status === 'APPROVING' } export function canCancelUserChange(row = {}) { if (!row?.id) return false if (String(row.approvalStatus || '').toUpperCase() === 'CANCELLED') return false return String(row.effectiveStatus || '').toUpperCase() !== 'EFFECTIVE' } export function matchDisplayStatus(row = {}, filter = '') { if (!filter || filter === 'all') return true if (filter === 'rejected') return ['rejected', 'withdrawn', 'cancelled'].includes(row.displayStatus) return row.displayStatus === filter } function stageKeyOf(displayStatus) { if (displayStatus === 'approved') return 'effective' if (displayStatus === 'pending') return 'approval' return 'submit' } function progressOf(displayStatus, stageKey) { if (displayStatus === 'approved') return 100 if (['rejected', 'withdrawn', 'cancelled'].includes(displayStatus)) return 30 const index = Math.max( userChangeStages.findIndex((stage) => stage.key === stageKey), 0, ) return Math.round(((index + 1) / userChangeStages.length) * 100) } function progressTipOf(displayStatus) { switch (displayStatus) { case 'draft': return '草稿尚未提交,提交后进入审批流程。' case 'pending': return '调岗审批中,请等待主管与 HR 的审批结果。' case 'approved': return '调岗审批已通过,将按生效日期更新岗位信息。' case 'rejected': return '审批已驳回,可修改后重新提交。' case 'withdrawn': return '本次调岗申请已撤回。' case 'cancelled': return '本次调岗申请已取消。' default: return '' } } // ============ VO / 演示记录 → 页面形状 ============ export function adaptUserChange(raw = {}, extras = {}) { const before = parseSnapshot(raw.beforeSnapshot) const after = parseSnapshot(raw.afterSnapshot) const details = Array.isArray(raw.details) ? raw.details : [] const map = detailMap(details) const deptDetail = map.DEPT const positionDetail = map.POSITION const gradeDetail = map.GRADE const locationDetail = map.WORK_LOCATION const changeType = String(raw.changeType || '').toUpperCase() const approvalStatus = String(raw.approvalStatus || '').toUpperCase() const effectiveStatus = String(raw.effectiveStatus || '').toUpperCase() const displayStatus = deriveDocStatus({ approvalStatus, effectiveStatus }) const statusMeta = TRANSFER_DISPLAY_STATUS[displayStatus] || TRANSFER_DISPLAY_STATUS.draft const stageKey = stageKeyOf(displayStatus) const name = extras.name || raw.name || raw.employeeName || '' const dept = extras.dept || raw.dept || raw.originDept || before.deptName || deptDetail?.beforeText || '' const position = extras.position || raw.position || raw.originPosition || before.positionName || positionDetail?.beforeText || '' const targetDept = raw.targetDept || raw.targetDeptName || after.deptName || deptDetail?.afterText || '' const targetPosition = raw.targetPosition || raw.targetPositionName || after.positionName || positionDetail?.afterText || '' return { id: raw.id, changeNo: raw.changeNo || raw.contractNo || (raw.id == null ? '' : String(raw.id)), source: extras.source || raw.source || '', userId: raw.userId ?? extras.userId, employeeId: raw.userId ?? extras.employeeId, name, avatar: raw.avatar || String(name).charAt(0), employeeNo: extras.employeeNo || raw.employeeNo || raw.userNo || '', dept, position, level: extras.level || raw.level || before.positionLevelName || before.grade || gradeDetail?.beforeText || '', targetDept, targetDeptId: raw.targetDeptId ?? deptDetail?.afterValue ?? null, targetPosition, targetPositionId: raw.targetPositionId ?? positionDetail?.afterValue ?? null, targetLevel: raw.targetLevel || after.positionLevelName || after.grade || gradeDetail?.afterText || '', workLocation: raw.workLocation || locationDetail?.afterText || '', changeType, changeTypeLabel: changeTypeLabel(changeType), effectiveDate: dateOnly(raw.effectiveDate), applyDate: dateOnly(raw.applyDate || raw.createTime || raw.submitDate), reason: raw.reason || '', remark: raw.remark || '', approvalNode: raw.approvalNode || '', approvalComment: raw.approvalComment || raw.hrComment || '', approvalStatus, approvalStatusLabel: labelOf(APPROVAL_STATUS_LABELS, approvalStatus, '草稿'), effectiveStatus, effectiveStatusLabel: labelOf(EFFECTIVE_STATUS_LABELS, effectiveStatus, '-'), displayStatus, statusLabel: statusMeta.label, statusClass: statusMeta.class, stageKey, progress: progressOf(displayStatus, stageKey), progressTip: progressTipOf(displayStatus), details, createTime: raw.createTime || '', raw, } } // ============ 演示模式本地实现 ============ function mergeDemoRecords() { const overrides = getUserChangeStates() const created = getCreatedUserChanges() const seen = new Set(created.map((item) => String(item.id))) const base = [...created, ...userChanges.filter((item) => !seen.has(String(item.id)))] return base.map((record) => { const merged = { ...record, ...(overrides[String(record.id)] || {}) } return adaptUserChange(merged, { source: 'demo' }) }) } function updateDemoUserChange(id, patch) { setUserChangeLocalState(id, patch) updateCreatedUserChange(id, patch) return mergeDemoRecords().find((item) => String(item.id) === String(id)) || null } function persistDemoTransfer(id, form, submit) { const user = getCurrentUser() || {} const name = form.name || user.name || '' const record = { id: String(id), userId: form.userId || form.employeeId || user.id || '', name, avatar: form.avatar || String(name).charAt(0), employeeNo: form.employeeNo || user.id || '', dept: form.dept || user.department || '', position: form.position || user.position || '', level: form.level || '', targetDeptId: form.targetDeptId ?? null, targetDept: form.targetDeptName || form.targetDept || '', targetPositionId: form.targetPositionId ?? null, targetPosition: form.targetPositionName || form.targetPosition || '', targetLevel: form.targetLevel || '', changeType: inferChangeType(form), effectiveDate: dateOnly(form.effectiveDate), applyDate: dateOnly(formatNow()), reason: form.reason || '', remark: form.remark || '', approvalNode: submit ? '主管审批' : '', approvalStatus: submit ? 'PENDING' : 'DRAFT', effectiveStatus: 'PENDING', source: 'demo', createdByApp: true, } addCreatedUserChange(record) return record } // ============ 服务器接口(M02-人事异动)============ const employeeCache = new Map() async function fetchEmployeeBrief(userId) { if (userId === undefined || userId === null || userId === '') return null const key = String(userId) if (employeeCache.has(key)) return employeeCache.get(key) try { const res = await request({ url: `${serverPath()}/main/user/getById/${key}`, method: 'GET' }) const vo = res.data || {} const brief = { name: vo.name || vo.userName || '', employeeNo: vo.jobNumber || '', dept: vo.deptName || '', position: vo.postName || vo.position || '', level: vo.positionLevelName || vo.grade || '', } employeeCache.set(key, brief) return brief } catch (error) { employeeCache.set(key, null) return null } } function extrasOf(brief) { if (!brief) return {} return { name: brief.name, employeeNo: brief.employeeNo, dept: brief.dept, position: brief.position, level: brief.level, } } /** 当前登录人的服务端数字主键(登录响应里的 userId) */ export function currentUserId() { const login = getLoginUser() || {} const raw = getCurrentUser()?.raw || {} const candidates = [login.userId, login.id, raw.userId, raw.id] return candidates.find((value) => value !== undefined && value !== null && value !== '') } function isMine(row = {}) { const user = getCurrentUser() || {} const login = getLoginUser() || {} const mine = [user.id, user.name, login.userId, login.loginName, login.userName] .filter((value) => value !== undefined && value !== null && value !== '') .map(String) if (!mine.length) return true const rowIds = [row.userId, row.employeeNo, row.name] .filter((value) => value !== undefined && value !== null && value !== '') .map(String) return rowIds.some((value) => mine.includes(value)) } function transferText(row = {}) { return `${row.name}${row.employeeNo}${row.changeNo}${row.dept}${row.position}${row.targetDept}${row.targetPosition}` } /** 条件查询异动单 GET /main/user-changes */ export async function getUserChangePage(params = {}) { const res = await request({ url: `${serverPath()}/main/user-changes`, method: 'GET', data: compact({ pageNum: params.pageNum, size: params.size, userId: keepBigIntId(params.userId), changeType: params.changeType, approvalStatus: params.approvalStatus, }), }) const data = res.data || {} return { list: asList(data), count: Number(data?.count ?? data?.total ?? 0) } } /** 某员工的异动记录 GET /main/user-changes/users/{userId} */ export async function getUserChangesByUserId(userId) { const res = await request({ url: `${serverPath()}/main/user-changes/users/${userId}`, method: 'GET', }) return asList(res.data) } /** 异动单详情 GET /main/user-changes/{id} */ export async function getUserChangeById(id) { const res = await request({ url: `${serverPath()}/main/user-changes/${id}`, method: 'GET' }) return res.data } /** 新建异动单(草稿)POST /main/user-changes */ export async function createUserChange(payload) { const res = await request({ url: `${serverPath()}/main/user-changes`, method: 'POST', data: payload, }) const id = extractId(res.data) if (id != null) rememberUserChangeId(id) return id } /** 修改异动草稿 PUT /main/user-changes */ export async function updateUserChange(payload) { await request({ url: `${serverPath()}/main/user-changes`, method: 'PUT', data: payload }) if (payload?.id != null) rememberUserChangeId(payload.id) return payload.id } /** 异动预校验 POST /main/user-changes/{id}/validate */ export async function validateUserChange(id) { const res = await request({ url: `${serverPath()}/main/user-changes/${id}/validate`, method: 'POST', }) return res.data } /** 提交异动审批 POST /main/user-changes/{id}/submit */ export async function submitUserChange(id) { const res = await request({ url: `${serverPath()}/main/user-changes/${id}/submit`, method: 'POST', }) return res.data } /** 撤回异动审批 POST /main/user-changes/{id}/withdraw */ export async function withdrawUserChange(id, reason = '') { const res = await request({ url: `${serverPath()}/main/user-changes/${id}/withdraw`, method: 'POST', data: compact({ reason }), }) return res.data } /** 取消异动单 POST /main/user-changes/{id}/cancel */ export async function cancelUserChange(id, reason = '') { const res = await request({ url: `${serverPath()}/main/user-changes/${id}/cancel`, method: 'POST', data: compact({ reason }), }) return res.data } // ============ 对外统一入口(页面只调用这些)============ /** * 调岗列表。 * @param {object} params * - scope: 'mine' 只看本人;'all' 全部员工(HR 视图) * - status: TRANSFER_STATUS_FILTERS 的 value * - keyword: 姓名 / 工号 / 单号 / 岗位 模糊匹配 */ export async function loadTransferList(params = {}) { const { scope = 'mine', status = 'all', keyword = '', pageNum = 1, size = 50 } = params if (!isServerMode()) { const list = mergeDemoRecords() .filter((row) => (scope === 'mine' ? isMine(row) : true)) .filter((row) => matchDisplayStatus(row, status)) .filter((row) => (keyword ? transferText(row).includes(keyword) : true)) .sort((a, b) => String(b.applyDate || b.createTime).localeCompare(String(a.applyDate || a.createTime)), ) return { list, count: list.length, source: 'demo' } } const userId = currentUserId() let rows = [] if (scope === 'mine' && userId != null) { try { rows = await getUserChangesByUserId(userId) } catch (error) { rows = [] } } if (!rows.length) { try { const page = await getUserChangePage({ pageNum, size, userId: scope === 'mine' ? userId : undefined, }) rows = page.list } catch (error) { rows = [] } } // 集合接口不可用时,按本机记住的主键逐个拉详情兜底(与 PC 端策略一致) if (!rows.length) { const details = await Promise.all( getUserChangeIds().map(async (id) => { try { return await getUserChangeById(id) } catch (error) { return null } }), ) rows = details.filter(Boolean) } const list = [] for (const vo of rows) { if (!vo || vo.id == null) continue const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId)) const row = adaptUserChange(vo, extras) rememberUserChangeId(row.id) if (scope === 'mine' && userId != null && !isMine(row)) continue if (!matchDisplayStatus(row, status)) continue if (keyword && !transferText(row).includes(keyword)) continue list.push(row) } return { list, count: list.length, source: 'api' } } /** 调岗单详情 */ export async function loadTransferDetail(id) { if (!id) return null if (!isServerMode()) { return mergeDemoRecords().find((item) => String(item.id) === String(id)) || null } const vo = await getUserChangeById(id) if (!vo) return null rememberUserChangeId(id) const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId)) return adaptUserChange(vo, extras) } // ============ 员工端申请表单 ============ function inferChangeType(form = {}) { if (form.changeType) return form.changeType const hasDept = form.targetDeptId != null && form.targetDeptId !== '' const hasPosition = form.targetPositionId != null && form.targetPositionId !== '' if (hasDept && hasPosition) return 'COMPOSITE_CHANGE' if (hasDept) return 'DEPARTMENT_TRANSFER' return 'POSITION_TRANSFER' } function buildDetails(form = {}) { if (Array.isArray(form.details) && form.details.length) return form.details const details = [] if (form.targetDeptName) { details.push({ changeField: 'DEPT', beforeValue: form.originDeptId != null ? String(form.originDeptId) : undefined, beforeText: form.dept || '', afterValue: form.targetDeptId != null ? String(form.targetDeptId) : undefined, afterText: form.targetDeptName, }) } if (form.targetPositionName) { details.push({ changeField: 'POSITION', beforeValue: form.originPositionId != null ? String(form.originPositionId) : undefined, beforeText: form.position || '', afterValue: form.targetPositionId != null ? String(form.targetPositionId) : undefined, afterText: form.targetPositionName, }) } // 职级未变化时不产生 GRADE 变动,避免把岗位自带层级误判为职级调整 if (form.targetLevel && form.targetLevel !== form.level) { details.push({ changeField: 'GRADE', beforeValue: form.level || undefined, beforeText: form.level || '', afterValue: form.targetLevel, afterText: form.targetLevel, }) } return details } /** * 表单字段 → /main/user-changes 请求参数(与 PC 端 buildUserChangePayload 对齐)。 * 申请人由服务端按当前任职确定,前端只送变动目标与事由。 */ export function buildUserChangePayload(form = {}) { return compact({ id: keepBigIntId(form.id), userId: keepBigIntId(form.userId || form.employeeId), changeType: inferChangeType(form), effectiveDate: dateOnly(form.effectiveDate) || undefined, reason: form.reason, remark: form.remark, targetDeptId: keepBigIntId(form.targetDeptId), targetPositionId: keepBigIntId(form.targetPositionId), targetPositionName: form.targetPositionName || form.targetPosition, details: buildDetails(form), }) } /** * 提交(或存草稿)调岗申请。 * 服务器模式:建单 POST /main/user-changes → 提交 POST /{id}/submit * 演示模式:本地落一条异动单,提交时状态置为审批中 * * @returns {Promise} 异动单主键 */ export async function submitTransferApplication({ form, submit = true, existingId = null } = {}) { if (!form) throw new Error('申请参数为空') const id = existingId || form.id || null if (!isServerMode()) { await delay(300) const localId = id || `DC${Date.now()}` persistDemoTransfer(localId, form, submit) return localId } const payload = buildUserChangePayload({ ...form, id }) const changeId = id || (await createUserChange(payload)) if (id) await updateUserChange({ ...payload, id: changeId }) if (submit && changeId != null) await submitUserChange(changeId) return changeId } // —— 流程动作:服务器模式走接口,演示模式改本地态 —— export async function actSubmitTransfer(row) { if (!row?.id) throw new Error('缺少异动单主键') if (isServerMode()) { // 与 PC 端一致:先预校验,再提交审批 const result = await validateUserChange(row.id) if (result && result.valid === false) { throw new Error((result.messages || []).join(';') || '异动预校验未通过') } return submitUserChange(row.id) } return updateDemoUserChange(row.id, { approvalStatus: 'PENDING', approvalNode: '主管审批' }) } export async function actWithdrawTransfer(row, reason = '') { if (!row?.id) throw new Error('缺少异动单主键') if (isServerMode()) return withdrawUserChange(row.id, reason) return updateDemoUserChange(row.id, { approvalStatus: 'WITHDRAWN', approvalComment: reason, approvalNode: '', }) } export async function actCancelTransfer(row, reason = '') { if (!row?.id) throw new Error('缺少异动单主键') if (isServerMode()) return cancelUserChange(row.id, reason) return updateDemoUserChange(row.id, { approvalStatus: 'CANCELLED', effectiveStatus: 'CANCELLED', approvalComment: reason, approvalNode: '', }) }