11'use client' ;
22
3- import React , { useState , useCallback , useRef , useEffect } from 'react' ;
3+ import React , { useState , useCallback , useRef , useEffect , useMemo } from 'react' ;
44import { Button , Input , List , Popover , Spin , Empty , ColorPicker } from 'antd' ;
55import type { Color } from 'antd/es/color-picker' ;
66import { SearchOutlined , CheckOutlined } from '@ant-design/icons' ;
77import { iconifyApi , type IconOption } from '@/api/iconify' ;
88import { PRESET_COLORS } from '@/utils/colorUtils' ;
9+ import { debounce } from '@/utils/debounce' ;
910
1011/**
1112 * IconifySelector 组件 Props
@@ -25,29 +26,6 @@ interface IconifySelectorProps {
2526 iconColor ?: string ;
2627}
2728
28- /**
29- * 防抖函数
30- */
31- function debounce < T extends ( ...args : any [ ] ) => any > (
32- func : T ,
33- delay : number
34- ) : ( ...args : Parameters < T > ) => void {
35- let timeoutId : NodeJS . Timeout | null = null ;
36-
37- return function debounced ( ...args : Parameters < T > ) {
38- // 清除之前的定时器
39- if ( timeoutId ) {
40- clearTimeout ( timeoutId ) ;
41- }
42-
43- // 设置新的定时器
44- timeoutId = setTimeout ( ( ) => {
45- func ( ...args ) ;
46- timeoutId = null ;
47- } , delay ) ;
48- } ;
49- }
50-
5129/**
5230 * 图标选项渲染组件
5331 */
@@ -93,13 +71,9 @@ const IconOptionItem: React.FC<{
9371 ?
9472 </ div >
9573 ) }
96- < span className = "overflow-hidden text-ellipsis whitespace-nowrap" >
97- { icon . label }
98- </ span >
74+ < span className = "overflow-hidden text-ellipsis whitespace-nowrap" > { icon . label } </ span >
9975 </ div >
100- { selected && (
101- < CheckOutlined className = "text-blue-500 shrink-0 ml-2" />
102- ) }
76+ { selected && < CheckOutlined className = "text-blue-500 shrink-0 ml-2" /> }
10377 </ div >
10478 </ List . Item >
10579 ) ;
@@ -123,17 +97,19 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
12397 const [ options , setOptions ] = useState < IconOption [ ] > ( [ ] ) ;
12498 const [ selectedIcon , setSelectedIcon ] = useState < IconOption | null > ( null ) ;
12599 const [ error , setError ] = useState < string > ( '' ) ;
126-
100+
127101 const searchInputRef = useRef < any > ( null ) ;
128- const searchCacheRef = useRef < Map < string , { results : IconOption [ ] ; timestamp : number } > > ( new Map ( ) ) ;
102+ const searchCacheRef = useRef < Map < string , { results : IconOption [ ] ; timestamp : number } > > (
103+ new Map ( )
104+ ) ;
129105 const CACHE_DURATION = 5 * 60 * 1000 ; // 5 分钟
130106
131107 // 初始化:如果有 value,尝试解析为 IconOption
132108 useEffect ( ( ) => {
133109 if ( value ) {
134110 const identifier = extractIconIdentifier ( value ) ;
135111 console . log ( 'IconifySelector 初始化:' , { value, identifier } ) ;
136-
112+
137113 if ( identifier && iconifyApi . isValidIconIdentifier ( identifier ) ) {
138114 const label = identifier . split ( ':' ) [ 1 ] || identifier ;
139115 console . log ( '设置选中图标:' , { identifier, label } ) ;
@@ -156,60 +132,60 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
156132 } ;
157133
158134 // 搜索图标
159- const searchIcons = useCallback ( async ( searchQuery : string ) => {
160- if ( ! searchQuery || searchQuery . trim ( ) === '' ) {
161- setOptions ( [ ] ) ;
162- setError ( '' ) ;
163- return ;
164- }
135+ const searchIcons = useCallback (
136+ async ( searchQuery : string ) => {
137+ if ( ! searchQuery || searchQuery . trim ( ) === '' ) {
138+ setOptions ( [ ] ) ;
139+ setError ( '' ) ;
140+ return ;
141+ }
165142
166- const trimmedQuery = searchQuery . trim ( ) . toLowerCase ( ) ;
143+ const trimmedQuery = searchQuery . trim ( ) . toLowerCase ( ) ;
167144
168- // 检查缓存
169- const cached = searchCacheRef . current . get ( trimmedQuery ) ;
170- if ( cached && Date . now ( ) - cached . timestamp < CACHE_DURATION ) {
171- console . log ( '使用缓存的搜索结果:' , trimmedQuery ) ;
172- setOptions ( cached . results ) ;
173- if ( cached . results . length === 0 ) {
174- setError ( '未找到相关图标' ) ;
145+ // 检查缓存
146+ const cached = searchCacheRef . current . get ( trimmedQuery ) ;
147+ if ( cached && Date . now ( ) - cached . timestamp < CACHE_DURATION ) {
148+ console . log ( '使用缓存的搜索结果:' , trimmedQuery ) ;
149+ setOptions ( cached . results ) ;
150+ if ( cached . results . length === 0 ) {
151+ setError ( '未找到相关图标' ) ;
152+ }
153+ return ;
175154 }
176- return ;
177- }
178155
179- setLoading ( true ) ;
180- setError ( '' ) ;
156+ setLoading ( true ) ;
157+ setError ( '' ) ;
181158
182- try {
183- const results = await iconifyApi . searchIcons ( {
184- query : trimmedQuery ,
185- limit : 200 ,
186- } ) ;
159+ try {
160+ const results = await iconifyApi . searchIcons ( {
161+ query : trimmedQuery ,
162+ limit : 200 ,
163+ } ) ;
164+
165+ // 缓存结果
166+ searchCacheRef . current . set ( trimmedQuery , {
167+ results,
168+ timestamp : Date . now ( ) ,
169+ } ) ;
187170
188- // 缓存结果
189- searchCacheRef . current . set ( trimmedQuery , {
190- results,
191- timestamp : Date . now ( ) ,
192- } ) ;
171+ setOptions ( results ) ;
193172
194- setOptions ( results ) ;
195-
196- if ( results . length === 0 ) {
197- setError ( '未找到相关图标' ) ;
173+ if ( results . length === 0 ) {
174+ setError ( '未找到相关图标' ) ;
175+ }
176+ } catch ( err ) {
177+ console . error ( '搜索图标失败:' , err ) ;
178+ setError ( '搜索失败,请稍后重试' ) ;
179+ setOptions ( [ ] ) ;
180+ } finally {
181+ setLoading ( false ) ;
198182 }
199- } catch ( err ) {
200- console . error ( '搜索图标失败:' , err ) ;
201- setError ( '搜索失败,请稍后重试' ) ;
202- setOptions ( [ ] ) ;
203- } finally {
204- setLoading ( false ) ;
205- }
206- } , [ CACHE_DURATION ] ) ;
183+ } ,
184+ [ CACHE_DURATION ]
185+ ) ;
207186
208187 // 防抖搜索
209- const debouncedSearch = useCallback (
210- debounce ( searchIcons , 500 ) ,
211- [ searchIcons ]
212- ) ;
188+ const debouncedSearch = useMemo ( ( ) => debounce ( searchIcons , 500 ) , [ searchIcons ] ) ;
213189
214190 // 处理搜索输入
215191 const handleSearchChange = useCallback (
@@ -226,7 +202,7 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
226202 ( icon : IconOption ) => {
227203 setSelectedIcon ( icon ) ;
228204 setOpen ( false ) ;
229-
205+
230206 if ( onChange ) {
231207 // 如果有颜色,添加 color 参数
232208 const iconUrl = iconColor ? `${ icon . url } ?color=${ encodeURIComponent ( iconColor ) } ` : icon . url ;
@@ -237,44 +213,44 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
237213 ) ;
238214
239215 // 处理键盘事件
240- const handleKeyDown = useCallback (
241- ( e : React . KeyboardEvent ) => {
242- if ( e . key === 'Escape' ) {
243- setOpen ( false ) ;
244- }
245- } ,
246- [ ]
247- ) ;
216+ const handleKeyDown = useCallback ( ( e : React . KeyboardEvent ) => {
217+ if ( e . key === 'Escape' ) {
218+ setOpen ( false ) ;
219+ }
220+ } , [ ] ) ;
248221
249222 // 处理下拉框打开
250- const handleOpenChange = useCallback ( ( visible : boolean ) => {
251- console . log ( '下拉框状态变化:' , { visible, selectedIcon } ) ;
252- setOpen ( visible ) ;
253-
254- if ( visible ) {
255- // 打开时,如果有选中的图标,回填图标名称到搜索框
256- if ( selectedIcon ) {
257- console . log ( '回填图标名称:' , selectedIcon . label ) ;
258- setQuery ( selectedIcon . label ) ;
259- // 自动搜索该图标名称
260- searchIcons ( selectedIcon . label ) ;
223+ const handleOpenChange = useCallback (
224+ ( visible : boolean ) => {
225+ console . log ( '下拉框状态变化:' , { visible, selectedIcon } ) ;
226+ setOpen ( visible ) ;
227+
228+ if ( visible ) {
229+ // 打开时,如果有选中的图标,回填图标名称到搜索框
230+ if ( selectedIcon ) {
231+ console . log ( '回填图标名称:' , selectedIcon . label ) ;
232+ setQuery ( selectedIcon . label ) ;
233+ // 自动搜索该图标名称
234+ searchIcons ( selectedIcon . label ) ;
235+ }
236+ // 聚焦搜索框
237+ setTimeout ( ( ) => {
238+ searchInputRef . current ?. focus ( ) ;
239+ } , 100 ) ;
240+ } else {
241+ // 关闭时清空搜索
242+ setQuery ( '' ) ;
243+ setOptions ( [ ] ) ;
244+ setError ( '' ) ;
261245 }
262- // 聚焦搜索框
263- setTimeout ( ( ) => {
264- searchInputRef . current ?. focus ( ) ;
265- } , 100 ) ;
266- } else {
267- // 关闭时清空搜索
268- setQuery ( '' ) ;
269- setOptions ( [ ] ) ;
270- setError ( '' ) ;
271- }
272- } , [ selectedIcon , searchIcons ] ) ;
246+ } ,
247+ [ selectedIcon , searchIcons ]
248+ ) ;
273249
274250 // 渲染下拉内容
275251 const renderContent = ( ) => (
276- < div
277- className = "w-80"
252+ < div
253+ className = "w-80"
278254 style = { { maxHeight : '400px' } }
279255 onKeyDown = { handleKeyDown }
280256 role = "dialog"
@@ -294,8 +270,8 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
294270 </ div >
295271
296272 { /* 图标列表 */ }
297- < div
298- className = "overflow-y-auto"
273+ < div
274+ className = "overflow-y-auto"
299275 style = { { maxHeight : '320px' } }
300276 role = "listbox"
301277 aria-label = "图标列表"
@@ -307,11 +283,7 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
307283 </ div >
308284 ) : error ? (
309285 < div role = "alert" >
310- < Empty
311- image = { Empty . PRESENTED_IMAGE_SIMPLE }
312- description = { error }
313- className = "py-8"
314- />
286+ < Empty image = { Empty . PRESENTED_IMAGE_SIMPLE } description = { error } className = "py-8" />
315287 </ div >
316288 ) : options . length > 0 ? (
317289 < List
@@ -337,12 +309,15 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
337309 ) ;
338310
339311 // 处理颜色变化
340- const handleColorChange = useCallback ( ( color : Color ) => {
341- const colorValue = color . toHexString ( ) ;
342- if ( onColorChange ) {
343- onColorChange ( colorValue ) ;
344- }
345- } , [ onColorChange ] ) ;
312+ const handleColorChange = useCallback (
313+ ( color : Color ) => {
314+ const colorValue = color . toHexString ( ) ;
315+ if ( onColorChange ) {
316+ onColorChange ( colorValue ) ;
317+ }
318+ } ,
319+ [ onColorChange ]
320+ ) ;
346321
347322 return (
348323 < div className = "flex items-center flex-1" >
@@ -364,24 +339,26 @@ export const IconifySelector: React.FC<IconifySelectorProps> = ({
364339 { selectedIcon ? (
365340 < div className = "flex items-center gap-2" >
366341 < img
367- src = { iconColor ? `${ selectedIcon . url } ?color=${ encodeURIComponent ( iconColor ) } ` : selectedIcon . url }
342+ src = {
343+ iconColor
344+ ? `${ selectedIcon . url } ?color=${ encodeURIComponent ( iconColor ) } `
345+ : selectedIcon . url
346+ }
368347 alt = { selectedIcon . label }
369348 className = "w-5 h-5"
370349 onError = { ( e ) => {
371350 e . currentTarget . style . display = 'none' ;
372351 } }
373352 />
374- < span className = "overflow-hidden text-ellipsis" >
375- { selectedIcon . label }
376- </ span >
353+ < span className = "overflow-hidden text-ellipsis" > { selectedIcon . label } </ span >
377354 </ div >
378355 ) : (
379356 < span className = "text-gray-400" > 选择 Iconify 图标</ span >
380357 ) }
381358 </ Button >
382359 </ Popover >
383360 < ColorPicker
384- className = ' w-28'
361+ className = " w-28"
385362 value = { iconColor || '#000000' }
386363 onChange = { handleColorChange }
387364 presets = { [
0 commit comments