Files

300 lines
8.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}