diff --git a/README.md b/README.md index 6b2f28d..2faf73d 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,16 @@ python generate_keys.py # 生成密钥, 并将生成的密钥填入 .env ./deploy.sh # 自动构建、启动、迁移 ``` +### 更新 + +```bash +# 拉取最新代码 +git pull + +# 自动部署脚本 +./deploy.sh +``` + ### 本地开发 ```bash diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index d2b6414..4464601 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -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 => { + 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 { + // refreshToken 会通过 adapter 处理 Demo 模式 return this.client.post('/api/auth/refresh', { refresh_token: refreshToken }) } + // 以下方法直接委托给 axios client,Demo 模式由 adapter 统一处理 async request(config: AxiosRequestConfig): Promise> { return this.client.request(config) } diff --git a/frontend/src/api/endpoints/types.ts b/frontend/src/api/endpoints/types.ts index 498b68c..ad822d2 100644 --- a/frontend/src/api/endpoints/types.ts +++ b/frontend/src/api/endpoints/types.ts @@ -81,6 +81,7 @@ export interface EndpointHealthDetail { api_format: string health_score: number is_active: boolean + active_keys?: number } export interface EndpointHealthEvent { diff --git a/frontend/src/config/demo.ts b/frontend/src/config/demo.ts new file mode 100644 index 0000000..2a1c680 --- /dev/null +++ b/frontend/src/config/demo.ts @@ -0,0 +1,37 @@ +/** + * Demo Mode Configuration + * 用于 GitHub Pages 等静态托管环境的演示模式 + */ + +// 检测是否为演示模式环境 +export function isDemoMode(): boolean { + const hostname = window.location.hostname + return ( + hostname.includes('github.io') || + hostname.includes('vercel.app') || + hostname.includes('netlify.app') || + hostname.includes('pages.dev') || + import.meta.env.VITE_DEMO_MODE === 'true' + ) +} + +// Demo 账号配置 +export const DEMO_ACCOUNTS = { + admin: { + email: 'admin@demo.aether.io', + password: 'demo123', + hint: '管理员账号' + }, + user: { + email: 'user@demo.aether.io', + password: 'demo123', + hint: '普通用户' + } +} as const + +// Demo 模式提示信息 +export const DEMO_MODE_INFO = { + title: '演示模式', + description: '当前处于演示模式,所有数据均为模拟数据,不会产生实际调用。', + accountHint: '可使用以下演示账号登录:' +} as const diff --git a/frontend/src/features/auth/components/LoginDialog.vue b/frontend/src/features/auth/components/LoginDialog.vue index 9664a88..5f2884f 100644 --- a/frontend/src/features/auth/components/LoginDialog.vue +++ b/frontend/src/features/auth/components/LoginDialog.vue @@ -11,6 +11,43 @@ + +
+
+
+ + + +
+
+

+ 演示模式 +

+

+ 当前处于演示模式,所有数据均为模拟数据。 +

+
+ + +
+
+
+
+
@@ -32,14 +69,14 @@ v-model="form.password" type="password" required - placeholder="••••••••" + placeholder="********" autocomplete="off" @keyup.enter="handleLogin" />
-

+

如需开通账户,请联系管理员配置访问权限

@@ -66,7 +103,7 @@