This commit is contained in:
382
src/components/rpg-entry/useRpgEntryBootstrap.ts
Normal file
382
src/components/rpg-entry/useRpgEntryBootstrap.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
CustomWorldWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
CustomWorldGalleryCard,
|
||||
CustomWorldLibraryEntry,
|
||||
PlatformBrowseHistoryEntry,
|
||||
PlatformBrowseHistoryWriteEntry,
|
||||
ProfileDashboardSummary,
|
||||
ProfileSaveArchiveSummary,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import { listRpgCreationWorks } from '../../services/rpg-creation/index';
|
||||
import {
|
||||
listRpgEntryWorldGallery,
|
||||
listRpgEntryWorldLibrary,
|
||||
listRpgProfileBrowseHistory,
|
||||
listRpgProfileSaveArchives,
|
||||
resumeRpgProfileSaveArchive,
|
||||
upsertRpgProfileBrowseHistory,
|
||||
} from '../../services/rpg-entry';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import type { PlatformHomeTab } from './RpgEntryHomeView';
|
||||
import { resolveRpgEntryErrorMessage } from './rpgEntryShared';
|
||||
|
||||
type UseRpgEntryBootstrapParams = {
|
||||
user: AuthUser | null | undefined;
|
||||
canAccessProtectedData?: boolean | undefined;
|
||||
getProfileDashboard: () => Promise<ProfileDashboardSummary | null>;
|
||||
handleContinueGame: (
|
||||
snapshot?: HydratedSavedGameSnapshot | null,
|
||||
) => void;
|
||||
hasInitialAgentSession: boolean;
|
||||
};
|
||||
|
||||
export function useRpgEntryBootstrap(
|
||||
params: UseRpgEntryBootstrapParams,
|
||||
) {
|
||||
const {
|
||||
user,
|
||||
canAccessProtectedData = Boolean(user),
|
||||
getProfileDashboard,
|
||||
handleContinueGame,
|
||||
hasInitialAgentSession,
|
||||
} = params;
|
||||
const isAuthenticated = Boolean(user);
|
||||
const canReadProtectedData = Boolean(user) && canAccessProtectedData;
|
||||
const platformTabBootstrapUserIdRef = useRef<string | null | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const hasExplicitPlatformTabSelectionRef = useRef(false);
|
||||
|
||||
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
|
||||
CustomWorldLibraryEntry<CustomWorldProfile>[]
|
||||
>([]);
|
||||
const [customWorldWorkEntries, setCustomWorldWorkEntries] = useState<
|
||||
CustomWorldWorkSummary[]
|
||||
>([]);
|
||||
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
|
||||
CustomWorldGalleryCard[]
|
||||
>([]);
|
||||
const [historyEntries, setHistoryEntries] = useState<
|
||||
PlatformBrowseHistoryEntry[]
|
||||
>([]);
|
||||
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>([]);
|
||||
const [platformTab, setPlatformTabState] = useState<PlatformHomeTab>('home');
|
||||
const [platformError, setPlatformError] = useState<string | null>(null);
|
||||
const [dashboardError, setDashboardError] = useState<string | null>(null);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [isLoadingPlatform, setIsLoadingPlatform] = useState(false);
|
||||
const [isLoadingDashboard, setIsLoadingDashboard] = useState(false);
|
||||
const [isResumingSaveWorldKey, setIsResumingSaveWorldKey] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [profileDashboard, setProfileDashboard] =
|
||||
useState<ProfileDashboardSummary | null>(null);
|
||||
|
||||
const setPlatformTab = useCallback((nextTab: PlatformHomeTab) => {
|
||||
// 区分“平台首屏默认落点”和“用户/流程显式切换”。
|
||||
// 一旦显式切过 Tab,就不能再被首屏异步请求回刷成首页或存档。
|
||||
hasExplicitPlatformTabSelectionRef.current = true;
|
||||
setPlatformTabState(nextTab);
|
||||
}, []);
|
||||
|
||||
const refreshProfileDashboard = useCallback(async () => {
|
||||
if (!user || !canReadProtectedData) {
|
||||
setProfileDashboard(null);
|
||||
setDashboardError(null);
|
||||
setIsLoadingDashboard(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingDashboard(true);
|
||||
setDashboardError(null);
|
||||
|
||||
try {
|
||||
setProfileDashboard(await getProfileDashboard());
|
||||
} catch (error) {
|
||||
setDashboardError(
|
||||
resolveRpgEntryErrorMessage(error, '读取个人数据看板失败。'),
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingDashboard(false);
|
||||
}
|
||||
}, [canReadProtectedData, getProfileDashboard, user]);
|
||||
|
||||
const refreshCustomWorldWorks = useCallback(async () => {
|
||||
if (!user || !canReadProtectedData) {
|
||||
setCustomWorldWorkEntries([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
const nextItems = await listRpgCreationWorks();
|
||||
setCustomWorldWorkEntries(nextItems);
|
||||
return nextItems;
|
||||
}, [canReadProtectedData, user]);
|
||||
|
||||
const refreshPublishedGallery = useCallback(async () => {
|
||||
const nextEntries = await listRpgEntryWorldGallery();
|
||||
setPublishedGalleryEntries(nextEntries);
|
||||
return nextEntries;
|
||||
}, []);
|
||||
|
||||
const refreshSavedCustomWorldLibrary = useCallback(async () => {
|
||||
if (!user || !canReadProtectedData) {
|
||||
setSavedCustomWorldEntries([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
const nextEntries = await listRpgEntryWorldLibrary();
|
||||
setSavedCustomWorldEntries(nextEntries);
|
||||
return nextEntries;
|
||||
}, [canReadProtectedData, user]);
|
||||
|
||||
const appendBrowseHistoryEntry = useCallback(
|
||||
async (entry: PlatformBrowseHistoryWriteEntry) => {
|
||||
setHistoryError(null);
|
||||
|
||||
try {
|
||||
const syncedEntries = await upsertRpgProfileBrowseHistory(entry);
|
||||
setHistoryEntries(syncedEntries);
|
||||
} catch (error) {
|
||||
setHistoryError(
|
||||
resolveRpgEntryErrorMessage(error, '写入浏览历史失败。'),
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleResumeSaveEntry = useCallback(
|
||||
async (entry: ProfileSaveArchiveSummary) => {
|
||||
if (!user || !canReadProtectedData || isResumingSaveWorldKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResumingSaveWorldKey(entry.worldKey);
|
||||
setSaveError(null);
|
||||
|
||||
try {
|
||||
const resumedArchive = await resumeRpgProfileSaveArchive(entry.worldKey);
|
||||
setSaveEntries((currentEntries) =>
|
||||
currentEntries.map((currentEntry) =>
|
||||
currentEntry.worldKey === resumedArchive.entry.worldKey
|
||||
? resumedArchive.entry
|
||||
: currentEntry,
|
||||
),
|
||||
);
|
||||
handleContinueGame(resumedArchive.snapshot);
|
||||
} catch (error) {
|
||||
setSaveError(resolveRpgEntryErrorMessage(error, '恢复存档失败。'));
|
||||
} finally {
|
||||
setIsResumingSaveWorldKey(null);
|
||||
}
|
||||
},
|
||||
[canReadProtectedData, handleContinueGame, isResumingSaveWorldKey, user],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
const nextPlatformBootstrapUserId = user?.id ?? null;
|
||||
const shouldApplyInitialPlatformTab =
|
||||
platformTabBootstrapUserIdRef.current !== nextPlatformBootstrapUserId;
|
||||
|
||||
if (shouldApplyInitialPlatformTab) {
|
||||
// 在请求发出前先占位,避免首屏请求未完成时用户切了 Tab,
|
||||
// 返回结果又被误判成“还没初始化过”并强制跳回默认页。
|
||||
platformTabBootstrapUserIdRef.current = nextPlatformBootstrapUserId;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
setHistoryEntries([]);
|
||||
setHistoryError(null);
|
||||
setSaveError(null);
|
||||
setIsLoadingPlatform(true);
|
||||
setPlatformError(null);
|
||||
setIsLoadingDashboard(canReadProtectedData);
|
||||
setDashboardError(null);
|
||||
if (!canReadProtectedData) {
|
||||
setSavedCustomWorldEntries([]);
|
||||
setCustomWorldWorkEntries([]);
|
||||
setSaveEntries([]);
|
||||
setProfileDashboard(null);
|
||||
}
|
||||
|
||||
try {
|
||||
const [
|
||||
libraryEntriesResult,
|
||||
workEntriesResult,
|
||||
galleryEntriesResult,
|
||||
dashboardResult,
|
||||
historyResult,
|
||||
saveArchivesResult,
|
||||
] = await Promise.allSettled([
|
||||
canReadProtectedData
|
||||
? listRpgEntryWorldLibrary()
|
||||
: Promise.resolve([]),
|
||||
canReadProtectedData
|
||||
? listRpgCreationWorks()
|
||||
: Promise.resolve([]),
|
||||
listRpgEntryWorldGallery(),
|
||||
canReadProtectedData ? getProfileDashboard() : Promise.resolve(null),
|
||||
canReadProtectedData
|
||||
? listRpgProfileBrowseHistory()
|
||||
: Promise.resolve([]),
|
||||
canReadProtectedData
|
||||
? listRpgProfileSaveArchives()
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (libraryEntriesResult.status === 'fulfilled') {
|
||||
setSavedCustomWorldEntries(libraryEntriesResult.value);
|
||||
} else {
|
||||
setSavedCustomWorldEntries([]);
|
||||
}
|
||||
|
||||
if (workEntriesResult.status === 'fulfilled') {
|
||||
setCustomWorldWorkEntries(workEntriesResult.value);
|
||||
} else {
|
||||
setCustomWorldWorkEntries([]);
|
||||
}
|
||||
|
||||
if (galleryEntriesResult.status === 'fulfilled') {
|
||||
setPublishedGalleryEntries(galleryEntriesResult.value);
|
||||
} else {
|
||||
setPublishedGalleryEntries([]);
|
||||
}
|
||||
|
||||
if (
|
||||
(canReadProtectedData &&
|
||||
libraryEntriesResult.status === 'rejected') ||
|
||||
(canReadProtectedData &&
|
||||
workEntriesResult.status === 'rejected') ||
|
||||
galleryEntriesResult.status === 'rejected'
|
||||
) {
|
||||
const platformFailure =
|
||||
libraryEntriesResult.status === 'rejected'
|
||||
? libraryEntriesResult.reason
|
||||
: workEntriesResult.status === 'rejected'
|
||||
? workEntriesResult.reason
|
||||
: galleryEntriesResult.status === 'rejected'
|
||||
? galleryEntriesResult.reason
|
||||
: null;
|
||||
setPlatformError(
|
||||
resolveRpgEntryErrorMessage(platformFailure, '读取平台数据失败。'),
|
||||
);
|
||||
}
|
||||
|
||||
if (dashboardResult.status === 'fulfilled') {
|
||||
setProfileDashboard(dashboardResult.value);
|
||||
} else if (canReadProtectedData) {
|
||||
setProfileDashboard(null);
|
||||
setDashboardError(
|
||||
resolveRpgEntryErrorMessage(
|
||||
dashboardResult.reason,
|
||||
'读取个人数据看板失败。',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (historyResult.status === 'fulfilled') {
|
||||
setHistoryEntries(historyResult.value);
|
||||
} else if (canReadProtectedData) {
|
||||
setHistoryError(
|
||||
resolveRpgEntryErrorMessage(historyResult.reason, '读取浏览历史失败。'),
|
||||
);
|
||||
}
|
||||
|
||||
if (saveArchivesResult.status === 'fulfilled') {
|
||||
setSaveEntries(saveArchivesResult.value);
|
||||
} else if (canReadProtectedData) {
|
||||
setSaveEntries([]);
|
||||
setSaveError(
|
||||
resolveRpgEntryErrorMessage(
|
||||
saveArchivesResult.reason,
|
||||
'读取存档列表失败。',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
shouldApplyInitialPlatformTab &&
|
||||
!hasInitialAgentSession &&
|
||||
!hasExplicitPlatformTabSelectionRef.current
|
||||
) {
|
||||
setPlatformTabState(
|
||||
isAuthenticated &&
|
||||
canReadProtectedData &&
|
||||
saveArchivesResult.status === 'fulfilled' &&
|
||||
saveArchivesResult.value.length > 0
|
||||
? 'saves'
|
||||
: 'home',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (isActive) {
|
||||
setIsLoadingPlatform(false);
|
||||
setIsLoadingDashboard(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [
|
||||
canReadProtectedData,
|
||||
getProfileDashboard,
|
||||
hasInitialAgentSession,
|
||||
isAuthenticated,
|
||||
user,
|
||||
]);
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
canReadProtectedData,
|
||||
platformTab,
|
||||
setPlatformTab,
|
||||
savedCustomWorldEntries,
|
||||
setSavedCustomWorldEntries,
|
||||
customWorldWorkEntries,
|
||||
setCustomWorldWorkEntries,
|
||||
publishedGalleryEntries,
|
||||
setPublishedGalleryEntries,
|
||||
historyEntries,
|
||||
setHistoryEntries,
|
||||
saveEntries,
|
||||
setSaveEntries,
|
||||
platformError,
|
||||
setPlatformError,
|
||||
dashboardError,
|
||||
setDashboardError,
|
||||
historyError,
|
||||
setHistoryError,
|
||||
saveError,
|
||||
setSaveError,
|
||||
isLoadingPlatform,
|
||||
isLoadingDashboard,
|
||||
isResumingSaveWorldKey,
|
||||
profileDashboard,
|
||||
setProfileDashboard,
|
||||
refreshProfileDashboard,
|
||||
refreshCustomWorldWorks,
|
||||
refreshPublishedGallery,
|
||||
refreshSavedCustomWorldLibrary,
|
||||
appendBrowseHistoryEntry,
|
||||
handleResumeSaveEntry,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧创作链命名,避免并行工作包在本轮迁移后断开导入。
|
||||
*/
|
||||
export const useRpgCreationPlatformBootstrap = useRpgEntryBootstrap;
|
||||
Reference in New Issue
Block a user