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