包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
155 lines
6.5 KiB
TypeScript
155 lines
6.5 KiB
TypeScript
"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>
|
||
);
|
||
}
|