feat(miniprogram): migrate advance apply list/form/detail from bx-mobile.

Reuse /advances APIs with bx envelope and Fresh Mint list/form styling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-11 23:20:54 +08:00
1 parent 507823dfce
commit 2ff754d572
9 files changed
+2264 -188

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
/** 对齐 bx-mobile/src/utils/format.ts(Batch2 所需子集) */
export function statusLabel(status) {
if (status == null || status === '') return '-'
const map = {
1: '启用',
0: '禁用',
draft: '待提交',
pending: '审批中',
approved: '已通过',
rejected: '已驳回',
withdrawn: '已撤回',
issued: '已发放',
paid: '已支付',
offset: '已冲抵',
partial: '部分冲抵',
processed: '已处理'
}
return map[status] ?? String(status)
}
export function statusTone(status) {
if (status == null) return 'info'
const map = {
1: 'done',
0: 'error',
draft: 'info',
pending: 'warn',
approved: 'done',
rejected: 'error',
withdrawn: 'warn',
issued: 'done',
paid: 'done',
offset: 'done',
partial: 'warn',
processed: 'done'
}
return map[status] ?? 'info'
}
export function workflowListStatusTagClass(status) {
const raw = status == null || status === '' ? 'draft' : String(status)
return `s-${raw}`
}
export function ticketHeaderStatusToneClass(status) {
const tone = statusTone(status)
const map = {
info: 'advance-ticket-status--info',
warn: 'advance-ticket-status--warn',
done: 'advance-ticket-status--done',
error: 'advance-ticket-status--err',
processing: 'advance-ticket-status--processing'
}
return map[tone] || 'advance-ticket-status--info'
}
export function formatAmount(amount) {
const n = typeof amount === 'string' ? parseFloat(amount) : amount
if (Number.isNaN(n)) return '0.00'
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
export function formatDate(dateStr) {
if (!dateStr) return '-'
return String(dateStr).substring(0, 10)
}
export function formatDateTime(dateStr) {
if (!dateStr) return '-'
return String(dateStr).substring(0, 16).replace('T', ' ')
}
export function pickApplyTime(row) {
if (!row || typeof row !== 'object') return undefined
const v = row.applyTime ?? row.createTime ?? row.apply_time ?? row.create_time
if (v == null || v === '') return undefined
return String(v)
}
export function formatApplyDateTime(row) {
return formatDateTime(pickApplyTime(row))
}
export function formatApplyDate(row) {
return formatDate(pickApplyTime(row))
}
+174
View File
@@ -0,0 +1,174 @@
/**
* 报销/备用金模块 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)
}
})
})
}
export default bxRequest