Reuse invoice-wallets/invoices APIs with summary, upload, ledgers, and admin proxy upload. Co-authored-by: Cursor <cursoragent@cursor.com>
244 lines
7.4 KiB
JavaScript
244 lines
7.4 KiB
JavaScript
/**
|
||
* 报销/备用金模块 HTTP(bx 信封:code === 0)
|
||
* 对齐 bx-mobile/src/api/bxClient.ts
|
||
*/
|
||
import { API_BASE, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY } from '@/config/api'
|
||
|
||
const BX_COLLECTION_ROOT = /^\/(reimburses|advances|payments|workflows|expense-items)$/
|
||
|
||
function normalizeBxUrl(url) {
|
||
if (!url || url.endsWith('/')) return url
|
||
return BX_COLLECTION_ROOT.test(url) ? `${url}/` : url
|
||
}
|
||
|
||
function buildQuery(params) {
|
||
if (!params || typeof params !== 'object') return ''
|
||
const parts = []
|
||
Object.keys(params).forEach((key) => {
|
||
const val = params[key]
|
||
if (val === undefined || val === null || val === '') return
|
||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(val))}`)
|
||
})
|
||
return parts.length ? `?${parts.join('&')}` : ''
|
||
}
|
||
|
||
function parseDetailMessage(data) {
|
||
if (!data || typeof data !== 'object') return ''
|
||
if (typeof data.msg === 'string' && data.msg) return data.msg
|
||
const detail = data.detail
|
||
if (typeof detail === 'string') return detail
|
||
if (Array.isArray(detail)) {
|
||
return detail
|
||
.map((x) => (x && x.msg) || '')
|
||
.filter(Boolean)
|
||
.join(';')
|
||
}
|
||
return data.message || ''
|
||
}
|
||
|
||
async function onUnauthorized() {
|
||
uni.removeStorageSync(TOKEN_KEY)
|
||
uni.removeStorageSync(REFRESH_TOKEN_KEY)
|
||
uni.removeStorageSync(USER_INFO_KEY)
|
||
try {
|
||
const { useUserStore } = await import('@/stores/user')
|
||
useUserStore().clearLoginInfo()
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
|
||
setTimeout(() => {
|
||
uni.reLaunch({ url: '/pages/login/login' })
|
||
}, 800)
|
||
}
|
||
|
||
/**
|
||
* @param {{ url: string, method?: string, data?: any, params?: any, showError?: boolean }} options
|
||
* @returns {Promise<{ code: number, data: any, msg?: string }>}
|
||
*/
|
||
export function bxRequest(options = {}) {
|
||
const method = String(options.method || 'GET').toUpperCase()
|
||
const showError = options.showError !== false
|
||
const token = uni.getStorageSync(TOKEN_KEY) || ''
|
||
let path = normalizeBxUrl(options.url || '')
|
||
const query = buildQuery(options.params)
|
||
const rawUrl = `${path}${query}`
|
||
const url = rawUrl.startsWith('http')
|
||
? rawUrl
|
||
: `${API_BASE}${rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`}`
|
||
|
||
return new Promise((resolve, reject) => {
|
||
uni.request({
|
||
url,
|
||
method,
|
||
data: method === 'GET' ? undefined : options.data,
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||
},
|
||
success: (res) => {
|
||
const status = res.statusCode
|
||
const body = res.data
|
||
if (status === 401) {
|
||
onUnauthorized()
|
||
reject(new Error('登录已过期'))
|
||
return
|
||
}
|
||
if (status < 200 || status >= 300) {
|
||
const message = parseDetailMessage(body) || `请求失败(${status})`
|
||
if (showError) {
|
||
uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
|
||
}
|
||
reject(new Error(message))
|
||
return
|
||
}
|
||
if (body && body.code !== undefined && body.code !== 0) {
|
||
const message = body.msg || '请求失败'
|
||
if (showError) {
|
||
uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
|
||
}
|
||
reject(new Error(message))
|
||
return
|
||
}
|
||
resolve(body)
|
||
},
|
||
fail: (err) => {
|
||
if (showError) {
|
||
uni.showToast({ title: '网络异常', icon: 'none' })
|
||
}
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 附件 multipart 上传(uni.uploadFile)
|
||
* @param {string} filePath
|
||
* @param {{ bizType?: string, bizId?: string|number, showError?: boolean }} opts
|
||
*/
|
||
export function bxUploadFile(filePath, opts = {}) {
|
||
const showError = opts.showError !== false
|
||
const token = uni.getStorageSync(TOKEN_KEY) || ''
|
||
const query = buildQuery({
|
||
biz_type: opts.bizType || '',
|
||
biz_id: opts.bizId != null && opts.bizId !== '' ? opts.bizId : 0
|
||
})
|
||
const url = `${API_BASE}/attachments/upload${query}`
|
||
|
||
return new Promise((resolve, reject) => {
|
||
uni.uploadFile({
|
||
url,
|
||
filePath,
|
||
name: 'file',
|
||
header: {
|
||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||
},
|
||
success: (res) => {
|
||
const status = res.statusCode
|
||
let body = res.data
|
||
if (typeof body === 'string') {
|
||
try {
|
||
body = JSON.parse(body)
|
||
} catch (e) {
|
||
body = {}
|
||
}
|
||
}
|
||
if (status === 401) {
|
||
onUnauthorized()
|
||
reject(new Error('登录已过期'))
|
||
return
|
||
}
|
||
if (status < 200 || status >= 300) {
|
||
const message = parseDetailMessage(body) || `上传失败(${status})`
|
||
if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
|
||
reject(new Error(message))
|
||
return
|
||
}
|
||
if (body && body.code !== undefined && body.code !== 0) {
|
||
const message = body.msg || '上传失败'
|
||
if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
|
||
reject(new Error(message))
|
||
return
|
||
}
|
||
resolve(body)
|
||
},
|
||
fail: (err) => {
|
||
if (showError) uni.showToast({ title: '上传失败', icon: 'none' })
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 发票 multipart 上传(POST /invoices,name=files)
|
||
* 对齐 bx-mobile FormData:amount / remark / files / targetUserId
|
||
* @param {{ amount: number|string, remark?: string, filePath: string, targetUserId?: string|number, showError?: boolean }} opts
|
||
*/
|
||
export function bxUploadInvoice(opts = {}) {
|
||
const showError = opts.showError !== false
|
||
const token = uni.getStorageSync(TOKEN_KEY) || ''
|
||
const filePath = opts.filePath
|
||
if (!filePath) {
|
||
return Promise.reject(new Error('请选择发票文件'))
|
||
}
|
||
const formData = {
|
||
amount: String(opts.amount ?? '')
|
||
}
|
||
if (opts.remark != null && String(opts.remark).trim()) {
|
||
formData.remark = String(opts.remark).trim()
|
||
}
|
||
if (opts.targetUserId != null && opts.targetUserId !== '') {
|
||
formData.targetUserId = String(opts.targetUserId)
|
||
}
|
||
const url = `${API_BASE}/invoices`
|
||
|
||
return new Promise((resolve, reject) => {
|
||
uni.uploadFile({
|
||
url,
|
||
filePath,
|
||
name: 'files',
|
||
formData,
|
||
header: {
|
||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||
},
|
||
success: (res) => {
|
||
const status = res.statusCode
|
||
let body = res.data
|
||
if (typeof body === 'string') {
|
||
try {
|
||
body = JSON.parse(body)
|
||
} catch (e) {
|
||
body = {}
|
||
}
|
||
}
|
||
if (status === 401) {
|
||
onUnauthorized()
|
||
reject(new Error('登录已过期'))
|
||
return
|
||
}
|
||
if (status < 200 || status >= 300) {
|
||
const message = parseDetailMessage(body) || `上传失败(${status})`
|
||
if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
|
||
reject(new Error(message))
|
||
return
|
||
}
|
||
if (body && body.code !== undefined && body.code !== 0) {
|
||
const message = body.msg || '上传失败'
|
||
if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
|
||
reject(new Error(message))
|
||
return
|
||
}
|
||
resolve(body)
|
||
},
|
||
fail: (err) => {
|
||
if (showError) uni.showToast({ title: '上传失败', icon: 'none' })
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
export default bxRequest
|