request.js 9.6 KB

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