feat: 使用 React + Vite + shadcn/ui 重构前端
- 使用 React 19 + TypeScript + Vite 7 重构整个前端 - 集成 shadcn/ui 组件库和 Tailwind CSS v4 - 使用 TanStack Query 管理服务端状态 - 使用 Zustand 管理客户端状态(认证) - 使用 React Router v7 实现路由 - 实现浅粉色主题,支持深色模式 - 添加 API 拦截器和全局错误处理 - 优化 Toast 通知样式 - 管理页面按钮右对齐 - Token 显示脱敏处理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de24d420a4
commit
4572e5fc9e
60 files changed
+6649
-3173
No files matched your search
@@ -0,0 +1,181 @@
|
||||
import { ApiResponse } from '@/types'
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || ''
|
||||
|
||||
// 错误类型
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
// 全局错误处理回调
|
||||
let onUnauthorized: (() => void) | null = null
|
||||
let onError: ((error: ApiError) => void) | null = null
|
||||
|
||||
export function setErrorHandlers(handlers: {
|
||||
onUnauthorized?: () => void
|
||||
onError?: (error: ApiError) => void
|
||||
}) {
|
||||
onUnauthorized = handlers.onUnauthorized || null
|
||||
onError = handlers.onError || null
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
async function requestInterceptor(config: RequestInit): Promise<RequestInit> {
|
||||
const headers = new Headers(config.headers)
|
||||
|
||||
// 设置默认 Content-Type
|
||||
if (!headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
// 添加 admin token
|
||||
const adminToken = localStorage.getItem('adminToken')
|
||||
if (adminToken) {
|
||||
headers.set('x-admin-token', adminToken)
|
||||
}
|
||||
|
||||
return { ...config, headers }
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
async function responseInterceptor(response: Response): Promise<any> {
|
||||
if (!response.ok) {
|
||||
let errorMessage: string
|
||||
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
errorMessage = errorData.message || errorData.error || JSON.stringify(errorData)
|
||||
} catch {
|
||||
errorMessage = await response.text() || `请求失败: ${response.status}`
|
||||
}
|
||||
|
||||
const error = new ApiError(errorMessage, response.status)
|
||||
|
||||
// 处理特定状态码
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
// 未授权 - 清除 token 并触发回调
|
||||
localStorage.removeItem('adminToken')
|
||||
onUnauthorized?.()
|
||||
error.message = '认证已过期,请重新登录'
|
||||
break
|
||||
case 403:
|
||||
error.message = '没有权限执行此操作'
|
||||
break
|
||||
case 404:
|
||||
error.message = '请求的资源不存在'
|
||||
break
|
||||
case 500:
|
||||
error.message = '服务器内部错误'
|
||||
break
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
error.message = '服务暂时不可用,请稍后重试'
|
||||
break
|
||||
}
|
||||
|
||||
// 触发全局错误回调
|
||||
onError?.(error)
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return response.text()
|
||||
}
|
||||
|
||||
// 统一请求方法
|
||||
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${BASE_URL}${endpoint}`
|
||||
|
||||
try {
|
||||
const config = await requestInterceptor(options)
|
||||
const response = await fetch(url, config)
|
||||
return responseInterceptor(response)
|
||||
} catch (error) {
|
||||
// 处理网络错误
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
const networkError = new ApiError('网络连接失败,请检查网络', 0)
|
||||
onError?.(networkError)
|
||||
throw networkError
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// API 方法
|
||||
export const apiClient = {
|
||||
// 通用方法
|
||||
get: <T>(endpoint: string) => request<T>(endpoint, { method: 'GET' }),
|
||||
|
||||
post: <T>(endpoint: string, data?: unknown) => request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}),
|
||||
|
||||
delete: <T>(endpoint: string) => request<T>(endpoint, { method: 'DELETE' }),
|
||||
|
||||
// Channel APIs
|
||||
getChannels: () => request<ApiResponse>('/api/admin/channel', { method: 'GET' }),
|
||||
|
||||
saveChannel: (key: string, config: unknown) =>
|
||||
request(`/api/admin/channel/${encodeURIComponent(key)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
deleteChannel: (key: string) =>
|
||||
request(`/api/admin/channel/${encodeURIComponent(key)}`, { method: 'DELETE' }),
|
||||
|
||||
// Token APIs
|
||||
getTokens: () => request<ApiResponse>('/api/admin/token', { method: 'GET' }),
|
||||
|
||||
saveToken: (key: string, config: unknown) =>
|
||||
request(`/api/admin/token/${encodeURIComponent(key)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
deleteToken: (key: string) =>
|
||||
request(`/api/admin/token/${encodeURIComponent(key)}`, { method: 'DELETE' }),
|
||||
|
||||
// Pricing APIs
|
||||
getPricing: () => request<ApiResponse>('/api/admin/pricing', { method: 'GET' }),
|
||||
|
||||
savePricing: (config: unknown) =>
|
||||
request('/api/admin/pricing', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
// Database APIs
|
||||
initDatabase: () => request('/api/admin/db_initialize', { method: 'POST' }),
|
||||
|
||||
// API Test - 使用自定义 token,不走通用拦截器
|
||||
testApi: async (endpoint: string, token: string, body: unknown) => {
|
||||
const url = `${BASE_URL}${endpoint}`
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
return responseInterceptor(response)
|
||||
},
|
||||
|
||||
// Auth check
|
||||
checkAuth: () => request<ApiResponse>('/api/admin/channel', { method: 'GET' }),
|
||||
}
|
||||
Reference in new issue
Block a user