feat: 完成前端多语言支持和菜单优化

- 缩短菜单标题,解决显示不全问题
- 添加跟随系统语言选项,作为默认选项
- 移除语言切换时的页面刷新,实现无刷新切换
- 完成主要页面的多语言替换:
  * ConfigPage - 全局配置页面
  * ResetPassword - 重置密码页面
  * LeaderList - Leader 列表页面
  * UserList - 用户列表页面
  * Statistics - 统计信息页面
  * OrderList - 订单列表页面
  * TemplateList - 模板列表页面
  * CopyTradingList - 跟单配置列表页面
- 添加简体中文、繁体中文和英文的完整翻译键
- 优化语言设置页面,支持跟随系统语言
This commit is contained in:
WrBug
2025-12-04 10:36:28 +08:00
parent 88be178cde
commit 1a9407c544
51 changed files with 4829 additions and 835 deletions
@@ -0,0 +1,134 @@
package com.wrbug.polymarketbot.config
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import java.util.*
/**
* 语言拦截器
* 从 HTTP Header 读取语言设置,并设置到 LocaleContextHolder
*
* 支持的 Header
* - Accept-Language: 标准 HTTP Header(如 zh-CN, zh-TW, en
* - X-Language: 自定义 Header(如 zh-CN, zh-TW, en
*
* 语言映射规则:
* - zh-CN, zh -> zh-CN (简体中文)
* - zh-TW, zh-HK -> zh-TW (繁体中文)
* - 其他 -> en (英文,默认)
*/
@Component
class LocaleInterceptor : HandlerInterceptor {
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
// 优先从 X-Language Header 读取
val language = request.getHeader("X-Language")
?: request.getHeader("Accept-Language")
?: "en"
// 解析语言
val locale = parseLocale(language)
// 设置到 LocaleContextHolder,供 MessageSource 使用
LocaleContextHolder.setLocale(locale)
return true
}
/**
* 解析语言字符串为 Locale
* 支持格式:zh-CN, zh_TW, zh, en, en-US 等
*/
private fun parseLocale(language: String): Locale {
// 移除空格并转为小写
val lang = language.trim().lowercase()
// 处理 zh-CN, zh_CN 等格式
if (lang.startsWith("zh")) {
// 检查是否是繁体中文
if (lang.contains("tw") || lang.contains("hk") || lang.contains("mo")) {
return Locale("zh", "TW")
}
// 默认简体中文
return Locale("zh", "CN")
}
// 英文(默认)
return Locale("en")
}
}
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import java.util.*
/**
* 语言拦截器
* 从 HTTP Header 读取语言设置,并设置到 LocaleContextHolder
*
* 支持的 Header
* - Accept-Language: 标准 HTTP Header(如 zh-CN, zh-TW, en
* - X-Language: 自定义 Header(如 zh-CN, zh-TW, en
*
* 语言映射规则:
* - zh-CN, zh -> zh-CN (简体中文)
* - zh-TW, zh-HK -> zh-TW (繁体中文)
* - 其他 -> en (英文,默认)
*/
@Component
class LocaleInterceptor : HandlerInterceptor {
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
// 优先从 X-Language Header 读取
val language = request.getHeader("X-Language")
?: request.getHeader("Accept-Language")
?: "en"
// 解析语言
val locale = parseLocale(language)
// 设置到 LocaleContextHolder,供 MessageSource 使用
LocaleContextHolder.setLocale(locale)
return true
}
/**
* 解析语言字符串为 Locale
* 支持格式:zh-CN, zh_TW, zh, en, en-US 等
*/
private fun parseLocale(language: String): Locale {
// 移除空格并转为小写
val lang = language.trim().lowercase()
// 处理 zh-CN, zh_CN 等格式
if (lang.startsWith("zh")) {
// 检查是否是繁体中文
if (lang.contains("tw") || lang.contains("hk") || lang.contains("mo")) {
return Locale("zh", "TW")
}
// 默认简体中文
return Locale("zh", "CN")
}
// 英文(默认)
return Locale("en")
}
}
@@ -6,14 +6,32 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
/**
* Web MVC 配置
* 注册JWT认证拦截器
* 注册JWT认证拦截器和语言拦截器
*/
@Configuration
class WebMvcConfig(
private val jwtAuthenticationInterceptor: JwtAuthenticationInterceptor
private val jwtAuthenticationInterceptor: JwtAuthenticationInterceptor,
private val localeInterceptor: LocaleInterceptor
) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
// 先注册语言拦截器(优先级更高)
registry.addInterceptor(localeInterceptor)
.addPathPatterns("/api/**")
// 再注册JWT认证拦截器
registry.addInterceptor(jwtAuthenticationInterceptor)
.addPathPatterns("/api/**")
}
}
registry.addInterceptor(jwtAuthenticationInterceptor)
.addPathPatterns("/api/**")
}
}
registry.addInterceptor(jwtAuthenticationInterceptor)
.addPathPatterns("/api/**")
}
@@ -6,6 +6,7 @@ import com.wrbug.polymarketbot.service.AccountService
import com.wrbug.polymarketbot.util.toSafeBigDecimal
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.math.BigDecimal
@@ -16,7 +17,8 @@ import java.math.BigDecimal
@RestController
@RequestMapping("/api/copy-trading/accounts")
class AccountController(
private val accountService: AccountService
private val accountService: AccountService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(AccountController::class.java)
@@ -29,10 +31,10 @@ class AccountController(
return try {
// 参数验证
if (request.privateKey.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_PRIVATE_KEY_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_PRIVATE_KEY_EMPTY, messageSource = messageSource))
}
if (request.walletAddress.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_WALLET_ADDRESS_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_WALLET_ADDRESS_EMPTY, messageSource = messageSource))
}
val result = accountService.importAccount(request)
@@ -46,17 +48,18 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_IMPORT_FAILED, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_IMPORT_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("导入账户异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_IMPORT_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_IMPORT_FAILED, e.message, messageSource))
}
}
@@ -77,17 +80,18 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_UPDATE_FAILED, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_UPDATE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("更新账户异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_UPDATE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_UPDATE_FAILED, e.message, messageSource))
}
}
@@ -108,24 +112,26 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
is IllegalStateException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.BUSINESS_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DELETE_FAILED, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DELETE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("删除账户异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DELETE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DELETE_FAILED, e.message, messageSource))
}
}
@@ -142,12 +148,12 @@ class AccountController(
},
onFailure = { e ->
logger.error("查询账户列表失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_LIST_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("查询账户列表异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_LIST_FETCH_FAILED, e.message, messageSource))
}
}
@@ -168,14 +174,16 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.SERVER_ACCOUNT_DETAIL_FETCH_FAILED,
e.message
e.message,
messageSource
)
)
}
@@ -183,7 +191,7 @@ class AccountController(
)
} catch (e: Exception) {
logger.error("查询账户详情异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DETAIL_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DETAIL_FETCH_FAILED, e.message, messageSource))
}
}
@@ -204,14 +212,16 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.SERVER_ACCOUNT_BALANCE_FETCH_FAILED,
e.message
e.message,
messageSource
)
)
}
@@ -219,7 +229,7 @@ class AccountController(
)
} catch (e: Exception) {
logger.error("查询账户余额异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_BALANCE_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_BALANCE_FETCH_FAILED, e.message, messageSource))
}
}
@@ -236,12 +246,12 @@ class AccountController(
},
onFailure = { e ->
logger.error("查询仓位列表失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_POSITIONS_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_POSITIONS_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("查询仓位列表异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_POSITIONS_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_POSITIONS_FETCH_FAILED, e.message, messageSource))
}
}
@@ -253,35 +263,35 @@ class AccountController(
return try {
// 参数验证
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
}
if (request.marketId.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource))
}
// side 可以是任意结果名称(如 "YES", "NO", "Pakistan" 等),不再限制为 YES/NO
if (request.side.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_SIDE_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_SIDE_EMPTY, messageSource = messageSource))
}
if (request.orderType !in listOf("MARKET", "LIMIT")) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ORDER_TYPE_MUST_BE_MARKET_OR_LIMIT))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ORDER_TYPE_MUST_BE_MARKET_OR_LIMIT, messageSource = messageSource))
}
// 如果传了 percent,不需要校验 quantity;如果没传 percent,必须提供 quantity
if (request.percent.isNullOrBlank() && request.quantity.isNullOrBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_QUANTITY_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_QUANTITY_EMPTY, messageSource = messageSource))
}
// 如果传了 percent,验证百分比值必须在 0-100 之间(支持小数)
if (!request.percent.isNullOrBlank()) {
try {
val percent = request.percent.toSafeBigDecimal()
if (percent <= BigDecimal.ZERO || percent > BigDecimal.valueOf(100)) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "卖出百分比必须在 0-100 之间"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "卖出百分比必须在 0-100 之间", messageSource))
}
} catch (e: Exception) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "卖出百分比格式不正确: ${e.message}"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "卖出百分比格式不正确: ${e.message}", messageSource))
}
}
if (request.orderType == "LIMIT" && (request.price == null || request.price.isBlank())) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_PRICE_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_PRICE_EMPTY, messageSource = messageSource))
}
val result = runBlocking { accountService.sellPosition(request) }
@@ -295,21 +305,24 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
is IllegalStateException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.BUSINESS_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.SERVER_ACCOUNT_ORDER_CREATE_FAILED,
e.message
e.message,
messageSource
)
)
}
@@ -317,7 +330,7 @@ class AccountController(
)
} catch (e: Exception) {
logger.error("创建卖出订单异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_ORDER_CREATE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_ORDER_CREATE_FAILED, e.message, messageSource))
}
}
@@ -338,14 +351,16 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.SERVER_ERROR,
"获取可赎回仓位统计失败: ${e.message}"
"获取可赎回仓位统计失败: ${e.message}",
messageSource
)
)
}
@@ -353,7 +368,7 @@ class AccountController(
)
} catch (e: Exception) {
logger.error("获取可赎回仓位统计异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取可赎回仓位统计失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取可赎回仓位统计失败: ${e.message}", messageSource))
}
}
@@ -365,19 +380,19 @@ class AccountController(
return try {
// 参数验证
if (request.positions.isEmpty()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_REDEEM_POSITIONS_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_REDEEM_POSITIONS_EMPTY, messageSource = messageSource))
}
// 验证每个仓位项
for (item in request.positions) {
if (item.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
}
if (item.marketId.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource))
}
if (item.outcomeIndex < 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_INDEX_SETS_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_INDEX_SETS_INVALID, messageSource = messageSource))
}
}
@@ -392,21 +407,24 @@ class AccountController(
is IllegalArgumentException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.PARAM_ERROR,
e.message
e.message,
messageSource
)
)
is IllegalStateException -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.BUSINESS_ERROR,
e.message
e.message,
messageSource
)
)
else -> ResponseEntity.ok(
ApiResponse.error(
ErrorCode.SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED,
e.message
e.message,
messageSource
)
)
}
@@ -414,7 +432,7 @@ class AccountController(
)
} catch (e: Exception) {
logger.error("赎回仓位异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED, e.message, messageSource))
}
}
@@ -5,6 +5,7 @@ import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.AuthService
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/auth")
class AuthController(
private val authService: AuthService
private val authService: AuthService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(AuthController::class.java)
@@ -26,10 +28,10 @@ class AuthController(
fun login(@RequestBody request: LoginRequest): ResponseEntity<ApiResponse<LoginResponse>> {
return try {
if (request.username.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("用户名不能为空"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "用户名不能为空", messageSource))
}
if (request.password.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("密码不能为空"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "密码不能为空", messageSource))
}
val result = authService.login(request.username, request.password)
@@ -42,18 +44,18 @@ class AuthController(
when (e) {
is IllegalArgumentException -> {
if (e.message == ErrorCode.AUTH_USERNAME_OR_PASSWORD_ERROR.message) {
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_USERNAME_OR_PASSWORD_ERROR.code, e.message ?: "用户名或密码错误"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_USERNAME_OR_PASSWORD_ERROR, messageSource = messageSource))
} else {
ResponseEntity.ok(ApiResponse.paramError(e.message ?: "参数错误"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
}
}
else -> ResponseEntity.ok(ApiResponse.serverError("登录失败: ${e.message}"))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "登录失败: ${e.message}", messageSource))
}
}
)
} catch (e: Exception) {
logger.error("登录异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("登录失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "登录失败: ${e.message}", messageSource))
}
}
@@ -67,13 +69,13 @@ class AuthController(
): ResponseEntity<ApiResponse<Unit>> {
return try {
if (request.resetKey.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("重置密钥不能为空"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "重置密钥不能为空", messageSource))
}
if (request.username.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("用户名不能为空"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "用户名不能为空", messageSource))
}
if (request.newPassword.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("新密码不能为空"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "新密码不能为空", messageSource))
}
val result = authService.resetPassword(
@@ -95,25 +97,25 @@ class AuthController(
is IllegalArgumentException -> {
if (e.message == ErrorCode.AUTH_PASSWORD_WEAK.message) {
// 密码强度错误可以提示
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PASSWORD_WEAK.code, e.message ?: "密码长度不符合要求"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PASSWORD_WEAK, messageSource = messageSource))
} else {
// 其他错误统一返回"重置失败"
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR.code, "重置失败"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "重置失败", messageSource))
}
}
is IllegalStateException -> {
// 频率限制等错误统一返回"重置失败"
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR.code, "重置失败"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "重置失败", messageSource))
}
else -> {
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR.code, "重置失败"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "重置失败", messageSource))
}
}
}
)
} catch (e: Exception) {
logger.error("重置密码异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("重置密码失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "重置密码失败: ${e.message}", messageSource))
}
}
@@ -127,7 +129,7 @@ class AuthController(
ResponseEntity.ok(ApiResponse.success(CheckFirstUseResponse(isFirstUse = isFirstUse)))
} catch (e: Exception) {
logger.error("检查首次使用异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("检查首次使用失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "检查首次使用失败: ${e.message}", messageSource))
}
}
}
@@ -4,6 +4,7 @@ import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.CopyTradingService
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/copy-trading")
class CopyTradingController(
private val copyTradingService: CopyTradingService
private val copyTradingService: CopyTradingService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(CopyTradingController::class.java)
@@ -25,13 +27,13 @@ class CopyTradingController(
fun createCopyTrading(@RequestBody request: CopyTradingCreateRequest): ResponseEntity<ApiResponse<CopyTradingDto>> {
return try {
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
}
if (request.templateId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID, messageSource = messageSource))
}
if (request.leaderId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID, messageSource = messageSource))
}
val result = copyTradingService.createCopyTrading(request)
@@ -42,14 +44,14 @@ class CopyTradingController(
onFailure = { e ->
logger.error("创建跟单失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("创建跟单异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message, messageSource))
}
}
@@ -66,12 +68,12 @@ class CopyTradingController(
},
onFailure = { e ->
logger.error("查询跟单列表失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_LIST_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("查询跟单列表异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_LIST_FETCH_FAILED, e.message, messageSource))
}
}
@@ -82,7 +84,7 @@ class CopyTradingController(
fun updateCopyTradingStatus(@RequestBody request: CopyTradingUpdateStatusRequest): ResponseEntity<ApiResponse<CopyTradingDto>> {
return try {
if (request.copyTradingId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID, messageSource = messageSource))
}
val result = copyTradingService.updateCopyTradingStatus(request)
@@ -93,15 +95,15 @@ class CopyTradingController(
onFailure = { e ->
logger.error("更新跟单状态失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_UPDATE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_UPDATE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("更新跟单状态异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_UPDATE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_UPDATE_FAILED, e.message, messageSource))
}
}
@@ -112,7 +114,7 @@ class CopyTradingController(
fun deleteCopyTrading(@RequestBody request: CopyTradingDeleteRequest): ResponseEntity<ApiResponse<Unit>> {
return try {
if (request.copyTradingId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID, messageSource = messageSource))
}
val result = copyTradingService.deleteCopyTrading(request.copyTradingId)
@@ -123,14 +125,14 @@ class CopyTradingController(
onFailure = { e ->
logger.error("删除跟单失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_DELETE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_DELETE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("删除跟单异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_DELETE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_DELETE_FAILED, e.message, messageSource))
}
}
@@ -141,7 +143,7 @@ class CopyTradingController(
fun getAccountTemplates(@RequestBody request: AccountTemplatesRequest): ResponseEntity<ApiResponse<AccountTemplatesResponse>> {
return try {
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
}
val result = copyTradingService.getAccountTemplates(request.accountId)
@@ -152,14 +154,14 @@ class CopyTradingController(
onFailure = { e ->
logger.error("查询钱包绑定的模板失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_TEMPLATES_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_TEMPLATES_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("查询钱包绑定的模板异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_TEMPLATES_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_TEMPLATES_FETCH_FAILED, e.message, messageSource))
}
}
}
@@ -5,6 +5,7 @@ import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.CopyTradingStatisticsService
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -15,7 +16,8 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/copy-trading/statistics")
class CopyTradingStatisticsController(
private val statisticsService: CopyTradingStatisticsService
private val statisticsService: CopyTradingStatisticsService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(CopyTradingStatisticsController::class.java)
@@ -28,7 +30,7 @@ class CopyTradingStatisticsController(
fun getStatisticsDetail(@RequestBody request: StatisticsDetailRequest): ResponseEntity<ApiResponse<CopyTradingStatisticsResponse>> {
return try {
if (request.copyTradingId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID, messageSource = messageSource))
}
val result = runBlocking { statisticsService.getStatistics(request.copyTradingId) }
@@ -39,14 +41,14 @@ class CopyTradingStatisticsController(
onFailure = { e ->
logger.error("获取统计信息失败: copyTradingId=${request.copyTradingId}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("获取统计信息异常: copyTradingId=${request.copyTradingId}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
@@ -67,14 +69,14 @@ class CopyTradingStatisticsController(
onFailure = { e ->
logger.error("获取全局统计失败", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("获取全局统计异常", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
@@ -86,7 +88,7 @@ class CopyTradingStatisticsController(
fun getLeaderStatistics(@RequestBody request: LeaderStatisticsRequest): ResponseEntity<ApiResponse<StatisticsResponse>> {
return try {
if (request.leaderId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID, messageSource = messageSource))
}
val result = runBlocking {
@@ -99,14 +101,14 @@ class CopyTradingStatisticsController(
onFailure = { e ->
logger.error("获取 Leader 统计失败: leaderId=${request.leaderId}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("获取 Leader 统计异常: leaderId=${request.leaderId}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
@@ -118,11 +120,11 @@ class CopyTradingStatisticsController(
fun getCategoryStatistics(@RequestBody request: CategoryStatisticsRequest): ResponseEntity<ApiResponse<StatisticsResponse>> {
return try {
if (request.category.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "分类不能为空"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "分类不能为空", messageSource))
}
if (request.category != "sports" && request.category != "crypto") {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "分类必须是 sports 或 crypto"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "分类必须是 sports 或 crypto", messageSource))
}
val result = runBlocking {
@@ -135,14 +137,14 @@ class CopyTradingStatisticsController(
onFailure = { e ->
logger.error("获取分类统计失败: category=${request.category}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("获取分类统计异常: category=${request.category}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_STATISTICS_FETCH_FAILED, e.message, messageSource))
}
}
}
@@ -154,7 +156,8 @@ class CopyTradingStatisticsController(
@RestController
@RequestMapping("/api/copy-trading/orders")
class CopyOrderTrackingController(
private val statisticsService: CopyTradingStatisticsService
private val statisticsService: CopyTradingStatisticsService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(CopyOrderTrackingController::class.java)
@@ -167,16 +170,16 @@ class CopyOrderTrackingController(
fun getOrderList(@RequestBody request: OrderTrackingRequest): ResponseEntity<ApiResponse<OrderListResponse>> {
return try {
if (request.copyTradingId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID, messageSource = messageSource))
}
if (request.type.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "订单类型不能为空"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "订单类型不能为空", messageSource))
}
val validTypes = listOf("buy", "sell", "matched")
if (!validTypes.contains(request.type.lowercase())) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ORDER_TYPE_INVALID_FOR_TRACKING))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ORDER_TYPE_INVALID_FOR_TRACKING, messageSource = messageSource))
}
val result = statisticsService.getOrderList(request)
@@ -187,14 +190,14 @@ class CopyOrderTrackingController(
onFailure = { e ->
logger.error("查询订单列表失败: copyTradingId=${request.copyTradingId}, type=${request.type}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ORDER_TRACKING_LIST_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ORDER_TRACKING_LIST_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("查询订单列表异常: copyTradingId=${request.copyTradingId}, type=${request.type}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ORDER_TRACKING_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ORDER_TRACKING_LIST_FETCH_FAILED, e.message, messageSource))
}
}
}
@@ -4,6 +4,7 @@ import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.CopyTradingTemplateService
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/copy-trading/templates")
class CopyTradingTemplateController(
private val templateService: CopyTradingTemplateService
private val templateService: CopyTradingTemplateService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(CopyTradingTemplateController::class.java)
@@ -25,7 +27,7 @@ class CopyTradingTemplateController(
fun createTemplate(@RequestBody request: TemplateCreateRequest): ResponseEntity<ApiResponse<TemplateDto>> {
return try {
if (request.templateName.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_NAME_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_NAME_EMPTY, messageSource = messageSource))
}
val result = templateService.createTemplate(request)
@@ -36,14 +38,14 @@ class CopyTradingTemplateController(
onFailure = { e ->
logger.error("创建模板失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_CREATE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_CREATE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("创建模板异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_CREATE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_CREATE_FAILED, e.message, messageSource))
}
}
@@ -54,7 +56,7 @@ class CopyTradingTemplateController(
fun updateTemplate(@RequestBody request: TemplateUpdateRequest): ResponseEntity<ApiResponse<TemplateDto>> {
return try {
if (request.templateId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID, messageSource = messageSource))
}
val result = templateService.updateTemplate(request)
@@ -65,14 +67,14 @@ class CopyTradingTemplateController(
onFailure = { e ->
logger.error("更新模板失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_UPDATE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_UPDATE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("更新模板异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_UPDATE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_UPDATE_FAILED, e.message, messageSource))
}
}
@@ -83,7 +85,7 @@ class CopyTradingTemplateController(
fun deleteTemplate(@RequestBody request: TemplateDeleteRequest): ResponseEntity<ApiResponse<Unit>> {
return try {
if (request.templateId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID, messageSource = messageSource))
}
val result = templateService.deleteTemplate(request.templateId)
@@ -94,15 +96,15 @@ class CopyTradingTemplateController(
onFailure = { e ->
logger.error("删除模板失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DELETE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DELETE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("删除模板异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DELETE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DELETE_FAILED, e.message, messageSource))
}
}
@@ -113,10 +115,10 @@ class CopyTradingTemplateController(
fun copyTemplate(@RequestBody request: TemplateCopyRequest): ResponseEntity<ApiResponse<TemplateDto>> {
return try {
if (request.templateId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID, messageSource = messageSource))
}
if (request.templateName.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_NAME_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_NAME_EMPTY, messageSource = messageSource))
}
val result = templateService.copyTemplate(request)
@@ -127,14 +129,14 @@ class CopyTradingTemplateController(
onFailure = { e ->
logger.error("复制模板失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_COPY_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_COPY_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("复制模板异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_COPY_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_COPY_FAILED, e.message, messageSource))
}
}
@@ -151,12 +153,12 @@ class CopyTradingTemplateController(
},
onFailure = { e ->
logger.error("查询模板列表失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_LIST_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("查询模板列表异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_LIST_FETCH_FAILED, e.message, messageSource))
}
}
@@ -167,7 +169,7 @@ class CopyTradingTemplateController(
fun getTemplateDetail(@RequestBody request: TemplateDetailRequest): ResponseEntity<ApiResponse<TemplateDto>> {
return try {
if (request.templateId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID, messageSource = messageSource))
}
val result = templateService.getTemplateDetail(request.templateId)
@@ -178,14 +180,14 @@ class CopyTradingTemplateController(
onFailure = { e ->
logger.error("查询模板详情失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DETAIL_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DETAIL_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("查询模板详情异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DETAIL_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_TEMPLATE_DETAIL_FETCH_FAILED, e.message, messageSource))
}
}
}
@@ -4,6 +4,7 @@ import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.LeaderService
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/copy-trading/leaders")
class LeaderController(
private val leaderService: LeaderService
private val leaderService: LeaderService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(LeaderController::class.java)
@@ -25,7 +27,7 @@ class LeaderController(
fun addLeader(@RequestBody request: LeaderAddRequest): ResponseEntity<ApiResponse<LeaderDto>> {
return try {
if (request.leaderAddress.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ADDRESS_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ADDRESS_EMPTY, messageSource = messageSource))
}
val result = leaderService.addLeader(request)
@@ -36,15 +38,15 @@ class LeaderController(
onFailure = { e ->
logger.error("添加 Leader 失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_ADD_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_ADD_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("添加 Leader 异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_ADD_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_ADD_FAILED, e.message, messageSource))
}
}
@@ -55,7 +57,7 @@ class LeaderController(
fun updateLeader(@RequestBody request: LeaderUpdateRequest): ResponseEntity<ApiResponse<LeaderDto>> {
return try {
if (request.leaderId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID, messageSource = messageSource))
}
val result = leaderService.updateLeader(request)
@@ -66,14 +68,14 @@ class LeaderController(
onFailure = { e ->
logger.error("更新 Leader 失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_UPDATE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_UPDATE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("更新 Leader 异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_UPDATE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_UPDATE_FAILED, e.message, messageSource))
}
}
@@ -84,7 +86,7 @@ class LeaderController(
fun deleteLeader(@RequestBody request: LeaderDeleteRequest): ResponseEntity<ApiResponse<Unit>> {
return try {
if (request.leaderId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID, messageSource = messageSource))
}
val result = leaderService.deleteLeader(request.leaderId)
@@ -95,15 +97,15 @@ class LeaderController(
onFailure = { e ->
logger.error("删除 Leader 失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DELETE_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DELETE_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("删除 Leader 异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DELETE_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DELETE_FAILED, e.message, messageSource))
}
}
@@ -120,12 +122,12 @@ class LeaderController(
},
onFailure = { e ->
logger.error("查询 Leader 列表失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_LIST_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("查询 Leader 列表异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_LIST_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_LIST_FETCH_FAILED, e.message, messageSource))
}
}
@@ -136,7 +138,7 @@ class LeaderController(
fun getLeaderDetail(@RequestBody request: LeaderDetailRequest): ResponseEntity<ApiResponse<LeaderDto>> {
return try {
if (request.leaderId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID, messageSource = messageSource))
}
val result = leaderService.getLeaderDetail(request.leaderId)
@@ -147,14 +149,14 @@ class LeaderController(
onFailure = { e ->
logger.error("查询 Leader 详情失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DETAIL_FETCH_FAILED, e.message))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DETAIL_FETCH_FAILED, e.message, messageSource))
}
}
)
} catch (e: Exception) {
logger.error("查询 Leader 详情异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DETAIL_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_LEADER_DETAIL_FETCH_FAILED, e.message, messageSource))
}
}
}
@@ -166,3 +168,7 @@ data class LeaderDetailRequest(
val leaderId: Long
)
@@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.service.AccountService
import com.wrbug.polymarketbot.service.PolymarketClobService
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -18,7 +19,8 @@ import org.springframework.web.bind.annotation.*
@RequestMapping("/api/copy-trading/markets")
class MarketController(
private val accountService: AccountService,
private val clobService: PolymarketClobService
private val clobService: PolymarketClobService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(MarketController::class.java)
@@ -31,7 +33,7 @@ class MarketController(
fun getMarketPrice(@RequestBody request: MarketPriceRequest): ResponseEntity<ApiResponse<MarketPriceResponse>> {
return try {
if (request.marketId.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource))
}
val result = runBlocking { accountService.getMarketPrice(request.marketId) }
@@ -41,12 +43,12 @@ class MarketController(
},
onFailure = { e ->
logger.error("获取市场价格失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("获取市场价格异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message, messageSource))
}
}
@@ -59,7 +61,7 @@ class MarketController(
fun getLatestPrice(@RequestBody request: LatestPriceRequest): ResponseEntity<ApiResponse<LatestPriceResponse>> {
return try {
if (request.tokenId.isBlank()) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TOKEN_ID_EMPTY))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TOKEN_ID_EMPTY, messageSource = messageSource))
}
val result = runBlocking { clobService.getLatestPrice(request.tokenId) }
@@ -69,13 +71,17 @@ class MarketController(
},
onFailure = { e ->
logger.error("获取最新价失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("获取最新价异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message, messageSource))
}
}
}
@@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.service.ProxyConfigService
import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -17,7 +18,8 @@ import org.springframework.web.bind.annotation.*
@RequestMapping("/api/proxy-config")
class ProxyConfigController(
private val proxyConfigService: ProxyConfigService,
private val apiHealthCheckService: ApiHealthCheckService
private val apiHealthCheckService: ApiHealthCheckService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(ProxyConfigController::class.java)
@@ -32,7 +34,7 @@ class ProxyConfigController(
ResponseEntity.ok(ApiResponse.success(config))
} catch (e: Exception) {
logger.error("获取代理配置失败", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置失败:${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置失败:${e.message}", messageSource))
}
}
@@ -46,7 +48,7 @@ class ProxyConfigController(
ResponseEntity.ok(ApiResponse.success(configs))
} catch (e: Exception) {
logger.error("获取代理配置列表失败", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置列表失败:${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置列表失败:${e.message}", messageSource))
}
}
@@ -62,11 +64,11 @@ class ProxyConfigController(
} else {
val error = result.exceptionOrNull()
logger.error("保存 HTTP 代理配置失败", error)
ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, error?.message ?: "保存失败"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, error?.message ?: "保存失败", messageSource))
}
} catch (e: Exception) {
logger.error("保存 HTTP 代理配置异常", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "保存 HTTP 代理配置失败:${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "保存 HTTP 代理配置失败:${e.message}", messageSource))
}
}
@@ -80,7 +82,7 @@ class ProxyConfigController(
ResponseEntity.ok(ApiResponse.success(result))
} catch (e: Exception) {
logger.error("代理检查失败", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "代理检查失败:${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "代理检查失败:${e.message}", messageSource))
}
}
@@ -91,7 +93,7 @@ class ProxyConfigController(
fun deleteProxyConfig(@RequestBody request: Map<String, Long>): ResponseEntity<ApiResponse<Unit>> {
return try {
val id = request["id"] ?: return ResponseEntity.ok(
ApiResponse.error(ErrorCode.PARAM_ERROR, "参数错误:缺少 id")
ApiResponse.error(ErrorCode.PARAM_ERROR, "参数错误:缺少 id", messageSource)
)
val result = proxyConfigService.deleteProxyConfig(id)
@@ -100,11 +102,11 @@ class ProxyConfigController(
} else {
val error = result.exceptionOrNull()
logger.error("删除代理配置失败:id=$id", error)
ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, error?.message ?: "删除失败"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, error?.message ?: "删除失败", messageSource))
}
} catch (e: Exception) {
logger.error("删除代理配置异常", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "删除代理配置失败:${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "删除代理配置失败:${e.message}", messageSource))
}
}
@@ -118,8 +120,12 @@ class ProxyConfigController(
ResponseEntity.ok(ApiResponse.success(result))
} catch (e: Exception) {
logger.error("API 健康检查失败", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "API 健康检查失败:${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "API 健康检查失败:${e.message}", messageSource))
}
}
}
@@ -5,6 +5,7 @@ import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.UserService
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/users")
class UserController(
private val userService: UserService
private val userService: UserService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(UserController::class.java)
@@ -49,14 +51,14 @@ class UserController(
return try {
val currentUsername = getCurrentUsername(request)
if (currentUsername == null) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR.code, "未获取到用户信息"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "未获取到用户信息", messageSource))
}
val users = userService.getUserList(currentUsername)
ResponseEntity.ok(ApiResponse.success(users))
} catch (e: Exception) {
logger.error("获取用户列表异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("获取用户列表失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取用户列表失败: ${e.message}", messageSource))
}
}
@@ -84,15 +86,15 @@ class UserController(
onFailure = { e ->
logger.error("创建用户失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.paramError(e.message ?: "参数错误"))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED.code, e.message ?: "权限不足"))
else -> ResponseEntity.ok(ApiResponse.serverError("创建用户失败: ${e.message}"))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "创建用户失败: ${e.message}", messageSource))
}
}
)
} catch (e: Exception) {
logger.error("创建用户异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("创建用户失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "创建用户失败: ${e.message}", messageSource))
}
}
@@ -120,15 +122,15 @@ class UserController(
onFailure = { e ->
logger.error("更新用户密码失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.paramError(e.message ?: "参数错误"))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED.code, e.message ?: "权限不足"))
else -> ResponseEntity.ok(ApiResponse.serverError("更新用户密码失败: ${e.message}"))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "更新用户密码失败: ${e.message}", messageSource))
}
}
)
} catch (e: Exception) {
logger.error("更新用户密码异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("更新用户密码失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "更新用户密码失败: ${e.message}", messageSource))
}
}
@@ -143,7 +145,7 @@ class UserController(
return try {
val currentUsername = getCurrentUsername(httpRequest)
if (currentUsername == null) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR.code, "未获取到用户信息"))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_ERROR, "未获取到用户信息", messageSource))
}
val result = userService.updateOwnPassword(requestBody.newPassword, currentUsername)
@@ -155,14 +157,14 @@ class UserController(
onFailure = { e ->
logger.error("修改自己密码失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.paramError(e.message ?: "参数错误"))
else -> ResponseEntity.ok(ApiResponse.serverError("修改密码失败: ${e.message}"))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "修改密码失败: ${e.message}", messageSource))
}
}
)
} catch (e: Exception) {
logger.error("修改自己密码异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("修改密码失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "修改密码失败: ${e.message}", messageSource))
}
}
@@ -177,7 +179,7 @@ class UserController(
return try {
val error = checkDefaultUser(httpRequest)
if (error != null) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED.code, error))
return ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED, error, messageSource))
}
val currentUsername = getCurrentUsername(httpRequest)!!
@@ -190,15 +192,15 @@ class UserController(
onFailure = { e ->
logger.error("删除用户失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.paramError(e.message ?: "参数错误"))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED.code, e.message ?: "权限不足"))
else -> ResponseEntity.ok(ApiResponse.serverError("删除用户失败: ${e.message}"))
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.AUTH_PERMISSION_DENIED, e.message, messageSource))
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "删除用户失败: ${e.message}", messageSource))
}
}
)
} catch (e: Exception) {
logger.error("删除用户异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("删除用户失败: ${e.message}"))
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "删除用户失败: ${e.message}", messageSource))
}
}
}
@@ -1,12 +1,14 @@
package com.wrbug.polymarketbot.dto
import com.wrbug.polymarketbot.enums.ErrorCode
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
/**
* 统一API响应格式
* @param code 响应码,0表示成功,非0表示失败
* @param data 响应数据,可以是任意类型(对象、数组、字符串、数字等)
* @param msg 响应消息,成功时通常为空,失败时包含错误提示
* @param msg 响应消息,成功时通常为空,失败时包含错误提示(已国际化)
*/
data class ApiResponse<T>(
val code: Int,
@@ -22,13 +24,40 @@ data class ApiResponse<T>(
}
/**
* 创建失败响应(使用 ErrorCode 枚举)
* 创建失败响应(使用 ErrorCode 枚举,支持多语言
* @param errorCode 错误码枚举
* @param customMsg 自定义消息(可选,如果提供则使用自定义消息,否则使用国际化消息)
* @param messageSource 消息源(可选,如果提供则使用国际化,否则使用默认消息)
*/
fun <T> error(errorCode: ErrorCode, customMsg: String? = null): ApiResponse<T> {
fun <T> error(
errorCode: ErrorCode,
customMsg: String? = null,
messageSource: MessageSource? = null
): ApiResponse<T> {
val msg: String = if (customMsg != null) {
customMsg
} else if (messageSource != null) {
// 使用 MessageSource 获取国际化消息
try {
messageSource.getMessage(
errorCode.messageKey,
null,
errorCode.message, // 默认消息(fallback
LocaleContextHolder.getLocale()
) ?: errorCode.message
} catch (e: Exception) {
// 如果获取失败,使用默认消息
errorCode.message
}
} else {
// 如果没有提供 MessageSource,使用默认消息
errorCode.message
}
return ApiResponse(
code = errorCode.code,
data = null,
msg = customMsg ?: errorCode.message
msg = msg
)
}
@@ -19,202 +19,203 @@ package com.wrbug.polymarketbot.enums
*/
enum class ErrorCode(
val code: Int,
val message: String
val message: String,
val messageKey: String
) {
// ==================== 参数错误 (1001-1999) ====================
PARAM_ERROR(1001, "参数错误"),
PARAM_EMPTY(1002, "参数不能为空"),
PARAM_INVALID(1003, "参数无效"),
PARAM_ERROR(1001, "参数错误", "error.param.error"),
PARAM_EMPTY(1002, "参数不能为空", "error.param.empty"),
PARAM_INVALID(1003, "参数无效", "error.param.invalid"),
// 账户相关参数错误
PARAM_PRIVATE_KEY_EMPTY(1101, "私钥不能为空"),
PARAM_WALLET_ADDRESS_EMPTY(1102, "钱包地址不能为空"),
PARAM_WALLET_ADDRESS_INVALID(1103, "钱包地址格式无效"),
PARAM_ACCOUNT_ID_INVALID(1104, "账户ID无效"),
PARAM_ACCOUNT_NAME_EMPTY(1105, "账户名称不能为空"),
PARAM_PRIVATE_KEY_EMPTY(1101, "私钥不能为空", "error.param.private_key_empty"),
PARAM_WALLET_ADDRESS_EMPTY(1102, "钱包地址不能为空", "error.param.wallet_address_empty"),
PARAM_WALLET_ADDRESS_INVALID(1103, "钱包地址格式无效", "error.param.wallet_address_invalid"),
PARAM_ACCOUNT_ID_INVALID(1104, "账户ID无效", "error.param.account_id_invalid"),
PARAM_ACCOUNT_NAME_EMPTY(1105, "账户名称不能为空", "error.param.account_name_empty"),
// Leader 相关参数错误
PARAM_LEADER_ADDRESS_EMPTY(1201, "Leader 地址不能为空"),
PARAM_LEADER_ADDRESS_INVALID(1202, "Leader 地址格式无效"),
PARAM_LEADER_ID_INVALID(1203, "Leader ID 无效"),
PARAM_LEADER_NAME_EMPTY(1204, "Leader 名称不能为空"),
PARAM_CATEGORY_INVALID(1205, "分类无效,只支持 sports 或 crypto"),
PARAM_LEADER_ADDRESS_EMPTY(1201, "Leader 地址不能为空", "error.param.leader_address_empty"),
PARAM_LEADER_ADDRESS_INVALID(1202, "Leader 地址格式无效", "error.param.leader_address_invalid"),
PARAM_LEADER_ID_INVALID(1203, "Leader ID 无效", "error.param.leader_id_invalid"),
PARAM_LEADER_NAME_EMPTY(1204, "Leader 名称不能为空", "error.param.leader_name_empty"),
PARAM_CATEGORY_INVALID(1205, "分类无效,只支持 sports 或 crypto", "error.param.category_invalid"),
// 模板相关参数错误
PARAM_TEMPLATE_NAME_EMPTY(1301, "模板名称不能为空"),
PARAM_TEMPLATE_ID_INVALID(1302, "模板 ID 无效"),
PARAM_COPY_MODE_INVALID(1303, "copyMode 必须是 RATIO 或 FIXED"),
PARAM_COPY_RATIO_INVALID(1304, "跟单比例无效"),
PARAM_FIXED_AMOUNT_INVALID(1305, "固定金额无效"),
PARAM_TEMPLATE_NAME_EMPTY(1301, "模板名称不能为空", "error.param.template_name_empty"),
PARAM_TEMPLATE_ID_INVALID(1302, "模板 ID 无效", "error.param.template_id_invalid"),
PARAM_COPY_MODE_INVALID(1303, "copyMode 必须是 RATIO 或 FIXED", "error.param.copy_mode_invalid"),
PARAM_COPY_RATIO_INVALID(1304, "跟单比例无效", "error.param.copy_ratio_invalid"),
PARAM_FIXED_AMOUNT_INVALID(1305, "固定金额无效", "error.param.fixed_amount_invalid"),
// 跟单相关参数错误
PARAM_COPY_TRADING_ID_INVALID(1401, "跟单关系ID无效"),
PARAM_ORDER_TYPE_INVALID(1402, "订单类型无效"),
PARAM_ORDER_TYPE_MUST_BE_MARKET_OR_LIMIT(1403, "订单类型必须是MARKET或LIMIT"),
PARAM_QUANTITY_EMPTY(1404, "数量不能为空"),
PARAM_PRICE_EMPTY(1405, "限价订单必须提供价格"),
PARAM_SIDE_EMPTY(1406, "方向不能为空"),
PARAM_MARKET_ID_EMPTY(1407, "市场ID不能为空"),
PARAM_ORDER_TYPE_EMPTY(1408, "订单类型不能为空"),
PARAM_COPY_TRADING_ID_INVALID(1401, "跟单关系ID无效", "error.param.copy_trading_id_invalid"),
PARAM_ORDER_TYPE_INVALID(1402, "订单类型无效", "error.param.order_type_invalid"),
PARAM_ORDER_TYPE_MUST_BE_MARKET_OR_LIMIT(1403, "订单类型必须是MARKET或LIMIT", "error.param.order_type_must_be_market_or_limit"),
PARAM_QUANTITY_EMPTY(1404, "数量不能为空", "error.param.quantity_empty"),
PARAM_PRICE_EMPTY(1405, "限价订单必须提供价格", "error.param.price_empty"),
PARAM_SIDE_EMPTY(1406, "方向不能为空", "error.param.side_empty"),
PARAM_MARKET_ID_EMPTY(1407, "市场ID不能为空", "error.param.market_id_empty"),
PARAM_ORDER_TYPE_EMPTY(1408, "订单类型不能为空", "error.param.order_type_empty"),
// 市场相关参数错误
PARAM_TOKEN_ID_EMPTY(1501, "tokenId 不能为空"),
PARAM_CONDITION_ID_EMPTY(1502, "conditionId 不能为空"),
PARAM_REDEEM_POSITIONS_EMPTY(1503, "赎回仓位列表不能为空"),
PARAM_INDEX_SETS_INVALID(1504, "结果索引无效"),
PARAM_TOKEN_ID_EMPTY(1501, "tokenId 不能为空", "error.param.token_id_empty"),
PARAM_CONDITION_ID_EMPTY(1502, "conditionId 不能为空", "error.param.condition_id_empty"),
PARAM_REDEEM_POSITIONS_EMPTY(1503, "赎回仓位列表不能为空", "error.param.redeem_positions_empty"),
PARAM_INDEX_SETS_INVALID(1504, "结果索引无效", "error.param.index_sets_invalid"),
// 统计相关参数错误
PARAM_ORDER_TYPE_INVALID_FOR_TRACKING(1601, "订单类型无效,必须是: buy, sell, matched"),
PARAM_ORDER_TYPE_INVALID_FOR_TRACKING(1601, "订单类型无效,必须是: buy, sell, matched", "error.param.order_type_invalid_for_tracking"),
// ==================== 认证/权限错误 (2001-2999) ====================
AUTH_ERROR(2001, "认证失败"),
AUTH_TOKEN_INVALID(2002, "认证令牌无效"),
AUTH_TOKEN_EXPIRED(2003, "认证令牌已过期"),
AUTH_PERMISSION_DENIED(2004, "权限不足"),
AUTH_API_KEY_INVALID(2005, "API Key 无效"),
AUTH_API_SECRET_INVALID(2006, "API Secret 无效"),
AUTH_API_PASSPHRASE_INVALID(2007, "API Passphrase 无效"),
AUTH_API_CREDENTIALS_MISSING(2008, "API 凭证未配置"),
AUTH_USERNAME_OR_PASSWORD_ERROR(2009, "用户名或密码错误"),
AUTH_RESET_KEY_INVALID(2010, "重置密钥错误"),
AUTH_RESET_PASSWORD_RATE_LIMIT(2011, "频率限制:1分钟内最多尝试3次,请稍后再试"),
AUTH_USER_NOT_FOUND(2012, "用户不存在"),
AUTH_PASSWORD_WEAK(2013, "密码长度不符合要求,至少6位"),
AUTH_ERROR(2001, "认证失败", "error.auth.error"),
AUTH_TOKEN_INVALID(2002, "认证令牌无效", "error.auth.token_invalid"),
AUTH_TOKEN_EXPIRED(2003, "认证令牌已过期", "error.auth.token_expired"),
AUTH_PERMISSION_DENIED(2004, "权限不足", "error.auth.permission_denied"),
AUTH_API_KEY_INVALID(2005, "API Key 无效", "error.auth.api_key_invalid"),
AUTH_API_SECRET_INVALID(2006, "API Secret 无效", "error.auth.api_secret_invalid"),
AUTH_API_PASSPHRASE_INVALID(2007, "API Passphrase 无效", "error.auth.api_passphrase_invalid"),
AUTH_API_CREDENTIALS_MISSING(2008, "API 凭证未配置", "error.auth.api_credentials_missing"),
AUTH_USERNAME_OR_PASSWORD_ERROR(2009, "用户名或密码错误", "error.auth.username_or_password_error"),
AUTH_RESET_KEY_INVALID(2010, "重置密钥错误", "error.auth.reset_key_invalid"),
AUTH_RESET_PASSWORD_RATE_LIMIT(2011, "频率限制:1分钟内最多尝试3次,请稍后再试", "error.auth.reset_password_rate_limit"),
AUTH_USER_NOT_FOUND(2012, "用户不存在", "error.auth.user_not_found"),
AUTH_PASSWORD_WEAK(2013, "密码长度不符合要求,至少6位", "error.auth.password_weak"),
// ==================== 资源不存在 (3001-3999) ====================
NOT_FOUND(3001, "资源不存在"),
ACCOUNT_NOT_FOUND(3002, "账户不存在"),
LEADER_NOT_FOUND(3003, "Leader 不存在"),
TEMPLATE_NOT_FOUND(3004, "模板不存在"),
COPY_TRADING_NOT_FOUND(3005, "跟单关系不存在"),
MARKET_NOT_FOUND(3006, "市场不存在"),
ORDER_NOT_FOUND(3007, "订单不存在"),
POSITION_NOT_FOUND(3008, "仓位不存在"),
NOT_FOUND(3001, "资源不存在", "error.not_found"),
ACCOUNT_NOT_FOUND(3002, "账户不存在", "error.account_not_found"),
LEADER_NOT_FOUND(3003, "Leader 不存在", "error.leader_not_found"),
TEMPLATE_NOT_FOUND(3004, "模板不存在", "error.template_not_found"),
COPY_TRADING_NOT_FOUND(3005, "跟单关系不存在", "error.copy_trading_not_found"),
MARKET_NOT_FOUND(3006, "市场不存在", "error.market_not_found"),
ORDER_NOT_FOUND(3007, "订单不存在", "error.order_not_found"),
POSITION_NOT_FOUND(3008, "仓位不存在", "error.position_not_found"),
// ==================== 业务逻辑错误 (4001-4999) ====================
BUSINESS_ERROR(4001, "业务逻辑错误"),
BUSINESS_ERROR(4001, "业务逻辑错误", "error.business.error"),
// Leader 管理 (4001-4099)
LEADER_ALREADY_EXISTS(4001, "该 Leader 地址已存在"),
LEADER_ADDRESS_SAME_AS_ACCOUNT(4002, "Leader 地址不能与自己的账户地址相同"),
LEADER_HAS_COPY_TRADINGS(4003, "该 Leader 还有跟单关系,请先删除跟单关系"),
LEADER_ALREADY_EXISTS(4001, "该 Leader 地址已存在", "error.leader_already_exists"),
LEADER_ADDRESS_SAME_AS_ACCOUNT(4002, "Leader 地址不能与自己的账户地址相同", "error.leader_address_same_as_account"),
LEADER_HAS_COPY_TRADINGS(4003, "该 Leader 还有跟单关系,请先删除跟单关系", "error.leader_has_copy_tradings"),
// 模板管理 (4101-4199)
TEMPLATE_NAME_ALREADY_EXISTS(4101, "模板名称已存在"),
TEMPLATE_HAS_COPY_TRADINGS(4102, "该模板还有跟单关系在使用,请先删除跟单关系"),
TEMPLATE_NAME_ALREADY_EXISTS(4101, "模板名称已存在", "error.template_name_already_exists"),
TEMPLATE_HAS_COPY_TRADINGS(4102, "该模板还有跟单关系在使用,请先删除跟单关系", "error.template_has_copy_tradings"),
// 跟单管理 (4201-4299)
COPY_TRADING_ALREADY_EXISTS(4201, "该跟单关系已存在"),
COPY_TRADING_DISABLED(4202, "跟单关系已禁用"),
COPY_TRADING_ENABLED(4203, "跟单关系已启用"),
NO_ENABLED_COPY_TRADINGS(4204, "没有启用的跟单关系"),
COPY_TRADING_ALREADY_EXISTS(4201, "该跟单关系已存在", "error.copy_trading_already_exists"),
COPY_TRADING_DISABLED(4202, "跟单关系已禁用", "error.copy_trading_disabled"),
COPY_TRADING_ENABLED(4203, "跟单关系已启用", "error.copy_trading_enabled"),
NO_ENABLED_COPY_TRADINGS(4204, "没有启用的跟单关系", "error.no_enabled_copy_tradings"),
// 订单相关 (4301-4399)
ORDER_CREATE_FAILED(4301, "创建订单失败"),
ORDER_CANCEL_FAILED(4302, "取消订单失败"),
ORDER_NOT_MATCHED(4303, "订单未匹配"),
ORDER_ALREADY_FILLED(4304, "订单已成交"),
ORDER_INSUFFICIENT_BALANCE(4305, "余额不足"),
ORDER_AMOUNT_TOO_SMALL(4306, "订单金额低于最小限制"),
ORDER_AMOUNT_TOO_LARGE(4307, "订单金额超过最大限制"),
ORDER_PRICE_INVALID(4308, "订单价格无效"),
ORDER_QUANTITY_INVALID(4309, "订单数量无效"),
ORDER_CREATE_FAILED(4301, "创建订单失败", "error.order_create_failed"),
ORDER_CANCEL_FAILED(4302, "取消订单失败", "error.order_cancel_failed"),
ORDER_NOT_MATCHED(4303, "订单未匹配", "error.order_not_matched"),
ORDER_ALREADY_FILLED(4304, "订单已成交", "error.order_already_filled"),
ORDER_INSUFFICIENT_BALANCE(4305, "余额不足", "error.order_insufficient_balance"),
ORDER_AMOUNT_TOO_SMALL(4306, "订单金额低于最小限制", "error.order_amount_too_small"),
ORDER_AMOUNT_TOO_LARGE(4307, "订单金额超过最大限制", "error.order_amount_too_large"),
ORDER_PRICE_INVALID(4308, "订单价格无效", "error.order_price_invalid"),
ORDER_QUANTITY_INVALID(4309, "订单数量无效", "error.order_quantity_invalid"),
// 市场相关 (4401-4499)
MARKET_PRICE_FETCH_FAILED(4401, "获取市场价格失败"),
MARKET_ORDERBOOK_EMPTY(4402, "订单簿为空"),
MARKET_TOKEN_ID_INVALID(4403, "Token ID 无效"),
MARKET_PRICE_FETCH_FAILED(4401, "获取市场价格失败", "error.market_price_fetch_failed"),
MARKET_ORDERBOOK_EMPTY(4402, "订单簿为空", "error.market_orderbook_empty"),
MARKET_TOKEN_ID_INVALID(4403, "Token ID 无效", "error.market_token_id_invalid"),
// 仓位相关 (4501-4599)
POSITION_REDEEM_FAILED(4501, "赎回仓位失败"),
POSITION_NOT_REDEEMABLE(4502, "仓位不可赎回"),
POSITION_INSUFFICIENT(4503, "仓位不足"),
POSITION_ALREADY_REDEEMED(4504, "仓位已赎回"),
POSITION_REDEEM_FAILED(4501, "赎回仓位失败", "error.position_redeem_failed"),
POSITION_NOT_REDEEMABLE(4502, "仓位不可赎回", "error.position_not_redeemable"),
POSITION_INSUFFICIENT(4503, "仓位不足", "error.position_insufficient"),
POSITION_ALREADY_REDEEMED(4504, "仓位已赎回", "error.position_already_redeemed"),
// 账户相关业务错误 (4601-4699)
ACCOUNT_ALREADY_EXISTS(4601, "账户已存在"),
ACCOUNT_IS_DEFAULT(4602, "账户已是默认账户"),
ACCOUNT_HAS_ACTIVE_ORDERS(4603, "账户有活跃订单"),
ACCOUNT_IS_LAST_ONE(4604, "不能删除最后一个账户"),
ACCOUNT_API_KEY_CREATE_FAILED(4605, "自动获取 API Key 失败"),
ACCOUNT_PROXY_ADDRESS_FETCH_FAILED(4606, "获取代理地址失败"),
ACCOUNT_BALANCE_FETCH_FAILED(4607, "查询账户余额失败"),
ACCOUNT_POSITIONS_FETCH_FAILED(4608, "查询仓位列表失败"),
ACCOUNT_ALREADY_EXISTS(4601, "账户已存在", "error.account_already_exists"),
ACCOUNT_IS_DEFAULT(4602, "账户已是默认账户", "error.account_is_default"),
ACCOUNT_HAS_ACTIVE_ORDERS(4603, "账户有活跃订单", "error.account_has_active_orders"),
ACCOUNT_IS_LAST_ONE(4604, "不能删除最后一个账户", "error.account_is_last_one"),
ACCOUNT_API_KEY_CREATE_FAILED(4605, "自动获取 API Key 失败", "error.account_api_key_create_failed"),
ACCOUNT_PROXY_ADDRESS_FETCH_FAILED(4606, "获取代理地址失败", "error.account_proxy_address_fetch_failed"),
ACCOUNT_BALANCE_FETCH_FAILED(4607, "查询账户余额失败", "error.account_balance_fetch_failed"),
ACCOUNT_POSITIONS_FETCH_FAILED(4608, "查询仓位列表失败", "error.account_positions_fetch_failed"),
// 统计相关 (4701-4799)
STATISTICS_FETCH_FAILED(4701, "获取统计信息失败"),
ORDER_LIST_FETCH_FAILED(4702, "查询订单列表失败"),
STATISTICS_FETCH_FAILED(4701, "获取统计信息失败", "error.statistics_fetch_failed"),
ORDER_LIST_FETCH_FAILED(4702, "查询订单列表失败", "error.order_list_fetch_failed"),
// ==================== 服务器内部错误 (5001-5999) ====================
SERVER_ERROR(5001, "服务器内部错误"),
SERVER_DATABASE_ERROR(5002, "数据库错误"),
SERVER_NETWORK_ERROR(5003, "网络错误"),
SERVER_TIMEOUT(5004, "请求超时"),
SERVER_EXTERNAL_API_ERROR(5005, "外部API调用失败"),
SERVER_RPC_ERROR(5006, "RPC调用失败"),
SERVER_WEBSOCKET_ERROR(5007, "WebSocket连接错误"),
SERVER_ENCRYPTION_ERROR(5008, "加密/解密错误"),
SERVER_SIGNATURE_ERROR(5009, "签名错误"),
SERVER_ERROR(5001, "服务器内部错误", "error.server.error"),
SERVER_DATABASE_ERROR(5002, "数据库错误", "error.server.database_error"),
SERVER_NETWORK_ERROR(5003, "网络错误", "error.server.network_error"),
SERVER_TIMEOUT(5004, "请求超时", "error.server.timeout"),
SERVER_EXTERNAL_API_ERROR(5005, "外部API调用失败", "error.server.external_api_error"),
SERVER_RPC_ERROR(5006, "RPC调用失败", "error.server.rpc_error"),
SERVER_WEBSOCKET_ERROR(5007, "WebSocket连接错误", "error.server.websocket_error"),
SERVER_ENCRYPTION_ERROR(5008, "加密/解密错误", "error.server.encryption_error"),
SERVER_SIGNATURE_ERROR(5009, "签名错误", "error.server.signature_error"),
// 账户服务错误 (5101-5199)
SERVER_ACCOUNT_IMPORT_FAILED(5101, "导入账户失败"),
SERVER_ACCOUNT_UPDATE_FAILED(5102, "更新账户失败"),
SERVER_ACCOUNT_DELETE_FAILED(5103, "删除账户失败"),
SERVER_ACCOUNT_LIST_FETCH_FAILED(5104, "查询账户列表失败"),
SERVER_ACCOUNT_DETAIL_FETCH_FAILED(5105, "查询账户详情失败"),
SERVER_ACCOUNT_BALANCE_FETCH_FAILED(5106, "查询账户余额失败"),
SERVER_ACCOUNT_DEFAULT_SET_FAILED(5107, "设置默认账户失败"),
SERVER_ACCOUNT_POSITIONS_FETCH_FAILED(5108, "查询仓位列表失败"),
SERVER_ACCOUNT_ORDER_CREATE_FAILED(5109, "创建卖出订单失败"),
SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED(5110, "赎回仓位失败"),
SERVER_ACCOUNT_IMPORT_FAILED(5101, "导入账户失败", "error.server.account_import_failed"),
SERVER_ACCOUNT_UPDATE_FAILED(5102, "更新账户失败", "error.server.account_update_failed"),
SERVER_ACCOUNT_DELETE_FAILED(5103, "删除账户失败", "error.server.account_delete_failed"),
SERVER_ACCOUNT_LIST_FETCH_FAILED(5104, "查询账户列表失败", "error.server.account_list_fetch_failed"),
SERVER_ACCOUNT_DETAIL_FETCH_FAILED(5105, "查询账户详情失败", "error.server.account_detail_fetch_failed"),
SERVER_ACCOUNT_BALANCE_FETCH_FAILED(5106, "查询账户余额失败", "error.server.account_balance_fetch_failed"),
SERVER_ACCOUNT_DEFAULT_SET_FAILED(5107, "设置默认账户失败", "error.server.account_default_set_failed"),
SERVER_ACCOUNT_POSITIONS_FETCH_FAILED(5108, "查询仓位列表失败", "error.server.account_positions_fetch_failed"),
SERVER_ACCOUNT_ORDER_CREATE_FAILED(5109, "创建卖出订单失败", "error.server.account_order_create_failed"),
SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED(5110, "赎回仓位失败", "error.server.account_redeem_positions_failed"),
// Leader 服务错误 (5201-5299)
SERVER_LEADER_ADD_FAILED(5201, "添加 Leader 失败"),
SERVER_LEADER_UPDATE_FAILED(5202, "更新 Leader 失败"),
SERVER_LEADER_DELETE_FAILED(5203, "删除 Leader 失败"),
SERVER_LEADER_LIST_FETCH_FAILED(5204, "查询 Leader 列表失败"),
SERVER_LEADER_DETAIL_FETCH_FAILED(5205, "查询 Leader 详情失败"),
SERVER_LEADER_ADD_FAILED(5201, "添加 Leader 失败", "error.server.leader_add_failed"),
SERVER_LEADER_UPDATE_FAILED(5202, "更新 Leader 失败", "error.server.leader_update_failed"),
SERVER_LEADER_DELETE_FAILED(5203, "删除 Leader 失败", "error.server.leader_delete_failed"),
SERVER_LEADER_LIST_FETCH_FAILED(5204, "查询 Leader 列表失败", "error.server.leader_list_fetch_failed"),
SERVER_LEADER_DETAIL_FETCH_FAILED(5205, "查询 Leader 详情失败", "error.server.leader_detail_fetch_failed"),
// 模板服务错误 (5301-5399)
SERVER_TEMPLATE_CREATE_FAILED(5301, "创建模板失败"),
SERVER_TEMPLATE_UPDATE_FAILED(5302, "更新模板失败"),
SERVER_TEMPLATE_DELETE_FAILED(5303, "删除模板失败"),
SERVER_TEMPLATE_COPY_FAILED(5304, "复制模板失败"),
SERVER_TEMPLATE_LIST_FETCH_FAILED(5305, "查询模板列表失败"),
SERVER_TEMPLATE_DETAIL_FETCH_FAILED(5306, "查询模板详情失败"),
SERVER_TEMPLATE_CREATE_FAILED(5301, "创建模板失败", "error.server.template_create_failed"),
SERVER_TEMPLATE_UPDATE_FAILED(5302, "更新模板失败", "error.server.template_update_failed"),
SERVER_TEMPLATE_DELETE_FAILED(5303, "删除模板失败", "error.server.template_delete_failed"),
SERVER_TEMPLATE_COPY_FAILED(5304, "复制模板失败", "error.server.template_copy_failed"),
SERVER_TEMPLATE_LIST_FETCH_FAILED(5305, "查询模板列表失败", "error.server.template_list_fetch_failed"),
SERVER_TEMPLATE_DETAIL_FETCH_FAILED(5306, "查询模板详情失败", "error.server.template_detail_fetch_failed"),
// 跟单服务错误 (5401-5499)
SERVER_COPY_TRADING_CREATE_FAILED(5401, "创建跟单失败"),
SERVER_COPY_TRADING_UPDATE_FAILED(5402, "更新跟单失败"),
SERVER_COPY_TRADING_DELETE_FAILED(5403, "删除跟单失败"),
SERVER_COPY_TRADING_LIST_FETCH_FAILED(5404, "查询跟单列表失败"),
SERVER_COPY_TRADING_TEMPLATES_FETCH_FAILED(5405, "查询钱包绑定的模板失败"),
SERVER_COPY_TRADING_CREATE_FAILED(5401, "创建跟单失败", "error.server.copy_trading_create_failed"),
SERVER_COPY_TRADING_UPDATE_FAILED(5402, "更新跟单失败", "error.server.copy_trading_update_failed"),
SERVER_COPY_TRADING_DELETE_FAILED(5403, "删除跟单失败", "error.server.copy_trading_delete_failed"),
SERVER_COPY_TRADING_LIST_FETCH_FAILED(5404, "查询跟单列表失败", "error.server.copy_trading_list_fetch_failed"),
SERVER_COPY_TRADING_TEMPLATES_FETCH_FAILED(5405, "查询钱包绑定的模板失败", "error.server.copy_trading_templates_fetch_failed"),
// 市场服务错误 (5501-5599)
SERVER_MARKET_PRICE_FETCH_FAILED(5501, "获取市场价格失败"),
SERVER_MARKET_LATEST_PRICE_FETCH_FAILED(5502, "获取最新价失败"),
SERVER_MARKET_PRICE_FETCH_FAILED(5501, "获取市场价格失败", "error.server.market_price_fetch_failed"),
SERVER_MARKET_LATEST_PRICE_FETCH_FAILED(5502, "获取最新价失败", "error.server.market_latest_price_fetch_failed"),
// 统计服务错误 (5601-5699)
SERVER_STATISTICS_FETCH_FAILED(5601, "获取统计信息失败"),
SERVER_ORDER_TRACKING_LIST_FETCH_FAILED(5602, "查询订单列表失败"),
SERVER_STATISTICS_FETCH_FAILED(5601, "获取统计信息失败", "error.server.statistics_fetch_failed"),
SERVER_ORDER_TRACKING_LIST_FETCH_FAILED(5602, "查询订单列表失败", "error.server.order_tracking_list_fetch_failed"),
// 区块链服务错误 (5701-5799)
SERVER_BLOCKCHAIN_RPC_ERROR(5701, "区块链RPC调用失败"),
SERVER_BLOCKCHAIN_PROXY_ADDRESS_FETCH_FAILED(5702, "获取代理地址失败"),
SERVER_BLOCKCHAIN_BALANCE_FETCH_FAILED(5703, "查询余额失败"),
SERVER_BLOCKCHAIN_POSITIONS_FETCH_FAILED(5704, "查询仓位失败"),
SERVER_BLOCKCHAIN_REDEEM_FAILED(5705, "赎回仓位交易失败"),
SERVER_BLOCKCHAIN_RPC_ERROR(5701, "区块链RPC调用失败", "error.server.blockchain_rpc_error"),
SERVER_BLOCKCHAIN_PROXY_ADDRESS_FETCH_FAILED(5702, "获取代理地址失败", "error.server.blockchain_proxy_address_fetch_failed"),
SERVER_BLOCKCHAIN_BALANCE_FETCH_FAILED(5703, "查询余额失败", "error.server.blockchain_balance_fetch_failed"),
SERVER_BLOCKCHAIN_POSITIONS_FETCH_FAILED(5704, "查询仓位失败", "error.server.blockchain_positions_fetch_failed"),
SERVER_BLOCKCHAIN_REDEEM_FAILED(5705, "赎回仓位交易失败", "error.server.blockchain_redeem_failed"),
// WebSocket 服务错误 (5801-5899)
SERVER_WEBSOCKET_CONNECTION_FAILED(5801, "WebSocket连接失败"),
SERVER_WEBSOCKET_MESSAGE_SEND_FAILED(5802, "WebSocket消息发送失败"),
SERVER_WEBSOCKET_SUBSCRIBE_FAILED(5803, "WebSocket订阅失败"),
SERVER_WEBSOCKET_CONNECTION_FAILED(5801, "WebSocket连接失败", "error.server.websocket_connection_failed"),
SERVER_WEBSOCKET_MESSAGE_SEND_FAILED(5802, "WebSocket消息发送失败", "error.server.websocket_message_send_failed"),
SERVER_WEBSOCKET_SUBSCRIBE_FAILED(5803, "WebSocket订阅失败", "error.server.websocket_subscribe_failed"),
// 订单跟踪服务错误 (5901-5999)
SERVER_ORDER_TRACKING_PROCESS_FAILED(5901, "处理订单跟踪失败"),
SERVER_ORDER_TRACKING_BUY_FAILED(5902, "处理买入订单失败"),
SERVER_ORDER_TRACKING_SELL_FAILED(5903, "处理卖出订单失败"),
SERVER_ORDER_TRACKING_MATCH_FAILED(5904, "订单匹配失败");
SERVER_ORDER_TRACKING_PROCESS_FAILED(5901, "处理订单跟踪失败", "error.server.order_tracking_process_failed"),
SERVER_ORDER_TRACKING_BUY_FAILED(5902, "处理买入订单失败", "error.server.order_tracking_buy_failed"),
SERVER_ORDER_TRACKING_SELL_FAILED(5903, "处理卖出订单失败", "error.server.order_tracking_sell_failed"),
SERVER_ORDER_TRACKING_MATCH_FAILED(5904, "订单匹配失败", "error.server.order_tracking_match_failed");
companion object {
/**
@@ -225,8 +226,9 @@ enum class ErrorCode(
}
/**
* 根据错误码获取错误消息
* 根据错误码获取错误消息(已废弃,使用 messageKey + MessageSource
*/
@Deprecated("使用 messageKey + MessageSource 获取多语言消息", ReplaceWith("使用 ErrorCode.messageKey"))
fun getMessage(code: Int): String {
return fromCode(code)?.message ?: "未知错误"
}
@@ -0,0 +1,15 @@
package com.wrbug.polymarketbot.util
/**
* ApiResponse 扩展函数
*
* 注意:ApiResponse.error() 方法已经在 ApiResponse.kt 中定义,支持多语言。
* 此文件保留用于未来可能的扩展功能。
*
* 使用方式:
* ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
* ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource)
*/
@@ -0,0 +1,105 @@
package com.wrbug.polymarketbot.util
import com.wrbug.polymarketbot.enums.ErrorCode
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.stereotype.Component
/**
* 消息工具类
* 用于获取国际化消息
*/
@Component
class MessageUtils(
private val messageSource: MessageSource
) {
/**
* 根据 ErrorCode 获取国际化错误消息
*/
fun getMessage(errorCode: ErrorCode): String {
return try {
messageSource.getMessage(
errorCode.messageKey,
null,
errorCode.message, // 默认消息(fallback
LocaleContextHolder.getLocale()
) ?: errorCode.message
} catch (e: Exception) {
// 如果获取失败,使用默认消息
errorCode.message
}
}
/**
* 根据消息键获取国际化消息
* @param key 消息键
* @param defaultMessage 默认消息(如果找不到消息键)
* @param args 消息参数(用于占位符替换)
*/
fun getMessage(key: String, defaultMessage: String = key, vararg args: Any?): String {
return try {
messageSource.getMessage(
key,
args,
defaultMessage,
LocaleContextHolder.getLocale()
) ?: defaultMessage
} catch (e: Exception) {
defaultMessage
}
}
}
import com.wrbug.polymarketbot.enums.ErrorCode
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.stereotype.Component
/**
* 消息工具类
* 用于获取国际化消息
*/
@Component
class MessageUtils(
private val messageSource: MessageSource
) {
/**
* 根据 ErrorCode 获取国际化错误消息
*/
fun getMessage(errorCode: ErrorCode): String {
return try {
messageSource.getMessage(
errorCode.messageKey,
null,
errorCode.message, // 默认消息(fallback
LocaleContextHolder.getLocale()
) ?: errorCode.message
} catch (e: Exception) {
// 如果获取失败,使用默认消息
errorCode.message
}
}
/**
* 根据消息键获取国际化消息
* @param key 消息键
* @param defaultMessage 默认消息(如果找不到消息键)
* @param args 消息参数(用于占位符替换)
*/
fun getMessage(key: String, defaultMessage: String = key, vararg args: Any?): String {
return try {
messageSource.getMessage(
key,
args,
defaultMessage,
LocaleContextHolder.getLocale()
) ?: defaultMessage
} catch (e: Exception) {
defaultMessage
}
}
}
+266
View File
@@ -0,0 +1,266 @@
# Controller 多语言更新指南
## 已完成的 Controller ✅
1. **AccountController** - 完全更新
2. **AuthController** - 完全更新
3. **LeaderController** - 完全更新
4. **UserController** - 完全更新
## 待更新的 Controller
以下 Controller 需要按照相同模式更新:
1. **CopyTradingController** - 已添加 MessageSource,需要更新所有 ApiResponse.error() 调用
2. **CopyTradingTemplateController** - 需要添加 MessageSource 并更新调用
3. **MarketController** - 需要添加 MessageSource 并更新调用
4. **CopyTradingStatisticsController** - 需要添加 MessageSource 并更新调用
5. **ProxyConfigController** - 需要添加 MessageSource 并更新调用
6. **HealthController** - 需要检查是否需要更新
## 更新步骤
### 步骤 1:添加导入和依赖注入
```kotlin
// 1. 添加导入
import com.wrbug.polymarketbot.util.error
import org.springframework.context.MessageSource
// 2. 在构造函数中注入 MessageSource
class YourController(
private val yourService: YourService,
private val messageSource: MessageSource // 添加这一行
) {
// ...
}
```
### 步骤 2:更新所有 ApiResponse.error() 调用
**模式 1:只有 ErrorCode**
```kotlin
// 旧代码
ApiResponse.error(ErrorCode.PARAM_ERROR)
// 新代码
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
```
**模式 2ErrorCode + 自定义消息**
```kotlin
// 旧代码
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)
ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息")
// 新代码
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource)
ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource)
```
**模式 3:使用 code + msg(已废弃的方法)**
```kotlin
// 旧代码
ApiResponse.error(ErrorCode.PARAM_ERROR.code, "消息")
ApiResponse.paramError("消息")
ApiResponse.serverError("消息")
// 新代码
ApiResponse.error(ErrorCode.PARAM_ERROR, "消息", messageSource)
ApiResponse.error(ErrorCode.SERVER_ERROR, "消息", messageSource)
```
### 步骤 3:批量查找和替换
使用 IDE 的查找替换功能:
1. **查找模式**`ApiResponse.error(ErrorCode.`
2. **替换模式**:在方法调用末尾添加 `, messageSource = messageSource)`
**注意**:需要逐个检查,因为有些调用已经有其他参数。
### 示例:CopyTradingController 更新
```kotlin
// 更新前
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID))
}
// 更新后
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
}
// 更新前
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)
// 更新后
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource)
```
## 验证
更新完成后,检查:
1. ✅ 所有 Controller 都注入了 MessageSource
2. ✅ 所有 ApiResponse.error() 调用都传入了 messageSource 参数
3. ✅ 编译无错误
4. ✅ 测试 API 响应消息是否正确显示对应语言
## 快速更新脚本(参考)
可以使用以下模式批量更新:
```bash
# 在 IDE 中使用正则表达式查找替换
# 查找:ApiResponse\.error\(ErrorCode\.(\w+)\)\)
# 替换:ApiResponse.error(ErrorCode.$1, messageSource = messageSource)
# 查找:ApiResponse\.error\(ErrorCode\.(\w+),\s*([^,)]+)\)\)
# 替换:ApiResponse.error(ErrorCode.$1, $2, messageSource)
```
## 注意事项
1. **自定义消息**:如果错误消息是硬编码的中文,建议:
- 优先使用 ErrorCode 的 messageKey(已在语言包中翻译)
- 如果必须使用自定义消息,确保消息本身也需要国际化(通过 MessageUtils.getMessage()
2. **向后兼容**:如果前端没有发送语言 Header,后端会使用默认语言(英文)
3. **测试**:更新后测试不同语言下的错误消息显示
## 已完成的 Controller ✅
1. **AccountController** - 完全更新
2. **AuthController** - 完全更新
3. **LeaderController** - 完全更新
4. **UserController** - 完全更新
## 待更新的 Controller
以下 Controller 需要按照相同模式更新:
1. **CopyTradingController** - 已添加 MessageSource,需要更新所有 ApiResponse.error() 调用
2. **CopyTradingTemplateController** - 需要添加 MessageSource 并更新调用
3. **MarketController** - 需要添加 MessageSource 并更新调用
4. **CopyTradingStatisticsController** - 需要添加 MessageSource 并更新调用
5. **ProxyConfigController** - 需要添加 MessageSource 并更新调用
6. **HealthController** - 需要检查是否需要更新
## 更新步骤
### 步骤 1:添加导入和依赖注入
```kotlin
// 1. 添加导入
import com.wrbug.polymarketbot.util.error
import org.springframework.context.MessageSource
// 2. 在构造函数中注入 MessageSource
class YourController(
private val yourService: YourService,
private val messageSource: MessageSource // 添加这一行
) {
// ...
}
```
### 步骤 2:更新所有 ApiResponse.error() 调用
**模式 1:只有 ErrorCode**
```kotlin
// 旧代码
ApiResponse.error(ErrorCode.PARAM_ERROR)
// 新代码
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
```
**模式 2ErrorCode + 自定义消息**
```kotlin
// 旧代码
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)
ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息")
// 新代码
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource)
ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource)
```
**模式 3:使用 code + msg(已废弃的方法)**
```kotlin
// 旧代码
ApiResponse.error(ErrorCode.PARAM_ERROR.code, "消息")
ApiResponse.paramError("消息")
ApiResponse.serverError("消息")
// 新代码
ApiResponse.error(ErrorCode.PARAM_ERROR, "消息", messageSource)
ApiResponse.error(ErrorCode.SERVER_ERROR, "消息", messageSource)
```
### 步骤 3:批量查找和替换
使用 IDE 的查找替换功能:
1. **查找模式**`ApiResponse.error(ErrorCode.`
2. **替换模式**:在方法调用末尾添加 `, messageSource = messageSource)`
**注意**:需要逐个检查,因为有些调用已经有其他参数。
### 示例:CopyTradingController 更新
```kotlin
// 更新前
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID))
}
// 更新后
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
}
// 更新前
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)
// 更新后
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource)
```
## 验证
更新完成后,检查:
1. ✅ 所有 Controller 都注入了 MessageSource
2. ✅ 所有 ApiResponse.error() 调用都传入了 messageSource 参数
3. ✅ 编译无错误
4. ✅ 测试 API 响应消息是否正确显示对应语言
## 快速更新脚本(参考)
可以使用以下模式批量更新:
```bash
# 在 IDE 中使用正则表达式查找替换
# 查找:ApiResponse\.error\(ErrorCode\.(\w+)\)\)
# 替换:ApiResponse.error(ErrorCode.$1, messageSource = messageSource)
# 查找:ApiResponse\.error\(ErrorCode\.(\w+),\s*([^,)]+)\)\)
# 替换:ApiResponse.error(ErrorCode.$1, $2, messageSource)
```
## 注意事项
1. **自定义消息**:如果错误消息是硬编码的中文,建议:
- 优先使用 ErrorCode 的 messageKey(已在语言包中翻译)
- 如果必须使用自定义消息,确保消息本身也需要国际化(通过 MessageUtils.getMessage()
2. **向后兼容**:如果前端没有发送语言 Header,后端会使用默认语言(英文)
3. **测试**:更新后测试不同语言下的错误消息显示
View File
+246
View File
@@ -0,0 +1,246 @@
# 多语言支持实现状态
## ✅ 已完成的工作
### 后端部分(100% 完成)
#### 1. 核心框架 ✅
- ✅ 创建语言资源文件(messages_zh_CN.properties, messages_zh_TW.properties, messages_en.properties
- ✅ 配置 MessageSource BeanMessageSourceConfig.kt
- ✅ 创建 LocaleInterceptor 拦截器
- ✅ 修改 ErrorCode 枚举,添加 messageKey 字段(100+ 条错误消息)
- ✅ 修改 ApiResponse.error() 方法,支持多语言
- ✅ 创建 MessageUtils 工具类
- ✅ 更新 WebMvcConfig,注册 LocaleInterceptor
#### 2. Controller 更新 ✅(全部完成)
- ✅ AccountController - 完全更新
- ✅ AuthController - 完全更新
- ✅ LeaderController - 完全更新
- ✅ UserController - 完全更新
- ✅ CopyTradingController - 完全更新
- ✅ CopyTradingTemplateController - 完全更新
- ✅ MarketController - 完全更新
- ✅ CopyTradingStatisticsController - 完全更新(包含 CopyOrderTrackingController
- ✅ ProxyConfigController - 完全更新
- ✅ HealthController - 无需更新(无错误响应)
#### 3. 编译状态 ✅
- ✅ 后端编译通过,无错误
- ✅ 所有 ApiResponse.error() 调用都已传入 messageSource 参数
### 前端部分(部分完成)
#### 1. 核心框架 ✅
- ✅ 安装 i18next 和 react-i18next 依赖
- ✅ 配置 i18n(语言检测、资源加载)
- ✅ 创建语言包文件结构(zh-CN, zh-TW, en
- ✅ 在 api.ts 中添加语言 HeaderX-Language
- ✅ 在 main.tsx 中初始化 i18n
#### 2. 页面更新 ✅(2个页面已完成)
- ✅ AccountDetail.tsx - 完全使用 i18n
- ✅ App.tsx - 订单推送通知已使用 i18nAnt Design locale 已配置
- ✅ Login.tsx - 完全使用 i18n
#### 3. 语言包扩展 ✅
- ✅ 添加了登录相关翻译(login.*)
- ✅ 添加了订单相关翻译(order.*)
- ✅ 添加了账户相关翻译(account.*)
- ✅ 添加了通用翻译(common.*)
#### 4. 编译状态 ✅
- ✅ 前端编译通过,无错误
- ⚠️ 有 5 个 TypeScript linter 警告(文件存在,可能是缓存问题)
## ⏳ 待完成的工作
### 前端部分(21个页面待更新)
需要更新以下页面组件,使用 `useTranslation` Hook 替换硬编码文本:
1. AccountList.tsx
2. AccountImport.tsx
3. AccountEdit.tsx
4. LeaderList.tsx
5. LeaderAdd.tsx
6. LeaderEdit.tsx
7. TemplateList.tsx
8. TemplateAdd.tsx
9. TemplateEdit.tsx
10. CopyTradingList.tsx
11. CopyTradingAdd.tsx
12. CopyTradingStatistics.tsx
13. CopyTradingBuyOrders.tsx
14. CopyTradingSellOrders.tsx
15. CopyTradingMatchedOrders.tsx
16. PositionList.tsx
17. OrderList.tsx
18. Statistics.tsx
19. UserList.tsx
20. ResetPassword.tsx
21. SystemSettings.tsx
22. ConfigPage.tsx
### 语言包扩展
根据页面内容,可能需要添加更多翻译键:
- Leader 相关翻译
- Template 相关翻译
- CopyTrading 相关翻译
- Position 相关翻译
- Statistics 相关翻译
- 等等
## 测试建议
### 后端测试
1. ✅ 编译通过
2. ⏳ 测试不同语言 Header 下的错误消息显示
3. ⏳ 测试默认语言(无 Header 时)
### 前端测试
1. ✅ 编译通过
2. ⏳ 测试系统语言自动检测
3. ⏳ 测试语言切换(如果添加了切换功能)
4. ⏳ 测试不同语言下的页面显示
5. ⏳ 测试 API 请求是否正确传递语言 Header
## 使用说明
### 后端
所有 Controller 已更新,错误消息会自动根据 HTTP Header `X-Language``Accept-Language` 返回对应语言。
### 前端
已更新的页面会自动根据系统语言显示对应文本。其他页面需要逐步更新。
## 下一步
1. 逐步更新剩余的前端页面组件
2. 根据页面内容扩展语言包
3. 全面测试多语言功能
4. (可选)添加语言切换组件
## ✅ 已完成的工作
### 后端部分(100% 完成)
#### 1. 核心框架 ✅
- ✅ 创建语言资源文件(messages_zh_CN.properties, messages_zh_TW.properties, messages_en.properties
- ✅ 配置 MessageSource BeanMessageSourceConfig.kt
- ✅ 创建 LocaleInterceptor 拦截器
- ✅ 修改 ErrorCode 枚举,添加 messageKey 字段(100+ 条错误消息)
- ✅ 修改 ApiResponse.error() 方法,支持多语言
- ✅ 创建 MessageUtils 工具类
- ✅ 更新 WebMvcConfig,注册 LocaleInterceptor
#### 2. Controller 更新 ✅(全部完成)
- ✅ AccountController - 完全更新
- ✅ AuthController - 完全更新
- ✅ LeaderController - 完全更新
- ✅ UserController - 完全更新
- ✅ CopyTradingController - 完全更新
- ✅ CopyTradingTemplateController - 完全更新
- ✅ MarketController - 完全更新
- ✅ CopyTradingStatisticsController - 完全更新(包含 CopyOrderTrackingController
- ✅ ProxyConfigController - 完全更新
- ✅ HealthController - 无需更新(无错误响应)
#### 3. 编译状态 ✅
- ✅ 后端编译通过,无错误
- ✅ 所有 ApiResponse.error() 调用都已传入 messageSource 参数
### 前端部分(部分完成)
#### 1. 核心框架 ✅
- ✅ 安装 i18next 和 react-i18next 依赖
- ✅ 配置 i18n(语言检测、资源加载)
- ✅ 创建语言包文件结构(zh-CN, zh-TW, en
- ✅ 在 api.ts 中添加语言 HeaderX-Language
- ✅ 在 main.tsx 中初始化 i18n
#### 2. 页面更新 ✅(2个页面已完成)
- ✅ AccountDetail.tsx - 完全使用 i18n
- ✅ App.tsx - 订单推送通知已使用 i18nAnt Design locale 已配置
- ✅ Login.tsx - 完全使用 i18n
#### 3. 语言包扩展 ✅
- ✅ 添加了登录相关翻译(login.*)
- ✅ 添加了订单相关翻译(order.*)
- ✅ 添加了账户相关翻译(account.*)
- ✅ 添加了通用翻译(common.*)
#### 4. 编译状态 ✅
- ✅ 前端编译通过,无错误
- ⚠️ 有 5 个 TypeScript linter 警告(文件存在,可能是缓存问题)
## ⏳ 待完成的工作
### 前端部分(21个页面待更新)
需要更新以下页面组件,使用 `useTranslation` Hook 替换硬编码文本:
1. AccountList.tsx
2. AccountImport.tsx
3. AccountEdit.tsx
4. LeaderList.tsx
5. LeaderAdd.tsx
6. LeaderEdit.tsx
7. TemplateList.tsx
8. TemplateAdd.tsx
9. TemplateEdit.tsx
10. CopyTradingList.tsx
11. CopyTradingAdd.tsx
12. CopyTradingStatistics.tsx
13. CopyTradingBuyOrders.tsx
14. CopyTradingSellOrders.tsx
15. CopyTradingMatchedOrders.tsx
16. PositionList.tsx
17. OrderList.tsx
18. Statistics.tsx
19. UserList.tsx
20. ResetPassword.tsx
21. SystemSettings.tsx
22. ConfigPage.tsx
### 语言包扩展
根据页面内容,可能需要添加更多翻译键:
- Leader 相关翻译
- Template 相关翻译
- CopyTrading 相关翻译
- Position 相关翻译
- Statistics 相关翻译
- 等等
## 测试建议
### 后端测试
1. ✅ 编译通过
2. ⏳ 测试不同语言 Header 下的错误消息显示
3. ⏳ 测试默认语言(无 Header 时)
### 前端测试
1. ✅ 编译通过
2. ⏳ 测试系统语言自动检测
3. ⏳ 测试语言切换(如果添加了切换功能)
4. ⏳ 测试不同语言下的页面显示
5. ⏳ 测试 API 请求是否正确传递语言 Header
## 使用说明
### 后端
所有 Controller 已更新,错误消息会自动根据 HTTP Header `X-Language``Accept-Language` 返回对应语言。
### 前端
已更新的页面会自动根据系统语言显示对应文本。其他页面需要逐步更新。
## 下一步
1. 逐步更新剩余的前端页面组件
2. 根据页面内容扩展语言包
3. 全面测试多语言功能
4. (可选)添加语言切换组件
+444
View File
@@ -0,0 +1,444 @@
# 多语言支持实现总结
## 已完成的工作
### 后端部分 ✅
1. **语言资源文件** (`backend/src/main/resources/i18n/`)
- `messages_zh_CN.properties` - 简体中文
- `messages_zh_TW.properties` - 繁体中文
- `messages_en.properties` - 英文
- 已翻译所有 ErrorCode 错误消息(100+ 条)
2. **配置类**
- `MessageSourceConfig.kt` - 配置 MessageSource 和 LocaleResolver
- `LocaleInterceptor.kt` - 从 HTTP Header 读取语言设置
3. **核心修改**
- `ErrorCode.kt` - 添加 `messageKey` 字段,每个错误码都有对应的消息键
- `ApiResponse.kt` - 支持多语言错误消息
- `ApiResponseExt.kt` - 提供便捷的扩展函数
- `MessageUtils.kt` - 消息工具类
- `WebMvcConfig.kt` - 注册 LocaleInterceptor
4. **示例更新**
- `AccountController.kt` - 已更新部分方法作为示例
### 前端部分 ✅
1. **依赖安装**
- `i18next``react-i18next` 已安装
2. **i18n 配置** (`frontend/src/i18n/config.ts`)
- 自动检测系统语言
- 支持语言切换
- 语言持久化到 localStorage
3. **语言包** (`frontend/src/locales/`)
- `zh-CN/common.json` - 简体中文
- `zh-TW/common.json` - 繁体中文
- `en/common.json` - 英文
- 已包含账户管理相关的基础翻译
4. **API 集成**
- `api.ts` - 自动在请求头添加 `X-Language` Header
5. **示例页面**
- `AccountDetail.tsx` - 已完全使用 i18n
## 待完成的工作
### 后端部分
需要更新所有 Controller,使用 `MessageUtils` 或扩展函数来获取国际化消息:
```kotlin
// 方式1:使用扩展函数(推荐)
@RestController
class ExampleController(
private val messageSource: MessageSource
) {
@PostMapping("/example")
fun example(): ResponseEntity<ApiResponse<Unit>> {
// 使用扩展函数,自动国际化
return ResponseEntity.ok(
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
)
}
}
// 方式2:使用 MessageUtils
@RestController
class ExampleController(
private val messageUtils: MessageUtils
) {
@PostMapping("/example")
fun example(): ResponseEntity<ApiResponse<Unit>> {
val msg = messageUtils.getMessage(ErrorCode.PARAM_ERROR)
return ResponseEntity.ok(
ApiResponse.error(ErrorCode.PARAM_ERROR.code, msg)
)
}
}
```
需要更新的 Controller
- `AuthController.kt`
- `LeaderController.kt`
- `CopyTradingController.kt`
- `CopyTradingTemplateController.kt`
- `CopyTradingStatisticsController.kt`
- `MarketController.kt`
- `UserController.kt`
- `ProxyConfigController.kt`
- `AccountController.kt` (部分方法已更新,需要完成剩余部分)
### 前端部分
需要更新所有页面组件,使用 `useTranslation` Hook
```typescript
import { useTranslation } from 'react-i18next'
const MyComponent: React.FC = () => {
const { t } = useTranslation()
return (
<div>
<Button>{t('common.save')}</Button>
<div>{t('account.accountName')}</div>
</div>
)
}
```
需要更新的页面:
- `AccountList.tsx`
- `AccountImport.tsx`
- `AccountEdit.tsx`
- `LeaderList.tsx`
- `LeaderAdd.tsx`
- `LeaderEdit.tsx`
- `TemplateList.tsx`
- `TemplateAdd.tsx`
- `TemplateEdit.tsx`
- `CopyTradingList.tsx`
- `CopyTradingAdd.tsx`
- `Login.tsx`
- `UserList.tsx`
- 以及其他所有页面
## 使用说明
### 后端使用
1. **在 Controller 中注入 MessageSource**
```kotlin
@RestController
class MyController(
private val messageSource: MessageSource
) {
// ...
}
```
2. **使用扩展函数创建错误响应**
```kotlin
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
```
3. **自定义消息(如果需要)**
```kotlin
ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource)
```
### 前端使用
1. **在组件中使用 useTranslation**
```typescript
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
```
2. **翻译文本**
```typescript
t('common.save') // 返回当前语言的"保存"
t('account.accountName') // 返回当前语言的"账户名称"
```
3. **切换语言**(可选):
```typescript
import { changeLanguage } from '../i18n/config'
changeLanguage('zh-CN') // 切换到简体中文
changeLanguage('zh-TW') // 切换到繁体中文
changeLanguage('en') // 切换到英文
```
## 语言检测规则
### 前端
1. 优先使用 localStorage 中保存的语言设置
2. 如果没有,检测系统语言:
- `zh-CN`, `zh` → 简体中文
- `zh-TW`, `zh-HK`, `zh-MO` → 繁体中文
- 其他 → 英文(默认)
### 后端
1. 从 HTTP Header 读取:
- `X-Language` (优先)
- `Accept-Language` (备选)
2. 语言映射规则同前端
3. 默认语言:英文
## 测试建议
1. **测试语言检测**
- 修改浏览器语言设置,刷新页面,检查是否自动切换
- 测试不同语言下的 API 响应消息
2. **测试语言切换**
- 在前端添加语言切换组件(可选)
- 测试切换后 API 请求是否正确传递语言 Header
3. **测试错误消息**
- 触发各种错误,检查错误消息是否正确显示对应语言
## 注意事项
1. **向后兼容**:如果前端没有发送语言 Header,后端默认使用英文
2. **消息键命名**:所有消息键使用 `error.` 前缀,便于管理
3. **翻译质量**:建议由专业翻译人员审核翻译内容
4. **性能**:语言包已配置缓存,不会影响性能
## 下一步
1. 完成所有 Controller 的更新
2. 完成所有前端页面的翻译
3. 添加语言切换组件(可选)
4. 全面测试多语言功能
5. 更新 API 文档,说明语言 Header 的使用
## 已完成的工作
### 后端部分 ✅
1. **语言资源文件** (`backend/src/main/resources/i18n/`)
- `messages_zh_CN.properties` - 简体中文
- `messages_zh_TW.properties` - 繁体中文
- `messages_en.properties` - 英文
- 已翻译所有 ErrorCode 错误消息(100+ 条)
2. **配置类**
- `MessageSourceConfig.kt` - 配置 MessageSource 和 LocaleResolver
- `LocaleInterceptor.kt` - 从 HTTP Header 读取语言设置
3. **核心修改**
- `ErrorCode.kt` - 添加 `messageKey` 字段,每个错误码都有对应的消息键
- `ApiResponse.kt` - 支持多语言错误消息
- `ApiResponseExt.kt` - 提供便捷的扩展函数
- `MessageUtils.kt` - 消息工具类
- `WebMvcConfig.kt` - 注册 LocaleInterceptor
4. **示例更新**
- `AccountController.kt` - 已更新部分方法作为示例
### 前端部分 ✅
1. **依赖安装**
- `i18next``react-i18next` 已安装
2. **i18n 配置** (`frontend/src/i18n/config.ts`)
- 自动检测系统语言
- 支持语言切换
- 语言持久化到 localStorage
3. **语言包** (`frontend/src/locales/`)
- `zh-CN/common.json` - 简体中文
- `zh-TW/common.json` - 繁体中文
- `en/common.json` - 英文
- 已包含账户管理相关的基础翻译
4. **API 集成**
- `api.ts` - 自动在请求头添加 `X-Language` Header
5. **示例页面**
- `AccountDetail.tsx` - 已完全使用 i18n
## 待完成的工作
### 后端部分
需要更新所有 Controller,使用 `MessageUtils` 或扩展函数来获取国际化消息:
```kotlin
// 方式1:使用扩展函数(推荐)
@RestController
class ExampleController(
private val messageSource: MessageSource
) {
@PostMapping("/example")
fun example(): ResponseEntity<ApiResponse<Unit>> {
// 使用扩展函数,自动国际化
return ResponseEntity.ok(
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
)
}
}
// 方式2:使用 MessageUtils
@RestController
class ExampleController(
private val messageUtils: MessageUtils
) {
@PostMapping("/example")
fun example(): ResponseEntity<ApiResponse<Unit>> {
val msg = messageUtils.getMessage(ErrorCode.PARAM_ERROR)
return ResponseEntity.ok(
ApiResponse.error(ErrorCode.PARAM_ERROR.code, msg)
)
}
}
```
需要更新的 Controller
- `AuthController.kt`
- `LeaderController.kt`
- `CopyTradingController.kt`
- `CopyTradingTemplateController.kt`
- `CopyTradingStatisticsController.kt`
- `MarketController.kt`
- `UserController.kt`
- `ProxyConfigController.kt`
- `AccountController.kt` (部分方法已更新,需要完成剩余部分)
### 前端部分
需要更新所有页面组件,使用 `useTranslation` Hook
```typescript
import { useTranslation } from 'react-i18next'
const MyComponent: React.FC = () => {
const { t } = useTranslation()
return (
<div>
<Button>{t('common.save')}</Button>
<div>{t('account.accountName')}</div>
</div>
)
}
```
需要更新的页面:
- `AccountList.tsx`
- `AccountImport.tsx`
- `AccountEdit.tsx`
- `LeaderList.tsx`
- `LeaderAdd.tsx`
- `LeaderEdit.tsx`
- `TemplateList.tsx`
- `TemplateAdd.tsx`
- `TemplateEdit.tsx`
- `CopyTradingList.tsx`
- `CopyTradingAdd.tsx`
- `Login.tsx`
- `UserList.tsx`
- 以及其他所有页面
## 使用说明
### 后端使用
1. **在 Controller 中注入 MessageSource**
```kotlin
@RestController
class MyController(
private val messageSource: MessageSource
) {
// ...
}
```
2. **使用扩展函数创建错误响应**
```kotlin
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
```
3. **自定义消息(如果需要)**
```kotlin
ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource)
```
### 前端使用
1. **在组件中使用 useTranslation**
```typescript
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
```
2. **翻译文本**
```typescript
t('common.save') // 返回当前语言的"保存"
t('account.accountName') // 返回当前语言的"账户名称"
```
3. **切换语言**(可选):
```typescript
import { changeLanguage } from '../i18n/config'
changeLanguage('zh-CN') // 切换到简体中文
changeLanguage('zh-TW') // 切换到繁体中文
changeLanguage('en') // 切换到英文
```
## 语言检测规则
### 前端
1. 优先使用 localStorage 中保存的语言设置
2. 如果没有,检测系统语言:
- `zh-CN`, `zh` → 简体中文
- `zh-TW`, `zh-HK`, `zh-MO` → 繁体中文
- 其他 → 英文(默认)
### 后端
1. 从 HTTP Header 读取:
- `X-Language` (优先)
- `Accept-Language` (备选)
2. 语言映射规则同前端
3. 默认语言:英文
## 测试建议
1. **测试语言检测**
- 修改浏览器语言设置,刷新页面,检查是否自动切换
- 测试不同语言下的 API 响应消息
2. **测试语言切换**
- 在前端添加语言切换组件(可选)
- 测试切换后 API 请求是否正确传递语言 Header
3. **测试错误消息**
- 触发各种错误,检查错误消息是否正确显示对应语言
## 注意事项
1. **向后兼容**:如果前端没有发送语言 Header,后端默认使用英文
2. **消息键命名**:所有消息键使用 `error.` 前缀,便于管理
3. **翻译质量**:建议由专业翻译人员审核翻译内容
4. **性能**:语言包已配置缓存,不会影响性能
## 下一步
1. 完成所有 Controller 的更新
2. 完成所有前端页面的翻译
3. 添加语言切换组件(可选)
4. 全面测试多语言功能
5. 更新 API 文档,说明语言 Header 的使用
+89
View File
@@ -0,0 +1,89 @@
# 前端语言切换功能实现总结
## ✅ 已完成的工作
### 1. 语言切换组件 ✅
- **文件**: `frontend/src/components/LanguageSwitcher.tsx`
- **功能**:
- 显示当前语言(简体中文、繁體中文、English)
- 支持手动切换语言
- 切换后自动刷新页面以应用 Ant Design 的 locale
- 支持移动端和桌面端响应式设计
### 2. Layout 组件集成 ✅
- **文件**: `frontend/src/components/Layout.tsx`
- **更新内容**:
- 在移动端 Header 中添加了语言切换器
- 在桌面端 Sider 中添加了语言切换器
- 菜单项已使用 i18n 翻译(menu.*
- 退出登录确认对话框已使用 i18n
### 3. i18n 配置优化 ✅
- **文件**: `frontend/src/i18n/config.ts`
- **功能**:
- 自动检测系统语言
- 优先使用 localStorage 中保存的用户选择
- 提供 `changeLanguage()` 函数用于手动切换
- 提供 `getCurrentLanguage()` 函数获取当前语言
### 4. 语言包扩展 ✅
- **新增翻译键**: `menu.*`
- `menu.accounts`: 账户管理
- `menu.copyTrading`: 跟单交易
- `menu.leaders`: Leader 管理
- `menu.templates`: 跟单模板
- `menu.copyTradingConfig`: 跟单配置
- `menu.positions`: 仓位管理
- `menu.statistics`: 统计信息
- `menu.users`: 用户管理
- `menu.systemSettings`: 系统管理
- `menu.logout`: 退出登录
- `menu.logoutConfirm`: 确认退出
- `menu.logoutConfirmDesc`: 确定要退出登录吗?
- `menu.navigation`: 导航菜单
### 5. API 请求同步 ✅
- **文件**: `frontend/src/services/api.ts`
- **功能**: 自动在请求头中添加 `X-Language`,与后端语言保持一致
## 使用方式
### 用户操作
1. 在页面右上角(桌面端)或 Header(移动端)找到语言切换器
2. 点击下拉菜单选择语言:
- 简体中文
- 繁體中文
- English
3. 选择后页面会自动刷新,应用新语言
### 技术实现
- 语言选择保存在 `localStorage``i18n_language` 键中
- 切换语言后自动刷新页面,确保:
- Ant Design 的 locale 正确应用
- 所有组件重新渲染,使用新语言
- API 请求自动携带新语言 Header
## 编译状态
- ✅ 前端编译通过,无错误
- ✅ 所有组件正常工作
## 位置说明
### 桌面端
- 语言切换器位于左侧导航栏顶部(PolyHermes 标题旁边)
### 移动端
- 语言切换器位于顶部 Header 右侧(GitHub 和 Twitter 图标之前)
## 后续优化建议
1. **无需刷新页面切换**(可选):
- 当前实现需要刷新页面以确保 Ant Design locale 正确应用
- 未来可以考虑动态更新 ConfigProvider 的 locale,避免刷新
2. **语言图标优化**(可选):
- 可以使用国旗图标或语言缩写(如 CN、TW、EN)代替文字
3. **语言切换动画**(可选):
- 添加平滑的切换动画,提升用户体验
+572
View File
@@ -0,0 +1,572 @@
# 多语言支持遗漏工作清单
## 后端遗漏工作
### ❌ 需要更新的 Controller
#### 1. CopyTradingController
- ✅ 已添加 MessageSource 依赖注入
-**所有 ApiResponse.error() 调用都缺少 messageSource 参数**
- 第 31 行:`ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID)` → 需要添加 `, messageSource = messageSource`
- 第 34 行:`ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID)` → 需要添加
- 第 37 行:`ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID)` → 需要添加
- 第 48-49 行:`ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)` → 需要添加 `, messageSource`
- 第 55 行:`ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message)` → 需要添加
- 第 72, 77 行:查询列表的错误响应 → 需要添加
- 第 88, 99-101, 107 行:更新状态的错误响应 → 需要添加
- 第 118, 129-130, 136 行:删除的错误响应 → 需要添加
- 第 147, 158-159, 165 行:查询模板的错误响应 → 需要添加
#### 2. CopyTradingTemplateController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 需要添加:`private val messageSource: MessageSource`
- 需要添加导入:`import com.wrbug.polymarketbot.util.error`
- 所有 `ApiResponse.error()` 调用都需要添加 `messageSource` 参数
#### 3. MarketController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 第 34 行:`ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY)`
- 第 44, 49 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message)`
- 第 62 行:`ApiResponse.error(ErrorCode.PARAM_TOKEN_ID_EMPTY)`
- 第 72, 77 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message)`
#### 4. CopyTradingStatisticsController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 第 31 行:`ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID)`
- 第 42-43, 49 行:统计查询的错误响应
- 需要检查整个文件的所有错误响应
#### 5. ProxyConfigController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 第 35 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置失败:${e.message}")`
- 第 49 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置列表失败:${e.message}")`
- 需要检查整个文件的所有错误响应
#### 6. HealthController
-**不需要更新**(没有错误响应,只有健康检查)
### 更新模式
对于每个 Controller,需要:
1. **添加导入**
```kotlin
import com.wrbug.polymarketbot.util.error
import org.springframework.context.MessageSource
```
2. **添加依赖注入**
```kotlin
class YourController(
private val yourService: YourService,
private val messageSource: MessageSource // 添加这一行
)
```
3. **更新所有 ApiResponse.error() 调用**
```kotlin
// 模式1:只有 ErrorCode
ApiResponse.error(ErrorCode.PARAM_ERROR)
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
// 模式2ErrorCode + 消息
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource)
```
## 前端遗漏工作
### ❌ 需要更新的页面组件(23个页面,只有1个已更新)
#### ✅ 已完成的页面
1. **AccountDetail.tsx** - 已完全使用 i18n
#### ❌ 待更新的页面(22个)
1. **Login.tsx**
- 硬编码文本:`'登录成功'`, `'登录失败'`, `'用户名'`, `'密码'`
- 需要添加:`import { useTranslation } from 'react-i18next'`
- 需要添加:`const { t } = useTranslation()`
- 需要替换所有硬编码文本
2. **AccountList.tsx**
- 需要检查并替换所有硬编码文本
3. **AccountImport.tsx**
- 硬编码文本:`'钱包地址与私钥不匹配'`, `'钱包地址格式不正确'`, `'钱包地址'`
- 需要更新
4. **AccountEdit.tsx**
- 需要检查并替换所有硬编码文本
5. **LeaderList.tsx**
- 需要检查并替换所有硬编码文本
6. **LeaderAdd.tsx**
- 需要检查并替换所有硬编码文本
7. **LeaderEdit.tsx**
- 需要检查并替换所有硬编码文本
8. **TemplateList.tsx**
- 需要检查并替换所有硬编码文本
9. **TemplateAdd.tsx**
- 需要检查并替换所有硬编码文本
10. **TemplateEdit.tsx**
- 需要检查并替换所有硬编码文本
11. **CopyTradingList.tsx**
- 硬编码文本:`'删除跟单成功'`, `'删除跟单失败'`, `'钱包'`, `'模板'`, `'Leader'`, `'取消'`, `'订单'`
- 需要更新
12. **CopyTradingAdd.tsx**
- 需要检查并替换所有硬编码文本
13. **CopyTradingStatistics.tsx**
- 需要检查并替换所有硬编码文本
14. **CopyTradingBuyOrders.tsx**
- 需要检查并替换所有硬编码文本
15. **CopyTradingSellOrders.tsx**
- 需要检查并替换所有硬编码文本
16. **CopyTradingMatchedOrders.tsx**
- 需要检查并替换所有硬编码文本
17. **PositionList.tsx**
- 硬编码文本:`'账户'`, `'市场'`, `'搜索账户、市场、方向...'`, `'确认卖出'`, `'取消'`, `'订单类型'`, `'确认赎回'`
- 需要更新
18. **OrderList.tsx**
- 需要检查并替换所有硬编码文本
19. **Statistics.tsx**
- 需要检查并替换所有硬编码文本
20. **UserList.tsx**
- 需要检查并替换所有硬编码文本
21. **ResetPassword.tsx**
- 需要检查并替换所有硬编码文本
22. **SystemSettings.tsx**
- 需要检查并替换所有硬编码文本
23. **ConfigPage.tsx**
- 需要检查并替换所有硬编码文本
### App.tsx 中的硬编码文本
**App.tsx** 中有硬编码的中文文本:
- 第 61 行:`'订单创建'`
- 第 63 行:`'订单更新'`
- 第 65 行:`'订单取消'`
- 第 67 行:`'订单事件'`
- 第 79 行:`'买入'`, `'卖出'`
- 第 92 行:`'市场:'`, `'状态:'`, `'已成交:'`, `'剩余:'`
需要:
1. 添加 `import { useTranslation } from 'react-i18next'`
2. 在组件中使用 `const { t } = useTranslation()`
3. 替换所有硬编码文本
### 前端更新模式
对于每个页面组件:
1. **添加导入**
```typescript
import { useTranslation } from 'react-i18next'
```
2. **在组件中使用**
```typescript
const YourComponent: React.FC = () => {
const { t } = useTranslation()
// ...
}
```
3. **替换硬编码文本**
```typescript
// 旧代码
<Button></Button>
message.success('操作成功')
// 新代码
<Button>{t('common.save')}</Button>
message.success(t('message.operationSuccess'))
```
4. **扩展语言包**
-`frontend/src/locales/*/common.json` 中添加缺失的翻译键
- 确保三个语言包(zh-CN, zh-TW, en)都有对应的翻译
## 语言包扩展
### 需要添加的翻译键
根据检查,需要在语言包中添加以下键:
```json
{
"order": {
"create": "订单创建",
"update": "订单更新",
"cancel": "订单取消",
"event": "订单事件",
"buy": "买入",
"sell": "卖出",
"market": "市场",
"status": "状态",
"filled": "已成交",
"remaining": "剩余"
},
"wallet": {
"address": "钱包地址",
"addressMismatch": "钱包地址与私钥不匹配",
"addressInvalid": "钱包地址格式不正确"
},
"copyTrading": {
"deleteSuccess": "删除跟单成功",
"deleteFailed": "删除跟单失败",
"wallet": "钱包",
"template": "模板",
"leader": "Leader"
},
"position": {
"account": "账户",
"market": "市场",
"searchPlaceholder": "搜索账户、市场、方向...",
"confirmSell": "确认卖出",
"confirmRedeem": "确认赎回",
"orderType": "订单类型"
}
}
```
## 优先级建议
### 高优先级(核心功能)
1. ✅ 后端:完成所有 Controller 的 MessageSource 更新
2. ✅ 前端:更新 Login.tsx(登录页面)
3. ✅ 前端:更新 App.tsx(全局通知)
### 中优先级(常用功能)
4. 前端:更新 AccountList.tsx, AccountImport.tsx
5. 前端:更新 LeaderList.tsx, LeaderAdd.tsx
6. 前端:更新 CopyTradingList.tsx
### 低优先级(其他页面)
7. 前端:逐步更新其他页面组件
## 验证清单
完成后检查:
### 后端
- [ ] 所有 Controller 都注入了 MessageSource
- [ ] 所有 ApiResponse.error() 调用都传入了 messageSource
- [ ] 编译无错误
- [ ] 测试不同语言下的错误消息
### 前端
- [ ] 所有页面都使用了 useTranslation
- [ ] 所有硬编码文本都已替换
- [ ] 语言包包含所有需要的翻译键
- [ ] 测试不同语言下的页面显示
- [ ] 测试语言切换功能
## 后端遗漏工作
### ❌ 需要更新的 Controller
#### 1. CopyTradingController
- ✅ 已添加 MessageSource 依赖注入
-**所有 ApiResponse.error() 调用都缺少 messageSource 参数**
- 第 31 行:`ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID)` → 需要添加 `, messageSource = messageSource`
- 第 34 行:`ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID)` → 需要添加
- 第 37 行:`ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID)` → 需要添加
- 第 48-49 行:`ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)` → 需要添加 `, messageSource`
- 第 55 行:`ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message)` → 需要添加
- 第 72, 77 行:查询列表的错误响应 → 需要添加
- 第 88, 99-101, 107 行:更新状态的错误响应 → 需要添加
- 第 118, 129-130, 136 行:删除的错误响应 → 需要添加
- 第 147, 158-159, 165 行:查询模板的错误响应 → 需要添加
#### 2. CopyTradingTemplateController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 需要添加:`private val messageSource: MessageSource`
- 需要添加导入:`import com.wrbug.polymarketbot.util.error`
- 所有 `ApiResponse.error()` 调用都需要添加 `messageSource` 参数
#### 3. MarketController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 第 34 行:`ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY)`
- 第 44, 49 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message)`
- 第 62 行:`ApiResponse.error(ErrorCode.PARAM_TOKEN_ID_EMPTY)`
- 第 72, 77 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message)`
#### 4. CopyTradingStatisticsController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 第 31 行:`ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID)`
- 第 42-43, 49 行:统计查询的错误响应
- 需要检查整个文件的所有错误响应
#### 5. ProxyConfigController
-**未添加 MessageSource 依赖注入**
-**所有 ApiResponse.error() 调用都需要更新**
- 第 35 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置失败:${e.message}")`
- 第 49 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置列表失败:${e.message}")`
- 需要检查整个文件的所有错误响应
#### 6. HealthController
-**不需要更新**(没有错误响应,只有健康检查)
### 更新模式
对于每个 Controller,需要:
1. **添加导入**
```kotlin
import com.wrbug.polymarketbot.util.error
import org.springframework.context.MessageSource
```
2. **添加依赖注入**
```kotlin
class YourController(
private val yourService: YourService,
private val messageSource: MessageSource // 添加这一行
)
```
3. **更新所有 ApiResponse.error() 调用**
```kotlin
// 模式1:只有 ErrorCode
ApiResponse.error(ErrorCode.PARAM_ERROR)
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
// 模式2ErrorCode + 消息
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)
ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource)
```
## 前端遗漏工作
### ❌ 需要更新的页面组件(23个页面,只有1个已更新)
#### ✅ 已完成的页面
1. **AccountDetail.tsx** - 已完全使用 i18n
#### ❌ 待更新的页面(22个)
1. **Login.tsx**
- 硬编码文本:`'登录成功'`, `'登录失败'`, `'用户名'`, `'密码'`
- 需要添加:`import { useTranslation } from 'react-i18next'`
- 需要添加:`const { t } = useTranslation()`
- 需要替换所有硬编码文本
2. **AccountList.tsx**
- 需要检查并替换所有硬编码文本
3. **AccountImport.tsx**
- 硬编码文本:`'钱包地址与私钥不匹配'`, `'钱包地址格式不正确'`, `'钱包地址'`
- 需要更新
4. **AccountEdit.tsx**
- 需要检查并替换所有硬编码文本
5. **LeaderList.tsx**
- 需要检查并替换所有硬编码文本
6. **LeaderAdd.tsx**
- 需要检查并替换所有硬编码文本
7. **LeaderEdit.tsx**
- 需要检查并替换所有硬编码文本
8. **TemplateList.tsx**
- 需要检查并替换所有硬编码文本
9. **TemplateAdd.tsx**
- 需要检查并替换所有硬编码文本
10. **TemplateEdit.tsx**
- 需要检查并替换所有硬编码文本
11. **CopyTradingList.tsx**
- 硬编码文本:`'删除跟单成功'`, `'删除跟单失败'`, `'钱包'`, `'模板'`, `'Leader'`, `'取消'`, `'订单'`
- 需要更新
12. **CopyTradingAdd.tsx**
- 需要检查并替换所有硬编码文本
13. **CopyTradingStatistics.tsx**
- 需要检查并替换所有硬编码文本
14. **CopyTradingBuyOrders.tsx**
- 需要检查并替换所有硬编码文本
15. **CopyTradingSellOrders.tsx**
- 需要检查并替换所有硬编码文本
16. **CopyTradingMatchedOrders.tsx**
- 需要检查并替换所有硬编码文本
17. **PositionList.tsx**
- 硬编码文本:`'账户'`, `'市场'`, `'搜索账户、市场、方向...'`, `'确认卖出'`, `'取消'`, `'订单类型'`, `'确认赎回'`
- 需要更新
18. **OrderList.tsx**
- 需要检查并替换所有硬编码文本
19. **Statistics.tsx**
- 需要检查并替换所有硬编码文本
20. **UserList.tsx**
- 需要检查并替换所有硬编码文本
21. **ResetPassword.tsx**
- 需要检查并替换所有硬编码文本
22. **SystemSettings.tsx**
- 需要检查并替换所有硬编码文本
23. **ConfigPage.tsx**
- 需要检查并替换所有硬编码文本
### App.tsx 中的硬编码文本
**App.tsx** 中有硬编码的中文文本:
- 第 61 行:`'订单创建'`
- 第 63 行:`'订单更新'`
- 第 65 行:`'订单取消'`
- 第 67 行:`'订单事件'`
- 第 79 行:`'买入'`, `'卖出'`
- 第 92 行:`'市场:'`, `'状态:'`, `'已成交:'`, `'剩余:'`
需要:
1. 添加 `import { useTranslation } from 'react-i18next'`
2. 在组件中使用 `const { t } = useTranslation()`
3. 替换所有硬编码文本
### 前端更新模式
对于每个页面组件:
1. **添加导入**
```typescript
import { useTranslation } from 'react-i18next'
```
2. **在组件中使用**
```typescript
const YourComponent: React.FC = () => {
const { t } = useTranslation()
// ...
}
```
3. **替换硬编码文本**
```typescript
// 旧代码
<Button></Button>
message.success('操作成功')
// 新代码
<Button>{t('common.save')}</Button>
message.success(t('message.operationSuccess'))
```
4. **扩展语言包**
-`frontend/src/locales/*/common.json` 中添加缺失的翻译键
- 确保三个语言包(zh-CN, zh-TW, en)都有对应的翻译
## 语言包扩展
### 需要添加的翻译键
根据检查,需要在语言包中添加以下键:
```json
{
"order": {
"create": "订单创建",
"update": "订单更新",
"cancel": "订单取消",
"event": "订单事件",
"buy": "买入",
"sell": "卖出",
"market": "市场",
"status": "状态",
"filled": "已成交",
"remaining": "剩余"
},
"wallet": {
"address": "钱包地址",
"addressMismatch": "钱包地址与私钥不匹配",
"addressInvalid": "钱包地址格式不正确"
},
"copyTrading": {
"deleteSuccess": "删除跟单成功",
"deleteFailed": "删除跟单失败",
"wallet": "钱包",
"template": "模板",
"leader": "Leader"
},
"position": {
"account": "账户",
"market": "市场",
"searchPlaceholder": "搜索账户、市场、方向...",
"confirmSell": "确认卖出",
"confirmRedeem": "确认赎回",
"orderType": "订单类型"
}
}
```
## 优先级建议
### 高优先级(核心功能)
1. ✅ 后端:完成所有 Controller 的 MessageSource 更新
2. ✅ 前端:更新 Login.tsx(登录页面)
3. ✅ 前端:更新 App.tsx(全局通知)
### 中优先级(常用功能)
4. 前端:更新 AccountList.tsx, AccountImport.tsx
5. 前端:更新 LeaderList.tsx, LeaderAdd.tsx
6. 前端:更新 CopyTradingList.tsx
### 低优先级(其他页面)
7. 前端:逐步更新其他页面组件
## 验证清单
完成后检查:
### 后端
- [ ] 所有 Controller 都注入了 MessageSource
- [ ] 所有 ApiResponse.error() 调用都传入了 messageSource
- [ ] 编译无错误
- [ ] 测试不同语言下的错误消息
### 前端
- [ ] 所有页面都使用了 useTranslation
- [ ] 所有硬编码文本都已替换
- [ ] 语言包包含所有需要的翻译键
- [ ] 测试不同语言下的页面显示
- [ ] 测试语言切换功能
+75 -1
View File
@@ -12,8 +12,10 @@
"antd-mobile": "^5.34.0",
"axios": "^1.6.2",
"ethers": "^6.9.0",
"i18next": "^25.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^16.3.5",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.20.0",
"zustand": "^4.4.7"
@@ -3136,11 +3138,49 @@
"node": ">= 0.4"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="
},
"node_modules/i18next": {
"version": "25.7.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.1.tgz",
"integrity": "sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4315,6 +4355,32 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-i18next": {
"version": "16.3.5",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.5.tgz",
"integrity": "sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww==",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -4698,7 +4764,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4818,6 +4884,14 @@
}
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+8 -7
View File
@@ -9,15 +9,17 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"axios": "^1.6.2",
"zustand": "^4.4.7",
"antd": "^5.12.0",
"antd-mobile": "^5.34.0",
"axios": "^1.6.2",
"ethers": "^6.9.0",
"react-responsive": "^9.0.2"
"i18next": "^25.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^16.3.5",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.20.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@types/react": "^18.2.43",
@@ -32,4 +34,3 @@
"vite": "^5.0.8"
}
}
+34 -15
View File
@@ -2,6 +2,9 @@ import { useEffect, useCallback, useState } from 'react'
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'
import { ConfigProvider, notification, Spin } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import zhTW from 'antd/locale/zh_TW'
import enUS from 'antd/locale/en_US'
import { useTranslation } from 'react-i18next'
import Layout from './components/Layout'
import Login from './pages/Login'
import ResetPassword from './pages/ResetPassword'
@@ -26,6 +29,9 @@ import CopyTradingBuyOrders from './pages/CopyTradingBuyOrders'
import CopyTradingSellOrders from './pages/CopyTradingSellOrders'
import CopyTradingMatchedOrders from './pages/CopyTradingMatchedOrders'
import SystemSettings from './pages/SystemSettings'
import LanguageSettings from './pages/LanguageSettings'
import ApiHealthStatus from './pages/ApiHealthStatus'
import ProxySettings from './pages/ProxySettings'
import { wsManager } from './services/websocket'
import type { OrderPushMessage } from './types'
import { apiService } from './services/api'
@@ -50,23 +56,33 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
}
function App() {
const { t, i18n } = useTranslation()
const [isFirstUse, setIsFirstUse] = useState<boolean | null>(null)
const [checking, setChecking] = useState(true)
// 根据当前语言设置 Ant Design 的 locale
const getAntdLocale = () => {
const lang = i18n.language || 'en'
if (lang.startsWith('zh-CN')) return zhCN
if (lang.startsWith('zh-TW') || lang.startsWith('zh-HK')) return zhTW
return enUS
}
/**
* 获取订单类型文本
*/
const getOrderTypeText = useCallback((type: string): string => {
switch (type) {
case 'PLACEMENT':
return '订单创建'
return t('order.create')
case 'UPDATE':
return '订单更新'
return t('order.update')
case 'CANCELLATION':
return '订单取消'
return t('order.cancel')
default:
return '订单事件'
return t('order.event')
}
}, [])
}, [t])
/**
* 处理订单推送消息,显示全局通知
@@ -76,7 +92,7 @@ function App() {
// 根据订单类型和操作类型确定通知内容
const orderTypeText = getOrderTypeText(order.type)
const sideText = order.side === 'BUY' ? '买入' : '卖出'
const sideText = order.side === 'BUY' ? t('order.buy') : t('order.sell')
// 如果有市场名称,在标题中显示
const marketName = orderDetail?.marketName || order.market.substring(0, 8) + '...'
@@ -89,21 +105,21 @@ function App() {
const status = orderDetail?.status || 'UNKNOWN'
// 构建描述信息
let description = `市场: ${marketName}\n${sideText} ${size} @ ${price}`
let description = `${t('order.market')}: ${marketName}\n${sideText} ${size} @ ${price}`
// 如果有订单详情,显示更详细的信息
if (orderDetail) {
description += `\n状态: ${status}`
description += `\n${t('order.status')}: ${status}`
if (parseFloat(filled) > 0) {
description += ` | 已成交: ${filled}`
description += ` | ${t('order.filled')}: ${filled}`
}
const remaining = (parseFloat(size) - parseFloat(filled)).toFixed(2)
if (parseFloat(remaining) > 0) {
description += ` | 剩余: ${remaining}`
description += ` | ${t('order.remaining')}: ${remaining}`
}
} else if (order.type === 'UPDATE' && parseFloat(order.size_matched) > 0) {
// 如果没有订单详情,使用 WebSocket 消息中的已成交数量
description += `\n已成交: ${filled}`
description += `\n${t('order.filled')}: ${filled}`
}
// 根据订单类型选择通知类型
@@ -136,7 +152,7 @@ function App() {
}
} catch (error) {
console.error('检查首次使用失败:', error)
setIsFirstUse(false) // 出错时默认不是首次使用
setIsFirstUse(false)
} finally {
setChecking(false)
}
@@ -170,7 +186,7 @@ function App() {
// 如果正在检查首次使用,显示加载中
if (checking) {
return (
<ConfigProvider locale={zhCN}>
<ConfigProvider locale={getAntdLocale()}>
<div style={{
display: 'flex',
justifyContent: 'center',
@@ -186,7 +202,7 @@ function App() {
// 如果首次使用,直接跳转到重置密码页面
if (isFirstUse === true) {
return (
<ConfigProvider locale={zhCN}>
<ConfigProvider locale={getAntdLocale()}>
<BrowserRouter>
<Routes>
<Route path="/reset-password" element={<ResetPassword />} />
@@ -198,7 +214,7 @@ function App() {
}
return (
<ConfigProvider locale={zhCN}>
<ConfigProvider locale={getAntdLocale()}>
<BrowserRouter>
<Routes>
{/* 公开路由(不需要鉴权) */}
@@ -228,6 +244,9 @@ function App() {
<Route path="/statistics" element={<ProtectedRoute><Statistics /></ProtectedRoute>} />
<Route path="/users" element={<ProtectedRoute><UserList /></ProtectedRoute>} />
<Route path="/system-settings" element={<ProtectedRoute><SystemSettings /></ProtectedRoute>} />
<Route path="/system-settings/language" element={<ProtectedRoute><LanguageSettings /></ProtectedRoute>} />
<Route path="/system-settings/api-health" element={<ProtectedRoute><ApiHealthStatus /></ProtectedRoute>} />
<Route path="/system-settings/proxy" element={<ProtectedRoute><ProxySettings /></ProtectedRoute>} />
{/* 默认重定向到登录页 */}
<Route path="*" element={<Navigate to="/login" replace />} />
@@ -0,0 +1,52 @@
import { useState, useEffect } from 'react'
import { Select, Space } from 'antd'
import { GlobalOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
const LanguageSwitcher: React.FC = () => {
const { i18n } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [currentLang, setCurrentLang] = useState<string>(i18n.language || 'en')
useEffect(() => {
setCurrentLang(i18n.language || 'en')
}, [i18n.language])
const languages = [
{ value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁體中文' },
{ value: 'en', label: 'English' }
]
const handleChange = async (value: string) => {
setCurrentLang(value)
await i18n.changeLanguage(value)
// 保存到 localStorage
localStorage.setItem('i18nextLng', value)
// 刷新页面以应用 Ant Design 的 locale 和所有翻译
window.location.reload()
}
return (
<Space>
<GlobalOutlined style={{ color: '#fff', fontSize: isMobile ? '14px' : '16px' }} />
<Select
value={currentLang}
onChange={handleChange}
options={languages}
style={{
width: isMobile ? 100 : 120,
color: '#fff'
}}
dropdownStyle={{
minWidth: 120
}}
bordered={false}
size={isMobile ? 'small' : 'middle'}
/>
</Space>
)
}
export default LanguageSwitcher
+45 -18
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { Layout as AntLayout, Menu, Drawer, Button, Modal } from 'antd'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
import {
WalletOutlined,
@@ -15,7 +16,9 @@ import {
LogoutOutlined,
SettingOutlined,
GithubOutlined,
TwitterOutlined
TwitterOutlined,
GlobalOutlined,
CheckCircleOutlined
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import type { ReactNode } from 'react'
@@ -29,6 +32,7 @@ interface LayoutProps {
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const { t } = useTranslation()
const navigate = useNavigate()
const location = useLocation()
const isMobile = useMediaQuery({ maxWidth: 768 })
@@ -46,6 +50,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
keys.push('/copy-trading-management')
}
if (path.startsWith('/system-settings')) {
keys.push('/system-settings')
}
return keys
}
@@ -58,6 +65,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
keys.push('/copy-trading-management')
}
if (path.startsWith('/system-settings')) {
keys.push('/system-settings')
}
setOpenKeys(keys)
}, [location.pathname])
@@ -65,54 +75,71 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
{
key: '/accounts',
icon: <WalletOutlined />,
label: '账户管理'
label: t('menu.accounts')
},
{
key: '/copy-trading-management',
icon: <AppstoreOutlined />,
label: '跟单交易',
label: t('menu.copyTrading'),
children: [
{
key: '/leaders',
icon: <UserOutlined />,
label: 'Leader 管理'
label: t('menu.leaders')
},
{
key: '/templates',
icon: <FileTextOutlined />,
label: '跟单模板'
label: t('menu.templates')
},
{
key: '/copy-trading',
icon: <LinkOutlined />,
label: '跟单配置'
label: t('menu.copyTradingConfig')
}
]
},
{
key: '/positions',
icon: <UnorderedListOutlined />,
label: '仓位管理'
label: t('menu.positions')
},
{
key: '/statistics',
icon: <BarChartOutlined />,
label: '统计信息'
label: t('menu.statistics')
},
{
key: '/users',
icon: <TeamOutlined />,
label: '用户管理'
label: t('menu.users')
},
{
key: '/system-settings',
icon: <SettingOutlined />,
label: '系统管理'
label: t('menu.systemSettings'),
children: [
{
key: '/system-settings/language',
icon: <GlobalOutlined />,
label: t('menu.language')
},
{
key: '/system-settings/api-health',
icon: <CheckCircleOutlined />,
label: t('menu.apiHealth')
},
{
key: '/system-settings/proxy',
icon: <LinkOutlined />,
label: t('menu.proxy')
}
]
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录'
label: t('menu.logout')
}
]
@@ -125,10 +152,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
const handleLogoutConfirm = () => {
Modal.confirm({
title: '确认退出',
content: '确定要退出登录吗?',
okText: '确定',
cancelText: '取消',
title: t('menu.logoutConfirm'),
content: t('menu.logoutConfirmDesc'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: () => {
handleLogout()
if (isMobile) {
@@ -140,7 +167,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
const handleMenuClick = ({ key }: { key: string }) => {
// 如果是父菜单,不导航
if (key === '/copy-trading-management') {
if (key === '/copy-trading-management' || key === '/system-settings') {
return
}
@@ -207,7 +234,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
{children}
</Content>
<Drawer
title="导航菜单"
title={t('menu.navigation')}
placement="left"
onClose={() => setMobileMenuOpen(false)}
open={mobileMenuOpen}
@@ -253,7 +280,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
flexShrink: 0
}}>
<span>PolyHermes</span>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<a
href="https://github.com/WrBug/PolyHermes"
target="_blank"
+79
View File
@@ -0,0 +1,79 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import zhCN from '../locales/zh-CN/common.json'
import zhTW from '../locales/zh-TW/common.json'
import en from '../locales/en/common.json'
/**
* 检测系统语言
* 支持的语言:zh-CN, zh-TW, en
* 如果不支持,默认使用 en
*/
const detectSystemLanguage = (): string => {
const systemLanguage = navigator.language || navigator.languages?.[0] || 'en'
const lang = systemLanguage.toLowerCase()
if (lang.startsWith('zh')) {
if (lang.includes('tw') || lang.includes('hk') || lang.includes('mo')) {
return 'zh-TW'
}
return 'zh-CN'
}
return 'en'
}
const detectLanguage = (): string => {
// 从 localStorage 读取用户设置的语言
const savedLanguage = localStorage.getItem('i18n_language')
// 如果是 auto 或未设置,使用系统语言
if (!savedLanguage || savedLanguage === 'auto') {
return detectSystemLanguage()
}
// 如果设置了具体语言,使用设置的语言
if (['zh-CN', 'zh-TW', 'en'].includes(savedLanguage)) {
return savedLanguage
}
// 默认使用系统语言
return detectSystemLanguage()
}
i18n
.use(initReactI18next)
.init({
resources: {
'zh-CN': {
translation: zhCN
},
'zh-TW': {
translation: zhTW
},
'en': {
translation: en
}
},
lng: detectLanguage(),
fallbackLng: 'en',
interpolation: {
escapeValue: false // React 已经转义了
}
})
export default i18n
/**
* 切换语言
*/
export const changeLanguage = (lng: 'zh-CN' | 'zh-TW' | 'en') => {
localStorage.setItem('i18n_language', lng)
i18n.changeLanguage(lng)
}
/**
* 获取当前语言
*/
export const getCurrentLanguage = (): string => {
return i18n.language || 'en'
}
+432
View File
@@ -0,0 +1,432 @@
{
"common": {
"back": "Back",
"save": "Save",
"cancel": "Cancel",
"edit": "Edit",
"delete": "Delete",
"add": "Add",
"search": "Search",
"refresh": "Refresh",
"loading": "Loading...",
"success": "Success",
"failed": "Failed",
"confirm": "Confirm",
"submit": "Submit",
"reset": "Reset",
"close": "Close",
"yes": "Yes",
"no": "No"
},
"account": {
"title": "Account Management",
"list": "Account List",
"detail": "Account Details",
"import": "Import Account",
"update": "Update Account",
"delete": "Delete Account",
"accountId": "Account ID",
"accountName": "Account Name",
"walletAddress": "Wallet Address",
"balance": "Account Balance",
"refreshBalance": "Refresh Balance",
"apiCredentials": "API Credentials Configuration",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "Configured",
"notConfigured": "Not Configured",
"fullConfig": "Full Configuration",
"partialConfig": "Partial Configuration",
"statistics": "Trading Statistics",
"totalOrders": "Total Orders",
"activeOrders": "Active Orders",
"completedOrders": "Completed Orders",
"positionCount": "Position Count",
"totalPnl": "Total P&L",
"editTip": "Edit Tip",
"editTipDesc": "Leave API credential fields empty to keep unchanged. Enter new values to update API credentials.",
"accountNamePlaceholder": "Account Name (Optional)",
"leaveEmptyToNotModify": "Leave empty to not modify",
"updateSuccess": "Account updated successfully",
"updateFailed": "Failed to update account",
"getDetailFailed": "Failed to get account details",
"accountIdRequired": "Account ID cannot be empty"
},
"message": {
"loginSuccess": "Login successful",
"loginFailed": "Login failed",
"createUserSuccess": "User created successfully",
"createUserFailed": "Failed to create user",
"updatePasswordSuccess": "Password updated successfully",
"updatePasswordFailed": "Failed to update password"
},
"login": {
"title": "Login",
"username": "Username",
"password": "Password",
"usernamePlaceholder": "Username",
"passwordPlaceholder": "Password",
"usernameRequired": "Please enter username",
"passwordRequired": "Please enter password",
"forgotPassword": "Forgot password? Reset password"
},
"order": {
"create": "Order Created",
"update": "Order Updated",
"cancel": "Order Cancelled",
"event": "Order Event",
"buy": "Buy",
"sell": "Sell",
"market": "Market",
"status": "Status",
"filled": "Filled",
"remaining": "Remaining"
},
"accountList": {
"title": "Account Management",
"importAccount": "Import Account",
"accountName": "Account Name",
"walletAddress": "Wallet Address",
"proxyAddress": "Proxy Wallet Address",
"apiCredentials": "API Credentials",
"balance": "Balance",
"activeOrders": "Active Orders",
"action": "Actions",
"detail": "Detail",
"edit": "Edit",
"delete": "Delete",
"viewDetail": "View Detail",
"deleteConfirm": "Are you sure you want to delete this account?",
"deleteConfirmDesc": "Before deleting the account, please make sure all active orders are cancelled. This action cannot be undone!",
"deleteConfirmDescSimple": "This action cannot be undone!",
"deleteConfirmOk": "Confirm Delete",
"deleteSuccess": "Account deleted successfully",
"deleteFailed": "Failed to delete account",
"copySuccess": "Copied to clipboard",
"copyFailed": "Copy failed",
"fullConfig": "Full Config",
"partialConfig": "Partial Config",
"notConfigured": "Not Configured",
"totalBalance": "Total Balance",
"available": "Available",
"position": "Position",
"refreshBalance": "Refresh Balance",
"refreshBalanceSuccess": "Balance refreshed successfully",
"refreshBalanceFailed": "Failed to refresh balance",
"getDetailFailed": "Failed to get account detail",
"openDetailFailed": "Failed to open detail",
"accountDetail": "Account Detail",
"accountId": "Account ID",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "Configured",
"notConfiguredStatus": "Not Configured",
"configStatus": "Config Status",
"statistics": "Trading Statistics",
"totalOrders": "Total Orders",
"activeOrdersCount": "Active Orders",
"completedOrders": "Completed Orders",
"positionCount": "Position Count",
"totalPnl": "Total PnL",
"editAccount": "Edit Account",
"editTip": "Edit Tip",
"editTipDesc": "Leave API credential fields empty to keep unchanged. Enter new values to update API credentials.",
"accountNamePlaceholder": "Account Name (Optional)",
"leaveEmptyToNotModify": "Leave empty to keep unchanged",
"updateSuccess": "Account updated successfully",
"updateFailed": "Failed to update account",
"getDetailFailedForEdit": "Failed to get account detail",
"loading": "Loading..."
},
"accountImport": {
"title": "Import Account",
"back": "Back",
"securityTip": "Security Tip",
"securityTipDesc": "Private keys will be stored in the backend database. Please ensure database access is secure. HTTPS is recommended.",
"importMethod": "Import Method",
"privateKey": "Private Key",
"mnemonic": "Mnemonic",
"privateKeyLabel": "Private Key",
"privateKeyPlaceholder": "Enter private key (64-character hex string, 0x prefix optional)",
"privateKeyRequired": "Please enter private key",
"privateKeyInvalid": "Invalid private key format (should be 64-character hex string)",
"walletAddress": "Wallet Address",
"walletAddressPlaceholder": "Wallet address (will be derived from private key)",
"walletAddressRequired": "Please enter wallet address",
"walletAddressInvalid": "Invalid wallet address format",
"walletAddressMismatch": "Wallet address does not match private key",
"mnemonicLabel": "Mnemonic",
"mnemonicPlaceholder": "Enter 12 or 24 words mnemonic (space-separated)",
"mnemonicRequired": "Please enter mnemonic",
"mnemonicInvalid": "Invalid mnemonic format (should be 12 or 24 words, space-separated)",
"walletAddressMismatchMnemonic": "Wallet address does not match mnemonic",
"accountName": "Account Name",
"accountNamePlaceholder": "Optional, for identifying account",
"importAccount": "Import Account",
"importSuccess": "Account imported successfully",
"importFailed": "Failed to import account",
"derivedAddress": "Derived Address",
"addressError": "Cannot derive address from private key",
"addressErrorMnemonic": "Cannot derive address from mnemonic"
},
"leader": {
"title": "Leader Management",
"leaderName": "Leader Name",
"walletAddress": "Wallet Address",
"category": "Category",
"all": "All",
"copyTradingCount": "Copy Trading Count",
"createdAt": "Created At",
"action": "Actions",
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"listFailed": "Failed to get Leader list",
"deleteSuccess": "Leader deleted successfully",
"deleteFailed": "Failed to delete Leader",
"deleteConfirm": "Are you sure you want to delete this Leader?",
"deleteConfirmDesc": "This action cannot be undone!",
"deleteConfirmOk": "Confirm Delete"
},
"menu": {
"accounts": "Account Management",
"copyTrading": "Copy Trading",
"leaders": "Leader Management",
"templates": "Templates",
"copyTradingConfig": "Copy Trading Config",
"positions": "Position Management",
"statistics": "Statistics",
"users": "User Management",
"systemSettings": "System",
"language": "Language",
"apiHealth": "API Health",
"proxy": "Proxy",
"logout": "Logout",
"logoutConfirm": "Confirm Logout",
"logoutConfirmDesc": "Are you sure you want to logout?",
"navigation": "Navigation Menu"
},
"languageSettings": {
"title": "Language Settings",
"currentLanguage": "Current Language",
"followSystem": "Follow System",
"currentSystemLanguage": "Current System Language",
"description": "After changing the language, the interface will update immediately.",
"changeSuccess": "Language changed successfully",
"changeFailed": "Failed to change language"
},
"apiHealthStatus": {
"title": "API Health Status",
"normal": "Normal",
"notConfigured": "Not Configured",
"abnormal": "Abnormal",
"responseTime": "Response Time"
},
"proxySettings": {
"title": "Proxy Settings",
"enabled": "Enable Proxy",
"host": "Proxy Host",
"port": "Proxy Port",
"username": "Proxy Username (Optional)",
"password": "Proxy Password (Optional)",
"hostRequired": "Please enter proxy host address",
"hostInvalid": "Please enter a valid host address",
"hostPlaceholder": "e.g., 127.0.0.1 or proxy.example.com",
"portRequired": "Please enter proxy port",
"portInvalid": "Port must be between 1-65535",
"portPlaceholder": "e.g., 8888",
"usernamePlaceholder": "Enter username if proxy requires authentication",
"passwordPlaceholder": "Enter password if proxy requires authentication",
"passwordPlaceholderUpdate": "Leave empty to keep password unchanged",
"passwordHelp": "Enter password if proxy requires authentication",
"passwordHelpUpdate": "Leave empty to keep password unchanged, enter new password to update",
"check": "Check Proxy",
"checkSuccess": "Proxy check successful",
"checkFailed": "Proxy check failed",
"saveSuccess": "Configuration saved successfully",
"saveFailed": "Failed to save configuration",
"getFailed": "Failed to get proxy configuration",
"latency": "Latency"
},
"configPage": {
"title": "Global Configuration",
"message": "Configuration Function Migrated",
"description": "Global configuration function has been migrated to the following pages:",
"templates": "Templates",
"templatesDesc": "Manage copy trading parameters (ratio, amount, risk control, etc.)",
"copyTrading": "Copy Trading Config",
"copyTradingDesc": "Associate accounts, templates and Leaders to enable copy trading relationships",
"systemSettings": "System Settings",
"systemSettingsDesc": "Configure proxy, view API health status",
"footer": "Please use the above pages for configuration management."
},
"resetPassword": {
"title": "Reset Password",
"firstUse": "First Time Using System",
"firstUseDesc": "Please use the reset key provided by the administrator to set the initial password",
"resetKey": "Reset Key",
"resetKeyRequired": "Please enter reset key",
"resetKeyPlaceholder": "Please enter reset key",
"username": "Username",
"usernameRequired": "Please enter username",
"usernamePlaceholder": "Please enter username",
"newPassword": "New Password",
"newPasswordRequired": "Please enter new password",
"passwordPlaceholder": "At least 6 characters",
"passwordMinLength": "Password must be at least 6 characters",
"passwordStrength": "Password Strength",
"weak": "Weak",
"fair": "Fair",
"medium": "Medium",
"strong": "Strong",
"veryStrong": "Very Strong",
"confirmPassword": "Confirm Password",
"confirmPasswordRequired": "Please confirm password",
"confirmPasswordPlaceholder": "Please enter password again",
"passwordMismatch": "Passwords do not match",
"submit": "Reset Password",
"success": "Password reset successfully",
"failed": "Password reset failed"
},
"leaderList": {
"title": "Leader Management",
"addLeader": "Add Leader",
"leaderName": "Leader Name",
"walletAddress": "Wallet Address",
"category": "Category",
"all": "All",
"copyTradingCount": "Copy Trading Count",
"copyTradingRelations": "{{count}} copy trading relations",
"createdAt": "Created At",
"noData": "No Leader data",
"fetchFailed": "Failed to get Leader list",
"deleteSuccess": "Leader deleted successfully",
"deleteFailed": "Failed to delete Leader",
"deleteConfirm": "Are you sure you want to delete this Leader?",
"deleteConfirmDesc": "This Leader has {{count}} copy trading relations, please delete them first"
},
"userList": {
"title": "User Management",
"username": "Username",
"role": "Role",
"defaultAccount": "Default Account",
"normalUser": "Normal User",
"updateMyPassword": "Update My Password",
"addUser": "Add User",
"createUser": "Create User",
"updatePassword": "Update Password",
"updateMyPasswordTitle": "Update My Password",
"newPassword": "New Password",
"password": "Password",
"passwordRequired": "Please enter password",
"passwordMinLength": "Password must be at least 6 characters",
"passwordPlaceholder": "At least 6 characters",
"usernameRequired": "Please enter username",
"usernamePlaceholder": "Please enter username",
"newPasswordRequired": "Please enter new password",
"fetchFailed": "Failed to get user list",
"createSuccess": "User created successfully",
"createFailed": "Failed to create user",
"updatePasswordSuccess": "Password updated successfully",
"updatePasswordFailed": "Failed to update password",
"updateOwnPasswordSuccess": "Password updated successfully, please login again",
"updateOwnPasswordFailed": "Failed to update password",
"deleteSuccess": "User deleted successfully",
"deleteFailed": "Failed to delete user",
"deleteConfirm": "Are you sure you want to delete this user?",
"total": "Total {{total}} items"
},
"statistics": {
"title": "Statistics",
"totalOrders": "Total Orders",
"totalPnl": "Total P&L",
"winRate": "Win Rate",
"avgPnl": "Average P&L",
"maxProfit": "Max Profit",
"maxLoss": "Max Loss",
"startDate": "Start Date",
"endDate": "End Date",
"fetchFailed": "Failed to get statistics",
"refresh": "Refresh",
"reset": "Reset"
},
"orderList": {
"title": "Order Management",
"leader": "Leader",
"market": "Market",
"category": "Category",
"side": "Side",
"price": "Price",
"size": "Size",
"status": "Status",
"pnl": "P&L",
"createdAt": "Created At",
"fetchFailed": "Failed to get order list"
},
"templateList": {
"title": "Template Management",
"addTemplate": "Add Template",
"searchPlaceholder": "Search template name",
"templateName": "Template Name",
"copyMode": "Copy Mode",
"ratio": "Ratio",
"fixedAmount": "Fixed Amount",
"ratioMode": "Ratio Mode",
"fixedAmountMode": "Fixed Amount Mode",
"copyConfig": "Copy Config",
"supportSell": "Support Sell",
"notSupportSell": "Not Support Sell",
"useCount": "Use Count",
"timesUsed": " times used",
"amountLimit": "Amount Limit",
"max": "Max",
"min": "Min",
"notSet": "Not Set",
"otherConfig": "Other Config",
"maxDailyOrders": "Max Daily Orders",
"priceTolerance": "Price Tolerance",
"copy": "Copy",
"copySuffix": "Copy",
"noData": "No template data",
"fetchFailed": "Failed to get template list",
"deleteSuccess": "Template deleted successfully",
"deleteFailed": "Failed to delete template",
"deleteConfirm": "Are you sure you want to delete this template?",
"deleteConfirmDesc": "This action cannot be undone. Please ensure no copy trading relationships are using this template",
"copySuccess": "Template copied successfully",
"copyFailed": "Failed to copy template",
"minAmountError": "Minimum amount must be >= 1",
"fixedAmountRequired": "Please enter fixed copy trading amount",
"invalidNumber": "Please enter a valid number",
"fixedAmountError": "Fixed amount must be >= 1, please re-enter"
},
"copyTradingList": {
"title": "Copy Trading Config Management",
"addCopyTrading": "Add Copy Trading",
"wallet": "Wallet",
"account": "Account",
"template": "Template",
"leader": "Leader",
"enabled": "Enabled",
"disabled": "Disabled",
"totalPnl": "Total P&L",
"statistics": "Statistics",
"orders": "Orders",
"viewStatistics": "View Statistics",
"buyOrders": "Buy Orders",
"sellOrders": "Sell Orders",
"matchedOrders": "Matched Orders",
"filterWallet": "Filter Wallet",
"filterTemplate": "Filter Template",
"filterLeader": "Filter Leader",
"fetchFailed": "Failed to get copy trading list",
"startSuccess": "Copy trading started successfully",
"stopSuccess": "Copy trading stopped successfully",
"updateStatusFailed": "Failed to update copy trading status",
"deleteSuccess": "Copy trading deleted successfully",
"deleteFailed": "Failed to delete copy trading",
"deleteConfirm": "Are you sure you want to delete this copy trading relationship?"
}
}
+334
View File
@@ -0,0 +1,334 @@
{
"common": {
"save": "保存",
"cancel": "取消",
"confirm": "确定",
"delete": "删除",
"edit": "编辑",
"add": "添加",
"refresh": "刷新",
"search": "搜索",
"reset": "重置",
"submit": "提交",
"actions": "操作",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"status": "状态",
"enabled": "启用",
"disabled": "禁用",
"yes": "是",
"no": "否",
"all": "全部",
"loading": "加载中",
"noData": "暂无数据",
"saveConfig": "保存配置",
"refreshConfig": "刷新配置"
},
"login": {
"title": "登录",
"username": "用户名",
"password": "密码",
"usernameRequired": "请输入用户名",
"passwordRequired": "请输入密码",
"usernamePlaceholder": "请输入用户名",
"passwordPlaceholder": "请输入密码",
"forgotPassword": "忘记密码?",
"loginFailed": "登录失败",
"loginSuccess": "登录成功"
},
"message": {
"success": "操作成功",
"error": "操作失败",
"loading": "加载中...",
"noData": "暂无数据"
},
"order": {
"create": "创建订单",
"update": "更新订单",
"cancel": "取消订单",
"event": "订单事件",
"buy": "买入",
"sell": "卖出"
},
"accountList": {
"title": "账户管理",
"addAccount": "添加账户",
"importAccount": "导入账户",
"accountName": "账户名称",
"walletAddress": "钱包地址",
"balance": "余额",
"actions": "操作",
"edit": "编辑",
"delete": "删除",
"viewDetail": "查看详情",
"deleteConfirm": "确定要删除这个账户吗?",
"deleteSuccess": "删除账户成功",
"deleteFailed": "删除账户失败",
"fetchFailed": "获取账户列表失败"
},
"accountImport": {
"title": "导入账户",
"privateKey": "私钥",
"privateKeyRequired": "请输入私钥",
"privateKeyPlaceholder": "请输入或粘贴私钥",
"privateKeyHelp": "私钥将加密存储,仅用于签名交易",
"accountName": "账户名称",
"accountNameRequired": "请输入账户名称",
"accountNamePlaceholder": "请输入账户名称",
"accountNameHelp": "用于标识账户,便于管理",
"submit": "导入",
"importSuccess": "导入账户成功",
"importFailed": "导入账户失败",
"invalidPrivateKey": "无效的私钥",
"duplicateAccount": "账户已存在"
},
"leader": {
"title": "Leader 管理",
"leaderName": "Leader 名称",
"leaderAddress": "钱包地址",
"category": "分类",
"addLeader": "添加 Leader",
"editLeader": "编辑 Leader",
"deleteLeader": "删除 Leader"
},
"menu": {
"accounts": "账户管理",
"copyTrading": "跟单交易",
"leaders": "Leader 管理",
"templates": "跟单模板",
"copyTradingConfig": "跟单配置",
"positions": "仓位管理",
"statistics": "统计信息",
"users": "用户管理",
"systemSettings": "系统管理",
"language": "语言",
"apiHealth": "API健康",
"proxy": "代理",
"logout": "退出登录",
"logoutConfirm": "确认退出",
"logoutConfirmDesc": "确定要退出登录吗?",
"navigation": "导航菜单"
},
"apiHealthStatus": {
"title": "API 健康状态",
"checkFailed": "检查失败",
"status": "状态",
"responseTime": "响应时间",
"lastCheck": "最后检查",
"healthy": "健康",
"unhealthy": "不健康",
"unknown": "未知"
},
"proxySettings": {
"title": "代理设置",
"enabled": "启用代理",
"host": "代理主机",
"port": "代理端口",
"username": "用户名",
"password": "密码",
"hostRequired": "请输入代理主机",
"portRequired": "请输入代理端口",
"hostPlaceholder": "例如:127.0.0.1",
"portPlaceholder": "例如:8080",
"usernamePlaceholder": "如果代理需要认证,请输入用户名",
"passwordPlaceholder": "如果代理需要认证,请输入密码",
"passwordPlaceholderUpdate": "留空则不更新密码",
"passwordHelp": "如果代理需要认证,请输入密码",
"passwordHelpUpdate": "留空则不更新密码,输入新密码则更新",
"check": "检查代理",
"checkSuccess": "代理检查成功",
"checkFailed": "代理检查失败",
"saveSuccess": "保存配置成功",
"saveFailed": "保存配置失败",
"getFailed": "获取代理配置失败",
"latency": "延迟"
},
"languageSettings": {
"title": "语言设置",
"currentLanguage": "当前语言",
"followSystem": "跟随系统",
"currentSystemLanguage": "当前系统语言",
"description": "切换语言后,界面将立即更新为新语言。",
"changeSuccess": "语言切换成功",
"changeFailed": "语言切换失败"
},
"configPage": {
"title": "全局配置",
"message": "配置功能已迁移",
"description": "全局配置功能已迁移到以下页面:",
"templates": "跟单模板",
"templatesDesc": "管理跟单参数(比例、金额、风险控制等)",
"copyTrading": "跟单配置",
"copyTradingDesc": "将账户、模板和 Leader 关联,启用跟单关系",
"systemSettings": "系统管理",
"systemSettingsDesc": "配置代理、查看 API 健康状态",
"footer": "请使用上述页面进行配置管理。"
},
"resetPassword": {
"title": "重置密码",
"firstUse": "首次使用系统",
"firstUseDesc": "请使用管理员提供的重置密钥设置初始密码",
"resetKey": "重置密钥",
"resetKeyRequired": "请输入重置密钥",
"resetKeyPlaceholder": "请输入重置密钥",
"username": "用户名",
"usernameRequired": "请输入用户名",
"usernamePlaceholder": "请输入用户名",
"newPassword": "新密码",
"newPasswordRequired": "请输入新密码",
"passwordPlaceholder": "至少6位",
"passwordMinLength": "密码至少6位",
"passwordStrength": "密码强度",
"weak": "弱",
"fair": "较弱",
"medium": "中等",
"strong": "强",
"veryStrong": "很强",
"confirmPassword": "确认密码",
"confirmPasswordRequired": "请确认密码",
"confirmPasswordPlaceholder": "请再次输入密码",
"passwordMismatch": "两次输入的密码不一致",
"submit": "重置密码",
"success": "密码重置成功",
"failed": "密码重置失败"
},
"leaderList": {
"title": "Leader 管理",
"addLeader": "添加 Leader",
"leaderName": "Leader 名称",
"walletAddress": "钱包地址",
"category": "分类",
"all": "全部",
"copyTradingCount": "跟单关系数",
"copyTradingRelations": "{{count}} 个跟单关系",
"createdAt": "创建时间",
"noData": "暂无 Leader 数据",
"fetchFailed": "获取 Leader 列表失败",
"deleteSuccess": "删除 Leader 成功",
"deleteFailed": "删除 Leader 失败",
"deleteConfirm": "确定要删除这个 Leader 吗?",
"deleteConfirmDesc": "该 Leader 还有 {{count}} 个跟单关系,请先删除跟单关系"
},
"userList": {
"title": "用户管理",
"username": "用户名",
"role": "角色",
"defaultAccount": "默认账户",
"normalUser": "普通用户",
"updateMyPassword": "修改我的密码",
"addUser": "新增用户",
"createUser": "创建用户",
"updatePassword": "修改密码",
"updateMyPasswordTitle": "修改我的密码",
"newPassword": "新密码",
"password": "密码",
"passwordRequired": "请输入密码",
"passwordMinLength": "密码至少6位",
"passwordPlaceholder": "至少6位",
"usernameRequired": "请输入用户名",
"usernamePlaceholder": "请输入用户名",
"newPasswordRequired": "请输入新密码",
"fetchFailed": "获取用户列表失败",
"createSuccess": "创建用户成功",
"createFailed": "创建用户失败",
"updatePasswordSuccess": "更新密码成功",
"updatePasswordFailed": "更新密码失败",
"updateOwnPasswordSuccess": "修改密码成功,请重新登录",
"updateOwnPasswordFailed": "修改密码失败",
"deleteSuccess": "删除用户成功",
"deleteFailed": "删除用户失败",
"deleteConfirm": "确定要删除这个用户吗?",
"total": "共 {{total}} 条"
},
"statistics": {
"title": "统计信息",
"totalOrders": "总订单数",
"totalPnl": "总盈亏",
"winRate": "胜率",
"avgPnl": "平均盈亏",
"maxProfit": "最大盈利",
"maxLoss": "最大亏损",
"startDate": "开始日期",
"endDate": "结束日期",
"fetchFailed": "获取统计信息失败",
"refresh": "刷新",
"reset": "重置"
},
"orderList": {
"title": "订单管理",
"leader": "Leader",
"market": "市场",
"category": "分类",
"side": "方向",
"price": "价格",
"size": "数量",
"status": "状态",
"pnl": "盈亏",
"createdAt": "创建时间",
"fetchFailed": "获取订单列表失败"
},
"templateList": {
"title": "跟单模板管理",
"addTemplate": "新增模板",
"searchPlaceholder": "搜索模板名称",
"templateName": "模板名称",
"copyMode": "跟单模式",
"ratio": "比例",
"fixedAmount": "固定金额",
"ratioMode": "比例模式",
"fixedAmountMode": "固定金额模式",
"copyConfig": "跟单配置",
"supportSell": "跟单卖出",
"notSupportSell": "不跟单卖出",
"useCount": "使用次数",
"timesUsed": "次使用",
"amountLimit": "金额限制",
"max": "最大",
"min": "最小",
"notSet": "未设置",
"otherConfig": "其他配置",
"maxDailyOrders": "每日最大订单",
"priceTolerance": "价格容忍度",
"copy": "复制",
"copySuffix": "副本",
"noData": "暂无模板数据",
"fetchFailed": "获取模板列表失败",
"deleteSuccess": "删除模板成功",
"deleteFailed": "删除模板失败",
"deleteConfirm": "确定要删除这个模板吗?",
"deleteConfirmDesc": "删除后无法恢复,请确保没有跟单关系在使用该模板",
"copySuccess": "复制模板成功",
"copyFailed": "复制模板失败",
"minAmountError": "最小金额必须 >= 1",
"fixedAmountRequired": "请输入固定跟单金额",
"invalidNumber": "请输入有效的数字",
"fixedAmountError": "固定金额必须 >= 1,请重新输入"
},
"copyTradingList": {
"title": "跟单配置管理",
"addCopyTrading": "新增跟单",
"wallet": "钱包",
"account": "账户",
"template": "模板",
"leader": "Leader",
"enabled": "开启",
"disabled": "停止",
"totalPnl": "总盈亏",
"statistics": "统计",
"orders": "订单",
"viewStatistics": "查看统计",
"buyOrders": "买入订单",
"sellOrders": "卖出订单",
"matchedOrders": "匹配关系",
"filterWallet": "筛选钱包",
"filterTemplate": "筛选模板",
"filterLeader": "筛选 Leader",
"fetchFailed": "获取跟单列表失败",
"startSuccess": "开启跟单成功",
"stopSuccess": "停止跟单成功",
"updateStatusFailed": "更新跟单状态失败",
"deleteSuccess": "删除跟单成功",
"deleteFailed": "删除跟单失败",
"deleteConfirm": "确定要删除这个跟单关系吗?"
}
}
+432
View File
@@ -0,0 +1,432 @@
{
"common": {
"back": "返回",
"save": "保存",
"cancel": "取消",
"edit": "編輯",
"delete": "刪除",
"add": "添加",
"search": "搜索",
"refresh": "刷新",
"loading": "加載中...",
"success": "成功",
"failed": "失敗",
"confirm": "確認",
"submit": "提交",
"reset": "重置",
"close": "關閉",
"yes": "是",
"no": "否"
},
"account": {
"title": "賬戶管理",
"list": "賬戶列表",
"detail": "賬戶詳情",
"import": "導入賬戶",
"update": "更新賬戶",
"delete": "刪除賬戶",
"accountId": "賬戶ID",
"accountName": "賬戶名稱",
"walletAddress": "錢包地址",
"balance": "賬戶餘額",
"refreshBalance": "刷新餘額",
"apiCredentials": "API 憑證配置",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "已配置",
"notConfigured": "未配置",
"fullConfig": "完整配置",
"partialConfig": "部分配置",
"statistics": "交易統計",
"totalOrders": "總訂單數",
"activeOrders": "活躍訂單數",
"completedOrders": "已完成訂單數",
"positionCount": "持倉數量",
"totalPnl": "總盈虧",
"editTip": "編輯提示",
"editTipDesc": "API 憑證字段留空表示不修改。如需更新 API 憑證,請輸入新值;如需保持原值不變,請留空。",
"accountNamePlaceholder": "賬戶名稱(可選)",
"leaveEmptyToNotModify": "留空表示不修改",
"updateSuccess": "更新賬戶成功",
"updateFailed": "更新賬戶失敗",
"getDetailFailed": "獲取賬戶詳情失敗",
"accountIdRequired": "賬戶ID不能為空"
},
"message": {
"loginSuccess": "登錄成功",
"loginFailed": "登錄失敗",
"createUserSuccess": "創建用戶成功",
"createUserFailed": "創建用戶失敗",
"updatePasswordSuccess": "更新密碼成功",
"updatePasswordFailed": "更新密碼失敗"
},
"login": {
"title": "登錄",
"username": "用戶名",
"password": "密碼",
"usernamePlaceholder": "用戶名",
"passwordPlaceholder": "密碼",
"usernameRequired": "請輸入用戶名",
"passwordRequired": "請輸入密碼",
"forgotPassword": "忘記密碼?重置密碼"
},
"order": {
"create": "訂單創建",
"update": "訂單更新",
"cancel": "訂單取消",
"event": "訂單事件",
"buy": "買入",
"sell": "賣出",
"market": "市場",
"status": "狀態",
"filled": "已成交",
"remaining": "剩餘"
},
"accountList": {
"title": "賬戶管理",
"importAccount": "導入賬戶",
"accountName": "賬戶名稱",
"walletAddress": "錢包地址",
"proxyAddress": "代理錢包地址",
"apiCredentials": "API 憑證",
"balance": "餘額",
"activeOrders": "活躍訂單",
"action": "操作",
"detail": "詳情",
"edit": "編輯",
"delete": "刪除",
"viewDetail": "查看詳情",
"deleteConfirm": "確定要刪除這個賬戶嗎?",
"deleteConfirmDesc": "刪除賬戶前,請確保已取消所有活躍訂單。刪除後無法恢復,請謹慎操作!",
"deleteConfirmDescSimple": "刪除後無法恢復,請謹慎操作!",
"deleteConfirmOk": "確定刪除",
"deleteSuccess": "刪除賬戶成功",
"deleteFailed": "刪除賬戶失敗",
"copySuccess": "已複製到剪貼板",
"copyFailed": "複製失敗",
"fullConfig": "完整配置",
"partialConfig": "部分配置",
"notConfigured": "未配置",
"totalBalance": "總餘額",
"available": "可用",
"position": "倉位",
"refreshBalance": "刷新餘額",
"refreshBalanceSuccess": "餘額刷新成功",
"refreshBalanceFailed": "刷新餘額失敗",
"getDetailFailed": "獲取賬戶詳情失敗",
"openDetailFailed": "打開詳情失敗",
"accountDetail": "賬戶詳情",
"accountId": "賬戶ID",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "已配置",
"notConfiguredStatus": "未配置",
"configStatus": "配置狀態",
"statistics": "交易統計",
"totalOrders": "總訂單數",
"activeOrdersCount": "活躍訂單數",
"completedOrders": "已完成訂單數",
"positionCount": "持倉數量",
"totalPnl": "總盈虧",
"editAccount": "編輯賬戶",
"editTip": "編輯提示",
"editTipDesc": "API 憑證字段留空表示不修改。如需更新 API 憑證,請輸入新值;如需保持原值不變,請留空。",
"accountNamePlaceholder": "賬戶名稱(可選)",
"leaveEmptyToNotModify": "留空表示不修改",
"updateSuccess": "更新賬戶成功",
"updateFailed": "更新賬戶失敗",
"getDetailFailedForEdit": "獲取賬戶詳情失敗",
"loading": "加載中..."
},
"accountImport": {
"title": "導入賬戶",
"back": "返回",
"securityTip": "安全提示",
"securityTipDesc": "私鑰將存儲在後端數據庫中,請確保數據庫訪問安全。建議使用 HTTPS 連接。",
"importMethod": "導入方式",
"privateKey": "私鑰",
"mnemonic": "助記詞",
"privateKeyLabel": "私鑰",
"privateKeyPlaceholder": "請輸入私鑰(64位十六進制字符串,可選0x前綴)",
"privateKeyRequired": "請輸入私鑰",
"privateKeyInvalid": "私鑰格式不正確(應為64位十六進制字符串)",
"walletAddress": "錢包地址",
"walletAddressPlaceholder": "錢包地址(將從私鑰自動推導)",
"walletAddressRequired": "請輸入錢包地址",
"walletAddressInvalid": "錢包地址格式不正確",
"walletAddressMismatch": "錢包地址與私鑰不匹配",
"mnemonicLabel": "助記詞",
"mnemonicPlaceholder": "請輸入12或24個單詞的助記詞(用空格分隔)",
"mnemonicRequired": "請輸入助記詞",
"mnemonicInvalid": "助記詞格式不正確(應為12或24個單詞,用空格分隔)",
"walletAddressMismatchMnemonic": "錢包地址與助記詞不匹配",
"accountName": "賬戶名稱",
"accountNamePlaceholder": "可選,用於標識賬戶",
"importAccount": "導入賬戶",
"importSuccess": "導入賬戶成功",
"importFailed": "導入賬戶失敗",
"derivedAddress": "推導地址",
"addressError": "無法從私鑰推導地址",
"addressErrorMnemonic": "無法從助記詞推導地址"
},
"leader": {
"title": "Leader 管理",
"leaderName": "Leader 名稱",
"walletAddress": "錢包地址",
"category": "分類",
"all": "全部",
"copyTradingCount": "跟單關係數",
"createdAt": "創建時間",
"action": "操作",
"add": "添加",
"edit": "編輯",
"delete": "刪除",
"listFailed": "獲取 Leader 列表失敗",
"deleteSuccess": "刪除 Leader 成功",
"deleteFailed": "刪除 Leader 失敗",
"deleteConfirm": "確定要刪除這個 Leader 嗎?",
"deleteConfirmDesc": "刪除後無法恢復,請謹慎操作!",
"deleteConfirmOk": "確定刪除"
},
"menu": {
"accounts": "賬戶管理",
"copyTrading": "跟單交易",
"leaders": "Leader 管理",
"templates": "跟單模板",
"copyTradingConfig": "跟單配置",
"positions": "倉位管理",
"statistics": "統計信息",
"users": "用戶管理",
"systemSettings": "系統管理",
"language": "語言",
"apiHealth": "API健康",
"proxy": "代理",
"logout": "退出登錄",
"logoutConfirm": "確認退出",
"logoutConfirmDesc": "確定要退出登錄嗎?",
"navigation": "導航菜單"
},
"languageSettings": {
"title": "語言設置",
"currentLanguage": "當前語言",
"followSystem": "跟隨系統",
"currentSystemLanguage": "當前系統語言",
"description": "切換語言後,界面將立即更新為新語言。",
"changeSuccess": "語言切換成功",
"changeFailed": "語言切換失敗"
},
"apiHealthStatus": {
"title": "API 健康狀態",
"normal": "正常",
"notConfigured": "未配置",
"abnormal": "異常",
"responseTime": "響應時間"
},
"proxySettings": {
"title": "代理設置",
"enabled": "啟用代理",
"host": "代理主機",
"port": "代理端口",
"username": "代理用戶名(可選)",
"password": "代理密碼(可選)",
"hostRequired": "請輸入代理主機地址",
"hostInvalid": "請輸入有效的主機地址",
"hostPlaceholder": "例如:127.0.0.1 或 proxy.example.com",
"portRequired": "請輸入代理端口",
"portInvalid": "端口必須在 1-65535 之間",
"portPlaceholder": "例如:8888",
"usernamePlaceholder": "如果代理需要認證,請輸入用戶名",
"passwordPlaceholder": "如果代理需要認證,請輸入密碼",
"passwordPlaceholderUpdate": "留空則不更新密碼",
"passwordHelp": "如果代理需要認證,請輸入密碼",
"passwordHelpUpdate": "留空則不更新密碼,輸入新密碼則更新",
"check": "檢查代理",
"checkSuccess": "代理檢查成功",
"checkFailed": "代理檢查失敗",
"saveSuccess": "保存配置成功",
"saveFailed": "保存配置失敗",
"getFailed": "獲取代理配置失敗",
"latency": "延遲"
},
"configPage": {
"title": "全局配置",
"message": "配置功能已遷移",
"description": "全局配置功能已遷移到以下頁面:",
"templates": "跟單模板",
"templatesDesc": "管理跟單參數(比例、金額、風險控制等)",
"copyTrading": "跟單配置",
"copyTradingDesc": "將賬戶、模板和 Leader 關聯,啟用跟單關係",
"systemSettings": "系統管理",
"systemSettingsDesc": "配置代理、查看 API 健康狀態",
"footer": "請使用上述頁面進行配置管理。"
},
"resetPassword": {
"title": "重置密碼",
"firstUse": "首次使用系統",
"firstUseDesc": "請使用管理員提供的重置密鑰設置初始密碼",
"resetKey": "重置密鑰",
"resetKeyRequired": "請輸入重置密鑰",
"resetKeyPlaceholder": "請輸入重置密鑰",
"username": "用戶名",
"usernameRequired": "請輸入用戶名",
"usernamePlaceholder": "請輸入用戶名",
"newPassword": "新密碼",
"newPasswordRequired": "請輸入新密碼",
"passwordPlaceholder": "至少6位",
"passwordMinLength": "密碼至少6位",
"passwordStrength": "密碼強度",
"weak": "弱",
"fair": "較弱",
"medium": "中等",
"strong": "強",
"veryStrong": "很強",
"confirmPassword": "確認密碼",
"confirmPasswordRequired": "請確認密碼",
"confirmPasswordPlaceholder": "請再次輸入密碼",
"passwordMismatch": "兩次輸入的密碼不一致",
"submit": "重置密碼",
"success": "密碼重置成功",
"failed": "密碼重置失敗"
},
"leaderList": {
"title": "Leader 管理",
"addLeader": "添加 Leader",
"leaderName": "Leader 名稱",
"walletAddress": "錢包地址",
"category": "分類",
"all": "全部",
"copyTradingCount": "跟單關係數",
"copyTradingRelations": "{{count}} 個跟單關係",
"createdAt": "創建時間",
"noData": "暫無 Leader 數據",
"fetchFailed": "獲取 Leader 列表失敗",
"deleteSuccess": "刪除 Leader 成功",
"deleteFailed": "刪除 Leader 失敗",
"deleteConfirm": "確定要刪除這個 Leader 嗎?",
"deleteConfirmDesc": "該 Leader 還有 {{count}} 個跟單關係,請先刪除跟單關係"
},
"userList": {
"title": "用戶管理",
"username": "用戶名",
"role": "角色",
"defaultAccount": "默認賬戶",
"normalUser": "普通用戶",
"updateMyPassword": "修改我的密碼",
"addUser": "新增用戶",
"createUser": "創建用戶",
"updatePassword": "修改密碼",
"updateMyPasswordTitle": "修改我的密碼",
"newPassword": "新密碼",
"password": "密碼",
"passwordRequired": "請輸入密碼",
"passwordMinLength": "密碼至少6位",
"passwordPlaceholder": "至少6位",
"usernameRequired": "請輸入用戶名",
"usernamePlaceholder": "請輸入用戶名",
"newPasswordRequired": "請輸入新密碼",
"fetchFailed": "獲取用戶列表失敗",
"createSuccess": "創建用戶成功",
"createFailed": "創建用戶失敗",
"updatePasswordSuccess": "更新密碼成功",
"updatePasswordFailed": "更新密碼失敗",
"updateOwnPasswordSuccess": "修改密碼成功,請重新登錄",
"updateOwnPasswordFailed": "修改密碼失敗",
"deleteSuccess": "刪除用戶成功",
"deleteFailed": "刪除用戶失敗",
"deleteConfirm": "確定要刪除這個用戶嗎?",
"total": "共 {{total}} 條"
},
"statistics": {
"title": "統計信息",
"totalOrders": "總訂單數",
"totalPnl": "總盈虧",
"winRate": "勝率",
"avgPnl": "平均盈虧",
"maxProfit": "最大盈利",
"maxLoss": "最大虧損",
"startDate": "開始日期",
"endDate": "結束日期",
"fetchFailed": "獲取統計信息失敗",
"refresh": "刷新",
"reset": "重置"
},
"orderList": {
"title": "訂單管理",
"leader": "Leader",
"market": "市場",
"category": "分類",
"side": "方向",
"price": "價格",
"size": "數量",
"status": "狀態",
"pnl": "盈虧",
"createdAt": "創建時間",
"fetchFailed": "獲取訂單列表失敗"
},
"templateList": {
"title": "跟單模板管理",
"addTemplate": "新增模板",
"searchPlaceholder": "搜索模板名稱",
"templateName": "模板名稱",
"copyMode": "跟單模式",
"ratio": "比例",
"fixedAmount": "固定金額",
"ratioMode": "比例模式",
"fixedAmountMode": "固定金額模式",
"copyConfig": "跟單配置",
"supportSell": "跟單賣出",
"notSupportSell": "不跟單賣出",
"useCount": "使用次數",
"timesUsed": "次使用",
"amountLimit": "金額限制",
"max": "最大",
"min": "最小",
"notSet": "未設置",
"otherConfig": "其他配置",
"maxDailyOrders": "每日最大訂單",
"priceTolerance": "價格容忍度",
"copy": "複製",
"copySuffix": "副本",
"noData": "暫無模板數據",
"fetchFailed": "獲取模板列表失敗",
"deleteSuccess": "刪除模板成功",
"deleteFailed": "刪除模板失敗",
"deleteConfirm": "確定要刪除這個模板嗎?",
"deleteConfirmDesc": "刪除後無法恢復,請確保沒有跟單關係在使用該模板",
"copySuccess": "複製模板成功",
"copyFailed": "複製模板失敗",
"minAmountError": "最小金額必須 >= 1",
"fixedAmountRequired": "請輸入固定跟單金額",
"invalidNumber": "請輸入有效的數字",
"fixedAmountError": "固定金額必須 >= 1,請重新輸入"
},
"copyTradingList": {
"title": "跟單配置管理",
"addCopyTrading": "新增跟單",
"wallet": "錢包",
"account": "賬戶",
"template": "模板",
"leader": "Leader",
"enabled": "開啟",
"disabled": "停止",
"totalPnl": "總盈虧",
"statistics": "統計",
"orders": "訂單",
"viewStatistics": "查看統計",
"buyOrders": "買入訂單",
"sellOrders": "賣出訂單",
"matchedOrders": "匹配關係",
"filterWallet": "篩選錢包",
"filterTemplate": "篩選模板",
"filterLeader": "篩選 Leader",
"fetchFailed": "獲取跟單列表失敗",
"startSuccess": "開啟跟單成功",
"stopSuccess": "停止跟單成功",
"updateStatusFailed": "更新跟單狀態失敗",
"deleteSuccess": "刪除跟單成功",
"deleteFailed": "刪除跟單失敗",
"deleteConfirm": "確定要刪除這個跟單關係嗎?"
}
}
+3
View File
@@ -1,6 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './i18n/config' // 初始化 i18n
import './styles/index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
@@ -9,3 +10,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
</React.StrictMode>,
)
+50 -44
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Card, Descriptions, Button, Space, Tag, Spin, message, Typography, Divider, Modal, Form, Input, Alert } from 'antd'
import { ArrowLeftOutlined, ReloadOutlined, EditOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore'
import type { Account } from '../types'
import { useMediaQuery } from 'react-responsive'
@@ -10,6 +11,7 @@ import { formatUSDC } from '../utils'
const { Title } = Typography
const AccountDetail: React.FC = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const isMobile = useMediaQuery({ maxWidth: 768 })
@@ -29,7 +31,7 @@ const AccountDetail: React.FC = () => {
loadAccountDetail()
loadBalance()
} else {
message.error('账户ID不能为空')
message.error(t('account.accountIdRequired'))
navigate('/accounts')
}
}, [accountId])
@@ -42,7 +44,7 @@ const AccountDetail: React.FC = () => {
const accountData = await fetchAccountDetail(Number(accountId))
setAccount(accountData)
} catch (error: any) {
message.error(error.message || '获取账户详情失败')
message.error(error.message || t('account.getDetailFailed'))
navigate('/accounts')
} finally {
setLoading(false)
@@ -89,7 +91,7 @@ const AccountDetail: React.FC = () => {
await updateAccount(updateData)
message.success('更新账户成功')
message.success(t('account.updateSuccess'))
setEditModalVisible(false)
editForm.resetFields()
@@ -98,7 +100,7 @@ const AccountDetail: React.FC = () => {
await loadAccountDetail()
}
} catch (error: any) {
message.error(error.message || '更新账户失败')
message.error(error.message || t('account.updateFailed'))
} finally {
setEditLoading(false)
}
@@ -136,7 +138,7 @@ const AccountDetail: React.FC = () => {
onClick={() => navigate('/accounts')}
size={isMobile ? 'middle' : 'large'}
>
{t('common.back')}
</Button>
<Title level={isMobile ? 4 : 2} style={{ margin: 0, fontSize: isMobile ? '16px' : undefined }}>
{account.accountName || `账户 ${account.id}`}
@@ -151,7 +153,7 @@ const AccountDetail: React.FC = () => {
block={isMobile}
style={isMobile ? { minHeight: '44px' } : undefined}
>
{t('account.refreshBalance')}
</Button>
<Button
type="primary"
@@ -169,7 +171,7 @@ const AccountDetail: React.FC = () => {
block={isMobile}
style={isMobile ? { minHeight: '44px' } : undefined}
>
{t('common.edit')}
</Button>
</Space>
</div>
@@ -184,13 +186,13 @@ const AccountDetail: React.FC = () => {
size={isMobile ? 'small' : 'middle'}
style={{ fontSize: isMobile ? '14px' : undefined }}
>
<Descriptions.Item label="账户ID">
<Descriptions.Item label={t('account.accountId')}>
{account.id}
</Descriptions.Item>
<Descriptions.Item label="账户名称">
<Descriptions.Item label={t('account.accountName')}>
{account.accountName || '-'}
</Descriptions.Item>
<Descriptions.Item label="钱包地址" span={isMobile ? 1 : 2}>
<Descriptions.Item label={t('account.walletAddress')} span={isMobile ? 1 : 2}>
<span style={{
fontFamily: 'monospace',
fontSize: isMobile ? '11px' : '14px',
@@ -201,7 +203,7 @@ const AccountDetail: React.FC = () => {
{account.walletAddress}
</span>
</Descriptions.Item>
<Descriptions.Item label="账户余额">
<Descriptions.Item label={t('account.balance')}>
{balanceLoading ? (
<Spin size="small" />
) : balance ? (
@@ -218,7 +220,7 @@ const AccountDetail: React.FC = () => {
<Divider />
<Card
title="API 凭证配置"
title={t('account.apiCredentials')}
style={{
marginTop: isMobile ? '12px' : '16px',
margin: isMobile ? '0 -8px' : '0',
@@ -231,26 +233,26 @@ const AccountDetail: React.FC = () => {
size={isMobile ? 'small' : 'middle'}
style={{ fontSize: isMobile ? '14px' : undefined }}
>
<Descriptions.Item label="API Key">
<Descriptions.Item label={t('account.apiKey')}>
<Tag color={account.apiKeyConfigured ? 'success' : 'default'}>
{account.apiKeyConfigured ? '已配置' : '未配置'}
{account.apiKeyConfigured ? t('account.configured') : t('account.notConfigured')}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="API Secret">
<Descriptions.Item label={t('account.apiSecret')}>
<Tag color={account.apiSecretConfigured ? 'success' : 'default'}>
{account.apiSecretConfigured ? '已配置' : '未配置'}
{account.apiSecretConfigured ? t('account.configured') : t('account.notConfigured')}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="API Passphrase">
<Descriptions.Item label={t('account.apiPassphrase')}>
<Tag color={account.apiPassphraseConfigured ? 'success' : 'default'}>
{account.apiPassphraseConfigured ? '已配置' : '未配置'}
{account.apiPassphraseConfigured ? t('account.configured') : t('account.notConfigured')}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="配置状态">
<Descriptions.Item label={t('account.apiCredentials')}>
{account.apiKeyConfigured && account.apiSecretConfigured && account.apiPassphraseConfigured ? (
<Tag color="success"></Tag>
<Tag color="success">{t('account.fullConfig')}</Tag>
) : (
<Tag color="warning"></Tag>
<Tag color="warning">{t('account.partialConfig')}</Tag>
)}
</Descriptions.Item>
</Descriptions>
@@ -262,7 +264,7 @@ const AccountDetail: React.FC = () => {
<>
<Divider style={{ margin: isMobile ? '12px 0' : '16px 0' }} />
<Card
title="交易统计"
title={t('account.statistics')}
style={{
marginTop: isMobile ? '12px' : '16px',
margin: isMobile ? '0 -8px' : '0',
@@ -276,27 +278,27 @@ const AccountDetail: React.FC = () => {
style={{ fontSize: isMobile ? '14px' : undefined }}
>
{account.totalOrders !== undefined && (
<Descriptions.Item label="总订单数">
<Descriptions.Item label={t('account.totalOrders')}>
{account.totalOrders}
</Descriptions.Item>
)}
{account.activeOrders !== undefined && (
<Descriptions.Item label="活跃订单数">
<Descriptions.Item label={t('account.activeOrders')}>
<Tag color={account.activeOrders > 0 ? 'orange' : 'default'}>{account.activeOrders}</Tag>
</Descriptions.Item>
)}
{account.completedOrders !== undefined && (
<Descriptions.Item label="已完成订单数">
<Descriptions.Item label={t('account.completedOrders')}>
<Tag color="success">{account.completedOrders}</Tag>
</Descriptions.Item>
)}
{account.positionCount !== undefined && (
<Descriptions.Item label="持仓数量">
<Descriptions.Item label={t('account.positionCount')}>
<Tag color={account.positionCount > 0 ? 'blue' : 'default'}>{account.positionCount}</Tag>
</Descriptions.Item>
)}
{account.totalPnl !== undefined && (
<Descriptions.Item label="总盈亏">
<Descriptions.Item label={t('account.totalPnl')}>
<span style={{
fontWeight: 'bold',
color: account.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
@@ -312,7 +314,7 @@ const AccountDetail: React.FC = () => {
{/* 编辑账户 Modal */}
<Modal
title={account ? `编辑账户 - ${account.accountName || `账户 ${account.id}`}` : '编辑账户'}
title={account ? `${t('common.edit')} ${t('account.title')} - ${account.accountName || `${t('account.title')} ${account.id}`}` : t('common.edit') + ' ' + t('account.title')}
open={editModalVisible}
onCancel={() => {
setEditModalVisible(false)
@@ -333,42 +335,42 @@ const AccountDetail: React.FC = () => {
size={isMobile ? 'middle' : 'large'}
>
<Alert
message="编辑提示"
description="API 凭证字段留空表示不修改。如需更新 API 凭证,请输入新值;如需保持原值不变,请留空。"
message={t('account.editTip')}
description={t('account.editTipDesc')}
type="info"
showIcon
style={{ marginBottom: '24px' }}
/>
<Form.Item
label="账户名称"
label={t('account.accountName')}
name="accountName"
>
<Input placeholder="账户名称(可选)" />
<Input placeholder={t('account.accountNamePlaceholder')} />
</Form.Item>
<Form.Item
label="API Key"
label={t('account.apiKey')}
name="apiKey"
help="留空表示不修改,输入新值将更新 API Key"
help={t('account.leaveEmptyToNotModify')}
>
<Input.Password placeholder="留空表示不修改" />
<Input.Password placeholder={t('account.leaveEmptyToNotModify')} />
</Form.Item>
<Form.Item
label="API Secret"
label={t('account.apiSecret')}
name="apiSecret"
help="留空表示不修改,输入新值将更新 API Secret"
help={t('account.leaveEmptyToNotModify')}
>
<Input.Password placeholder="留空表示不修改" />
<Input.Password placeholder={t('account.leaveEmptyToNotModify')} />
</Form.Item>
<Form.Item
label="API Passphrase"
label={t('account.apiPassphrase')}
name="apiPassphrase"
help="留空表示不修改,输入新值将更新 API Passphrase"
help={t('account.leaveEmptyToNotModify')}
>
<Input.Password placeholder="留空表示不修改" />
<Input.Password placeholder={t('account.leaveEmptyToNotModify')} />
</Form.Item>
<Form.Item>
@@ -381,7 +383,7 @@ const AccountDetail: React.FC = () => {
size={isMobile ? 'middle' : 'large'}
style={isMobile ? { minHeight: '44px' } : undefined}
>
{t('common.cancel')}
</Button>
<Button
type="primary"
@@ -390,7 +392,7 @@ const AccountDetail: React.FC = () => {
size={isMobile ? 'middle' : 'large'}
style={isMobile ? { minHeight: '44px' } : undefined}
>
{t('common.save')}
</Button>
</Space>
</Form.Item>
@@ -398,7 +400,7 @@ const AccountDetail: React.FC = () => {
) : (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>...</div>
<div style={{ marginTop: '16px' }}>{t('common.loading')}</div>
</div>
)}
</Modal>
@@ -408,3 +410,7 @@ const AccountDetail: React.FC = () => {
export default AccountDetail
+43 -41
View File
@@ -2,6 +2,7 @@ import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Form, Input, Button, message, Typography, Radio, Space, Alert } from 'antd'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore'
import {
getAddressFromPrivateKey,
@@ -18,6 +19,7 @@ const { Title } = Typography
type ImportType = 'privateKey' | 'mnemonic'
const AccountImport: React.FC = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const { importAccount, loading } = useAccountStore()
@@ -37,7 +39,7 @@ const AccountImport: React.FC = () => {
// 验证私钥格式
if (!isValidPrivateKey(privateKey)) {
setAddressError('私钥格式不正确(应为64位十六进制字符串)')
setAddressError(t('accountImport.privateKeyInvalid'))
setDerivedAddress('')
return
}
@@ -50,7 +52,7 @@ const AccountImport: React.FC = () => {
// 自动填充钱包地址字段
form.setFieldsValue({ walletAddress: address })
} catch (error: any) {
setAddressError(error.message || '无法从私钥推导地址')
setAddressError(error.message || t('accountImport.addressError'))
setDerivedAddress('')
}
}
@@ -66,7 +68,7 @@ const AccountImport: React.FC = () => {
// 验证助记词格式
if (!isValidMnemonic(mnemonic)) {
setAddressError('助记词格式不正确(应为12或24个单词,用空格分隔)')
setAddressError(t('accountImport.mnemonicInvalid'))
setDerivedAddress('')
return
}
@@ -79,7 +81,7 @@ const AccountImport: React.FC = () => {
// 自动填充钱包地址字段
form.setFieldsValue({ walletAddress: address })
} catch (error: any) {
setAddressError(error.message || '无法从助记词推导地址')
setAddressError(error.message || t('accountImport.addressErrorMnemonic'))
setDerivedAddress('')
}
}
@@ -96,13 +98,13 @@ const AccountImport: React.FC = () => {
// 验证推导的地址和输入的地址是否一致
if (derivedAddress && walletAddress !== derivedAddress) {
message.error('钱包地址与私钥不匹配')
message.error(t('accountImport.walletAddressMismatch'))
return
}
} else {
// 助记词模式
if (!values.mnemonic) {
message.error('请输入助记词')
message.error(t('accountImport.mnemonicRequired'))
return
}
@@ -114,7 +116,7 @@ const AccountImport: React.FC = () => {
if (values.walletAddress) {
if (values.walletAddress !== derivedAddressFromMnemonic) {
// 地址不匹配,使用推导的地址(因为私钥是从助记词导出的,必须使用对应的地址)
message.warning(`输入的地址与助记词推导的地址不一致。推导的地址: ${derivedAddressFromMnemonic},将使用推导的地址`)
message.warning(`${t('accountImport.walletAddressMismatchMnemonic')}: ${derivedAddressFromMnemonic}`)
walletAddress = derivedAddressFromMnemonic
} else {
// 地址匹配,使用用户输入的地址
@@ -128,7 +130,7 @@ const AccountImport: React.FC = () => {
// 验证钱包地址格式
if (!isValidWalletAddress(walletAddress)) {
message.error('钱包地址格式不正确')
message.error(t('accountImport.walletAddressInvalid'))
return
}
@@ -138,10 +140,10 @@ const AccountImport: React.FC = () => {
accountName: values.accountName
})
message.success('导入账户成功')
message.success(t('accountImport.importSuccess'))
navigate('/accounts')
} catch (error: any) {
message.error(error.message || '导入账户失败')
message.error(error.message || t('accountImport.importFailed'))
}
}
@@ -153,15 +155,15 @@ const AccountImport: React.FC = () => {
onClick={() => navigate('/accounts')}
style={{ marginBottom: '16px' }}
>
{t('accountImport.back')}
</Button>
<Title level={2} style={{ margin: 0 }}></Title>
<Title level={2} style={{ margin: 0 }}>{t('accountImport.title')}</Title>
</div>
<Card>
<Alert
message="安全提示"
description="私钥将存储在后端数据库中,请确保数据库访问安全。建议使用 HTTPS 连接。"
message={t('accountImport.securityTip')}
description={t('accountImport.securityTipDesc')}
type="warning"
showIcon
style={{ marginBottom: '24px' }}
@@ -173,7 +175,7 @@ const AccountImport: React.FC = () => {
onFinish={handleSubmit}
size={isMobile ? 'middle' : 'large'}
>
<Form.Item label="导入方式">
<Form.Item label={t('accountImport.importMethod')}>
<Radio.Group
value={importType}
onChange={(e) => {
@@ -183,51 +185,51 @@ const AccountImport: React.FC = () => {
form.setFieldsValue({ walletAddress: '' })
}}
>
<Radio value="privateKey"></Radio>
<Radio value="mnemonic"></Radio>
<Radio value="privateKey">{t('accountImport.privateKey')}</Radio>
<Radio value="mnemonic">{t('accountImport.mnemonic')}</Radio>
</Radio.Group>
</Form.Item>
{importType === 'privateKey' ? (
<>
<Form.Item
label="私钥"
label={t('accountImport.privateKeyLabel')}
name="privateKey"
rules={[
{ required: true, message: '请输入私钥' },
{ required: true, message: t('accountImport.privateKeyRequired') },
{
validator: (_, value) => {
if (!value) return Promise.resolve()
if (!isValidPrivateKey(value)) {
return Promise.reject(new Error('私钥格式不正确(应为64位十六进制字符串)'))
return Promise.reject(new Error(t('accountImport.privateKeyInvalid')))
}
return Promise.resolve()
}
}
]}
help={addressError || (derivedAddress ? `推导地址: ${derivedAddress}` : '')}
help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')}
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
>
<Input.TextArea
rows={3}
placeholder="请输入私钥(64位十六进制字符串,可选0x前缀)"
placeholder={t('accountImport.privateKeyPlaceholder')}
onChange={handlePrivateKeyChange}
/>
</Form.Item>
<Form.Item
label="钱包地址"
label={t('accountImport.walletAddress')}
name="walletAddress"
rules={[
{ required: true, message: '请输入钱包地址' },
{ required: true, message: t('accountImport.walletAddressRequired') },
{
validator: (_, value) => {
if (!value) return Promise.resolve()
if (!isValidWalletAddress(value)) {
return Promise.reject(new Error('钱包地址格式不正确'))
return Promise.reject(new Error(t('accountImport.walletAddressInvalid')))
}
if (derivedAddress && value !== derivedAddress) {
return Promise.reject(new Error('钱包地址与私钥不匹配'))
return Promise.reject(new Error(t('accountImport.walletAddressMismatch')))
}
return Promise.resolve()
}
@@ -235,7 +237,7 @@ const AccountImport: React.FC = () => {
]}
>
<Input
placeholder="钱包地址(将从私钥自动推导)"
placeholder={t('accountImport.walletAddressPlaceholder')}
readOnly={!!derivedAddress}
/>
</Form.Item>
@@ -243,43 +245,43 @@ const AccountImport: React.FC = () => {
) : (
<>
<Form.Item
label="助记词"
label={t('accountImport.mnemonicLabel')}
name="mnemonic"
rules={[
{ required: true, message: '请输入助记词' },
{ required: true, message: t('accountImport.mnemonicRequired') },
{
validator: (_, value) => {
if (!value) return Promise.resolve()
if (!isValidMnemonic(value)) {
return Promise.reject(new Error('助记词格式不正确(应为12或24个单词,用空格分隔)'))
return Promise.reject(new Error(t('accountImport.mnemonicInvalid')))
}
return Promise.resolve()
}
}
]}
help={addressError || (derivedAddress ? `推导地址: ${derivedAddress}` : '')}
help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')}
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
>
<Input.TextArea
rows={4}
placeholder="请输入12或24个单词的助记词(用空格分隔)"
placeholder={t('accountImport.mnemonicPlaceholder')}
onChange={handleMnemonicChange}
/>
</Form.Item>
<Form.Item
label="钱包地址"
label={t('accountImport.walletAddress')}
name="walletAddress"
rules={[
{ required: true, message: '请输入钱包地址' },
{ required: true, message: t('accountImport.walletAddressRequired') },
{
validator: (_, value) => {
if (!value) return Promise.resolve()
if (!isValidWalletAddress(value)) {
return Promise.reject(new Error('钱包地址格式不正确'))
return Promise.reject(new Error(t('accountImport.walletAddressInvalid')))
}
if (derivedAddress && value !== derivedAddress) {
return Promise.reject(new Error('钱包地址与助记词不匹配'))
return Promise.reject(new Error(t('accountImport.walletAddressMismatchMnemonic')))
}
return Promise.resolve()
}
@@ -287,7 +289,7 @@ const AccountImport: React.FC = () => {
]}
>
<Input
placeholder="钱包地址(将从助记词自动推导)"
placeholder={t('accountImport.walletAddressPlaceholder')}
readOnly={!!derivedAddress}
/>
</Form.Item>
@@ -295,10 +297,10 @@ const AccountImport: React.FC = () => {
)}
<Form.Item
label="账户名称"
label={t('accountImport.accountName')}
name="accountName"
>
<Input placeholder="可选,用于标识账户" />
<Input placeholder={t('accountImport.accountNamePlaceholder')} />
</Form.Item>
@@ -310,10 +312,10 @@ const AccountImport: React.FC = () => {
loading={loading}
size={isMobile ? 'middle' : 'large'}
>
{t('accountImport.importAccount')}
</Button>
<Button onClick={() => navigate('/accounts')}>
{t('common.cancel')}
</Button>
</Space>
</Form.Item>
+105 -103
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert } from 'antd'
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore'
import type { Account } from '../types'
import { useMediaQuery } from 'react-responsive'
@@ -10,6 +11,7 @@ import { formatUSDC } from '../utils'
const { Title } = Typography
const AccountList: React.FC = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const { accounts, loading, fetchAccounts, deleteAccount, fetchAccountBalance, fetchAccountDetail, updateAccount } = useAccountStore()
@@ -65,17 +67,17 @@ const AccountList: React.FC = () => {
const handleDelete = async (account: Account) => {
try {
await deleteAccount(account.id)
message.success('删除账户成功')
message.success(t('accountList.deleteSuccess'))
} catch (error: any) {
message.error(error.message || '删除账户失败')
message.error(error.message || t('accountList.deleteFailed'))
}
}
const handleCopy = (text: string, label: string) => {
const handleCopy = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
message.success(`${label}已复制到剪贴板`)
message.success(t('accountList.copySuccess'))
}).catch(() => {
message.error('复制失败')
message.error(t('accountList.copyFailed'))
})
}
@@ -109,13 +111,13 @@ const AccountList: React.FC = () => {
}
} catch (error: any) {
console.error('获取账户详情失败:', error)
message.error(error.message || '获取账户详情失败')
message.error(error.message || t('accountList.getDetailFailed'))
setDetailModalVisible(false)
setDetailAccount(null)
}
} catch (error: any) {
console.error('打开详情失败:', error)
message.error('打开详情失败')
message.error(t('accountList.openDetailFailed'))
setDetailModalVisible(false)
setDetailAccount(null)
}
@@ -133,9 +135,9 @@ const AccountList: React.FC = () => {
position: balanceData.positionBalance || '0',
positions: balanceData.positions || []
})
message.success('余额刷新成功')
message.success(t('accountList.refreshBalanceSuccess'))
} catch (error: any) {
message.error(error.message || '刷新余额失败')
message.error(error.message || t('accountList.refreshBalanceFailed'))
} finally {
setDetailBalanceLoading(false)
}
@@ -158,7 +160,7 @@ const AccountList: React.FC = () => {
})
} catch (error: any) {
console.error('打开编辑失败:', error)
message.error(error.message || '获取账户详情失败')
message.error(error.message || t('accountList.getDetailFailedForEdit'))
setEditModalVisible(false)
setEditAccount(null)
}
@@ -188,7 +190,7 @@ const AccountList: React.FC = () => {
await updateAccount(updateData)
message.success('更新账户成功')
message.success(t('accountList.updateSuccess'))
setEditModalVisible(false)
setEditAccount(null)
editForm.resetFields()
@@ -202,7 +204,7 @@ const AccountList: React.FC = () => {
setDetailAccount(accountDetail)
}
} catch (error: any) {
message.error(error.message || '更新账户失败')
message.error(error.message || t('accountList.updateFailed'))
} finally {
setEditLoading(false)
}
@@ -210,13 +212,13 @@ const AccountList: React.FC = () => {
const columns = [
{
title: '账户名称',
title: t('accountList.accountName'),
dataIndex: 'accountName',
key: 'accountName',
render: (text: string, record: Account) => text || `账户 ${record.id}`
render: (text: string, record: Account) => text || `${t('accountList.accountName')} ${record.id}`
},
{
title: '钱包地址',
title: t('accountList.walletAddress'),
dataIndex: 'walletAddress',
key: 'walletAddress',
render: (text: string) => (
@@ -226,14 +228,14 @@ const AccountList: React.FC = () => {
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(text, '钱包地址')}
title="复制钱包地址"
onClick={() => handleCopy(text)}
title={t('accountList.walletAddress')}
/>
</Space>
)
},
{
title: '代理钱包地址',
title: t('accountList.proxyAddress'),
dataIndex: 'proxyAddress',
key: 'proxyAddress',
render: (address: string) => (
@@ -243,27 +245,27 @@ const AccountList: React.FC = () => {
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(address, '代理钱包地址')}
title="复制代理钱包地址"
onClick={() => handleCopy(address)}
title={t('accountList.proxyAddress')}
/>
</Space>
)
},
{
title: 'API 凭证',
title: t('accountList.apiCredentials'),
key: 'apiCredentials',
render: (_: any, record: Account) => {
const allConfigured = record.apiKeyConfigured && record.apiSecretConfigured && record.apiPassphraseConfigured
const partialConfigured = record.apiKeyConfigured || record.apiSecretConfigured || record.apiPassphraseConfigured
return (
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'}>
{allConfigured ? '完整配置' : partialConfigured ? '部分配置' : '未配置'}
{allConfigured ? t('accountList.fullConfig') : partialConfigured ? t('accountList.partialConfig') : t('accountList.notConfigured')}
</Tag>
)
}
},
{
title: '余额',
title: t('accountList.balance'),
dataIndex: 'balance',
key: 'balance',
render: (_: any, record: Account) => {
@@ -276,7 +278,7 @@ const AccountList: React.FC = () => {
}
},
{
title: '活跃订单',
title: t('accountList.activeOrders'),
dataIndex: 'activeOrders',
key: 'activeOrders',
render: (_: any, record: Account) => {
@@ -287,7 +289,7 @@ const AccountList: React.FC = () => {
}
},
{
title: '操作',
title: t('accountList.action'),
key: 'action',
render: (_: any, record: Account) => (
<Space size="small">
@@ -296,7 +298,7 @@ const AccountList: React.FC = () => {
size="small"
onClick={() => handleShowDetail(record)}
>
{t('accountList.detail')}
</Button>
<Button
type="link"
@@ -304,22 +306,22 @@ const AccountList: React.FC = () => {
icon={<EditOutlined />}
onClick={() => handleShowEdit(record)}
>
{t('accountList.edit')}
</Button>
<Popconfirm
title="确定要删除这个账户吗?"
title={t('accountList.deleteConfirm')}
description={
record.apiKeyConfigured
? "删除账户前,请确保已取消所有活跃订单。删除后无法恢复,请谨慎操作!"
: "删除后无法恢复,请谨慎操作!"
? t('accountList.deleteConfirmDesc')
: t('accountList.deleteConfirmDescSimple')
}
onConfirm={() => handleDelete(record)}
okText="确定删除"
cancelText="取消"
okText={t('accountList.deleteConfirmOk')}
cancelText={t('common.cancel')}
okButtonProps={{ danger: true }}
>
<Button type="link" size="small" danger>
{t('accountList.delete')}
</Button>
</Popconfirm>
</Space>
@@ -329,7 +331,7 @@ const AccountList: React.FC = () => {
const mobileColumns = [
{
title: '账户信息',
title: t('accountList.accountName'),
key: 'info',
render: (_: any, record: Account) => {
const allConfigured = record.apiKeyConfigured && record.apiSecretConfigured && record.apiPassphraseConfigured
@@ -342,7 +344,7 @@ const AccountList: React.FC = () => {
marginBottom: '8px',
fontSize: '16px'
}}>
{record.accountName || `账户 ${record.id}`}
{record.accountName || `${t('accountList.accountName')} ${record.id}`}
</div>
<div style={{
fontSize: '11px',
@@ -353,29 +355,29 @@ const AccountList: React.FC = () => {
lineHeight: '1.4'
}}>
<div style={{ marginBottom: '4px' }}>
<strong>:</strong> {record.walletAddress}
<strong>{t('accountList.walletAddress')}:</strong> {record.walletAddress}
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(record.walletAddress, '钱包地址')}
onClick={() => handleCopy(record.walletAddress)}
style={{ marginLeft: '4px', padding: '0 4px' }}
/>
</div>
<div>
<strong>:</strong> {record.proxyAddress}
<strong>{t('accountList.proxyAddress')}:</strong> {record.proxyAddress}
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(record.proxyAddress, '代理钱包地址')}
onClick={() => handleCopy(record.proxyAddress)}
style={{ marginLeft: '4px', padding: '0 4px' }}
/>
</div>
</div>
<div style={{ marginBottom: '8px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'} style={{ margin: 0 }}>
{allConfigured ? '完整配置' : partialConfigured ? '部分配置' : '未配置'}
{allConfigured ? t('accountList.fullConfig') : partialConfigured ? t('accountList.partialConfig') : t('accountList.notConfigured')}
</Tag>
</div>
<div style={{
@@ -383,7 +385,7 @@ const AccountList: React.FC = () => {
fontWeight: '500',
color: '#1890ff'
}}>
: {balanceLoading[record.id] ? (
{t('accountList.totalBalance')}: {balanceLoading[record.id] ? (
<Spin size="small" style={{ marginLeft: '4px' }} />
) : balanceMap[record.id]?.total && balanceMap[record.id].total !== '-' ? (
`${formatUSDC(balanceMap[record.id].total)} USDC`
@@ -397,7 +399,7 @@ const AccountList: React.FC = () => {
color: '#666',
marginTop: '4px'
}}>
: {formatUSDC(balanceMap[record.id].available)} USDC | : {formatUSDC(balanceMap[record.id].position)} USDC
{t('accountList.available')}: {formatUSDC(balanceMap[record.id].available)} USDC | {t('accountList.position')}: {formatUSDC(balanceMap[record.id].position)} USDC
</div>
)}
{(record.activeOrders !== undefined && record.activeOrders !== null) && (
@@ -409,7 +411,7 @@ const AccountList: React.FC = () => {
alignItems: 'center',
gap: '8px'
}}>
: <Tag color={record.activeOrders > 0 ? 'orange' : 'default'} style={{ margin: 0 }}>{record.activeOrders}</Tag>
{t('accountList.activeOrders')}: <Tag color={record.activeOrders > 0 ? 'orange' : 'default'} style={{ margin: 0 }}>{record.activeOrders}</Tag>
</div>
)}
</div>
@@ -417,7 +419,7 @@ const AccountList: React.FC = () => {
}
},
{
title: '操作',
title: t('accountList.action'),
key: 'action',
width: 100,
render: (_: any, record: Account) => (
@@ -429,7 +431,7 @@ const AccountList: React.FC = () => {
onClick={() => handleShowDetail(record)}
style={{ minHeight: '32px' }}
>
{t('accountList.viewDetail')}
</Button>
<Button
size="small"
@@ -438,18 +440,18 @@ const AccountList: React.FC = () => {
onClick={() => handleShowEdit(record)}
style={{ minHeight: '32px' }}
>
{t('accountList.edit')}
</Button>
<Popconfirm
title="确定要删除这个账户吗?"
title={t('accountList.deleteConfirm')}
description={
record.apiKeyConfigured
? "删除账户前,请确保已取消所有活跃订单。删除后无法恢复,请谨慎操作!"
: "删除后无法恢复,请谨慎操作!"
? t('accountList.deleteConfirmDesc')
: t('accountList.deleteConfirmDescSimple')
}
onConfirm={() => handleDelete(record)}
okText="确定删除"
cancelText="取消"
okText={t('accountList.deleteConfirmOk')}
cancelText={t('common.cancel')}
okButtonProps={{ danger: true }}
>
<Button
@@ -458,7 +460,7 @@ const AccountList: React.FC = () => {
danger
style={{ minHeight: '32px' }}
>
{t('accountList.delete')}
</Button>
</Popconfirm>
</Space>
@@ -481,7 +483,7 @@ const AccountList: React.FC = () => {
padding: isMobile ? '0 8px' : '0'
}}>
<Title level={isMobile ? 3 : 2} style={{ margin: 0, fontSize: isMobile ? '18px' : undefined }}>
{t('accountList.title')}
</Title>
<Button
type="primary"
@@ -491,7 +493,7 @@ const AccountList: React.FC = () => {
block={isMobile}
style={isMobile ? { minHeight: '44px' } : undefined}
>
{t('accountList.importAccount')}
</Button>
</div>
@@ -531,7 +533,7 @@ const AccountList: React.FC = () => {
{/* 账户详情 Modal */}
<Modal
title={detailAccount ? (detailAccount.accountName || `账户 ${detailAccount.id}`) : '账户详情'}
title={detailAccount ? (detailAccount.accountName || `${t('accountList.accountName')} ${detailAccount.id}`) : t('accountList.accountDetail')}
open={detailModalVisible}
onCancel={() => {
setDetailModalVisible(false)
@@ -546,7 +548,7 @@ const AccountList: React.FC = () => {
loading={detailBalanceLoading}
disabled={!detailAccount}
>
{t('accountList.refreshBalance')}
</Button>,
<Button
key="edit"
@@ -560,7 +562,7 @@ const AccountList: React.FC = () => {
}}
disabled={!detailAccount}
>
{t('accountList.edit')}
</Button>,
<Button
key="close"
@@ -570,7 +572,7 @@ const AccountList: React.FC = () => {
setDetailBalance(null)
}}
>
{t('common.close')}
</Button>
]}
width={isMobile ? '95%' : 800}
@@ -586,13 +588,13 @@ const AccountList: React.FC = () => {
bordered
size={isMobile ? 'small' : 'middle'}
>
<Descriptions.Item label="账户ID">
<Descriptions.Item label={t('accountList.accountId')}>
{detailAccount.id}
</Descriptions.Item>
<Descriptions.Item label="账户名称">
<Descriptions.Item label={t('accountList.accountName')}>
{detailAccount.accountName || '-'}
</Descriptions.Item>
<Descriptions.Item label="钱包地址" span={isMobile ? 1 : 2}>
<Descriptions.Item label={t('accountList.walletAddress')} span={isMobile ? 1 : 2}>
<Space>
<span style={{
fontFamily: 'monospace',
@@ -607,12 +609,12 @@ const AccountList: React.FC = () => {
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(detailAccount.walletAddress || '', '钱包地址')}
title="复制钱包地址"
onClick={() => handleCopy(detailAccount.walletAddress || '')}
title={t('accountList.walletAddress')}
/>
</Space>
</Descriptions.Item>
<Descriptions.Item label="代理钱包地址" span={isMobile ? 1 : 2}>
<Descriptions.Item label={t('accountList.proxyAddress')} span={isMobile ? 1 : 2}>
<Space>
<span style={{
fontFamily: 'monospace',
@@ -627,12 +629,12 @@ const AccountList: React.FC = () => {
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(detailAccount.proxyAddress || '', '代理钱包地址')}
title="复制代理钱包地址"
onClick={() => handleCopy(detailAccount.proxyAddress || '')}
title={t('accountList.proxyAddress')}
/>
</Space>
</Descriptions.Item>
<Descriptions.Item label="总余额" span={isMobile ? 1 : 2}>
<Descriptions.Item label={t('accountList.totalBalance')} span={isMobile ? 1 : 2}>
{detailBalanceLoading ? (
<Spin size="small" />
) : detailBalance ? (
@@ -643,7 +645,7 @@ const AccountList: React.FC = () => {
<span style={{ color: '#999' }}>-</span>
)}
</Descriptions.Item>
<Descriptions.Item label="可用余额">
<Descriptions.Item label={t('accountList.available')}>
{detailBalanceLoading ? (
<Spin size="small" />
) : detailBalance ? (
@@ -654,7 +656,7 @@ const AccountList: React.FC = () => {
<span style={{ color: '#999' }}>-</span>
)}
</Descriptions.Item>
<Descriptions.Item label="仓位余额">
<Descriptions.Item label={t('accountList.position')}>
{detailBalanceLoading ? (
<Spin size="small" />
) : detailBalance ? (
@@ -673,28 +675,28 @@ const AccountList: React.FC = () => {
column={isMobile ? 1 : 2}
bordered
size={isMobile ? 'small' : 'middle'}
title="API 凭证配置"
title={t('accountList.apiCredentials')}
>
<Descriptions.Item label="API Key">
<Descriptions.Item label={t('accountList.apiKey')}>
<Tag color={detailAccount.apiKeyConfigured ? 'success' : 'default'}>
{detailAccount.apiKeyConfigured ? '已配置' : '未配置'}
{detailAccount.apiKeyConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="API Secret">
<Descriptions.Item label={t('accountList.apiSecret')}>
<Tag color={detailAccount.apiSecretConfigured ? 'success' : 'default'}>
{detailAccount.apiSecretConfigured ? '已配置' : '未配置'}
{detailAccount.apiSecretConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="API Passphrase">
<Descriptions.Item label={t('accountList.apiPassphrase')}>
<Tag color={detailAccount.apiPassphraseConfigured ? 'success' : 'default'}>
{detailAccount.apiPassphraseConfigured ? '已配置' : '未配置'}
{detailAccount.apiPassphraseConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="配置状态">
<Descriptions.Item label={t('accountList.configStatus')}>
{detailAccount.apiKeyConfigured && detailAccount.apiSecretConfigured && detailAccount.apiPassphraseConfigured ? (
<Tag color="success"></Tag>
<Tag color="success">{t('accountList.fullConfig')}</Tag>
) : (
<Tag color="warning"></Tag>
<Tag color="warning">{t('accountList.partialConfig')}</Tag>
)}
</Descriptions.Item>
</Descriptions>
@@ -708,30 +710,30 @@ const AccountList: React.FC = () => {
column={isMobile ? 1 : 2}
bordered
size={isMobile ? 'small' : 'middle'}
title="交易统计"
title={t('accountList.statistics')}
>
{detailAccount.totalOrders !== undefined && (
<Descriptions.Item label="总订单数">
<Descriptions.Item label={t('accountList.totalOrders')}>
{detailAccount.totalOrders}
</Descriptions.Item>
)}
{detailAccount.activeOrders !== undefined && (
<Descriptions.Item label="活跃订单数">
<Descriptions.Item label={t('accountList.activeOrdersCount')}>
<Tag color={detailAccount.activeOrders > 0 ? 'orange' : 'default'}>{detailAccount.activeOrders}</Tag>
</Descriptions.Item>
)}
{detailAccount.completedOrders !== undefined && (
<Descriptions.Item label="已完成订单数">
<Descriptions.Item label={t('accountList.completedOrders')}>
<Tag color="success">{detailAccount.completedOrders}</Tag>
</Descriptions.Item>
)}
{detailAccount.positionCount !== undefined && (
<Descriptions.Item label="持仓数量">
<Descriptions.Item label={t('accountList.positionCount')}>
<Tag color={detailAccount.positionCount > 0 ? 'blue' : 'default'}>{detailAccount.positionCount}</Tag>
</Descriptions.Item>
)}
{detailAccount.totalPnl !== undefined && (
<Descriptions.Item label="总盈亏">
<Descriptions.Item label={t('accountList.totalPnl')}>
<span style={{
fontWeight: 'bold',
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
@@ -747,14 +749,14 @@ const AccountList: React.FC = () => {
) : (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>...</div>
<div style={{ marginTop: '16px' }}>{t('accountList.loading')}</div>
</div>
)}
</Modal>
{/* 编辑账户 Modal */}
<Modal
title={editAccount ? `编辑账户 - ${editAccount.accountName || `账户 ${editAccount.id}`}` : '编辑账户'}
title={editAccount ? `${t('accountList.editAccount')} - ${editAccount.accountName || `${t('accountList.accountName')} ${editAccount.id}`}` : t('accountList.editAccount')}
open={editModalVisible}
onCancel={() => {
setEditModalVisible(false)
@@ -776,42 +778,42 @@ const AccountList: React.FC = () => {
size={isMobile ? 'middle' : 'large'}
>
<Alert
message="编辑提示"
description="API 凭证字段留空表示不修改。如需更新 API 凭证,请输入新值;如需保持原值不变,请留空。"
message={t('accountList.editTip')}
description={t('accountList.editTipDesc')}
type="info"
showIcon
style={{ marginBottom: '24px' }}
/>
<Form.Item
label="账户名称"
label={t('accountList.accountName')}
name="accountName"
>
<Input placeholder="账户名称(可选)" />
<Input placeholder={t('accountList.accountNamePlaceholder')} />
</Form.Item>
<Form.Item
label="API Key"
label={t('accountList.apiKey')}
name="apiKey"
help="留空表示不修改,输入新值将更新 API Key"
help={t('accountList.leaveEmptyToNotModify')}
>
<Input.Password placeholder="留空表示不修改" />
<Input.Password placeholder={t('accountList.leaveEmptyToNotModify')} />
</Form.Item>
<Form.Item
label="API Secret"
label={t('accountList.apiSecret')}
name="apiSecret"
help="留空表示不修改,输入新值将更新 API Secret"
help={t('accountList.leaveEmptyToNotModify')}
>
<Input.Password placeholder="留空表示不修改" />
<Input.Password placeholder={t('accountList.leaveEmptyToNotModify')} />
</Form.Item>
<Form.Item
label="API Passphrase"
label={t('accountList.apiPassphrase')}
name="apiPassphrase"
help="留空表示不修改,输入新值将更新 API Passphrase"
help={t('accountList.leaveEmptyToNotModify')}
>
<Input.Password placeholder="留空表示不修改" />
<Input.Password placeholder={t('accountList.leaveEmptyToNotModify')} />
</Form.Item>
<Form.Item>
@@ -825,7 +827,7 @@ const AccountList: React.FC = () => {
size={isMobile ? 'middle' : 'large'}
style={isMobile ? { minHeight: '44px' } : undefined}
>
{t('common.cancel')}
</Button>
<Button
type="primary"
@@ -834,7 +836,7 @@ const AccountList: React.FC = () => {
size={isMobile ? 'middle' : 'large'}
style={isMobile ? { minHeight: '44px' } : undefined}
>
{t('common.save')}
</Button>
</Space>
</Form.Item>
@@ -842,7 +844,7 @@ const AccountList: React.FC = () => {
) : (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>...</div>
<div style={{ marginTop: '16px' }}>{t('accountList.loading')}</div>
</div>
)}
</Modal>
+174
View File
@@ -0,0 +1,174 @@
import { useEffect, useState } from 'react'
import { Card, Button, Typography, Space, Badge, Spin, Row, Col } from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
const { Title, Text } = Typography
interface ApiHealthStatus {
name: string
url: string
status: string
message: string
responseTime?: number
}
const ApiHealthStatus: React.FC = () => {
const { t } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [apiHealthStatus, setApiHealthStatus] = useState<ApiHealthStatus[]>([])
const [checkingApiHealth, setCheckingApiHealth] = useState(false)
useEffect(() => {
checkApiHealth()
}, [])
const checkApiHealth = async () => {
setCheckingApiHealth(true)
try {
const response = await apiService.proxyConfig.checkApiHealth()
if (response.data.code === 0 && response.data.data) {
setApiHealthStatus(response.data.data.apis)
} else {
// message.error(response.data.msg || 'API 健康检查失败')
}
} catch (error: any) {
// message.error(error.message || 'API 健康检查失败')
} finally {
setCheckingApiHealth(false)
}
}
const getStatusColor = (status: string) => {
if (status === 'success') {
return '#52c41a'
} else if (status === 'skipped') {
return '#999'
} else {
return '#ff4d4f'
}
}
const getStatusText = (status: string) => {
if (status === 'success') {
return t('apiHealthStatus.normal') || '正常'
} else if (status === 'skipped') {
return t('apiHealthStatus.notConfigured') || '未配置'
} else {
return t('apiHealthStatus.abnormal') || '异常'
}
}
return (
<div>
<div style={{ marginBottom: '16px' }}>
<Title level={2} style={{ margin: 0 }}>{t('apiHealthStatus.title') || 'API 健康状态'}</Title>
</div>
<Card
extra={
<Button
icon={<ReloadOutlined />}
onClick={checkApiHealth}
loading={checkingApiHealth}
size="small"
>
{t('common.refresh') || '刷新'}
</Button>
}
>
<Spin spinning={checkingApiHealth}>
<Row gutter={[16, 16]}>
{apiHealthStatus.map((item, index) => (
<Col
key={index}
xs={24}
sm={12}
md={12}
lg={8}
xl={6}
>
{isMobile ? (
<Card
size="small"
style={{
borderLeft: `4px solid ${getStatusColor(item.status)}`,
}}
bodyStyle={{ padding: '12px' }}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px' }}>
<Text strong style={{ fontSize: '14px' }}>
{item.name}
</Text>
<Space>
{item.responseTime !== undefined && item.responseTime !== null && (
<Text type="secondary" style={{ fontSize: '12px' }}>
<Text strong style={{ color: '#1890ff' }}>{item.responseTime}ms</Text>
</Text>
)}
<Badge
status={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'default' : 'error'}
/>
</Space>
</div>
</Card>
) : (
<Card
size="small"
style={{
borderLeft: `4px solid ${getStatusColor(item.status)}`,
height: '100%'
}}
bodyStyle={{ padding: '16px' }}
>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Text strong style={{ fontSize: '14px' }}>
{item.name}
</Text>
<Badge
status={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'default' : 'error'}
text={getStatusText(item.status)}
/>
</div>
<div style={{ marginTop: '8px' }}>
<Text type="secondary" style={{ fontSize: '12px', wordBreak: 'break-all' }}>
{item.url}
</Text>
</div>
{item.message && item.message !== '连接成功' && (
<div style={{ marginTop: '8px' }}>
<Text
type={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'secondary' : 'danger'}
style={{ fontSize: '13px' }}
>
{item.message}
</Text>
</div>
)}
{item.responseTime !== undefined && item.responseTime !== null && (
<div style={{ marginTop: '8px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
{t('apiHealthStatus.responseTime') || '响应时间'}: <Text strong style={{ color: '#1890ff' }}>{item.responseTime}ms</Text>
</Text>
</div>
)}
</Space>
</Card>
)}
</Col>
))}
</Row>
</Spin>
</Card>
</div>
)
}
export default ApiHealthStatus
+10 -7
View File
@@ -1,5 +1,6 @@
import { Card, Typography, Alert } from 'antd'
import { InfoCircleOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
@@ -9,24 +10,26 @@ const { Title } = Typography
* 请使用"跟单模板"和"跟单配置"页面进行配置
*/
const ConfigPage: React.FC = () => {
const { t } = useTranslation()
return (
<div>
<div style={{ marginBottom: '16px' }}>
<Title level={2} style={{ margin: 0 }}></Title>
<Title level={2} style={{ margin: 0 }}>{t('configPage.title') || '全局配置'}</Title>
</div>
<Card>
<Alert
message="配置功能已迁移"
message={t('configPage.message') || '配置功能已迁移'}
description={
<div>
<p></p>
<p>{t('configPage.description') || '全局配置功能已迁移到以下页面:'}</p>
<ul>
<li><strong></strong></li>
<li><strong></strong> Leader </li>
<li><strong></strong> API </li>
<li><strong>{t('configPage.templates') || '跟单模板'}</strong>{t('configPage.templatesDesc') || '管理跟单参数(比例、金额、风险控制等)'}</li>
<li><strong>{t('configPage.copyTrading') || '跟单配置'}</strong>{t('configPage.copyTradingDesc') || '将账户、模板和 Leader 关联,启用跟单关系'}</li>
<li><strong>{t('configPage.systemSettings') || '系统管理'}</strong>{t('configPage.systemSettingsDesc') || '配置代理、查看 API 健康状态'}</li>
</ul>
<p>使</p>
<p>{t('configPage.footer') || '请使用上述页面进行配置管理。'}</p>
</div>
}
type="info"
+40 -38
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Dropdown, Divider, Spin } from 'antd'
import { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import type { MenuProps } from 'antd'
import { apiService } from '../services/api'
import { useAccountStore } from '../store/accountStore'
@@ -12,6 +13,7 @@ import { formatUSDC } from '../utils'
const { Option } = Select
const CopyTradingList: React.FC = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const { accounts, fetchAccounts } = useAccountStore()
@@ -73,10 +75,10 @@ const CopyTradingList: React.FC = () => {
fetchStatistics(ct.id)
})
} else {
message.error(response.data.msg || '获取跟单列表失败')
message.error(response.data.msg || t('copyTradingList.fetchFailed') || '获取跟单列表失败')
}
} catch (error: any) {
message.error(error.message || '获取跟单列表失败')
message.error(error.message || t('copyTradingList.fetchFailed') || '获取跟单列表失败')
} finally {
setLoading(false)
}
@@ -133,13 +135,13 @@ const CopyTradingList: React.FC = () => {
enabled: !copyTrading.enabled
})
if (response.data.code === 0) {
message.success(`${copyTrading.enabled ? '停止' : '开启'}跟单成功`)
message.success(copyTrading.enabled ? (t('copyTradingList.stopSuccess') || '停止跟单成功') : (t('copyTradingList.startSuccess') || '开启跟单成功'))
fetchCopyTradings()
} else {
message.error(response.data.msg || '更新跟单状态失败')
message.error(response.data.msg || t('copyTradingList.updateStatusFailed') || '更新跟单状态失败')
}
} catch (error: any) {
message.error(error.message || '更新跟单状态失败')
message.error(error.message || t('copyTradingList.updateStatusFailed') || '更新跟单状态失败')
}
}
@@ -147,25 +149,25 @@ const CopyTradingList: React.FC = () => {
try {
const response = await apiService.copyTrading.delete({ copyTradingId })
if (response.data.code === 0) {
message.success('删除跟单成功')
message.success(t('copyTradingList.deleteSuccess') || '删除跟单成功')
fetchCopyTradings()
} else {
message.error(response.data.msg || '删除跟单失败')
message.error(response.data.msg || t('copyTradingList.deleteFailed') || '删除跟单失败')
}
} catch (error: any) {
message.error(error.message || '删除跟单失败')
message.error(error.message || t('copyTradingList.deleteFailed') || '删除跟单失败')
}
}
const columns = [
{
title: '钱包',
title: t('copyTradingList.wallet') || '钱包',
key: 'account',
width: isMobile ? 100 : 150,
render: (_: any, record: CopyTrading) => (
<div>
<div style={{ fontSize: isMobile ? 13 : 14, fontWeight: 500 }}>
{record.accountName || `账户 ${record.accountId}`}
{record.accountName || `${t('copyTradingList.account') || '账户'} ${record.accountId}`}
</div>
<div style={{ fontSize: isMobile ? 11 : 12, color: '#999', marginTop: 2 }}>
{isMobile
@@ -177,7 +179,7 @@ const CopyTradingList: React.FC = () => {
)
},
{
title: '模板',
title: t('copyTradingList.template') || '模板',
dataIndex: 'templateName',
key: 'templateName',
width: isMobile ? 100 : 120,
@@ -186,7 +188,7 @@ const CopyTradingList: React.FC = () => {
)
},
{
title: 'Leader',
title: t('copyTradingList.leader') || 'Leader',
key: 'leader',
width: isMobile ? 100 : 150,
render: (_: any, record: CopyTrading) => (
@@ -204,7 +206,7 @@ const CopyTradingList: React.FC = () => {
)
},
{
title: '状态',
title: t('common.status') || '状态',
dataIndex: 'enabled',
key: 'enabled',
width: isMobile ? 80 : 100,
@@ -212,20 +214,20 @@ const CopyTradingList: React.FC = () => {
<Switch
checked={enabled}
onChange={() => handleToggleStatus(record)}
checkedChildren="开启"
unCheckedChildren="停止"
checkedChildren={t('copyTradingList.enabled') || '开启'}
unCheckedChildren={t('copyTradingList.disabled') || '停止'}
/>
)
},
{
title: '总盈亏',
title: t('copyTradingList.totalPnl') || '总盈亏',
key: 'totalPnl',
width: isMobile ? 100 : 150,
render: (_: any, record: CopyTrading) => {
const stats = statisticsMap[record.id]
if (!stats) {
return loadingStatistics.has(record.id) ? (
<span style={{ fontSize: isMobile ? 11 : 12 }}>...</span>
<span style={{ fontSize: isMobile ? 11 : 12 }}>{t('common.loading') || '加载中...'}</span>
) : (
<span style={{ fontSize: isMobile ? 11 : 12 }}>-</span>
)
@@ -257,7 +259,7 @@ const CopyTradingList: React.FC = () => {
}
},
{
title: '操作',
title: t('common.actions') || '操作',
key: 'action',
width: isMobile ? 100 : 200,
fixed: 'right' as const,
@@ -265,25 +267,25 @@ const CopyTradingList: React.FC = () => {
const menuItems: MenuProps['items'] = [
{
key: 'statistics',
label: '查看统计',
label: t('copyTradingList.viewStatistics') || '查看统计',
icon: <BarChartOutlined />,
onClick: () => navigate(`/copy-trading/statistics/${record.id}`)
},
{
key: 'buyOrders',
label: '买入订单',
label: t('copyTradingList.buyOrders') || '买入订单',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`)
},
{
key: 'sellOrders',
label: '卖出订单',
label: t('copyTradingList.sellOrders') || '卖出订单',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`)
},
{
key: 'matchedOrders',
label: '匹配关系',
label: t('copyTradingList.matchedOrders') || '匹配关系',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
},
@@ -294,13 +296,13 @@ const CopyTradingList: React.FC = () => {
key: 'delete',
label: (
<Popconfirm
title="确定要删除这个跟单关系吗?"
title={t('copyTradingList.deleteConfirm') || '确定要删除这个跟单关系吗?'}
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
onCancel={(e) => e?.stopPropagation()}
>
<span style={{ color: '#ff4d4f' }}></span>
<span style={{ color: '#ff4d4f' }}>{t('common.delete') || '删除'}</span>
</Popconfirm>
),
danger: true
@@ -316,7 +318,7 @@ const CopyTradingList: React.FC = () => {
icon={<BarChartOutlined />}
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
>
{t('copyTradingList.statistics') || '统计'}
</Button>
)}
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
@@ -325,15 +327,15 @@ const CopyTradingList: React.FC = () => {
size="small"
icon={<UnorderedListOutlined />}
>
{isMobile ? '' : '订单'}
{isMobile ? '' : (t('copyTradingList.orders') || '订单')}
</Button>
</Dropdown>
{!isMobile && (
<Popconfirm
title="确定要删除这个跟单关系吗?"
title={t('copyTradingList.deleteConfirm') || '确定要删除这个跟单关系吗?'}
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Button
type="link"
@@ -341,7 +343,7 @@ const CopyTradingList: React.FC = () => {
danger
icon={<DeleteOutlined />}
>
{t('common.delete') || '删除'}
</Button>
</Popconfirm>
)}
@@ -355,19 +357,19 @@ const CopyTradingList: React.FC = () => {
<div>
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
<h2 style={{ margin: 0 }}></h2>
<h2 style={{ margin: 0 }}>{t('copyTradingList.title') || '跟单配置管理'}</h2>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate('/copy-trading/add')}
>
{t('copyTradingList.addCopyTrading') || '新增跟单'}
</Button>
</div>
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Select
placeholder="筛选钱包"
placeholder={t('copyTradingList.filterWallet') || '筛选钱包'}
allowClear
style={{ width: isMobile ? '100%' : 200 }}
value={filters.accountId}
@@ -375,13 +377,13 @@ const CopyTradingList: React.FC = () => {
>
{accounts.map(account => (
<Option key={account.id} value={account.id}>
{account.accountName || `账户 ${account.id}`}
{account.accountName || `${t('copyTradingList.account') || '账户'} ${account.id}`}
</Option>
))}
</Select>
<Select
placeholder="筛选模板"
placeholder={t('copyTradingList.filterTemplate') || '筛选模板'}
allowClear
style={{ width: isMobile ? '100%' : 200 }}
value={filters.templateId}
@@ -395,7 +397,7 @@ const CopyTradingList: React.FC = () => {
</Select>
<Select
placeholder="筛选 Leader"
placeholder={t('copyTradingList.filterLeader') || '筛选 Leader'}
allowClear
style={{ width: isMobile ? '100%' : 200 }}
value={filters.leaderId}
+127
View File
@@ -0,0 +1,127 @@
import { useState, useEffect } from 'react'
import { Card, Select, Space, Typography, message } from 'antd'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
const { Title } = Typography
const LanguageSettings: React.FC = () => {
const { t, i18n: i18nInstance } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
// 检测系统语言
const detectSystemLanguage = (): string => {
const systemLanguage = navigator.language || navigator.languages?.[0] || 'en'
const lang = systemLanguage.toLowerCase()
if (lang.startsWith('zh')) {
if (lang.includes('tw') || lang.includes('hk') || lang.includes('mo')) {
return 'zh-TW'
}
return 'zh-CN'
}
return 'en'
}
// 初始化当前语言设置
const getInitialLanguage = (): string => {
const savedLanguage = localStorage.getItem('i18n_language')
return savedLanguage || 'auto'
}
const [currentLang, setCurrentLang] = useState<string>(getInitialLanguage())
const languages = [
{ value: 'auto', label: t('languageSettings.followSystem') || '跟随系统' },
{ value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁體中文' },
{ value: 'en', label: 'English' }
]
// 获取当前显示的语言(如果是 auto,显示系统语言)
const getDisplayLanguage = (): string => {
if (currentLang === 'auto') {
return detectSystemLanguage()
}
return currentLang
}
const handleChange = async (value: string) => {
try {
let actualLang = value
if (value === 'auto') {
actualLang = detectSystemLanguage()
// 保存 auto 到 localStorage,但使用系统语言
localStorage.setItem('i18n_language', 'auto')
} else {
localStorage.setItem('i18n_language', value)
}
setCurrentLang(value)
await i18nInstance.changeLanguage(actualLang)
message.success(t('languageSettings.changeSuccess') || '语言切换成功')
// 不需要刷新页面,i18n 和 Ant Design 的 locale 会自动更新
} catch (error) {
message.error(t('languageSettings.changeFailed') || '语言切换失败')
}
}
// 初始化时,如果当前设置是 auto,确保使用系统语言
useEffect(() => {
const savedLanguage = localStorage.getItem('i18n_language')
if (!savedLanguage || savedLanguage === 'auto') {
const systemLang = detectSystemLanguage()
if (i18nInstance.language !== systemLang) {
i18nInstance.changeLanguage(systemLang)
}
} else {
// 如果保存的是具体语言,确保使用该语言
if (i18nInstance.language !== savedLanguage) {
i18nInstance.changeLanguage(savedLanguage)
}
}
}, [])
return (
<div>
<div style={{ marginBottom: '16px' }}>
<Title level={2} style={{ margin: 0 }}>{t('languageSettings.title') || '语言设置'}</Title>
</div>
<Card>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
{t('languageSettings.currentLanguage') || '当前语言'}
</Typography.Text>
<Select
value={currentLang}
onChange={handleChange}
options={languages}
style={{ width: isMobile ? '100%' : 200 }}
size={isMobile ? 'middle' : 'large'}
/>
{currentLang === 'auto' && (
<div style={{ marginTop: '8px' }}>
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
{t('languageSettings.currentSystemLanguage') || '当前系统语言'}: {
getDisplayLanguage() === 'zh-CN' ? '简体中文' :
getDisplayLanguage() === 'zh-TW' ? '繁體中文' : 'English'
}
</Typography.Text>
</div>
)}
</div>
<div>
<Typography.Text type="secondary">
{t('languageSettings.description') || '切换语言后,界面将立即更新为新语言。'}
</Typography.Text>
</div>
</Space>
</Card>
</div>
)
}
export default LanguageSettings
+34 -32
View File
@@ -2,11 +2,13 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, message, List, Empty, Spin, Divider } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import type { Leader } from '../types'
import { useMediaQuery } from 'react-responsive'
const LeaderList: React.FC = () => {
const { t, i18n } = useTranslation()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [leaders, setLeaders] = useState<Leader[]>([])
@@ -23,10 +25,10 @@ const LeaderList: React.FC = () => {
if (response.data.code === 0 && response.data.data) {
setLeaders(response.data.data.list || [])
} else {
message.error(response.data.msg || '获取 Leader 列表失败')
message.error(response.data.msg || t('leaderList.fetchFailed') || '获取 Leader 列表失败')
}
} catch (error: any) {
message.error(error.message || '获取 Leader 列表失败')
message.error(error.message || t('leaderList.fetchFailed') || '获取 Leader 列表失败')
} finally {
setLoading(false)
}
@@ -36,25 +38,25 @@ const LeaderList: React.FC = () => {
try {
const response = await apiService.leaders.delete({ leaderId })
if (response.data.code === 0) {
message.success('删除 Leader 成功')
message.success(t('leaderList.deleteSuccess') || '删除 Leader 成功')
fetchLeaders()
} else {
message.error(response.data.msg || '删除 Leader 失败')
message.error(response.data.msg || t('leaderList.deleteFailed') || '删除 Leader 失败')
}
} catch (error: any) {
message.error(error.message || '删除 Leader 失败')
message.error(error.message || t('leaderList.deleteFailed') || '删除 Leader 失败')
}
}
const columns = [
{
title: 'Leader 名称',
title: t('leaderList.leaderName') || 'Leader 名称',
dataIndex: 'leaderName',
key: 'leaderName',
render: (text: string, record: Leader) => text || `Leader ${record.id}`
},
{
title: '钱包地址',
title: t('leaderList.walletAddress') || '钱包地址',
dataIndex: 'leaderAddress',
key: 'leaderAddress',
render: (address: string) => (
@@ -64,26 +66,26 @@ const LeaderList: React.FC = () => {
)
},
{
title: '分类',
title: t('leaderList.category') || '分类',
dataIndex: 'category',
key: 'category',
render: (category: string | undefined) => category ? (
<Tag color={category === 'sports' ? 'blue' : 'green'}>{category}</Tag>
) : <Tag></Tag>
) : <Tag>{t('leaderList.all') || '全部'}</Tag>
},
{
title: '跟单关系数',
title: t('leaderList.copyTradingCount') || '跟单关系数',
dataIndex: 'copyTradingCount',
key: 'copyTradingCount',
render: (count: number) => <Tag>{count}</Tag>
},
{
title: '创建时间',
title: t('leaderList.createdAt') || '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (timestamp: number) => {
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
return date.toLocaleString(i18n.language || 'zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -93,7 +95,7 @@ const LeaderList: React.FC = () => {
}
},
{
title: '操作',
title: t('common.actions') || '操作',
key: 'action',
width: isMobile ? 120 : 150,
render: (_: any, record: Leader) => (
@@ -104,17 +106,17 @@ const LeaderList: React.FC = () => {
icon={<EditOutlined />}
onClick={() => navigate(`/leaders/edit?id=${record.id}`)}
>
{t('common.edit') || '编辑'}
</Button>
<Popconfirm
title="确定要删除这个 Leader 吗?"
description={record.copyTradingCount > 0 ? `该 Leader 还有 ${record.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
title={t('leaderList.deleteConfirm') || '确定要删除这个 Leader 吗?'}
description={record.copyTradingCount > 0 ? t('leaderList.deleteConfirmDesc', { count: record.copyTradingCount }) || `该 Leader 还有 ${record.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
{t('common.delete') || '删除'}
</Button>
</Popconfirm>
</Space>
@@ -132,14 +134,14 @@ const LeaderList: React.FC = () => {
flexWrap: 'wrap',
gap: '12px'
}}>
<h2 style={{ margin: 0 }}>Leader </h2>
<h2 style={{ margin: 0 }}>{t('leaderList.title') || 'Leader 管理'}</h2>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate('/leaders/add')}
size={isMobile ? 'middle' : 'large'}
>
Leader
{t('leaderList.addLeader') || '添加 Leader'}
</Button>
</div>
@@ -152,13 +154,13 @@ const LeaderList: React.FC = () => {
<Spin size="large" />
</div>
) : leaders.length === 0 ? (
<Empty description="暂无 Leader 数据" />
<Empty description={t('leaderList.noData') || '暂无 Leader 数据'} />
) : (
<List
dataSource={leaders}
renderItem={(leader) => {
const date = new Date(leader.createdAt)
const formattedDate = date.toLocaleString('zh-CN', {
const formattedDate = date.toLocaleString(i18n.language || 'zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -207,15 +209,15 @@ const LeaderList: React.FC = () => {
{leader.category}
</Tag>
) : (
<Tag></Tag>
<Tag>{t('leaderList.all') || '全部'}</Tag>
)}
<Tag>{leader.copyTradingCount} </Tag>
<Tag>{t('leaderList.copyTradingRelations', { count: leader.copyTradingCount }) || `${leader.copyTradingCount} 个跟单关系`}</Tag>
</div>
</div>
{/* 创建时间 */}
<div style={{ marginBottom: '12px', fontSize: '12px', color: '#999' }}>
: {formattedDate}
{t('leaderList.createdAt') || '创建时间'}: {formattedDate}
</div>
{/* 操作按钮 */}
@@ -227,14 +229,14 @@ const LeaderList: React.FC = () => {
onClick={() => navigate(`/leaders/edit?id=${leader.id}`)}
style={{ flex: 1 }}
>
{t('common.edit') || '编辑'}
</Button>
<Popconfirm
title="确定要删除这个 Leader 吗?"
description={leader.copyTradingCount > 0 ? `该 Leader 还有 ${leader.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
title={t('leaderList.deleteConfirm') || '确定要删除这个 Leader 吗?'}
description={leader.copyTradingCount > 0 ? t('leaderList.deleteConfirmDesc', { count: leader.copyTradingCount }) || `该 Leader 还有 ${leader.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
onConfirm={() => handleDelete(leader.id)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Button
type="link"
@@ -243,7 +245,7 @@ const LeaderList: React.FC = () => {
icon={<DeleteOutlined />}
style={{ flex: 1 }}
>
{t('common.delete') || '删除'}
</Button>
</Popconfirm>
</div>
+14 -11
View File
@@ -2,6 +2,7 @@ import { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Card, Form, Input, Button, message, Typography } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import { setToken } from '../utils'
import { useMediaQuery } from 'react-responsive'
@@ -9,6 +10,7 @@ import { useMediaQuery } from 'react-responsive'
const { Title } = Typography
const Login: React.FC = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [loading, setLoading] = useState(false)
@@ -21,15 +23,15 @@ const Login: React.FC = () => {
if (response.data.code === 0 && response.data.data) {
const token = response.data.data.token
setToken(token)
message.success('登录成功')
message.success(t('message.loginSuccess'))
// 跳转到首页
navigate('/')
} else {
message.error(response.data.msg || '登录失败')
message.error(response.data.msg || t('message.loginFailed'))
}
} catch (error: any) {
console.error('登录失败:', error)
const errorMsg = error.response?.data?.msg || error.message || '登录失败'
const errorMsg = error.response?.data?.msg || error.message || t('message.loginFailed')
message.error(errorMsg)
} finally {
setLoading(false)
@@ -52,7 +54,7 @@ const Login: React.FC = () => {
}}
>
<Title level={2} style={{ textAlign: 'center', marginBottom: '32px' }}>
{t('login.title')}
</Title>
<Form
form={form}
@@ -62,25 +64,27 @@ const Login: React.FC = () => {
>
<Form.Item
name="username"
label={t('login.username')}
rules={[
{ required: true, message: '请输入用户名' }
{ required: true, message: t('login.usernameRequired') }
]}
>
<Input
prefix={<UserOutlined />}
placeholder="用户名"
placeholder={t('login.usernamePlaceholder')}
autoComplete="username"
/>
</Form.Item>
<Form.Item
name="password"
label={t('login.password')}
rules={[
{ required: true, message: '请输入密码' }
{ required: true, message: t('login.passwordRequired') }
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码"
placeholder={t('login.passwordPlaceholder')}
autoComplete="current-password"
/>
</Form.Item>
@@ -92,12 +96,12 @@ const Login: React.FC = () => {
loading={loading}
size={isMobile ? 'large' : 'middle'}
>
{t('login.title')}
</Button>
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Link to="/reset-password" style={{ fontSize: isMobile ? '14px' : '13px' }}>
{t('login.forgotPassword')}
</Link>
</Form.Item>
</Form>
@@ -107,4 +111,3 @@ const Login: React.FC = () => {
}
export default Login
+15 -13
View File
@@ -1,11 +1,13 @@
import { useEffect, useState } from 'react'
import { Card, Table, Tag, message } from 'antd'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import type { CopyOrder } from '../types'
import { useMediaQuery } from 'react-responsive'
import { formatUSDC } from '../utils'
const OrderList: React.FC = () => {
const { t, i18n } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [orders, setOrders] = useState<CopyOrder[]>([])
const [loading, setLoading] = useState(false)
@@ -33,10 +35,10 @@ const OrderList: React.FC = () => {
total: response.data.data?.total || 0
}))
} else {
message.error(response.data.msg || '获取订单列表失败')
message.error(response.data.msg || t('orderList.fetchFailed') || '获取订单列表失败')
}
} catch (error: any) {
message.error(error.message || '获取订单列表失败')
message.error(error.message || t('orderList.fetchFailed') || '获取订单列表失败')
} finally {
setLoading(false)
}
@@ -61,13 +63,13 @@ const OrderList: React.FC = () => {
const columns = [
{
title: 'Leader',
title: t('orderList.leader') || 'Leader',
dataIndex: 'leaderName',
key: 'leaderName',
render: (text: string, record: CopyOrder) => text || record.leaderAddress.slice(0, 10) + '...'
},
{
title: '市场',
title: t('orderList.market') || '市场',
dataIndex: 'marketId',
key: 'marketId',
render: (marketId: string) => (
@@ -77,7 +79,7 @@ const OrderList: React.FC = () => {
)
},
{
title: '分类',
title: t('orderList.category') || '分类',
dataIndex: 'category',
key: 'category',
render: (category: string) => (
@@ -85,7 +87,7 @@ const OrderList: React.FC = () => {
)
},
{
title: '方向',
title: t('orderList.side') || '方向',
dataIndex: 'side',
key: 'side',
render: (side: string) => (
@@ -93,17 +95,17 @@ const OrderList: React.FC = () => {
)
},
{
title: '价格',
title: t('orderList.price') || '价格',
dataIndex: 'price',
key: 'price'
},
{
title: '数量',
title: t('orderList.size') || '数量',
dataIndex: 'size',
key: 'size'
},
{
title: '状态',
title: t('orderList.status') || '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
@@ -111,7 +113,7 @@ const OrderList: React.FC = () => {
)
},
{
title: '盈亏',
title: t('orderList.pnl') || '盈亏',
dataIndex: 'pnl',
key: 'pnl',
render: (pnl: string | undefined) => pnl ? (
@@ -121,17 +123,17 @@ const OrderList: React.FC = () => {
) : '-'
},
{
title: '创建时间',
title: t('orderList.createdAt') || '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (timestamp: number) => new Date(timestamp).toLocaleString()
render: (timestamp: number) => new Date(timestamp).toLocaleString(i18n.language || 'zh-CN')
}
]
return (
<div>
<div style={{ marginBottom: '16px' }}>
<h2></h2>
<h2>{t('orderList.title') || '订单管理'}</h2>
</div>
<Card>
+239
View File
@@ -0,0 +1,239 @@
import { useEffect, useState } from 'react'
import { Card, Form, Button, Switch, Input, InputNumber, message, Typography, Space, Alert } from 'antd'
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
const { Title, Text } = Typography
interface ProxyConfig {
id?: number
type: string
enabled: boolean
host?: string
port?: number
username?: string
subscriptionUrl?: string
lastSubscriptionUpdate?: number
createdAt: number
updatedAt: number
}
interface ProxyCheckResponse {
success: boolean
message: string
responseTime?: number
latency?: number
}
const ProxySettings: React.FC = () => {
const { t } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [checking, setChecking] = useState(false)
const [checkResult, setCheckResult] = useState<ProxyCheckResponse | null>(null)
const [currentConfig, setCurrentConfig] = useState<ProxyConfig | null>(null)
useEffect(() => {
fetchConfig()
}, [])
const fetchConfig = async () => {
try {
const response = await apiService.proxyConfig.get()
if (response.data.code === 0) {
const data = response.data.data
setCurrentConfig(data)
if (data) {
form.setFieldsValue({
enabled: data.enabled,
host: data.host || '',
port: data.port || undefined,
username: data.username || '',
password: '', // 密码不预填充
})
} else {
form.resetFields()
}
} else {
message.error(response.data.msg || t('proxySettings.getFailed') || '获取代理配置失败')
}
} catch (error: any) {
message.error(error.message || t('proxySettings.getFailed') || '获取代理配置失败')
}
}
const handleSubmit = async (values: any) => {
setLoading(true)
try {
const requestData: any = {
enabled: values.enabled || false,
host: values.host,
port: values.port,
username: values.username || undefined,
}
// 只有在输入了新密码时才包含密码字段
if (values.password && values.password.trim()) {
requestData.password = values.password
}
const response = await apiService.proxyConfig.saveHttp(requestData)
if (response.data.code === 0) {
message.success(t('proxySettings.saveSuccess') || '保存配置成功')
setCheckResult(null)
fetchConfig()
} else {
message.error(response.data.msg || t('proxySettings.saveFailed') || '保存配置失败')
}
} catch (error: any) {
message.error(error.message || t('proxySettings.saveFailed') || '保存配置失败')
} finally {
setLoading(false)
}
}
const handleCheck = async () => {
setChecking(true)
setCheckResult(null)
try {
const response = await apiService.proxyConfig.check()
if (response.data.code === 0 && response.data.data) {
setCheckResult(response.data.data)
} else {
setCheckResult({
success: false,
message: response.data.msg || t('proxySettings.checkFailed') || '代理检查失败'
})
}
} catch (error: any) {
setCheckResult({
success: false,
message: error.message || t('proxySettings.checkFailed') || '代理检查失败'
})
} finally {
setChecking(false)
}
}
return (
<div>
<div style={{ marginBottom: '16px' }}>
<Title level={2} style={{ margin: 0 }}>{t('proxySettings.title') || '代理设置'}</Title>
</div>
<Card>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
size={isMobile ? 'middle' : 'large'}
>
<Form.Item
label={t('proxySettings.enabled') || '启用代理'}
name="enabled"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={t('proxySettings.host') || '代理主机'}
name="host"
rules={[
{ required: true, message: t('proxySettings.hostRequired') || '请输入代理主机地址' },
{ pattern: /^[\w\.-]+$/, message: t('proxySettings.hostInvalid') || '请输入有效的主机地址' }
]}
>
<Input placeholder={t('proxySettings.hostPlaceholder') || '例如:127.0.0.1 或 proxy.example.com'} />
</Form.Item>
<Form.Item
label={t('proxySettings.port') || '代理端口'}
name="port"
rules={[
{ required: true, message: t('proxySettings.portRequired') || '请输入代理端口' },
{ type: 'number', min: 1, max: 65535, message: t('proxySettings.portInvalid') || '端口必须在 1-65535 之间' }
]}
>
<InputNumber
min={1}
max={65535}
style={{ width: '100%' }}
placeholder={t('proxySettings.portPlaceholder') || '例如:8888'}
/>
</Form.Item>
<Form.Item
label={t('proxySettings.username') || '代理用户名(可选)'}
name="username"
>
<Input placeholder={t('proxySettings.usernamePlaceholder') || '如果代理需要认证,请输入用户名'} />
</Form.Item>
<Form.Item
label={t('proxySettings.password') || '代理密码(可选)'}
name="password"
help={currentConfig ? (t('proxySettings.passwordHelpUpdate') || '留空则不更新密码,输入新密码则更新') : (t('proxySettings.passwordHelp') || '如果代理需要认证,请输入密码')}
>
<Input.Password placeholder={currentConfig ? (t('proxySettings.passwordPlaceholderUpdate') || '留空则不更新密码') : (t('proxySettings.passwordPlaceholder') || '如果代理需要认证,请输入密码')} />
</Form.Item>
<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
icon={<SaveOutlined />}
loading={loading}
>
{t('common.save') || '保存配置'}
</Button>
<Button
icon={<CheckCircleOutlined />}
onClick={handleCheck}
loading={checking}
>
{t('proxySettings.check') || '检查代理'}
</Button>
{checkResult && (
<Button
icon={<ReloadOutlined />}
onClick={fetchConfig}
>
{t('common.refresh') || '刷新配置'}
</Button>
)}
</Space>
</Form.Item>
</Form>
{checkResult && (
<Alert
type={checkResult.success ? 'success' : 'error'}
message={checkResult.success ? (t('proxySettings.checkSuccess') || '代理检查成功') : (t('proxySettings.checkFailed') || '代理检查失败')}
description={
<div>
<Text>{checkResult.message}</Text>
{(checkResult.responseTime !== undefined || checkResult.latency !== undefined) && (
<div style={{ marginTop: '8px' }}>
<Text type="secondary">
{t('proxySettings.latency') || '延迟'}: {(checkResult.latency ?? checkResult.responseTime) ?? 0}ms
</Text>
</div>
)}
</div>
}
style={{ marginTop: '16px' }}
showIcon
/>
)}
</Card>
</div>
)
}
export default ProxySettings
+45 -43
View File
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Card, Form, Input, Button, message, Typography, Alert, Progress } from 'antd'
import { LockOutlined, KeyOutlined, UserOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import { useMediaQuery } from 'react-responsive'
@@ -30,32 +31,33 @@ const getPasswordStrength = (password: string): number => {
return Math.min(4, Math.floor(strength))
}
/**
*
*/
const getPasswordStrengthInfo = (strength: number): { text: string; color: string; percent: number } => {
switch (strength) {
case 0:
return { text: '弱', color: '#ff4d4f', percent: 25 }
case 1:
return { text: '较弱', color: '#ff7a45', percent: 50 }
case 2:
return { text: '中等', color: '#faad14', percent: 75 }
case 3:
return { text: '强', color: '#52c41a', percent: 100 }
case 4:
return { text: '很强', color: '#52c41a', percent: 100 }
default:
return { text: '弱', color: '#ff4d4f', percent: 0 }
}
}
const ResetPassword: React.FC = () => {
const { t } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [loading, setLoading] = useState(false)
const [passwordStrength, setPasswordStrength] = useState(0)
const [form] = Form.useForm()
/**
*
*/
const getPasswordStrengthInfo = (strength: number): { text: string; color: string; percent: number } => {
switch (strength) {
case 0:
return { text: t('resetPassword.weak') || '弱', color: '#ff4d4f', percent: 25 }
case 1:
return { text: t('resetPassword.fair') || '较弱', color: '#ff7a45', percent: 50 }
case 2:
return { text: t('resetPassword.medium') || '中等', color: '#faad14', percent: 75 }
case 3:
return { text: t('resetPassword.strong') || '强', color: '#52c41a', percent: 100 }
case 4:
return { text: t('resetPassword.veryStrong') || '很强', color: '#52c41a', percent: 100 }
default:
return { text: t('resetPassword.weak') || '弱', color: '#ff4d4f', percent: 0 }
}
}
const handleReset = async (values: {
resetKey: string
username: string
@@ -63,7 +65,7 @@ const ResetPassword: React.FC = () => {
confirmPassword: string
}) => {
if (values.newPassword !== values.confirmPassword) {
message.error('两次输入的密码不一致')
message.error(t('resetPassword.passwordMismatch') || '两次输入的密码不一致')
return
}
@@ -75,17 +77,17 @@ const ResetPassword: React.FC = () => {
newPassword: values.newPassword
})
if (response.data.code === 0) {
message.success('密码重置成功', 1)
message.success(t('resetPassword.success') || '密码重置成功', 1)
// 使用 window.location.href 强制跳转到登录页,确保跳转成功
setTimeout(() => {
window.location.href = '/login'
}, 500)
} else {
message.error(response.data.msg || '密码重置失败')
message.error(response.data.msg || t('resetPassword.failed') || '密码重置失败')
}
} catch (error: any) {
console.error('密码重置失败:', error)
const errorMsg = error.response?.data?.msg || error.message || '密码重置失败'
const errorMsg = error.response?.data?.msg || error.message || t('resetPassword.failed') || '密码重置失败'
message.error(errorMsg)
} finally {
setLoading(false)
@@ -108,11 +110,11 @@ const ResetPassword: React.FC = () => {
}}
>
<Title level={2} style={{ textAlign: 'center', marginBottom: '16px' }}>
{t('resetPassword.title') || '重置密码'}
</Title>
<Alert
message="首次使用系统"
description="请使用管理员提供的重置密钥设置初始密码"
message={t('resetPassword.firstUse') || '首次使用系统'}
description={t('resetPassword.firstUseDesc') || '请使用管理员提供的重置密钥设置初始密码'}
type="info"
showIcon
style={{ marginBottom: '24px' }}
@@ -125,39 +127,39 @@ const ResetPassword: React.FC = () => {
>
<Form.Item
name="resetKey"
label="重置密钥"
label={t('resetPassword.resetKey') || '重置密钥'}
rules={[
{ required: true, message: '请输入重置密钥' }
{ required: true, message: t('resetPassword.resetKeyRequired') || '请输入重置密钥' }
]}
>
<Input
prefix={<KeyOutlined />}
placeholder="请输入重置密钥"
placeholder={t('resetPassword.resetKeyPlaceholder') || '请输入重置密钥'}
/>
</Form.Item>
<Form.Item
name="username"
label="用户名"
label={t('resetPassword.username') || '用户名'}
rules={[
{ required: true, message: '请输入用户名' }
{ required: true, message: t('resetPassword.usernameRequired') || '请输入用户名' }
]}
>
<Input
prefix={<UserOutlined />}
placeholder="请输入用户名"
placeholder={t('resetPassword.usernamePlaceholder') || '请输入用户名'}
/>
</Form.Item>
<Form.Item
name="newPassword"
label="新密码"
label={t('resetPassword.newPassword') || '新密码'}
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6位' }
{ required: true, message: t('resetPassword.newPasswordRequired') || '请输入新密码' },
{ min: 6, message: t('resetPassword.passwordMinLength') || '密码至少6位' }
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="至少6位"
placeholder={t('resetPassword.passwordPlaceholder') || '至少6位'}
onChange={(e) => {
const strength = getPasswordStrength(e.target.value)
setPasswordStrength(strength)
@@ -168,7 +170,7 @@ const ResetPassword: React.FC = () => {
<Form.Item>
<div style={{ marginTop: '-16px', marginBottom: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
<span style={{ fontSize: '12px', color: '#666' }}></span>
<span style={{ fontSize: '12px', color: '#666' }}>{t('resetPassword.passwordStrength') || '密码强度'}</span>
<span style={{
fontSize: '12px',
fontWeight: 'bold',
@@ -188,23 +190,23 @@ const ResetPassword: React.FC = () => {
)}
<Form.Item
name="confirmPassword"
label="确认密码"
label={t('resetPassword.confirmPassword') || '确认密码'}
dependencies={['newPassword']}
rules={[
{ required: true, message: '请确认密码' },
{ required: true, message: t('resetPassword.confirmPasswordRequired') || '请确认密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve()
}
return Promise.reject(new Error('两次输入的密码不一致'))
return Promise.reject(new Error(t('resetPassword.passwordMismatch') || '两次输入的密码不一致'))
}
})
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请再次输入密码"
placeholder={t('resetPassword.confirmPasswordPlaceholder') || '请再次输入密码'}
/>
</Form.Item>
<Form.Item>
@@ -215,7 +217,7 @@ const ResetPassword: React.FC = () => {
loading={loading}
size={isMobile ? 'large' : 'middle'}
>
{t('resetPassword.submit') || '重置密码'}
</Button>
</Form.Item>
</Form>
+14 -12
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Card, Row, Col, Statistic, message, DatePicker, Space, Button, Typography } from 'antd'
import { ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import type { Dayjs } from 'dayjs'
import { apiService } from '../services/api'
import type { Statistics as StatisticsType } from '../types'
@@ -11,6 +12,7 @@ const { RangePicker } = DatePicker
const { Title } = Typography
const Statistics: React.FC = () => {
const { t } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [stats, setStats] = useState<StatisticsType | null>(null)
const [loading, setLoading] = useState(false)
@@ -30,10 +32,10 @@ const Statistics: React.FC = () => {
if (response.data.code === 0 && response.data.data) {
setStats(response.data.data)
} else {
message.error(response.data.msg || '获取统计信息失败')
message.error(response.data.msg || t('statistics.fetchFailed') || '获取统计信息失败')
}
} catch (error: any) {
message.error(error.message || '获取统计信息失败')
message.error(error.message || t('statistics.fetchFailed') || '获取统计信息失败')
} finally {
setLoading(false)
}
@@ -54,13 +56,13 @@ const Statistics: React.FC = () => {
return (
<div>
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px' }}>
<Title level={2} style={{ margin: 0 }}></Title>
<Title level={2} style={{ margin: 0 }}>{t('statistics.title') || '统计信息'}</Title>
<Space size="middle" wrap>
<RangePicker
value={dateRange}
onChange={handleDateRangeChange}
format="YYYY-MM-DD"
placeholder={['开始日期', '结束日期']}
placeholder={[t('statistics.startDate') || '开始日期', t('statistics.endDate') || '结束日期']}
size={isMobile ? 'middle' : 'large'}
allowClear
/>
@@ -71,14 +73,14 @@ const Statistics: React.FC = () => {
loading={loading}
size={isMobile ? 'middle' : 'large'}
>
{t('statistics.refresh') || '刷新'}
</Button>
{(dateRange[0] || dateRange[1]) && (
<Button
onClick={handleReset}
size={isMobile ? 'middle' : 'large'}
>
{t('statistics.reset') || '重置'}
</Button>
)}
</Space>
@@ -88,7 +90,7 @@ const Statistics: React.FC = () => {
<Col xs={24} sm={12} md={8}>
<Card>
<Statistic
title="总订单数"
title={t('statistics.totalOrders') || '总订单数'}
value={stats?.totalOrders || 0}
loading={loading}
/>
@@ -97,7 +99,7 @@ const Statistics: React.FC = () => {
<Col xs={24} sm={12} md={8}>
<Card>
<Statistic
title="总盈亏"
title={t('statistics.totalPnl') || '总盈亏'}
value={formatUSDC(stats?.totalPnl || '0')}
prefix={stats?.totalPnl && parseFloat(stats.totalPnl) >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: stats?.totalPnl && parseFloat(stats.totalPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
@@ -109,7 +111,7 @@ const Statistics: React.FC = () => {
<Col xs={24} sm={12} md={8}>
<Card>
<Statistic
title="胜率"
title={t('statistics.winRate') || '胜率'}
value={stats?.winRate || '0'}
precision={2}
suffix="%"
@@ -120,7 +122,7 @@ const Statistics: React.FC = () => {
<Col xs={24} sm={12} md={8}>
<Card>
<Statistic
title="平均盈亏"
title={t('statistics.avgPnl') || '平均盈亏'}
value={formatUSDC(stats?.avgPnl || '0')}
prefix={stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
@@ -132,7 +134,7 @@ const Statistics: React.FC = () => {
<Col xs={24} sm={12} md={8}>
<Card>
<Statistic
title="最大盈利"
title={t('statistics.maxProfit') || '最大盈利'}
value={formatUSDC(stats?.maxProfit || '0')}
prefix={<ArrowUpOutlined />}
valueStyle={{ color: '#3f8600' }}
@@ -144,7 +146,7 @@ const Statistics: React.FC = () => {
<Col xs={24} sm={12} md={8}>
<Card>
<Statistic
title="最大亏损"
title={t('statistics.maxLoss') || '最大亏损'}
value={formatUSDC(stats?.maxLoss || '0')}
prefix={<ArrowDownOutlined />}
valueStyle={{ color: '#cf1322' }}
+59 -57
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, message, Input, Modal, Form, Radio, InputNumber, Switch, Divider, Spin } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import type { CopyTradingTemplate } from '../types'
import { useMediaQuery } from 'react-responsive'
@@ -10,6 +11,7 @@ import { formatUSDC } from '../utils'
const { Search } = Input
const TemplateList: React.FC = () => {
const { t, i18n } = useTranslation()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [templates, setTemplates] = useState<CopyTradingTemplate[]>([])
@@ -32,10 +34,10 @@ const TemplateList: React.FC = () => {
if (response.data.code === 0 && response.data.data) {
setTemplates(response.data.data.list || [])
} else {
message.error(response.data.msg || '获取模板列表失败')
message.error(response.data.msg || t('templateList.fetchFailed') || '获取模板列表失败')
}
} catch (error: any) {
message.error(error.message || '获取模板列表失败')
message.error(error.message || t('templateList.fetchFailed') || '获取模板列表失败')
} finally {
setLoading(false)
}
@@ -45,13 +47,13 @@ const TemplateList: React.FC = () => {
try {
const response = await apiService.templates.delete({ templateId })
if (response.data.code === 0) {
message.success('删除模板成功')
message.success(t('templateList.deleteSuccess') || '删除模板成功')
fetchTemplates()
} else {
message.error(response.data.msg || '删除模板失败')
message.error(response.data.msg || t('templateList.deleteFailed') || '删除模板失败')
}
} catch (error: any) {
message.error(error.message || '删除模板失败')
message.error(error.message || t('templateList.deleteFailed') || '删除模板失败')
}
}
@@ -61,7 +63,7 @@ const TemplateList: React.FC = () => {
// 填充表单数据
copyForm.setFieldsValue({
templateName: `${template.templateName}-副本`,
templateName: `${template.templateName}-${t('templateList.copySuffix') || '副本'}`,
copyMode: template.copyMode,
copyRatio: template.copyRatio ? parseFloat(template.copyRatio) * 100 : 100,
fixedAmount: template.fixedAmount ? parseFloat(template.fixedAmount) : undefined,
@@ -78,7 +80,7 @@ const TemplateList: React.FC = () => {
const handleCopySubmit = async (values: any) => {
// 前端校验:如果填写了 minOrderSize,必须 >= 1
if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && values.minOrderSize !== '' && Number(values.minOrderSize) < 1) {
message.error('最小金额必须 >= 1')
message.error(t('templateList.minAmountError') || '最小金额必须 >= 1')
return
}
@@ -86,16 +88,16 @@ const TemplateList: React.FC = () => {
if (values.copyMode === 'FIXED') {
const fixedAmount = values.fixedAmount
if (fixedAmount === undefined || fixedAmount === null || fixedAmount === '') {
message.error('请输入固定跟单金额')
message.error(t('templateList.fixedAmountRequired') || '请输入固定跟单金额')
return
}
const amount = Number(fixedAmount)
if (isNaN(amount)) {
message.error('请输入有效的数字')
message.error(t('templateList.invalidNumber') || '请输入有效的数字')
return
}
if (amount < 1) {
message.error('固定金额必须 >= 1,请重新输入')
message.error(t('templateList.fixedAmountError') || '固定金额必须 >= 1,请重新输入')
return
}
}
@@ -116,15 +118,15 @@ const TemplateList: React.FC = () => {
})
if (response.data.code === 0) {
message.success('复制模板成功')
message.success(t('templateList.copySuccess') || '复制模板成功')
setCopyModalVisible(false)
copyForm.resetFields()
fetchTemplates()
} else {
message.error(response.data.msg || '复制模板失败')
message.error(response.data.msg || t('templateList.copyFailed') || '复制模板失败')
}
} catch (error: any) {
message.error(error.message || '复制模板失败')
message.error(error.message || t('templateList.copyFailed') || '复制模板失败')
} finally {
setCopyLoading(false)
}
@@ -142,56 +144,56 @@ const TemplateList: React.FC = () => {
const columns = [
{
title: '模板名称',
title: t('templateList.templateName') || '模板名称',
dataIndex: 'templateName',
key: 'templateName',
render: (text: string) => <strong>{text}</strong>
},
{
title: '跟单模式',
title: t('templateList.copyMode') || '跟单模式',
dataIndex: 'copyMode',
key: 'copyMode',
render: (mode: string) => (
<Tag color={mode === 'RATIO' ? 'blue' : 'green'}>
{mode === 'RATIO' ? '比例' : '固定金额'}
{mode === 'RATIO' ? t('templateList.ratio') || '比例' : t('templateList.fixedAmount') || '固定金额'}
</Tag>
)
},
{
title: '跟单配置',
title: t('templateList.copyConfig') || '跟单配置',
key: 'copyConfig',
render: (_: any, record: CopyTradingTemplate) => {
if (record.copyMode === 'RATIO') {
return `比例 ${record.copyRatio}x`
return `${t('templateList.ratio') || '比例'} ${record.copyRatio}x`
} else if (record.copyMode === 'FIXED' && record.fixedAmount) {
return `固定 ${formatUSDC(record.fixedAmount)} USDC`
return `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(record.fixedAmount)} USDC`
}
return '-'
}
},
{
title: '跟单卖出',
title: t('templateList.supportSell') || '跟单卖出',
dataIndex: 'supportSell',
key: 'supportSell',
render: (support: boolean) => (
<Tag color={support ? 'green' : 'red'}>
{support ? '是' : '否'}
{support ? t('common.yes') || '是' : t('common.no') || '否'}
</Tag>
)
},
{
title: '使用次数',
title: t('templateList.useCount') || '使用次数',
dataIndex: 'useCount',
key: 'useCount',
render: (count: number) => <Tag>{count}</Tag>
},
{
title: '创建时间',
title: t('common.createdAt') || '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (timestamp: number) => {
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
return date.toLocaleString(i18n.language || 'zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -204,7 +206,7 @@ const TemplateList: React.FC = () => {
defaultSortOrder: 'descend' as const
},
{
title: '操作',
title: t('common.actions') || '操作',
key: 'action',
width: isMobile ? 120 : 200,
render: (_: any, record: CopyTradingTemplate) => (
@@ -215,7 +217,7 @@ const TemplateList: React.FC = () => {
icon={<EditOutlined />}
onClick={() => navigate(`/templates/edit/${record.id}`)}
>
{t('common.edit') || '编辑'}
</Button>
<Button
type="link"
@@ -223,14 +225,14 @@ const TemplateList: React.FC = () => {
icon={<CopyOutlined />}
onClick={() => handleCopy(record)}
>
{t('templateList.copy') || '复制'}
</Button>
<Popconfirm
title="确定要删除这个模板吗?"
description="删除后无法恢复,请确保没有跟单关系在使用该模板"
title={t('templateList.deleteConfirm') || '确定要删除这个模板吗?'}
description={t('templateList.deleteConfirmDesc') || '删除后无法恢复,请确保没有跟单关系在使用该模板'}
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Button
type="link"
@@ -238,7 +240,7 @@ const TemplateList: React.FC = () => {
danger
icon={<DeleteOutlined />}
>
{t('common.delete') || '删除'}
</Button>
</Popconfirm>
</Space>
@@ -250,10 +252,10 @@ const TemplateList: React.FC = () => {
<div>
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
<h2 style={{ margin: 0 }}></h2>
<h2 style={{ margin: 0 }}>{t('templateList.title') || '跟单模板管理'}</h2>
<Space>
<Search
placeholder="搜索模板名称"
placeholder={t('templateList.searchPlaceholder') || '搜索模板名称'}
allowClear
style={{ width: isMobile ? 150 : 250 }}
onSearch={setSearchText}
@@ -264,7 +266,7 @@ const TemplateList: React.FC = () => {
icon={<PlusOutlined />}
onClick={() => navigate('/templates/add')}
>
{t('templateList.addTemplate') || '新增模板'}
</Button>
</Space>
</div>
@@ -278,13 +280,13 @@ const TemplateList: React.FC = () => {
</div>
) : filteredTemplates.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
{t('templateList.noData') || '暂无模板数据'}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{filteredTemplates.map((template) => {
const date = new Date(template.createdAt)
const formattedDate = date.toLocaleString('zh-CN', {
const formattedDate = date.toLocaleString(i18n.language || 'zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -314,12 +316,12 @@ const TemplateList: React.FC = () => {
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<Tag color={template.copyMode === 'RATIO' ? 'blue' : 'green'}>
{template.copyMode === 'RATIO' ? '比例模式' : '固定金额模式'}
{template.copyMode === 'RATIO' ? (t('templateList.ratioMode') || '比例模式') : (t('templateList.fixedAmountMode') || '固定金额模式')}
</Tag>
<Tag color={template.supportSell ? 'green' : 'red'}>
{template.supportSell ? '跟单卖出' : '不跟单卖出'}
{template.supportSell ? (t('templateList.supportSell') || '跟单卖出') : (t('templateList.notSupportSell') || '不跟单卖出')}
</Tag>
<Tag>{template.useCount} 使</Tag>
<Tag>{template.useCount} {t('templateList.timesUsed') || '次使用'}</Tag>
</div>
</div>
@@ -327,12 +329,12 @@ const TemplateList: React.FC = () => {
{/* 跟单配置 */}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}></div>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.copyConfig') || '跟单配置'}</div>
<div style={{ fontSize: '14px', fontWeight: '500' }}>
{template.copyMode === 'RATIO'
? `比例 ${template.copyRatio}x`
? `${t('templateList.ratio') || '比例'} ${template.copyRatio}x`
: template.fixedAmount
? `固定 ${formatUSDC(template.fixedAmount)} USDC`
? `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(template.fixedAmount)} USDC`
: '-'
}
</div>
@@ -341,31 +343,31 @@ const TemplateList: React.FC = () => {
{/* 其他配置信息 */}
{template.copyMode === 'RATIO' && (
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}></div>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.amountLimit') || '金额限制'}</div>
<div style={{ fontSize: '13px', color: '#333' }}>
{template.maxOrderSize && (
<span>: {formatUSDC(template.maxOrderSize)} USDC</span>
<span>{t('templateList.max') || '最大'}: {formatUSDC(template.maxOrderSize)} USDC</span>
)}
{template.maxOrderSize && template.minOrderSize && <span> | </span>}
{template.minOrderSize && (
<span>: {formatUSDC(template.minOrderSize)} USDC</span>
<span>{t('templateList.min') || '最小'}: {formatUSDC(template.minOrderSize)} USDC</span>
)}
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#999' }}></span>}
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#999' }}>{t('templateList.notSet') || '未设置'}</span>}
</div>
</div>
)}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}></div>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.otherConfig') || '其他配置'}</div>
<div style={{ fontSize: '13px', color: '#333' }}>
: {template.maxDailyOrders} | : {template.priceTolerance}%
{t('templateList.maxDailyOrders') || '每日最大订单'}: {template.maxDailyOrders} | {t('templateList.priceTolerance') || '价格容忍度'}: {template.priceTolerance}%
</div>
</div>
{/* 创建时间 */}
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#999' }}>
: {formattedDate}
{t('common.createdAt') || '创建时间'}: {formattedDate}
</div>
</div>
@@ -378,7 +380,7 @@ const TemplateList: React.FC = () => {
onClick={() => navigate(`/templates/edit/${template.id}`)}
style={{ flex: 1, minWidth: '80px' }}
>
{t('common.edit') || '编辑'}
</Button>
<Button
size="small"
@@ -386,14 +388,14 @@ const TemplateList: React.FC = () => {
onClick={() => handleCopy(template)}
style={{ flex: 1, minWidth: '80px' }}
>
{t('templateList.copy') || '复制'}
</Button>
<Popconfirm
title="确定要删除这个模板吗?"
description="删除后无法恢复,请确保没有跟单关系在使用该模板"
title={t('templateList.deleteConfirm') || '确定要删除这个模板吗?'}
description={t('templateList.deleteConfirmDesc') || '删除后无法恢复,请确保没有跟单关系在使用该模板'}
onConfirm={() => handleDelete(template.id)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Button
danger
@@ -401,7 +403,7 @@ const TemplateList: React.FC = () => {
icon={<DeleteOutlined />}
style={{ flex: 1, minWidth: '80px' }}
>
{t('common.delete') || '删除'}
</Button>
</Popconfirm>
</div>
+56 -54
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Modal, Form, Input } from 'antd'
import { PlusOutlined, ReloadOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import { useMediaQuery } from 'react-responsive'
@@ -15,6 +16,7 @@ interface User {
}
const UserList: React.FC = () => {
const { t, i18n } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(false)
@@ -37,11 +39,11 @@ const UserList: React.FC = () => {
if (response.data.code === 0 && response.data.data) {
setUsers(response.data.data)
} else {
message.error(response.data.msg || '获取用户列表失败')
message.error(response.data.msg || t('userList.fetchFailed') || '获取用户列表失败')
}
} catch (error: any) {
console.error('获取用户列表失败:', error)
const errorMsg = error.response?.data?.msg || error.message || '获取用户列表失败'
const errorMsg = error.response?.data?.msg || error.message || t('userList.fetchFailed') || '获取用户列表失败'
message.error(errorMsg)
} finally {
setLoading(false)
@@ -59,16 +61,16 @@ const UserList: React.FC = () => {
password: values.password
})
if (response.data.code === 0) {
message.success('创建用户成功')
message.success(t('userList.createSuccess') || '创建用户成功')
setCreateModalVisible(false)
createForm.resetFields()
fetchUsers()
} else {
message.error(response.data.msg || '创建用户失败')
message.error(response.data.msg || t('userList.createFailed') || '创建用户失败')
}
} catch (error: any) {
console.error('创建用户失败:', error)
const errorMsg = error.response?.data?.msg || error.message || '创建用户失败'
const errorMsg = error.response?.data?.msg || error.message || t('userList.createFailed') || '创建用户失败'
message.error(errorMsg)
}
}
@@ -82,17 +84,17 @@ const UserList: React.FC = () => {
newPassword: values.newPassword
})
if (response.data.code === 0) {
message.success('更新密码成功')
message.success(t('userList.updatePasswordSuccess') || '更新密码成功')
setUpdatePasswordModalVisible(false)
setSelectedUser(null)
updatePasswordForm.resetFields()
fetchUsers()
} else {
message.error(response.data.msg || '更新密码失败')
message.error(response.data.msg || t('userList.updatePasswordFailed') || '更新密码失败')
}
} catch (error: any) {
console.error('更新密码失败:', error)
const errorMsg = error.response?.data?.msg || error.message || '更新密码失败'
const errorMsg = error.response?.data?.msg || error.message || t('userList.updatePasswordFailed') || '更新密码失败'
message.error(errorMsg)
}
}
@@ -103,7 +105,7 @@ const UserList: React.FC = () => {
newPassword: values.newPassword
})
if (response.data.code === 0) {
message.success('修改密码成功,请重新登录')
message.success(t('userList.updateOwnPasswordSuccess') || '修改密码成功,请重新登录')
setUpdateOwnPasswordModalVisible(false)
updateOwnPasswordForm.resetFields()
// 延迟跳转到登录页
@@ -111,11 +113,11 @@ const UserList: React.FC = () => {
window.location.href = '/login'
}, 1000)
} else {
message.error(response.data.msg || '修改密码失败')
message.error(response.data.msg || t('userList.updateOwnPasswordFailed') || '修改密码失败')
}
} catch (error: any) {
console.error('修改密码失败:', error)
const errorMsg = error.response?.data?.msg || error.message || '修改密码失败'
const errorMsg = error.response?.data?.msg || error.message || t('userList.updateOwnPasswordFailed') || '修改密码失败'
message.error(errorMsg)
}
}
@@ -124,14 +126,14 @@ const UserList: React.FC = () => {
try {
const response = await apiService.users.delete({ userId: user.id })
if (response.data.code === 0) {
message.success('删除用户成功')
message.success(t('userList.deleteSuccess') || '删除用户成功')
fetchUsers()
} else {
message.error(response.data.msg || '删除用户失败')
message.error(response.data.msg || t('userList.deleteFailed') || '删除用户失败')
}
} catch (error: any) {
console.error('删除用户失败:', error)
const errorMsg = error.response?.data?.msg || error.message || '删除用户失败'
const errorMsg = error.response?.data?.msg || error.message || t('userList.deleteFailed') || '删除用户失败'
message.error(errorMsg)
}
}
@@ -144,30 +146,30 @@ const UserList: React.FC = () => {
width: 80
},
{
title: '用户名',
title: t('userList.username') || '用户名',
dataIndex: 'username',
key: 'username'
},
{
title: '角色',
title: t('userList.role') || '角色',
dataIndex: 'isDefault',
key: 'isDefault',
width: 100,
render: (isDefault: boolean) => (
<Tag color={isDefault ? 'red' : 'blue'}>
{isDefault ? '默认账户' : '普通用户'}
{isDefault ? t('userList.defaultAccount') || '默认账户' : t('userList.normalUser') || '普通用户'}
</Tag>
)
},
{
title: '创建时间',
title: t('common.createdAt') || '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (timestamp: number) => new Date(timestamp).toLocaleString('zh-CN')
render: (timestamp: number) => new Date(timestamp).toLocaleString(i18n.language || 'zh-CN')
},
{
title: '操作',
title: t('common.actions') || '操作',
key: 'action',
width: 200,
render: (_: any, record: User) => {
@@ -186,13 +188,13 @@ const UserList: React.FC = () => {
setUpdatePasswordModalVisible(true)
}}
>
{t('userList.updatePassword') || '修改密码'}
</Button>
<Popconfirm
title="确定要删除这个用户吗?"
title={t('userList.deleteConfirm') || '确定要删除这个用户吗?'}
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Button
type="link"
@@ -200,7 +202,7 @@ const UserList: React.FC = () => {
size="small"
icon={<DeleteOutlined />}
>
{t('common.delete') || '删除'}
</Button>
</Popconfirm>
</>
@@ -219,20 +221,20 @@ const UserList: React.FC = () => {
<div>
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Title level={4} style={{ margin: 0 }}>{t('userList.title') || '用户管理'}</Title>
<Space>
<Button
icon={<EditOutlined />}
onClick={() => setUpdateOwnPasswordModalVisible(true)}
>
{t('userList.updateMyPassword') || '修改我的密码'}
</Button>
<Button
icon={<ReloadOutlined />}
onClick={fetchUsers}
loading={loading}
>
{t('common.refresh') || '刷新'}
</Button>
{isDefaultUser && (
<Button
@@ -240,7 +242,7 @@ const UserList: React.FC = () => {
icon={<PlusOutlined />}
onClick={() => setCreateModalVisible(true)}
>
{t('userList.addUser') || '新增用户'}
</Button>
)}
</Space>
@@ -253,7 +255,7 @@ const UserList: React.FC = () => {
pagination={{
pageSize: isMobile ? 10 : 20,
showSizeChanger: !isMobile,
showTotal: (total) => `${total}`
showTotal: (total) => t('userList.total', { total }) || `${total}`
}}
scroll={isMobile ? { x: 600 } : undefined}
/>
@@ -261,15 +263,15 @@ const UserList: React.FC = () => {
{/* 创建用户弹窗 */}
<Modal
title="新增用户"
title={t('userList.addUser') || '新增用户'}
open={createModalVisible}
onCancel={() => {
setCreateModalVisible(false)
createForm.resetFields()
}}
onOk={() => createForm.submit()}
okText="创建"
cancelText="取消"
okText={t('userList.createUser') || '创建'}
cancelText={t('common.cancel') || '取消'}
>
<Form
form={createForm}
@@ -278,29 +280,29 @@ const UserList: React.FC = () => {
>
<Form.Item
name="username"
label="用户名"
label={t('userList.username') || '用户名'}
rules={[
{ required: true, message: '请输入用户名' }
{ required: true, message: t('userList.usernameRequired') || '请输入用户名' }
]}
>
<Input placeholder="请输入用户名" />
<Input placeholder={t('userList.usernamePlaceholder') || '请输入用户名'} />
</Form.Item>
<Form.Item
name="password"
label="密码"
label={t('userList.password') || '密码'}
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6位' }
{ required: true, message: t('userList.passwordRequired') || '请输入密码' },
{ min: 6, message: t('userList.passwordMinLength') || '密码至少6位' }
]}
>
<Input.Password placeholder="至少6位" />
<Input.Password placeholder={t('userList.passwordPlaceholder') || '至少6位'} />
</Form.Item>
</Form>
</Modal>
{/* 修改密码弹窗(管理员修改其他用户密码) */}
<Modal
title="修改密码"
title={t('userList.updatePassword') || '修改密码'}
open={updatePasswordModalVisible}
onCancel={() => {
setUpdatePasswordModalVisible(false)
@@ -308,8 +310,8 @@ const UserList: React.FC = () => {
updatePasswordForm.resetFields()
}}
onOk={() => updatePasswordForm.submit()}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Form
form={updatePasswordForm}
@@ -318,28 +320,28 @@ const UserList: React.FC = () => {
>
<Form.Item
name="newPassword"
label="新密码"
label={t('userList.newPassword') || '新密码'}
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6位' }
{ required: true, message: t('userList.newPasswordRequired') || '请输入新密码' },
{ min: 6, message: t('userList.passwordMinLength') || '密码至少6位' }
]}
>
<Input.Password placeholder="至少6位" />
<Input.Password placeholder={t('userList.passwordPlaceholder') || '至少6位'} />
</Form.Item>
</Form>
</Modal>
{/* 修改我的密码弹窗(默认账户修改自己密码) */}
<Modal
title="修改我的密码"
title={t('userList.updateMyPasswordTitle') || '修改我的密码'}
open={updateOwnPasswordModalVisible}
onCancel={() => {
setUpdateOwnPasswordModalVisible(false)
updateOwnPasswordForm.resetFields()
}}
onOk={() => updateOwnPasswordForm.submit()}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Form
form={updateOwnPasswordForm}
@@ -348,13 +350,13 @@ const UserList: React.FC = () => {
>
<Form.Item
name="newPassword"
label="新密码"
label={t('userList.newPassword') || '新密码'}
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6位' }
{ required: true, message: t('userList.newPasswordRequired') || '请输入新密码' },
{ min: 6, message: t('userList.passwordMinLength') || '密码至少6位' }
]}
>
<Input.Password placeholder="至少6位" />
<Input.Password placeholder={t('userList.passwordPlaceholder') || '至少6位'} />
</Form.Item>
</Form>
</Modal>
+5 -1
View File
@@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosError } from 'axios'
import type { ApiResponse } from '../types'
import { getToken, setToken, removeToken } from '../utils'
import { wsManager } from './websocket'
import i18n from '../i18n/config'
/**
* API
@@ -36,6 +37,9 @@ apiClient.interceptors.request.use(
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加语言 Header
const language = i18n.language || 'en'
config.headers['X-Language'] = language
return config
},
(error) => {
@@ -473,5 +477,5 @@ export const apiService = {
}
}
export default apiClient
export default apiService