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 { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { HistoryPoint } from "@/lib/api";
|
|
||||||
import {
|
|
||||||
LineChart, Line, XAxis, YAxis, Tooltip, Legend,
|
|
||||||
ResponsiveContainer, ReferenceLine
|
|
||||||
} from "recharts";
|
|
||||||
|
|
||||||
export default function HistoryPage() {
|
export default function HistoryPage() {
|
||||||
const [btc, setBtc] = useState<HistoryPoint[]>([]);
|
redirect("/");
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
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 RateCard from "@/components/RateCard";
|
||||||
import StatsCard from "@/components/StatsCard";
|
import StatsCard from "@/components/StatsCard";
|
||||||
import FundingChart from "@/components/FundingChart";
|
import FundingChart from "@/components/FundingChart";
|
||||||
|
import {
|
||||||
|
LineChart, Line, XAxis, YAxis, Tooltip, Legend,
|
||||||
|
ResponsiveContainer, ReferenceLine
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [rates, setRates] = useState<RatesResponse | null>(null);
|
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()]);
|
const [s, h] = await Promise.all([api.stats(), api.history()]);
|
||||||
setStats(s);
|
setStats(s);
|
||||||
setHistory(h);
|
setHistory(h);
|
||||||
} catch {
|
} catch {}
|
||||||
// stats/history 失败不影响主界面
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -42,6 +44,21 @@ export default function Dashboard() {
|
|||||||
return () => { clearInterval(rateInterval); clearInterval(slowInterval); };
|
return () => { clearInterval(rateInterval); clearInterval(slowInterval); };
|
||||||
}, [fetchRates, fetchAll]);
|
}, [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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -77,7 +94,7 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* History Chart */}
|
{/* 7天走势图(FundingChart) */}
|
||||||
{history && (
|
{history && (
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
<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>
|
<h2 className="text-slate-800 font-semibold mb-4">过去7天资金费率走势</h2>
|
||||||
@ -85,6 +102,55 @@ export default function Dashboard() {
|
|||||||
</div>
|
</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 */}
|
{/* Strategy note */}
|
||||||
<div className="rounded-lg border border-blue-100 bg-blue-50 px-5 py-3 text-sm text-slate-600">
|
<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>
|
<span className="text-blue-600 font-medium">策略原理:</span>
|
||||||
@ -93,3 +159,4 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ const navLinks = [
|
|||||||
{ href: "/", label: "仪表盘" },
|
{ href: "/", label: "仪表盘" },
|
||||||
{ href: "/kline", label: "K线" },
|
{ href: "/kline", label: "K线" },
|
||||||
{ href: "/live", label: "实时" },
|
{ href: "/live", label: "实时" },
|
||||||
{ href: "/history", label: "历史" },
|
|
||||||
{ href: "/signals", label: "信号" },
|
{ href: "/signals", label: "信号" },
|
||||||
{ href: "/about", label: "说明" },
|
{ href: "/about", label: "说明" },
|
||||||
];
|
];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user