feat: merge history into dashboard, remove history nav entry
This commit is contained in:
parent
eebbfd456c
commit
1a7a77e183
@ -1,91 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { HistoryPoint } from "@/lib/api";
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, Tooltip, Legend,
|
||||
ResponsiveContainer, ReferenceLine
|
||||
} from "recharts";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function HistoryPage() {
|
||||
const [btc, setBtc] = useState<HistoryPoint[]>([]);
|
||||
const [eth, setEth] = useState<HistoryPoint[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/history")
|
||||
.then((r) => r.json())
|
||||
.then((d) => { setBtc(d.BTC ?? []); setEth(d.ETH ?? []); setLoading(false); })
|
||||
.catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const btcMap = new Map(btc.map((p) => [p.timestamp.slice(0, 13), p.fundingRate * 100]));
|
||||
const ethMap = new Map(eth.map((p) => [p.timestamp.slice(0, 13), p.fundingRate * 100]));
|
||||
const allTimes = Array.from(new Set([...btcMap.keys(), ...ethMap.keys()])).sort();
|
||||
const chartData = allTimes.slice(-42).map((t) => ({
|
||||
time: t.slice(5).replace("T", " "),
|
||||
BTC: btcMap.get(t) ?? null,
|
||||
ETH: ethMap.get(t) ?? null,
|
||||
}));
|
||||
|
||||
const tableData = allTimes.slice().reverse().slice(0, 50).map((t) => ({
|
||||
time: t.replace("T", " "),
|
||||
btc: btcMap.get(t),
|
||||
eth: ethMap.get(t),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">历史费率</h1>
|
||||
<p className="text-slate-500 text-sm mt-1">过去7天 BTC / ETH 资金费率记录</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-slate-500">加载中...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6">
|
||||
<h2 className="text-slate-800 font-semibold mb-4">费率走势(过去7天)</h2>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<LineChart data={chartData} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
|
||||
<XAxis dataKey="time" tick={{ fill: "#64748b", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
|
||||
<YAxis tick={{ fill: "#64748b", fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => `${v.toFixed(3)}%`} width={60} />
|
||||
<Tooltip formatter={(v) => [`${Number(v).toFixed(4)}%`]} contentStyle={{ background: "#1e293b", border: "1px solid #475569", borderRadius: 8 }} />
|
||||
<Legend wrapperStyle={{ fontSize: 12, color: "#94a3b8" }} />
|
||||
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 2" />
|
||||
<Line type="monotone" dataKey="BTC" stroke="#06b6d4" strokeWidth={1.5} dot={false} connectNulls />
|
||||
<Line type="monotone" dataKey="ETH" stroke="#8b5cf6" strokeWidth={1.5} dot={false} connectNulls />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-800">
|
||||
<th className="text-left px-4 py-3 text-slate-500 font-medium">时间</th>
|
||||
<th className="text-right px-4 py-3 text-blue-600 font-medium">BTC 费率</th>
|
||||
<th className="text-right px-4 py-3 text-violet-400 font-medium">ETH 费率</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableData.map((row, i) => (
|
||||
<tr key={i} className="border-b border-slate-200/50 hover:bg-slate-50">
|
||||
<td className="px-4 py-2 text-slate-500 font-mono text-xs">{row.time}</td>
|
||||
<td className={`px-4 py-2 text-right font-mono text-xs ${row.btc == null ? "text-slate-500" : row.btc >= 0 ? "text-emerald-400" : "text-red-400"}`}>
|
||||
{row.btc != null ? `${row.btc.toFixed(4)}%` : "--"}
|
||||
</td>
|
||||
<td className={`px-4 py-2 text-right font-mono text-xs ${row.eth == null ? "text-slate-500" : row.eth >= 0 ? "text-emerald-400" : "text-red-400"}`}>
|
||||
{row.eth != null ? `${row.eth.toFixed(4)}%` : "--"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { api, RatesResponse, StatsResponse, HistoryResponse } from "@/lib/api";
|
||||
import { api, RatesResponse, StatsResponse, HistoryResponse, HistoryPoint } from "@/lib/api";
|
||||
import RateCard from "@/components/RateCard";
|
||||
import StatsCard from "@/components/StatsCard";
|
||||
import FundingChart from "@/components/FundingChart";
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, Tooltip, Legend,
|
||||
ResponsiveContainer, ReferenceLine
|
||||
} from "recharts";
|
||||
|
||||
export default function Dashboard() {
|
||||
const [rates, setRates] = useState<RatesResponse | null>(null);
|
||||
@ -29,9 +33,7 @@ export default function Dashboard() {
|
||||
const [s, h] = await Promise.all([api.stats(), api.history()]);
|
||||
setStats(s);
|
||||
setHistory(h);
|
||||
} catch {
|
||||
// stats/history 失败不影响主界面
|
||||
}
|
||||
} catch {}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -42,6 +44,21 @@ export default function Dashboard() {
|
||||
return () => { clearInterval(rateInterval); clearInterval(slowInterval); };
|
||||
}, [fetchRates, fetchAll]);
|
||||
|
||||
// 历史费率表格数据
|
||||
const btcMap = new Map((history?.BTC ?? []).map((p: HistoryPoint) => [p.timestamp.slice(0, 13), p.fundingRate * 100]));
|
||||
const ethMap = new Map((history?.ETH ?? []).map((p: HistoryPoint) => [p.timestamp.slice(0, 13), p.fundingRate * 100]));
|
||||
const allTimes = Array.from(new Set([...btcMap.keys(), ...ethMap.keys()])).sort();
|
||||
const historyChartData = allTimes.slice(-42).map((t) => ({
|
||||
time: t.slice(5).replace("T", " "),
|
||||
BTC: btcMap.get(t) ?? null,
|
||||
ETH: ethMap.get(t) ?? null,
|
||||
}));
|
||||
const tableData = allTimes.slice().reverse().slice(0, 50).map((t) => ({
|
||||
time: t.replace("T", " "),
|
||||
btc: btcMap.get(t),
|
||||
eth: ethMap.get(t),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@ -77,7 +94,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* History Chart */}
|
||||
{/* 7天走势图(FundingChart) */}
|
||||
{history && (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
||||
<h2 className="text-slate-800 font-semibold mb-4">过去7天资金费率走势</h2>
|
||||
@ -85,6 +102,55 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 历史费率折线图 */}
|
||||
{allTimes.length > 0 && (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
||||
<h2 className="text-slate-800 font-semibold mb-4">历史费率走势(过去7天,每8小时结算)</h2>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={historyChartData} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
|
||||
<XAxis dataKey="time" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
|
||||
<YAxis tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => `${v.toFixed(3)}%`} width={60} />
|
||||
<Tooltip formatter={(v) => [`${Number(v).toFixed(4)}%`]} contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} />
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="4 2" />
|
||||
<Line type="monotone" dataKey="BTC" stroke="#2563eb" strokeWidth={1.5} dot={false} connectNulls />
|
||||
<Line type="monotone" dataKey="ETH" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 历史费率明细表 */}
|
||||
{tableData.length > 0 && (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-100">
|
||||
<h2 className="text-slate-800 font-semibold">历史费率明细(最近50条)</h2>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="text-left px-4 py-3 text-slate-500 font-medium">时间</th>
|
||||
<th className="text-right px-4 py-3 text-blue-600 font-medium">BTC 费率</th>
|
||||
<th className="text-right px-4 py-3 text-violet-600 font-medium">ETH 费率</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableData.map((row, i) => (
|
||||
<tr key={i} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-4 py-2 text-slate-500 font-mono text-xs">{row.time}</td>
|
||||
<td className={`px-4 py-2 text-right font-mono text-xs ${row.btc == null ? "text-slate-400" : row.btc >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
||||
{row.btc != null ? `${row.btc.toFixed(4)}%` : "--"}
|
||||
</td>
|
||||
<td className={`px-4 py-2 text-right font-mono text-xs ${row.eth == null ? "text-slate-400" : row.eth >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
||||
{row.eth != null ? `${row.eth.toFixed(4)}%` : "--"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Strategy note */}
|
||||
<div className="rounded-lg border border-blue-100 bg-blue-50 px-5 py-3 text-sm text-slate-600">
|
||||
<span className="text-blue-600 font-medium">策略原理:</span>
|
||||
@ -93,3 +159,4 @@ export default function Dashboard() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ const navLinks = [
|
||||
{ href: "/", label: "仪表盘" },
|
||||
{ href: "/kline", label: "K线" },
|
||||
{ href: "/live", label: "实时" },
|
||||
{ href: "/history", label: "历史" },
|
||||
{ href: "/signals", label: "信号" },
|
||||
{ href: "/about", label: "说明" },
|
||||
];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user