ソースを参照

全局请求错误处理

yuanmingze 8 ヶ月 前
コミット
b0ef6c65bc

+ 0 - 9
src/App.vue

@@ -1,15 +1,6 @@
 <script setup lang="ts">
 import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
 
-import { setTokenProvider } from '@/services/request/tokenProvider'
-
-import { useUserStore } from '@/stores/modules/user'
-
-setTokenProvider(() => {
-  const userStore = useUserStore()
-  return userStore.access_token
-})
-
 onLaunch(() => {
   console.log('App Launch')
 })

+ 1 - 3
src/main.ts

@@ -1,8 +1,6 @@
 import { createSSRApp } from 'vue'
-import { setupStore } from '@/stores'
-
-
 
+import { setupStore } from '@/stores'
 
 import App from './App.vue'
 export function createApp() {

+ 3 - 18
src/pages/login/index.vue

@@ -179,12 +179,10 @@ const sendCodeImpl = async () => {
   ])
 
   if (!usernameOk || !passwordOk) return
-  uni.showLoading()
   const res = await getAuthTokenApi({
     username: form.username,
     password: form.password,
   })
-  uni.hideLoading()
   if (res.access_token) {
     const codeRes = await getMobileCodeApi(form.username)
     if (codeRes.code === 0) {
@@ -212,29 +210,16 @@ const submit = async () => {
   const formOk = await validate()
   if (!agreementOk || !formOk) return
 
-  uni.showLoading()
-
   const res = await userStore.login({
     username: form.username,
     code: form.code,
   })
-
-  if (!res.success) {
-    uni.showModal({
-      title: '登录失败',
-      content: res.message ?? '登录失败,请重试',
-      showCancel: false,
-      confirmText: '我知道了',
+  if (res.success) {
+    uni.reLaunch({
+      url: '/pages/index/index',
     })
-    return
   }
-
-  uni.hideLoading()
-  uni.reLaunch({
-    url: '/pages/index/index',
-  })
 }
-
 /* ---------------- 其他 ---------------- */
 
 const forgetPassword = () => {

+ 25 - 4
src/services/modules/auth/index.ts

@@ -20,7 +20,11 @@ export const getAuthTokenApi = (data: AuthTokenRequest) => {
 export const getTokenBySmsApi = (data: LoginRequest) => {
   return http.postRaw(
     `/auth/mobile/token/sms?code=${data.code}&mobile=SMS@${data.username}&grant_type=mobile`,
-    {}
+    {},
+    {
+      loading: true,
+      loadingText: '登录中...',
+    }
   )
 }
 // 通过微信 code 获取用户信息
@@ -35,13 +39,30 @@ export const checkBlacklistApi = (data: CheckBlacklistRequest) => {
 }
 
 export const getMobileCodeApi = (phone: string) => {
-  return http.get<boolean>(`/admin/mobile/${phone}`)
+  return http.get<boolean>(
+    `/admin/mobile/${phone}`,
+    {},
+    {
+      loading: true,
+      loadingText: '发送中...',
+    }
+  )
 }
 
 export const getPwdCodeForNoAuthApi = (username: string) => {
-  return http.get<boolean>(`/admin/mobile/for-pwd-noauth?username=${username}`)
+  return http.get<boolean>(
+    `/admin/mobile/for-pwd-noauth?username=${username}`,
+    {},
+    {
+      loading: true,
+      loadingText: '发送中...',
+    }
+  )
 }
 
 export const updUserPwdApi = (data: UpdUserPwdRequest) => {
-  return http.post<boolean>('/admin/user/upd-for-app', data)
+  return http.post<boolean>('/admin/user/upd-for-app', data, {
+    loading: true,
+    loadingText: '发送中...',
+  })
 }

+ 118 - 21
src/services/request/index.ts

@@ -1,12 +1,8 @@
-import {
-  BASE_API,
-  MODE,
-  NOT_OAUTH_BASIC_TOKEN,
-  OAUTH_BASIC_TOKEN,
-  SUCCESS_CODE_LIST,
-  TIMEOUT,
-} from './config'
-import { getToken } from './tokenProvider'
+import { useUserStoreWithOut } from '@/stores/modules/user'
+
+import { hideGlobalLoading, showGlobalLoading } from '@/utils/loading'
+
+import { BASE_API, MODE, NOT_OAUTH_BASIC_TOKEN, OAUTH_BASIC_TOKEN, TIMEOUT } from './config'
 
 /* -------------------------------------------------------------------------- */
 /*                                   类型                                     */
@@ -22,6 +18,8 @@ export interface BaseOptions {
   header?: UniApp.RequestOptions['header']
   timeout?: number
   silent?: boolean
+  loading?: boolean // 是否显示 loading(默认 false)
+  loadingText?: string //  loading 文字描述
 }
 
 export interface RequestConfig extends BaseOptions {
@@ -35,6 +33,11 @@ export interface RequestConfig extends BaseOptions {
 /* -------------------------------------------------------------------------- */
 
 export function request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
+  const needLoading = config.loading === true
+  if (needLoading) {
+    showGlobalLoading(config.loadingText)
+  }
+
   return new Promise((resolve, reject) => {
     uni.request({
       url: resolveUrl(config.url),
@@ -44,32 +47,67 @@ export function request<T = any>(config: RequestConfig): Promise<ApiResponse<T>>
       header: buildHeaders(config.url, config.header),
 
       success(res) {
-        const result = res.data as ApiResponse<T>
+        const statusCode = res.statusCode ?? 0
+        const body = res.data as ApiResponse<T>
 
-        // 白名单接口:不校验 code
+        // 白名单接口:完全放行
         if (isWhiteList(config.url)) {
-          return resolve(result)
+          return resolve(body)
+        }
+
+        // 仅校验 HTTP 状态码
+        if (statusCode === 200) {
+          return resolve(body)
+        }
+
+        // 401:统一登出
+        if (statusCode === 401) {
+          handleUnauthorized()
+          return reject(normalizeHttpError(res, config.url))
         }
 
-        // 业务失败
-        if (!SUCCESS_CODE_LIST.includes(result.code)) {
-          return reject(result)
+        // 其他 HTTP 错误
+        if (!config.silent) {
+          uni.showModal({
+            title: '提示',
+            content: getErrorMessage(body, statusCode),
+            showCancel: false,
+          })
         }
 
-        resolve(result)
+        reject(normalizeHttpError(res, config.url))
       },
 
       fail(err) {
+        if (!config.silent) {
+          uni.showModal({
+            title: '提示',
+            content: (err as any)?.errMsg || '网络异常,请稍后再试',
+            showCancel: false,
+          })
+        }
         reject(err)
       },
+      complete() {
+        if (needLoading) {
+          hideGlobalLoading()
+        }
+      },
     })
   })
 }
 
-/**
- * 后端不包 ApiResponse 的接口
- */
+/* -------------------------------------------------------------------------- */
+/*                              Raw request                                   */
+/* -------------------------------------------------------------------------- */
+
 export function requestRaw<T = any>(config: RequestConfig): Promise<T> {
+  const needLoading = config.loading === true
+
+  if (needLoading) {
+    showGlobalLoading(config.loadingText)
+  }
+
   return new Promise((resolve, reject) => {
     uni.request({
       url: resolveUrl(config.url),
@@ -79,12 +117,44 @@ export function requestRaw<T = any>(config: RequestConfig): Promise<T> {
       header: buildHeaders(config.url, config.header),
 
       success(res) {
-        resolve(res.data as T)
+        const statusCode = res.statusCode ?? 0
+
+        if (isWhiteList(config.url) || statusCode === 200) {
+          return resolve(res.data as T)
+        }
+
+        if (statusCode === 401) {
+          handleUnauthorized()
+          return reject(normalizeHttpError(res, config.url))
+        }
+
+        if (!config.silent) {
+          uni.showModal({
+            title: '提示',
+            content: `请求失败(HTTP ${statusCode})`,
+            showCancel: false,
+          })
+        }
+
+        reject(normalizeHttpError(res, config.url))
       },
 
       fail(err) {
+        if (!config.silent) {
+          uni.showModal({
+            title: '提示',
+            content: (err as any)?.errMsg || '网络异常,请稍后再试',
+            showCancel: false,
+          })
+        }
         reject(err)
       },
+
+      complete() {
+        if (needLoading) {
+          hideGlobalLoading()
+        }
+      },
     })
   })
 }
@@ -139,7 +209,8 @@ function buildHeaders(url: string, extraHeader?: UniApp.RequestOptions['header']
     ...(extraHeader || {}),
   }
 
-  const accessToken = getToken()
+  const userStore = useUserStoreWithOut()
+  const accessToken = userStore.access_token
 
   if (url.includes('auth/mobile/token/sms')) {
     headers.Authorization = NOT_OAUTH_BASIC_TOKEN
@@ -152,3 +223,29 @@ function buildHeaders(url: string, extraHeader?: UniApp.RequestOptions['header']
 
   return headers
 }
+
+function handleUnauthorized() {
+  const userStore = useUserStoreWithOut()
+  userStore.logout()
+
+  uni.reLaunch({
+    url: '/pages/login/index',
+  })
+}
+
+function getErrorMessage(body: unknown, statusCode: number) {
+  if (body && typeof body === 'object') {
+    const msg = (body as any).msg
+    if (typeof msg === 'string' && msg.trim()) return msg
+  }
+  return `请求失败(HTTP ${statusCode})`
+}
+
+function normalizeHttpError(res: UniApp.RequestSuccessCallbackResult, url: string) {
+  return {
+    url,
+    statusCode: res.statusCode ?? 0,
+    data: res.data,
+    header: res.header,
+  }
+}

+ 0 - 9
src/services/request/tokenProvider.ts

@@ -1,9 +0,0 @@
-let tokenProvider: (() => string | undefined) | null = null
-
-export function setTokenProvider(fn: () => string | undefined) {
-  tokenProvider = fn
-}
-
-export function getToken() {
-  return tokenProvider?.()
-}

+ 6 - 4
src/stores/index.ts

@@ -1,14 +1,16 @@
 // store/index.ts
 import { createPinia } from 'pinia'
+
 import persistedstate from 'pinia-plugin-persistedstate'
 import type { App } from 'vue'
 
-const store = createPinia()
-store.use(persistedstate)
+const pinia = createPinia()
+pinia.use(persistedstate)
+
+export { pinia }
 
 export function setupStore(app: App) {
-  app.use(store)
+  app.use(pinia)
 }
 
-export { store }
 export * from './modules/user'

+ 9 - 0
src/stores/modules/user.ts

@@ -10,6 +10,8 @@ import type {
 } from '@/services/modules/auth/type'
 import type { UserInfoItem } from '@/services/modules/auth/userInfo'
 
+import { pinia } from '@/stores/index'
+
 import { uniStorage } from '@/utils/uniStorage'
 
 interface UserState {
@@ -75,6 +77,9 @@ export const useUserStore = defineStore('user', {
         this.isBlacklisted = idCardNumberStatus || phoneNumberStatus
       }
     },
+    logout() {
+      this.$reset()
+    },
   },
   persist: {
     key: 'user-store',
@@ -82,3 +87,7 @@ export const useUserStore = defineStore('user', {
     pick: ['access_token', 'currentUserInfoIndex', 'userInfoList', 'isBlacklisted'],
   },
 })
+
+export function useUserStoreWithOut() {
+  return useUserStore(pinia)
+}

+ 20 - 0
src/utils/loading.ts

@@ -0,0 +1,20 @@
+let loadingCount = 0
+
+export function showGlobalLoading(title = '') {
+  if (loadingCount === 0) {
+    uni.showLoading({
+      title,
+      mask: true,
+    })
+  }
+  loadingCount++
+}
+
+export function hideGlobalLoading() {
+  if (loadingCount <= 0) return
+
+  loadingCount--
+  if (loadingCount === 0) {
+    uni.hideLoading()
+  }
+}