commit b5d8d4e71b19c8884de06a345835f98d4dbedff7 Author: ZhangPeng <592658577@qq.com> Date: Thu Feb 26 22:36:31 2026 +0800 feat: implement UI design system unification - Add design-system tokens (colors, spacing, typography, radii, shadows) - Add cn utility for class merging - Refactor StatCard to dark theme (fixes light theme issue) - Refactor PositionTable to use Tailwind CSS (fixes inline styles and color inconsistency) - Remove duplicate AgentTable from ui/ - Update AgentTable in agents/ to use AgentStatus type - Update AgentStatus type to include all status values - Update mockData to match AgentStatus type Co-Authored-By: Claude Sonnet 4.6 diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..22e2fb2 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { DollarSign, TrendingUp, Activity, Bot } from "lucide-react"; + +import { MetricCard } from "@/components/ui/MetricCard"; +import { AgentTable } from "@/components/agents/AgentTable"; +import { TradeList } from "@/components/ui/TradeList"; +import { EquityChart } from "@/components/ui/EquityChart"; +import { PnlDistributionChart } from "@/components/ui/PnlDistributionChart"; +import { getMetrics, getEquityCurve, getPnlDistribution } from "@/api/metrics"; +import { getAgents } from "@/api/agents"; +import { getRecentTrades } from "@/api/trades"; +import type { + AgentStatus, + TradeRecord, + Metrics, + EquityPoint, + PnlDistribution, +} from "@/types/models"; + +export default function Dashboard() { + const [metrics, setMetrics] = useState(null); + const [equityData, setEquityData] = useState([]); + const [pnlDistribution, setPnlDistribution] = useState([]); + const [agents, setAgents] = useState([]); + const [trades, setTrades] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchData() { + try { + setLoading(true); + const [metricsRes, equityRes, pnlRes, agentsRes, tradesRes] = + await Promise.all([ + getMetrics(), + getEquityCurve(), + getPnlDistribution(), + getAgents(), + getRecentTrades(10), + ]); + setMetrics(metricsRes); + setEquityData(equityRes); + setPnlDistribution(pnlRes); + setAgents(agentsRes); + setTrades(tradesRes); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch data"); + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); + + if (loading) { + return ( +
+
+ Loading dashboard data... +
+
+ ); + } + + if (error) { + return ( +
+
Error: {error}
+
+ ); + } + + return ( +
+ {/* Metric Cards */} +
+ = 0 ? "positive" : "negative" + } + icon={DollarSign} + /> + = 0 ? "positive" : "negative" + } + icon={TrendingUp} + /> + + +
+ + {/* Charts */} +
+
+ +
+
+ +
+
+ + {/* Tables */} +
+
+ +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/agents/AgentTable.tsx b/frontend/src/components/agents/AgentTable.tsx new file mode 100644 index 0000000..c952e3c --- /dev/null +++ b/frontend/src/components/agents/AgentTable.tsx @@ -0,0 +1,131 @@ +'use client'; + +import { Play, Pause, Square, Bot } from "lucide-react"; +import { cn } from "@/design-system/utils/cn"; +import type { AgentStatus } from "@/types/models"; + +interface AgentTableProps { + agents: AgentStatus[]; +} + +const statusConfig: Record< + string, + { color: string; icon: typeof Play; label: string; textColor: string } +> = { + running: { color: "bg-[#00ffcc]", icon: Play, label: "RUNNING", textColor: "text-[#00ffcc]" }, + active: { color: "bg-[#00ffcc]", icon: Play, label: "ACTIVE", textColor: "text-[#00ffcc]" }, + paused: { color: "bg-[#ffb74d]", icon: Pause, label: "PAUSED", textColor: "text-[#ffb74d]" }, + resting: { color: "bg-[#ffb74d]", icon: Pause, label: "RESTING", textColor: "text-[#ffb74d]" }, + stopped: { color: "bg-[#666666]", icon: Square, label: "STOPPED", textColor: "text-[#666666]" }, + error: { color: "bg-[#ff5252]", icon: Square, label: "ERROR", textColor: "text-[#ff5252]" }, + danger: { color: "bg-[#ff5252]", icon: Square, label: "DANGER", textColor: "text-[#ff5252]" }, + bankrupt: { color: "bg-[#ff5252]", icon: Square, label: "BANKRUPT", textColor: "text-[#ff5252]" }, +}; + +export function AgentTable({ agents }: AgentTableProps) { + return ( +
+
+

+ + Deployment Matrix +

+ + {agents.length} Active Nodes + +
+
+ + + + + + + + + + + + + {agents.map((agent) => { + const status = statusConfig[agent.status] || { + color: "bg-[#666666]", + icon: Square, + label: agent.status.toUpperCase(), + textColor: "text-[#666666]" + }; + const StatusIcon = status.icon; + + return ( + + + + + + + + + ); + })} + +
+ Unit + + Status + + Logic + + Asset + + P&L + + OPS +
+
+

+ {agent.name} +

+

{agent.id ? agent.id.slice(0, 12) : 'Unknown ID'}

+
+
+
+ + + {status.label} + +
+
+ + {agent.strategy} + + + + {agent.symbol} + + +
+

= 0 + ? "text-[#00ffcc]" + : "text-[#ff5252]", + )} + > + {agent.profitLoss >= 0 ? "+" : ""} + {agent.profitLoss?.toFixed(2) ?? "0.00"} +

