fix: improve trading and UI

This commit is contained in:
tinkle-community
2025-12-11 00:47:12 +08:00
parent e9e60c82cb
commit 19937ee260
13 changed files with 328 additions and 76 deletions

View File

@@ -25,6 +25,8 @@ import {
Plus,
Users,
Pencil,
Eye,
EyeOff,
} from 'lucide-react'
import { confirmToast } from '../lib/notify'
import { toast } from 'sonner'
@@ -337,6 +339,23 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
}
const handleToggleCompetition = async (traderId: string, currentShowInCompetition: boolean) => {
try {
const newValue = !currentShowInCompetition
await toast.promise(api.toggleCompetition(traderId, newValue), {
loading: '正在更新…',
success: newValue ? '已在竞技场显示' : '已在竞技场隐藏',
error: '更新失败',
})
// Immediately refresh traders list to update status
await mutateTraders()
} catch (error) {
console.error('Failed to toggle competition visibility:', error)
toast.error(t('operationFailed', language))
}
}
const handleModelClick = (modelId: string) => {
if (!isModelInUse(modelId)) {
setEditingModel(modelId)
@@ -1069,6 +1088,29 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
: t('start', language)}
</button>
<button
onClick={() => handleToggleCompetition(trader.trader_id, trader.show_in_competition ?? true)}
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 whitespace-nowrap flex items-center gap-1"
style={
trader.show_in_competition !== false
? {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
}
: {
background: 'rgba(132, 142, 156, 0.1)',
color: '#848E9C',
}
}
title={trader.show_in_competition !== false ? '在竞技场显示' : '在竞技场隐藏'}
>
{trader.show_in_competition !== false ? (
<Eye className="w-3 h-3 md:w-4 md:h-4" />
) : (
<EyeOff className="w-3 h-3 md:w-4 md:h-4" />
)}
</button>
<button
onClick={() => handleDeleteTrader(trader.trader_id)}
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105"

View File

@@ -32,6 +32,7 @@ interface FormState {
exchange_id: string
strategy_id: string
is_cross_margin: boolean
show_in_competition: boolean
scan_interval_minutes: number
initial_balance?: number
}
@@ -62,6 +63,7 @@ export function TraderConfigModal({
exchange_id: '',
strategy_id: '',
is_cross_margin: true,
show_in_competition: true,
scan_interval_minutes: 3,
})
const [isSaving, setIsSaving] = useState(false)
@@ -109,6 +111,7 @@ export function TraderConfigModal({
exchange_id: availableExchanges[0]?.id || '',
strategy_id: '',
is_cross_margin: true,
show_in_competition: true,
scan_interval_minutes: 3,
})
}
@@ -162,6 +165,7 @@ export function TraderConfigModal({
exchange_id: formData.exchange_id,
strategy_id: formData.strategy_id || undefined,
is_cross_margin: formData.is_cross_margin,
show_in_competition: formData.show_in_competition,
scan_interval_minutes: formData.scan_interval_minutes,
}
@@ -439,6 +443,40 @@ export function TraderConfigModal({
</div>
</div>
{/* Competition visibility */}
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleInputChange('show_in_competition', true)}
className={`flex-1 px-3 py-2 rounded text-sm ${
formData.show_in_competition
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
}`}
>
</button>
<button
type="button"
onClick={() => handleInputChange('show_in_competition', false)}
className={`flex-1 px-3 py-2 rounded text-sm ${
!formData.show_in_competition
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
}`}
>
</button>
</div>
<p className="text-xs text-[#848E9C] mt-1">
</p>
</div>
{/* Initial Balance (Edit mode only) */}
{isEditMode && (
<div>

View File

@@ -11,20 +11,20 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
{
icon: Download,
number: '01',
title: language === 'zh' ? '克隆项目' : 'Clone Project',
title: language === 'zh' ? '一键部署' : 'One-Click Deploy',
desc: language === 'zh'
? 'git clone 项目到本地'
: 'git clone the project locally',
code: 'git clone https://github.com/NoFxAiOS/nofx.git',
? '在你的服务器上运行一条命令即可完成部署'
: 'Run a single command on your server to deploy',
code: 'curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash',
},
{
icon: Rocket,
number: '02',
title: language === 'zh' ? '启动服务' : 'Start Service',
title: language === 'zh' ? '访问面板' : 'Access Dashboard',
desc: language === 'zh'
? 'Docker 一键启动所有服务'
: 'Docker one-click start all services',
code: './start.sh start --build',
? '通过浏览器访问你的服务'
: 'Access your server via browser',
code: 'http://YOUR_SERVER_IP:3000',
},
{
icon: TrendingUp,
@@ -33,7 +33,7 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
desc: language === 'zh'
? '创建交易员,让 AI 开始工作'
: 'Create trader, let AI do the work',
code: 'http://localhost:3000',
code: language === 'zh' ? '配置模型 → 配置交易所 → 创建交易员' : 'Configure Model → Exchange → Create Trader',
},
]

View File

@@ -1,4 +1,4 @@
import { Bot, BarChart3, Trash2, Pencil } from 'lucide-react'
import { Bot, BarChart3, Trash2, Pencil, Eye, EyeOff } from 'lucide-react'
import { t, type Language } from '../../../i18n/translations'
import { getModelDisplayName } from '../index'
import type { TraderInfo, Exchange } from '../../../types'
@@ -12,6 +12,7 @@ interface TradersGridProps {
onEditTrader: (traderId: string) => void
onDeleteTrader: (traderId: string) => void
onToggleTrader: (traderId: string, running: boolean) => void
onToggleCompetition?: (traderId: string, showInCompetition: boolean) => void
}
export function TradersGrid({
@@ -22,6 +23,7 @@ export function TradersGrid({
onEditTrader,
onDeleteTrader,
onToggleTrader,
onToggleCompetition,
}: TradersGridProps) {
// Helper function to get exchange display name
const getExchangeDisplayName = (exchangeId: string | undefined) => {
@@ -166,6 +168,31 @@ export function TradersGrid({
{trader.is_running ? t('stop', language) : t('start', language)}
</button>
{onToggleCompetition && (
<button
onClick={() => onToggleCompetition(trader.trader_id, trader.show_in_competition ?? true)}
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 whitespace-nowrap flex items-center gap-1"
style={
trader.show_in_competition !== false
? {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
}
: {
background: 'rgba(132, 142, 156, 0.1)',
color: '#848E9C',
}
}
title={trader.show_in_competition !== false ? '在竞技场显示' : '在竞技场隐藏'}
>
{trader.show_in_competition !== false ? (
<Eye className="w-3 h-3 md:w-4 md:h-4" />
) : (
<EyeOff className="w-3 h-3 md:w-4 md:h-4" />
)}
</button>
)}
<button
onClick={() => onDeleteTrader(trader.trader_id)}
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105"