优化小程序报销模块
This commit is contained in:
1 parent
1e9666cefd
commit
48f3bd997c
21 files changed
+5738
-1750
No files matched your search
@@ -0,0 +1,277 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { request } from '@/api/request.js'
|
||||
import { getBaseURL } from '@/api/config.js'
|
||||
import { getToken } from '@/utils/auth.js'
|
||||
|
||||
/**
|
||||
* OA 工作日程(日历待办)接口封装,对应后端 /api/oa/schedule/*。
|
||||
* 数据表 yz_backend_oa_schedule,与「日程提醒」(/api/schedule/*,
|
||||
* yz_backend_schedule)是两个独立功能,不要混用。
|
||||
*/
|
||||
|
||||
/** 优先级选项(与 Web 后台一致:0 普通 / 1 重要 / 2 紧急) */
|
||||
export const OA_SCHEDULE_PRIORITY_OPTIONS = [
|
||||
{ value: 0, label: '普通' },
|
||||
{ value: 1, label: '重要' },
|
||||
{ value: 2, label: '紧急' }
|
||||
]
|
||||
|
||||
/** 归一化单条日程:补齐端上排序/分组用的时间戳与派生字段 */
|
||||
export function normalizeOaSchedule(item) {
|
||||
const date = String(item.schedule_date || '').slice(0, 10)
|
||||
const startTime = item.start_time || ''
|
||||
// 拼一个 Date 时间戳便于日历分组与时间排序(全天事件按 00:00)
|
||||
let timestamp = 0
|
||||
if (date) {
|
||||
const [y, m, d] = date.split('-').map(Number)
|
||||
const [hh = 0, mm = 0] = startTime ? startTime.split(':').map(Number) : [0, 0]
|
||||
timestamp = new Date(y, m - 1, d, hh, mm).getTime()
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
schedule_date: date,
|
||||
completed: item.status === 1,
|
||||
allDay: item.all_day === 1,
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作日程列表(支持日期区间 / 分类筛选)
|
||||
* 返回 { list, total, counts },counts 为各分类数量角标
|
||||
*/
|
||||
export async function fetchOaSchedules(params = {}) {
|
||||
const data = await request({
|
||||
url: '/api/oa/schedule/list',
|
||||
data: {
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 500,
|
||||
keyword: params.keyword || '',
|
||||
status: params.status || '',
|
||||
priority: params.priority || '',
|
||||
start_date: params.startDate || '',
|
||||
end_date: params.endDate || '',
|
||||
category: params.category || 'all'
|
||||
}
|
||||
})
|
||||
const list = (data.list || []).map(normalizeOaSchedule)
|
||||
return { list, total: data.total || 0, counts: data.counts || {} }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条日程详情
|
||||
*/
|
||||
export async function getOaSchedule(id) {
|
||||
const data = await request({ url: `/api/oa/schedule/detail/${id}` })
|
||||
if (!data || !data.item) return null
|
||||
return { ...normalizeOaSchedule(data.item), images: data.images || [] }
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建日程
|
||||
*/
|
||||
export async function createOaSchedule(payload) {
|
||||
return request({
|
||||
url: '/api/oa/schedule/create',
|
||||
method: 'POST',
|
||||
data: buildOaSchedulePayload(payload)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新日程
|
||||
*/
|
||||
export async function updateOaSchedule(id, payload) {
|
||||
return request({
|
||||
url: `/api/oa/schedule/update/${id}`,
|
||||
method: 'POST',
|
||||
data: buildOaSchedulePayload(payload)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除日程(软删除)
|
||||
*/
|
||||
export async function deleteOaSchedule(id) {
|
||||
await request({ url: `/api/oa/schedule/delete/${id}`, method: 'DELETE' })
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换完成状态(待办 <-> 已完成)
|
||||
*/
|
||||
export async function toggleOaScheduleComplete(id) {
|
||||
const res = await request({ url: `/api/oa/schedule/finish/${id}`, method: 'POST' })
|
||||
return { status: res?.status ?? 0, completed: res?.status === 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传日程资料图片(multipart,单张 ≤10MB)
|
||||
* 文件落入租户文件管理,返回 { id, url, name } 引用信息
|
||||
*/
|
||||
export function uploadOaScheduleImage(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${getBaseURL().replace(/\/$/, '')}/api/oa/schedule/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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存日程资料图片引用列表(整体覆盖)
|
||||
*/
|
||||
export async function setOaScheduleImages(id, images) {
|
||||
const res = await request({
|
||||
url: `/api/oa/schedule/images/${id}`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
images: (images || []).map(img => ({
|
||||
id: Number(img.id) || 0,
|
||||
url: img.url,
|
||||
name: img.name || ''
|
||||
}))
|
||||
}
|
||||
})
|
||||
return res?.images || []
|
||||
}
|
||||
|
||||
/** 组装后端要求的请求体(提醒绑定暂不在小程序端开放) */
|
||||
function buildOaSchedulePayload(p) {
|
||||
return {
|
||||
title: p.title,
|
||||
content: p.content || '',
|
||||
schedule_date: p.scheduleDate,
|
||||
start_time: p.allDay ? '' : p.startTime || '',
|
||||
end_time: p.allDay ? '' : p.endTime || '',
|
||||
all_day: p.allDay ? 1 : 0,
|
||||
priority: p.priority || 0,
|
||||
color: p.color || '#409EFF',
|
||||
reminder_enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
export function getOaPriorityLabel(priority) {
|
||||
return (
|
||||
OA_SCHEDULE_PRIORITY_OPTIONS.find(item => item.value === Number(priority))?.label || '普通'
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user