refactor: optimize codebase encoding

This commit is contained in:
tinkle-community
2026-03-12 16:12:08 +08:00
parent 2314ece9d1
commit 736d2d385d
61 changed files with 2301 additions and 1533 deletions

View File

@@ -1,4 +1,5 @@
import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
export function DataPage() {
const { language } = useLanguage()
@@ -7,7 +8,7 @@ export function DataPage() {
<div className="w-full h-[calc(100vh-64px)]">
<iframe
src="https://nofxos.ai/dashboard"
title={language === 'zh' ? '数据中心' : 'Data Center'}
title={t('dataCenter', language)}
className="w-full h-full border-0"
allow="fullscreen"
/>

View File

@@ -20,6 +20,7 @@ import {
import { useLanguage } from '../contexts/LanguageContext'
import { useAuth } from '../contexts/AuthContext'
import { toast } from 'sonner'
import { t } from '../i18n/translations'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
interface PublicStrategy {
@@ -106,88 +107,7 @@ export function StrategyMarketPage() {
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [copiedId, setCopiedId] = useState<string | null>(null)
const texts = {
zh: {
title: '策略市场',
subtitle: 'STRATEGY MARKETPLACE',
description: '发现、学习并复用社区精英交易员的策略配置',
search: '搜索参数...',
all: '全部协议',
popular: '热门配置',
recent: '最新提交',
myStrategies: '我的库',
noStrategies: '无信号',
noStrategiesDesc: '当前频段未检测到策略信号',
author: 'OPERATOR',
createdAt: 'TIMESTAMP',
viewConfig: 'DECRYPT CONFIG',
hideConfig: 'ENCRYPT',
copyConfig: 'CLONE CONFIG',
copied: 'COPIED',
configHidden: 'ENCRYPTED',
configHiddenDesc: '配置参数已加密',
indicators: 'INDICATORS',
maxPositions: 'POS_LIMIT',
maxLeverage: 'LEV_MAX',
shareYours: 'UPLOAD_STRATEGY',
makePublic: 'PUBLISH',
loading: 'INITIALIZING...'
},
en: {
title: 'STRATEGY MARKET',
subtitle: 'GLOBAL STRATEGY DATABASE',
description: 'Discover, analyze, and clone high-performance trading algorithms',
search: 'SEARCH PARAMETERS...',
all: 'ALL PROTOCOLS',
popular: 'TRENDING',
recent: 'LATEST',
myStrategies: 'MY LIBRARY',
noStrategies: 'NO SIGNAL',
noStrategiesDesc: 'No strategic signals detected in this frequency',
author: 'OPERATOR',
createdAt: 'TIMESTAMP',
viewConfig: 'DECRYPT CONFIG',
hideConfig: 'ENCRYPT',
copyConfig: 'CLONE CONFIG',
copied: 'COPIED',
configHidden: 'ENCRYPTED',
configHiddenDesc: 'Configuration parameters encrypted',
indicators: 'INDICATORS',
maxPositions: 'POS_LIMIT',
maxLeverage: 'LEV_MAX',
shareYours: 'UPLOAD_STRATEGY',
makePublic: 'PUBLISH',
loading: 'INITIALIZING...'
},
id: {
title: 'PASAR STRATEGI',
subtitle: 'DATABASE STRATEGI GLOBAL',
description: 'Temukan, analisis, dan kloning algoritma trading berperforma tinggi',
search: 'CARI PARAMETER...',
all: 'SEMUA PROTOKOL',
popular: 'TREN',
recent: 'TERBARU',
myStrategies: 'PERPUSTAKAAN SAYA',
noStrategies: 'TIDAK ADA SINYAL',
noStrategiesDesc: 'Tidak ada sinyal strategis terdeteksi pada frekuensi ini',
author: 'OPERATOR',
createdAt: 'TIMESTAMP',
viewConfig: 'DEKRIPSI CONFIG',
hideConfig: 'ENKRIPSI',
copyConfig: 'KLON CONFIG',
copied: 'DISALIN',
configHidden: 'TERENKRIPSI',
configHiddenDesc: 'Parameter konfigurasi terenkripsi',
indicators: 'INDIKATOR',
maxPositions: 'BATAS_POS',
maxLeverage: 'LEV_MAKS',
shareYours: 'UNGGAH_STRATEGI',
makePublic: 'PUBLIKASI',
loading: 'MENGINISIALISASI...'
}
}
const t = texts[language]
const tr = (key: string) => t(`strategyMarket.${key}`, language)
// Fetch public strategies
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
@@ -218,7 +138,7 @@ export function StrategyMarketPage() {
try {
await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2))
setCopiedId(strategy.id)
toast.success(t.copied)
toast.success(tr('copied'))
setTimeout(() => setCopiedId(null), 2000)
} catch (err) {
console.error('Failed to copy:', err)
@@ -271,16 +191,16 @@ export function StrategyMarketPage() {
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
</div>
<div>
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
{t.title}
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={tr('title')}>
{tr('title')}
</h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
// {t.subtitle}
// {tr('subtitle')}
</p>
</div>
</div>
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
{t.description}
{tr('description')}
</p>
</div>
@@ -295,7 +215,7 @@ export function StrategyMarketPage() {
</div>
<input
type="text"
placeholder={t.search}
placeholder={tr('search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
@@ -324,7 +244,7 @@ export function StrategyMarketPage() {
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
<span className="relative z-10">{tr(cat)}</span>
</button>
))}
</div>
@@ -340,7 +260,7 @@ export function StrategyMarketPage() {
<Cpu size={24} className="text-nofx-gold/50" />
</div>
</div>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.loading}</p>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{tr('loading')}</p>
<div className="flex gap-1">
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
@@ -357,9 +277,9 @@ export function StrategyMarketPage() {
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
</div>
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{t.noStrategies}]
[{tr('noStrategies')}]
</h3>
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
<p className="text-zinc-600 text-xs tracking-wide uppercase">{tr('noStrategiesDesc')}</p>
</div>
)}
@@ -423,11 +343,11 @@ export function StrategyMarketPage() {
{/* Meta Data */}
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
<div className="flex flex-col">
<span className="text-zinc-700 uppercase">{t.author}</span>
<span className="text-zinc-700 uppercase">{tr('author')}</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
</div>
<div className="flex flex-col text-right">
<span className="text-zinc-700 uppercase">{t.createdAt}</span>
<span className="text-zinc-700 uppercase">{tr('createdAt')}</span>
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
</div>
</div>
@@ -468,7 +388,7 @@ export function StrategyMarketPage() {
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
<EyeOff size={16} className="mb-1 opacity-50" />
<span className="text-[9px] uppercase tracking-widest">{t.configHiddenDesc}</span>
<span className="text-[9px] uppercase tracking-widest">{tr('configHiddenDesc')}</span>
</div>
)}
</div>
@@ -483,19 +403,19 @@ export function StrategyMarketPage() {
{copiedId === strategy.id ? (
<>
<Check className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-500">{t.copied}</span>
<span className="text-emerald-500">{tr('copied')}</span>
</>
) : (
<>
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
{t.copyConfig}
{tr('copyConfig')}
</>
)}
</button>
) : (
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
<Shield size={12} />
{t.hideConfig}
{tr('hideConfig')}
</button>
)}
</div>
@@ -521,7 +441,7 @@ export function StrategyMarketPage() {
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
<div className="text-left">
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.shareYours}</div>
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{tr('shareYours')}</div>
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
</div>
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>

View File

@@ -39,6 +39,7 @@ import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEdito
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
import { t } from '../i18n/translations'
const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -108,7 +109,7 @@ export function StrategyStudioPage() {
})
if (response.ok) {
const data = await response.json()
// 后端返回的是数组,不是 { models: [] }
// Backend returns an array, not { models: [] }
const allModels = Array.isArray(data) ? data : (data.models || [])
const enabledModels = allModels.filter((m: AIModel) => m.enabled)
setAiModels(enabledModels)
@@ -209,7 +210,7 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: language === 'zh' ? '新策略' : 'New Strategy',
name: tr('newStrategyName'),
description: '',
config: defaultConfig,
}),
@@ -222,7 +223,7 @@ export function StrategyStudioPage() {
const now = new Date().toISOString()
const newStrategy = {
id: result.id,
name: language === 'zh' ? '新策略' : 'New Strategy',
name: tr('newStrategyName'),
description: '',
is_active: false,
is_default: false,
@@ -246,11 +247,11 @@ export function StrategyStudioPage() {
if (!token) return
const confirmed = await confirmToast(
language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?',
tr('confirmDeleteStrategy'),
{
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
okText: language === 'zh' ? '删除' : 'Delete',
cancelText: language === 'zh' ? '取消' : 'Cancel',
title: tr('confirmDelete'),
okText: tr('delete'),
cancelText: tr('cancel'),
}
)
if (!confirmed) return
@@ -261,7 +262,7 @@ export function StrategyStudioPage() {
headers: { Authorization: `Bearer ${token}` },
})
if (!response.ok) throw new Error('Failed to delete strategy')
notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted')
notify.success(tr('strategyDeleted'))
// Clear selection if deleted strategy was selected
if (selectedStrategy?.id === id) {
setSelectedStrategy(null)
@@ -287,7 +288,7 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: language === 'zh' ? '策略副本' : 'Strategy Copy',
name: tr('strategyCopy'),
}),
})
if (!response.ok) throw new Error('Failed to duplicate strategy')
@@ -330,7 +331,7 @@ export function StrategyStudioPage() {
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
notify.success(language === 'zh' ? '策略已导出' : 'Strategy exported')
notify.success(tr('strategyExported'))
}
// Import strategy from JSON file
@@ -344,7 +345,7 @@ export function StrategyStudioPage() {
// Validate imported data
if (!importData.config || !importData.name) {
throw new Error(language === 'zh' ? '无效的策略文件' : 'Invalid strategy file')
throw new Error(tr('invalidStrategyFile'))
}
// Create new strategy with imported config
@@ -355,14 +356,14 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: `${importData.name} (${language === 'zh' ? '导入' : 'Imported'})`,
name: `${importData.name} (${tr('imported')})`,
description: importData.description || '',
config: importData.config,
}),
})
if (!response.ok) throw new Error('Failed to import strategy')
notify.success(language === 'zh' ? '策略已导入' : 'Strategy imported')
notify.success(tr('strategyImported'))
await fetchStrategies()
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
@@ -402,7 +403,7 @@ export function StrategyStudioPage() {
)
if (!response.ok) throw new Error('Failed to save strategy')
setHasChanges(false)
notify.success(language === 'zh' ? '策略已保存' : 'Strategy saved')
notify.success(tr('strategySaved'))
await fetchStrategies()
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
@@ -482,51 +483,7 @@ export function StrategyStudioPage() {
}
}
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
strategyStudio: { zh: '策略工作室', en: 'Strategy Studio' },
subtitle: { zh: '可视化配置和测试交易策略', en: 'Configure and test trading strategies' },
strategies: { zh: '策略', en: 'Strategies' },
newStrategy: { zh: '新建', en: 'New' },
strategyType: { zh: '策略类型', en: 'Strategy Type' },
aiTrading: { zh: 'AI 智能交易', en: 'AI Trading' },
aiTradingDesc: { zh: 'AI 分析市场并自主决策买卖', en: 'AI analyzes market and makes trading decisions' },
gridTrading: { zh: 'AI 网格交易', en: 'AI Grid Trading' },
gridTradingDesc: { zh: 'AI 控制网格策略,在震荡市场获利', en: 'AI-controlled grid strategy for ranging markets' },
gridConfig: { zh: '网格配置', en: 'Grid Configuration' },
coinSource: { zh: '币种来源', en: 'Coin Source' },
indicators: { zh: '技术指标', en: 'Indicators' },
riskControl: { zh: '风控参数', en: 'Risk Control' },
promptSections: { zh: 'Prompt 编辑', en: 'Prompt Editor' },
customPrompt: { zh: '附加提示', en: 'Extra Prompt' },
save: { zh: '保存', en: 'Save' },
saving: { zh: '保存中...', en: 'Saving...' },
activate: { zh: '激活', en: 'Activate' },
active: { zh: '激活中', en: 'Active' },
default: { zh: '默认', en: 'Default' },
promptPreview: { zh: 'Prompt 预览', en: 'Prompt Preview' },
aiTestRun: { zh: 'AI 测试', en: 'AI Test' },
systemPrompt: { zh: 'System Prompt', en: 'System Prompt' },
userPrompt: { zh: 'User Prompt', en: 'User Prompt' },
loadPrompt: { zh: '生成 Prompt', en: 'Generate Prompt' },
refreshPrompt: { zh: '刷新', en: 'Refresh' },
promptVariant: { zh: '风格', en: 'Style' },
balanced: { zh: '平衡', en: 'Balanced' },
aggressive: { zh: '激进', en: 'Aggressive' },
conservative: { zh: '保守', en: 'Conservative' },
selectModel: { zh: '选择 AI 模型', en: 'Select AI Model' },
runTest: { zh: '运行 AI 测试', en: 'Run AI Test' },
running: { zh: '运行中...', en: 'Running...' },
aiOutput: { zh: 'AI 输出', en: 'AI Output' },
reasoning: { zh: '思维链', en: 'Reasoning' },
decisions: { zh: '决策', en: 'Decisions' },
duration: { zh: '耗时', en: 'Duration' },
noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' },
testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' },
publishSettings: { zh: '发布设置', en: 'Publish' },
}
return translations[key]?.[language] || key
}
const tr = (key: string) => t(`strategyStudio.${key}`, language)
if (isLoading) {
return (
@@ -550,7 +507,7 @@ export function StrategyStudioPage() {
key: 'gridConfig' as const,
icon: Activity,
color: '#0ECB81',
title: t('gridConfig'),
title: tr('gridConfig'),
forStrategyType: 'grid_trading' as const,
content: editingConfig?.grid_config && (
<GridConfigEditor
@@ -566,7 +523,7 @@ export function StrategyStudioPage() {
key: 'coinSource' as const,
icon: Target,
color: '#F0B90B',
title: t('coinSource'),
title: tr('coinSource'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<CoinSourceEditor
@@ -581,7 +538,7 @@ export function StrategyStudioPage() {
key: 'indicators' as const,
icon: BarChart3,
color: '#0ECB81',
title: t('indicators'),
title: tr('indicators'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<IndicatorEditor
@@ -596,7 +553,7 @@ export function StrategyStudioPage() {
key: 'riskControl' as const,
icon: Shield,
color: '#F6465D',
title: t('riskControl'),
title: tr('riskControl'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<RiskControlEditor
@@ -611,7 +568,7 @@ export function StrategyStudioPage() {
key: 'promptSections' as const,
icon: FileText,
color: '#a855f7',
title: t('promptSections'),
title: tr('promptSections'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<PromptSectionsEditor
@@ -626,18 +583,18 @@ export function StrategyStudioPage() {
key: 'customPrompt' as const,
icon: Settings,
color: '#60a5fa',
title: t('customPrompt'),
title: tr('customPrompt'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<div>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{language === 'zh' ? '附加在 System Prompt 末尾的额外提示,用于补充个性化交易风格' : 'Extra prompt appended to System Prompt for personalized trading style'}
{tr('customPromptDesc')}
</p>
<textarea
value={editingConfig.custom_prompt || ''}
onChange={(e) => updateConfig('custom_prompt', e.target.value)}
disabled={selectedStrategy?.is_default}
placeholder={language === 'zh' ? '输入自定义提示词...' : 'Enter custom prompt...'}
placeholder={tr('customPromptPlaceholder')}
className="w-full h-32 px-3 py-2 rounded-lg resize-none font-mono text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
@@ -648,7 +605,7 @@ export function StrategyStudioPage() {
key: 'publishSettings' as const,
icon: Globe,
color: '#0ECB81',
title: t('publishSettings'),
title: tr('publishSettings'),
forStrategyType: 'both' as const,
content: selectedStrategy && (
<PublishSettingsEditor
@@ -683,8 +640,8 @@ export function StrategyStudioPage() {
<Sparkles className="w-5 h-5 text-black" />
</div>
<div>
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1>
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p>
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
</div>
</div>
{error && (
@@ -702,10 +659,10 @@ export function StrategyStudioPage() {
<div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10">
<div className="p-2">
<div className="flex items-center justify-between mb-2 px-2">
<span className="text-xs font-medium text-nofx-text-muted">{t('strategies')}</span>
<span className="text-xs font-medium text-nofx-text-muted">{tr('strategies')}</span>
<div className="flex items-center gap-1">
{/* Import button with hidden file input */}
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={tr('importStrategy')}>
<Upload className="w-4 h-4" />
<input
type="file"
@@ -717,7 +674,7 @@ export function StrategyStudioPage() {
<button
onClick={handleCreateStrategy}
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
title={language === 'zh' ? '新建策略' : 'New Strategy'}
title={tr('newStrategyTooltip')}
>
<Plus className="w-4 h-4" />
</button>
@@ -745,7 +702,7 @@ export function StrategyStudioPage() {
<button
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
title={language === 'zh' ? '导出' : 'Export'}
title={tr('export')}
>
<Download className="w-3 h-3" />
</button>
@@ -754,14 +711,14 @@ export function StrategyStudioPage() {
<button
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
title={language === 'zh' ? '复制' : 'Duplicate'}
title={tr('duplicate')}
>
<Copy className="w-3 h-3" />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
title={language === 'zh' ? '删除' : 'Delete'}
title={tr('deleteTooltip')}
>
<Trash2 className="w-3 h-3" />
</button>
@@ -772,18 +729,18 @@ export function StrategyStudioPage() {
<div className="flex items-center gap-1 mt-1 flex-wrap">
{strategy.is_active && (
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
{t('active')}
{tr('active')}
</span>
)}
{strategy.is_default && (
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
{t('default')}
{tr('default')}
</span>
)}
{strategy.is_public && (
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400">
<Globe className="w-2.5 h-2.5" />
{language === 'zh' ? '公开' : 'Public'}
{tr('public')}
</span>
)}
</div>
@@ -818,11 +775,11 @@ export function StrategyStudioPage() {
setHasChanges(true)
}}
disabled={selectedStrategy.is_default}
placeholder={language === 'zh' ? '添加策略简介...' : 'Add strategy description...'}
placeholder={tr('addDescription')}
className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1"
/>
{hasChanges && (
<span className="text-xs text-nofx-gold"> {language === 'zh' ? '未保存' : 'Unsaved'}</span>
<span className="text-xs text-nofx-gold"> {tr('unsaved')}</span>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
@@ -832,7 +789,7 @@ export function StrategyStudioPage() {
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20"
>
<Check className="w-3 h-3" />
{t('activate')}
{tr('activate')}
</button>
)}
{!selectedStrategy.is_default && (
@@ -843,7 +800,7 @@ export function StrategyStudioPage() {
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
>
<Save className="w-3 h-3" />
{isSaving ? t('saving') : t('save')}
{isSaving ? tr('saving') : tr('save')}
</button>
)}
</div>
@@ -854,7 +811,7 @@ export function StrategyStudioPage() {
<div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20">
<div className="flex items-center gap-2 mb-3">
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium text-nofx-text">{t('strategyType')}</span>
<span className="text-sm font-medium text-nofx-text">{tr('strategyType')}</span>
</div>
<div className="grid grid-cols-2 gap-3">
<button
@@ -874,9 +831,9 @@ export function StrategyStudioPage() {
>
<div className="flex items-center gap-2 mb-1">
<Bot className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium text-nofx-text">{t('aiTrading')}</span>
<span className="text-sm font-medium text-nofx-text">{tr('aiTrading')}</span>
</div>
<p className="text-xs text-nofx-text-muted text-left">{t('aiTradingDesc')}</p>
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
</button>
<button
onClick={() => {
@@ -897,9 +854,9 @@ export function StrategyStudioPage() {
>
<div className="flex items-center gap-2 mb-1">
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
<span className="text-sm font-medium text-nofx-text">{t('gridTrading')}</span>
<span className="text-sm font-medium text-nofx-text">{tr('gridTrading')}</span>
</div>
<p className="text-xs text-nofx-text-muted text-left">{t('gridTradingDesc')}</p>
<p className="text-xs text-nofx-text-muted text-left">{tr('gridTradingDesc')}</p>
</button>
</div>
</div>
@@ -940,7 +897,7 @@ export function StrategyStudioPage() {
<div className="text-center">
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
<p className="text-sm text-nofx-text-muted">
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
{tr('selectOrCreate')}
</p>
</div>
</div>
@@ -957,7 +914,7 @@ export function StrategyStudioPage() {
}`}
>
<Eye className="w-4 h-4" />
{t('promptPreview')}
{tr('promptPreview')}
</button>
<button
onClick={() => setActiveRightTab('test')}
@@ -965,7 +922,7 @@ export function StrategyStudioPage() {
}`}
>
<Play className="w-4 h-4" />
{t('aiTestRun')}
{tr('aiTestRun')}
</button>
</div>
@@ -981,9 +938,9 @@ export function StrategyStudioPage() {
onChange={(e) => setSelectedVariant(e.target.value)}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
>
<option value="balanced">{t('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option>
<option value="conservative">{t('conservative')}</option>
<option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{tr('conservative')}</option>
</select>
<button
onClick={fetchPromptPreview}
@@ -991,7 +948,7 @@ export function StrategyStudioPage() {
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white"
>
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
{promptPreview ? t('refreshPrompt') : t('loadPrompt')}
{promptPreview ? tr('refreshPrompt') : tr('loadPrompt')}
</button>
</div>
@@ -1018,7 +975,7 @@ export function StrategyStudioPage() {
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5">
<FileText className="w-3 h-3 text-purple-500" />
<span className="text-xs font-medium text-nofx-text">{t('systemPrompt')}</span>
<span className="text-xs font-medium text-nofx-text">{tr('systemPrompt')}</span>
</div>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted">
{promptPreview.system_prompt.length.toLocaleString()} chars
@@ -1035,7 +992,7 @@ export function StrategyStudioPage() {
) : (
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
<Eye className="w-10 h-10 mb-2 opacity-30" />
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
<p className="text-sm">{tr('generatePromptPreview')}</p>
</div>
)}
</div>
@@ -1046,7 +1003,7 @@ export function StrategyStudioPage() {
<div className="space-y-2">
<div className="flex items-center gap-2">
<Bot className="w-4 h-4 text-green-500" />
<span className="text-xs font-medium text-nofx-text">{t('selectModel')}</span>
<span className="text-xs font-medium text-nofx-text">{tr('selectModel')}</span>
</div>
{aiModels.length > 0 ? (
<select
@@ -1062,7 +1019,7 @@ export function StrategyStudioPage() {
</select>
) : (
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
{t('noModel')}
{tr('noModel')}
</div>
)}
@@ -1072,9 +1029,9 @@ export function StrategyStudioPage() {
onChange={(e) => setSelectedVariant(e.target.value)}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
>
<option value="balanced">{t('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option>
<option value="conservative">{t('conservative')}</option>
<option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{tr('conservative')}</option>
</select>
<button
onClick={runAiTest}
@@ -1084,17 +1041,17 @@ export function StrategyStudioPage() {
{isRunningAiTest ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{t('running')}
{tr('running')}
</>
) : (
<>
<Send className="w-4 h-4" />
{t('runTest')}
{tr('runTest')}
</>
)}
</button>
</div>
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p>
<p className="text-[10px] text-nofx-text-muted">{tr('testNote')}</p>
</div>
{/* Test Results */}
@@ -1110,7 +1067,7 @@ export function StrategyStudioPage() {
<div className="flex items-center gap-2">
<Clock className="w-3 h-3 text-nofx-text-muted" />
<span className="text-xs text-nofx-text-muted">
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
{tr('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
</span>
</div>
)}
@@ -1120,7 +1077,7 @@ export function StrategyStudioPage() {
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<Terminal className="w-3 h-3 text-blue-400" />
<span className="text-xs font-medium text-nofx-text">{t('userPrompt')} (Input)</span>
<span className="text-xs font-medium text-nofx-text">{tr('userPrompt')} (Input)</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
@@ -1136,7 +1093,7 @@ export function StrategyStudioPage() {
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<Sparkles className="w-3 h-3 text-nofx-gold" />
<span className="text-xs font-medium text-nofx-text">{t('reasoning')}</span>
<span className="text-xs font-medium text-nofx-text">{tr('reasoning')}</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text"
@@ -1152,7 +1109,7 @@ export function StrategyStudioPage() {
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<Activity className="w-3 h-3 text-green-500" />
<span className="text-xs font-medium text-nofx-text">{t('decisions')}</span>
<span className="text-xs font-medium text-nofx-text">{tr('decisions')}</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
@@ -1168,7 +1125,7 @@ export function StrategyStudioPage() {
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<FileText className="w-3 h-3 text-nofx-text-muted" />
<span className="text-xs font-medium text-nofx-text">{t('aiOutput')} (Raw)</span>
<span className="text-xs font-medium text-nofx-text">{tr('aiOutput')} (Raw)</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
@@ -1184,7 +1141,7 @@ export function StrategyStudioPage() {
) : (
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
<Play className="w-10 h-10 mb-2 opacity-30" />
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
<p className="text-sm">{tr('runAiTestHint')}</p>
</div>
)}
</div>

View File

@@ -23,7 +23,7 @@ import type {
// --- Helper Functions ---
// 获取友好的AI模型名称
// Get friendly AI model display name
function getModelDisplayName(modelId: string): string {
switch (modelId.toLowerCase()) {
case 'deepseek':
@@ -189,19 +189,17 @@ export function TraderDashboardPage({
}, 100)
}
// 平仓操作
// Close position handler
const handleClosePosition = async (symbol: string, side: string) => {
if (!selectedTraderId) return
const confirmMsg =
language === 'zh'
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
const sideLabel = side === 'LONG' ? 'LONG' : 'SHORT'
const confirmMsg = t('traderDashboard.confirmClosePosition', language, { symbol, side: sideLabel })
const confirmed = await confirmToast(confirmMsg, {
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
okText: language === 'zh' ? '确认' : 'Confirm',
cancelText: language === 'zh' ? '取消' : 'Cancel',
title: t('traderDashboard.confirmClose', language),
okText: t('traderDashboard.confirm', language),
cancelText: t('traderDashboard.cancel', language),
})
if (!confirmed) return
@@ -209,10 +207,8 @@ export function TraderDashboardPage({
setClosingPosition(symbol)
try {
await api.closePosition(selectedTraderId, symbol, side)
notify.success(
language === 'zh' ? '平仓成功' : 'Position closed successfully'
)
// 使用 SWR mutate 刷新数据而非重新加载页面
notify.success(t('traderDashboard.positionClosed', language))
// Use SWR mutate to refresh data instead of reloading page
await Promise.all([
mutate(`positions-${selectedTraderId}`),
mutate(`account-${selectedTraderId}`),
@@ -221,9 +217,7 @@ export function TraderDashboardPage({
const errorMsg =
err instanceof Error
? err.message
: language === 'zh'
? '平仓失败'
: 'Failed to close position'
: t('traderDashboard.closeFailed', language)
notify.error(errorMsg)
} finally {
setClosingPosition(null)
@@ -257,18 +251,16 @@ export function TraderDashboardPage({
</svg>
</div>
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'}
{t('traderDashboard.connectionFailed', language)}
</h2>
<p className="text-base mb-6 text-nofx-text-muted">
{language === 'zh'
? '请确认后端服务已启动。'
: 'Please check if the backend service is running.'}
{t('traderDashboard.connectionFailedDesc', language)}
</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
>
{language === 'zh' ? '重试' : 'Retry'}
{t('traderDashboard.retry', language)}
</button>
</div>
</div>
@@ -414,12 +406,8 @@ export function TraderDashboardPage({
className="p-1 rounded hover:bg-white/10 transition-colors"
title={
showWalletAddress
? language === 'zh'
? '隐藏地址'
: 'Hide address'
: language === 'zh'
? '显示完整地址'
: 'Show full address'
? t('traderDashboard.hideAddress', language)
: t('traderDashboard.showFullAddress', language)
}
>
{showWalletAddress ? (
@@ -432,7 +420,7 @@ export function TraderDashboardPage({
type="button"
onClick={handleCopyAddress}
className="p-1 rounded hover:bg-white/10 transition-colors"
title={language === 'zh' ? '复制地址' : 'Copy address'}
title={t('traderDashboard.copyAddress', language)}
>
{copiedAddress ? (
<Check className="w-3.5 h-3.5 text-nofx-green" />
@@ -443,7 +431,7 @@ export function TraderDashboardPage({
</>
) : (
<span className="text-xs text-nofx-text-muted">
{language === 'zh' ? '未配置地址' : 'No address configured'}
{t('traderDashboard.noAddressConfigured', language)}
</span>
)}
</div>
@@ -599,14 +587,14 @@ export function TraderDashboardPage({
<tr>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{language === 'zh' ? '操作' : 'Action'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{language === 'zh' ? '入场价' : 'Entry'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{language === 'zh' ? '标记价' : 'Mark'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{language === 'zh' ? '数量' : 'Qty'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{language === 'zh' ? '价值' : 'Value'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{language === 'zh' ? '杠杆' : 'Lev.'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{language === 'zh' ? '未实现盈亏' : 'uPnL'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{language === 'zh' ? '强平价' : 'Liq.'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('traderDashboard.action', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{t('traderDashboard.entry', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{t('traderDashboard.mark', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{t('traderDashboard.qty', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{t('traderDashboard.value', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{t('traderDashboard.lev', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{t('traderDashboard.uPnL', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{t('traderDashboard.liq', language)}</th>
</tr>
</thead>
<tbody>
@@ -644,14 +632,14 @@ export function TraderDashboardPage({
}}
disabled={closingPosition === pos.symbol}
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20"
title={language === 'zh' ? '平仓' : 'Close Position'}
title={t('traderDashboard.closePosition', language)}
>
{closingPosition === pos.symbol ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<LogOut className="w-3 h-3" />
)}
{language === 'zh' ? '平仓' : 'Close'}
{t('traderDashboard.close', language)}
</button>
</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td>
@@ -678,13 +666,11 @@ export function TraderDashboardPage({
{totalPositions > 10 && (
<div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted">
<span>
{language === 'zh'
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
{t('traderDashboard.showingPositions', language, { shown: paginatedPositions.length, total: totalPositions })}
</span>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span>{language === 'zh' ? '每页' : 'Per page'}:</span>
<span>{t('traderDashboard.perPage', language)}:</span>
<select
value={positionsPageSize}
onChange={(e) => setPositionsPageSize(Number(e.target.value))}