import { request } from '@/api/request.js' import { getBaseURL } from '@/api/config.js' import { getToken } from '@/utils/auth.js' /** * OA 报销管理接口封装,对应后端 /api/oa/expense/*。 * 与 Web 后台 /backend/reimbursements* 是同一套数据(yz_backend_reimbursements), * 状态机:0草稿 → 1审批中 → 2已通过 / 3已驳回 / 4已撤回 → 5已打款。 */ /** 报销单状态常量 */ export const EXPENSE_STATUS = { DRAFT: 0, APPROVING: 1, APPROVED: 2, REJECTED: 3, WITHDRAWN: 4, PAID: 5 } /** 状态选项(label 与 Web 端一致;type 为 u-tag 颜色) */ export const EXPENSE_STATUS_OPTIONS = [ { value: 0, label: '草稿', type: 'info' }, { value: 1, label: '审批中', type: 'primary' }, { value: 2, label: '已通过', type: 'success' }, { value: 3, label: '已驳回', type: 'error' }, { value: 4, label: '已撤回', type: 'warning' }, { value: 5, label: '已打款', type: 'success' } ] /** 审批记录动作文案(与 Web 端 detail 页 actionLabel 一致) */ export const EXPENSE_ACTION_LABELS = { 1: '提交申请', 2: '审批通过', 3: '审批驳回', 4: '转审', 5: '撤回' } export function getExpenseStatusLabel(status) { return EXPENSE_STATUS_OPTIONS.find(item => item.value === Number(status))?.label || '未知' } export function getExpenseStatusType(status) { return EXPENSE_STATUS_OPTIONS.find(item => item.value === Number(status))?.type || 'info' } export function getExpenseActionLabel(action) { return EXPENSE_ACTION_LABELS[Number(action)] || '审批操作' } /** 金额展示:统一两位小数 */ export function moneyText(value) { return Number(value || 0).toFixed(2) } /** * 报销单列表(支持状态/关键词筛选) * 返回 { list, total } */ export async function fetchExpenses(params = {}) { const data = await request({ url: '/api/oa/expense/list', data: { page: params.page || 1, pageSize: params.pageSize || 20, keyword: params.keyword || '', status: params.status ?? '' } }) return { list: data.list || [], total: data.total || 0 } } /** * 报销单详情(含费用明细与发票文件) */ export async function getExpense(id) { const data = await request({ url: `/api/oa/expense/detail/${id}` }) if (!data) return null return { ...data, items: (data.items || []).map(normalizeExpenseItem) } } /** * 新建报销单(草稿),返回新单据 id */ export async function createExpense(payload) { const res = await request({ url: '/api/oa/expense/create', method: 'POST', data: { apply_date: payload.applyDate, description: payload.description || '' } }) return res?.id } /** * 编辑报销单基本信息(仅草稿) */ export function updateExpense(id, payload) { return request({ url: `/api/oa/expense/update/${id}`, method: 'POST', data: { apply_date: payload.applyDate, description: payload.description || '' } }) } /** * 删除报销单(仅草稿,软删除) */ export async function deleteExpense(id) { await request({ url: `/api/oa/expense/delete/${id}`, method: 'DELETE' }) return true } /** * 新增费用明细(仅草稿) * item: { expenseType, expenseDate, amount, quantity, unitPrice, description, receipts: [{id,url,name}] } */ export async function addExpenseItem(reimbursementId, item) { return request({ url: `/api/oa/expense/item/add/${reimbursementId}`, method: 'POST', data: buildExpenseItemPayload(item) }) } /** * 更新费用明细(仅草稿) */ export async function updateExpenseItem(reimbursementId, itemId, item) { return request({ url: `/api/oa/expense/item/update/${reimbursementId}/${itemId}`, method: 'POST', data: buildExpenseItemPayload(item) }) } /** * 删除费用明细(仅草稿) */ export async function deleteExpenseItem(reimbursementId, itemId) { await request({ url: `/api/oa/expense/item/delete/${reimbursementId}/${itemId}`, method: 'DELETE' }) return true } /** * 提交审批(仅草稿;需租户已配置启用的审批流程) */ export async function submitExpense(id) { await request({ url: `/api/oa/expense/submit/${id}`, method: 'POST' }) return true } /** * 报销人确认款项到账(仅「已通过」的单据可用) * 确认成功后单据状态变为「已打款」;其它状态由审批引擎推进,端上不可手动修改。 */ export async function confirmExpensePaid(id) { const res = await request({ url: `/api/oa/expense/status/${id}`, method: 'POST', data: { status: EXPENSE_STATUS.PAID } }) return res?.status ?? EXPENSE_STATUS.PAID } /** * 审批记录(时间线) */ export async function fetchExpenseRecords(id) { const rows = await request({ url: `/api/oa/expense/records/${id}` }) return Array.isArray(rows) ? rows : [] } /** * 费用类型下拉(全局通用 + 本租户专有,仅启用) */ export async function fetchExpenseTypes() { const rows = await request({ url: '/api/oa/expense/types' }) return (Array.isArray(rows) ? rows : []).map(row => ({ id: row.id, name: row.name })) } /** * 上传报销发票附件(multipart,图片或 PDF,单个 ≤10MB) * 文件落入租户文件管理,返回 { id, url, name } 引用信息 */ export function uploadExpenseReceipt(filePath) { return new Promise((resolve, reject) => { uni.uploadFile({ url: `${getBaseURL().replace(/\/$/, '')}/api/oa/expense/upload`, filePath, name: 'file', header: { Authorization: `Bearer ${getToken()}` }, success(res) { let body = {} try { body = JSON.parse(res.data || '{}') } catch (e) { // 非 JSON 响应按失败处理 } if (res.statusCode >= 200 && res.statusCode < 300 && body.code === 200 && body.data) { resolve(body.data) } else { const msg = body.msg || `上传失败(${res.statusCode})` uni.showToast({ title: msg, icon: 'none' }) reject(new Error(msg)) } }, fail(err) { uni.showToast({ title: '网络异常,上传失败', icon: 'none' }) reject(err) } }) }) } /** * 组装后端要求的费用明细请求体(发票只传文件 ID 列表,发票金额暂不在端上维护)。 * * 不传发票挂靠字段:挂靠只允许挂「当前报销单」里上传的发票,而移动端保存是 * 「删旧重建」且不带 receipt_amounts(票面金额),重建期间发票池暂时为空或无票面, * 带上旧挂靠会导致校验失败并留下半重建数据。挂靠统一在 Web 端建立。 */ function buildExpenseItemPayload(item) { const receipts = item.receipts || [] return { expense_type: item.expenseType, expense_date: item.expenseDate, amount: Number(item.amount) || 0, quantity: Number(item.quantity) || 1, unit_price: Number(item.unitPrice) || 0, description: item.description || '', receipt_flag: receipts.length > 0 ? 1 : 0, receipt_file_ids: receipts.map(r => r.id).join(',') } } /** * 图片地址兜底:后端可能返回相对路径(如 /uploads/...), * 小程序 image 组件无法加载相对路径,统一拼上 baseURL。 */ export function resolveImageUrl(url) { const value = String(url || '').trim() if (!value) return '' if (/^https?:\/\//i.test(value) || value.startsWith('data:')) return value return `${getBaseURL().replace(/\/$/, '')}${value.startsWith('/') ? '' : '/'}${value}` } /** 查我的待审批任务(审批人视角):返回 {task_id,node_name},无待办返回 null */ export function fetchMyApproveTask(id) { return request({ url: `/api/oa/expense/my-approve-task/${id}` }) } /** 通过当前审批节点(body: {comment}),返回引擎提示(会签等待/流转/完成) */ export function approveExpense(id, comment = '') { return request({ url: `/api/oa/expense/approve/${id}`, method: 'POST', data: { comment } }) } /** 驳回报销单(body: {comment}) */ export function rejectExpense(id, comment = '') { return request({ url: `/api/oa/expense/reject/${id}`, method: 'POST', data: { comment } }) } /** * 发票引用是否为 PDF 文件:PDF 不能用 image 组件渲染(会被 ORB 拦截不显示), * 需要走 openDocument 打开。 */ export function isPdfReceipt(receipt) { const target = `${receipt?.name || ''} ${receipt?.url || ''}`.toLowerCase() return target.includes('.pdf') } /** 归一化明细:日期取前 10 位,发票引用统一为 {id,url,name}(后端 receipt_files 元素为 file_id) */ function normalizeExpenseItem(item) { const receipts = (Array.isArray(item.receipt_files) ? item.receipt_files : []).map(img => ({ id: img.file_id, url: resolveImageUrl(img.url), name: img.name || '' })) return { ...item, expense_date: String(item.expense_date || '').slice(0, 10), receipts } }