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