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