feat: 全新的Vue3管理后台(admin-spa)和路由重构

🎨 新增功能:
- 使用Vue3 + Vite构建的全新管理后台界面
- 支持Tab切换的API统计页面(统计查询/使用教程)
- 优雅的胶囊式Tab切换设计
- 同步了PR #106的会话窗口管理功能
- 完整的响应式设计和骨架屏加载状态

🔧 路由调整:
- 新版管理后台部署在 /admin-next/ 路径
- 将根路径 / 重定向到 /admin-next/api-stats
- 将 /web 页面路由重定向到新版,保留 /web/auth/* 认证路由
- 将 /apiStats 页面路由重定向到新版,保留API端点

🗑️ 清理工作:
- 删除旧版 web/admin/ 静态文件
- 删除旧版 web/apiStats/ 静态文件
- 清理相关的文件服务代码

🐛 修复问题:
- 修复重定向循环问题
- 修复环境变量配置
- 修复路由404错误
- 优化构建配置

🚀 生成方式:使用 Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-29 12:40:51 +08:00
parent c98de2aca5
commit 414856f152
70 changed files with 18748 additions and 10314 deletions

View File

@@ -0,0 +1,173 @@
// API 配置
import { APP_CONFIG, getLoginUrl } from './app'
const isDev = import.meta.env.DEV
// 开发环境使用 /webapi 前缀,生产环境不使用前缀
export const API_PREFIX = APP_CONFIG.apiPrefix
// 创建完整的 API URL
export function createApiUrl(path) {
// 确保路径以 / 开头
if (!path.startsWith('/')) {
path = '/' + path
}
return API_PREFIX + path
}
// API 请求的基础配置
export function getRequestConfig(token) {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
}
// 统一的 API 请求类
class ApiClient {
constructor() {
this.baseURL = API_PREFIX
}
// 获取认证 token
getAuthToken() {
const authToken = localStorage.getItem('authToken')
return authToken || null
}
// 构建请求配置
buildConfig(options = {}) {
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
}
// 添加认证 token
const token = this.getAuthToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
}
// 处理响应
async handleResponse(response) {
// 401 未授权,需要重新登录
if (response.status === 401) {
// 如果当前已经在登录页面,不要再次跳转
const currentPath = window.location.pathname + window.location.hash
const isLoginPage = currentPath.includes('/login') || currentPath.endsWith('/')
if (!isLoginPage) {
localStorage.removeItem('authToken')
// 使用统一的登录URL
window.location.href = getLoginUrl()
}
throw new Error('Unauthorized')
}
// 尝试解析 JSON
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
const data = await response.json()
// 如果响应不成功,抛出错误
if (!response.ok) {
throw new Error(data.message || `HTTP ${response.status}`)
}
return data
}
// 非 JSON 响应
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response
}
// GET 请求
async get(url, options = {}) {
const fullUrl = createApiUrl(url)
const config = this.buildConfig({
...options,
method: 'GET'
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API GET Error:', error)
throw error
}
}
// POST 请求
async post(url, data = null, options = {}) {
const fullUrl = createApiUrl(url)
const config = this.buildConfig({
...options,
method: 'POST',
body: data ? JSON.stringify(data) : undefined
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API POST Error:', error)
throw error
}
}
// PUT 请求
async put(url, data = null, options = {}) {
const fullUrl = createApiUrl(url)
const config = this.buildConfig({
...options,
method: 'PUT',
body: data ? JSON.stringify(data) : undefined
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API PUT Error:', error)
throw error
}
}
// DELETE 请求
async delete(url, options = {}) {
const fullUrl = createApiUrl(url)
const config = this.buildConfig({
...options,
method: 'DELETE'
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API DELETE Error:', error)
throw error
}
}
}
// 导出单例实例
export const apiClient = new ApiClient()

View File

@@ -0,0 +1,81 @@
// API Stats 专用 API 客户端
// 与管理员 API 隔离,不需要认证
class ApiStatsClient {
constructor() {
this.baseURL = window.location.origin
// 开发环境需要为 admin 路径添加 /webapi 前缀
this.isDev = import.meta.env.DEV
}
async request(url, options = {}) {
try {
// 在开发环境中,为 /admin 路径添加 /webapi 前缀
if (this.isDev && url.startsWith('/admin')) {
url = '/webapi' + url
}
const response = await fetch(`${this.baseURL}${url}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `请求失败: ${response.status}`)
}
return data
} catch (error) {
console.error('API Stats request error:', error)
throw error
}
}
// 获取 API Key ID
async getKeyId(apiKey) {
return this.request('/apiStats/api/get-key-id', {
method: 'POST',
body: JSON.stringify({ apiKey })
})
}
// 获取用户统计数据
async getUserStats(apiId) {
return this.request('/apiStats/api/user-stats', {
method: 'POST',
body: JSON.stringify({ apiId })
})
}
// 获取模型使用统计
async getUserModelStats(apiId, period = 'daily') {
return this.request('/apiStats/api/user-model-stats', {
method: 'POST',
body: JSON.stringify({ apiId, period })
})
}
// 获取 OEM 设置(用于网站名称和图标)
async getOemSettings() {
try {
return await this.request('/admin/oem-settings')
} catch (error) {
console.error('Failed to load OEM settings:', error)
return {
success: true,
data: {
siteName: 'Claude Relay Service',
siteIcon: '',
siteIconData: ''
}
}
}
}
}
export const apiStatsClient = new ApiStatsClient()

View File

@@ -0,0 +1,28 @@
// 应用配置
export const APP_CONFIG = {
// 应用基础路径
basePath: import.meta.env.VITE_APP_BASE_URL || (import.meta.env.DEV ? '/admin/' : '/web/admin/'),
// 应用标题
title: import.meta.env.VITE_APP_TITLE || 'Claude Relay Service - 管理后台',
// 是否为开发环境
isDev: import.meta.env.DEV,
// API 前缀
apiPrefix: import.meta.env.DEV ? '/webapi' : ''
}
// 获取完整的应用URL
export function getAppUrl(path = '') {
// 确保路径以 / 开头
if (path && !path.startsWith('/')) {
path = '/' + path
}
return APP_CONFIG.basePath + (path.startsWith('#') ? path : '#' + path)
}
// 获取登录页面URL
export function getLoginUrl() {
return getAppUrl('/login')
}