+
+
+ + {agent.tradesCount} + +
+
+
+ ); +} + +export default AgentTable; diff --git a/frontend/src/components/agents/mockData.ts b/frontend/src/components/agents/mockData.ts new file mode 100644 index 0000000..e1bc1de --- /dev/null +++ b/frontend/src/components/agents/mockData.ts @@ -0,0 +1,71 @@ +import type { AgentStatus } from "@/types/models"; + +// 虚拟数据 +export const mockAgents: AgentStatus[] = [ + { + id: "AGT-001", + name: "Alpha Trader", + status: "running", + strategy: "Momentum", + symbol: "BTC/USDT", + profitLoss: 2580.5, + profitLossPercent: 25.8, + tradesCount: 156, + lastUpdated: new Date().toISOString(), + }, + { + id: "AGT-002", + name: "ETH Scalper", + status: "active", + strategy: "Scalping", + symbol: "ETH/USDT", + profitLoss: 920.25, + profitLossPercent: 9.2, + tradesCount: 89, + lastUpdated: new Date().toISOString(), + }, + { + id: "AGT-003", + name: "Range Bot", + status: "resting", + strategy: "Mean Reversion", + symbol: "SOL/USDT", + profitLoss: -360.0, + profitLossPercent: -3.6, + tradesCount: 45, + lastUpdated: new Date().toISOString(), + }, + { + id: "AGT-004", + name: "Danger Bot", + status: "danger", + strategy: "Trend Following", + symbol: "DOGE/USDT", + profitLoss: -899.25, + profitLossPercent: -9.0, + tradesCount: 23, + lastUpdated: new Date().toISOString(), + }, + { + id: "AGT-005", + name: "Failed Bot", + status: "bankrupt", + strategy: "Arbitrage", + symbol: "BNB/USDT", + profitLoss: -1000.0, + profitLossPercent: -10.0, + tradesCount: 12, + lastUpdated: new Date().toISOString(), + }, + { + id: "AGT-006", + name: "Star Trader", + status: "running", + strategy: "Grid Trading", + symbol: "XRP/USDT", + profitLoss: 4670.8, + profitLossPercent: 46.7, + tradesCount: 312, + lastUpdated: new Date().toISOString(), + }, +]; diff --git a/frontend/src/components/trading/PositionTable.tsx b/frontend/src/components/trading/PositionTable.tsx new file mode 100644 index 0000000..eec3e60 --- /dev/null +++ b/frontend/src/components/trading/PositionTable.tsx @@ -0,0 +1,144 @@ +'use client'; + +import type { Position } from '@/types/models'; +import { cn } from '@/design-system/utils/cn'; + +interface PositionTableProps { + positions: Position[]; + onClosePosition?: (positionId: string) => void; +} + +export function PositionTable({ positions, onClosePosition }: PositionTableProps) { + return ( +
+ {/* Header */} +
+

