This commit is contained in:
@@ -99,7 +99,10 @@ import {
|
||||
buildPublicWorkStagePath,
|
||||
pushAppHistoryPath,
|
||||
} from '../../routing/appPageRoutes';
|
||||
import { resolveRuntimeNotFoundRecoveryAction } from '../../routing/runtimeNotFoundRecovery';
|
||||
import {
|
||||
resolveRuntimeNotFoundRecoveryAction,
|
||||
resolveWorkNotFoundRecoveryAction,
|
||||
} from '../../routing/runtimeNotFoundRecovery';
|
||||
import {
|
||||
ApiClientError,
|
||||
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||
@@ -907,6 +910,24 @@ function maybeAlertRuntimeNotFoundAndReturnHome() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function maybeAlertWorkNotFoundAndReturnHome() {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const recoveryAction = resolveWorkNotFoundRecoveryAction(
|
||||
window.location.pathname,
|
||||
);
|
||||
if (!recoveryAction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 中文注释:直接打开公开详情或运行态深链失效时,确认提示后必须离开空详情页。
|
||||
window.alert('作品不存在或已下架,将返回首页。');
|
||||
pushAppHistoryPath(recoveryAction.nextPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasSeenPuzzleOnboarding() {
|
||||
if (typeof window === 'undefined') {
|
||||
return true;
|
||||
@@ -4334,7 +4355,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPublicWorkDetailError(null);
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertRuntimeNotFoundAndReturnHome()) {
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
return false;
|
||||
@@ -5836,7 +5857,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPublicWorkDetailError(null);
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertRuntimeNotFoundAndReturnHome()) {
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
return;
|
||||
@@ -6057,7 +6078,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPublicWorkDetailError(null);
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertRuntimeNotFoundAndReturnHome()) {
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
return;
|
||||
@@ -7387,6 +7408,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
} catch (error) {
|
||||
if (selectionStage === 'work-detail') {
|
||||
setSelectedPublicWorkDetail(null);
|
||||
setSelectedDetailEntry(null);
|
||||
setSelectedPuzzleDetail(null);
|
||||
setPuzzleDetailReturnTarget(null);
|
||||
setPuzzleRun(null);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
if (!maybeAlertWorkNotFoundAndReturnHome()) {
|
||||
pushAppHistoryPath('/');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setPublicSearchError(
|
||||
resolveRpgCreationErrorMessage(error, '未找到对应的百梦号或作品号。'),
|
||||
);
|
||||
@@ -7407,6 +7445,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
refreshSquareHoleGallery,
|
||||
refreshVisualNovelGallery,
|
||||
squareHoleGalleryEntries,
|
||||
selectionStage,
|
||||
setPlatformTab,
|
||||
visualNovelGalleryEntries,
|
||||
],
|
||||
);
|
||||
@@ -8091,6 +8131,20 @@ export function PlatformEntryFlowShellImpl({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'work-detail' && !selectedPublicWorkDetail && (
|
||||
<motion.div
|
||||
key="platform-work-detail-empty"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 items-center justify-center"
|
||||
>
|
||||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||||
{publicWorkDetailError || '正在读取作品详情...'}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'work-detail' && selectedPublicWorkDetail && (
|
||||
<motion.div
|
||||
key="platform-work-detail"
|
||||
|
||||
@@ -31,6 +31,10 @@ import type {
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import {
|
||||
readPublicWorkCodeFromLocationSearch,
|
||||
resolveSelectionStageFromPath,
|
||||
} from '../../routing/appPageRoutes';
|
||||
import { ApiClientError } from '../../services/apiClient';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import {
|
||||
@@ -1423,15 +1427,17 @@ function TestWrapper({
|
||||
onSelectWorld?: RpgEntryFlowShellProps['handleCustomWorldSelect'];
|
||||
} = {}) {
|
||||
const [selectionStage, setSelectionStage] = useState<SelectionStage>(() =>
|
||||
window.location.pathname === '/creation/rpg/agent'
|
||||
? 'agent-workspace'
|
||||
: 'platform',
|
||||
resolveSelectionStageFromPath(window.location.pathname),
|
||||
);
|
||||
const [initialPublicWorkCode] = useState(() =>
|
||||
readPublicWorkCodeFromLocationSearch(window.location.search),
|
||||
);
|
||||
|
||||
const content = (
|
||||
<RpgEntryFlowShell
|
||||
selectionStage={selectionStage}
|
||||
setSelectionStage={setSelectionStage}
|
||||
initialPublicWorkCode={initialPublicWorkCode}
|
||||
hasSavedGame={false}
|
||||
savedSnapshot={null}
|
||||
handleContinueGame={onContinueGame ?? (() => {})}
|
||||
@@ -4392,6 +4398,39 @@ test('missing puzzle public detail returns to platform home', async () => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('direct missing public work detail alert returns to platform home', async () => {
|
||||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
|
||||
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/works/detail?work=PZ-7A7B18D9',
|
||||
);
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [],
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
expect(await screen.findByText('正在读取作品详情...')).toBeTruthy();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(alertSpy).toHaveBeenCalledWith('作品不存在或已下架,将返回首页。');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(window.location.pathname).toBe('/');
|
||||
});
|
||||
expect(window.location.search).toBe('');
|
||||
await waitFor(() => {
|
||||
expect(getPlatformTabPanel('home').getAttribute('aria-hidden')).toBe(
|
||||
'false',
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('详情')).toBeNull();
|
||||
expect(screen.queryByText('未找到拼图作品。')).toBeNull();
|
||||
expect(startPuzzleRun).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('public code search opens a published big fish work by BF code', async () => {
|
||||
const user = userEvent.setup();
|
||||
const bigFishWork: BigFishWorkSummary = {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { resolveRuntimeNotFoundRecoveryAction } from './runtimeNotFoundRecovery';
|
||||
import {
|
||||
resolveRuntimeNotFoundRecoveryAction,
|
||||
resolveWorkNotFoundRecoveryAction,
|
||||
} from './runtimeNotFoundRecovery';
|
||||
|
||||
test('runtime not found recovery returns home after direct runtime route alert', () => {
|
||||
expect(resolveRuntimeNotFoundRecoveryAction('/runtime/puzzle')).toEqual({
|
||||
@@ -19,3 +22,21 @@ test('runtime not found recovery only handles direct runtime routes', () => {
|
||||
expect(resolveRuntimeNotFoundRecoveryAction('/gallery/puzzle/detail')).toBeNull();
|
||||
expect(resolveRuntimeNotFoundRecoveryAction('/creation/puzzle/result')).toBeNull();
|
||||
});
|
||||
|
||||
test('work not found recovery returns home for direct public detail routes', () => {
|
||||
expect(resolveWorkNotFoundRecoveryAction('/works/detail')).toEqual({
|
||||
nextStage: 'platform',
|
||||
nextPath: '/',
|
||||
shouldAlert: true,
|
||||
});
|
||||
expect(resolveWorkNotFoundRecoveryAction('/works/detail/')).toEqual({
|
||||
nextStage: 'platform',
|
||||
nextPath: '/',
|
||||
shouldAlert: true,
|
||||
});
|
||||
expect(resolveWorkNotFoundRecoveryAction('/gallery/puzzle/detail')).toEqual({
|
||||
nextStage: 'platform',
|
||||
nextPath: '/',
|
||||
shouldAlert: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,3 +28,27 @@ export function resolveRuntimeNotFoundRecoveryAction(
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文注释:公开作品详情页和运行态深链都可能在作品被删除或下架后失效。
|
||||
* 这类入口没有上一层可回退的详情数据,确认提示后统一回首页,避免空详情页白屏。
|
||||
*/
|
||||
export function resolveWorkNotFoundRecoveryAction(
|
||||
pathname: string,
|
||||
): RuntimeNotFoundRecoveryAction | null {
|
||||
const normalizedPath = pathname.trim().toLowerCase().replace(/\/+$/u, '');
|
||||
|
||||
if (
|
||||
normalizedPath === '/works/detail' ||
|
||||
normalizedPath === '/gallery/puzzle/detail' ||
|
||||
normalizedPath === '/gallery/visual-novel/detail'
|
||||
) {
|
||||
return {
|
||||
nextStage: 'platform',
|
||||
nextPath: '/',
|
||||
shouldAlert: true,
|
||||
};
|
||||
}
|
||||
|
||||
return resolveRuntimeNotFoundRecoveryAction(pathname);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user