Files
yunzerwebsiteallinone/uniapp/api/expense.js
T

278 lines
7.5 KiB
JavaScript
Raw 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
}
/**
* 手动确认更改单据状态(与 Web 端「手动确认」能力一致)
*/
export async function updateExpenseStatus(id, status) {
const res = await request({
url: `/api/oa/expense/status/${id}`,
method: 'POST',
data: { status }
})
return res?.status
}
/**
* 审批记录(时间线)
*/
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 列表,发票金额暂不在端上维护) */
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}`
}
/**
* 发票引用是否为 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
}
}