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

166 lines
6.3 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 } from "react";
import Image from "next/image";
import { Button } from "@heroui/react";
import { useRouter } from "next/navigation";
import { useApp } from "@/contexts/AppContext";
import { useAccount } from "wagmi";
import { useCollateralBalance, useCollateralValue } from "@/hooks/useCollateral";
import { fetchProducts } from "@/lib/api/fundmarket";
import { useTokenBySymbol } from "@/hooks/useTokenBySymbol";
interface BorrowMarketItemData {
tokenType: string;
icon: string;
iconBg: string;
name: string;
category: string;
contractAddress: string;
}
// 渐变色映射(用于不同的 YT token
const GRADIENT_COLORS: Record<string, string> = {
'YT-A': 'linear-gradient(135deg, #FF8904 0%, #F54900 100%)',
'YT-B': 'linear-gradient(135deg, #00BBA7 0%, #007A55 100%)',
'YT-C': 'linear-gradient(135deg, #667EEA 0%, #764BA2 100%)',
};
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #6B7280 0%, #374151 100%)';
// 单个代币行组件
function BorrowMarketItem({ tokenData }: { tokenData: BorrowMarketItemData }) {
const { t } = useApp();
const router = useRouter();
const { isConnected } = useAccount();
const [mounted, setMounted] = useState(false);
// 查询该代币的抵押品余额
const ytToken = useTokenBySymbol(tokenData.tokenType);
const { formattedBalance, isLoading: isBalanceLoading } = useCollateralBalance(ytToken);
// 查询抵押品价值(用 valueRaw 避免 toFixed 截断误差)
const { valueRaw: collateralValueRaw } = useCollateralValue(ytToken);
useEffect(() => {
setMounted(true);
}, []);
return (
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-6 flex items-center gap-3">
{/* Token Info */}
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-10 h-10 shrink-0 rounded-full flex items-center justify-center overflow-hidden shadow-md"
style={{ background: tokenData.iconBg }}>
<Image src={tokenData.icon || "/assets/tokens/default.svg"} alt={tokenData.name} width={32} height={32} />
</div>
<div className="flex flex-col min-w-0">
<span className="text-body-default font-bold text-text-primary dark:text-white leading-[150%] truncate">
{tokenData.name}
</span>
<span className="text-caption-tiny font-regular text-text-tertiary dark:text-gray-400 leading-[150%] truncate">
{tokenData.category}
</span>
</div>
</div>
{/* Your Balance */}
<div className="flex flex-col items-center flex-1">
<span className="text-caption-tiny font-regular text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{t("lending.yourBalance")}
</span>
<span className="text-body-small font-bold text-text-primary dark:text-white leading-[150%] whitespace-nowrap">
{!mounted || isBalanceLoading ? '...' : `$${collateralValueRaw.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
</span>
<span className="text-[10px] text-text-tertiary dark:text-gray-400 whitespace-nowrap">
{!mounted || isBalanceLoading ? '' : `${parseFloat(formattedBalance).toLocaleString()} ${tokenData.name}`}
</span>
</div>
{/* View Details Button */}
<Button
className={`shrink-0 rounded-xl h-10 px-4 text-body-small font-bold transition-opacity ${
!isConnected || !tokenData.contractAddress
? '!bg-[#E5E7EB] !text-[#9CA1AF] cursor-not-allowed'
: 'bg-foreground text-background hover:opacity-80'
}`}
isDisabled={!isConnected || !tokenData.contractAddress}
onPress={() => router.push(`/lending/repay?token=${tokenData.tokenType}`)}
>
{t("lending.viewDetails") || "View Details"}
</Button>
</div>
);
}
// 主组件
export default function BorrowMarket({ market }: { market: 'USDC' | 'USDT' }) {
const { t } = useApp();
const [tokens, setTokens] = useState<BorrowMarketItemData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadTokens() {
setLoading(true);
try {
const products = await fetchProducts();
const tokenList: BorrowMarketItemData[] = products.map((product) => ({
tokenType: product.tokenSymbol,
icon: product.iconUrl,
iconBg: GRADIENT_COLORS[product.name] || 'linear-gradient(135deg, #667EEA 0%, #764BA2 100%)',
name: product.name,
category: product.category || `Yield Token ${product.name.split('-')[1]}`,
contractAddress: product.contractAddress || '',
}));
setTokens(tokenList);
} catch (error) {
console.error('Failed to fetch tokens:', error);
} finally {
setLoading(false);
}
}
loadTokens();
}, []);
if (loading) {
return (
<div className="flex flex-col gap-3">
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
{market} {t("lending.borrowMarket")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-6 h-[120px] animate-pulse">
<div className="flex items-center gap-8">
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3" />
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
</div>
</div>
</div>
))}
</div>
</div>
);
}
return (
<div className="flex flex-col gap-3">
{/* Title */}
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
{market} {t("lending.borrowMarket")}
</h2>
{/* Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{tokens.map((tokenData) => (
<BorrowMarketItem
key={tokenData.tokenType}
tokenData={tokenData}
/>
))}
</div>
</div>
);
}