bxRequest.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /**
  2. * 报销/备用金模块 HTTP(bx 信封:code === 0)
  3. * 对齐 bx-mobile/src/api/bxClient.ts
  4. */
  5. import { TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY, resolveApiBase } from '@/config/api'
  6. const BX_COLLECTION_ROOT = /^\/(reimburses|advances|payments|workflows|expense-items)$/
  7. function normalizeBxUrl(url) {
  8. if (!url || url.endsWith('/')) return url
  9. return BX_COLLECTION_ROOT.test(url) ? `${url}/` : url
  10. }
  11. function buildQuery(params) {
  12. if (!params || typeof params !== 'object') return ''
  13. const parts = []
  14. Object.keys(params).forEach((key) => {
  15. const val = params[key]
  16. if (val === undefined || val === null || val === '') return
  17. parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(val))}`)
  18. })
  19. return parts.length ? `?${parts.join('&')}` : ''
  20. }
  21. function parseDetailMessage(data) {
  22. if (!data || typeof data !== 'object') return ''
  23. if (typeof data.msg === 'string' && data.msg) return data.msg
  24. const detail = data.detail
  25. if (typeof detail === 'string') return detail
  26. if (Array.isArray(detail)) {
  27. return detail
  28. .map((x) => (x && x.msg) || '')
  29. .filter(Boolean)
  30. .join(';')
  31. }
  32. return data.message || ''
  33. }
  34. async function onUnauthorized() {
  35. uni.removeStorageSync(TOKEN_KEY)
  36. uni.removeStorageSync(REFRESH_TOKEN_KEY)
  37. uni.removeStorageSync(USER_INFO_KEY)
  38. try {
  39. const { useUserStore } = await import('@/stores/user')
  40. useUserStore().clearLoginInfo()
  41. } catch (e) {
  42. // ignore
  43. }
  44. uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
  45. setTimeout(() => {
  46. uni.reLaunch({ url: '/pages/login/login' })
  47. }, 800)
  48. }
  49. /**
  50. * @param {{ url: string, method?: string, data?: any, params?: any, showError?: boolean }} options
  51. * @returns {Promise<{ code: number, data: any, msg?: string }>}
  52. */
  53. export function bxRequest(options = {}) {
  54. const method = String(options.method || 'GET').toUpperCase()
  55. const showError = options.showError !== false
  56. const token = uni.getStorageSync(TOKEN_KEY) || ''
  57. let path = normalizeBxUrl(options.url || '')
  58. const query = buildQuery(options.params)
  59. const rawUrl = `${path}${query}`
  60. const url = rawUrl.startsWith('http')
  61. ? rawUrl
  62. : `${resolveApiBase()}${rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`}`
  63. return new Promise((resolve, reject) => {
  64. uni.request({
  65. url,
  66. method,
  67. data: method === 'GET' ? undefined : options.data,
  68. header: {
  69. 'Content-Type': 'application/json',
  70. ...(token ? { Authorization: `Bearer ${token}` } : {})
  71. },
  72. success: (res) => {
  73. const status = res.statusCode
  74. const body = res.data
  75. if (status === 401) {
  76. onUnauthorized()
  77. reject(new Error('登录已过期'))
  78. return
  79. }
  80. if (status < 200 || status >= 300) {
  81. const message = parseDetailMessage(body) || `请求失败(${status})`
  82. if (showError) {
  83. uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
  84. }
  85. reject(new Error(message))
  86. return
  87. }
  88. if (body && body.code !== undefined && body.code !== 0) {
  89. const message = body.msg || '请求失败'
  90. if (showError) {
  91. uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
  92. }
  93. reject(new Error(message))
  94. return
  95. }
  96. resolve(body)
  97. },
  98. fail: (err) => {
  99. if (showError) {
  100. uni.showToast({ title: '网络异常', icon: 'none' })
  101. }
  102. reject(err)
  103. }
  104. })
  105. })
  106. }
  107. /**
  108. * 附件 multipart 上传(uni.uploadFile)
  109. * @param {string} filePath
  110. * @param {{ bizType?: string, bizId?: string|number, showError?: boolean }} opts
  111. */
  112. export function bxUploadFile(filePath, opts = {}) {
  113. const showError = opts.showError !== false
  114. const token = uni.getStorageSync(TOKEN_KEY) || ''
  115. const query = buildQuery({
  116. biz_type: opts.bizType || '',
  117. biz_id: opts.bizId != null && opts.bizId !== '' ? opts.bizId : 0
  118. })
  119. const url = `${resolveApiBase()}/attachments/upload${query}`
  120. return new Promise((resolve, reject) => {
  121. uni.uploadFile({
  122. url,
  123. filePath,
  124. name: 'file',
  125. header: {
  126. ...(token ? { Authorization: `Bearer ${token}` } : {})
  127. },
  128. success: (res) => {
  129. const status = res.statusCode
  130. let body = res.data
  131. if (typeof body === 'string') {
  132. try {
  133. body = JSON.parse(body)
  134. } catch (e) {
  135. body = {}
  136. }
  137. }
  138. if (status === 401) {
  139. onUnauthorized()
  140. reject(new Error('登录已过期'))
  141. return
  142. }
  143. if (status < 200 || status >= 300) {
  144. const message = parseDetailMessage(body) || `上传失败(${status})`
  145. if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
  146. reject(new Error(message))
  147. return
  148. }
  149. if (body && body.code !== undefined && body.code !== 0) {
  150. const message = body.msg || '上传失败'
  151. if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
  152. reject(new Error(message))
  153. return
  154. }
  155. resolve(body)
  156. },
  157. fail: (err) => {
  158. if (showError) uni.showToast({ title: '上传失败', icon: 'none' })
  159. reject(err)
  160. }
  161. })
  162. })
  163. }
  164. /**
  165. * 发票 multipart 上传(POST /invoices,name=files)
  166. * 对齐 bx-mobile FormData:amount / remark / files / targetUserId
  167. * @param {{ amount: number|string, remark?: string, filePath: string, targetUserId?: string|number, showError?: boolean }} opts
  168. */
  169. export function bxUploadInvoice(opts = {}) {
  170. const showError = opts.showError !== false
  171. const token = uni.getStorageSync(TOKEN_KEY) || ''
  172. const filePath = opts.filePath
  173. if (!filePath) {
  174. return Promise.reject(new Error('请选择发票文件'))
  175. }
  176. const formData = {
  177. amount: String(opts.amount ?? '')
  178. }
  179. if (opts.remark != null && String(opts.remark).trim()) {
  180. formData.remark = String(opts.remark).trim()
  181. }
  182. if (opts.targetUserId != null && opts.targetUserId !== '') {
  183. formData.targetUserId = String(opts.targetUserId)
  184. }
  185. const url = `${resolveApiBase()}/invoices`
  186. return new Promise((resolve, reject) => {
  187. uni.uploadFile({
  188. url,
  189. filePath,
  190. name: 'files',
  191. formData,
  192. header: {
  193. ...(token ? { Authorization: `Bearer ${token}` } : {})
  194. },
  195. success: (res) => {
  196. const status = res.statusCode
  197. let body = res.data
  198. if (typeof body === 'string') {
  199. try {
  200. body = JSON.parse(body)
  201. } catch (e) {
  202. body = {}
  203. }
  204. }
  205. if (status === 401) {
  206. onUnauthorized()
  207. reject(new Error('登录已过期'))
  208. return
  209. }
  210. if (status < 200 || status >= 300) {
  211. const message = parseDetailMessage(body) || `上传失败(${status})`
  212. if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
  213. reject(new Error(message))
  214. return
  215. }
  216. if (body && body.code !== undefined && body.code !== 0) {
  217. const message = body.msg || '上传失败'
  218. if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
  219. reject(new Error(message))
  220. return
  221. }
  222. resolve(body)
  223. },
  224. fail: (err) => {
  225. if (showError) uni.showToast({ title: '上传失败', icon: 'none' })
  226. reject(err)
  227. }
  228. })
  229. })
  230. }
  231. export default bxRequest