"use client"; import { useState, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { useApp } from "@/contexts/AppContext"; import * as echarts from "echarts"; interface HistoryPoint { time: string; ts: number; poolAPR: number; alpPrice: number; feeSurplus: number; poolValue: number; } export default function PriceHistoryCard() { const { t } = useApp(); const [activeTab, setActiveTab] = useState<"price" | "apr">("price"); const chartRef = useRef(null); const chartInstance = useRef(null); const { data: history = [], isLoading } = useQuery({ queryKey: ["alp-history"], queryFn: async () => { const res = await fetch("/api/alp/history?days=30"); const json = await res.json(); return json.data ?? []; }, refetchInterval: 5 * 60 * 1000, }); const isEmpty = history.length < 2; const labels = isEmpty ? [] : history.map(p => p.time); const priceData = isEmpty ? [] : history.map(p => p.alpPrice); const aprData = isEmpty ? [] : history.map(p => p.poolAPR); const activeData = activeTab === "price" ? priceData : aprData; const activeColor = activeTab === "price" ? "#10b981" : "#1447e6"; const areaColor0 = activeTab === "price" ? "rgba(16,185,129,0.3)" : "rgba(20,71,230,0.25)"; const areaColor1 = activeTab === "price" ? "rgba(16,185,129,0)" : "rgba(20,71,230,0)"; const suffix = activeTab === "price" ? " USDC" : "%"; const precision = activeTab === "price" ? 4 : 4; const highest = activeData.length > 0 ? Math.max(...activeData) : 0; const lowest = activeData.length > 0 ? Math.min(...activeData) : 0; const current = activeData.length > 0 ? activeData[activeData.length - 1] : 0; // Avg APR from feeSurplus / poolValue delta const actualDays = history.length >= 2 ? (history[history.length - 1].ts - history[0].ts) / 86400 : 0; const avgAPR = (() => { if (history.length < 2 || actualDays <= 0) return 0; const first = history[0]; const last = history[history.length - 1]; const surplus = (last.feeSurplus ?? 0) - (first.feeSurplus ?? 0); const pv = last.poolValue ?? 0; if (pv <= 0 || surplus <= 0) return 0; return surplus / pv / actualDays * 365 * 100; })(); const periodLabel = actualDays >= 1 ? `Last ${Math.round(actualDays)} days` : actualDays > 0 ? `Last ${Math.round(actualDays * 24)}h` : "Last 30 days"; const fmt = (v: number) => activeTab === "price" ? `$${v.toFixed(precision)}` : `${v.toFixed(precision)}%`; const updateChart = () => { if (!chartInstance.current) return; // 自适应 Y 轴:以数据范围为基础,上下各留 20% 的 padding const yMin = activeData.length > 0 ? Math.min(...activeData) : 0; const yMax = activeData.length > 0 ? Math.max(...activeData) : 1; const range = yMax - yMin || yMax * 0.01 || 0.01; const yAxisMin = yMin - range * 0.2; const yAxisMax = yMax + range * 0.2; chartInstance.current.setOption({ grid: { left: 0, right: 0, top: 10, bottom: 24 }, tooltip: { trigger: "axis", confine: true, backgroundColor: "rgba(17,24,39,0.9)", borderColor: "#374151", textStyle: { color: "#f9fafb", fontSize: 12 }, formatter: (params: any) => { const d = params[0]; const val = Number(d.value).toFixed(precision); const display = activeTab === "price" ? `$${val}` : `${val}%`; return `
${d.name}
${display}
`; }, }, xAxis: { type: "category", data: labels, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { show: true, color: "#9ca3af", fontSize: 11, interval: Math.max(0, Math.floor(labels.length / 6) - 1), }, boundaryGap: false, }, yAxis: { type: "value", min: yAxisMin, max: yAxisMax, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false }, }, series: [{ data: activeData, type: "line", smooth: true, symbol: "circle", symbolSize: 5, lineStyle: { color: activeColor, width: 2 }, itemStyle: { color: activeColor }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: areaColor0 }, { offset: 1, color: areaColor1 }, ], }, }, }], }, true); }; useEffect(() => { const frame = requestAnimationFrame(() => { if (!chartRef.current) return; if (!chartInstance.current) { chartInstance.current = echarts.init(chartRef.current); } updateChart(); chartInstance.current?.resize(); }); const handleResize = () => chartInstance.current?.resize(); window.addEventListener("resize", handleResize); return () => { cancelAnimationFrame(frame); window.removeEventListener("resize", handleResize); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab, history]); useEffect(() => { return () => { chartInstance.current?.dispose(); }; }, []); return (
{/* Tabs */}
{/* Period label + avg for APR */}
{periodLabel}
{activeTab === "apr" && (
Avg APR: {avgAPR.toFixed(4)}%
)}
{/* Chart */}
{isLoading ? (
{t("common.loading")}
) : isEmpty ? (
{t("common.noData")}
) : (
)}
{/* Stats */}
{[ { label: t("alp.highest"), value: isEmpty ? "--" : fmt(highest) }, { label: t("alp.lowest"), value: isEmpty ? "--" : fmt(lowest) }, { label: t("alp.current"), value: isEmpty ? "--" : fmt(current) }, ].map(({ label, value }) => (
{label}
{value}
))}
); }