refactor(folders): 优化刷新逻辑,只更新特定文件夹而非全量刷新
- FoldersClient: 使用 onUpdateFolder/onDeleteFolder 回调局部更新 - ExploreClient: 使用 onUpdateFavorite 只更新收藏数 - FavoritesClient: 使用 onRemoveFavorite 从列表移除,避免重新请求
This commit is contained in:
@@ -23,10 +23,10 @@ import { authClient } from "@/lib/auth-client";
|
||||
interface PublicFolderCardProps {
|
||||
folder: TPublicFolder;
|
||||
currentUserId?: string;
|
||||
onFavoriteChange?: () => void;
|
||||
onUpdateFavorite: (folderId: number, isFavorited: boolean, favoriteCount: number) => void;
|
||||
}
|
||||
|
||||
const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFolderCardProps) => {
|
||||
const PublicFolderCard = ({ folder, currentUserId, onUpdateFavorite }: PublicFolderCardProps) => {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("explore");
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
@@ -53,7 +53,7 @@ const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFol
|
||||
if (result.success && result.data) {
|
||||
setIsFavorited(result.data.isFavorited);
|
||||
setFavoriteCount(result.data.favoriteCount);
|
||||
onFavoriteChange?.();
|
||||
onUpdateFavorite(folder.id, result.data.isFavorited, result.data.favoriteCount);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
@@ -128,13 +128,12 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const refreshFolders = async () => {
|
||||
setLoading(true);
|
||||
const result = await actionSearchPublicFolders(searchQuery.trim() || "");
|
||||
if (result.success && result.data) {
|
||||
setPublicFolders(result.data);
|
||||
}
|
||||
setLoading(false);
|
||||
const handleUpdateFavorite = (folderId: number, _isFavorited: boolean, favoriteCount: number) => {
|
||||
setPublicFolders((prev) =>
|
||||
prev.map((f) =>
|
||||
f.id === folderId ? { ...f, favoriteCount } : f
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -177,7 +176,7 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
currentUserId={currentUserId}
|
||||
onFavoriteChange={refreshFolders}
|
||||
onUpdateFavorite={handleUpdateFavorite}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { PageHeader } from "@/components/ui/PageHeader";
|
||||
import { CardList } from "@/components/ui/CardList";
|
||||
import { actionGetUserFavorites } from "@/modules/folder/folder-aciton";
|
||||
import { actionGetUserFavorites, actionToggleFavorite } from "@/modules/folder/folder-aciton";
|
||||
|
||||
type UserFavorite = {
|
||||
id: number;
|
||||
@@ -27,11 +28,27 @@ type UserFavorite = {
|
||||
|
||||
interface FavoriteCardProps {
|
||||
favorite: UserFavorite;
|
||||
onRemoveFavorite: (folderId: number) => void;
|
||||
}
|
||||
|
||||
const FavoriteCard = ({ favorite }: FavoriteCardProps) => {
|
||||
const FavoriteCard = ({ favorite, onRemoveFavorite }: FavoriteCardProps) => {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("favorites");
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
const handleRemoveFavorite = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isRemoving) return;
|
||||
|
||||
setIsRemoving(true);
|
||||
const result = await actionToggleFavorite(favorite.folderId);
|
||||
if (result.success) {
|
||||
onRemoveFavorite(favorite.folderId);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
setIsRemoving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -57,7 +74,11 @@ const FavoriteCard = ({ favorite }: FavoriteCardProps) => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart size={18} className="fill-red-500 text-red-500" />
|
||||
<Heart
|
||||
size={18}
|
||||
className="fill-red-500 text-red-500 cursor-pointer hover:scale-110 transition-transform"
|
||||
onClick={handleRemoveFavorite}
|
||||
/>
|
||||
<ChevronRight size={20} className="text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,31 +107,37 @@ export function FavoritesClient({ userId }: FavoritesClientProps) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleRemoveFavorite = (folderId: number) => {
|
||||
setFavorites((prev) => prev.filter((f) => f.folderId !== folderId));
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<div className="mt-4">
|
||||
<CardList>
|
||||
{loading ? (
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
||||
<p className="text-sm text-gray-500">{t("loading")}</p>
|
||||
<CardList>
|
||||
{loading ? (
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
||||
<p className="text-sm text-gray-500">{t("loading")}</p>
|
||||
</div>
|
||||
) : favorites.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<Heart size={24} className="text-gray-400" />
|
||||
</div>
|
||||
) : favorites.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<Heart size={24} className="text-gray-400" />
|
||||
</div>
|
||||
<p className="text-sm">{t("noFavorites")}</p>
|
||||
</div>
|
||||
) : (
|
||||
favorites.map((favorite) => (
|
||||
<FavoriteCard key={favorite.id} favorite={favorite} />
|
||||
))
|
||||
)}
|
||||
</CardList>
|
||||
</div>
|
||||
<p className="text-sm">{t("noFavorites")}</p>
|
||||
</div>
|
||||
) : (
|
||||
favorites.map((favorite) => (
|
||||
<FavoriteCard
|
||||
key={favorite.id}
|
||||
favorite={favorite}
|
||||
onRemoveFavorite={handleRemoveFavorite}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</CardList>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,10 +28,11 @@ import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
||||
|
||||
interface FolderCardProps {
|
||||
folder: TSharedFolderWithTotalPairs;
|
||||
refresh: () => void;
|
||||
onUpdateFolder: (folderId: number, updates: Partial<TSharedFolderWithTotalPairs>) => void;
|
||||
onDeleteFolder: (folderId: number) => void;
|
||||
}
|
||||
|
||||
const FolderCard = ({ folder, refresh }: FolderCardProps) => {
|
||||
const FolderCard = ({ folder, onUpdateFolder, onDeleteFolder }: FolderCardProps) => {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("folders");
|
||||
|
||||
@@ -40,12 +41,38 @@ const FolderCard = ({ folder, refresh }: FolderCardProps) => {
|
||||
const newVisibility = folder.visibility === "PUBLIC" ? "PRIVATE" : "PUBLIC";
|
||||
const result = await actionSetFolderVisibility(folder.id, newVisibility);
|
||||
if (result.success) {
|
||||
refresh();
|
||||
onUpdateFolder(folder.id, { visibility: newVisibility });
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRename = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const newName = prompt(t("enterNewName"))?.trim();
|
||||
if (newName && newName.length > 0) {
|
||||
const result = await actionRenameFolderById(folder.id, newName);
|
||||
if (result.success) {
|
||||
onUpdateFolder(folder.id, { name: newName });
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const confirm = prompt(t("confirmDelete", { name: folder.name }));
|
||||
if (confirm === folder.name) {
|
||||
const result = await actionDeleteFolderById(folder.id);
|
||||
if (result.success) {
|
||||
onDeleteFolder(folder.id);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex justify-between items-center group py-4 px-5 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||
@@ -91,42 +118,16 @@ const FolderCard = ({ folder, refresh }: FolderCardProps) => {
|
||||
<Globe size={18} />
|
||||
)}
|
||||
</CircleButton>
|
||||
<CircleButton
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const newName = prompt(t("enterNewName"))?.trim();
|
||||
if (newName && newName.length > 0) {
|
||||
actionRenameFolderById(folder.id, newName).then((result) => {
|
||||
if (result.success) {
|
||||
refresh();
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CircleButton onClick={handleRename}>
|
||||
<FolderPen size={18} />
|
||||
</CircleButton>
|
||||
<CircleButton
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const confirm = prompt(t("confirmDelete", { name: folder.name }));
|
||||
if (confirm === folder.name) {
|
||||
actionDeleteFolderById(folder.id).then((result) => {
|
||||
if (result.success) {
|
||||
refresh();
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="text-gray-400 hover:text-red-500 hover:bg-red-50"
|
||||
onClick={handleDelete}
|
||||
className="hover:text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</CircleButton>
|
||||
<ChevronRight size={20} className="text-gray-400 ml-1" />
|
||||
<ChevronRight size={20} className="text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -155,19 +156,25 @@ export function FoldersClient({ userId }: FoldersClientProps) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleUpdateFolder = (folderId: number, updates: Partial<TSharedFolderWithTotalPairs>) => {
|
||||
setFolders((prev) =>
|
||||
prev.map((f) => (f.id === folderId ? { ...f, ...updates } : f))
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteFolder = (folderId: number) => {
|
||||
setFolders((prev) => prev.filter((f) => f.id !== folderId));
|
||||
};
|
||||
|
||||
const handleCreateFolder = async () => {
|
||||
const folderName = prompt(t("enterFolderName"));
|
||||
if (!folderName) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await actionCreateFolder(userId, folderName);
|
||||
if (result.success) {
|
||||
loadFolders();
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!folderName?.trim()) return;
|
||||
|
||||
const result = await actionCreateFolder(userId, folderName.trim());
|
||||
if (result.success) {
|
||||
loadFolders();
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,14 +182,12 @@ export function FoldersClient({ userId }: FoldersClientProps) {
|
||||
<PageLayout>
|
||||
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<LightButton
|
||||
onClick={handleCreateFolder}
|
||||
disabled={loading}
|
||||
className="w-full border-dashed mb-4"
|
||||
>
|
||||
<FolderPlus size={20} />
|
||||
<span>{loading ? t("creating") : t("newFolder")}</span>
|
||||
</LightButton>
|
||||
<div className="mb-4">
|
||||
<LightButton onClick={handleCreateFolder}>
|
||||
<FolderPlus size={18} />
|
||||
{t("newFolder")}
|
||||
</LightButton>
|
||||
</div>
|
||||
|
||||
<CardList>
|
||||
{loading ? (
|
||||
@@ -193,16 +198,19 @@ export function FoldersClient({ userId }: FoldersClientProps) {
|
||||
) : folders.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<FolderPlus size={24} className="text-gray-400" />
|
||||
<Fd size={24} className="text-gray-400" />
|
||||
</div>
|
||||
<p className="text-sm">{t("noFoldersYet")}</p>
|
||||
</div>
|
||||
) : (
|
||||
folders
|
||||
.toSorted((a, b) => b.id - a.id)
|
||||
.map((folder) => (
|
||||
<FolderCard key={folder.id} folder={folder} refresh={loadFolders} />
|
||||
))
|
||||
folders.map((folder) => (
|
||||
<FolderCard
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
onUpdateFolder={handleUpdateFolder}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</CardList>
|
||||
</PageLayout>
|
||||
|
||||
Reference in New Issue
Block a user