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 {
|
interface PublicFolderCardProps {
|
||||||
folder: TPublicFolder;
|
folder: TPublicFolder;
|
||||||
currentUserId?: string;
|
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 router = useRouter();
|
||||||
const t = useTranslations("explore");
|
const t = useTranslations("explore");
|
||||||
const [isFavorited, setIsFavorited] = useState(false);
|
const [isFavorited, setIsFavorited] = useState(false);
|
||||||
@@ -53,7 +53,7 @@ const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFol
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setIsFavorited(result.data.isFavorited);
|
setIsFavorited(result.data.isFavorited);
|
||||||
setFavoriteCount(result.data.favoriteCount);
|
setFavoriteCount(result.data.favoriteCount);
|
||||||
onFavoriteChange?.();
|
onUpdateFavorite(folder.id, result.data.isFavorited, result.data.favoriteCount);
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message);
|
toast.error(result.message);
|
||||||
}
|
}
|
||||||
@@ -128,13 +128,12 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshFolders = async () => {
|
const handleUpdateFavorite = (folderId: number, _isFavorited: boolean, favoriteCount: number) => {
|
||||||
setLoading(true);
|
setPublicFolders((prev) =>
|
||||||
const result = await actionSearchPublicFolders(searchQuery.trim() || "");
|
prev.map((f) =>
|
||||||
if (result.success && result.data) {
|
f.id === folderId ? { ...f, favoriteCount } : f
|
||||||
setPublicFolders(result.data);
|
)
|
||||||
}
|
);
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,7 +176,7 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
|
|||||||
key={folder.id}
|
key={folder.id}
|
||||||
folder={folder}
|
folder={folder}
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
onFavoriteChange={refreshFolders}
|
onUpdateFavorite={handleUpdateFavorite}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import {
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { PageHeader } from "@/components/ui/PageHeader";
|
import { PageHeader } from "@/components/ui/PageHeader";
|
||||||
import { CardList } from "@/components/ui/CardList";
|
import { CardList } from "@/components/ui/CardList";
|
||||||
import { actionGetUserFavorites } from "@/modules/folder/folder-aciton";
|
import { actionGetUserFavorites, actionToggleFavorite } from "@/modules/folder/folder-aciton";
|
||||||
|
|
||||||
type UserFavorite = {
|
type UserFavorite = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -27,11 +28,27 @@ type UserFavorite = {
|
|||||||
|
|
||||||
interface FavoriteCardProps {
|
interface FavoriteCardProps {
|
||||||
favorite: UserFavorite;
|
favorite: UserFavorite;
|
||||||
|
onRemoveFavorite: (folderId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FavoriteCard = ({ favorite }: FavoriteCardProps) => {
|
const FavoriteCard = ({ favorite, onRemoveFavorite }: FavoriteCardProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations("favorites");
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -57,7 +74,11 @@ const FavoriteCard = ({ favorite }: FavoriteCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<ChevronRight size={20} className="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,11 +107,14 @@ export function FavoritesClient({ userId }: FavoritesClientProps) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemoveFavorite = (folderId: number) => {
|
||||||
|
setFavorites((prev) => prev.filter((f) => f.folderId !== folderId));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<CardList>
|
<CardList>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
@@ -106,11 +130,14 @@ export function FavoritesClient({ userId }: FavoritesClientProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
favorites.map((favorite) => (
|
favorites.map((favorite) => (
|
||||||
<FavoriteCard key={favorite.id} favorite={favorite} />
|
<FavoriteCard
|
||||||
|
key={favorite.id}
|
||||||
|
favorite={favorite}
|
||||||
|
onRemoveFavorite={handleRemoveFavorite}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</CardList>
|
</CardList>
|
||||||
</div>
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
|||||||
|
|
||||||
interface FolderCardProps {
|
interface FolderCardProps {
|
||||||
folder: TSharedFolderWithTotalPairs;
|
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 router = useRouter();
|
||||||
const t = useTranslations("folders");
|
const t = useTranslations("folders");
|
||||||
|
|
||||||
@@ -40,12 +41,38 @@ const FolderCard = ({ folder, refresh }: FolderCardProps) => {
|
|||||||
const newVisibility = folder.visibility === "PUBLIC" ? "PRIVATE" : "PUBLIC";
|
const newVisibility = folder.visibility === "PUBLIC" ? "PRIVATE" : "PUBLIC";
|
||||||
const result = await actionSetFolderVisibility(folder.id, newVisibility);
|
const result = await actionSetFolderVisibility(folder.id, newVisibility);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
refresh();
|
onUpdateFolder(folder.id, { visibility: newVisibility });
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message);
|
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 (
|
return (
|
||||||
<div
|
<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"
|
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} />
|
<Globe size={18} />
|
||||||
)}
|
)}
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton onClick={handleRename}>
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FolderPen size={18} />
|
<FolderPen size={18} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={handleDelete}
|
||||||
e.stopPropagation();
|
className="hover:text-red-500 hover:bg-red-50"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<Trash2 size={18} />
|
<Trash2 size={18} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<ChevronRight size={20} className="text-gray-400 ml-1" />
|
<ChevronRight size={20} className="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -155,34 +156,38 @@ export function FoldersClient({ userId }: FoldersClientProps) {
|
|||||||
setLoading(false);
|
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 handleCreateFolder = async () => {
|
||||||
const folderName = prompt(t("enterFolderName"));
|
const folderName = prompt(t("enterFolderName"));
|
||||||
if (!folderName) return;
|
if (!folderName?.trim()) return;
|
||||||
setLoading(true);
|
|
||||||
try {
|
const result = await actionCreateFolder(userId, folderName.trim());
|
||||||
const result = await actionCreateFolder(userId, folderName);
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
loadFolders();
|
loadFolders();
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message);
|
toast.error(result.message);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||||
|
|
||||||
<LightButton
|
<div className="mb-4">
|
||||||
onClick={handleCreateFolder}
|
<LightButton onClick={handleCreateFolder}>
|
||||||
disabled={loading}
|
<FolderPlus size={18} />
|
||||||
className="w-full border-dashed mb-4"
|
{t("newFolder")}
|
||||||
>
|
|
||||||
<FolderPlus size={20} />
|
|
||||||
<span>{loading ? t("creating") : t("newFolder")}</span>
|
|
||||||
</LightButton>
|
</LightButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CardList>
|
<CardList>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -193,15 +198,18 @@ export function FoldersClient({ userId }: FoldersClientProps) {
|
|||||||
) : folders.length === 0 ? (
|
) : folders.length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-400">
|
<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">
|
<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>
|
</div>
|
||||||
<p className="text-sm">{t("noFoldersYet")}</p>
|
<p className="text-sm">{t("noFoldersYet")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
folders
|
folders.map((folder) => (
|
||||||
.toSorted((a, b) => b.id - a.id)
|
<FolderCard
|
||||||
.map((folder) => (
|
key={folder.id}
|
||||||
<FolderCard key={folder.id} folder={folder} refresh={loadFolders} />
|
folder={folder}
|
||||||
|
onUpdateFolder={handleUpdateFolder}
|
||||||
|
onDeleteFolder={handleDeleteFolder}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</CardList>
|
</CardList>
|
||||||
|
|||||||
Reference in New Issue
Block a user