feat:项目界面补充

This commit is contained in:
JiaJun
2026-04-10 17:29:49 +08:00
parent af3ed15ba2
commit 8fcf0355b3
19 changed files with 292 additions and 407 deletions

View File

@@ -1,7 +1,9 @@
import ky, {HTTPError, type Input, type KyInstance, type Options} from 'ky'
import ky, {HTTPError, type AfterResponseHook, type Input, type KyInstance, type Options} from 'ky'
import {notifyError, resolveToastMessage} from '@/features/notifications'
import {useUserStore} from '@/store/user.ts'
import type {ValidateTokenData} from '@/types/auth.type.ts'
import {objectToFormData} from './tool'
type RequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'
type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'response'
@@ -44,13 +46,97 @@ const isApiEnvelope = (value: unknown): value is ApiEnvelope =>
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api/'
const REQUEST_TIMEOUT = 10_000
const AUTH_RETRY_HEADER = 'x-auth-retried'
const VERIFY_TOKEN_PATH = '/v1/mall/verifyToken'
let accessTokenFormatter: TokenFormatter = (token) => `Bearer ${token}`
let refreshAuthInfoPromise: Promise<ValidateTokenData> | null = null
export const setAccessTokenFormatter = (formatter?: TokenFormatter) => {
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
}
const authRefreshClient = ky.create({
baseUrl: API_BASE_URL,
timeout: REQUEST_TIMEOUT,
retry: 0,
headers: {
lang: 'zh',
},
})
const handleUnauthorizedRetry: AfterResponseHook = async ({request, response}) => {
if (!shouldRefreshOnUnauthorized(request, response)) {
return response
}
try {
const authInfo = await refreshAuthInfo()
const nextRequest = withRefreshedSessionId(request, authInfo.session_id)
return await requestClient(nextRequest)
} catch {
useUserStore.getState().clearUserInfo()
return response
}
}
async function refreshAuthInfo() {
if (refreshAuthInfoPromise) {
return refreshAuthInfoPromise
}
refreshAuthInfoPromise = (async () => {
const token = useUserStore.getState().userInfo?.token?.trim()
if (!token) {
throw new RequestError('Unauthorized')
}
const response = await authRefreshClient.post('v1/mall/verifyToken', {
body: objectToFormData({token}),
}).json<{
code: number
msg?: string
data?: ValidateTokenData
}>()
if (!response || (response.code !== 1 && response.code !== 200) || !response.data?.session_id) {
throw new RequestError(resolveToastMessage(response, 'Unauthorized'))
}
useUserStore.getState().setAuthInfo(response.data)
return response.data
})()
try {
return await refreshAuthInfoPromise
} finally {
refreshAuthInfoPromise = null
}
}
function shouldRefreshOnUnauthorized(request: Request, response: Response) {
if (response.status !== 401) {
return false
}
if (request.headers.get(AUTH_RETRY_HEADER) === '1') {
return false
}
return !request.url.includes(VERIFY_TOKEN_PATH)
}
function withRefreshedSessionId(request: Request, sessionId: string) {
const nextUrl = new URL(request.url)
if (nextUrl.searchParams.has('session_id')) {
nextUrl.searchParams.set('session_id', sessionId)
}
const nextRequest = new Request(nextUrl, request.clone())
nextRequest.headers.set(AUTH_RETRY_HEADER, '1')
return nextRequest
}
const requestClient = ky.create({
baseUrl: API_BASE_URL,
timeout: REQUEST_TIMEOUT,
@@ -69,6 +155,9 @@ const requestClient = ky.create({
request.headers.set('Authorization', accessTokenFormatter(token))
},
],
afterResponse: [
handleUnauthorizedRetry,
],
beforeError: [
async ({error}) => {
if (error instanceof HTTPError) {