Merge pull request #338 from GreatAuk/fix-auth-guard

fix: 如果路由拦截器和http响应拦截都符合条件,可能会多次跳转登录页面
This commit is contained in:
菲鸽
2025-11-03 16:46:12 +08:00
committed by GitHub
9 changed files with 228 additions and 16 deletions

View File

@@ -5,8 +5,8 @@ export default defineConfig([
describe: 'unibest-openapi-test',
schemaPath: 'https://ukw0y1.laf.run/unibest-opapi-test.json',
serversPath: './src/service',
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions } from '@/http/types';`,
requestOptionsType: 'CustomRequestOptions',
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions_ } from '@/http/types';`,
requestOptionsType: 'CustomRequestOptions_',
isGenReactQuery: false,
reactQueryMode: 'vue',
isGenJavaScript: false,

View File

@@ -4,7 +4,7 @@ import AdapterUniapp from '@alova/adapter-uniapp'
import { createAlova } from 'alova'
import { createServerTokenAuthentication } from 'alova/client'
import VueHook from 'alova/vue'
import { LOGIN_PAGE } from '@/router/config'
import { toLoginPage } from '@/utils/toLoginPage'
import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
// 配置动态Tag
@@ -31,7 +31,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
}
catch (error) {
// 切换到登录页
await uni.reLaunch({ url: LOGIN_PAGE })
toLoginPage({ mode: 'reLaunch' })
throw error
}
},

View File

