119 lines
4.4 KiB
TypeScript
119 lines
4.4 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { authFetch } from "@/lib/auth";
|
||
|
||
interface TradeRow {
|
||
time_ms: number;
|
||
price: number;
|
||
qty: number;
|
||
is_buyer_maker: number; // 0=主动买, 1=主动卖
|
||
agg_id: number;
|
||
}
|
||
|
||
interface TradesResponse {
|
||
symbol: string;
|
||
count: number;
|
||
data: TradeRow[];
|
||
}
|
||
|
||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "";
|
||
|
||
async function fetchLatestTrades(symbol: string): Promise<TradeRow[]> {
|
||
// 查最近5秒的原始trades
|
||
const end_ms = Date.now();
|
||
const start_ms = end_ms - 5000;
|
||
const res = await authFetch(
|
||
`/api/trades/latest?symbol=${symbol}&limit=30`,
|
||
{ cache: "no-store" }
|
||
);
|
||
if (!res.ok) return [];
|
||
const data: TradesResponse = await res.json();
|
||
return data.data || [];
|
||
}
|
||
|
||
function timeStr(ms: number): string {
|
||
const d = new Date(ms + 8 * 3600 * 1000);
|
||
return `${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}:${String(d.getUTCSeconds()).padStart(2,"0")}`;
|
||
}
|
||
|
||
export default function LiveTradesCard({ symbol }: { symbol: "BTC" | "ETH" }) {
|
||
const [trades, setTrades] = useState<TradeRow[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState("");
|
||
|
||
useEffect(() => {
|
||
let running = true;
|
||
|
||
const fetch = async () => {
|
||
try {
|
||
const data = await fetchLatestTrades(symbol);
|
||
if (running) {
|
||
setTrades(prev => {
|
||
// 合并新数据,按时间降序,最多保留50条
|
||
const existingIds = new Set(prev.map(t => t.agg_id));
|
||
const newOnes = data.filter(t => !existingIds.has(t.agg_id));
|
||
return [...newOnes, ...prev].slice(0, 50);
|
||
});
|
||
setLoading(false);
|
||
}
|
||
} catch (e) {
|
||
if (running) setError("加载失败");
|
||
}
|
||
};
|
||
|
||
fetch();
|
||
const iv = setInterval(fetch, 1000);
|
||
return () => { running = false; clearInterval(iv); };
|
||
}, [symbol]);
|
||
|
||
return (
|
||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
||
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
||
<h2 className="font-semibold text-slate-800 text-sm">实时成交流 · {symbol}/USDT</h2>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||
<span className="text-xs text-emerald-600">实时</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="overflow-hidden" style={{ maxHeight: 320 }}>
|
||
{loading ? (
|
||
<div className="flex items-center justify-center h-32 text-slate-400 text-sm">加载中...</div>
|
||
) : error ? (
|
||
<div className="flex items-center justify-center h-32 text-red-400 text-sm">{error}</div>
|
||
) : trades.length === 0 ? (
|
||
<div className="flex items-center justify-center h-32 text-slate-400 text-sm">等待数据...</div>
|
||
) : (
|
||
<table className="w-full text-xs">
|
||
<thead className="sticky top-0 bg-white z-10">
|
||
<tr className="bg-slate-50 border-b border-slate-100">
|
||
<th className="text-left px-3 py-2 text-slate-500 font-medium">价格(USDT)</th>
|
||
<th className="text-right px-3 py-2 text-slate-500 font-medium">数量</th>
|
||
<th className="text-right px-3 py-2 text-slate-500 font-medium">时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{trades.map(t => (
|
||
<tr key={t.agg_id} className="border-b border-slate-50 hover:bg-slate-50 transition-colors">
|
||
<td className={`px-3 py-1.5 font-mono font-semibold ${t.is_buyer_maker === 0 ? "text-emerald-600" : "text-red-500"}`}>
|
||
{t.is_buyer_maker === 0 ? "▲" : "▼"} {t.price.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}
|
||
</td>
|
||
<td className="px-3 py-1.5 text-right font-mono text-slate-600">
|
||
{t.qty >= 1000
|
||
? `${(t.qty / 1000).toFixed(2)}K`
|
||
: t.qty.toFixed(symbol === "BTC" ? 4 : 3)}
|
||
</td>
|
||
<td className="px-3 py-1.5 text-right text-slate-400 font-mono">
|
||
{timeStr(t.time_ms)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|