Skip to content

Commit 5d50269

Browse files
committed
feat(cards): add render limit and optimize data management
- limit initial card display to 168 with load all button - refactor useDragSort to pending reorder pattern - add optimistic cache updates for game deletion
1 parent 6957cb4 commit 5d50269

2 files changed

Lines changed: 66 additions & 35 deletions

File tree

src/components/Cards.tsx

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { CSS } from "@dnd-kit/utilities";
2727
import CheckIcon from "@mui/icons-material/Check";
2828
import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
2929
import Box from "@mui/material/Box";
30+
import Button from "@mui/material/Button";
3031
import Card from "@mui/material/Card";
3132
import CardActionArea from "@mui/material/CardActionArea";
3233
import CardMedia from "@mui/material/CardMedia";
@@ -241,16 +242,18 @@ function useDragSort(options: {
241242
const { gamesData, categoryId, enabled } = options;
242243
const updateCategoryGamesMutation = useUpdateCategoryGames();
243244

244-
const [games, setGames] = useState(gamesData);
245+
const [sortableGames, setSortableGames] = useState(gamesData);
245246
const [activeId, setActiveId] = useState<number | null>(null);
246247
const isDraggingRef = useRef(false);
247248

248-
// 同步外部数据到本地状态(仅在非拖拽状态下)
249+
const games = enabled ? sortableGames : gamesData;
250+
251+
// 排序模式保留本地顺序,非排序模式直接使用外部数据,避免删除后慢一帧
249252
useEffect(() => {
250-
if (!isDraggingRef.current) {
251-
setGames(gamesData);
253+
if (enabled && !isDraggingRef.current) {
254+
setSortableGames(gamesData);
252255
}
253-
}, [gamesData]);
256+
}, [enabled, gamesData]);
254257

255258
// 传感器配置
256259
const sensors = useSensors(
@@ -271,6 +274,11 @@ function useDragSort(options: {
271274
[enabled],
272275
);
273276

277+
const handleDragCancel = useCallback(() => {
278+
isDraggingRef.current = false;
279+
setActiveId(null);
280+
}, []);
281+
274282
const handleDragEnd = useCallback(
275283
async (event: DragEndEvent) => {
276284
const { active, over } = event;
@@ -286,7 +294,7 @@ function useDragSort(options: {
286294

287295
if (oldIndex !== -1 && newIndex !== -1) {
288296
const newGames = arrayMove(games, oldIndex, newIndex);
289-
setGames(newGames);
297+
setSortableGames(newGames);
290298

291299
try {
292300
const gameIds = newGames.map((g) => g.id as number);
@@ -296,14 +304,11 @@ function useDragSort(options: {
296304
});
297305
} catch (error) {
298306
console.error("排序更新失败:", error);
299-
setGames(games); // 回滚
307+
setSortableGames(games); // 回滚
300308
}
301309
}
302310

303-
// 延迟重置拖拽状态
304-
setTimeout(() => {
305-
isDraggingRef.current = false;
306-
}, 100);
311+
isDraggingRef.current = false;
307312
},
308313
[games, categoryId, updateCategoryGamesMutation],
309314
);
@@ -319,6 +324,7 @@ function useDragSort(options: {
319324
activeGame,
320325
sensors,
321326
handleDragStart,
327+
handleDragCancel,
322328
handleDragEnd,
323329
};
324330
}
@@ -415,7 +421,6 @@ export const CardItem = memo(
415421
image={coverImage}
416422
alt="Card Image"
417423
draggable="false"
418-
loading="lazy"
419424
/>
420425
<div
421426
className={`flex items-center justify-center h-8 px-1 w-full ${isActive ? "!font-bold text-blue-500" : ""}`}
@@ -488,6 +493,10 @@ const Cards: React.FC<CardsProps> = ({ gamesData, categoryId }) => {
488493
const isCollectionCategory = typeof categoryId === "number" && categoryId > 0;
489494
const canUseBatchMode = isLibraries || isCollectionCategory;
490495

496+
// 卡片渲染限制
497+
const CARD_LIMIT = 168;
498+
const [showAll, setShowAll] = useState(false);
499+
491500
// Store 状态
492501
const {
493502
selectedGameId,
@@ -531,26 +540,31 @@ const Cards: React.FC<CardsProps> = ({ gamesData, categoryId }) => {
531540
!!gamesData;
532541

533542
// 拖拽排序 Hook
534-
const { games, activeGame, sensors, handleDragStart, handleDragEnd } =
535-
useDragSort({
536-
gamesData,
537-
categoryId,
538-
enabled: isSortable,
539-
});
543+
const {
544+
games,
545+
activeGame,
546+
sensors,
547+
handleDragStart,
548+
handleDragCancel,
549+
handleDragEnd,
550+
} = useDragSort({
551+
gamesData,
552+
categoryId,
553+
enabled: isSortable,
554+
});
555+
556+
// 根据 showAll 状态决定显示的卡片数量
557+
const displayedGames = useMemo(
558+
() => (showAll ? games : games.slice(0, CARD_LIMIT)),
559+
[games, showAll],
560+
);
540561

541562
// 缓存 SortableContext 的 items 数组,避免每次渲染重新创建
542563
const sortableIds = useMemo(() => games.map((g) => g.id as number), [games]);
543564
const gameIds = useMemo(
544565
() => games.map((game) => game.id).filter((id): id is number => id != null),
545566
[games],
546567
);
547-
const selectionScope = `${path}:${categoryId ?? "all"}`;
548-
549-
useEffect(() => {
550-
void selectionScope;
551-
setBatchMode(false);
552-
setSelectedBatchGameIds([]);
553-
}, [selectionScope]);
554568

555569
const toggleBatchGame = useCallback((gameId: number) => {
556570
setSelectedBatchGameIds((prev) =>
@@ -773,7 +787,7 @@ const Cards: React.FC<CardsProps> = ({ gamesData, categoryId }) => {
773787
"text-center grid lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8 3xl:grid-cols-10 4xl:grid-cols-12 gap-4"
774788
}
775789
>
776-
{games.map((card) => {
790+
{displayedGames.map((card) => {
777791
const props = getCardProps(card);
778792
return isSortable ? (
779793
<SortableCardItem key={card.id} {...props} />
@@ -782,6 +796,16 @@ const Cards: React.FC<CardsProps> = ({ gamesData, categoryId }) => {
782796
);
783797
})}
784798
</div>
799+
{!showAll && games.length > CARD_LIMIT && (
800+
<Box className="flex justify-center py-6">
801+
<Button variant="outlined" onClick={() => setShowAll(true)}>
802+
{t("components.Cards.loadAll", {
803+
defaultValue: `加载全部(${games.length})`,
804+
count: games.length,
805+
})}
806+
</Button>
807+
</Box>
808+
)}
785809
</div>
786810
</>
787811
);
@@ -793,6 +817,7 @@ const Cards: React.FC<CardsProps> = ({ gamesData, categoryId }) => {
793817
sensors={sensors}
794818
collisionDetection={closestCenter}
795819
onDragStart={handleDragStart}
820+
onDragCancel={handleDragCancel}
796821
onDragEnd={handleDragEnd}
797822
>
798823
<SortableContext items={sortableIds} strategy={rectSortingStrategy}>

src/hooks/queries/useGames.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { GameType, SortOption, SortOrder } from "@/services/invoke";
1414
import { gameService } from "@/services/invoke";
1515
import type {
1616
BatchOperationResult,
17+
FullGameData,
1718
InsertGameParams,
1819
UpdateGameParams,
1920
} from "@/types";
@@ -148,10 +149,12 @@ function useDeleteGame() {
148149
return useMutation({
149150
mutationFn: (gameId: number) => gameService.deleteGame(gameId),
150151
onSuccess: (_, gameId) => {
151-
queryClient.invalidateQueries({
152-
queryKey: gameKeys.all,
153-
exact: true,
154-
});
152+
// 乐观更新:立即从缓存中移除已删除的游戏,避免导航回来时闪烁
153+
queryClient.setQueriesData<FullGameData[]>(
154+
{ queryKey: gameKeys.lists() },
155+
(old) => old?.filter((g) => g.id !== gameId),
156+
);
157+
queryClient.invalidateQueries({ queryKey: gameKeys.all, exact: true });
155158
queryClient.invalidateQueries({ queryKey: gameKeys.lists() });
156159
queryClient.invalidateQueries({
157160
queryKey: gameKeys.detail(gameId),
@@ -168,11 +171,14 @@ function useDeleteGames() {
168171

169172
return useMutation({
170173
mutationFn: (gameIds: number[]) => gameService.deleteGames(gameIds),
171-
onSuccess: () => {
172-
queryClient.invalidateQueries({
173-
queryKey: gameKeys.all,
174-
exact: true,
175-
});
174+
onSuccess: (_, gameIds) => {
175+
// 乐观更新:立即从缓存中移除已删除的游戏,避免导航回来时闪烁
176+
const deleteIdSet = new Set(gameIds);
177+
queryClient.setQueriesData<FullGameData[]>(
178+
{ queryKey: gameKeys.lists() },
179+
(old) => old?.filter((g) => g.id != null && !deleteIdSet.has(g.id)),
180+
);
181+
queryClient.invalidateQueries({ queryKey: gameKeys.all, exact: true });
176182
queryClient.invalidateQueries({ queryKey: gameKeys.lists() });
177183
queryClient.invalidateQueries({ queryKey: ["collections"] });
178184
queryClient.invalidateQueries({ queryKey: ["stats"] });

0 commit comments

Comments
 (0)