@@ -1,9 +1,9 @@
import type { IDoubleTokenRes } from '@/api/types/login'
import type { CustomRequestOptions, IResponse } from '@/http/types'
import { nextTick } from 'vue'
import { LOGIN_PAGE } from '@/router/config'
import { useTokenStore } from '@/store/token'
import { isDoubleTokenMode } from '@/utils'
import { toLoginPage } from '@/utils/toLoginPage'
import { ResultEnum } from './tools/enum'
// 刷新 token 状态管理
@@ -32,7 +32,7 @@ export function http<T>(options: CustomRequestOptions) {
if (!isDoubleTokenMode) {
// 未启用双token策略清理用户信息跳转到登录页
tokenStore.logout()
uni.navigateTo({ url: LOGIN_PAGE })
toLoginPage()
return reject(res)
}
@@ -80,7 +80,7 @@ export function http<T>(options: CustomRequestOptions) {
await tokenStore.logout()
// 跳转到登录页
setTimeout(() => {
uni.navigateTo({ url: LOGIN_PAGE })
toLoginPage()
}, 2000)
}
finally {

View File

@@ -7,6 +7,9 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
hideErrorToast?: boolean
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
/** 主要提供给 openapi-ts-request 生成的代码使用 */
export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
export interface HttpRequestResult<T> {
promise: Promise<T>
requestTask: UniApp.RequestTask

View File

@@ -7,6 +7,7 @@ import { isMp } from '@uni-helper/uni-env'
import { useTokenStore } from '@/store/token'
import { isPageTabbar, tabbarStore } from '@/tabbar/store'
import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index'
import { toLoginPage } from '@/utils/toLoginPage'
import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP, NOT_FOUND_PAGE } from './config'
export const FG_LOG_ENABLE = false
@@ -83,7 +84,7 @@ export const navigateToInterceptor = {
if (Object.keys(myQuery).length) {
fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}`
}
const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}`
const redirectQuery = `?redirect=${encodeURIComponent(fullPath)}`
// #region 1/2 默认需要登录的情况(白名单策略) ---------------------------
if (isNeedLoginMode) {
@@ -96,8 +97,8 @@ export const navigateToInterceptor = {
if (path === LOGIN_PAGE) {
return true // 明确表示允许路由继续执行
}
FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl)
uni.navigateTo({ url: redirectUrl })
FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) url:', fullPath)
toLoginPage({ queryString: redirectQuery })
return false // 明确表示阻止原路由继续执行
}
}
@@ -107,8 +108,8 @@ export const navigateToInterceptor = {
else {
// 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页
if (judgeIsExcludePath(path)) {
FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl)
uni.navigateTo({ url: redirectUrl })
FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) url:', fullPath)
toLoginPage({ queryString: redirectQuery })
return false // 修改为false阻止原路由继续执行
}
return true // 明确表示允许路由继续执行

View File

@@ -1,12 +1,12 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import { CustomRequestOptions } from '@/http/types';
import { CustomRequestOptions_ } from '@/http/types';
import * as API from './types';
/** 用户信息 GET /user/info */
export function infoUsingGet({ options }: { options?: CustomRequestOptions }) {
export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) {
return request<API.InfoUsingGetResponse>('/user/info', {
method: 'GET',
...(options || {}),

View File

@@ -1,7 +1,7 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import { CustomRequestOptions } from '@/http/types';
import { CustomRequestOptions_ } from '@/http/types';
import * as API from './types';
@@ -9,7 +9,7 @@ import * as API from './types';
export function listAllUsingGet({
options,
}: {
options?: CustomRequestOptions;
options?: CustomRequestOptions_;
}) {
return request<API.ListAllUsingGetResponse>('/user/listAll', {
method: 'GET',

166
src/utils/debounce.ts Normal file
View File

@@ -0,0 +1,166 @@
// fork from https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts
// 文档可查看https://es-toolkit.dev/reference/function/debounce.html
// 如需要 throttle 功能,可 copy https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts
interface DebounceOptions {
/**
* An optional AbortSignal to cancel the debounced function.
*/
signal?: AbortSignal
/**
* An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.
* If `edges` includes "leading", the function will be invoked at the start of the delay period.
* If `edges` includes "trailing", the function will be invoked at the end of the delay period.
* If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period.
* @default ["trailing"]
*/
edges?: Array<'leading' | 'trailing'>
}
export interface DebouncedFunction<F extends (...args: any[]) => void> {
(...args: Parameters<F>): void
/**
* Schedules the execution of the debounced function after the specified debounce delay.
* This method resets any existing timer, ensuring that the function is only invoked
* after the delay has elapsed since the last call to the debounced function.
* It is typically called internally whenever the debounced function is invoked.
*
* @returns {void}
*/
schedule: () => void
/**
* Cancels any pending execution of the debounced function.
* This method clears the active timer and resets any stored context or arguments.
*/
cancel: () => void
/**
* Immediately invokes the debounced function if there is a pending execution.
* This method executes the function right away if there is a pending execution.
*/
flush: () => void
}
/**
* Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds
* have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel`
* method to cancel any pending execution.
*
* @template F - The type of function.
* @param {F} func - The function to debounce.
* @param {number} debounceMs - The number of milliseconds to delay.
* @param {DebounceOptions} options - The options object
* @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function.
* @returns A new debounced function with a `cancel` method.
*
* @example
* const debouncedFunction = debounce(() => {
* console.log('Function executed');
* }, 1000);
*
* // Will log 'Function executed' after 1 second if not called again in that time
* debouncedFunction();
*
* // Will not log anything as the previous call is canceled
* debouncedFunction.cancel();
*
* // With AbortSignal
* const controller = new AbortController();
* const signal = controller.signal;
* const debouncedWithSignal = debounce(() => {
* console.log('Function executed');
* }, 1000, { signal });
*
* debouncedWithSignal();
*
* // Will cancel the debounced function call
* controller.abort();
*/
export function debounce<F extends (...args: any[]) => void>(
func: F,
debounceMs: number,
{ signal, edges }: DebounceOptions = {},
): DebouncedFunction<F> {
let pendingThis: any
let pendingArgs: Parameters<F> | null = null
const leading = edges != null && edges.includes('leading')
const trailing = edges == null || edges.includes('trailing')
const invoke = () => {
if (pendingArgs !== null) {
func.apply(pendingThis, pendingArgs)
pendingThis = undefined
pendingArgs = null
}
}
const onTimerEnd = () => {
if (trailing) {
invoke()
}
// eslint-disable-next-line ts/no-use-before-define
cancel()
}
let timeoutId: ReturnType<typeof setTimeout> | null = null
const schedule = () => {
if (timeoutId != null) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
timeoutId = null
onTimerEnd()
}, debounceMs)
}
const cancelTimer = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId)
timeoutId = null
}
}
const cancel = () => {
cancelTimer()
pendingThis = undefined
pendingArgs = null
}
const flush = () => {
invoke()
}
const debounced = function (this: any, ...args: Parameters<F>) {
if (signal?.aborted) {
return
}
// eslint-disable-next-line ts/no-this-alias
pendingThis = this
pendingArgs = args
const isFirstCall = timeoutId == null
schedule()
if (leading && isFirstCall) {
invoke()
}
}
debounced.schedule = schedule
debounced.cancel = cancel
debounced.flush = flush
signal?.addEventListener('abort', cancel, { once: true })
return debounced
}

42
src/utils/toLoginPage.ts Normal file
View File

@@ -0,0 +1,42 @@
import { LOGIN_PAGE } from '@/router/config'
import { getLastPage } from '@/utils'
import { debounce } from '@/utils/debounce'
interface ToLoginPageOptions {
/**
* 跳转模式, uni.navigateTo | uni.reLaunch
* @default 'navigateTo'
*/
mode?: 'navigateTo' | 'reLaunch'
/**
* 查询参数
* @example '?redirect=/pages/home/index'
*/
queryString?: string
}
/**
* 跳转到登录页, 带防抖处理
*
* 如果要立即跳转,不做延时,可以使用 `toLoginPage.flush()` 方法
*/
export const toLoginPage = debounce((options: ToLoginPageOptions = {}) => {
const { mode = 'navigateTo', queryString = '' } = options
const url = `${LOGIN_PAGE}${queryString}`
// 获取当前页面路径
const currentPage = getLastPage()
const currentPath = `/${currentPage.route}`
// 如果已经在登录页,则不跳转
if (currentPath === LOGIN_PAGE) {
return
}
if (mode === 'navigateTo') {
uni.navigateTo({ url })
}
else {
uni.reLaunch({ url })
}
}, 500)