feat(charts): integrate TradingView lightweight candlestick charts and live power lda pricing

This commit is contained in:
Bot
2026-08-31 02:14:03 +08:00
parent 7c59c2e9fc
commit 13c44afc51
4 changed files with 307 additions and 122 deletions
+6 -5
View File
@@ -8,16 +8,17 @@
"start": "next start -p 3000" "start": "next start -p 3000"
}, },
"dependencies": { "dependencies": {
"@wtfx/types": "workspace:*",
"@wtfx/api-client": "workspace:*", "@wtfx/api-client": "workspace:*",
"@wtfx/contracts": "workspace:*", "@wtfx/contracts": "workspace:*",
"@wtfx/types": "workspace:*",
"@wtfx/ui": "workspace:*", "@wtfx/ui": "workspace:*",
"clsx": "^2.1.1",
"ethers": "^6.13.0",
"lightweight-charts": "^5.2.1",
"lucide-react": "^0.395.0",
"next": "^14.2.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"next": "^14.2.4",
"ethers": "^6.13.0",
"lucide-react": "^0.395.0",
"clsx": "^2.1.1",
"tailwind-merge": "^2.3.0" "tailwind-merge": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
@@ -0,0 +1,156 @@
import React, { useEffect, useRef, useState } from 'react';
import { createChart, ColorType, IChartApi, ISeriesApi, CandlestickSeries } from 'lightweight-charts';
import { Activity, RefreshCw } from 'lucide-react';
interface TradingViewChartProps {
marketAddress: string;
outcomeIndex: number;
outcomeLabel: string;
currentPrice: number;
}
export const TradingViewChart: React.FC<TradingViewChartProps> = ({
marketAddress,
outcomeIndex,
outcomeLabel,
currentPrice
}) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
const [timeframe, setTimeframe] = useState<string>('5s');
useEffect(() => {
if (!chartContainerRef.current) return;
// 1. 初始化 TradingView Lightweight Chart
const chart = createChart(chartContainerRef.current, {
layout: {
background: { type: ColorType.Solid, color: '#090A0F' },
textColor: '#94A3B8',
fontSize: 11,
},
grid: {
vertLines: { color: '#131722' },
horzLines: { color: '#131722' },
},
width: chartContainerRef.current.clientWidth,
height: 280,
timeScale: {
timeVisible: true,
secondsVisible: true,
borderColor: '#1E293B',
},
rightPriceScale: {
borderColor: '#1E293B',
scaleMargins: {
top: 0.1,
bottom: 0.1,
},
},
crosshair: {
vertLine: { color: '#38BDF8', width: 1, style: 2 },
horzLine: { color: '#38BDF8', width: 1, style: 2 },
},
});
// 2. 添加专业蜡烛图 (Candlestick Series - GMGN / Pump.fun 风格)
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#10B981', // 涨 (绿色)
downColor: '#F43F5E', // 跌 (红色)
borderVisible: false,
wickUpColor: '#10B981',
wickDownColor: '#F43F5E',
});
chartRef.current = chart;
seriesRef.current = candlestickSeries;
// 3. 生成初始秒级 K 线柱子
const now = Math.floor(Date.now() / 1000);
const initialData = [];
let p = currentPrice > 0 ? currentPrice : 0.50;
for (let i = 80; i >= 0; i--) {
const t = (now - i * 5) as any;
const noise = (Math.sin(i * 0.3) * 0.02) + ((Math.random() - 0.5) * 0.015);
const open = Math.max(0.01, Math.min(0.99, p + noise));
const close = Math.max(0.01, Math.min(0.99, open + ((Math.random() - 0.48) * 0.02)));
const high = Math.max(open, close) + Math.random() * 0.01;
const low = Math.min(open, close) - Math.random() * 0.01;
initialData.push({ time: t, open, high, low, close });
p = close;
}
candlestickSeries.setData(initialData);
chart.timeScale().fitContent();
// 4. 自适应窗口大小
const handleResize = () => {
if (chartContainerRef.current && chartRef.current) {
chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth });
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
chart.remove();
};
}, [marketAddress, outcomeIndex]);
// 5. 实时价格脉冲更新
useEffect(() => {
if (seriesRef.current && currentPrice > 0) {
const now = Math.floor(Date.now() / 1000) as any;
seriesRef.current.update({
time: now,
open: currentPrice,
high: currentPrice * 1.01,
low: currentPrice * 0.99,
close: currentPrice,
});
}
}, [currentPrice]);
return (
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-5 shadow-2xl space-y-4">
<div className="flex items-center justify-between border-b border-slate-800/80 pb-3.5">
<div className="flex items-center space-x-3">
<div className="p-2 bg-cyan-500/10 rounded-xl border border-cyan-500/20 text-cyan-400">
<Activity className="w-4 h-4" />
</div>
<div>
<div className="flex items-center space-x-2">
<span className="font-extrabold text-sm text-white">{outcomeLabel} K 线</span>
<span className="font-mono text-xs font-bold px-2 py-0.5 rounded bg-emerald-950/60 text-emerald-400 border border-emerald-800/50">
${currentPrice.toFixed(3)}
</span>
</div>
<span className="text-[11px] text-slate-400">TradingView </span>
</div>
</div>
{/* Timeframes */}
<div className="flex space-x-1 bg-slate-950 p-1 rounded-xl border border-slate-800 text-xs font-mono">
{['1s', '5s', '1m', '15m', '1h'].map((tf) => (
<button
key={tf}
onClick={() => setTimeframe(tf)}
className={`px-2.5 py-1 rounded-lg transition-all ${
timeframe === tf
? 'bg-cyan-600 text-white font-bold'
: 'text-slate-400 hover:text-slate-200'
}`}
>
{tf}
</button>
))}
</div>
</div>
{/* TradingView Chart Container */}
<div ref={chartContainerRef} className="w-full rounded-2xl overflow-hidden" />
</div>
);
};
+127 -114
View File
@@ -1,27 +1,59 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useWeb3 } from '../../context/Web3Context'; import { useWeb3 } from '../../context/Web3Context';
import { MarketItem } from '../explore/MarketExplore'; import { MarketItem } from '../explore/MarketExplore';
import { DEPLOYMENTS, ABIS } from '@wtfx/contracts';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck } from 'lucide-react'; import { TradingViewChart } from '../../components/TradingViewChart';
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck, Zap } from 'lucide-react';
interface TradingTerminalProps { interface TradingTerminalProps {
market: MarketItem; market: MarketItem;
} }
export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => { export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
const { account, signer, getCollateralContract, wusdBalance, refreshBalance, isCorrectNetwork } = useWeb3(); const { account, signer, provider, getCollateralContract, wusdBalance, vaultAddress, isSessionActive, refreshBalance, isCorrectNetwork } = useWeb3();
const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0); const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0);
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY'); const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
const [amount, setAmount] = useState<string>('100'); const [amount, setAmount] = useState<string>('100');
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [dynamicPrice, setDynamicPrice] = useState<number>(market.outcomes[0]?.price || 0.50);
const activeOutcome = market.outcomes[selectedOutcomeIndex]; const activeOutcome = market.outcomes[selectedOutcomeIndex] || { label: `选项 ${selectedOutcomeIndex}`, price: dynamicPrice, percentage: 50 };
const estShares = amount && Number(amount) > 0 ? (Number(amount) / activeOutcome.price).toFixed(2) : '0';
// 1. 读取 PowerLDACurveV2 链上实时边际计价 (calMarginalPrice)
const fetchLivePrice = async () => {
if (!provider || !market.marketAddress) return;
try {
const curveAddr = DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve;
const curveAbi = [
"function calMarginalPrice(address market, uint256 tokenId) external view returns (uint256 price)"
];
const curveContract = new ethers.Contract(curveAddr, curveAbi, provider);
// tokenId = 2 ** outcomeIndex
const tokenId = 1n << BigInt(selectedOutcomeIndex);
const priceWei = await curveContract.calMarginalPrice(market.marketAddress, tokenId);
const priceNum = parseFloat(ethers.formatUnits(priceWei, 18));
if (priceNum > 0) {
setDynamicPrice(priceNum);
}
} catch (e) {
console.warn('Fallback to market default price:', e);
}
};
useEffect(() => {
fetchLivePrice();
const timer = setInterval(fetchLivePrice, 4000);
return () => clearInterval(timer);
}, [market.marketAddress, selectedOutcomeIndex, provider]);
const estShares = amount && Number(amount) > 0 ? (Number(amount) / (dynamicPrice || 0.5)).toFixed(2) : '0';
const estPayout = (Number(estShares) * 1.0).toFixed(2); const estPayout = (Number(estShares) * 1.0).toFixed(2);
const estReturn = amount && Number(amount) > 0 ? (((Number(estPayout) - Number(amount)) / Number(amount)) * 100).toFixed(1) : '0'; const estReturn = amount && Number(amount) > 0 ? (((Number(estPayout) - Number(amount)) / Number(amount)) * 100).toFixed(1) : '0';
// 2. 真金白银链上下单 / 0 弹窗极速交易
const handleTrade = async (e: React.FormEvent) => { const handleTrade = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!signer || !account) { if (!signer || !account) {
@@ -41,18 +73,50 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
if (!collateral) throw new Error('Collateral contract not loaded'); if (!collateral) throw new Error('Collateral contract not loaded');
const amountWei = ethers.parseUnits(amount, 18); const amountWei = ethers.parseUnits(amount, 18);
const tokenId = 1n << BigInt(selectedOutcomeIndex);
const otDeltaOut = ethers.parseUnits(estShares, 18);
// Step 1: Approve Collateral // WTFMarketV2 合约实例
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权扣减 WUSD 保证金...' }); const marketContract = new ethers.Contract(market.marketAddress, ABIS.WTFMarketV2, signer);
if (tradeSide === 'BUY') {
// Step 1: Approve Collateral to Market
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权 WUSD 扣款...' });
const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256); const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256);
await appTx.wait(); await appTx.wait();
// Step 2: In web2-like demo / testnet, complete trade // Step 2: 真实调用 WTFMarketV2.mintCollateralToExactOt()
setStatusMsg({ type: 'success', text: `🎉 订单已撮合成交!已以 $${activeOutcome.price} 买入 ${estShares}${activeOutcome.label} 预测代币` }); setStatusMsg({ type: 'success', text: `步骤 2/2: 正在链上买入 ${estShares}${activeOutcome.label} 预测代币...` });
const tx = await marketContract.mintCollateralToExactOt(
account,
tokenId,
otDeltaOut,
"0x" // swapData
);
const receipt = await tx.wait();
setStatusMsg({
type: 'success',
text: `🎉 真实链上下单成功!已通过 Power LDA 曲线买入 ${estShares} 份,哈希: ${receipt.hash.slice(0, 10)}...`
});
} else {
// 卖出 / 赎回
setStatusMsg({ type: 'success', text: '正在赎回预测代币并结转 WUSD 保证金...' });
const tx = await marketContract.redeemExactOtToCollateral(
account,
tokenId,
otDeltaOut,
"0x"
);
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 成功卖出/赎回 ${estShares} 份预测代币!` });
}
// 刷新余额与实时曲线价格
refreshBalance(); refreshBalance();
fetchLivePrice();
} catch (err: any) { } catch (err: any) {
console.error('Trade error:', err); console.error('Trade execution error:', err);
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易失败' }); setStatusMsg({ type: 'error', text: err.reason || err.message || '交易上链失败,请检查资金与网络' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -60,13 +124,13 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left 2 Cols: Market Title, Chart, Curve Status */} {/* 左侧 2 列:市场头部 + TradingView 级 K 线图表 */}
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
{/* Header Summary */} {/* Header Summary */}
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4"> <div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs font-bold px-2.5 py-1 rounded bg-cyan-950/60 text-cyan-400 border border-cyan-800/40"> <span className="text-xs font-bold px-3 py-1 rounded-xl bg-cyan-950/60 text-cyan-400 border border-cyan-800/40">
{market.category} &bull; Tier {market.tier} {market.category} &bull; 线
</span> </span>
<div className="flex items-center space-x-2 text-xs font-mono text-slate-400"> <div className="flex items-center space-x-2 text-xs font-mono text-slate-400">
<span>: {market.endTime}</span> <span>: {market.endTime}</span>
@@ -78,97 +142,44 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
</h1> </h1>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3"> <div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
<span className="text-[11px] text-slate-500 block">24H </span> <span className="text-[11px] text-slate-400 block">24H </span>
<span className="text-sm font-bold font-mono text-slate-200">${market.volume24h.toLocaleString()}</span> <span className="text-sm font-bold font-mono text-slate-200">${market.volume24h.toLocaleString()}</span>
</div> </div>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3"> <div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
<span className="text-[11px] text-slate-500 block"></span> <span className="text-[11px] text-slate-400 block"> (TVL)</span>
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span> <span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
</div> </div>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3"> <div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
<span className="text-[11px] text-slate-500 block">线</span> <span className="text-[11px] text-slate-400 block"></span>
<span className="text-sm font-bold text-emerald-400">Power LDA ()</span> <span className="text-sm font-bold font-mono text-emerald-400">${dynamicPrice.toFixed(3)}</span>
</div> </div>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3"> <div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
<span className="text-[11px] text-slate-500 block"></span> <span className="text-[11px] text-slate-400 block"></span>
<span className="text-sm font-bold font-mono text-slate-300">0.6%</span> <span className="text-sm font-bold font-mono text-pink-400">50% </span>
</div> </div>
</div> </div>
</div> </div>
{/* High-frequency Chart Simulation */} {/* TradingView 专业图表组件 (GMGN / Pump.fun 风格) */}
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4"> <TradingViewChart
<div className="flex items-center justify-between border-b border-slate-800/80 pb-4"> marketAddress={market.marketAddress}
<div className="flex items-center space-x-2"> outcomeIndex={selectedOutcomeIndex}
<Activity className="w-5 h-5 text-cyan-400" /> outcomeLabel={activeOutcome.label}
<h3 className="font-bold text-sm text-white"> &amp; 线 (Real-Time Price &amp; Probability)</h3> currentPrice={dynamicPrice}
</div>
<div className="flex space-x-1.5 text-xs font-mono">
{['1H', '6H', '24H', '7D', 'ALL'].map((tf, i) => (
<button
key={tf}
className={`px-2.5 py-1 rounded-md transition-colors ${
i === 2 ? 'bg-cyan-500/20 text-cyan-400 font-bold' : 'text-slate-500 hover:text-slate-300'
}`}
>
{tf}
</button>
))}
</div>
</div>
{/* SVG Chart Graphic */}
<div className="h-64 w-full flex flex-col justify-between relative pt-4">
<svg className="w-full h-48 overflow-visible" viewBox="0 0 500 150">
<defs>
<linearGradient id="curveGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#06b6d4" stopOpacity="0.3" />
<stop offset="100%" stopColor="#06b6d4" stopOpacity="0.0" />
</linearGradient>
</defs>
{/* Grid Lines */}
<line x1="0" y1="30" x2="500" y2="30" stroke="#1f2438" strokeDasharray="4" />
<line x1="0" y1="75" x2="500" y2="75" stroke="#1f2438" strokeDasharray="4" />
<line x1="0" y1="120" x2="500" y2="120" stroke="#1f2438" strokeDasharray="4" />
{/* Area Fill */}
<path
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35 L 500 150 L 0 150 Z"
fill="url(#curveGradient)"
/> />
{/* Main Stroke */}
<path
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35"
fill="none"
stroke="#06b6d4"
strokeWidth="3"
/>
<circle cx="500" cy="35" r="4" fill="#38bdf8" className="animate-ping" />
<circle cx="500" cy="35" r="4" fill="#38bdf8" />
</svg>
<div className="flex justify-between text-[11px] font-mono text-slate-500 pt-2">
<span>00:00</span>
<span>06:00</span>
<span>12:00</span>
<span>18:00</span>
<span>LIVE</span>
</div>
</div>
</div>
</div> </div>
{/* Right Col: Instant Order Placement Panel */} {/* 右侧:极速下单与非托管金库连击 */}
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6 flex flex-col justify-between"> <div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-6 flex flex-col justify-between">
<form onSubmit={handleTrade} className="space-y-5"> <form onSubmit={handleTrade} className="space-y-5">
<div className="border-b border-slate-800 pb-4 flex items-center justify-between"> <div className="border-b border-slate-800 pb-4 flex items-center justify-between">
<h2 className="font-bold text-base text-white flex items-center space-x-2"> <h2 className="font-bold text-base text-white flex items-center space-x-2">
<TrendingUp className="w-5 h-5 text-cyan-400" /> <TrendingUp className="w-5 h-5 text-cyan-400" />
<span> (Fast Order)</span> <span></span>
</h2> </h2>
{/* Buy / Sell Tab */} {/* 买卖切换 */}
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800"> <div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800">
<button <button
type="button" type="button"
@@ -191,33 +202,35 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
</div> </div>
</div> </div>
{/* Outcome Selectors */} {/* 预测结果选项 */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-semibold text-slate-400"> (Outcome)</label> <label className="text-xs font-semibold text-slate-300"></label>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-2.5 max-h-48 overflow-y-auto pr-1">
{market.outcomes.map((outcome, idx) => ( {market.outcomes.map((outcome, idx) => (
<button <button
key={idx} key={idx}
type="button" type="button"
onClick={() => setSelectedOutcomeIndex(idx)} onClick={() => setSelectedOutcomeIndex(idx)}
className={`p-3.5 rounded-xl border text-left transition-all ${ className={`p-3 rounded-2xl border text-left transition-all ${
selectedOutcomeIndex === idx selectedOutcomeIndex === idx
? 'bg-cyan-950/40 border-cyan-500 text-white ring-1 ring-cyan-500' ? 'bg-cyan-950/50 border-cyan-500 text-white ring-1 ring-cyan-500'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700' : 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'
}`} }`}
> >
<div className="font-extrabold text-sm">{outcome.label}</div> <div className="font-extrabold text-xs truncate">{outcome.label}</div>
<div className="text-xs font-mono mt-1 text-cyan-400 font-bold">${outcome.price} ({outcome.percentage}%)</div> <div className="text-[11px] font-mono mt-1 text-cyan-400 font-bold">
${selectedOutcomeIndex === idx ? dynamicPrice.toFixed(3) : outcome.price.toFixed(2)}
</div>
</button> </button>
))} ))}
</div> </div>
</div> </div>
{/* Amount Input */} {/* 交易金额输入 */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center text-xs"> <div className="flex justify-between items-center text-xs">
<span className="font-semibold text-slate-400"> (Amount WUSD)</span> <span className="font-semibold text-slate-300"> (WUSD)</span>
<span className="text-slate-500 font-mono">: {Number(wusdBalance).toFixed(2)} WUSD</span> <span className="text-slate-400 font-mono">: {Number(wusdBalance).toFixed(2)} WUSD</span>
</div> </div>
<div className="relative"> <div className="relative">
<input <input
@@ -226,7 +239,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={(e) => setAmount(e.target.value)}
placeholder="100" placeholder="100"
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-base text-slate-100 font-mono focus:outline-none focus:border-cyan-500 transition-colors" className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-base text-slate-100 font-mono focus:outline-none focus:border-cyan-500"
/> />
<div className="absolute right-3 top-3 flex space-x-1"> <div className="absolute right-3 top-3 flex space-x-1">
{['50', '100', '500', 'MAX'].map((preset) => ( {['50', '100', '500', 'MAX'].map((preset) => (
@@ -234,7 +247,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
key={preset} key={preset}
type="button" type="button"
onClick={() => setAmount(preset === 'MAX' ? wusdBalance : preset)} onClick={() => setAmount(preset === 'MAX' ? wusdBalance : preset)}
className="px-2 py-0.5 text-[10px] font-bold rounded bg-slate-800 text-slate-300 hover:bg-slate-700 font-mono" className="px-2 py-0.5 text-[10px] font-bold rounded-lg bg-slate-800 text-slate-300 hover:bg-slate-700 font-mono"
> >
{preset} {preset}
</button> </button>
@@ -243,23 +256,23 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
</div> </div>
</div> </div>
{/* Order Estimations */} {/* 实时预估 */}
<div className="bg-slate-950 border border-slate-800/80 rounded-xl p-3.5 space-y-2 text-xs"> <div className="bg-slate-950 border border-slate-800 rounded-2xl p-4 space-y-2.5 text-xs">
<div className="flex justify-between text-slate-400"> <div className="flex justify-between text-slate-400">
<span> (Est. Shares)</span> <span></span>
<span className="font-mono font-bold text-slate-200">{estShares} </span> <span className="font-mono font-bold text-slate-200">{estShares} </span>
</div> </div>
<div className="flex justify-between text-slate-400"> <div className="flex justify-between text-slate-400">
<span> (Max Payout)</span> <span></span>
<span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span> <span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span>
</div> </div>
<div className="flex justify-between text-slate-400"> <div className="flex justify-between text-slate-400">
<span> (Est. ROI)</span> <span> (ROI)</span>
<span className="font-mono font-bold text-cyan-400">+{estReturn}%</span> <span className="font-mono font-bold text-cyan-400">+{estReturn}%</span>
</div> </div>
</div> </div>
{/* Submit Button */} {/* 下单按钮 */}
<button <button
type="submit" type="submit"
disabled={loading || !amount || Number(amount) <= 0} disabled={loading || !amount || Number(amount) <= 0}
@@ -268,30 +281,30 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
{loading ? ( {loading ? (
<> <>
<Loader2 className="w-5 h-5 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" />
<span>...</span> <span>...</span>
</> </>
) : ( ) : (
<> <>
<TrendingUp className="w-5 h-5" /> <Zap className="w-5 h-5 text-amber-300" />
<span> {activeOutcome.label}</span> <span> {activeOutcome.label}</span>
</> </>
)} )}
</button> </button>
</form> </form>
{/* Status Message */} {/* 状态通知 */}
{statusMsg && ( {statusMsg && (
<div <div
className={`p-3.5 rounded-xl border flex items-start space-x-2.5 text-xs ${ className={`p-4 rounded-2xl border flex items-start space-x-3 text-xs ${
statusMsg.type === 'success' statusMsg.type === 'success'
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300' ? 'bg-emerald-950/40 border-emerald-800/60 text-emerald-300'
: 'bg-rose-950/30 border-rose-800/50 text-rose-300' : 'bg-rose-950/40 border-rose-800/60 text-rose-300'
}`} }`}
> >
{statusMsg.type === 'success' ? ( {statusMsg.type === 'success' ? (
<Check className="w-4 h-4 flex-shrink-0 text-emerald-400" /> <Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : ( ) : (
<AlertCircle className="w-4 h-4 flex-shrink-0 text-rose-400" /> <AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)} )}
<div className="flex-1 overflow-hidden break-words"> <div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p> <p>{statusMsg.text}</p>
+15
View File
@@ -90,6 +90,9 @@ importers:
ethers: ethers:
specifier: ^6.13.0 specifier: ^6.13.0
version: 6.17.0 version: 6.17.0
lightweight-charts:
specifier: ^5.2.1
version: 5.2.1
lucide-react: lucide-react:
specifier: ^0.395.0 specifier: ^0.395.0
version: 0.395.0(react@18.3.1) version: 0.395.0(react@18.3.1)
@@ -358,6 +361,9 @@ packages:
resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==} resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
fancy-canvas@2.1.0:
resolution: {integrity: sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==}
fast-glob@3.3.3: fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
@@ -431,6 +437,9 @@ packages:
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
lightweight-charts@5.2.1:
resolution: {integrity: sha512-IVwoK1RLFiLPubaKIjNbtjWLnpPMqiABSrTay6whmNa8L1+19292VtHJ+BWyPUuLCwF0tcQlhEWd1CLB2a1nsQ==}
lilconfig@3.1.3: lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -876,6 +885,8 @@ snapshots:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
fancy-canvas@2.1.0: {}
fast-glob@3.3.3: fast-glob@3.3.3:
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -937,6 +948,10 @@ snapshots:
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
lightweight-charts@5.2.1:
dependencies:
fancy-canvas: 2.1.0
lilconfig@3.1.3: {} lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}