| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- /**
- * 统一 HTTP 请求封装
- *
- * 功能:
- * 1. 自动携带 Token
- * 2. 统一 Loading 控制 (支持开启/关闭)
- * 3. 统一错误处理 (支持开启/关闭)
- * 4. 请求/响应拦截器
- * 5. 支持单次请求覆盖全局配置
- *
- * 用法:
- * import http from '@/utils/http'
- *
- * // 基本请求
- * const res = await http.get('/user/info')
- *
- * // 关闭 loading
- * const res = await http.post('/login', data, { showLoading: false })
- *
- * // 关闭错误提示
- * const res = await http.get('/check', { showError: false })
- *
- * // 不需要 token
- * const res = await http.post('/login', data, { withToken: false })
- */
- import config from './config.js'
- import toast from '@/utils/toast.js'
- // ============== Token 管理 ==============
- /** 获取本地存储的 Token */
- export function getToken() {
- return uni.getStorageSync(config.tokenStorageKey) || ''
- }
- /** 设置 Token */
- export function setToken(token) {
- uni.setStorageSync(config.tokenStorageKey, token || '')
- }
- /** 移除 Token */
- export function removeToken() {
- uni.removeStorageSync(config.tokenStorageKey)
- }
- // ============== Loading 计数器 ==============
- let loadingCount = 0
- let loadingTimer = null
- function showLoading(title = '加载中...') {
- if (loadingCount === 0) {
- // 延迟 300ms 显示,避免快速请求闪烁
- loadingTimer = setTimeout(() => {
- toast.loading(title)
- }, 300)
- }
- loadingCount++
- }
- function hideLoading() {
- loadingCount = Math.max(0, loadingCount - 1)
- if (loadingCount === 0) {
- clearTimeout(loadingTimer)
- loadingTimer = null
- toast.hide()
- }
- }
- // ============== 错误处理 ==============
- function handleError(message, statusCode) {
- toast.error(message || '请求失败')
- }
- function handleAuthError() {
- removeToken()
- const pages = getCurrentPages()
- const currentPath = pages.length ? '/' + pages[pages.length - 1].route : ''
- // 避免在登录页重复跳转
- if (currentPath !== config.loginPage) {
- uni.navigateTo({
- url: config.loginPage,
- fail: () => {
- // navigateTo 失败时尝试 reLaunch
- uni.reLaunch({ url: config.loginPage })
- }
- })
- }
- }
- // ============== 拦截器 ==============
- const interceptors = {
- request: [],
- response: []
- }
- /**
- * 添加请求拦截器
- * @param {Function} handler (config) => config
- */
- export function addRequestInterceptor(handler) {
- if (typeof handler === 'function') {
- interceptors.request.push(handler)
- }
- }
- /**
- * 添加响应拦截器
- * @param {Function} handler (response) => response
- */
- export function addResponseInterceptor(handler) {
- if (typeof handler === 'function') {
- interceptors.response.push(handler)
- }
- }
- // ============== 核心请求方法 ==============
- /**
- * 发起 HTTP 请求
- * @param {Object} options 请求选项
- * @param {string} options.url - 接口路径 (会自动拼接 baseUrl)
- * @param {string} [options.method='GET'] - 请求方法
- * @param {Object} [options.data] - 请求数据
- * @param {Object} [options.header] - 自定义请求头
- * @param {boolean} [options.showLoading=true] - 是否显示 loading
- * @param {string} [options.loadingText='加载中...'] - loading 文字
- * @param {boolean} [options.showError=true] - 是否显示错误提示
- * @param {boolean} [options.withToken=true] - 是否携带 token
- * @param {string} [options.baseUrl] - 覆盖全局 baseUrl
- * @param {number} [options.timeout] - 覆盖全局超时
- * @param {string} [options.responseType] - 响应类型
- * @param {string} [options.dataType='json'] - 数据类型
- * @returns {Promise<any>} 响应数据 (默认返回 response.data.data)
- */
- function request(options = {}) {
- let {
- url = '',
- method = 'GET',
- data,
- header = {},
- showLoading: needLoading = true,
- loadingText = '加载中...',
- showError: needError = true,
- withToken = true,
- baseUrl: customBaseUrl,
- timeout: customTimeout,
- responseType,
- dataType = 'json'
- } = options
- // 拼接完整 URL
- const base = customBaseUrl !== undefined ? customBaseUrl : config.baseUrl
- console.log(base)
- if (url && !url.startsWith('http')) {
- url = base + url
- }
- // 合并 header
- const mergedHeader = { ...config.headers, ...header }
- // 注入 Token
- if (withToken) {
- const token = getToken()
- if (token) {
- mergedHeader[config.tokenKey] = config.tokenPrefix + token
- }
- }
- // 构建请求配置
- let reqConfig = {
- url,
- method: method.toUpperCase(),
- data,
- header: mergedHeader,
- timeout: customTimeout || config.timeout,
- dataType,
- responseType
- }
- // 执行请求拦截器
- for (const handler of interceptors.request) {
- reqConfig = handler(reqConfig) || reqConfig
- }
- // 显示 Loading
- if (needLoading) showLoading(loadingText)
- return new Promise((resolve, reject) => {
- uni.request({
- ...reqConfig,
- success: (res) => {
- // 执行响应拦截器
- let response = res
- for (const handler of interceptors.response) {
- response = handler(response) || response
- }
- const { statusCode, data: resData } = response
- // HTTP 状态码判断
- if (statusCode >= 200 && statusCode < 300) {
- // 业务逻辑判断 (适配 { code, data, message } 格式)
- if (resData && typeof resData === 'object' && 'code' in resData) {
- if (resData.code === config.successCode) {
- resolve(resData.data !== undefined ? resData.data : resData)
- } else if (config.authErrorCodes.includes(resData.code)) {
- // 业务层返回的认证错误
- handleAuthError()
- if (needError) handleError(resData.message || '登录已过期,请重新登录')
- reject(new Error(resData.message || 'AUTH_ERROR'))
- } else {
- // 业务错误
- if (needError) handleError(resData.message || '操作失败')
- reject(new Error(resData.message || 'BUSINESS_ERROR'))
- }
- } else {
- // 非标准格式,直接返回
- resolve(resData)
- }
- } else if (config.authErrorCodes.includes(statusCode)) {
- // HTTP 401/403
- handleAuthError()
- if (needError) handleError('登录已过期,请重新登录')
- reject(new Error('AUTH_ERROR'))
- } else {
- // 其他 HTTP 错误
- const msg = getHttpErrorMessage(statusCode)
- if (needError) handleError(msg)
- reject(new Error(msg))
- }
- },
- fail: (err) => {
- console.error(err)
- // 网络错误 / 超时
- const msg = err.errMsg || ''
- let errorText = '网络异常,请检查网络连接'
- if (msg.includes('timeout')) {
- errorText = '请求超时,请稍后重试'
- } else if (msg.includes('abort')) {
- errorText = '请求已取消'
- }
- if (needError) handleError(errorText)
- reject(new Error(errorText))
- },
- complete: () => {
- if (needLoading) hideLoading()
- }
- })
- })
- }
- // ============== HTTP 状态码映射 ==============
- function getHttpErrorMessage(statusCode) {
- const messages = {
- 400: '请求参数错误',
- 401: '未授权,请重新登录',
- 403: '拒绝访问',
- 404: '请求资源不存在',
- 405: '请求方法不允许',
- 408: '请求超时',
- 500: '服务器内部错误',
- 502: '网关错误',
- 503: '服务不可用',
- 504: '网关超时'
- }
- return messages[statusCode] || `请求失败 (${statusCode})`
- }
- // ============== 便捷方法 ==============
- function get(url, options = {}) {
- return request({ ...options, url, method: 'GET' })
- }
- function post(url, data, options = {}) {
- return request({ ...options, url, method: 'POST', data })
- }
- function put(url, data, options = {}) {
- return request({ ...options, url, method: 'PUT', data })
- }
- function del(url, data, options = {}) {
- return request({ ...options, url, method: 'DELETE', data })
- }
- /** 文件上传 */
- function upload(url, options = {}) {
- const {
- filePath,
- name = 'file',
- formData = {},
- header = {},
- showLoading: needLoading = true,
- loadingText = '上传中...',
- showError: needError = true,
- withToken = true
- } = options
- const mergedHeader = { ...header }
- if (withToken) {
- const token = getToken()
- if (token) {
- mergedHeader[config.tokenKey] = config.tokenPrefix + token
- }
- }
- const base = config.baseUrl
- const fullUrl = url.startsWith('http') ? url : base + url
- if (needLoading) showLoading(loadingText)
- return new Promise((resolve, reject) => {
- uni.uploadFile({
- url: fullUrl,
- filePath,
- name,
- formData,
- header: mergedHeader,
- success: (res) => {
- let resData = res.data
- // uploadFile 返回的 data 是 string,需要手动解析
- if (typeof resData === 'string') {
- try { resData = JSON.parse(resData) } catch (_) {}
- }
- if (resData && resData.code === config.successCode) {
- resolve(resData.data !== undefined ? resData.data : resData)
- } else {
- const msg = (resData && resData.message) || '上传失败'
- if (needError) handleError(msg)
- reject(new Error(msg))
- }
- },
- fail: (err) => {
- if (needError) handleError('上传失败,请检查网络')
- reject(new Error(err.errMsg || '上传失败'))
- },
- complete: () => {
- if (needLoading) hideLoading()
- }
- })
- })
- }
- // ============== 导出 ==============
- export default {
- request,
- get,
- post,
- put,
- delete: del,
- upload,
- getToken,
- setToken,
- removeToken,
- addRequestInterceptor,
- addResponseInterceptor,
- config
- }
|