refactor(web): restructure AITradersPage into modular architecture (#1023)

* refactor(web): restructure AITradersPage into modular architecture
Refactored the massive 2652-line AITradersPage.tsx into a clean, modular architecture following React best practices.
**Changes:**
- Decomposed 2652-line component into 12 focused modules
- Introduced Zustand stores for config and modal state management
- Extracted all business logic into useTraderActions custom hook (633 lines)
- Created reusable section components (PageHeader, TradersGrid, etc.)
- Separated complex modal logic into dedicated components
- Added TraderConfig type, eliminated all any types
- Fixed critical bugs in configuredExchanges logic and getState() usage
**File Structure:**
- Main page reduced from 2652 → 234 lines (91% reduction)
- components/traders/: 7 UI components + 5 section components
- stores/: tradersConfigStore, tradersModalStore
- hooks/: useTraderActions (all business logic)
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* chore: ignore PR_DESCRIPTION.md
* fix(web): restore trader dashboard navigation functionality
Fixed missing navigation logic in refactored AITradersPage. The "查看" (View) button now correctly navigates to the trader dashboard.
**Root Cause:**
During refactoring, the `useNavigate` hook and default navigation logic were inadvertently omitted from the main page component.
**Changes:**
- Added `useNavigate` import from react-router-dom
- Implemented `handleTraderSelect` function with fallback navigation
- Restored original behavior: use `onTraderSelect` prop if provided, otherwise navigate to `/dashboard?trader=${traderId}`
**Testing:**
-  Click "查看" button navigates to trader dashboard
-  Query parameter correctly passed to dashboard
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* fix(web): correct type definitions for trader configuration
Fixed TypeScript build errors by using the correct `TraderConfigData` type instead of the incorrect `TraderConfig` type.
**Root Cause:**
During refactoring, a new `TraderConfig` type was incorrectly created that extended `CreateTraderRequest` (with fields like `name`, `ai_model_id`). However, the `TraderConfigModal` component and API responses actually use `TraderConfigData` (with fields like `trader_name`, `ai_model`).
**Changes:**
- Replaced all `TraderConfig` references with `TraderConfigData`:
  - stores/tradersModalStore.ts
  - hooks/useTraderActions.ts
  - lib/api.ts
- Removed incorrect `TraderConfig` type definition from types.ts
- Added null check for `editingTrader.trader_id` to satisfy TypeScript
**Build Status:**
-  TypeScript compilation: PASS
-  Vite production build: PASS
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
---------
Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
Ember
2025-11-15 12:33:48 +08:00
committed by tangmengqiu
parent 7eab056394
commit c64d4ff549
19 changed files with 3050 additions and 21 deletions

2
web/src/stores/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { useTradersConfigStore } from './tradersConfigStore'
export { useTradersModalStore } from './tradersModalStore'

View File

@@ -0,0 +1,128 @@
import { create } from 'zustand'
import type { AIModel, Exchange } from '../types'
import { api } from '../lib/api'
interface SignalSource {
coinPoolUrl: string
oiTopUrl: string
}
interface TradersConfigState {
// 数据
allModels: AIModel[]
allExchanges: Exchange[]
supportedModels: AIModel[]
supportedExchanges: Exchange[]
userSignalSource: SignalSource
// 计算属性
configuredModels: AIModel[]
configuredExchanges: Exchange[]
// Actions
setAllModels: (models: AIModel[]) => void
setAllExchanges: (exchanges: Exchange[]) => void
setSupportedModels: (models: AIModel[]) => void
setSupportedExchanges: (exchanges: Exchange[]) => void
setUserSignalSource: (source: SignalSource) => void
// 异步加载
loadConfigs: (user: any, token: string | null) => Promise<void>
// 重置
reset: () => void
}
const initialState = {
allModels: [],
allExchanges: [],
supportedModels: [],
supportedExchanges: [],
userSignalSource: { coinPoolUrl: '', oiTopUrl: '' },
configuredModels: [],
configuredExchanges: [],
}
export const useTradersConfigStore = create<TradersConfigState>((set, get) => ({
...initialState,
setAllModels: (models) => {
set({ allModels: models })
// 更新 configuredModels
const configuredModels = models.filter((m) => {
return m.enabled || (m.customApiUrl && m.customApiUrl.trim() !== '')
})
set({ configuredModels })
},
setAllExchanges: (exchanges) => {
set({ allExchanges: exchanges })
// 更新 configuredExchanges
const configuredExchanges = exchanges.filter((e) => {
if (e.id === 'aster') {
return e.asterUser && e.asterUser.trim() !== ''
}
if (e.id === 'hyperliquid') {
return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== ''
}
// 修复: 添加 enabled 判断,与原始逻辑保持一致
return e.enabled || (e.apiKey && e.apiKey.trim() !== '')
})
set({ configuredExchanges })
},
setSupportedModels: (models) => set({ supportedModels: models }),
setSupportedExchanges: (exchanges) => set({ supportedExchanges: exchanges }),
setUserSignalSource: (source) => set({ userSignalSource: source }),
loadConfigs: async (user, token) => {
if (!user || !token) {
// 未登录时只加载公开的支持模型和交易所
try {
const [supportedModels, supportedExchanges] = await Promise.all([
api.getSupportedModels(),
api.getSupportedExchanges(),
])
get().setSupportedModels(supportedModels)
get().setSupportedExchanges(supportedExchanges)
} catch (err) {
console.error('Failed to load supported configs:', err)
}
return
}
try {
const [
modelConfigs,
exchangeConfigs,
supportedModels,
supportedExchanges,
] = await Promise.all([
api.getModelConfigs(),
api.getExchangeConfigs(),
api.getSupportedModels(),
api.getSupportedExchanges(),
])
get().setAllModels(modelConfigs)
get().setAllExchanges(exchangeConfigs)
get().setSupportedModels(supportedModels)
get().setSupportedExchanges(supportedExchanges)
// 加载用户信号源配置
try {
const signalSource = await api.getUserSignalSource()
get().setUserSignalSource({
coinPoolUrl: signalSource.coin_pool_url || '',
oiTopUrl: signalSource.oi_top_url || '',
})
} catch (error) {
console.log('📡 用户信号源配置暂未设置')
}
} catch (error) {
console.error('Failed to load configs:', error)
}
},
reset: () => set(initialState),
}))

View File

@@ -0,0 +1,79 @@
import { create } from 'zustand'
import type { TraderConfigData } from '../types'
interface TradersModalState {
// Modal 显示状态
showCreateModal: boolean
showEditModal: boolean
showModelModal: boolean
showExchangeModal: boolean
showSignalSourceModal: boolean
// 编辑状态
editingModel: string | null
editingExchange: string | null
editingTrader: TraderConfigData | null
// Actions
setShowCreateModal: (show: boolean) => void
setShowEditModal: (show: boolean) => void
setShowModelModal: (show: boolean) => void
setShowExchangeModal: (show: boolean) => void
setShowSignalSourceModal: (show: boolean) => void
setEditingModel: (modelId: string | null) => void
setEditingExchange: (exchangeId: string | null) => void
setEditingTrader: (trader: TraderConfigData | null) => void
// 便捷方法
openModelModal: (modelId?: string) => void
closeModelModal: () => void
openExchangeModal: (exchangeId?: string) => void
closeExchangeModal: () => void
// 重置
reset: () => void
}
const initialState = {
showCreateModal: false,
showEditModal: false,
showModelModal: false,
showExchangeModal: false,
showSignalSourceModal: false,
editingModel: null,
editingExchange: null,
editingTrader: null,
}
export const useTradersModalStore = create<TradersModalState>((set) => ({
...initialState,
setShowCreateModal: (show) => set({ showCreateModal: show }),
setShowEditModal: (show) => set({ showEditModal: show }),
setShowModelModal: (show) => set({ showModelModal: show }),
setShowExchangeModal: (show) => set({ showExchangeModal: show }),
setShowSignalSourceModal: (show) => set({ showSignalSourceModal: show }),
setEditingModel: (modelId) => set({ editingModel: modelId }),
setEditingExchange: (exchangeId) => set({ editingExchange: exchangeId }),
setEditingTrader: (trader) => set({ editingTrader: trader }),
openModelModal: (modelId) => {
set({ editingModel: modelId || null, showModelModal: true })
},
closeModelModal: () => {
set({ showModelModal: false, editingModel: null })
},
openExchangeModal: (exchangeId) => {
set({ editingExchange: exchangeId || null, showExchangeModal: true })
},
closeExchangeModal: () => {
set({ showExchangeModal: false, editingExchange: null })
},
reset: () => set(initialState),
}))