Files
assetx/webapp/components/lending/supply/SupplyContent.tsx
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

179 lines
6.7 KiB
TypeScript
Raw Permalink 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 { useState, useEffect, useRef } from "react";
import Image from "next/image";
import { Button } from "@heroui/react";
import { useApp } from "@/contexts/AppContext";
import { useQuery } from "@tanstack/react-query";
import { useChainId } from "wagmi";
import * as echarts from "echarts";
import { fetchLendingAPYHistory } from "@/lib/api/lending";
type Period = "1W" | "1M" | "1Y";
export default function SupplyContent() {
const { t } = useApp();
const chainId = useChainId();
const [selectedPeriod, setSelectedPeriod] = useState<Period>("1W");
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
const { data: apyData, isLoading } = useQuery({
queryKey: ["lending-apy-history", selectedPeriod, chainId],
queryFn: () => fetchLendingAPYHistory(selectedPeriod, chainId),
staleTime: 5 * 60 * 1000,
});
const chartData = apyData?.history.map((p) => p.supply_apy) ?? [];
const xLabels = apyData?.history.map((p) => {
const d = new Date(p.time);
if (selectedPeriod === "1W") return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, "0")}:00`;
if (selectedPeriod === "1M") return `${d.getMonth() + 1}/${d.getDate()}`;
return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
}) ?? [];
const currentAPY = apyData?.current_supply_apy ?? 0;
const apyChange = apyData?.apy_change ?? 0;
// 初始化 & 销毁(只在 mount/unmount 执行)
useEffect(() => {
if (!chartRef.current) return;
chartInstance.current = echarts.init(chartRef.current);
const observer = new ResizeObserver(() => chartInstance.current?.resize());
observer.observe(chartRef.current);
return () => {
observer.disconnect();
chartInstance.current?.dispose();
chartInstance.current = null;
};
}, []);
// 更新图表数据period/data 变化时)
useEffect(() => {
if (!chartInstance.current) return;
chartInstance.current.setOption({
grid: { left: 0, right: 0, top: 10, bottom: 0 },
tooltip: {
trigger: "axis",
show: true,
confine: true,
backgroundColor: "rgba(17, 24, 39, 0.9)",
borderColor: "#374151",
textStyle: { color: "#f9fafb", fontSize: 12, fontWeight: 500 },
formatter: (params: any) => {
const d = params[0];
return `<div style="padding: 4px 8px;">
<span style="color: #9ca3af; font-size: 11px;">${d.axisValueLabel}</span><br/>
<span style="color: #10b981; font-weight: 600; font-size: 14px;">${Number(d.value).toFixed(4)}%</span>
</div>`;
},
},
xAxis: {
type: "category",
data: xLabels,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: xLabels.length > 0 && xLabels.length <= 24,
color: "#9ca3af",
fontSize: 10,
fontWeight: 500,
interval: Math.max(0, Math.floor(xLabels.length / 7) - 1),
},
},
yAxis: {
type: "value",
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false },
},
series: [
{
data: chartData,
type: "line",
smooth: true,
symbol: chartData.length <= 30 ? "circle" : "none",
symbolSize: 4,
lineStyle: { color: "#10b981", width: 2 },
itemStyle: { color: "#10b981" },
areaStyle: {
color: {
type: "linear",
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: "rgba(16, 185, 129, 0.3)" },
{ offset: 1, color: "rgba(16, 185, 129, 0)" },
],
},
},
},
],
}, true);
}, [chartData, xLabels]);
return (
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-4 w-full shadow-md">
{/* Header */}
<div className="flex items-center gap-3 flex-shrink-0">
<Image src="/assets/tokens/usd-coin-usdc-logo-10.svg" alt="USDC" width={32} height={32} />
<h1 className="text-heading-h2 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.01em]">
{t("supply.usdcLendPool")}
</h1>
</div>
{/* Historical APY Section */}
<div className="flex items-start justify-between flex-shrink-0">
{/* Left - APY Display */}
<div className="flex flex-col gap-1">
<span className="text-[12px] font-bold text-text-tertiary dark:text-gray-400 leading-[16px] tracking-[1.2px] uppercase">
{t("supply.historicalApy")}
</span>
<div className="flex items-center gap-2">
<span className="text-heading-h2 font-bold text-[#10b981] dark:text-green-400 leading-[130%] tracking-[-0.01em]">
{isLoading ? "--" : currentAPY > 0 ? `${currentAPY.toFixed(4)}%` : "--"}
</span>
{!isLoading && apyData && apyData.history.length > 1 && (
<div className={`rounded-full px-2 py-0.5 flex items-center justify-center ${apyChange >= 0 ? "bg-[#e1f8ec] dark:bg-green-900/30" : "bg-red-50 dark:bg-red-900/30"}`}>
<span className={`text-caption-tiny font-bold leading-[150%] tracking-[0.01em] ${apyChange >= 0 ? "text-[#10b981] dark:text-green-400" : "text-red-500 dark:text-red-400"}`}>
{apyChange >= 0 ? "+" : ""}{apyChange.toFixed(4)}%
</span>
</div>
)}
</div>
</div>
{/* Right - Period Selector */}
<div className="bg-[#F9FAFB] dark:bg-gray-700 rounded-xl p-1 flex items-center gap-1">
{(["1W", "1M", "1Y"] as Period[]).map((p) => (
<Button
key={p}
size="sm"
variant={selectedPeriod === p ? "solid" : "light"}
onPress={() => setSelectedPeriod(p)}
>
{p}
</Button>
))}
</div>
</div>
{/* Chart Section — 始终渲染 div避免 echarts 实例因 DOM 销毁而失效 */}
<div className="w-full relative" style={{ height: "260px" }}>
<div
ref={chartRef}
className="w-full h-full"
style={{ background: "linear-gradient(0deg, rgba(16, 185, 129, 0.1) 0%, transparent 100%)" }}
/>
{/* Loading 遮罩 */}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/60 dark:bg-gray-800/60">
<span className="text-sm text-text-tertiary dark:text-gray-400">{t("common.loading")}</span>
</div>
)}
</div>
</div>
);
}