Positions

+
+ + {/* Table */} +
+ + + + + + + + + + + + + + {positions.length === 0 ? ( + + + + ) : ( + positions.map((position) => ( + + {/* Symbol */} + + + {/* Side */} + + + {/* Quantity */} + + + {/* Entry Price */} + + + {/* Mark Price */} + + + {/* Unrealized P&L */} + + + {/* Action */} + + + )) + )} + +
+ Symbol + + Side + + Quantity + + Entry Price + + Mark Price + + Unrealized P&L + + Action +
+ No positions +
+ + {position.symbol} + + + + {position.side === 'long' ? 'Long' : 'Short'} + + + {position.quantity} + + + {position.entryPrice.toFixed(2)} + + + + {position.markPrice.toFixed(2)} + + +
+

= 0 ? 'text-[#00ffcc]' : 'text-[#ff5252]' + )} + > + {position.unrealizedPnl >= 0 ? '+' : ''} + {position.unrealizedPnl.toFixed(2)} +

+

= 0 ? 'text-[#00ffcc]' : 'text-[#ff5252]' + )} + > + {position.unrealizedPnlPercent >= 0 ? '+' : ''} + {position.unrealizedPnlPercent.toFixed(2)}% +

+
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/ui/StatCard.tsx b/frontend/src/components/ui/StatCard.tsx new file mode 100644 index 0000000..4090c0e --- /dev/null +++ b/frontend/src/components/ui/StatCard.tsx @@ -0,0 +1,68 @@ +import { cn } from "@/design-system/utils/cn"; + +interface StatCardProps { + label: string; + value: string | number; + subValue?: string; + change?: { + value: number; + isPositive: boolean; + }; + icon?: React.ReactNode; + isLoading?: boolean; + className?: string; +} + +export function StatCard({ + label, + value, + subValue, + change, + icon, + isLoading, + className, +}: StatCardProps) { + if (isLoading) { + return ( +
+
+
+
+ ); + } + + return ( +
+
+ + {label} + + {icon && {icon}} +
+ +
+ {value} + {change && ( + + {change.isPositive ? "+" : ""} + {change.value}% + + )} +
+ + {subValue &&

{subValue}

} +
+ ); +} diff --git a/frontend/src/design-system/tokens/colors.ts b/frontend/src/design-system/tokens/colors.ts new file mode 100644 index 0000000..6933c4b --- /dev/null +++ b/frontend/src/design-system/tokens/colors.ts @@ -0,0 +1,44 @@ +export const colors = { + // 背景层级 - 从深到浅 + background: { + base: '#121212', // 页面最底层背景 + elevated: '#1e1e1e', // 卡片、面板、弹窗 + overlay: '#2c2c2c', // 悬停状态、下拉菜单 + sunken: '#0a0a0a', // 输入框、内嵌区域 + }, + + // 品牌色 + primary: { + DEFAULT: '#00ffcc', + hover: '#00e6b8', + active: '#00cca3', + muted: 'rgba(0, 255, 204, 0.1)', + glow: 'rgba(0, 255, 204, 0.3)', + }, + + // 功能色 + semantic: { + success: '#00ffcc', // 与主色保持一致 + warning: '#ffb74d', + error: '#ff5252', + info: '#64b5f6', + }, + + // 文字色 + text: { + primary: '#e1e1e1', // 主要文字 + secondary: '#a0a0a0', // 次要文字 + muted: '#666666', // 弱化文字 + disabled: '#444444', // 禁用状态 + inverse: '#121212', // 用于主色背景上的文字 + }, + + // 边框 + border: { + DEFAULT: '#2c2c2c', + light: '#383838', + focus: '#00ffcc', + }, +} as const; + +export type Colors = typeof colors; diff --git a/frontend/src/design-system/tokens/index.ts b/frontend/src/design-system/tokens/index.ts new file mode 100644 index 0000000..7153727 --- /dev/null +++ b/frontend/src/design-system/tokens/index.ts @@ -0,0 +1,5 @@ +export { colors } from './colors'; +export { spacing } from './spacing'; +export { typography } from './typography'; +export { radii } from './radii'; +export { shadows } from './shadows'; diff --git a/frontend/src/design-system/tokens/radii.ts b/frontend/src/design-system/tokens/radii.ts new file mode 100644 index 0000000..e5ec2f9 --- /dev/null +++ b/frontend/src/design-system/tokens/radii.ts @@ -0,0 +1,12 @@ +export const radii = { + none: '0', + sm: '0.25rem', // 4px + DEFAULT: '0.375rem', // 6px + md: '0.5rem', // 8px + lg: '0.75rem', // 12px + xl: '1rem', // 16px + '2xl': '1.5rem', // 24px + full: '9999px', +} as const; + +export type Radii = typeof radii; diff --git a/frontend/src/design-system/tokens/shadows.ts b/frontend/src/design-system/tokens/shadows.ts new file mode 100644 index 0000000..c2360b2 --- /dev/null +++ b/frontend/src/design-system/tokens/shadows.ts @@ -0,0 +1,12 @@ +export const shadows = { + none: 'none', + sm: '0 1px 2px 0 rgba(0, 0, 0, 0.3)', + DEFAULT: '0 4px 6px -1px rgba(0, 0, 0, 0.4)', + md: '0 6px 12px -2px rgba(0, 0, 0, 0.5)', + lg: '0 10px 20px -4px rgba(0, 0, 0, 0.5)', + xl: '0 20px 40px -8px rgba(0, 0, 0, 0.6)', + glow: '0 0 20px rgba(0, 255, 204, 0.3)', + 'glow-sm': '0 0 10px rgba(0, 255, 204, 0.2)', +} as const; + +export type Shadows = typeof shadows; diff --git a/frontend/src/design-system/tokens/spacing.ts b/frontend/src/design-system/tokens/spacing.ts new file mode 100644 index 0000000..50f4ae9 --- /dev/null +++ b/frontend/src/design-system/tokens/spacing.ts @@ -0,0 +1,15 @@ +export const spacing = { + 0: '0', + 1: '0.25rem', // 4px + 2: '0.5rem', // 8px + 3: '0.75rem', // 12px + 4: '1rem', // 16px + 5: '1.25rem', // 20px + 6: '1.5rem', // 24px + 8: '2rem', // 32px + 10: '2.5rem', // 40px + 12: '3rem', // 48px + 16: '4rem', // 64px +} as const; + +export type Spacing = typeof spacing; diff --git a/frontend/src/design-system/tokens/typography.ts b/frontend/src/design-system/tokens/typography.ts new file mode 100644 index 0000000..c4de2f5 --- /dev/null +++ b/frontend/src/design-system/tokens/typography.ts @@ -0,0 +1,26 @@ +export const typography = { + fontFamily: { + sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], + mono: ['JetBrains Mono', 'Fira Code', 'monospace'], + }, + + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.05em' }], + sm: ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.025em' }], + base: ['1rem', { lineHeight: '1.5rem' }], + lg: ['1.125rem', { lineHeight: '1.75rem' }], + xl: ['1.25rem', { lineHeight: '1.75rem' }], + '2xl': ['1.5rem', { lineHeight: '2rem' }], + '3xl': ['1.875rem', { lineHeight: '2.25rem' }], + }, + + fontWeight: { + normal: '400', + medium: '500', + semibold: '600', + bold: '700', + black: '900', + }, +} as const; + +export type Typography = typeof typography; diff --git a/frontend/src/design-system/utils/cn.ts b/frontend/src/design-system/utils/cn.ts new file mode 100644 index 0000000..2819a83 --- /dev/null +++ b/frontend/src/design-system/utils/cn.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/src/design-system/utils/index.ts b/frontend/src/design-system/utils/index.ts new file mode 100644 index 0000000..414c24d --- /dev/null +++ b/frontend/src/design-system/utils/index.ts @@ -0,0 +1 @@ +export { cn } from './cn'; diff --git a/frontend/src/types/models.ts b/frontend/src/types/models.ts new file mode 100644 index 0000000..5f43f25 --- /dev/null +++ b/frontend/src/types/models.ts @@ -0,0 +1,176 @@ +export interface AgentStatus { + id: string; + name: string; + status: + | "running" + | "active" + | "paused" + | "resting" + | "stopped" + | "error" + | "danger" + | "bankrupt"; + strategy: string; + symbol: string; + profitLoss: number; + profitLossPercent: number; + tradesCount: number; + lastUpdated: string; +} + +export interface TradeRecord { + id: string; + agentId: string; + agentName: string; + symbol: string; + side: "buy" | "sell"; + quantity: number; + price: number; + total: number; + timestamp: string; + pnl?: number; + pnlPercent?: number; +} + +export interface Metrics { + totalEquity: number; + dailyPnl: number; + dailyPnlPercent: number; + totalTrades: number; + winRate: number; + activeAgents: number; + totalAgents: number; +} + +export interface EquityPoint { + timestamp: string; + equity: number; +} + +export interface PnlDistribution { + range: string; + count: number; +} + +// Trading related types +export interface Order { + id: string; + symbol: string; + side: "buy" | "sell"; + type: "market" | "limit" | "stop" | "stop_limit"; + status: + | "pending" + | "open" + | "filled" + | "partially_filled" + | "cancelled" + | "rejected"; + quantity: number; + filledQuantity: number; + price?: number; + stopPrice?: number; + total?: number; + timestamp: string; + updatedAt?: string; +} + +export interface Position { + id: string; + symbol: string; + side: "long" | "short"; + quantity: number; + entryPrice: number; + markPrice: number; + liquidationPrice?: number; + margin: number; + leverage: number; + unrealizedPnl: number; + unrealizedPnlPercent: number; + realizedPnl: number; + openedAt: string; +} + +export interface Ticker { + symbol: string; + lastPrice: number; + priceChange: number; + priceChangePercent: number; + high24h: number; + low24h: number; + volume24h: number; + quoteVolume24h: number; + bidPrice: number; + askPrice: number; + bidQty: number; + askQty: number; + timestamp: string; +} + +export interface OrderBookEntry { + price: number; + quantity: number; + total?: number; +} + +export interface OrderBook { + symbol: string; + bids: OrderBookEntry[]; + asks: OrderBookEntry[]; + timestamp: string; +} + +export interface Kline { + timestamp: string; + open: number; + high: number; + low: number; + close: number; + volume: number; + quoteVolume: number; +} + +export type TradingMode = "simulation" | "live"; + +// Trade type for recent trades +export interface Trade { + id: string; + symbol: string; + side: "buy" | "sell"; + price: number; + quantity: number; + total: number; + timestamp: string; +} + +// Exchange configuration +export interface Exchange { + id: string; + name: string; + type: "binance" | "okx" | "bybit" | "custom"; + apiKey: string; + apiSecret?: string; + passphrase?: string; + testnet: boolean; + enabled?: boolean; + status: "connected" | "disconnected" | "error"; + lastTested?: string; + createdAt: string; + updatedAt: string; +} + +export interface ExchangeCreateRequest { + name: string; + type: "binance" | "okx" | "bybit" | "custom"; + apiKey: string; + apiSecret: string; + passphrase?: string; + testnet: boolean; +} + +export interface ExchangeUpdateRequest { + name?: string; + apiKey?: string; + apiSecret?: string; + passphrase?: string; + testnet?: boolean; +}