Skip to content

Commit c4f4a1f

Browse files
committed
refactor(certain settings): migrate zustand's async logic to react-query and build feat layer to handle the complex logic between UI/UX and zustand and react-query
1 parent d0ed1f9 commit c4f4a1f

18 files changed

Lines changed: 411 additions & 326 deletions

File tree

src/components/AddModal/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { isYmgalDataComplete } from "@/api/gameMetadataService";
4242
import { ViewGameBox } from "@/components/AlertBox";
4343
import { snackbar } from "@/components/Snackbar";
4444
import { useTauriDragDrop } from "@/hooks/common/useTauriDragDrop";
45+
import { useSettingsResources } from "@/hooks/queries/useSettings";
4546
import { useStore } from "@/store/";
4647
import type { FullGameData, InsertGameParams } from "@/types";
4748
import { handleDirectory } from "@/utils";
@@ -159,8 +160,8 @@ function extractFolderName(path: string): string {
159160
const AddModal: React.FC = () => {
160161
const { t } = useTranslation();
161162
const navigate = useNavigate();
163+
const { bgmToken } = useSettingsResources();
162164

163-
const bgmToken = useStore((state) => state.bgmToken);
164165
const apiSource = useStore((state) => state.apiSource);
165166
const setApiSource = useStore((state) => state.setApiSource);
166167
const addGame = useStore((state) => state.addGame);

src/components/RightMenu/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import { useEffect, useState } from "react";
3333
import { useTranslation } from "react-i18next";
3434
import { AlertConfirmBox } from "@/components/AlertBox";
35-
import { useUpdatePlayStatusWithRefresh } from "@/hooks/queries/usePlayStatus";
35+
import { useGameStatusActions } from "@/hooks/features/games/useGameStatusActions";
3636
import { useStore } from "@/store";
3737
import { useGamePlayStore } from "@/store/gamePlayStore";
3838
import type { GameData } from "@/types";
@@ -72,8 +72,8 @@ const RightMenu: React.FC<RightMenuProps> = ({
7272
const [gameData, setGameData] = useState<GameData | null>(null);
7373
const { t } = useTranslation();
7474

75-
// 使用 react-query mutation 更新游戏状态
76-
const { mutate: updatePlayStatus } = useUpdatePlayStatusWithRefresh();
75+
// 使用 Feature Facade 更新游戏状态
76+
const { updatePlayStatus } = useGameStatusActions();
7777

7878
// 检查该游戏是否正在运行
7979
const isThisGameRunning = isGameRunning(id === null ? undefined : id);
@@ -151,6 +151,7 @@ const RightMenu: React.FC<RightMenuProps> = ({
151151
updatePlayStatus(
152152
{ gameId: id, newStatus },
153153
{
154+
invalidateScope: "all",
154155
onSuccess: (updatedGame) => {
155156
// 更新本地状态,不关闭菜单
156157
setGameData(updatedGame);

src/components/Toolbar/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { PathSettingsModal } from "@/components/PathSettingsModal";
5555
import { PlayStatusSubmenu } from "@/components/RightMenu/PlayStatusSubmenu";
5656
import { snackbar } from "@/components/Snackbar";
5757
import SortModal from "@/components/SortModal";
58-
import { useUpdatePlayStatus } from "@/hooks/queries/usePlayStatus";
58+
import { useGameStatusActions } from "@/hooks/features/games/useGameStatusActions";
5959
import { settingsService } from "@/services";
6060
import { useStore } from "@/store";
6161
import type { HanleGamesProps } from "@/types";
@@ -191,8 +191,8 @@ const MoreButton = () => {
191191
const open = Boolean(anchorEl);
192192
const [pathSettingsModalOpen, setPathSettingsModalOpen] = useState(false);
193193

194-
// 使用 react-query mutation 更新游戏状态
195-
const { mutate: updatePlayStatus } = useUpdatePlayStatus();
194+
// 使用 Feature Facade 更新游戏状态
195+
const { updatePlayStatus } = useGameStatusActions();
196196

197197
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
198198
setAnchorEl(event.currentTarget);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { useTranslation } from "react-i18next";
2+
import { snackbar } from "@/components/Snackbar";
3+
import {
4+
type UpdatePlayStatusParams,
5+
useUpdatePlayStatus,
6+
} from "@/hooks/queries/usePlayStatus";
7+
import { useStore } from "@/store";
8+
import type { GameData } from "@/types";
9+
import { getErrorMessage } from "@/utils";
10+
11+
interface UpdatePlayStatusOptions {
12+
invalidateScope?: "game" | "all";
13+
onSuccess?: (
14+
updatedGame: GameData,
15+
variables: UpdatePlayStatusParams,
16+
) => void;
17+
onError?: (error: Error, variables: UpdatePlayStatusParams) => void;
18+
onSettled?: (
19+
updatedGame: GameData | undefined,
20+
error: Error | null,
21+
variables: UpdatePlayStatusParams,
22+
) => void;
23+
}
24+
25+
/**
26+
* 游戏状态更新业务编排层
27+
*
28+
* 说明:
29+
* - 组合 Query Mutation 与 Zustand UI 状态同步
30+
* - 对外保持接近 useStore 的动作调用体验
31+
* - 统一处理错误提示与回滚逻辑
32+
*/
33+
export function useGameStatusActions() {
34+
const { t } = useTranslation();
35+
const { updateGamePlayStatusInStore, setSelectedGame } = useStore();
36+
const updateMutation = useUpdatePlayStatus();
37+
38+
const updatePlayStatus = (
39+
params: UpdatePlayStatusParams,
40+
options?: UpdatePlayStatusOptions,
41+
) => {
42+
const invalidateScope = options?.invalidateScope ?? "game";
43+
const useGlobalInvalidate = invalidateScope === "all";
44+
45+
const currentSelectedGame = useStore.getState().selectedGame;
46+
const previousGame =
47+
!useGlobalInvalidate && currentSelectedGame?.id === params.gameId
48+
? { ...currentSelectedGame }
49+
: undefined;
50+
51+
if (!useGlobalInvalidate) {
52+
updateGamePlayStatusInStore(params.gameId, params.newStatus, true);
53+
}
54+
55+
updateMutation.mutate(
56+
{ ...params, invalidateScope },
57+
{
58+
onSuccess: (updatedGame, variables) => {
59+
if (useGlobalInvalidate) {
60+
updateGamePlayStatusInStore(
61+
variables.gameId,
62+
variables.newStatus,
63+
false,
64+
);
65+
} else {
66+
const latestSelectedGame = useStore.getState().selectedGame;
67+
if (latestSelectedGame?.id === variables.gameId) {
68+
setSelectedGame(updatedGame);
69+
}
70+
}
71+
options?.onSuccess?.(updatedGame, variables);
72+
},
73+
onError: (error, variables) => {
74+
if (!useGlobalInvalidate && previousGame) {
75+
updateGamePlayStatusInStore(
76+
variables.gameId,
77+
previousGame.clear ?? 1,
78+
true,
79+
);
80+
const latestSelectedGame = useStore.getState().selectedGame;
81+
if (latestSelectedGame?.id === variables.gameId) {
82+
setSelectedGame(previousGame);
83+
}
84+
}
85+
snackbar.error(
86+
`${t("errors.updatePlayStatusFailed", "更新游戏状态失败")}: ${getErrorMessage(error)}`,
87+
);
88+
options?.onError?.(error, variables);
89+
},
90+
onSettled: (updatedGame, error, variables) => {
91+
options?.onSettled?.(updatedGame, error, variables);
92+
},
93+
},
94+
);
95+
};
96+
97+
return {
98+
updatePlayStatus,
99+
isUpdatingPlayStatus: updateMutation.isPending,
100+
};
101+
}

src/hooks/queries/usePlayStatus.ts

Lines changed: 26 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
*/
1010

1111
import { useMutation, useQueryClient } from "@tanstack/react-query";
12-
import { useTranslation } from "react-i18next";
13-
import { snackbar } from "@/components/Snackbar";
1412
import { gameService } from "@/services";
15-
import { useStore } from "@/store";
1613
import type { GameData } from "@/types";
1714
import type { PlayStatus } from "@/types/collection";
1815
import { getDisplayGameData } from "@/utils/dataTransform";
@@ -30,13 +27,30 @@ export const playStatusKeys = {
3027
// Mutations - 数据操作 hooks
3128
// ============================================================================
3229

33-
interface UpdatePlayStatusParams {
30+
export interface UpdatePlayStatusParams {
3431
gameId: number;
3532
newStatus: PlayStatus;
33+
invalidateScope?: "game" | "all";
3634
}
3735

38-
interface UpdatePlayStatusContext {
39-
previousGame?: GameData;
36+
async function updatePlayStatus({
37+
gameId,
38+
newStatus,
39+
}: UpdatePlayStatusParams): Promise<GameData> {
40+
const fullGame = await gameService.getGameById(gameId);
41+
if (!fullGame) {
42+
throw new Error("游戏数据未找到");
43+
}
44+
45+
await gameService.updateGame(gameId, {
46+
clear: newStatus,
47+
});
48+
49+
const game = getDisplayGameData(fullGame);
50+
return {
51+
...game,
52+
clear: newStatus,
53+
};
4054
}
4155

4256
/**
@@ -53,121 +67,18 @@ interface UpdatePlayStatusContext {
5367
*/
5468
export function useUpdatePlayStatus() {
5569
const queryClient = useQueryClient();
56-
const { t } = useTranslation();
57-
const { updateGamePlayStatusInStore, setSelectedGame, selectedGame } =
58-
useStore();
59-
60-
return useMutation<
61-
GameData,
62-
Error,
63-
UpdatePlayStatusParams,
64-
UpdatePlayStatusContext
65-
>({
66-
mutationFn: async ({ gameId, newStatus }) => {
67-
// 获取完整游戏数据
68-
const fullGame = await gameService.getGameById(gameId);
69-
if (!fullGame) {
70-
throw new Error(t("errors.gameNotFound", "游戏数据未找到"));
71-
}
72-
73-
// 更新数据库
74-
await gameService.updateGame(gameId, {
75-
clear: newStatus,
76-
});
77-
78-
// 返回更新后的游戏数据
79-
const game = getDisplayGameData(fullGame);
80-
return {
81-
...game,
82-
clear: newStatus,
83-
};
84-
},
85-
onMutate: async ({ gameId, newStatus }) => {
86-
// 乐观更新:立即更新 store 中的数据
87-
const previousGame =
88-
selectedGame?.id === gameId ? { ...selectedGame } : undefined;
89-
90-
// 更新 store 中的游戏列表
91-
updateGamePlayStatusInStore(gameId, newStatus, true);
92-
93-
return { previousGame };
94-
},
95-
onSuccess: (updatedGame, { gameId }) => {
96-
// 如果当前选中的游戏就是被更新的游戏,更新 selectedGame
97-
if (selectedGame?.id === gameId) {
98-
setSelectedGame(updatedGame);
99-
}
100-
101-
// 使相关的查询失效,触发重新获取
102-
queryClient.invalidateQueries({
103-
queryKey: playStatusKeys.game(gameId),
104-
});
105-
},
106-
onError: (error, { gameId }, context) => {
107-
// 发生错误时回滚到之前的状态
108-
if (context?.previousGame) {
109-
updateGamePlayStatusInStore(
110-
gameId,
111-
context.previousGame.clear ?? 1,
112-
true,
113-
);
114-
if (selectedGame?.id === gameId) {
115-
setSelectedGame(context.previousGame);
116-
}
117-
}
118-
119-
snackbar.error(
120-
`${t("errors.updatePlayStatusFailed", "更新游戏状态失败")}: ${error.message}`,
121-
);
122-
},
123-
});
124-
}
125-
126-
/**
127-
* 更新游戏状态(用于右键菜单,需要刷新游戏列表)
128-
*
129-
* 与 useUpdatePlayStatus 的区别:
130-
* - 成功后会刷新游戏列表(skipRefresh = false)
131-
* - 适用于库列表页面的右键菜单
132-
*/
133-
export function useUpdatePlayStatusWithRefresh() {
134-
const queryClient = useQueryClient();
135-
const { t } = useTranslation();
136-
const { updateGamePlayStatusInStore } = useStore();
13770

13871
return useMutation<GameData, Error, UpdatePlayStatusParams>({
139-
mutationFn: async ({ gameId, newStatus }) => {
140-
// 获取完整游戏数据
141-
const fullGame = await gameService.getGameById(gameId);
142-
if (!fullGame) {
143-
throw new Error(t("errors.gameNotFound", "游戏数据未找到"));
72+
mutationFn: updatePlayStatus,
73+
onSuccess: (_, { gameId, invalidateScope = "game" }) => {
74+
if (invalidateScope === "all") {
75+
queryClient.invalidateQueries({
76+
queryKey: playStatusKeys.all,
77+
});
14478
}
145-
146-
// 更新数据库
147-
await gameService.updateGame(gameId, {
148-
clear: newStatus,
149-
});
150-
151-
// 返回更新后的游戏数据
152-
const game = getDisplayGameData(fullGame);
153-
return {
154-
...game,
155-
clear: newStatus,
156-
};
157-
},
158-
onSuccess: (_, { gameId, newStatus }) => {
159-
// 更新 store 并刷新游戏列表
160-
updateGamePlayStatusInStore(gameId, newStatus, false);
161-
162-
// 使相关的查询失效
16379
queryClient.invalidateQueries({
16480
queryKey: playStatusKeys.game(gameId),
16581
});
16682
},
167-
onError: (error) => {
168-
snackbar.error(
169-
`${t("errors.updatePlayStatusFailed", "更新游戏状态失败")}: ${error.message}`,
170-
);
171-
},
17283
});
17384
}

0 commit comments

Comments
 (0)