feat: 前端新增 Demo 模式支持

- 新增 demo 配置模块,支持静态托管环境(GitHub Pages/Vercel/Netlify)
- API client 集成自定义 adapter,Demo 模式下自动拦截请求返回 mock 数据
- 登录对话框适配 Demo 模式,显示演示账号提示信息
- 添加完整的 mock 数据处理器(用户、配额、用量、供应商等)
- 同步 token 状态到 mock handler,支持页面刷新后恢复会话
This commit is contained in:
fawney19
2025-12-11 10:03:10 +08:00
parent a7b658a768
commit 6016f08d1c
7 changed files with 3173 additions and 6 deletions

View File

@@ -1,6 +1,8 @@
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios, { getAdapter } from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosAdapter } from 'axios'
import { NETWORK_CONFIG, AUTH_CONFIG } from '@/config/constants'
import { isDemoMode } from '@/config/demo'
import { handleMockRequest, setMockUserToken } from '@/mocks'
import { log } from '@/utils/logger'
// 在开发环境下使用代理,生产环境使用环境变量
@@ -42,6 +44,39 @@ function isRefreshableAuthError(errorDetail: string): boolean {
return !nonRefreshableErrors.some((msg) => errorDetail.includes(msg))
}
/**
* 创建 Demo 模式的自定义 adapter
* 在 Demo 模式下拦截请求并返回 mock 数据
*/
function createDemoAdapter(defaultAdapter: AxiosAdapter) {
return async (config: InternalAxiosRequestConfig): Promise<AxiosResponse> => {
if (isDemoMode()) {
try {
const mockResponse = await handleMockRequest({
method: config.method?.toUpperCase(),
url: config.url,
data: config.data,
params: config.params,
})
if (mockResponse) {
// 确保响应包含 config
mockResponse.config = config
return mockResponse
}
} catch (error: any) {
// Mock 错误需要附加 config否则 handleResponseError 会崩溃
if (error.response) {
error.config = config
error.response.config = config
}
throw error
}
}
// 非 Demo 模式或没有 mock 响应时,使用默认 adapter
return defaultAdapter(config)
}
}
class ApiClient {
private client: AxiosInstance
private token: string | null = null
@@ -57,6 +92,10 @@ class ApiClient {
},
})
// 设置自定义 adapter 处理 Demo 模式
const defaultAdapter = getAdapter(this.client.defaults.adapter)
this.client.defaults.adapter = createDemoAdapter(defaultAdapter)
this.setupInterceptors()
}
@@ -64,7 +103,7 @@ class ApiClient {
* 配置请求和响应拦截器
*/
private setupInterceptors(): void {
// 请求拦截器
// 请求拦截器 - 仅处理认证
this.client.interceptors.request.use(
(config) => {
const requiresAuth = !isPublicEndpoint(config.url, config.method) &&
@@ -207,11 +246,19 @@ class ApiClient {
setToken(token: string): void {
this.token = token
localStorage.setItem('access_token', token)
// 同步到 mock handler
if (isDemoMode()) {
setMockUserToken(token)
}
}
getToken(): string | null {
if (!this.token) {
this.token = localStorage.getItem('access_token')
// 页面刷新时,从 localStorage 恢复 token 到 mock handler
if (this.token && isDemoMode()) {
setMockUserToken(this.token)
}
}
return this.token
}
@@ -220,12 +267,18 @@ class ApiClient {
this.token = null
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
// 同步清除 mock token
if (isDemoMode()) {
setMockUserToken(null)
}
}
async refreshToken(refreshToken: string): Promise<AxiosResponse> {
// refreshToken 会通过 adapter 处理 Demo 模式
return this.client.post('/api/auth/refresh', { refresh_token: refreshToken })
}
// 以下方法直接委托给 axios clientDemo 模式由 adapter 统一处理
async request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.request<T>(config)
}