|
|
@@ -0,0 +1,169 @@
|
|
|
+/**
|
|
|
+ * 统一请求封装(uniapp)
|
|
|
+ * - 自动注入 Authorization(Bearer token)
|
|
|
+ * - 解析后端 R<T> 响应: code=200 → resolve(data),其他 → reject(msg)
|
|
|
+ * - 401/403 → 清登录态 + 跳登录页
|
|
|
+ * - 网络/HTTP 异常 → reject 携带可读 message
|
|
|
+ *
|
|
|
+ * 注意: 默认走相对路径(开发 = /dev-api,生产 = 同域反代),与 plus-ui/uni.request 已有的 url 风格保持一致
|
|
|
+ */
|
|
|
+import { getToken, removeToken } from './auth'
|
|
|
+
|
|
|
+// 标记正在跳转登录页,避免并发的 401 触发多次 reLaunch
|
|
|
+let redirecting = false
|
|
|
+
|
|
|
+function redirectToLogin() {
|
|
|
+ if (redirecting) return
|
|
|
+ redirecting = true
|
|
|
+ // 先清掉 token(再尝试登录)
|
|
|
+ removeToken()
|
|
|
+ uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 1500 })
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.reLaunch({ url: '/pages/login/index' })
|
|
|
+ setTimeout(() => {
|
|
|
+ redirecting = false
|
|
|
+ }, 1000)
|
|
|
+ }, 800)
|
|
|
+}
|
|
|
+
|
|
|
+export interface RequestOptions {
|
|
|
+ /** 业务路径(以 / 开头),会自动拼接默认 BASE_API */
|
|
|
+ url: string
|
|
|
+ /** HTTP method,默认 GET */
|
|
|
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
|
+ /** query 或 body 参数 */
|
|
|
+ data?: any
|
|
|
+ /** 额外 header(会与默认 Authorization 合并) */
|
|
|
+ header?: Record<string, string>
|
|
|
+ /** 是否跳过 token 注入(如登录本身) */
|
|
|
+ skipAuth?: boolean
|
|
|
+ /** 调用方自定义的超时(ms),默认 15000 */
|
|
|
+ timeout?: number
|
|
|
+}
|
|
|
+
|
|
|
+export interface BizResponse<T = any> {
|
|
|
+ code: number
|
|
|
+ msg: string
|
|
|
+ data: T
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 默认 BASE_API
|
|
|
+ * - 开发: /dev-api(vite.config.ts 已代理到 9225)
|
|
|
+ * - 生产: 与 plus-ui 的 nginx 反代一致,使用 /prod-api
|
|
|
+ *
|
|
|
+ * 之所以这里仍写 'dev-api' 是因为本仓库 uniapp 的 manifest.json 里 h5.devServer.proxy
|
|
|
+ * 只配了 /dev-api,所有现有页面(/dev-api/...)都按此风格;生产环境需要 nginx 把 /prod-api
|
|
|
+ * 转发到后端 8080 即可(参见 plus-ui 部署说明)
|
|
|
+ */
|
|
|
+const BASE_API = '/dev-api'
|
|
|
+
|
|
|
+/** 拼接 url: 支持绝对路径(http开头)和相对路径 */
|
|
|
+function buildUrl(url: string): string {
|
|
|
+ if (/^https?:\/\//i.test(url)) return url
|
|
|
+ // 已经带 /dev-api /prod-api 等 BASE 前缀,直接返回
|
|
|
+ if (url.startsWith(BASE_API)) return url
|
|
|
+ return BASE_API + url
|
|
|
+}
|
|
|
+
|
|
|
+export function request<T = any>(opts: RequestOptions): Promise<T> {
|
|
|
+ const header: Record<string, string> = { ...(opts.header || {}) }
|
|
|
+ if (!opts.skipAuth) {
|
|
|
+ const token = getToken()
|
|
|
+ if (token) header['Authorization'] = `Bearer ${token}`
|
|
|
+ }
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.request({
|
|
|
+ url: buildUrl(opts.url),
|
|
|
+ method: opts.method || 'GET',
|
|
|
+ data: opts.data,
|
|
|
+ header,
|
|
|
+ timeout: opts.timeout ?? 15000,
|
|
|
+ success: (res) => {
|
|
|
+ const status = res.statusCode
|
|
|
+ // 网络层错误
|
|
|
+ if (status === 401 || status === 403) {
|
|
|
+ redirectToLogin()
|
|
|
+ reject(new Error('登录已过期'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (status < 200 || status >= 300) {
|
|
|
+ reject(new Error(`网络异常(${status})`))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 后端 R<T> 结构
|
|
|
+ const body = res.data as BizResponse | undefined
|
|
|
+ if (!body || typeof body !== 'object' || typeof body.code === 'undefined') {
|
|
|
+ // 没有 R 包装,直接返回原数据(如文件上传/特殊接口)
|
|
|
+ resolve(res.data as any)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (body.code === 200) {
|
|
|
+ resolve(body.data as T)
|
|
|
+ } else {
|
|
|
+ // Sa-Token 401101 等业务码也跳登录
|
|
|
+ if (body.code === 401 || body.code === 401101 || body.code === 500001) {
|
|
|
+ redirectToLogin()
|
|
|
+ }
|
|
|
+ reject(new Error(body.msg || `请求失败(${body.code})`))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(new Error(err?.errMsg || '网络异常'))
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 兼容文件上传(走 uni.uploadFile,与 request 分开)
|
|
|
+ * - 这里只追加 token,不强制走 R 解析
|
|
|
+ */
|
|
|
+export function uploadFile<T = any>(opts: {
|
|
|
+ url: string
|
|
|
+ filePath: string
|
|
|
+ name?: string
|
|
|
+ formData?: Record<string, any>
|
|
|
+ skipAuth?: boolean
|
|
|
+}): Promise<T> {
|
|
|
+ const header: Record<string, string> = {}
|
|
|
+ if (!opts.skipAuth) {
|
|
|
+ const token = getToken()
|
|
|
+ if (token) header['Authorization'] = `Bearer ${token}`
|
|
|
+ }
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.uploadFile({
|
|
|
+ url: buildUrl(opts.url),
|
|
|
+ filePath: opts.filePath,
|
|
|
+ name: opts.name || 'file',
|
|
|
+ formData: opts.formData,
|
|
|
+ header,
|
|
|
+ success: (res) => {
|
|
|
+ const status = res.statusCode
|
|
|
+ if (status === 401 || status === 403) {
|
|
|
+ redirectToLogin()
|
|
|
+ reject(new Error('登录已过期'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (status < 200 || status >= 300) {
|
|
|
+ reject(new Error(`上传失败(${status})`))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
|
|
|
+ if (body?.code === 200) {
|
|
|
+ resolve(body.data as T)
|
|
|
+ } else {
|
|
|
+ reject(new Error(body?.msg || `上传失败(${body?.code})`))
|
|
|
+ }
|
|
|
+ } catch (e: any) {
|
|
|
+ reject(new Error('上传响应解析失败: ' + (e?.message || '')))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(new Error(err?.errMsg || '网络异常,上传失败'))
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|