From 2a87f38d2a17cace8d98e65b9e03e7cad5108bd2 Mon Sep 17 00:00:00 2001 From: weizwz <1124725517@qq.com> Date: Fri, 28 Nov 2025 17:25:07 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=AE=A1=E7=90=86=E7=9A=84=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/management/DataTable.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/components/management/DataTable.tsx b/components/management/DataTable.tsx index af93b33..b25c5ef 100644 --- a/components/management/DataTable.tsx +++ b/components/management/DataTable.tsx @@ -51,8 +51,6 @@ export const DataTable: React.FC = ({ onSelectionChange, }) => { const [internalSelectedRowKeys, setInternalSelectedRowKeys] = useState([]); - const [pageSize, setPageSize] = useState(100); - const [currentPage, setCurrentPage] = useState(1); const [expandedRowKeys, setExpandedRowKeys] = useState([]); // 获取分类数据 @@ -339,21 +337,7 @@ export const DataTable: React.FC = ({ onExpandedRowsChange: (keys) => setExpandedRowKeys([...keys]), // defaultExpandAllRows: true, }} - pagination={{ - current: currentPage, - pageSize: pageSize, - showSizeChanger: true, - showTotal: () => { - const categoryCount = treeData.length; - const linkCount = links.length; - return `共 ${categoryCount} 个分类,${linkCount} 条链接`; - }, - onChange: (page, size) => { - setCurrentPage(page); - setPageSize(size); - }, - pageSizeOptions: ['10', '20', '50', '100'], - }} + pagination={false} scroll={{ x: 1200 }} /> From d9e3ebdce5b0e42af125f53aca84f365e038fa8e Mon Sep 17 00:00:00 2001 From: weizwz <1124725517@qq.com> Date: Fri, 28 Nov 2025 21:17:26 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/modals/IconifySelector.tsx | 233 +++++++++---------- components/navigation/CategorySidebar.tsx | 271 +++++++++++----------- 2 files changed, 244 insertions(+), 260 deletions(-) diff --git a/components/modals/IconifySelector.tsx b/components/modals/IconifySelector.tsx index b91f6d2..4110e9c 100644 --- a/components/modals/IconifySelector.tsx +++ b/components/modals/IconifySelector.tsx @@ -1,11 +1,12 @@ 'use client'; -import React, { useState, useCallback, useRef, useEffect } from 'react'; +import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { Button, Input, List, Popover, Spin, Empty, ColorPicker } from 'antd'; import type { Color } from 'antd/es/color-picker'; import { SearchOutlined, CheckOutlined } from '@ant-design/icons'; import { iconifyApi, type IconOption } from '@/api/iconify'; import { PRESET_COLORS } from '@/utils/colorUtils'; +import { debounce } from '@/utils/debounce'; /** * IconifySelector 组件 Props @@ -25,29 +26,6 @@ interface IconifySelectorProps { iconColor?: string; } -/** - * 防抖函数 - */ -function debounce any>( - func: T, - delay: number -): (...args: Parameters) => void { - let timeoutId: NodeJS.Timeout | null = null; - - return function debounced(...args: Parameters) { - // 清除之前的定时器 - if (timeoutId) { - clearTimeout(timeoutId); - } - - // 设置新的定时器 - timeoutId = setTimeout(() => { - func(...args); - timeoutId = null; - }, delay); - }; -} - /** * 图标选项渲染组件 */ @@ -93,13 +71,9 @@ const IconOptionItem: React.FC<{ ? )} - - {icon.label} - + {icon.label} - {selected && ( - - )} + {selected && } ); @@ -123,9 +97,11 @@ export const IconifySelector: React.FC = ({ const [options, setOptions] = useState([]); const [selectedIcon, setSelectedIcon] = useState(null); const [error, setError] = useState(''); - + const searchInputRef = useRef(null); - const searchCacheRef = useRef>(new Map()); + const searchCacheRef = useRef>( + new Map() + ); const CACHE_DURATION = 5 * 60 * 1000; // 5 分钟 // 初始化:如果有 value,尝试解析为 IconOption @@ -133,7 +109,7 @@ export const IconifySelector: React.FC = ({ if (value) { const identifier = extractIconIdentifier(value); console.log('IconifySelector 初始化:', { value, identifier }); - + if (identifier && iconifyApi.isValidIconIdentifier(identifier)) { const label = identifier.split(':')[1] || identifier; console.log('设置选中图标:', { identifier, label }); @@ -156,60 +132,60 @@ export const IconifySelector: React.FC = ({ }; // 搜索图标 - const searchIcons = useCallback(async (searchQuery: string) => { - if (!searchQuery || searchQuery.trim() === '') { - setOptions([]); - setError(''); - return; - } + const searchIcons = useCallback( + async (searchQuery: string) => { + if (!searchQuery || searchQuery.trim() === '') { + setOptions([]); + setError(''); + return; + } - const trimmedQuery = searchQuery.trim().toLowerCase(); + const trimmedQuery = searchQuery.trim().toLowerCase(); - // 检查缓存 - const cached = searchCacheRef.current.get(trimmedQuery); - if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { - console.log('使用缓存的搜索结果:', trimmedQuery); - setOptions(cached.results); - if (cached.results.length === 0) { - setError('未找到相关图标'); + // 检查缓存 + const cached = searchCacheRef.current.get(trimmedQuery); + if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { + console.log('使用缓存的搜索结果:', trimmedQuery); + setOptions(cached.results); + if (cached.results.length === 0) { + setError('未找到相关图标'); + } + return; } - return; - } - setLoading(true); - setError(''); + setLoading(true); + setError(''); - try { - const results = await iconifyApi.searchIcons({ - query: trimmedQuery, - limit: 200, - }); + try { + const results = await iconifyApi.searchIcons({ + query: trimmedQuery, + limit: 200, + }); + + // 缓存结果 + searchCacheRef.current.set(trimmedQuery, { + results, + timestamp: Date.now(), + }); - // 缓存结果 - searchCacheRef.current.set(trimmedQuery, { - results, - timestamp: Date.now(), - }); + setOptions(results); - setOptions(results); - - if (results.length === 0) { - setError('未找到相关图标'); + if (results.length === 0) { + setError('未找到相关图标'); + } + } catch (err) { + console.error('搜索图标失败:', err); + setError('搜索失败,请稍后重试'); + setOptions([]); + } finally { + setLoading(false); } - } catch (err) { - console.error('搜索图标失败:', err); - setError('搜索失败,请稍后重试'); - setOptions([]); - } finally { - setLoading(false); - } - }, [CACHE_DURATION]); + }, + [CACHE_DURATION] + ); // 防抖搜索 - const debouncedSearch = useCallback( - debounce(searchIcons, 500), - [searchIcons] - ); + const debouncedSearch = useMemo(() => debounce(searchIcons, 500), [searchIcons]); // 处理搜索输入 const handleSearchChange = useCallback( @@ -226,7 +202,7 @@ export const IconifySelector: React.FC = ({ (icon: IconOption) => { setSelectedIcon(icon); setOpen(false); - + if (onChange) { // 如果有颜色,添加 color 参数 const iconUrl = iconColor ? `${icon.url}?color=${encodeURIComponent(iconColor)}` : icon.url; @@ -237,44 +213,44 @@ export const IconifySelector: React.FC = ({ ); // 处理键盘事件 - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setOpen(false); - } - }, - [] - ); + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setOpen(false); + } + }, []); // 处理下拉框打开 - const handleOpenChange = useCallback((visible: boolean) => { - console.log('下拉框状态变化:', { visible, selectedIcon }); - setOpen(visible); - - if (visible) { - // 打开时,如果有选中的图标,回填图标名称到搜索框 - if (selectedIcon) { - console.log('回填图标名称:', selectedIcon.label); - setQuery(selectedIcon.label); - // 自动搜索该图标名称 - searchIcons(selectedIcon.label); + const handleOpenChange = useCallback( + (visible: boolean) => { + console.log('下拉框状态变化:', { visible, selectedIcon }); + setOpen(visible); + + if (visible) { + // 打开时,如果有选中的图标,回填图标名称到搜索框 + if (selectedIcon) { + console.log('回填图标名称:', selectedIcon.label); + setQuery(selectedIcon.label); + // 自动搜索该图标名称 + searchIcons(selectedIcon.label); + } + // 聚焦搜索框 + setTimeout(() => { + searchInputRef.current?.focus(); + }, 100); + } else { + // 关闭时清空搜索 + setQuery(''); + setOptions([]); + setError(''); } - // 聚焦搜索框 - setTimeout(() => { - searchInputRef.current?.focus(); - }, 100); - } else { - // 关闭时清空搜索 - setQuery(''); - setOptions([]); - setError(''); - } - }, [selectedIcon, searchIcons]); + }, + [selectedIcon, searchIcons] + ); // 渲染下拉内容 const renderContent = () => ( -
= ({
{/* 图标列表 */} -
= ({
) : error ? (
- +
) : options.length > 0 ? ( = ({ ); // 处理颜色变化 - const handleColorChange = useCallback((color: Color) => { - const colorValue = color.toHexString(); - if (onColorChange) { - onColorChange(colorValue); - } - }, [onColorChange]); + const handleColorChange = useCallback( + (color: Color) => { + const colorValue = color.toHexString(); + if (onColorChange) { + onColorChange(colorValue); + } + }, + [onColorChange] + ); return (
@@ -364,16 +339,18 @@ export const IconifySelector: React.FC = ({ {selectedIcon ? (
{selectedIcon.label} { e.currentTarget.style.display = 'none'; }} /> - - {selectedIcon.label} - + {selectedIcon.label}
) : ( 选择 Iconify 图标 @@ -381,7 +358,7 @@ export const IconifySelector: React.FC = ({ = ({ onDelete, renderIcon, }) => { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: category.id, }); @@ -90,22 +88,17 @@ const DraggableCategoryItem: React.FC = ({ ]; return ( -
- +
+
{ @@ -116,12 +109,18 @@ const DraggableCategoryItem: React.FC = ({ }} > {/* 分类图标 */} -
+
{renderIcon(category.icon)}
- + {/* 分类名称 */} -
+
{category.name}
@@ -177,24 +176,30 @@ const CategorySidebarBase: React.FC = ({ className, style }, []); // 处理分类切换 - const handleCategoryChange = useCallback((categoryName: string) => { - dispatch(setCurrentCategory(categoryName)); - }, [dispatch]); + const handleCategoryChange = useCallback( + (categoryName: string) => { + dispatch(setCurrentCategory(categoryName)); + }, + [dispatch] + ); // 处理拖拽结束 - const handleDragEnd = useCallback((event: DragEndEvent) => { - const { active, over } = event; + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; - if (over && active.id !== over.id) { - const oldIndex = categories.findIndex((cat) => cat.id === active.id); - const newIndex = categories.findIndex((cat) => cat.id === over.id); + if (over && active.id !== over.id) { + const oldIndex = categories.findIndex((cat) => cat.id === active.id); + const newIndex = categories.findIndex((cat) => cat.id === over.id); - if (oldIndex !== -1 && newIndex !== -1) { - dispatch(reorderCategories({ fromIndex: oldIndex, toIndex: newIndex })); - showSuccess('分类排序已更新'); + if (oldIndex !== -1 && newIndex !== -1) { + dispatch(reorderCategories({ fromIndex: oldIndex, toIndex: newIndex })); + showSuccess('分类排序已更新'); + } } - } - }, [categories, dispatch]); + }, + [categories, dispatch] + ); // 处理添加分类 const handleAddCategory = useCallback(() => { @@ -209,79 +214,92 @@ const CategorySidebarBase: React.FC = ({ className, style }, []); // 处理删除分类 - const handleDeleteCategory = useCallback((category: Category) => { - // 查找该分类下的链接数量 - const linksInCategory = links.filter(link => link.category === category.name); - - showConfirm({ - title: '确认删除分类', - content: linksInCategory.length > 0 - ? `该分类下有 ${linksInCategory.length} 个链接,删除后这些链接的分类将被清空。确定要删除吗?` - : '确定要删除这个分类吗?', - okText: '删除', - cancelText: '取消', - okType: 'danger', - onOk: () => { - // 将该分类下的所有链接的分类字段置空 - if (linksInCategory.length > 0) { - linksInCategory.forEach(link => { - dispatch(updateLink({ + const handleDeleteCategory = useCallback( + (category: Category) => { + // 查找该分类下的链接数量 + const linksInCategory = links.filter((link) => link.category === category.name); + + showConfirm({ + title: '确认删除分类', + content: + linksInCategory.length > 0 + ? `该分类下有 ${linksInCategory.length} 个链接,删除后这些链接的分类将被清空。确定要删除吗?` + : '确定要删除这个分类吗?', + okText: '删除', + cancelText: '取消', + okType: 'danger', + onOk: () => { + // 将该分类下的所有链接的分类字段置空 + if (linksInCategory.length > 0) { + linksInCategory.forEach((link) => { + dispatch( + updateLink({ + id: link.id, + category: '', + }) + ); + }); + } + + // 删除分类 + dispatch(deleteCategory(category.id)); + + // 如果删除的是当前选中的分类,切换到第一个分类 + if (currentCategory === category.name) { + const remainingCategories = categories.filter((cat) => cat.id !== category.id); + const sortedCategories = [...remainingCategories].sort((a, b) => a.order - b.order); + const nextCategory = sortedCategories[0]?.name || '主页'; + dispatch(setCurrentCategory(nextCategory)); + } + + showSuccess('分类已删除'); + }, + }); + }, + [dispatch, links, currentCategory, categories] + ); + + // 处理分类编辑提交 + const handleCategorySubmit = useCallback( + (data: { name: string; icon: string }) => { + if (editingCategory) { + // 更新分类 + const oldName = editingCategory.name; + dispatch( + updateCategory({ + id: editingCategory.id, + ...data, + }) + ); + + // 更新所有使用该分类的链接 + const linksToUpdate = links.filter((link) => link.category === oldName); + linksToUpdate.forEach((link) => { + dispatch( + updateLink({ id: link.id, - category: '', - })); - }); - } - - // 删除分类 - dispatch(deleteCategory(category.id)); - - // 如果删除的是当前选中的分类,切换到第一个分类 - if (currentCategory === category.name) { - const remainingCategories = categories.filter(cat => cat.id !== category.id); - const sortedCategories = [...remainingCategories].sort((a, b) => a.order - b.order); - const nextCategory = sortedCategories[0]?.name || '主页'; - dispatch(setCurrentCategory(nextCategory)); + category: data.name, + }) + ); + }); + + // 如果修改的是当前选中的分类,更新当前分类 + if (currentCategory === oldName) { + dispatch(setCurrentCategory(data.name)); } - - showSuccess('分类已删除'); - }, - }); - }, [dispatch, links, currentCategory, categories]); - // 处理分类编辑提交 - const handleCategorySubmit = useCallback((data: { name: string; icon: string }) => { - if (editingCategory) { - // 更新分类 - const oldName = editingCategory.name; - dispatch(updateCategory({ - id: editingCategory.id, - ...data, - })); - - // 更新所有使用该分类的链接 - const linksToUpdate = links.filter(link => link.category === oldName); - linksToUpdate.forEach(link => { - dispatch(updateLink({ - id: link.id, - category: data.name, - })); - }); - - // 如果修改的是当前选中的分类,更新当前分类 - if (currentCategory === oldName) { - dispatch(setCurrentCategory(data.name)); + showSuccess('分类已更新'); + } else { + // 添加新分类 + dispatch(addCategory(data)); + showSuccess('分类已添加'); } - - showSuccess('分类已更新'); - } else { - // 添加新分类 - dispatch(addCategory(data)); - showSuccess('分类已添加'); - } - - setEditModalOpen(false); - setEditingCategory(null); - }, [dispatch, editingCategory, links, currentCategory]); + + setEditModalOpen(false); + setEditingCategory(null); + }, + [dispatch, editingCategory, links, currentCategory] + ); // 渲染图标 const renderIcon = useCallback((iconName: string) => { @@ -290,14 +308,16 @@ const CategorySidebarBase: React.FC = ({ className, style }, []); // 计算未分类链接数量 - const uncategorizedCount = useMemo(() => - links.filter(link => !link.category || link.category === '').length - , [links]); + const uncategorizedCount = useMemo( + () => links.filter((link) => !link.category || link.category === '').length, + [links] + ); // 排序后的分类列表 - const sortedCategories = useMemo(() => - [...categories].sort((a, b) => a.order - b.order) - , [categories]); + const sortedCategories = useMemo( + () => [...categories].sort((a, b) => a.order - b.order), + [categories] + ); // 处理点击未分类按钮 const handleUncategorizedClick = useCallback(() => { @@ -307,12 +327,7 @@ const CategorySidebarBase: React.FC = ({ className, style // 在挂载前不渲染菜单,避免 hydration 不匹配 if (!mounted) { return ( -