arbitrage-engine/frontend/components/LiveTradesCard.tsx

125 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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">
<div>
<h2 className="font-semibold text-slate-800 text-sm"> · {symbol}/USDT</h2>
<p className="text-xs text-slate-400 mt-0.5">
<span className="text-emerald-600 font-medium"> 绿</span> = &nbsp;&nbsp;
<span className="text-red-500 font-medium"> </span> =
</p>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<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>
);
}