142 lines
5.6 KiB
TypeScript
142 lines
5.6 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Search } from "lucide-react";
|
|
import { searchLucideIcons, ICON_CATEGORIES } from '@/config/lucideIcons';
|
|
import DynamicIcon from '@/components/DynamicIcon';
|
|
|
|
interface LucideIconSelectProps {
|
|
/** 当前选中的图标名称 */
|
|
value?: string;
|
|
/** 图标变化回调 */
|
|
onChange?: (iconName: string) => void;
|
|
/** 是否显示弹窗 */
|
|
open: boolean;
|
|
/** 关闭弹窗回调 */
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
/**
|
|
* Lucide 图标选择器组件
|
|
*
|
|
* 支持:
|
|
* - 搜索图标
|
|
* - 分类浏览
|
|
* - 点击选择
|
|
*/
|
|
const LucideIconSelect: React.FC<LucideIconSelectProps> = ({
|
|
value,
|
|
onChange,
|
|
open,
|
|
onOpenChange
|
|
}) => {
|
|
const [search, setSearch] = useState('');
|
|
const [category, setCategory] = useState('all');
|
|
|
|
// 过滤图标列表
|
|
const iconList = useMemo(() => {
|
|
return searchLucideIcons(search, category);
|
|
}, [search, category]);
|
|
|
|
// 选择图标
|
|
const handleSelect = (iconName: string) => {
|
|
onChange?.(iconName);
|
|
onOpenChange(false);
|
|
setSearch('');
|
|
setCategory('all');
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>选择图标</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* 搜索框 */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
placeholder="搜索图标名称..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
|
|
{/* 分类标签 */}
|
|
<Tabs value={category} onValueChange={setCategory}>
|
|
<TabsList className="w-full flex-wrap h-auto">
|
|
<TabsTrigger value="all" className="text-xs">全部 ({iconList.length})</TabsTrigger>
|
|
{Object.keys(ICON_CATEGORIES).map((cat) => (
|
|
<TabsTrigger key={cat} value={cat} className="text-xs">
|
|
{cat}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
|
|
<TabsContent value={category} className="mt-4">
|
|
{/* 图标网格 */}
|
|
<div className="grid grid-cols-8 gap-2 max-h-[400px] overflow-y-auto p-2 border rounded-md">
|
|
{iconList.length > 0 ? (
|
|
iconList.map(({ name, component: Icon }) => (
|
|
<Button
|
|
key={name}
|
|
variant={value === name ? "default" : "outline"}
|
|
className="h-20 flex flex-col items-center justify-center gap-2 p-2"
|
|
onClick={() => handleSelect(name)}
|
|
title={name}
|
|
>
|
|
<Icon className="h-6 w-6" />
|
|
<span className="text-[10px] truncate w-full text-center">
|
|
{name}
|
|
</span>
|
|
</Button>
|
|
))
|
|
) : (
|
|
<div className="col-span-8 text-center py-8 text-muted-foreground">
|
|
未找到匹配的图标
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* 当前选中 */}
|
|
{value && (
|
|
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
|
|
<span className="text-sm text-muted-foreground">当前选中:</span>
|
|
<div className="flex items-center gap-2">
|
|
<DynamicIcon name={value} className="h-5 w-5" />
|
|
<code className="text-sm">{value}</code>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="ml-auto"
|
|
onClick={() => {
|
|
onChange?.('');
|
|
onOpenChange(false);
|
|
}}
|
|
>
|
|
清除
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default LucideIconSelect;
|
|
|