import React, { useCallback, useEffect, useRef, useState } from 'react'; import type { EditorProps } from './EditorProps'; /** * 生成 iconify 图标的 HTML * * @param icon 图标名称 * @param style 样式对象 * @returns 包含 iconify 图标的 HTML 字符串 */ const iconifyIcon = (icon: string, style: string = '') => { return ``; }; const API_BASE_URLS = ['https://api.iconify.design', 'https://api.simplesvg.com', 'https://api.unisvg.com']; const API_ENDPOINTS = { COLLECTIONS: '/collections', COLLECTION: '/collection', SEARCH: '/search', }; // 每页加载的图标数量 const ICONS_PER_PAGE = 20; interface IconSetInfo { name: string; total?: number; author?: { name: string; url?: string; }; license?: { title: string; url?: string; }; samples?: string[]; height?: number; [key: string]: any; } /** * 从多个API源获取 iconify 数据 * * @param endpoint API端点 * @param params URL参数对象 * @returns 获取到的数据 * @throws 如果所有API源都失败,则抛出错误 */ const fetchFromAPI = async (endpoint: string, params: Record = {}) => { const queryString = Object.entries(params) .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) .join('&'); const urlSuffix = queryString ? `${endpoint}?${queryString}` : endpoint; let lastError = null; let lastResponseText = ''; for (const baseUrl of API_BASE_URLS) { try { const url = `${baseUrl}${urlSuffix}`; const response = await fetch(url); if (response.ok) { return await response.json(); } lastResponseText = await response.text(); console.warn(`API 请求失败: ${url}, 状态码: ${response.status}, 响应: ${lastResponseText.substring(0, 100)}...`); } catch (err) { lastError = err; console.warn(`从 ${baseUrl}${urlSuffix} 获取数据失败`, err); } } const errorMsg = lastResponseText ? `API 返回错误: ${lastResponseText.substring(0, 200)}` : '无法从任何API源获取数据'; console.error(errorMsg, lastError); throw new Error(errorMsg); }; /** * 图标编辑器组件,用于实现 iconify 图标替换。 */ export const IconEditor: React.FC = ({ element, onClose }) => { const [searchTerm, setSearchTerm] = useState(''); const [currentIcon, setCurrentIcon] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [iconSets, setIconSets] = useState([]); const [iconSetsInfo, setIconSetsInfo] = useState>({}); const [selectedIconSet, setSelectedIconSet] = useState(''); const [icons, setIcons] = useState([]); const [, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [allIconNames, setAllIconNames] = useState([]); const [loadedIconNames, setLoadedIconNames] = useState([]); const [isLoadingMore, setIsLoadingMore] = useState(false); const scrollContainerRef = useRef(null); const observerRef = useRef(null); const loadMoreTriggerRef = useRef(null); useEffect(() => { if (element && element.tagName.toLowerCase() === 'iconify-icon') { const iconName = element.getAttribute('icon') || ''; setCurrentIcon(iconName); } }, [element]); useEffect(() => { const fetchIconSets = async () => { setIsLoading(true); try { const data = await fetchFromAPI(API_ENDPOINTS.COLLECTIONS); const prefixes = Object.keys(data); setIconSets(prefixes); setIconSetsInfo(data); if (currentIcon) { const [prefix] = currentIcon.split(':'); if (prefixes.includes(prefix)) { setSelectedIconSet(prefix); return; } } setSelectedIconSet(prefixes[0]); } catch (err) { console.error('获取图标集失败', err); setError('获取图标集失败,请稍后再试'); } finally { setIsLoading(false); } }; if (iconSets.length === 0) { fetchIconSets(); } }, [currentIcon]); useEffect(() => { if (observerRef.current) { observerRef.current.disconnect(); observerRef.current = null; } if ('IntersectionObserver' in window && loadMoreTriggerRef.current && !isSearching && hasMore) { observerRef.current = new IntersectionObserver( (entries) => { const [entry] = entries; if (entry.isIntersecting && hasMore && !isLoading && !isLoadingMore) { loadMoreIcons(); } }, { threshold: 0.1, rootMargin: '0px 0px 100px 0px' }, ); observerRef.current.observe(loadMoreTriggerRef.current); } return () => { if (observerRef.current) { observerRef.current.disconnect(); observerRef.current = null; } }; }, [hasMore, isLoading, isLoadingMore, isSearching, selectedIconSet]); useEffect(() => { if (selectedIconSet) { setPage(1); setIcons([]); setAllIconNames([]); setLoadedIconNames([]); setHasMore(true); fetchAllIconNames(selectedIconSet); } }, [selectedIconSet]); const fetchAllIconNames = async (prefix: string) => { setIsLoading(true); setError(null); try { const data = await fetchFromAPI(API_ENDPOINTS.COLLECTION, { prefix, chars: 'true', aliases: 'true', }); const iconNamesSet = new Set(); if (data.uncategorized && Array.isArray(data.uncategorized)) { data.uncategorized.forEach((name: string) => iconNamesSet.add(name)); } if (data.categories && typeof data.categories === 'object') { Object.values(data.categories).forEach((icons: any) => { if (Array.isArray(icons)) { icons.forEach((name: string) => iconNamesSet.add(name)); } }); } if (data.icons && Array.isArray(data.icons)) { data.icons.forEach((name: string) => iconNamesSet.add(name)); } if (Array.isArray(data)) { data.forEach((name: string) => iconNamesSet.add(name)); } if (iconNamesSet.size === 0 && typeof data === 'object') { Object.keys(data).forEach((key) => { if (typeof data[key] === 'object' && data[key] !== null) { iconNamesSet.add(key); } }); } const iconNames = Array.from(iconNamesSet); setAllIconNames(iconNames); if (iconNames.length > 0) { fetchIconsBatch(prefix, iconNames.slice(0, ICONS_PER_PAGE)); } else { setHasMore(false); } } catch (err) { console.error('获取图标集失败', err instanceof Error ? err.message : String(err)); setError('获取图标集失败,请稍后再试'); setIsLoading(false); } }; const fetchIconsBatch = async (prefix: string, iconBatch: string[]) => { if (iconBatch.length === 0) { setIsLoading(false); setIsLoadingMore(false); return; } setError(null); try { const iconsParam = iconBatch.join(','); await fetchFromAPI(`/${prefix}.json`, { icons: iconsParam }); const newIcons = iconBatch.map((name) => `${prefix}:${name}`); setIcons((prev) => { const updated = [...prev, ...newIcons]; return updated; }); setLoadedIconNames((prev) => { const updated = [...prev, ...iconBatch]; const hasMoreIcons = updated.length < allIconNames.length; setTimeout(() => { setHasMore(hasMoreIcons); }, 0); return updated; }); setPage((prevPage) => prevPage + 1); } catch (err) { console.error('获取图标失败', err instanceof Error ? err.message : String(err)); setError('获取图标失败,请稍后再试'); } finally { setIsLoading(false); setIsLoadingMore(false); } }; const loadMoreIcons = useCallback(() => { if (isLoading || isLoadingMore || !hasMore || isSearching || !selectedIconSet) { return; } setIsLoadingMore(true); const startIndex = loadedIconNames.length; const endIndex = Math.min(startIndex + ICONS_PER_PAGE, allIconNames.length); if (startIndex < endIndex) { const nextBatch = allIconNames.slice(startIndex, endIndex); fetchIconsBatch(selectedIconSet, nextBatch); } else { setHasMore(false); setIsLoadingMore(false); } }, [isLoading, isLoadingMore, hasMore, isSearching, selectedIconSet, loadedIconNames.length, allIconNames]); const searchIcons = async () => { if (!searchTerm.trim()) { setSearchResults([]); setIsSearching(false); return; } setIsSearching(true); setIsLoading(true); setError(null); try { const data = await fetchFromAPI(API_ENDPOINTS.SEARCH, { query: searchTerm }); let results: string[] = []; if (data.icons && Array.isArray(data.icons)) { results = data.icons; } else if (Array.isArray(data)) { results = data; } else if (data.results && Array.isArray(data.results)) { results = data.results; } else if (typeof data === 'object') { const possibleResults = Object.keys(data).filter((key) => typeof data[key] === 'object' && data[key] !== null); if (possibleResults.length > 0) { results = possibleResults; } } setSearchResults(results); } catch (err) { console.error('搜索图标失败', err instanceof Error ? err.message : String(err)); setError('搜索图标失败,请稍后再试'); } finally { setIsLoading(false); } }; const handleSearch = useCallback(() => { searchIcons(); }, [searchTerm]); const loadMore = useCallback(() => { if (!isLoading && hasMore && !isLoadingMore && selectedIconSet) { loadMoreIcons(); return; } setHasMore(false); setIsLoadingMore(false); }, [isLoading, hasMore, isLoadingMore, selectedIconSet, loadMoreIcons]); const selectIcon = (iconName: string) => { if (element && element.tagName.toLowerCase() === 'iconify-icon') { element.setAttribute('icon', iconName); setCurrentIcon(iconName); onClose(); } }; const handleIconSetChange = (e: React.ChangeEvent) => { setSelectedIconSet(e.target.value); setPage(1); setIcons([]); }; const renderIcons = () => { const iconsToRender = isSearching ? searchResults : icons; if (iconsToRender.length === 0) { return (
{isLoading ? ( <>
加载中...
) : ( <>
没有找到图标
{isSearching ? '请尝试其他搜索关键词' : '请选择其他图标集'}
)}
); } return (
{iconsToRender.map((iconName) => (
selectIcon(iconName)} style={{ cursor: 'pointer', padding: '8px 4px', border: iconName === currentIcon ? '2px solid #3b82f6' : '1px solid #e2e8f0', borderRadius: '8px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: iconName === currentIcon ? '#eff6ff' : '#ffffff', boxShadow: iconName === currentIcon ? '0 1px 3px rgba(59, 130, 246, 0.1)' : '0 1px 2px rgba(0, 0, 0, 0.02)', transition: 'all 0.2s ease', height: '70px', }} onMouseOver={(e) => { if (iconName !== currentIcon) { e.currentTarget.style.backgroundColor = '#f8fafc'; e.currentTarget.style.borderColor = '#cbd5e1'; e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.05)'; } }} onMouseOut={(e) => { if (iconName !== currentIcon) { e.currentTarget.style.backgroundColor = '#ffffff'; e.currentTarget.style.borderColor = '#e2e8f0'; e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.02)'; } }} >
`, }} >
{iconName.split(':')[1] || iconName}
))}
); }; return (
当前图标: {currentIcon ? ( `, }} > {currentIcon} ) : ( 未选择 )}
setSearchTerm(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder="搜索图标..." style={{ flex: 1, padding: '10px 14px', border: 'none', outline: 'none', fontSize: '14px', backgroundColor: '#ffffff', }} />
{!isSearching && (
)} {error && (
{error}
)}
{ const target = e.currentTarget; const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 100; if (isNearBottom && hasMore && !isLoading && !isLoadingMore && !isSearching) { loadMoreIcons(); } }} > {isLoading && icons.length === 0 && !isSearching ? (

正在加载图标集...

) : ( renderIcons() )}
{hasMore && !isSearching && !isLoadingMore && (
滚动加载更多...
)}
{isLoadingMore && !isSearching && (
加载更多图标...
)}
{!isSearching && hasMore && ( )} {isSearching && ( )}
); };