arbitrage-engine/frontend/app/page.tsx

118 lines
4.0 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, useCallback } from "react";
import { api, RatesResponse, StatsResponse, HistoryResponse } from "@/lib/api";
import RateCard from "@/components/RateCard";
import StatsCard from "@/components/StatsCard";
import FundingChart from "@/components/FundingChart";
export default function Dashboard() {
const [rates, setRates] = useState<RatesResponse | null>(null);
const [stats, setStats] = useState<StatsResponse | null>(null);
const [history, setHistory] = useState<HistoryResponse | null>(null);
const [status, setStatus] = useState<"loading" | "running" | "error">("loading");
const [lastUpdate, setLastUpdate] = useState<string>("");
const fetchRates = useCallback(async () => {
try {
const r = await api.rates();
setRates(r);
setStatus("running");
setLastUpdate(new Date().toLocaleTimeString("zh-CN"));
} catch {
setStatus("error");
}
}, []);
const fetchAll = useCallback(async () => {
try {
const [s, h] = await Promise.all([api.stats(), api.history()]);
setStats(s);
setHistory(h);
} catch {
// stats/history 失败不影响主界面
}
}, []);
useEffect(() => {
fetchRates();
fetchAll();
const rateInterval = setInterval(fetchRates, 2_000); // 价格 2秒刷新
const slowInterval = setInterval(fetchAll, 120_000); // 统计 2分钟刷新
return () => { clearInterval(rateInterval); clearInterval(slowInterval); };
}, [fetchRates, fetchAll]);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100"></h1>
<p className="text-slate-400 text-sm mt-1"> BTC / ETH </p>
</div>
<div className="flex items-center gap-2 text-sm">
<span
className={`w-2 h-2 rounded-full ${
status === "running"
? "bg-emerald-400 animate-pulse"
: status === "error"
? "bg-red-400"
: "bg-slate-500"
}`}
/>
<span className={status === "running" ? "text-emerald-400" : status === "error" ? "text-red-400" : "text-slate-400"}>
{status === "running" ? "运行中" : status === "error" ? "连接失败" : "加载中..."}
</span>
{lastUpdate && (
<span className="text-slate-500 ml-2"> {lastUpdate}</span>
)}
</div>
</div>
{/* Rate Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RateCard asset="BTC" data={rates?.BTC ?? null} />
<RateCard asset="ETH" data={rates?.ETH ?? null} />
</div>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<StatsCard
title="BTC 套利"
mean7d={stats.BTC.mean7d}
annualized={stats.BTC.annualized}
accent="cyan"
/>
<StatsCard
title="ETH 套利"
mean7d={stats.ETH.mean7d}
annualized={stats.ETH.annualized}
accent="violet"
/>
<StatsCard
title="50/50 组合"
mean7d={stats.combo.mean7d}
annualized={stats.combo.annualized}
accent="emerald"
/>
</div>
)}
{/* History Chart */}
{history && (
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6">
<h2 className="text-slate-200 font-semibold mb-4">7</h2>
<FundingChart history={history} />
</div>
)}
{/* Strategy note */}
<div className="rounded-lg border border-slate-700 bg-slate-800/30 px-5 py-3 text-sm text-slate-400">
<span className="text-cyan-400 font-medium"></span>
+ 8
</div>
</div>
);
}