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

155 lines
6.5 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 { useState, useEffect, useMemo } from 'react';
import { useAccount, useReadContract } from 'wagmi';
import { bscTestnet } from 'wagmi/chains';
import { useQuery } from '@tanstack/react-query';
import { useTokenBalance } from '@/hooks/useBalance';
import { useTokenBySymbol } from '@/hooks/useTokenBySymbol';
import { useApp } from '@/contexts/AppContext';
import { fetchContracts } from '@/lib/api/contracts';
import { abis } from '@/lib/contracts';
import ProductHeader from "./ProductHeader";
import StatsCards from "@/components/fundmarket/StatsCards";
import AssetOverviewCard from "./AssetOverviewCard";
import APYHistoryCard from "./APYHistoryCard";
import AssetDescriptionCard from "./AssetDescriptionCard";
import MintSwapPanel from "./MintSwapPanel";
import ProtocolInformation from "./ProtocolInformation";
import PerformanceAnalysis from "./PerformanceAnalysis";
import Season1Rewards from "./Season1Rewards";
import AssetCustodyVerification from "./AssetCustodyVerification";
import { ProductDetail } from "@/lib/api/fundmarket";
interface OverviewTabProps {
product: ProductDetail;
}
const formatUSD = (v: number) =>
"$" + v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
export default function OverviewTab({ product }: OverviewTabProps) {
const { t } = useApp();
const { isConnected, chainId } = useAccount();
const ytToken = useTokenBySymbol(product.tokenSymbol);
const { formattedBalance: ytBalance } = useTokenBalance(ytToken?.contractAddress, ytToken?.decimals ?? 18);
// Shared getVaultInfo read — single source of truth for all children
const { data: contractConfigs = [] } = useQuery({
queryKey: ['contract-registry'],
queryFn: fetchContracts,
staleTime: 5 * 60 * 1000,
});
const factoryAddress = useMemo(() => {
const c = contractConfigs.find(c => c.name === 'YTAssetFactory' && c.chain_id === product.chainId);
return c?.address as `0x${string}` | undefined;
}, [contractConfigs, product.chainId]);
const hasContract = !!factoryAddress && !!product.contractAddress;
const { data: vaultInfo, isLoading: isVaultLoading, refetch: refetchVault } = useReadContract({
address: factoryAddress,
abi: abis.YTAssetFactory as any,
functionName: 'getVaultInfo',
args: product.contractAddress ? [product.contractAddress as `0x${string}`] : undefined,
chainId: product.chainId,
query: { enabled: hasContract },
});
// 3s timeout — after this, fall back to DB snapshot values
const [vaultTimedOut, setVaultTimedOut] = useState(false);
useEffect(() => {
if (!hasContract) return;
setVaultTimedOut(false);
const t = setTimeout(() => setVaultTimedOut(true), 3000);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasContract]);
const vaultReady = !hasContract || vaultInfo !== undefined || vaultTimedOut;
// Real-time TVL: getVaultInfo[1] = totalAssets (USDC)
const usdcDecimals = product.chainId === 421614 ? 6 : 18;
const totalAssetsRaw: bigint = vaultInfo ? ((vaultInfo as any[])[1] as bigint) ?? 0n : 0n;
const liveTVL = totalAssetsRaw > 0n ? Number(totalAssetsRaw) / Math.pow(10, usdcDecimals) : null;
const tvlDisplay = !vaultReady
? "..."
: liveTVL !== null
? formatUSD(liveTVL)
: formatUSD(product.tvlUsd);
const balanceUSD = parseFloat(ytBalance) * (product.currentPrice || 1);
const balanceDisplay = !isConnected
? "--"
: chainId !== bscTestnet.id
? "-- (Switch to BSC)"
: formatUSD(balanceUSD);
const volume24hDisplay = product.volume24hUsd > 0 ? formatUSD(product.volume24hUsd) : "--";
const volChange = product.volumeChangeVsAvg ?? 0;
const volChangeStr = volChange !== 0
? `${volChange > 0 ? "↑" : "↓"} ${Math.abs(volChange).toFixed(0)}% vs Avg`
: "";
const stats = [
{ label: t("productPage.totalValueLocked"), value: tvlDisplay, change: "", isPositive: true },
{ label: t("productPage.volume24h"), value: volume24hDisplay, change: volChangeStr, isPositive: volChange >= 0 },
{ label: t("productPage.cumulativeYield"), value: formatUSD(0), change: "", isPositive: true },
{ label: t("productPage.yourTotalBalance"), value: balanceDisplay, change: "", isPositive: true },
{ label: t("productPage.yourTotalEarning"), value: "--", change: "", isPositive: true },
];
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8 w-full">
{/* ① Header + Stats — 始终在最顶部桌面端跨3列 */}
<div className="order-1 md:col-span-3 min-w-0">
<div className="bg-white dark:bg-gray-800 border border-[#f3f4f6] dark:border-gray-700 flex flex-col overflow-hidden rounded-2xl md:rounded-3xl p-4 md:p-8 gap-5 md:gap-6">
<ProductHeader product={product} />
<StatsCards stats={stats} />
</div>
</div>
{/* ② 交易板 + Protocol — 移动端排第2桌面端在右列 */}
<div className="order-2 md:order-3 md:col-span-1 min-w-0 flex flex-col gap-6 md:gap-8">
<MintSwapPanel
tokenType={product.tokenSymbol}
decimals={product.decimals}
onVaultRefresh={refetchVault}
/>
<ProtocolInformation product={product} />
</div>
{/* ③ Asset Overview + APY + Description — 移动端排第3桌面端在左列 */}
<div className="order-3 md:order-2 md:col-span-2 min-w-0 flex flex-col gap-6 md:gap-8">
<AssetOverviewCard
product={product}
vaultInfo={vaultInfo as any[] | undefined}
isVaultLoading={isVaultLoading}
vaultTimedOut={vaultTimedOut}
/>
<APYHistoryCard productId={product.id} />
<div id="asset-description">
<AssetDescriptionCard product={product} />
</div>
</div>
{/* ④ Season 1 Rewards — 全宽 */}
<div className="order-4 md:col-span-3 min-w-0">
<Season1Rewards />
</div>
{/* ⑤ Performance Analysis (Daily Net Returns) — 全宽 */}
<div id="performance-analysis" className="order-5 md:col-span-3 min-w-0">
<PerformanceAnalysis productId={product.id} />
</div>
{/* ⑥ Asset Custody & Verification — 全宽 */}
<div id="asset-custody" className="order-6 md:col-span-3 min-w-0">
<AssetCustodyVerification product={product} />
</div>
</div>
);
}