fix public work detail not found recovery
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-11 19:52:25 +08:00
parent 7cea41c911
commit 54968701f0
7 changed files with 182 additions and 8 deletions

View File

@@ -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"

View File

@@ -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 = {

View File

@@ -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,
});
});

View File

@@ -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);
}