Files
PolyHermes/frontend/src/services/api.ts
T
WrBug fd25821e39 fix: 优化回测功能UI和修复问题
## 主要修改

### UI 优化
- 将回测功能移到跟单交易菜单下作为子菜单
- 创建回测改为使用 Modal 方式(移除独立页面)
- 优化导航路径,使用 useNavigate 替代 window.location.href

### 功能修复
- 修复 API 路径问题:移除重复的 /api 前缀(/api/backtest -> /backtest)
- 修复 copyRatio 输入框单位:改为百分比输入(100 表示 100%),与跟单配置保持一致
- 修复回测列表中 leader 列显示为空的问题:优先显示 leaderName,为空时显示 leaderAddress

### 多语言完善
- 补充缺失的多语言 key:
  - common.ascending / common.descending / common.day
  - backtest.sortBy / backtest.sortOrder / backtest.createdAt
  - backtest.copyRatioTooltip / backtest.copyRatioPlaceholder
  - 以及其他表单验证相关的 key
- 为中文简体、中文繁体、英文三种语言都添加了完整的翻译

## 修改的文件
- frontend/src/App.tsx - 移除 BacktestCreate 路由
- frontend/src/components/Layout.tsx - 调整菜单结构
- frontend/src/pages/BacktestList.tsx - Modal 创建、修复 leader 显示
- frontend/src/pages/BacktestDetail.tsx - 优化导航
- frontend/src/pages/BacktestCreate.tsx - 同步 copyRatio 修改
- frontend/src/services/api.ts - 修复 API 路径
- frontend/src/locales/* - 补充多语言配置
2026-01-31 07:46:16 +08:00

798 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios, { AxiosInstance, AxiosError } from 'axios'
import type { ApiResponse, NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest } from '../types'
import { getToken, setToken, removeToken } from '../utils'
import { wsManager } from './websocket'
import i18n from '../i18n/config'
/**
* API 基础配置
* 默认使用相对路径 /api(通过反向代理转发)
* 如果设置了 VITE_API_URL 环境变量,则使用完整 URL(用于跨域场景)
*/
const getBaseURL = (): string => {
const envApiUrl = import.meta.env.VITE_API_URL
if (envApiUrl) {
// 如果设置了环境变量,使用完整 URL(支持跨域)
return `${envApiUrl}/api`
}
// 否则使用相对路径(通过反向代理转发)
return '/api'
}
const apiClient: AxiosInstance = axios.create({
baseURL: getBaseURL(),
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
/**
* 获取当前语言设置(优先从 localStorage 读取,确保获取最新值)
*/
const getCurrentLanguage = (): string => {
// 优先从 localStorage 读取用户设置的语言(统一使用 i18n_language
let savedLanguage = localStorage.getItem('i18n_language')
// 如果 i18n_language 不存在,尝试从 i18nextLng 读取(i18next 默认使用的 key
if (!savedLanguage) {
savedLanguage = localStorage.getItem('i18nextLng')
}
// 如果设置了具体语言,使用设置的语言
if (savedLanguage && savedLanguage !== 'auto' && ['zh-CN', 'zh-TW', 'en'].includes(savedLanguage)) {
return savedLanguage
}
// 如果设置为 auto 或未设置,使用 i18n 的当前语言
// 如果 i18n.language 也没有,使用默认值 'en'
const currentLang = i18n.language || 'en'
// 确保返回的语言格式正确(移除可能的区域代码,如 'en-US' -> 'en'
if (currentLang.startsWith('zh-CN')) return 'zh-CN'
if (currentLang.startsWith('zh-TW') || currentLang.startsWith('zh-HK')) return 'zh-TW'
if (currentLang.startsWith('en')) return 'en'
return 'en'
}
/**
* 请求拦截器
*/
apiClient.interceptors.request.use(
(config) => {
// 从 localStorage 读取 token 并添加到请求头
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加语言 Header(每次请求都获取最新值)
const language = getCurrentLanguage()
config.headers['X-Language'] = language
return config
},
(error) => {
return Promise.reject(error)
}
)
/**
* 处理认证错误(自动登出)
*/
const handleAuthError = (code: number) => {
// 检查是否是认证错误(2001-2999)
if (code >= 2001 && code < 3000) {
// 清除 token
removeToken()
// 断开 WebSocket 连接
wsManager.disconnect()
// 跳转到登录页(避免循环跳转)
if (window.location.pathname !== '/login' && window.location.pathname !== '/reset-password') {
window.location.href = '/login'
}
}
}
/**
* 响应拦截器
*/
apiClient.interceptors.response.use(
(response) => {
// 检查响应头中是否有新的 token(自动刷新)
const newToken = response.headers['x-new-token']
if (newToken) {
setToken(newToken)
}
// 检查响应体中的 code,如果是认证错误,自动登出
// 后端可能返回 200 状态码,但 code 是 2001(认证失败)
const data = response.data as ApiResponse<any>
if (data && data.code !== undefined) {
handleAuthError(data.code)
}
return response
},
(error: AxiosError<ApiResponse<any>>) => {
if (error.response) {
const response = error.response
const data = response.data
// 检查是否是认证错误(2001-2999)
if (data && data.code !== undefined) {
handleAuthError(data.code)
}
console.error('API 错误:', data)
} else if (error.request) {
console.error('网络错误:', error.request)
} else {
console.error('请求错误:', error.message)
}
return Promise.reject(error)
}
)
/**
* API 服务
*/
export const apiService = {
/**
* 用户管理 API
*/
users: {
/**
* 获取用户列表
*/
list: () =>
apiClient.post<ApiResponse<any[]>>('/system/users/list', {}),
/**
* 创建用户
*/
create: (data: { username: string; password: string }) =>
apiClient.post<ApiResponse<any>>('/system/users/create', data),
/**
* 更新用户密码
*/
updatePassword: (data: { userId: number; newPassword: string }) =>
apiClient.post<ApiResponse<void>>('/system/users/update-password', data),
/**
* 删除用户
*/
delete: (data: { userId: number }) =>
apiClient.post<ApiResponse<void>>('/system/users/delete', data),
/**
* 用户修改自己的密码
*/
updateOwnPassword: (data: { newPassword: string }) =>
apiClient.post<ApiResponse<void>>('/system/users/update-own-password', data)
},
/**
* 认证 API
*/
auth: {
/**
* 登录
*/
login: (data: { username: string; password: string }) =>
apiClient.post<ApiResponse<{ token: string }>>('/auth/login', data),
/**
* 重置密码
*/
resetPassword: (data: { resetKey: string; username: string; newPassword: string }) =>
apiClient.post<ApiResponse<void>>('/auth/reset-password', data),
/**
* 检查是否首次使用
*/
checkFirstUse: () =>
apiClient.post<ApiResponse<{ isFirstUse: boolean }>>('/auth/check-first-use', {}),
/**
* 获取 WebSocket 连接票据
* 返回一个短期有效(30秒)的一次性票据
*/
getWebSocketTicket: () =>
apiClient.post<ApiResponse<{ ticket: string }>>('/auth/ws-ticket', {})
},
/**
* 账户管理 API
*/
accounts: {
/**
* 导入账户
*/
import: (data: any) =>
apiClient.post<ApiResponse<any>>('/accounts/import', data),
/**
* 更新账户
*/
update: (data: any) =>
apiClient.post<ApiResponse<any>>('/accounts/update', data),
/**
* 删除账户
*/
delete: (data: { accountId: number }) =>
apiClient.post<ApiResponse<void>>('/accounts/delete', data),
/**
* 查询账户列表
*/
list: () =>
apiClient.post<ApiResponse<any>>('/accounts/list', {}),
/**
* 查询账户详情
*/
detail: (data: { accountId?: number }) =>
apiClient.post<ApiResponse<any>>('/accounts/detail', data),
/**
* 查询账户余额
*/
balance: (data: { accountId?: number }) =>
apiClient.post<ApiResponse<any>>('/accounts/balance', data),
/**
* 查询所有账户的仓位列表
*/
positionsList: () =>
apiClient.post<ApiResponse<any>>('/accounts/positions/list', {}),
/**
* 卖出仓位
*/
sellPosition: (data: any) =>
apiClient.post<ApiResponse<any>>('/accounts/positions/sell', data),
/**
* 获取可赎回仓位统计
*/
getRedeemableSummary: (data: { accountId?: number }) =>
apiClient.post<ApiResponse<any>>('/accounts/positions/redeemable-summary', data),
/**
* 赎回仓位
*/
redeemPositions: (data: any) =>
apiClient.post<ApiResponse<any>>('/accounts/positions/redeem', data),
},
/**
* 市场数据 API
*/
markets: {
/**
* 获取市场价格(通过 Gamma API)
*/
getMarketPrice: (data: { marketId: string; outcomeIndex?: number }) =>
apiClient.post<ApiResponse<any>>('/markets/price', data),
/**
* 获取最新价(从订单表获取,供前端下单时显示)
*/
getLatestPrice: (data: { tokenId: string }) =>
apiClient.post<ApiResponse<any>>('/markets/latest-price', data)
},
/**
* Leader 管理 API
*/
leaders: {
/**
* 添加 Leader
*/
add: (data: { leaderAddress: string; leaderName?: string; remark?: string; website?: string; category?: string }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/leaders/add', data),
/**
* 更新 Leader
*/
update: (data: { leaderId: number; leaderName?: string; remark?: string; website?: string; category?: string }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/leaders/update', data),
/**
* 删除 Leader
*/
delete: (data: { leaderId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/leaders/delete', data),
/**
* 查询 Leader 列表
*/
list: (data: { category?: string } = {}) =>
apiClient.post<ApiResponse<any>>('/copy-trading/leaders/list', data),
/**
* 查询 Leader 详情
*/
detail: (data: { leaderId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/leaders/detail', data),
/**
* 查询 Leader 余额
*/
balance: (data: { leaderId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/leaders/balance', data)
},
/**
* 跟单模板管理 API(子菜单:跟单模板)
*/
templates: {
/**
* 创建模板
*/
create: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/templates/create', data),
/**
* 更新模板
*/
update: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/templates/update', data),
/**
* 删除模板
*/
delete: (data: { templateId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/templates/delete', data),
/**
* 复制模板
*/
copy: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/templates/copy', data),
/**
* 查询模板列表
*/
list: () =>
apiClient.post<ApiResponse<any>>('/copy-trading/templates/list', {}),
/**
* 查询模板详情
*/
detail: (data: { templateId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/templates/detail', data)
},
/**
* 跟单配置管理 API(子菜单:跟单配置)
*/
copyTrading: {
/**
* 创建跟单配置
* 支持两种方式:
* 1. 提供 templateId:从模板填充配置,可以覆盖部分字段
* 2. 不提供 templateId:手动输入所有配置参数
*/
create: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/configs/create', data),
/**
* 更新跟单配置
*/
update: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/configs/update', data),
/**
* 查询跟单列表
*/
list: (data: { accountId?: number; leaderId?: number; enabled?: boolean } = {}) =>
apiClient.post<ApiResponse<any>>('/copy-trading/configs/list', data),
/**
* 更新跟单状态(兼容旧接口)
*/
updateStatus: (data: { copyTradingId: number; enabled: boolean }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/configs/update-status', data),
/**
* 删除跟单
*/
delete: (data: { copyTradingId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/configs/delete', data),
/**
* 查询钱包绑定的跟单配置(兼容旧接口)
*/
getAccountTemplates: (data: { accountId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/configs/account-templates', data),
/**
* 查询被过滤订单列表
*/
getFilteredOrders: (data: {
copyTradingId: number
filterType?: string
page?: number
limit?: number
startTime?: number
endTime?: number
}) =>
apiClient.post<ApiResponse<any>>('/copy-trading/configs/filtered-orders', data)
},
/**
* 订单管理 API
*/
orders: {
/**
* 查询跟单订单列表
*/
list: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/orders/list', data),
/**
* 取消跟单订单
*/
cancel: (data: { copyOrderId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/orders/cancel', data)
},
/**
* 统计 API
*/
statistics: {
/**
* 获取全局统计
*/
global: (data: { startTime?: number; endTime?: number } = {}) =>
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/global', data),
/**
* 获取 Leader 统计
*/
leader: (data: { leaderId: number; startTime?: number; endTime?: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/leader', data),
/**
* 获取分类统计
*/
category: (data: { category: string; startTime?: number; endTime?: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/category', data),
/**
* 获取跟单关系统计详情
*/
detail: (data: { copyTradingId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/detail', data)
},
/**
* 订单跟踪 API
*/
orderTracking: {
/**
* 查询订单列表(买入/卖出/匹配)
*/
list: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/orders/tracking', data),
/**
* 查询按市场分组的订单列表(买入/卖出)
*/
listGroupedByMarket: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/orders/grouped-by-market', data)
},
/**
* 代理配置 API
*/
proxyConfig: {
/**
* 获取当前代理配置
*/
get: () =>
apiClient.post<ApiResponse<any>>('/system/proxy/get', {}),
/**
* 获取所有代理配置
*/
list: () =>
apiClient.post<ApiResponse<any[]>>('/system/proxy/list', {}),
/**
* 保存 HTTP 代理配置
*/
saveHttp: (data: {
enabled: boolean
host: string
port: number
username?: string
password?: string
}) =>
apiClient.post<ApiResponse<any>>('/system/proxy/http/save', data),
/**
* 检查代理是否可用
*/
check: () =>
apiClient.post<ApiResponse<{
success: boolean
message: string
responseTime?: number
}>>('/system/proxy/check', {}),
/**
* 删除代理配置
*/
delete: (data: { id: number }) =>
apiClient.post<ApiResponse<void>>('/system/proxy/delete', data),
/**
* 检查所有 API 的健康状态
*/
checkApiHealth: () =>
apiClient.post<ApiResponse<{
apis: Array<{
name: string
url: string
status: string
message: string
responseTime?: number
}>
}>>('/system/proxy/api-health-check', {})
},
/**
* 消息推送配置 API
*/
notifications: {
/**
* 获取配置列表
*/
list: (data?: { type?: string }) =>
apiClient.post<ApiResponse<NotificationConfig[]>>('/system/notifications/configs/list', data || {}),
/**
* 获取配置详情
*/
detail: (data: { id: number }) =>
apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/detail', data),
/**
* 创建配置
*/
create: (data: NotificationConfigRequest) =>
apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/create', data),
/**
* 更新配置
*/
update: (data: NotificationConfigUpdateRequest) =>
apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/update', data),
/**
* 更新启用状态
*/
updateEnabled: (data: { id: number; enabled: boolean }) =>
apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/update-enabled', data),
/**
* 删除配置
*/
delete: (data: { id: number }) =>
apiClient.post<ApiResponse<void>>('/system/notifications/configs/delete', data),
/**
* 测试通知
*/
test: (data?: { message?: string }) =>
apiClient.post<ApiResponse<boolean>>('/system/notifications/test', data || {}),
/**
* 获取 Telegram Chat IDs
*/
getTelegramChatIds: (data: { botToken: string }) =>
apiClient.post<ApiResponse<string[]>>('/system/notifications/telegram/get-chat-ids', data)
},
/**
* 系统配置 API
*/
systemConfig: {
/**
* 获取系统配置
*/
get: () =>
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/get', {}),
/**
* 更新 Builder API Key 配置
*/
updateBuilderApiKey: (data: import('../types').BuilderApiKeyUpdateRequest) =>
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/builder-api-key/update', data),
/**
* 检查 Builder API Key 是否已配置
*/
checkBuilderApiKey: () =>
apiClient.post<ApiResponse<{ configured: boolean }>>('/system/config/builder-api-key/check', {}),
/**
* 更新自动赎回配置
*/
updateAutoRedeem: (data: { enabled: boolean }) =>
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/auto-redeem/update', data),
/**
* 获取自动赎回状态
*/
getAutoRedeemStatus: () =>
apiClient.post<ApiResponse<{ enabled: boolean }>>('/system/config/auto-redeem/status', {})
},
/**
* 公告 API
*/
/**
* RPC 节点配置 API
*/
rpcNodes: {
list: () =>
apiClient.post<ApiResponse<import('../types').RpcNodeConfig[]>>('/system/rpc-nodes/list', {}),
add: (data: import('../types').RpcNodeAddRequest) =>
apiClient.post<ApiResponse<import('../types').RpcNodeConfig>>('/system/rpc-nodes/add', data),
update: (data: import('../types').RpcNodeUpdateRequest) =>
apiClient.post<ApiResponse<import('../types').RpcNodeConfig>>('/system/rpc-nodes/update', data),
delete: (data: { id: number }) =>
apiClient.post<ApiResponse<void>>('/system/rpc-nodes/delete', data),
updatePriority: (data: { id: number; priority: number }) =>
apiClient.post<ApiResponse<void>>('/system/rpc-nodes/update-priority', data),
checkHealth: (data: { id?: number }) =>
apiClient.post<ApiResponse<any>>('/system/rpc-nodes/check-health', data),
validate: (data: import('../types').RpcNodeAddRequest) =>
apiClient.post<ApiResponse<{ valid: boolean; message: string; responseTimeMs?: number }>>('/system/rpc-nodes/validate', data)
},
announcements: {
/**
* 获取公告列表(最近10条)
*/
list: (data?: { forceRefresh?: boolean }) =>
apiClient.post<ApiResponse<{
list: Array<{
id: number
title: string
body: string
author: string
authorAvatarUrl?: string
createdAt: number
updatedAt: number
reactions?: {
plusOne?: number
minusOne?: number
laugh?: number
confused?: number
heart?: number
hooray?: number
eyes?: number
rocket?: number
total?: number
}
}>
hasMore: boolean
total: number
}>>('/announcements/list', data || {}),
/**
* 获取公告详情
*/
detail: (data: { id?: number; forceRefresh?: boolean }) =>
apiClient.post<ApiResponse<{
id: number
title: string
body: string
author: string
authorAvatarUrl?: string
createdAt: number
updatedAt: number
reactions?: {
plusOne?: number
minusOne?: number
laugh?: number
confused?: number
heart?: number
hooray?: number
eyes?: number
rocket?: number
total?: number
}
}>>('/announcements/detail', data)
}
}
// 导出 apiClient 供需要直接使用 axios 实例的组件使用
export { apiClient }
export default apiService
/**
* 回测服务
*/
export const backtestService = {
/**
* 创建回测任务
*/
create: (data: {
taskName: string
leaderId: number
initialBalance: string
backtestDays: number
copyMode?: 'RATIO' | 'FIXED'
copyRatio?: string
fixedAmount?: string
maxOrderSize?: string
minOrderSize?: string
maxDailyLoss?: string
maxDailyOrders?: number
priceTolerance?: string
delaySeconds?: number
supportSell?: boolean
minOrderDepth?: string
maxSpread?: string
minPrice?: string
maxPrice?: string
maxPositionValue?: string
keywordFilterMode?: 'DISABLED' | 'WHITELIST' | 'BLACKLIST'
keywords?: string[]
maxMarketEndDate?: number | null
}) => apiClient.post('/backtest/tasks', data),
/**
* 查询回测任务列表
*/
list: (data: {
leaderId?: number
status?: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'STOPPED' | 'FAILED'
sortBy?: 'profitAmount' | 'profitRate' | 'createdAt'
sortOrder?: 'asc' | 'desc'
page: number
size: number
}) => apiClient.post('/backtest/tasks/list', data),
/**
* 查询回测任务详情
*/
detail: (data: { id: number }) => apiClient.post('/backtest/tasks/detail', data),
/**
* 查询回测交易记录
*/
trades: (data: {
taskId: number
page: number
size: number
}) => apiClient.post('/backtest/tasks/trades', data),
/**
* 停止回测任务
*/
stop: (data: { id: number }) => apiClient.post('/backtest/tasks/stop', data),
/**
* 删除回测任务
*/
delete: (data: { id: number }) => apiClient.post('/backtest/tasks/delete', data)
}