/** * 统一 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) { console.log('handleError', message, statusCode) // 延迟弹出,避免被 complete 回调中的 hideLoading (toast.hide) 立即关闭 setTimeout(() => { toast.error(message || '请求失败') }, 150) } 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} 响应数据 (默认返回 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) => { console.log(reqConfig,"返回数据",res) // 执行响应拦截器 let response = res for (const handler of interceptors.response) { response = handler(response) || response } const { statusCode, data: resData } = response console.log("返回数据2",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 }