mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 23:36:55 +08:00
feat: improve trading UI with interactive position table and chart tabs
- Add clickable position rows that scroll to chart and update symbol - Add framer-motion animations to chart tab transitions - Sync exchange selection between positions and TradingView chart - Optimize position table layout with compact styling - Update CSS with glass effects and premium button styles
This commit is contained in:
117
web/src/App.tsx
117
web/src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
import { api } from './lib/api'
|
||||
import { ChartTabs } from './components/ChartTabs'
|
||||
@@ -527,6 +527,9 @@ function TraderDetailsPage({
|
||||
language: Language
|
||||
}) {
|
||||
const [closingPosition, setClosingPosition] = useState<string | null>(null)
|
||||
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>(undefined)
|
||||
const [chartUpdateKey, setChartUpdateKey] = useState<number>(0)
|
||||
const chartSectionRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 平仓操作
|
||||
const handleClosePosition = async (symbol: string, side: string) => {
|
||||
@@ -770,7 +773,7 @@ function TraderDetailsPage({
|
||||
>
|
||||
{getModelDisplayName(
|
||||
selectedTrader.ai_model.split('_').pop() ||
|
||||
selectedTrader.ai_model
|
||||
selectedTrader.ai_model
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -832,8 +835,17 @@ function TraderDetailsPage({
|
||||
{/* 左侧:图表 + 持仓 */}
|
||||
<div className="space-y-6">
|
||||
{/* Chart Tabs (Equity / K-line) */}
|
||||
<div className="animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||
<ChartTabs traderId={selectedTrader.trader_id} />
|
||||
<div
|
||||
ref={chartSectionRef}
|
||||
className="chart-container animate-slide-in scroll-mt-32"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<ChartTabs
|
||||
traderId={selectedTrader.trader_id}
|
||||
selectedSymbol={selectedChartSymbol}
|
||||
updateKey={chartUpdateKey}
|
||||
exchangeId={selectedTrader.exchange_id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current Positions */}
|
||||
@@ -863,38 +875,38 @@ function TraderDetailsPage({
|
||||
</div>
|
||||
{positions && positions.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-left border-b border-gray-800">
|
||||
<tr>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-left">
|
||||
{t('symbol', language)}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center">
|
||||
{t('side', language)}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center">
|
||||
{language === 'zh' ? '操作' : 'Action'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('entryPrice', language)}
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('entryPrice', language)}>
|
||||
{language === 'zh' ? '入场价' : 'Entry'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('markPrice', language)}
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('markPrice', language)}>
|
||||
{language === 'zh' ? '标记价' : 'Mark'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('quantity', language)}
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('quantity', language)}>
|
||||
{language === 'zh' ? '数量' : 'Qty'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('positionValue', language)}
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('positionValue', language)}>
|
||||
{language === 'zh' ? '价值' : 'Value'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('leverage', language)}
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center" title={t('leverage', language)}>
|
||||
{language === 'zh' ? '杠杆' : 'Lev.'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('unrealizedPnL', language)}
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>
|
||||
{language === 'zh' ? '未实现盈亏' : 'uPnL'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('liqPrice', language)}
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('liqPrice', language)}>
|
||||
{language === 'zh' ? '强平价' : 'Liq.'}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -902,24 +914,32 @@ function TraderDetailsPage({
|
||||
{positions.map((pos, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className="border-b border-gray-800 last:border-0"
|
||||
className="border-b border-gray-800 last:border-0 transition-colors hover:bg-opacity-10 hover:bg-yellow-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedChartSymbol(pos.symbol)
|
||||
setChartUpdateKey(Date.now())
|
||||
// Smooth scroll to chart with ref
|
||||
if (chartSectionRef.current) {
|
||||
chartSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className="py-3 font-mono font-semibold">
|
||||
<td className="px-1 py-3 font-mono font-semibold whitespace-nowrap text-left">
|
||||
{pos.symbol}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<span
|
||||
className="px-2 py-1 rounded text-xs font-bold"
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-bold"
|
||||
style={
|
||||
pos.side === 'long'
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}
|
||||
}
|
||||
>
|
||||
{t(
|
||||
@@ -928,17 +948,15 @@ function TraderDetailsPage({
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleClosePosition(pos.symbol, pos.side.toUpperCase())}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
border: '1px solid rgba(246, 70, 93, 0.3)',
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent row click
|
||||
handleClosePosition(pos.symbol, pos.side.toUpperCase())
|
||||
}}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="btn-danger inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto"
|
||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
@@ -950,36 +968,36 @@ function TraderDetailsPage({
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{pos.entry_price.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{pos.mark_price.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{pos.quantity.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono font-bold"
|
||||
className="px-1 py-3 font-mono font-bold whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{(pos.quantity * pos.mark_price).toFixed(2)} USDT
|
||||
{(pos.quantity * pos.mark_price).toFixed(2)}
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-center"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{pos.leverage}x
|
||||
</td>
|
||||
<td className="py-3 font-mono">
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right">
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
@@ -988,12 +1006,11 @@ function TraderDetailsPage({
|
||||
}}
|
||||
>
|
||||
{pos.unrealized_pnl >= 0 ? '+' : ''}
|
||||
{pos.unrealized_pnl.toFixed(2)} (
|
||||
{pos.unrealized_pnl_pct.toFixed(2)}%)
|
||||
{pos.unrealized_pnl.toFixed(2)}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
{pos.liquidation_price.toFixed(4)}
|
||||
|
||||
Reference in New Issue
Block a user