feat: 完成前端多语言支持和菜单优化
- 缩短菜单标题,解决显示不全问题 - 添加跟随系统语言选项,作为默认选项 - 移除语言切换时的页面刷新,实现无刷新切换 - 完成主要页面的多语言替换: * ConfigPage - 全局配置页面 * ResetPassword - 重置密码页面 * LeaderList - Leader 列表页面 * UserList - 用户列表页面 * Statistics - 统计信息页面 * OrderList - 订单列表页面 * TemplateList - 模板列表页面 * CopyTradingList - 跟单配置列表页面 - 添加简体中文、繁体中文和英文的完整翻译键 - 优化语言设置页面,支持跟随系统语言
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-22
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-24
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-25
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+16
-10
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
**模式 2:ErrorCode + 自定义消息**
|
||||
```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)
|
||||
```
|
||||
|
||||
**模式 2:ErrorCode + 自定义消息**
|
||||
```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. **测试**:更新后测试不同语言下的错误消息显示
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
# 多语言支持实现状态
|
||||
|
||||
## ✅ 已完成的工作
|
||||
|
||||
### 后端部分(100% 完成)
|
||||
|
||||
#### 1. 核心框架 ✅
|
||||
- ✅ 创建语言资源文件(messages_zh_CN.properties, messages_zh_TW.properties, messages_en.properties)
|
||||
- ✅ 配置 MessageSource Bean(MessageSourceConfig.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 中添加语言 Header(X-Language)
|
||||
- ✅ 在 main.tsx 中初始化 i18n
|
||||
|
||||
#### 2. 页面更新 ✅(2个页面已完成)
|
||||
- ✅ AccountDetail.tsx - 完全使用 i18n
|
||||
- ✅ App.tsx - 订单推送通知已使用 i18n,Ant 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 Bean(MessageSourceConfig.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 中添加语言 Header(X-Language)
|
||||
- ✅ 在 main.tsx 中初始化 i18n
|
||||
|
||||
#### 2. 页面更新 ✅(2个页面已完成)
|
||||
- ✅ AccountDetail.tsx - 完全使用 i18n
|
||||
- ✅ App.tsx - 订单推送通知已使用 i18n,Ant 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. (可选)添加语言切换组件
|
||||
|
||||
@@ -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 的使用
|
||||
|
||||
@@ -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. **语言切换动画**(可选):
|
||||
- 添加平滑的切换动画,提升用户体验
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 模式2:ErrorCode + 消息
|
||||
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)
|
||||
|
||||
// 模式2:ErrorCode + 消息
|
||||
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
|
||||
- [ ] 所有硬编码文本都已替换
|
||||
- [ ] 语言包包含所有需要的翻译键
|
||||
- [ ] 测试不同语言下的页面显示
|
||||
- [ ] 测试语言切换功能
|
||||
|
||||
Generated
+75
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
}
|
||||
@@ -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": "确定要删除这个跟单关系吗?"
|
||||
}
|
||||
}
|
||||
@@ -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": "確定要刪除這個跟單關係嗎?"
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user