mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 03:50:59 +08:00
* 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>
640 lines
19 KiB
TypeScript
640 lines
19 KiB
TypeScript
import { api } from '../lib/api'
|
|
import type {
|
|
TraderInfo,
|
|
CreateTraderRequest,
|
|
TraderConfigData,
|
|
AIModel,
|
|
Exchange,
|
|
} from '../types'
|
|
import { t } from '../i18n/translations'
|
|
import { confirmToast } from '../lib/notify'
|
|
import { toast } from 'sonner'
|
|
import type { Language } from '../i18n/translations'
|
|
|
|
interface UseTraderActionsParams {
|
|
traders: TraderInfo[] | undefined
|
|
allModels: AIModel[]
|
|
allExchanges: Exchange[]
|
|
supportedModels: AIModel[]
|
|
supportedExchanges: Exchange[]
|
|
language: Language
|
|
mutateTraders: () => Promise<any>
|
|
setAllModels: (models: AIModel[]) => void
|
|
setAllExchanges: (exchanges: Exchange[]) => void
|
|
setUserSignalSource: (config: {
|
|
coinPoolUrl: string
|
|
oiTopUrl: string
|
|
}) => void
|
|
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
|
|
editingTrader: TraderConfigData | null
|
|
setEditingTrader: (trader: TraderConfigData | null) => void
|
|
}
|
|
|
|
export function useTraderActions({
|
|
traders,
|
|
allModels,
|
|
allExchanges,
|
|
supportedModels,
|
|
supportedExchanges,
|
|
language,
|
|
mutateTraders,
|
|
setAllModels,
|
|
setAllExchanges,
|
|
setUserSignalSource,
|
|
setShowCreateModal,
|
|
setShowEditModal,
|
|
setShowModelModal,
|
|
setShowExchangeModal,
|
|
setShowSignalSourceModal,
|
|
setEditingModel,
|
|
setEditingExchange,
|
|
editingTrader,
|
|
setEditingTrader,
|
|
}: UseTraderActionsParams) {
|
|
// 检查模型是否正在被运行中的交易员使用(用于UI禁用)
|
|
const isModelInUse = (modelId: string) => {
|
|
return traders?.some((t) => t.ai_model === modelId && t.is_running) || false
|
|
}
|
|
|
|
// 检查交易所是否正在被运行中的交易员使用(用于UI禁用)
|
|
const isExchangeInUse = (exchangeId: string) => {
|
|
return (
|
|
traders?.some((t) => t.exchange_id === exchangeId && t.is_running) ||
|
|
false
|
|
)
|
|
}
|
|
|
|
// 检查模型是否被任何交易员使用(包括停止状态的)
|
|
const isModelUsedByAnyTrader = (modelId: string) => {
|
|
return traders?.some((t) => t.ai_model === modelId) || false
|
|
}
|
|
|
|
// 检查交易所是否被任何交易员使用(包括停止状态的)
|
|
const isExchangeUsedByAnyTrader = (exchangeId: string) => {
|
|
return traders?.some((t) => t.exchange_id === exchangeId) || false
|
|
}
|
|
|
|
// 获取使用特定模型的交易员列表
|
|
const getTradersUsingModel = (modelId: string) => {
|
|
return traders?.filter((t) => t.ai_model === modelId) || []
|
|
}
|
|
|
|
// 获取使用特定交易所的交易员列表
|
|
const getTradersUsingExchange = (exchangeId: string) => {
|
|
return traders?.filter((t) => t.exchange_id === exchangeId) || []
|
|
}
|
|
|
|
const handleCreateTrader = async (data: CreateTraderRequest) => {
|
|
try {
|
|
const model = allModels?.find((m) => m.id === data.ai_model_id)
|
|
const exchange = allExchanges?.find((e) => e.id === data.exchange_id)
|
|
|
|
if (!model?.enabled) {
|
|
toast.error(t('modelNotConfigured', language))
|
|
return
|
|
}
|
|
|
|
if (!exchange?.enabled) {
|
|
toast.error(t('exchangeNotConfigured', language))
|
|
return
|
|
}
|
|
|
|
await toast.promise(api.createTrader(data), {
|
|
loading: '正在创建…',
|
|
success: '创建成功',
|
|
error: '创建失败',
|
|
})
|
|
setShowCreateModal(false)
|
|
// Immediately refresh traders list for better UX
|
|
await mutateTraders()
|
|
} catch (error) {
|
|
console.error('Failed to create trader:', error)
|
|
toast.error(t('createTraderFailed', language))
|
|
}
|
|
}
|
|
|
|
const handleEditTrader = async (traderId: string) => {
|
|
try {
|
|
const traderConfig = await api.getTraderConfig(traderId)
|
|
setEditingTrader(traderConfig)
|
|
setShowEditModal(true)
|
|
} catch (error) {
|
|
console.error('Failed to fetch trader config:', error)
|
|
toast.error(t('getTraderConfigFailed', language))
|
|
}
|
|
}
|
|
|
|
const handleSaveEditTrader = async (data: CreateTraderRequest) => {
|
|
if (!editingTrader || !editingTrader.trader_id) return
|
|
|
|
try {
|
|
const enabledModels = allModels?.filter((m) => m.enabled) || []
|
|
const enabledExchanges =
|
|
allExchanges?.filter((e) => {
|
|
if (!e.enabled) return false
|
|
|
|
// Aster 交易所需要特殊字段
|
|
if (e.id === 'aster') {
|
|
return (
|
|
e.asterUser &&
|
|
e.asterUser.trim() !== '' &&
|
|
e.asterSigner &&
|
|
e.asterSigner.trim() !== ''
|
|
)
|
|
}
|
|
|
|
// Hyperliquid 需要钱包地址
|
|
if (e.id === 'hyperliquid') {
|
|
return (
|
|
e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== ''
|
|
)
|
|
}
|
|
|
|
return true
|
|
}) || []
|
|
|
|
const model = enabledModels?.find((m) => m.id === data.ai_model_id)
|
|
const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id)
|
|
|
|
if (!model) {
|
|
toast.error(t('modelConfigNotExist', language))
|
|
return
|
|
}
|
|
|
|
if (!exchange) {
|
|
toast.error(t('exchangeConfigNotExist', language))
|
|
return
|
|
}
|
|
|
|
const request = {
|
|
name: data.name,
|
|
ai_model_id: data.ai_model_id,
|
|
exchange_id: data.exchange_id,
|
|
initial_balance: data.initial_balance,
|
|
scan_interval_minutes: data.scan_interval_minutes,
|
|
btc_eth_leverage: data.btc_eth_leverage,
|
|
altcoin_leverage: data.altcoin_leverage,
|
|
trading_symbols: data.trading_symbols,
|
|
custom_prompt: data.custom_prompt,
|
|
override_base_prompt: data.override_base_prompt,
|
|
system_prompt_template: data.system_prompt_template,
|
|
is_cross_margin: data.is_cross_margin,
|
|
use_coin_pool: data.use_coin_pool,
|
|
use_oi_top: data.use_oi_top,
|
|
}
|
|
|
|
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
|
|
loading: '正在保存…',
|
|
success: '保存成功',
|
|
error: '保存失败',
|
|
})
|
|
setShowEditModal(false)
|
|
setEditingTrader(null)
|
|
// Immediately refresh traders list for better UX
|
|
await mutateTraders()
|
|
} catch (error) {
|
|
console.error('Failed to update trader:', error)
|
|
toast.error(t('updateTraderFailed', language))
|
|
}
|
|
}
|
|
|
|
const handleDeleteTrader = async (traderId: string) => {
|
|
{
|
|
const ok = await confirmToast(t('confirmDeleteTrader', language))
|
|
if (!ok) return
|
|
}
|
|
|
|
try {
|
|
await toast.promise(api.deleteTrader(traderId), {
|
|
loading: '正在删除…',
|
|
success: '删除成功',
|
|
error: '删除失败',
|
|
})
|
|
|
|
// Immediately refresh traders list for better UX
|
|
await mutateTraders()
|
|
} catch (error) {
|
|
console.error('Failed to delete trader:', error)
|
|
toast.error(t('deleteTraderFailed', language))
|
|
}
|
|
}
|
|
|
|
const handleToggleTrader = async (traderId: string, running: boolean) => {
|
|
try {
|
|
if (running) {
|
|
await toast.promise(api.stopTrader(traderId), {
|
|
loading: '正在停止…',
|
|
success: '已停止',
|
|
error: '停止失败',
|
|
})
|
|
} else {
|
|
await toast.promise(api.startTrader(traderId), {
|
|
loading: '正在启动…',
|
|
success: '已启动',
|
|
error: '启动失败',
|
|
})
|
|
}
|
|
|
|
// Immediately refresh traders list to update running status
|
|
await mutateTraders()
|
|
} catch (error) {
|
|
console.error('Failed to toggle trader:', error)
|
|
toast.error(t('operationFailed', language))
|
|
}
|
|
}
|
|
|
|
const handleModelClick = (modelId: string) => {
|
|
if (!isModelInUse(modelId)) {
|
|
setEditingModel(modelId)
|
|
setShowModelModal(true)
|
|
}
|
|
}
|
|
|
|
const handleExchangeClick = (exchangeId: string) => {
|
|
if (!isExchangeInUse(exchangeId)) {
|
|
setEditingExchange(exchangeId)
|
|
setShowExchangeModal(true)
|
|
}
|
|
}
|
|
|
|
// 通用删除配置处理函数
|
|
const handleDeleteConfig = async <T extends { id: string }>(config: {
|
|
id: string
|
|
type: 'model' | 'exchange'
|
|
checkInUse: (id: string) => boolean
|
|
getUsingTraders: (id: string) => any[]
|
|
cannotDeleteKey: string
|
|
confirmDeleteKey: string
|
|
allItems: T[] | undefined
|
|
clearFields: (item: T) => T
|
|
buildRequest: (items: T[]) => any
|
|
updateApi: (request: any) => Promise<void>
|
|
refreshApi: () => Promise<T[]>
|
|
setItems: (items: T[]) => void
|
|
closeModal: () => void
|
|
errorKey: string
|
|
}) => {
|
|
// 检查是否有交易员正在使用
|
|
if (config.checkInUse(config.id)) {
|
|
const usingTraders = config.getUsingTraders(config.id)
|
|
const traderNames = usingTraders.map((t) => t.trader_name).join(', ')
|
|
toast.error(
|
|
`${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}`
|
|
)
|
|
return
|
|
}
|
|
|
|
{
|
|
const ok = await confirmToast(t(config.confirmDeleteKey, language))
|
|
if (!ok) return
|
|
}
|
|
|
|
try {
|
|
const updatedItems =
|
|
config.allItems?.map((item) =>
|
|
item.id === config.id ? config.clearFields(item) : item
|
|
) || []
|
|
|
|
const request = config.buildRequest(updatedItems)
|
|
await toast.promise(config.updateApi(request), {
|
|
loading: '正在更新配置…',
|
|
success: '配置已更新',
|
|
error: '更新配置失败',
|
|
})
|
|
|
|
// 重新获取用户配置以确保数据同步
|
|
const refreshedItems = await config.refreshApi()
|
|
config.setItems(refreshedItems)
|
|
|
|
config.closeModal()
|
|
} catch (error) {
|
|
console.error(`Failed to delete ${config.type} config:`, error)
|
|
toast.error(t(config.errorKey, language))
|
|
}
|
|
}
|
|
|
|
const handleDeleteModel = async (modelId: string) => {
|
|
await handleDeleteConfig({
|
|
id: modelId,
|
|
type: 'model',
|
|
checkInUse: isModelUsedByAnyTrader,
|
|
getUsingTraders: getTradersUsingModel,
|
|
cannotDeleteKey: 'cannotDeleteModelInUse',
|
|
confirmDeleteKey: 'confirmDeleteModel',
|
|
allItems: allModels,
|
|
clearFields: (m) => ({
|
|
...m,
|
|
apiKey: '',
|
|
customApiUrl: '',
|
|
customModelName: '',
|
|
enabled: false,
|
|
}),
|
|
buildRequest: (models) => ({
|
|
models: Object.fromEntries(
|
|
models.map((model) => [
|
|
model.provider,
|
|
{
|
|
enabled: model.enabled,
|
|
api_key: model.apiKey || '',
|
|
custom_api_url: model.customApiUrl || '',
|
|
custom_model_name: model.customModelName || '',
|
|
},
|
|
])
|
|
),
|
|
}),
|
|
updateApi: api.updateModelConfigs,
|
|
refreshApi: api.getModelConfigs,
|
|
setItems: (items) => {
|
|
// 使用函数式更新确保状态正确更新
|
|
setAllModels([...items])
|
|
},
|
|
closeModal: () => {
|
|
setShowModelModal(false)
|
|
setEditingModel(null)
|
|
},
|
|
errorKey: 'deleteConfigFailed',
|
|
})
|
|
}
|
|
|
|
const handleSaveModel = async (
|
|
modelId: string,
|
|
apiKey: string,
|
|
customApiUrl?: string,
|
|
customModelName?: string
|
|
) => {
|
|
try {
|
|
// 创建或更新用户的模型配置
|
|
const existingModel = allModels?.find((m) => m.id === modelId)
|
|
let updatedModels
|
|
|
|
// 找到要配置的模型(优先从已配置列表,其次从支持列表)
|
|
const modelToUpdate =
|
|
existingModel || supportedModels?.find((m) => m.id === modelId)
|
|
if (!modelToUpdate) {
|
|
toast.error(t('modelNotExist', language))
|
|
return
|
|
}
|
|
|
|
if (existingModel) {
|
|
// 更新现有配置
|
|
updatedModels =
|
|
allModels?.map((m) =>
|
|
m.id === modelId
|
|
? {
|
|
...m,
|
|
apiKey,
|
|
customApiUrl: customApiUrl || '',
|
|
customModelName: customModelName || '',
|
|
enabled: true,
|
|
}
|
|
: m
|
|
) || []
|
|
} else {
|
|
// 添加新配置
|
|
const newModel = {
|
|
...modelToUpdate,
|
|
apiKey,
|
|
customApiUrl: customApiUrl || '',
|
|
customModelName: customModelName || '',
|
|
enabled: true,
|
|
}
|
|
updatedModels = [...(allModels || []), newModel]
|
|
}
|
|
|
|
const request = {
|
|
models: Object.fromEntries(
|
|
updatedModels.map((model) => [
|
|
model.provider, // 使用 provider 而不是 id
|
|
{
|
|
enabled: model.enabled,
|
|
api_key: model.apiKey || '',
|
|
custom_api_url: model.customApiUrl || '',
|
|
custom_model_name: model.customModelName || '',
|
|
},
|
|
])
|
|
),
|
|
}
|
|
|
|
await toast.promise(api.updateModelConfigs(request), {
|
|
loading: '正在更新模型配置…',
|
|
success: '模型配置已更新',
|
|
error: '更新模型配置失败',
|
|
})
|
|
|
|
// 重新获取用户配置以确保数据同步
|
|
const refreshedModels = await api.getModelConfigs()
|
|
setAllModels(refreshedModels)
|
|
|
|
setShowModelModal(false)
|
|
setEditingModel(null)
|
|
} catch (error) {
|
|
console.error('Failed to save model config:', error)
|
|
toast.error(t('saveConfigFailed', language))
|
|
}
|
|
}
|
|
|
|
const handleDeleteExchange = async (exchangeId: string) => {
|
|
await handleDeleteConfig({
|
|
id: exchangeId,
|
|
type: 'exchange',
|
|
checkInUse: isExchangeUsedByAnyTrader,
|
|
getUsingTraders: getTradersUsingExchange,
|
|
cannotDeleteKey: 'cannotDeleteExchangeInUse',
|
|
confirmDeleteKey: 'confirmDeleteExchange',
|
|
allItems: allExchanges,
|
|
clearFields: (e) => ({
|
|
...e,
|
|
apiKey: '',
|
|
secretKey: '',
|
|
hyperliquidWalletAddr: '',
|
|
asterUser: '',
|
|
asterSigner: '',
|
|
asterPrivateKey: '',
|
|
enabled: false,
|
|
}),
|
|
buildRequest: (exchanges) => ({
|
|
exchanges: Object.fromEntries(
|
|
exchanges.map((exchange) => [
|
|
exchange.id,
|
|
{
|
|
enabled: exchange.enabled,
|
|
api_key: exchange.apiKey || '',
|
|
secret_key: exchange.secretKey || '',
|
|
testnet: exchange.testnet || false,
|
|
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
|
aster_user: exchange.asterUser || '',
|
|
aster_signer: exchange.asterSigner || '',
|
|
aster_private_key: exchange.asterPrivateKey || '',
|
|
},
|
|
])
|
|
),
|
|
}),
|
|
updateApi: api.updateExchangeConfigsEncrypted,
|
|
refreshApi: api.getExchangeConfigs,
|
|
setItems: (items) => {
|
|
// 使用函数式更新确保状态正确更新
|
|
setAllExchanges([...items])
|
|
},
|
|
closeModal: () => {
|
|
setShowExchangeModal(false)
|
|
setEditingExchange(null)
|
|
},
|
|
errorKey: 'deleteExchangeConfigFailed',
|
|
})
|
|
}
|
|
|
|
const handleSaveExchange = async (
|
|
exchangeId: string,
|
|
apiKey: string,
|
|
secretKey?: string,
|
|
testnet?: boolean,
|
|
hyperliquidWalletAddr?: string,
|
|
asterUser?: string,
|
|
asterSigner?: string,
|
|
asterPrivateKey?: string
|
|
) => {
|
|
try {
|
|
// 找到要配置的交易所(从supportedExchanges中)
|
|
const exchangeToUpdate = supportedExchanges?.find(
|
|
(e) => e.id === exchangeId
|
|
)
|
|
if (!exchangeToUpdate) {
|
|
toast.error(t('exchangeNotExist', language))
|
|
return
|
|
}
|
|
|
|
// 创建或更新用户的交易所配置
|
|
const existingExchange = allExchanges?.find((e) => e.id === exchangeId)
|
|
let updatedExchanges
|
|
|
|
if (existingExchange) {
|
|
// 更新现有配置
|
|
updatedExchanges =
|
|
allExchanges?.map((e) =>
|
|
e.id === exchangeId
|
|
? {
|
|
...e,
|
|
apiKey,
|
|
secretKey,
|
|
testnet,
|
|
hyperliquidWalletAddr,
|
|
asterUser,
|
|
asterSigner,
|
|
asterPrivateKey,
|
|
enabled: true,
|
|
}
|
|
: e
|
|
) || []
|
|
} else {
|
|
// 添加新配置
|
|
const newExchange = {
|
|
...exchangeToUpdate,
|
|
apiKey,
|
|
secretKey,
|
|
testnet,
|
|
hyperliquidWalletAddr,
|
|
asterUser,
|
|
asterSigner,
|
|
asterPrivateKey,
|
|
enabled: true,
|
|
}
|
|
updatedExchanges = [...(allExchanges || []), newExchange]
|
|
}
|
|
|
|
const request = {
|
|
exchanges: Object.fromEntries(
|
|
updatedExchanges.map((exchange) => [
|
|
exchange.id,
|
|
{
|
|
enabled: exchange.enabled,
|
|
api_key: exchange.apiKey || '',
|
|
secret_key: exchange.secretKey || '',
|
|
testnet: exchange.testnet || false,
|
|
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
|
aster_user: exchange.asterUser || '',
|
|
aster_signer: exchange.asterSigner || '',
|
|
aster_private_key: exchange.asterPrivateKey || '',
|
|
},
|
|
])
|
|
),
|
|
}
|
|
|
|
await toast.promise(api.updateExchangeConfigsEncrypted(request), {
|
|
loading: '正在更新交易所配置…',
|
|
success: '交易所配置已更新',
|
|
error: '更新交易所配置失败',
|
|
})
|
|
|
|
// 重新获取用户配置以确保数据同步
|
|
const refreshedExchanges = await api.getExchangeConfigs()
|
|
setAllExchanges(refreshedExchanges)
|
|
|
|
setShowExchangeModal(false)
|
|
setEditingExchange(null)
|
|
} catch (error) {
|
|
console.error('Failed to save exchange config:', error)
|
|
toast.error(t('saveConfigFailed', language))
|
|
}
|
|
}
|
|
|
|
const handleAddModel = () => {
|
|
setEditingModel(null)
|
|
setShowModelModal(true)
|
|
}
|
|
|
|
const handleAddExchange = () => {
|
|
setEditingExchange(null)
|
|
setShowExchangeModal(true)
|
|
}
|
|
|
|
const handleSaveSignalSource = async (
|
|
coinPoolUrl: string,
|
|
oiTopUrl: string
|
|
) => {
|
|
try {
|
|
await toast.promise(api.saveUserSignalSource(coinPoolUrl, oiTopUrl), {
|
|
loading: '正在保存…',
|
|
success: '保存成功',
|
|
error: '保存失败',
|
|
})
|
|
setUserSignalSource({ coinPoolUrl, oiTopUrl })
|
|
setShowSignalSourceModal(false)
|
|
} catch (error) {
|
|
console.error('Failed to save signal source:', error)
|
|
toast.error(t('saveSignalSourceFailed', language))
|
|
}
|
|
}
|
|
|
|
return {
|
|
// 辅助函数
|
|
isModelInUse,
|
|
isExchangeInUse,
|
|
isModelUsedByAnyTrader,
|
|
isExchangeUsedByAnyTrader,
|
|
getTradersUsingModel,
|
|
getTradersUsingExchange,
|
|
|
|
// 事件处理函数
|
|
handleCreateTrader,
|
|
handleEditTrader,
|
|
handleSaveEditTrader,
|
|
handleDeleteTrader,
|
|
handleToggleTrader,
|
|
handleAddModel,
|
|
handleAddExchange,
|
|
handleModelClick,
|
|
handleExchangeClick,
|
|
handleSaveModel,
|
|
handleDeleteModel,
|
|
handleSaveExchange,
|
|
handleDeleteExchange,
|
|
handleSaveSignalSource,
|
|
}
|
|
}
|