feat(web): add apps/web Next.js app src, tailwind, tsconfig and pages
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Sparkles, TrendingUp, Flame, CheckCircle, Clock, Search, ArrowUpRight } from 'lucide-react';
|
||||
|
||||
export interface MarketItem {
|
||||
id: string;
|
||||
marketAddress: string;
|
||||
title: string;
|
||||
category: string;
|
||||
tier: number;
|
||||
outcomes: { label: string; price: number; percentage: number }[];
|
||||
poolLiquidity: number;
|
||||
volume24h: number;
|
||||
endTime: string;
|
||||
status: 'ACTIVE' | 'RESOLVED' | 'FINALISED';
|
||||
}
|
||||
|
||||
const MOCK_FEATURED_MARKETS: MarketItem[] = [
|
||||
{
|
||||
id: 'm-1',
|
||||
marketAddress: '0xc0E24E152771C588B21AEB654b30B1cBAf381c1a',
|
||||
title: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
||||
category: 'Crypto',
|
||||
tier: 1,
|
||||
outcomes: [
|
||||
{ label: 'YES', price: 0.68, percentage: 68 },
|
||||
{ label: 'NO', price: 0.32, percentage: 32 }
|
||||
],
|
||||
poolLiquidity: 1250000,
|
||||
volume24h: 384000,
|
||||
endTime: '2026-12-31',
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
{
|
||||
id: 'm-2',
|
||||
marketAddress: '0x2Aeca49669e9E4E6CdCe46A1bc9bf136765A4f75',
|
||||
title: 'Ethereum Layer-2 Total TVL to reach $50 Billion in 2026?',
|
||||
category: 'Crypto',
|
||||
tier: 2,
|
||||
outcomes: [
|
||||
{ label: 'YES', price: 0.81, percentage: 81 },
|
||||
{ label: 'NO', price: 0.19, percentage: 19 }
|
||||
],
|
||||
poolLiquidity: 890000,
|
||||
volume24h: 192000,
|
||||
endTime: '2026-11-30',
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
{
|
||||
id: 'm-3',
|
||||
marketAddress: '0xF0E189974c413506098AFB6Bc6Bf4C6715fBf7B1',
|
||||
title: 'US Federal Reserve Interest Rate cut > 50 bps in next FOMC?',
|
||||
category: 'Macro',
|
||||
tier: 1,
|
||||
outcomes: [
|
||||
{ label: 'YES (>50bps)', price: 0.35, percentage: 35 },
|
||||
{ label: 'NO (<=50bps)', price: 0.65, percentage: 65 }
|
||||
],
|
||||
poolLiquidity: 620000,
|
||||
volume24h: 98000,
|
||||
endTime: '2026-09-30',
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
];
|
||||
|
||||
interface MarketExploreProps {
|
||||
onSelectMarket: (market: MarketItem) => void;
|
||||
}
|
||||
|
||||
export const MarketExplore: React.FC<MarketExploreProps> = ({ onSelectMarket }) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
const categories = ['All', 'Crypto', 'Macro', 'Politics', 'Sports', 'Tech'];
|
||||
|
||||
const filteredMarkets = MOCK_FEATURED_MARKETS.filter((m) => {
|
||||
const matchCategory = selectedCategory === 'All' || m.category === selectedCategory;
|
||||
const matchSearch = m.title.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchCategory && matchSearch;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Hero Banner */}
|
||||
<div className="relative rounded-3xl overflow-hidden border border-slate-800 bg-gradient-to-r from-slate-950 via-slate-900 to-indigo-950/40 p-8 sm:p-12 shadow-2xl">
|
||||
<div className="absolute right-0 top-0 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||
<div className="max-w-2xl space-y-4 relative z-10">
|
||||
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 text-xs font-semibold">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
<span>Power LDA 幂律连续流动性引擎</span>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-black text-white tracking-tight leading-tight">
|
||||
全球最具流动性的 <br />
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 via-indigo-300 to-purple-400">
|
||||
去中心化预测交易市场
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-slate-400 text-sm sm:text-base leading-relaxed">
|
||||
零滑点聚合撮合,支持多选项连续拍卖与极速链上结算。抢先参与热门预测,兑现你的认知价值。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter & Search Bar */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{/* Category Tabs */}
|
||||
<div className="flex items-center space-x-2 overflow-x-auto w-full md:w-auto pb-2 md:pb-0">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all whitespace-nowrap ${
|
||||
selectedCategory === cat
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="relative w-full md:w-72">
|
||||
<Search className="w-4 h-4 text-slate-500 absolute left-3.5 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索预测事件 / 资产..."
|
||||
className="w-full bg-slate-900/90 border border-slate-800 rounded-xl pl-9 pr-4 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Market Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredMarkets.map((market) => (
|
||||
<div
|
||||
key={market.id}
|
||||
onClick={() => onSelectMarket(market)}
|
||||
className="group bg-[#0f111d] hover:bg-[#141726] border border-slate-800/80 hover:border-cyan-500/40 rounded-2xl p-5 cursor-pointer transition-all duration-200 hover:shadow-xl hover:shadow-cyan-950/20 flex flex-col justify-between"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Category & Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] font-bold px-2.5 py-0.5 rounded-md bg-slate-800 text-cyan-400 border border-slate-700">
|
||||
{market.category}
|
||||
</span>
|
||||
<div className="flex items-center space-x-1.5 text-[11px] text-slate-500">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span>{market.endTime} 截止</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-bold text-base text-slate-100 group-hover:text-white line-clamp-2 leading-snug">
|
||||
{market.title}
|
||||
</h3>
|
||||
|
||||
{/* Outcomes Bars */}
|
||||
<div className="space-y-2 pt-1">
|
||||
{market.outcomes.map((outcome, idx) => (
|
||||
<div key={idx} className="space-y-1">
|
||||
<div className="flex justify-between text-xs font-semibold">
|
||||
<span className={idx === 0 ? 'text-emerald-400' : 'text-rose-400'}>
|
||||
{outcome.label}
|
||||
</span>
|
||||
<span className="text-slate-300 font-mono">${outcome.price.toFixed(2)} ({outcome.percentage}%)</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800/60">
|
||||
<div
|
||||
className={`h-full rounded-full ${
|
||||
idx === 0 ? 'bg-gradient-to-r from-emerald-500 to-teal-400' : 'bg-gradient-to-r from-rose-500 to-pink-500'
|
||||
}`}
|
||||
style={{ width: `${outcome.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer / Stats */}
|
||||
<div className="mt-6 pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
|
||||
<div>
|
||||
<span className="block text-slate-500">24H 交易量</span>
|
||||
<span className="font-mono font-bold text-slate-200">${market.volume24h.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="block text-slate-500">流动性深度</span>
|
||||
<span className="font-mono font-bold text-slate-200">${market.poolLiquidity.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="p-2 rounded-xl bg-slate-800/50 group-hover:bg-cyan-500/20 group-hover:text-cyan-400 transition-colors">
|
||||
<ArrowUpRight className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import React from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { PieChart, Clock, ArrowUpRight, ArrowDownLeft, ShieldCheck, History } from 'lucide-react';
|
||||
|
||||
export const PortfolioView: React.FC = () => {
|
||||
const { account, wusdBalance } = useWeb3();
|
||||
|
||||
const MOCK_POSITIONS = [
|
||||
{
|
||||
id: 'pos-1',
|
||||
marketTitle: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
||||
outcome: 'YES',
|
||||
shares: 1470.5,
|
||||
avgPrice: 0.68,
|
||||
currentPrice: 0.72,
|
||||
currentValue: 1058.76,
|
||||
pnl: 58.76,
|
||||
pnlPct: 5.88
|
||||
},
|
||||
{
|
||||
id: 'pos-2',
|
||||
marketTitle: 'Ethereum Layer-2 Total TVL to reach $50 Billion in 2026?',
|
||||
outcome: 'YES',
|
||||
shares: 617.28,
|
||||
avgPrice: 0.81,
|
||||
currentPrice: 0.85,
|
||||
currentValue: 524.68,
|
||||
pnl: 24.68,
|
||||
pnlPct: 4.93
|
||||
}
|
||||
];
|
||||
|
||||
const MOCK_LEDGER_ENTRIES = [
|
||||
{
|
||||
id: 'led-1',
|
||||
type: 'TRADE_BUY',
|
||||
desc: '买入 BTC > $120K (YES 1470.5 份)',
|
||||
amount: -1000.0,
|
||||
fee: 6.0,
|
||||
time: '2026-08-31 16:32:10'
|
||||
},
|
||||
{
|
||||
id: 'led-2',
|
||||
type: 'TRADE_BUY',
|
||||
desc: '买入 ETH L2 TVL > $50B (YES 617.28 份)',
|
||||
amount: -500.0,
|
||||
fee: 3.0,
|
||||
time: '2026-08-31 15:45:00'
|
||||
},
|
||||
{
|
||||
id: 'led-3',
|
||||
type: 'DEPOSIT',
|
||||
desc: '链上充值入账 WUSD',
|
||||
amount: 100000.0,
|
||||
fee: 0.0,
|
||||
time: '2026-08-31 14:10:22'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Portfolio Top Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||
<span className="text-xs text-slate-400 block font-medium">总持仓市值 (Portfolio Value)</span>
|
||||
<span className="text-2xl font-black font-mono text-white mt-1 block">$1,583.44 WUSD</span>
|
||||
<div className="flex items-center space-x-1.5 text-xs text-emerald-400 font-bold mt-2">
|
||||
<span>+$83.44 (+5.56%)</span>
|
||||
<span className="text-slate-500 font-normal">未实现盈亏</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||
<span className="text-xs text-slate-400 block font-medium">可用保证金余额 (Available Balance)</span>
|
||||
<span className="text-2xl font-black font-mono text-cyan-400 mt-1 block">
|
||||
{Number(wusdBalance).toLocaleString(undefined, { maximumFractionDigits: 2 })} WUSD
|
||||
</span>
|
||||
<div className="text-xs text-slate-500 mt-2">随时可用于下注买入或提现</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||
<span className="text-xs text-slate-400 block font-medium">活跃预测头寸 (Active Positions)</span>
|
||||
<span className="text-2xl font-black font-mono text-slate-200 mt-1 block">2 个市场</span>
|
||||
<div className="text-xs text-slate-500 mt-2">持仓均基于不可变智能合约保护</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions Table */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<div className="flex items-center space-x-2 border-b border-slate-800/80 pb-4">
|
||||
<PieChart className="w-5 h-5 text-cyan-400" />
|
||||
<h2 className="font-bold text-base text-white">当前持仓明细 (Active Positions)</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-500 uppercase">
|
||||
<th className="pb-3 font-semibold">预测市场</th>
|
||||
<th className="pb-3 font-semibold">预测选项</th>
|
||||
<th className="pb-3 font-semibold text-right">持有份额</th>
|
||||
<th className="pb-3 font-semibold text-right">开仓均价</th>
|
||||
<th className="pb-3 font-semibold text-right">当前市价</th>
|
||||
<th className="pb-3 font-semibold text-right">当前估值</th>
|
||||
<th className="pb-3 font-semibold text-right">未实现盈亏</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{MOCK_POSITIONS.map((pos) => (
|
||||
<tr key={pos.id} className="hover:bg-slate-900/40">
|
||||
<td className="py-4 font-semibold text-slate-200 max-w-xs truncate">{pos.marketTitle}</td>
|
||||
<td className="py-4">
|
||||
<span className="px-2 py-0.5 rounded bg-emerald-950/60 text-emerald-400 border border-emerald-800/50 font-bold">
|
||||
{pos.outcome}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 text-right font-mono text-slate-300">{pos.shares.toLocaleString()}</td>
|
||||
<td className="py-4 text-right font-mono text-slate-400">${pos.avgPrice}</td>
|
||||
<td className="py-4 text-right font-mono text-slate-200 font-bold">${pos.currentPrice}</td>
|
||||
<td className="py-4 text-right font-mono text-slate-100 font-bold">${pos.currentValue.toFixed(2)}</td>
|
||||
<td className="py-4 text-right font-mono font-bold text-emerald-400">
|
||||
+${pos.pnl.toFixed(2)} (+{pos.pnlPct}%)
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Immutable Ledger History Table */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<div className="flex items-center space-x-2 border-b border-slate-800/80 pb-4">
|
||||
<History className="w-5 h-5 text-indigo-400" />
|
||||
<div>
|
||||
<h2 className="font-bold text-base text-white">不可变复式记账流水 (Immutable Ledger)</h2>
|
||||
<p className="text-xs text-slate-500">法定链上/链下资金变动流水,防篡改逐笔审计</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-800/60">
|
||||
{MOCK_LEDGER_ENTRIES.map((entry) => (
|
||||
<div key={entry.id} className="py-3.5 flex items-center justify-between text-xs">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`p-2 rounded-xl border ${
|
||||
entry.type === 'DEPOSIT'
|
||||
? 'bg-emerald-950/40 border-emerald-800/50 text-emerald-400'
|
||||
: 'bg-indigo-950/40 border-indigo-800/50 text-indigo-400'
|
||||
}`}
|
||||
>
|
||||
{entry.type === 'DEPOSIT' ? <ArrowDownLeft className="w-4 h-4" /> : <ArrowUpRight className="w-4 h-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-200">{entry.desc}</div>
|
||||
<div className="text-slate-500 text-[11px] mt-0.5">{entry.time} • 手续费: ${entry.fee}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right font-mono font-bold">
|
||||
<span className={entry.amount >= 0 ? 'text-emerald-400' : 'text-slate-200'}>
|
||||
{entry.amount >= 0 ? `+${entry.amount.toLocaleString()}` : `${entry.amount.toLocaleString()}`} WUSD
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,304 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { MarketItem } from '../explore/MarketExplore';
|
||||
import { ethers } from 'ethers';
|
||||
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck } from 'lucide-react';
|
||||
|
||||
interface TradingTerminalProps {
|
||||
market: MarketItem;
|
||||
}
|
||||
|
||||
export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
||||
const { account, signer, getCollateralContract, wusdBalance, refreshBalance, isCorrectNetwork } = useWeb3();
|
||||
|
||||
const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0);
|
||||
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
|
||||
const [amount, setAmount] = useState<string>('100');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
const activeOutcome = market.outcomes[selectedOutcomeIndex];
|
||||
const estShares = amount && Number(amount) > 0 ? (Number(amount) / activeOutcome.price).toFixed(2) : '0';
|
||||
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 handleTrade = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!signer || !account) {
|
||||
setStatusMsg({ type: 'error', text: '请先连接钱包以进行交易!' });
|
||||
return;
|
||||
}
|
||||
if (!isCorrectNetwork) {
|
||||
setStatusMsg({ type: 'error', text: '请切换至 Robinhood Testnet 网络!' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
|
||||
const collateral = getCollateralContract();
|
||||
if (!collateral) throw new Error('Collateral contract not loaded');
|
||||
|
||||
const amountWei = ethers.parseUnits(amount, 18);
|
||||
|
||||
// Step 1: Approve Collateral
|
||||
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权扣减 WUSD 保证金...' });
|
||||
const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256);
|
||||
await appTx.wait();
|
||||
|
||||
// Step 2: In web2-like demo / testnet, complete trade
|
||||
setStatusMsg({ type: 'success', text: `🎉 订单已撮合成交!已以 $${activeOutcome.price} 买入 ${estShares} 份 ${activeOutcome.label} 预测代币。` });
|
||||
refreshBalance();
|
||||
} catch (err: any) {
|
||||
console.error('Trade error:', err);
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left 2 Cols: Market Title, Chart, Curve Status */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Header Summary */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<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">
|
||||
{market.category} • Tier {market.tier}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2 text-xs font-mono text-slate-400">
|
||||
<span>结算截止: {market.endTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl sm:text-2xl font-black text-white leading-snug">
|
||||
{market.title}
|
||||
</h1>
|
||||
|
||||
<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">
|
||||
<span className="text-[11px] text-slate-500 block">24H 交易量</span>
|
||||
<span className="text-sm font-bold font-mono text-slate-200">${market.volume24h.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
||||
<span className="text-[11px] text-slate-500 block">流动性资金池</span>
|
||||
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
||||
<span className="text-[11px] text-slate-500 block">联合曲线状态</span>
|
||||
<span className="text-sm font-bold text-emerald-400">Power LDA (活跃)</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
||||
<span className="text-[11px] text-slate-500 block">协议手续费</span>
|
||||
<span className="text-sm font-bold font-mono text-slate-300">0.6%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* High-frequency Chart Simulation */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-slate-800/80 pb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Activity className="w-5 h-5 text-cyan-400" />
|
||||
<h3 className="font-bold text-sm text-white">实时胜率走势 & 概率曲线 (Real-Time Price & Probability)</h3>
|
||||
</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>
|
||||
|
||||
{/* 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">
|
||||
<form onSubmit={handleTrade} className="space-y-5">
|
||||
<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">
|
||||
<TrendingUp className="w-5 h-5 text-cyan-400" />
|
||||
<span>快速交易 (Fast Order)</span>
|
||||
</h2>
|
||||
|
||||
{/* Buy / Sell Tab */}
|
||||
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTradeSide('BUY')}
|
||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
||||
tradeSide === 'BUY' ? 'bg-cyan-600 text-white' : 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
买入 (Buy)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTradeSide('SELL')}
|
||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
||||
tradeSide === 'SELL' ? 'bg-rose-600 text-white' : 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
卖出 (Sell)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outcome Selectors */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-slate-400">选择预测结果 (Outcome)</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{market.outcomes.map((outcome, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={() => setSelectedOutcomeIndex(idx)}
|
||||
className={`p-3.5 rounded-xl border text-left transition-all ${
|
||||
selectedOutcomeIndex === idx
|
||||
? 'bg-cyan-950/40 border-cyan-500 text-white ring-1 ring-cyan-500'
|
||||
: 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div className="font-extrabold text-sm">{outcome.label}</div>
|
||||
<div className="text-xs font-mono mt-1 text-cyan-400 font-bold">${outcome.price} ({outcome.percentage}%)</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="font-semibold text-slate-400">交易金额 (Amount WUSD)</span>
|
||||
<span className="text-slate-500 font-mono">余额: {Number(wusdBalance).toFixed(2)} WUSD</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
<div className="absolute right-3 top-3 flex space-x-1">
|
||||
{['50', '100', '500', 'MAX'].map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{preset}
|
||||
</button>
|
||||
))}
|
||||
</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="flex justify-between text-slate-400">
|
||||
<span>预估获买份额 (Est. Shares)</span>
|
||||
<span className="font-mono font-bold text-slate-200">{estShares} 份</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>若胜出最终兑付 (Max Payout)</span>
|
||||
<span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>预期收益率 (Est. ROI)</span>
|
||||
<span className="font-mono font-bold text-cyan-400">+{estReturn}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !amount || Number(amount) <= 0}
|
||||
className="w-full py-3.5 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-cyan-500/25 flex items-center justify-center space-x-2 transition-all disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>正在上链成交中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
<span>立即买入 {activeOutcome.label}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Status Message */}
|
||||
{statusMsg && (
|
||||
<div
|
||||
className={`p-3.5 rounded-xl border flex items-start space-x-2.5 text-xs ${
|
||||
statusMsg.type === 'success'
|
||||
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
|
||||
: 'bg-rose-950/30 border-rose-800/50 text-rose-300'
|
||||
}`}
|
||||
>
|
||||
{statusMsg.type === 'success' ? (
|
||||
<Check className="w-4 h-4 flex-shrink-0 text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0 text-rose-400" />
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden break-words">
|
||||
<p>{statusMsg.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user