request.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /**
  2. * 统一 HTTP 请求封装
  3. *
  4. * 功能:
  5. * 1. 自动携带 Token
  6. * 2. 统一 Loading 控制 (支持开启/关闭)
  7. * 3. 统一错误处理 (支持开启/关闭)
  8. * 4. 请求/响应拦截器
  9. * 5. 支持单次请求覆盖全局配置
  10. *
  11. * 用法:
  12. * import http from '@/utils/http'
  13. *
  14. * // 基本请求
  15. * const res = await http.get('/user/info')
  16. *
  17. * // 关闭 loading
  18. * const res = await http.post('/login', data, { showLoading: false })
  19. *
  20. * // 关闭错误提示
  21. * const res = await http.get('/check', { showError: false })
  22. *
  23. * // 不需要 token
  24. * const res = await http.post('/login', data, { withToken: false })
  25. */
  26. import config from './config.js'
  27. import toast from '@/utils/toast.js'
  28. // ============== Token 管理 ==============
  29. /** 获取本地存储的 Token */
  30. export function getToken() {
  31. return uni.getStorageSync(config.tokenStorageKey) || ''
  32. }
  33. /** 设置 Token */
  34. export function setToken(token) {
  35. uni.setStorageSync(config.tokenStorageKey, token || '')
  36. }
  37. /** 移除 Token */
  38. export function removeToken() {
  39. uni.removeStorageSync(config.tokenStorageKey)
  40. }
  41. // ============== Loading 计数器 ==============
  42. let loadingCount = 0
  43. let loadingTimer = null
  44. function showLoading(title = '加载中...') {
  45. if (loadingCount === 0) {
  46. // 延迟 300ms 显示,避免快速请求闪烁
  47. loadingTimer = setTimeout(() => {
  48. toast.loading(title)
  49. }, 300)
  50. }
  51. loadingCount++
  52. }
  53. function hideLoading() {
  54. loadingCount = Math.max(0, loadingCount - 1)
  55. if (loadingCount === 0) {
  56. clearTimeout(loadingTimer)
  57. loadingTimer = null
  58. toast.hide()
  59. }
  60. }
  61. // ============== 错误处理 ==============
  62. function handleError(message, statusCode) {
  63. console.log('handleError', message, statusCode)
  64. // 延迟弹出,避免被 complete 回调中的 hideLoading (toast.hide) 立即关闭
  65. setTimeout(() => {
  66. toast.error(message || '请求失败')
  67. }, 150)
  68. }
  69. function handleAuthError() {
  70. removeToken()
  71. const pages = getCurrentPages()
  72. const currentPath = pages.length ? '/' + pages[pages.length - 1].route : ''
  73. // 避免在登录页重复跳转
  74. if (currentPath !== config.loginPage) {
  75. uni.navigateTo({
  76. url: config.loginPage,
  77. fail: () => {
  78. // navigateTo 失败时尝试 reLaunch
  79. uni.reLaunch({ url: config.loginPage })
  80. }
  81. })
  82. }
  83. }
  84. // ============== 拦截器 ==============
  85. const interceptors = {
  86. request: [],
  87. response: []
  88. }
  89. /**
  90. * 添加请求拦截器
  91. * @param {Function} handler (config) => config
  92. */
  93. export function addRequestInterceptor(handler) {
  94. if (typeof handler === 'function') {
  95. interceptors.request.push(handler)
  96. }
  97. }
  98. /**
  99. * 添加响应拦截器
  100. * @param {Function} handler (response) => response
  101. */
  102. export function addResponseInterceptor(handler) {
  103. if (typeof handler === 'function') {
  104. interceptors.response.push(handler)
  105. }
  106. }
  107. // ============== 核心请求方法 ==============
  108. /**
  109. * 发起 HTTP 请求
  110. * @param {Object} options 请求选项
  111. * @param {string} options.url - 接口路径 (会自动拼接 baseUrl)
  112. * @param {string} [options.method='GET'] - 请求方法
  113. * @param {Object} [options.data] - 请求数据
  114. * @param {Object} [options.header] - 自定义请求头
  115. * @param {boolean} [options.showLoading=true] - 是否显示 loading
  116. * @param {string} [options.loadingText='加载中...'] - loading 文字
  117. * @param {boolean} [options.showError=true] - 是否显示错误提示
  118. * @param {boolean} [options.withToken=true] - 是否携带 token
  119. * @param {string} [options.baseUrl] - 覆盖全局 baseUrl
  120. * @param {number} [options.timeout] - 覆盖全局超时
  121. * @param {string} [options.responseType] - 响应类型
  122. * @param {string} [options.dataType='json'] - 数据类型
  123. * @returns {Promise<any>} 响应数据 (默认返回 response.data.data)
  124. */
  125. function request(options = {}) {
  126. let {
  127. url = '',
  128. method = 'GET',
  129. data,
  130. header = {},
  131. showLoading: needLoading = true,
  132. loadingText = '加载中...',
  133. showError: needError = true,
  134. withToken = true,
  135. baseUrl: customBaseUrl,
  136. timeout: customTimeout,
  137. responseType,
  138. dataType = 'json'
  139. } = options
  140. // 拼接完整 URL
  141. const base = customBaseUrl !== undefined ? customBaseUrl : config.baseUrl
  142. console.log(base)
  143. if (url && !url.startsWith('http')) {
  144. url = base + url
  145. }
  146. // 合并 header
  147. const mergedHeader = { ...config.headers, ...header }
  148. // 注入 Token
  149. if (withToken) {
  150. const token = getToken()
  151. if (token) {
  152. mergedHeader[config.tokenKey] = config.tokenPrefix + token
  153. }
  154. }
  155. // 构建请求配置
  156. let reqConfig = {
  157. url,
  158. method: method.toUpperCase(),
  159. data,
  160. header: mergedHeader,
  161. timeout: customTimeout || config.timeout,
  162. dataType,
  163. responseType
  164. }
  165. // 执行请求拦截器
  166. for (const handler of interceptors.request) {
  167. reqConfig = handler(reqConfig) || reqConfig
  168. }
  169. // 显示 Loading
  170. if (needLoading) showLoading(loadingText)
  171. return new Promise((resolve, reject) => {
  172. uni.request({
  173. ...reqConfig,
  174. success: (res) => {
  175. console.log(reqConfig,"返回数据",res)
  176. // 执行响应拦截器
  177. let response = res
  178. for (const handler of interceptors.response) {
  179. response = handler(response) || response
  180. }
  181. const { statusCode, data: resData } = response
  182. console.log("返回数据2",response)
  183. // HTTP 状态码判断
  184. if (statusCode >= 200 && statusCode < 300) {
  185. // 业务逻辑判断 (适配 { code, data, message } 格式)
  186. if (resData && typeof resData === 'object' && 'code' in resData) {
  187. if (resData.code === config.successCode) {
  188. resolve(resData.data !== undefined ? resData.data : resData)
  189. } else if (config.authErrorCodes.includes(resData.code)) {
  190. // 业务层返回的认证错误
  191. handleAuthError()
  192. if (needError) handleError(resData.message || '登录已过期,请重新登录')
  193. reject(new Error(resData.message || 'AUTH_ERROR'))
  194. } else {
  195. // 业务错误
  196. if (needError) handleError(resData.message || '操作失败')
  197. reject(new Error(resData.message || 'BUSINESS_ERROR'))
  198. }
  199. } else {
  200. // 非标准格式,直接返回
  201. resolve(resData)
  202. }
  203. } else if (config.authErrorCodes.includes(statusCode)) {
  204. // HTTP 401/403
  205. handleAuthError()
  206. if (needError) handleError('登录已过期,请重新登录')
  207. reject(new Error('AUTH_ERROR'))
  208. } else {
  209. // 其他 HTTP 错误
  210. const msg = getHttpErrorMessage(statusCode)
  211. if (needError) handleError(msg)
  212. reject(new Error(msg))
  213. }
  214. },
  215. fail: (err) => {
  216. console.error(err)
  217. // 网络错误 / 超时
  218. const msg = err.errMsg || ''
  219. let errorText = '网络异常,请检查网络连接'
  220. if (msg.includes('timeout')) {
  221. errorText = '请求超时,请稍后重试'
  222. } else if (msg.includes('abort')) {
  223. errorText = '请求已取消'
  224. }
  225. if (needError) handleError(errorText)
  226. reject(new Error(errorText))
  227. },
  228. complete: () => {
  229. if (needLoading) hideLoading()
  230. }
  231. })
  232. })
  233. }
  234. // ============== HTTP 状态码映射 ==============
  235. function getHttpErrorMessage(statusCode) {
  236. const messages = {
  237. 400: '请求参数错误',
  238. 401: '未授权,请重新登录',
  239. 403: '拒绝访问',
  240. 404: '请求资源不存在',
  241. 405: '请求方法不允许',
  242. 408: '请求超时',
  243. 500: '服务器内部错误',
  244. 502: '网关错误',
  245. 503: '服务不可用',
  246. 504: '网关超时'
  247. }
  248. return messages[statusCode] || `请求失败 (${statusCode})`
  249. }
  250. // ============== 便捷方法 ==============
  251. function get(url, options = {}) {
  252. return request({ ...options, url, method: 'GET' })
  253. }
  254. function post(url, data, options = {}) {
  255. return request({ ...options, url, method: 'POST', data })
  256. }
  257. function put(url, data, options = {}) {
  258. return request({ ...options, url, method: 'PUT', data })
  259. }
  260. function del(url, data, options = {}) {
  261. return request({ ...options, url, method: 'DELETE', data })
  262. }
  263. /** 文件上传 */
  264. function upload(url, options = {}) {
  265. const {
  266. filePath,
  267. name = 'file',
  268. formData = {},
  269. header = {},
  270. showLoading: needLoading = true,
  271. loadingText = '上传中...',
  272. showError: needError = true,
  273. withToken = true
  274. } = options
  275. const mergedHeader = { ...header }
  276. if (withToken) {
  277. const token = getToken()
  278. if (token) {
  279. mergedHeader[config.tokenKey] = config.tokenPrefix + token
  280. }
  281. }
  282. const base = config.baseUrl
  283. const fullUrl = url.startsWith('http') ? url : base + url
  284. if (needLoading) showLoading(loadingText)
  285. return new Promise((resolve, reject) => {
  286. uni.uploadFile({
  287. url: fullUrl,
  288. filePath,
  289. name,
  290. formData,
  291. header: mergedHeader,
  292. success: (res) => {
  293. let resData = res.data
  294. // uploadFile 返回的 data 是 string,需要手动解析
  295. if (typeof resData === 'string') {
  296. try { resData = JSON.parse(resData) } catch (_) {}
  297. }
  298. if (resData && resData.code === config.successCode) {
  299. resolve(resData.data !== undefined ? resData.data : resData)
  300. } else {
  301. const msg = (resData && resData.message) || '上传失败'
  302. if (needError) handleError(msg)
  303. reject(new Error(msg))
  304. }
  305. },
  306. fail: (err) => {
  307. if (needError) handleError('上传失败,请检查网络')
  308. reject(new Error(err.errMsg || '上传失败'))
  309. },
  310. complete: () => {
  311. if (needLoading) hideLoading()
  312. }
  313. })
  314. })
  315. }
  316. // ============== 导出 ==============
  317. export default {
  318. request,
  319. get,
  320. post,
  321. put,
  322. delete: del,
  323. upload,
  324. getToken,
  325. setToken,
  326. removeToken,
  327. addRequestInterceptor,
  328. addResponseInterceptor,
  329. config
  330. }