refactor(web): redesign httpClient with axios and unified error handling (#1061)

* fix(web): remove duplicate PasswordChecklist in error block
- Remove duplicate PasswordChecklist component from error message area
- Keep only the real-time password validation checklist
- Error block now displays only the error message text
Bug was introduced in commit aa0bd93 (PR #872)
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* refactor(web): redesign httpClient with axios and unified error handling
Major refactoring to improve error handling architecture:
## Changes
### 1. HTTP Client Redesign (httpClient.ts)
- Replaced native fetch with axios for better interceptor support
- Implemented request/response interceptors for centralized error handling
- Added automatic Bearer token injection in request interceptor
- Network errors and system errors (404, 403, 500) now intercepted and shown via toast
- Only business logic errors (4xx except 401/403/404) returned to caller
- New ApiResponse<T> interface for type-safe responses
### 2. API Migration (api.ts)
- Migrated all 31 API methods from legacy fetch-style to new httpClient
- Updated pattern: from `res.ok/res.json()` to `result.success/result.data`
- Removed getAuthHeaders() helper (token now auto-injected)
- Added TypeScript generics for better type safety
### 3. Component Updates
- AuthContext.tsx: Updated register() to use new API
- TraderConfigModal.tsx: Migrated 3 API calls (config, templates, balance)
- RegisterPage.tsx: Simplified error display (error type handling now in API layer)
### 4. Removed Legacy Code
- Removed legacyHttpClient compatibility wrapper (~30 lines)
- Removed legacyRequest() method
- Clean separation: API layer handles all error classification
## Benefits
- Centralized error handling - no need to check network/system errors in components
- Better UX - automatic toast notifications for system errors
- Type safety - generic ApiResponse<T> provides compile-time checks
- Cleaner business components - only handle business logic errors
- Consistent error messages across the application
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
---------
Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
Ember
2025-11-17 14:48:14 +08:00
committed by GitHub
parent b60383f22b
commit cdb7a6ba06
7 changed files with 428 additions and 346 deletions

View File

@@ -18,117 +18,92 @@ import { httpClient } from './httpClient'
const API_BASE = '/api'
// Helper function to get auth headers
function getAuthHeaders(): Record<string, string> {
const token = localStorage.getItem('auth_token')
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
return headers
}
export const api = {
// AI交易员管理接口
async getTraders(): Promise<TraderInfo[]> {
const res = await httpClient.get(`${API_BASE}/my-traders`, getAuthHeaders())
if (!res.ok) throw new Error('获取trader列表失败')
return res.json()
const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`)
if (!result.success) throw new Error('获取trader列表失败')
return result.data!
},
// 获取公开的交易员列表(无需认证)
async getPublicTraders(): Promise<any[]> {
const res = await httpClient.get(`${API_BASE}/traders`)
if (!res.ok) throw new Error('获取公开trader列表失败')
return res.json()
const result = await httpClient.get<any[]>(`${API_BASE}/traders`)
if (!result.success) throw new Error('获取公开trader列表失败')
return result.data!
},
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
const res = await httpClient.post(
const result = await httpClient.post<TraderInfo>(
`${API_BASE}/traders`,
request,
getAuthHeaders()
request
)
if (!res.ok) throw new Error('创建交易员失败')
return res.json()
if (!result.success) throw new Error('创建交易员失败')
return result.data!
},
async deleteTrader(traderId: string): Promise<void> {
const res = await httpClient.delete(
`${API_BASE}/traders/${traderId}`,
getAuthHeaders()
)
if (!res.ok) throw new Error('删除交易员失败')
const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`)
if (!result.success) throw new Error('删除交易员失败')
},
async startTrader(traderId: string): Promise<void> {
const res = await httpClient.post(
`${API_BASE}/traders/${traderId}/start`,
undefined,
getAuthHeaders()
const result = await httpClient.post(
`${API_BASE}/traders/${traderId}/start`
)
if (!res.ok) throw new Error('启动交易员失败')
if (!result.success) throw new Error('启动交易员失败')
},
async stopTrader(traderId: string): Promise<void> {
const res = await httpClient.post(
`${API_BASE}/traders/${traderId}/stop`,
undefined,
getAuthHeaders()
)
if (!res.ok) throw new Error('停止交易员失败')
const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`)
if (!result.success) throw new Error('停止交易员失败')
},
async updateTraderPrompt(
traderId: string,
customPrompt: string
): Promise<void> {
const res = await httpClient.put(
const result = await httpClient.put(
`${API_BASE}/traders/${traderId}/prompt`,
{ custom_prompt: customPrompt },
getAuthHeaders()
{ custom_prompt: customPrompt }
)
if (!res.ok) throw new Error('更新自定义策略失败')
if (!result.success) throw new Error('更新自定义策略失败')
},
async getTraderConfig(traderId: string): Promise<TraderConfigData> {
const res = await httpClient.get(
`${API_BASE}/traders/${traderId}/config`,
getAuthHeaders()
const result = await httpClient.get<TraderConfigData>(
`${API_BASE}/traders/${traderId}/config`
)
if (!res.ok) throw new Error('获取交易员配置失败')
return res.json()
if (!result.success) throw new Error('获取交易员配置失败')
return result.data!
},
async updateTrader(
traderId: string,
request: CreateTraderRequest
): Promise<TraderInfo> {
const res = await httpClient.put(
const result = await httpClient.put<TraderInfo>(
`${API_BASE}/traders/${traderId}`,
request,
getAuthHeaders()
request
)
if (!res.ok) throw new Error('更新交易员失败')
return res.json()
if (!result.success) throw new Error('更新交易员失败')
return result.data!
},
// AI模型配置接口
async getModelConfigs(): Promise<AIModel[]> {
const res = await httpClient.get(`${API_BASE}/models`, getAuthHeaders())
if (!res.ok) throw new Error('获取模型配置失败')
return res.json()
const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`)
if (!result.success) throw new Error('获取模型配置失败')
return result.data!
},
// 获取系统支持的AI模型列表无需认证
async getSupportedModels(): Promise<AIModel[]> {
const res = await httpClient.get(`${API_BASE}/supported-models`)
if (!res.ok) throw new Error('获取支持的模型失败')
return res.json()
const result = await httpClient.get<AIModel[]>(
`${API_BASE}/supported-models`
)
if (!result.success) throw new Error('获取支持的模型失败')
return result.data!
},
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
@@ -150,37 +125,31 @@ export const api = {
)
// 发送加密数据
const res = await httpClient.put(
`${API_BASE}/models`,
encryptedPayload,
getAuthHeaders()
)
if (!res.ok) throw new Error('更新模型配置失败')
const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload)
if (!result.success) throw new Error('更新模型配置失败')
},
// 交易所配置接口
async getExchangeConfigs(): Promise<Exchange[]> {
const res = await httpClient.get(`${API_BASE}/exchanges`, getAuthHeaders())
if (!res.ok) throw new Error('获取交易所配置失败')
return res.json()
const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`)
if (!result.success) throw new Error('获取交易所配置失败')
return result.data!
},
// 获取系统支持的交易所列表(无需认证)
async getSupportedExchanges(): Promise<Exchange[]> {
const res = await httpClient.get(`${API_BASE}/supported-exchanges`)
if (!res.ok) throw new Error('获取支持的交易所失败')
return res.json()
const result = await httpClient.get<Exchange[]>(
`${API_BASE}/supported-exchanges`
)
if (!result.success) throw new Error('获取支持的交易所失败')
return result.data!
},
async updateExchangeConfigs(
request: UpdateExchangeConfigRequest
): Promise<void> {
const res = await httpClient.put(
`${API_BASE}/exchanges`,
request,
getAuthHeaders()
)
if (!res.ok) throw new Error('更新交易所配置失败')
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
if (!result.success) throw new Error('更新交易所配置失败')
},
// 使用加密传输更新交易所配置
@@ -205,12 +174,11 @@ export const api = {
)
// 发送加密数据
const res = await httpClient.put(
const result = await httpClient.put(
`${API_BASE}/exchanges`,
encryptedPayload,
getAuthHeaders()
encryptedPayload
)
if (!res.ok) throw new Error('更新交易所配置失败')
if (!result.success) throw new Error('更新交易所配置失败')
},
// 获取系统状态支持trader_id
@@ -218,9 +186,9 @@ export const api = {
const url = traderId
? `${API_BASE}/status?trader_id=${traderId}`
: `${API_BASE}/status`
const res = await httpClient.get(url, getAuthHeaders())
if (!res.ok) throw new Error('获取系统状态失败')
return res.json()
const result = await httpClient.get<SystemStatus>(url)
if (!result.success) throw new Error('获取系统状态失败')
return result.data!
},
// 获取账户信息支持trader_id
@@ -228,17 +196,10 @@ export const api = {
const url = traderId
? `${API_BASE}/account?trader_id=${traderId}`
: `${API_BASE}/account`
const res = await httpClient.request(url, {
cache: 'no-store',
headers: {
...getAuthHeaders(),
'Cache-Control': 'no-cache',
},
})
if (!res.ok) throw new Error('获取账户信息失败')
const data = await res.json()
console.log('Account data fetched:', data)
return data
const result = await httpClient.get<AccountInfo>(url)
if (!result.success) throw new Error('获取账户信息失败')
console.log('Account data fetched:', result.data)
return result.data!
},
// 获取持仓列表支持trader_id
@@ -246,9 +207,9 @@ export const api = {
const url = traderId
? `${API_BASE}/positions?trader_id=${traderId}`
: `${API_BASE}/positions`
const res = await httpClient.get(url, getAuthHeaders())
if (!res.ok) throw new Error('获取持仓列表失败')
return res.json()
const result = await httpClient.get<Position[]>(url)
if (!result.success) throw new Error('获取持仓列表失败')
return result.data!
},
// 获取决策日志支持trader_id
@@ -256,9 +217,9 @@ export const api = {
const url = traderId
? `${API_BASE}/decisions?trader_id=${traderId}`
: `${API_BASE}/decisions`
const res = await httpClient.get(url, getAuthHeaders())
if (!res.ok) throw new Error('获取决策日志失败')
return res.json()
const result = await httpClient.get<DecisionRecord[]>(url)
if (!result.success) throw new Error('获取决策日志失败')
return result.data!
},
// 获取最新决策支持trader_id和limit参数
@@ -272,12 +233,11 @@ export const api = {
}
params.append('limit', limit.toString())
const res = await httpClient.get(
`${API_BASE}/decisions/latest?${params}`,
getAuthHeaders()
const result = await httpClient.get<DecisionRecord[]>(
`${API_BASE}/decisions/latest?${params}`
)
if (!res.ok) throw new Error('获取最新决策失败')
return res.json()
if (!result.success) throw new Error('获取最新决策失败')
return result.data!
},
// 获取统计信息支持trader_id
@@ -285,9 +245,9 @@ export const api = {
const url = traderId
? `${API_BASE}/statistics?trader_id=${traderId}`
: `${API_BASE}/statistics`
const res = await httpClient.get(url, getAuthHeaders())
if (!res.ok) throw new Error('获取统计信息失败')
return res.json()
const result = await httpClient.get<Statistics>(url)
if (!result.success) throw new Error('获取统计信息失败')
return result.data!
},
// 获取收益率历史数据支持trader_id
@@ -295,32 +255,35 @@ export const api = {
const url = traderId
? `${API_BASE}/equity-history?trader_id=${traderId}`
: `${API_BASE}/equity-history`
const res = await httpClient.get(url, getAuthHeaders())
if (!res.ok) throw new Error('获取历史数据失败')
return res.json()
const result = await httpClient.get<any[]>(url)
if (!result.success) throw new Error('获取历史数据失败')
return result.data!
},
// 批量获取多个交易员的历史数据(无需认证)
async getEquityHistoryBatch(traderIds: string[]): Promise<any> {
const res = await httpClient.post(`${API_BASE}/equity-history-batch`, {
trader_ids: traderIds,
})
if (!res.ok) throw new Error('获取批量历史数据失败')
return res.json()
const result = await httpClient.post<any>(
`${API_BASE}/equity-history-batch`,
{ trader_ids: traderIds }
)
if (!result.success) throw new Error('获取批量历史数据失败')
return result.data!
},
// 获取前5名交易员数据无需认证
async getTopTraders(): Promise<any[]> {
const res = await httpClient.get(`${API_BASE}/top-traders`)
if (!res.ok) throw new Error('获取前5名交易员失败')
return res.json()
const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`)
if (!result.success) throw new Error('获取前5名交易员失败')
return result.data!
},
// 获取公开交易员配置(无需认证)
async getPublicTraderConfig(traderId: string): Promise<any> {
const res = await httpClient.get(`${API_BASE}/trader/${traderId}/config`)
if (!res.ok) throw new Error('获取公开交易员配置失败')
return res.json()
const result = await httpClient.get<any>(
`${API_BASE}/trader/${traderId}/config`
)
if (!result.success) throw new Error('获取公开交易员配置失败')
return result.data!
},
// 获取AI学习表现分析支持trader_id
@@ -328,16 +291,18 @@ export const api = {
const url = traderId
? `${API_BASE}/performance?trader_id=${traderId}`
: `${API_BASE}/performance`
const res = await httpClient.get(url, getAuthHeaders())
if (!res.ok) throw new Error('获取AI学习数据失败')
return res.json()
const result = await httpClient.get<any>(url)
if (!result.success) throw new Error('获取AI学习数据失败')
return result.data!
},
// 获取竞赛数据(无需认证)
async getCompetition(): Promise<CompetitionData> {
const res = await httpClient.get(`${API_BASE}/competition`)
if (!res.ok) throw new Error('获取竞赛数据失败')
return res.json()
const result = await httpClient.get<CompetitionData>(
`${API_BASE}/competition`
)
if (!result.success) throw new Error('获取竞赛数据失败')
return result.data!
},
// 用户信号源配置接口
@@ -345,27 +310,23 @@ export const api = {
coin_pool_url: string
oi_top_url: string
}> {
const res = await httpClient.get(
`${API_BASE}/user/signal-sources`,
getAuthHeaders()
)
if (!res.ok) throw new Error('获取用户信号源配置失败')
return res.json()
const result = await httpClient.get<{
coin_pool_url: string
oi_top_url: string
}>(`${API_BASE}/user/signal-sources`)
if (!result.success) throw new Error('获取用户信号源配置失败')
return result.data!
},
async saveUserSignalSource(
coinPoolUrl: string,
oiTopUrl: string
): Promise<void> {
const res = await httpClient.post(
`${API_BASE}/user/signal-sources`,
{
coin_pool_url: coinPoolUrl,
oi_top_url: oiTopUrl,
},
getAuthHeaders()
)
if (!res.ok) throw new Error('保存用户信号源配置失败')
const result = await httpClient.post(`${API_BASE}/user/signal-sources`, {
coin_pool_url: coinPoolUrl,
oi_top_url: oiTopUrl,
})
if (!result.success) throw new Error('保存用户信号源配置失败')
},
// 获取服务器IP需要认证用于白名单配置
@@ -373,8 +334,11 @@ export const api = {
public_ip: string
message: string
}> {
const res = await httpClient.get(`${API_BASE}/server-ip`, getAuthHeaders())
if (!res.ok) throw new Error('获取服务器IP失败')
return res.json()
const result = await httpClient.get<{
public_ip: string
message: string
}>(`${API_BASE}/server-ip`)
if (!result.success) throw new Error('获取服务器IP失败')
return result.data!
},
}