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

138 lines
4.9 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 { useEffect, useRef } from "react";
import Image from "next/image";
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from "@heroui/react";
import { useTokenList } from "@/hooks/useTokenList";
import { Token } from "@/lib/api/tokens";
interface TokenSelectorProps {
selectedToken?: Token;
onSelect: (token: Token) => void;
filterTypes?: ('stablecoin' | 'yield-token')[];
disabled?: boolean;
defaultSymbol?: string;
}
export default function TokenSelector({
selectedToken,
onSelect,
filterTypes = ['stablecoin', 'yield-token'],
disabled = false,
defaultSymbol,
}: TokenSelectorProps) {
const hasInitialized = useRef(false);
const { tokens: allTokens, isLoading } = useTokenList();
const tokens = allTokens.filter(t => filterTypes.includes(t.tokenType));
// 数据加载完成后自动选中默认 token仅初始化一次
useEffect(() => {
if (hasInitialized.current || selectedToken || tokens.length === 0) return;
hasInitialized.current = true;
const target = defaultSymbol
? (tokens.find(t => t.symbol === defaultSymbol) ?? tokens[0])
: tokens[0];
onSelect(target);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tokens]);
if (isLoading) {
return (
<button className="bg-white dark:bg-gray-700 rounded-full border border-gray-400 dark:border-gray-500 px-4 h-[46px] flex items-center gap-2 opacity-50 cursor-not-allowed">
<div className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-600 animate-pulse" />
<span className="text-body-default font-bold text-text-primary dark:text-white">
Loading...
</span>
</button>
);
}
return (
<Dropdown shouldBlockScroll={false}>
<DropdownTrigger>
<button
disabled={disabled}
className={`bg-white dark:bg-gray-700 rounded-full border border-gray-400 dark:border-gray-500 px-4 h-[46px] flex items-center justify-between gap-2 outline-none focus:outline-none transition-all duration-200 ease-in-out ${
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary dark:hover:border-primary-dark hover:shadow-md'
}`}
>
{selectedToken ? (
<>
<div className="flex items-center gap-2">
<Image
src={selectedToken.iconUrl || '/assets/tokens/default.svg'}
alt={selectedToken.symbol}
width={32}
height={32}
className="rounded-full transition-opacity duration-150"
priority
onError={(e) => {
(e.target as HTMLImageElement).src = '/assets/tokens/default.svg';
}}
/>
<span className="text-body-default font-bold text-text-primary dark:text-white transition-opacity duration-150">
{selectedToken.symbol}
</span>
</div>
<svg
className="w-4 h-4 text-text-tertiary dark:text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</>
) : (
<span className="text-body-default text-text-tertiary dark:text-gray-400">
No tokens available
</span>
)}
</button>
</DropdownTrigger>
<DropdownMenu
aria-label="Token selection"
onAction={(key) => {
const token = tokens.find(t => t.symbol === key);
if (token) onSelect(token);
}}
>
{tokens.map((token) => (
<DropdownItem
key={token.symbol}
className="hover:bg-bg-subtle dark:hover:bg-gray-700 rounded-lg"
textValue={token.symbol}
>
<div className="flex items-center gap-3 py-1">
<Image
src={token.iconUrl || '/assets/tokens/default.svg'}
alt={token.symbol || 'token'}
width={32}
height={32}
className="rounded-full"
onError={(e) => {
(e.target as HTMLImageElement).src = '/assets/tokens/default.svg';
}}
/>
<div className="flex flex-col">
<span className="text-body-default font-bold text-text-primary dark:text-white">
{token.symbol}
</span>
<span className="text-caption-tiny text-text-tertiary dark:text-gray-400">
{token.name}
</span>
</div>
</div>
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
);
}