init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
154
webapp/components/product/OverviewTab.tsx
Normal file
154
webapp/components/product/OverviewTab.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user