Resolve merge conflicts in AITradersPage.tsx

- Fixed import statement conflict (using 'type Language')
- Merged exchange configuration logic preserving support for multiple exchange types
- Kept comprehensive form handling for Binance, Hyperliquid, Aster, and OKX exchanges
- Updated security warning messages to use proper translation keys

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
icy
2025-11-01 19:01:44 +08:00
15 changed files with 753 additions and 381 deletions

View File

@@ -3,7 +3,7 @@ import useSWR from 'swr';
import { api } from '../lib/api';
import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types';
import { useLanguage } from '../contexts/LanguageContext';
import { t, Language } from '../i18n/translations';
import { t, type Language } from '../i18n/translations';
import { getExchangeIcon } from './ExchangeIcons';
import { getModelIcon } from './ModelIcons';
import { TraderConfigModal } from './TraderConfigModal';
@@ -134,25 +134,25 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const handleCreateTrader = async (data: CreateTraderRequest) => {
try {
const model = allModels?.find(m => m.id === data.ai_model_id);
const model = allModels?.find(m => m.provider === data.ai_model_id);
const exchange = allExchanges?.find(e => e.id === data.exchange_id);
if (!model?.enabled) {
alert(t('modelNotConfigured', language));
return;
}
if (!exchange?.enabled) {
alert(t('exchangeNotConfigured', language));
return;
}
await api.createTrader(data);
setShowCreateModal(false);
mutateTraders();
} catch (error) {
console.error('Failed to create trader:', error);
alert('创建交易员失败');
alert(t('createTraderFailed', language));
}
};
@@ -163,24 +163,24 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowEditModal(true);
} catch (error) {
console.error('Failed to fetch trader config:', error);
alert('获取交易员配置失败');
alert(t('getTraderConfigFailed', language));
}
};
const handleSaveEditTrader = async (data: CreateTraderRequest) => {
if (!editingTrader) return;
try {
const model = enabledModels?.find(m => m.id === data.ai_model_id);
const model = enabledModels?.find(m => m.provider === data.ai_model_id);
const exchange = enabledExchanges?.find(e => e.id === data.exchange_id);
if (!model) {
alert('AI模型配置不存在或未启用');
alert(t('modelConfigNotExist', language));
return;
}
if (!exchange) {
alert('交易所配置不存在或未启用');
alert(t('exchangeConfigNotExist', language));
return;
}
@@ -205,19 +205,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
mutateTraders();
} catch (error) {
console.error('Failed to update trader:', error);
alert('更新交易员失败');
alert(t('updateTraderFailed', language));
}
};
const handleDeleteTrader = async (traderId: string) => {
if (!confirm(t('confirmDeleteTrader', language))) return;
try {
await api.deleteTrader(traderId);
mutateTraders();
} catch (error) {
console.error('Failed to delete trader:', error);
alert('删除交易员失败');
alert(t('deleteTraderFailed', language));
}
};
@@ -231,7 +231,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
mutateTraders();
} catch (error) {
console.error('Failed to toggle trader:', error);
alert('操作失败');
alert(t('operationFailed', language));
}
};
@@ -250,88 +250,91 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
};
const handleDeleteModelConfig = async (modelId: string) => {
if (!confirm('确定要删除此AI模型配置吗')) return;
if (!confirm(t('confirmDeleteModel', language))) return;
try {
const updatedModels = allModels?.map(m =>
m.id === modelId ? { ...m, apiKey: '', enabled: false } : m
const updatedModels = allModels?.map(m =>
m.id === modelId ? { ...m, apiKey: '', customApiUrl: '', customModelName: '', enabled: false } : m
) || [];
const request = {
models: Object.fromEntries(
updatedModels.map(model => [
model.id,
model.provider, // 使用 provider 而不是 id
{
enabled: model.enabled,
api_key: model.apiKey || ''
api_key: model.apiKey || '',
custom_api_url: model.customApiUrl || '',
custom_model_name: model.customModelName || ''
}
])
)
};
await api.updateModelConfigs(request);
setAllModels(updatedModels);
setShowModelModal(false);
setEditingModel(null);
} catch (error) {
console.error('Failed to delete model config:', error);
alert('删除配置失败');
alert(t('deleteConfigFailed', language));
}
};
const handleSaveModelConfig = async (modelId: string, apiKey: string, customApiUrl?: string) => {
const handleSaveModelConfig = async (modelId: string, apiKey: string, customApiUrl?: string, customModelName?: string) => {
try {
// 找到要配置的模型从supportedModels中
const modelToUpdate = supportedModels?.find(m => m.id === modelId);
if (!modelToUpdate) {
alert('模型不存在');
alert(t('modelNotExist', language));
return;
}
// 创建或更新用户的模型配置
const existingModel = allModels?.find(m => m.id === modelId);
let updatedModels;
if (existingModel) {
// 更新现有配置
updatedModels = allModels?.map(m =>
m.id === modelId ? { ...m, apiKey, customApiUrl: customApiUrl || '', enabled: true } : m
updatedModels = allModels?.map(m =>
m.id === modelId ? { ...m, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true } : m
) || [];
} else {
// 添加新配置
const newModel = { ...modelToUpdate, apiKey, customApiUrl: customApiUrl || '', enabled: true };
const newModel = { ...modelToUpdate, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true };
updatedModels = [...(allModels || []), newModel];
}
const request = {
models: Object.fromEntries(
updatedModels.map(model => [
model.id,
model.provider, // 使用 provider 而不是 id
{
enabled: model.enabled,
api_key: model.apiKey || '',
custom_api_url: model.customApiUrl || ''
custom_api_url: model.customApiUrl || '',
custom_model_name: model.customModelName || ''
}
])
)
};
await api.updateModelConfigs(request);
// 重新获取用户配置以确保数据同步
const refreshedModels = await api.getModelConfigs();
setAllModels(refreshedModels);
setShowModelModal(false);
setEditingModel(null);
} catch (error) {
console.error('Failed to save model config:', error);
alert('保存配置失败');
alert(t('saveConfigFailed', language));
}
};
const handleDeleteExchangeConfig = async (exchangeId: string) => {
if (!confirm('确定要删除此交易所配置吗?')) return;
if (!confirm(t('confirmDeleteExchange', language))) return;
try {
const updatedExchanges = allExchanges?.map(e =>
@@ -358,7 +361,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingExchange(null);
} catch (error) {
console.error('Failed to delete exchange config:', error);
alert('删除交易所配置失败');
alert(t('deleteExchangeConfigFailed', language));
}
};
@@ -367,7 +370,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// 找到要配置的交易所从supportedExchanges中
const exchangeToUpdate = supportedExchanges?.find(e => e.id === exchangeId);
if (!exchangeToUpdate) {
alert('交易所不存在');
alert(t('exchangeNotExist', language));
return;
}
@@ -434,7 +437,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingExchange(null);
} catch (error) {
console.error('Failed to save exchange config:', error);
alert('保存交易所配置失败');
alert(t('saveConfigFailed', language));
}
};
@@ -455,7 +458,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowSignalSourceModal(false);
} catch (error) {
console.error('Failed to save signal source:', error);
alert('保存信号源配置失败');
alert(t('saveSignalSourceFailed', language));
}
};
@@ -516,13 +519,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button
onClick={() => setShowSignalSourceModal(true)}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#2B3139',
color: '#EAECEF',
border: '1px solid #474D57'
style={{
background: '#2B3139',
color: '#EAECEF',
border: '1px solid #474D57'
}}
>
📡
📡 {t('signalSource', language)}
</button>
<button
@@ -575,7 +578,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(model.name)}</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{inUse ? '正在使用' : model.enabled ? '已启用' : '已配置'}
{inUse ? t('inUse', language) : model.enabled ? t('enabled', language) : t('configured', language)}
</div>
</div>
</div>
@@ -586,7 +589,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{configuredModels.length === 0 && (
<div className="text-center py-8" style={{ color: '#848E9C' }}>
<Brain className="w-12 h-12 mx-auto mb-2 opacity-50" />
<div className="text-sm">AI模型</div>
<div className="text-sm">{t('noModelsConfigured', language)}</div>
</div>
)}
</div>
@@ -617,7 +620,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(exchange.name)}</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{exchange.type.toUpperCase()} {inUse ? '正在使用' : exchange.enabled ? '已启用' : '已配置'}
{exchange.type.toUpperCase()} {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)}
</div>
</div>
</div>
@@ -628,7 +631,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{configuredExchanges.length === 0 && (
<div className="text-center py-8" style={{ color: '#848E9C' }}>
<Landmark className="w-12 h-12 mx-auto mb-2 opacity-50" />
<div className="text-sm"></div>
<div className="text-sm">{t('noExchangesConfigured', language)}</div>
</div>
)}
</div>
@@ -692,19 +695,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
style={{ background: 'rgba(99, 102, 241, 0.1)', color: '#6366F1' }}
>
<BarChart3 className="w-4 h-4" />
{t('view', language)}
</button>
<button
onClick={() => handleEditTrader(trader.trader_id)}
disabled={trader.is_running}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
style={{
background: trader.is_running ? 'rgba(132, 142, 156, 0.1)' : 'rgba(255, 193, 7, 0.1)',
color: trader.is_running ? '#848E9C' : '#FFC107'
style={{
background: trader.is_running ? 'rgba(132, 142, 156, 0.1)' : 'rgba(255, 193, 7, 0.1)',
color: trader.is_running ? '#848E9C' : '#FFC107'
}}
>
{t('edit', language)}
</button>
<button
@@ -789,6 +792,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowModelModal(false);
setEditingModel(null);
}}
language={language}
/>
)}
@@ -814,6 +818,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
oiTopUrl={userSignalSource.oiTopUrl}
onSave={handleSaveSignalSource}
onClose={() => setShowSignalSourceModal(false)}
language={language}
/>
)}
</div>
@@ -825,12 +830,14 @@ function SignalSourceModal({
coinPoolUrl,
oiTopUrl,
onSave,
onClose
onClose,
language
}: {
coinPoolUrl: string;
oiTopUrl: string;
onSave: (coinPoolUrl: string, oiTopUrl: string) => void;
onClose: () => void;
language: Language;
}) {
const [coinPool, setCoinPool] = useState(coinPoolUrl || '');
const [oiTop, setOiTop] = useState(oiTopUrl || '');
@@ -844,7 +851,7 @@ function SignalSourceModal({
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}>
<h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}>
📡
📡 {t('signalSourceConfig', language)}
</h3>
<form onSubmit={handleSubmit} className="space-y-4">
@@ -861,7 +868,7 @@ function SignalSourceModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
API地址使
{t('coinPoolDescription', language)}
</div>
</div>
@@ -878,18 +885,18 @@ function SignalSourceModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
API地址使
{t('oiTopDescription', language)}
</div>
</div>
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
{t('information', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div> URL</div>
<div> 使</div>
<div> URL将用于获取市场数据和交易信号</div>
<div>{t('signalSourceInfo1', language)}</div>
<div>{t('signalSourceInfo2', language)}</div>
<div>{t('signalSourceInfo3', language)}</div>
</div>
</div>
@@ -900,14 +907,14 @@ function SignalSourceModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{t('cancel', language)}
</button>
<button
type="submit"
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#F0B90B', color: '#000' }}
>
{t('save', language)}
</button>
</div>
</form>
@@ -923,37 +930,41 @@ function ModelConfigModal({
editingModelId,
onSave,
onDelete,
onClose
onClose,
language
}: {
allModels: AIModel[];
configuredModels: AIModel[];
editingModelId: string | null;
onSave: (modelId: string, apiKey: string, baseUrl?: string) => void;
onSave: (modelId: string, apiKey: string, baseUrl?: string, modelName?: string) => void;
onDelete: (modelId: string) => void;
onClose: () => void;
language: Language;
}) {
const [selectedModelId, setSelectedModelId] = useState(editingModelId || '');
const [apiKey, setApiKey] = useState('');
const [baseUrl, setBaseUrl] = useState('');
const [modelName, setModelName] = useState('');
// 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找
const selectedModel = editingModelId
? configuredModels?.find(m => m.id === selectedModelId)
const selectedModel = editingModelId
? configuredModels?.find(m => m.id === selectedModelId)
: allModels?.find(m => m.id === selectedModelId);
// 如果是编辑现有模型初始化API KeyBase URL
// 如果是编辑现有模型初始化API KeyBase URL和Model Name
useEffect(() => {
if (editingModelId && selectedModel) {
setApiKey(selectedModel.apiKey || '');
setBaseUrl(selectedModel.customApiUrl || '');
setModelName(selectedModel.customModelName || '');
}
}, [editingModelId, selectedModel]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!selectedModelId || !apiKey.trim()) return;
onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined);
onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined);
};
// 可选择的模型列表(所有支持的模型)
@@ -964,30 +975,30 @@ function ModelConfigModal({
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{editingModelId ? '编辑AI模型' : '添加AI模型'}
{editingModelId ? t('editAIModel', language) : t('addAIModel', language)}
</h3>
{editingModelId && (
<button
type="button"
onClick={() => {
if (confirm('确定要删除此AI模型配置吗')) {
if (confirm(t('confirmDeleteModel', language))) {
onDelete(editingModelId);
}
}}
className="p-2 rounded hover:bg-red-100 transition-colors"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
title="删除配置"
title={t('deleteConfigFailed', language)}
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{!editingModelId && (
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
AI模型
{t('selectModel', language)}
</label>
<select
value={selectedModelId}
@@ -996,7 +1007,7 @@ function ModelConfigModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
>
<option value=""></option>
<option value="">{t('pleaseSelectModel', language)}</option>
{availableModels.map(model => (
<option key={model.id} value={model.id}>
{getShortName(model.name)} ({model.provider})
@@ -1040,7 +1051,7 @@ function ModelConfigModal({
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="输入API密钥"
placeholder={t('enterAPIKey', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
@@ -1049,29 +1060,46 @@ function ModelConfigModal({
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
Base URL ()
{t('customBaseURL', language)}
</label>
<input
type="url"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder="自定义API基础URL如: https://api.openai.com/v1"
placeholder={t('customBaseURLPlaceholder', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
使API地址
{t('leaveBlankForDefault', language)}
</div>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
Model Name ()
</label>
<input
type="text"
value={modelName}
onChange={(e) => setModelName(e.target.value)}
placeholder="例如: deepseek-chat, qwen-plus, gpt-4"
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
使
</div>
</div>
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
{t('information', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div> API Key将被加密存储</div>
<div> Base URL用于自定义API服务器地址</div>
<div> 使</div>
<div>{t('modelConfigInfo1', language)}</div>
<div>{t('modelConfigInfo2', language)}</div>
<div>{t('modelConfigInfo3', language)}</div>
</div>
</div>
</>
@@ -1084,7 +1112,7 @@ function ModelConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{t('cancel', language)}
</button>
<button
type="submit"
@@ -1092,7 +1120,7 @@ function ModelConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }}
>
{t('saveConfig', language)}
</button>
</div>
</form>
@@ -1184,30 +1212,30 @@ function ExchangeConfigModal({
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{editingExchangeId ? '编辑交易所' : '添加交易所'}
{editingExchangeId ? t('editExchange', language) : t('addExchange', language)}
</h3>
{editingExchangeId && (
<button
type="button"
onClick={() => {
if (confirm('确定要删除此交易所配置吗?')) {
if (confirm(t('confirmDeleteExchange', language))) {
onDelete(editingExchangeId);
}
}}
className="p-2 rounded hover:bg-red-100 transition-colors"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
title="删除配置"
title={t('deleteConfigFailed', language)}
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{!editingExchangeId && (
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('selectExchange', language)}
</label>
<select
value={selectedExchangeId}
@@ -1216,7 +1244,7 @@ function ExchangeConfigModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
>
<option value=""></option>
<option value="">{t('pleaseSelectExchange', language)}</option>
{availableExchanges.map(exchange => (
<option key={exchange.id} value={exchange.id}>
{getShortName(exchange.name)} ({exchange.type.toUpperCase()})
@@ -1405,12 +1433,12 @@ function ExchangeConfigModal({
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
{t('securityWarning', language)}
{t('securityWarning', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div>{t('securityTip1', language)}</div>
<div>{t('securityTip2', language)}</div>
<div>{t('securityTip3', language)}</div>
<div>{t('exchangeConfigWarning1', language)}</div>
<div>{t('exchangeConfigWarning2', language)}</div>
<div>{t('exchangeConfigWarning3', language)}</div>
</div>
</div>
</>
@@ -1438,7 +1466,7 @@ function ExchangeConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }}
>
{t('saveConfiguration', language)}
{t('saveConfig', language)}
</button>
</div>
</form>

View File

@@ -72,7 +72,7 @@ export function TraderConfigModal({
} else if (!isEditMode) {
setFormData({
trader_name: '',
ai_model: availableModels[0]?.id || '',
ai_model: availableModels[0]?.provider || '',
exchange_id: availableExchanges[0]?.id || '',
btc_eth_leverage: 5,
altcoin_leverage: 3,
@@ -217,7 +217,7 @@ export function TraderConfigModal({
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
{availableModels.map(model => (
<option key={model.id} value={model.id}>
<option key={model.id} value={model.provider}>
{getShortName(model.name || model.id).toUpperCase()}
</option>
))}