diff --git a/src/app.tsx b/src/app.tsx index 1f9a1f14525532ff2e69326df07526dd9fa0fd7a..62fe97662b545e50d2674aa8d72552a3a13702ce 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -47,19 +47,21 @@ function LogoutDialog() { return ( - - - 登录已过期 - - 您的登录状态已过期,请重新登录以继续使用。 - - - - - 返回登录 - - - + {showLogoutDialog && ( + + + 登录已过期 + + 您的登录状态已过期,请重新登录以继续使用。 + + + + + 返回登录 + + + + )} ) } diff --git a/src/components/confirm-dialog.tsx b/src/components/confirm-dialog.tsx index 2db6b255299e211ce2897d5b8ec9de1a3f792cda..0a0344c7de8c13c89ffb6655bcd38ce4dde48818 100644 --- a/src/components/confirm-dialog.tsx +++ b/src/components/confirm-dialog.tsx @@ -41,27 +41,29 @@ export function ConfirmDialog(props: ConfirmDialogProps) { } = props return ( - - - {title} - -
{desc}
-
-
- {children} - - - {cancelBtnText ?? 'Cancel'} - - - -
+ {actions.open && ( + + + {title} + +
{desc}
+
+
+ {children} + + + {cancelBtnText ?? 'Cancel'} + + + +
+ )}
) } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index c6e95c2fd9daac5d1ee5ebe5286b0ab70fd0474a..6ece881a992b55a1a9c09cd0ca71793e031d0292 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -36,11 +36,13 @@ type DialogContentProps = React.ComponentPropsWithoutRef< const DialogContent = React.forwardRef< React.ElementRef, DialogContentProps ->(({ className, children, hideClose, ...props }, ref) => ( +>(({ className, children, hideClose, 'aria-describedby': ariaDescribedby, ...props }, ref) => ( (null) + // 提到 map 外部,避免每个 item 重复计算 + const selectedSet = new Set(selectedKeys) + const selectedFiles = fileList.filter((f) => selectedSet.has(f.id)) + const hasUnfavorited = selectedFiles.some((f) => !f.isFavorite) + const downloadableFiles = selectedFiles.filter((f) => !f.isDir) + // 拖拽功能 const { dragState, @@ -102,11 +108,9 @@ export function FileGridView({ if (onDragStateChange) { onDragStateChange(dragState.dropTargetName, dragState.draggedItems.length) } - }, [ - dragState.dropTargetName, - dragState.draggedItems.length, - onDragStateChange, - ]) + // onDragStateChange 是父组件传入的稳定引用,不加入依赖以避免无效重跑 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dragState.dropTargetName, dragState.draggedItems.length]) const handleItemClick = (file: FileItem, event: React.MouseEvent) => { const isMultiSelect = event.ctrlKey || event.metaKey @@ -146,17 +150,12 @@ export function FileGridView({
{fileList.map((file) => { - const isSelected = selectedKeys.includes(file.id) + const isSelected = selectedSet.has(file.id) const isDragging = dragState.draggedItems.some( (f) => f.id === file.id ) const isDropTarget = file.isDir && dragState.dropTargetId === file.id const isMultiSelected = selectedKeys.length > 1 && isSelected - const selectedFiles = fileList.filter((f) => - selectedKeys.includes(f.id) - ) - const hasUnfavorited = selectedFiles.some((f) => !f.isFavorite) - const downloadableFiles = selectedFiles.filter((f) => !f.isDir) return ( diff --git a/src/pages/files/components/MySharesView.tsx b/src/pages/files/components/MySharesView.tsx index 1dc78a03d42f613b8a18c2a2d45c66c81be70e06..42d715c04f285f2c8a46acec7ebf8e8a36ae13fc 100644 --- a/src/pages/files/components/MySharesView.tsx +++ b/src/pages/files/components/MySharesView.tsx @@ -1019,23 +1019,25 @@ export function MySharesView() { open={deleteDialogVisible} onOpenChange={setDeleteDialogVisible} > - - - 确认取消分享 - - 确定要取消分享 "{deletingShare?.shareName}" 吗?取消后将无法恢复! - - - - 取消 - - 确认 - - - + {deleteDialogVisible && ( + + + 确认取消分享 + + 确定要取消分享 "{deletingShare?.shareName}" 吗?取消后将无法恢复! + + + + 取消 + + 确认 + + + + )} {/* 清空所有分享确认 */} @@ -1043,23 +1045,25 @@ export function MySharesView() { open={clearAllDialogVisible} onOpenChange={setClearAllDialogVisible} > - - - 确认清空所有分享 - - 确定要清空所有分享吗?所有分享链接将失效且无法恢复! - - - - 取消 - - 清空 - - - + {clearAllDialogVisible && ( + + + 确认清空所有分享 + + 确定要清空所有分享吗?所有分享链接将失效且无法恢复! + + + + 取消 + + 清空 + + + + )} {/* 批量删除确认弹窗 */} @@ -1067,24 +1071,26 @@ export function MySharesView() { open={batchDeleteDialogVisible} onOpenChange={setBatchDeleteDialogVisible} > - - - 确认批量取消 - - 确定要取消选中的 {selectedKeys.length}{' '} - 个分享吗?取消后将无法恢复! - - - - 取消 - - 确认 - - - + {batchDeleteDialogVisible && ( + + + 确认批量取消 + + 确定要取消选中的 {selectedKeys.length}{' '} + 个分享吗?取消后将无法恢复! + + + + 取消 + + 确认 + + + + )}
) diff --git a/src/pages/files/components/RecycleBinView.tsx b/src/pages/files/components/RecycleBinView.tsx index aefb93ff70c131431841d92d798372170873d231..b420c0b44b7ba52350bd2cb7e5c32e2e4b2369bd 100644 --- a/src/pages/files/components/RecycleBinView.tsx +++ b/src/pages/files/components/RecycleBinView.tsx @@ -166,16 +166,20 @@ export default function RecycleBinView() { } const confirmClearRecycle = async () => { - setLoading(true) try { await clearRecycle() - toast.success('回收站已清空') + // 乐观更新:先清空本地数据,避免 loading 状态导致的闪烁 + setFileList([]) + setTotal(0) setClearDialogOpen(false) setSelectedIds([]) commitSearch('') setPagination((p) => ({ ...p, pageIndex: 0 })) - } finally { - setLoading(false) + toast.success('回收站已清空') + void fetchRecyclePage() + } catch { + // 失败时重新拉取,恢复真实数据 + void fetchRecyclePage() } } @@ -570,62 +574,68 @@ export default function RecycleBinView() { )} - - - 确认还原 - - {operatingItem - ? `确定要还原文件 "${operatingItem.name}" 吗?` - : `确定要还原选中的 ${selectedIds.length} 个文件吗?`} - - - - 取消 - 还原 - - + {restoreDialogOpen && ( + + + 确认还原 + + {operatingItem + ? `确定要还原文件 "${operatingItem.name}" 吗?` + : `确定要还原选中的 ${selectedIds.length} 个文件吗?`} + + + + 取消 + 还原 + + + )} - - - 确认彻底删除 - - {operatingItem - ? `确定要彻底删除文件 "${operatingItem.name}" 吗?删除后将无法恢复!` - : `确定要彻底删除选中的 ${selectedIds.length} 个文件吗?删除后将无法恢复!`} - - - - 取消 - - 删除 - - - + {deleteDialogOpen && ( + + + 确认彻底删除 + + {operatingItem + ? `确定要彻底删除文件 "${operatingItem.name}" 吗?删除后将无法恢复!` + : `确定要彻底删除选中的 ${selectedIds.length} 个文件吗?删除后将无法恢复!`} + + + + 取消 + + 删除 + + + + )} - - - 确认清空回收站 - - 确定要清空回收站吗?所有文件将被彻底删除且无法恢复! - - - - 取消 - - 清空 - - - + {clearDialogOpen && ( + + + 确认清空回收站 + + 确定要清空回收站吗?所有文件将被彻底删除且无法恢复! + + + + 取消 + + 清空 + + + + )}
) diff --git a/src/pages/files/hooks/useFileList.ts b/src/pages/files/hooks/useFileList.ts index a37358f11c77cbe62f9a7a0affc9d97dc7e54135..a158b696a061c4962604a071512349605d080836 100644 --- a/src/pages/files/hooks/useFileList.ts +++ b/src/pages/files/hooks/useFileList.ts @@ -209,6 +209,16 @@ export function useFileList() { fetchInitial() }, [fetchInitial]) + /** 本地更新部分文件字段,避免不必要的列表刷新 */ + const updateFileItems = useCallback( + (ids: string[], patch: Partial) => { + setFileList((prev) => + prev.map((f) => (ids.includes(f.id) ? { ...f, ...patch } : f)) + ) + }, + [] + ) + const handleSortChange = useCallback( (field: string, direction: SortOrder) => { setOrderBy(field) @@ -263,6 +273,7 @@ export function useFileList() { enterFolder, navigateToFolder, refresh, + updateFileItems, commitSearch, handleSortChange, } diff --git a/src/pages/files/hooks/useFileOperations.ts b/src/pages/files/hooks/useFileOperations.ts index c401e884009d7b84f36a274aebcc0423e46a457c..256ccb3fbf86dec744a1ddb98ee325c749bdde6d 100644 --- a/src/pages/files/hooks/useFileOperations.ts +++ b/src/pages/files/hooks/useFileOperations.ts @@ -14,7 +14,8 @@ import { openFilePreviewWithToken } from '@/utils/preview' export function useFileOperations( refreshCallback: () => void, clearSelectionCallback?: () => void, - onCreateFolderSuccess?: () => void + onCreateFolderSuccess?: () => void, + updateFileItemsCallback?: (ids: string[], patch: Partial) => void ) { // 模态框状态 const [createFolderModalVisible, setCreateFolderModalVisible] = @@ -213,6 +214,7 @@ export function useFileOperations( /** * 收藏/取消收藏 + * 使用乐观更新:直接修改本地状态,失败时回滚刷新列表 */ const handleFavorite = useCallback( async (files: FileItem | FileItem[]) => { @@ -221,6 +223,11 @@ export function useFileOperations( // 判断是收藏还是取消收藏(如果有任何一个未收藏,就执行收藏操作) const hasUnfavorited = fileArray.some((f) => !f.isFavorite) + const newFavoriteState = hasUnfavorited + + // 乐观更新本地状态 + updateFileItemsCallback?.(fileIds, { isFavorite: newFavoriteState }) + clearSelectionCallback?.() try { if (hasUnfavorited) { @@ -230,13 +237,13 @@ export function useFileOperations( await unfavoriteFile(fileIds) toast.success('取消收藏成功') } - clearSelectionCallback?.() - refreshCallback() } catch (error) { + // 失败时回滚:刷新列表恢复真实状态 + updateFileItemsCallback?.(fileIds, { isFavorite: !newFavoriteState }) toast.error(hasUnfavorited ? '收藏失败' : '取消收藏失败') } }, - [refreshCallback, clearSelectionCallback] + [clearSelectionCallback, updateFileItemsCallback] ) /** diff --git a/src/pages/files/index.tsx b/src/pages/files/index.tsx index 21c6745746b388d721751fa03e2a167b5cf33f53..f58ff330a8d723f98a78c1c03d17ce387023b164 100644 --- a/src/pages/files/index.tsx +++ b/src/pages/files/index.tsx @@ -89,7 +89,7 @@ export default function FilesPage() { if (isFavoritesView || isRecentsView || isTypeFilter || isDirFilter) { navigate(`/files?viewMode=${viewMode}`) } - }) + }, fileList.updateFileItems) // 计算当前视图类型 const viewType = searchParams.get('view')