/** * 报销/备用金模块 HTTP(bx 信封:code === 0) * 对齐 bx-mobile/src/api/bxClient.ts */ import { TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY, resolveApiBase } 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 : `${resolveApiBase()}${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 = `${resolveApiBase()}/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 = `${resolveApiBase()}/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