修正画布路由与刷新加载

缺少项目 ID 的画布直达链接会回到创作页

路由级黑底加载只保留给移动端推荐页

补充画布直达和推荐加载开关回归测试
This commit is contained in:
2026-06-25 23:30:49 +08:00
parent 24f42061c5
commit 00b2720237
7 changed files with 176 additions and 14 deletions
+55
View File
@@ -294,6 +294,61 @@ describe('App navigation history', () => {
);
});
test('缺少 projectid 的项目画布直达会替换回创作页', () => {
window.history.replaceState(null, '', '/editor/canvas');
const replaceStateSpy = vi.spyOn(window.history, 'replaceState');
renderApp();
expect(screen.getByTestId('selection-stage').textContent).toBe(
'creation-home',
);
expect(window.location.pathname).toBe('/creation');
expect(window.location.search).toBe('');
expect(replaceStateSpy).toHaveBeenCalledWith(
{ [APP_HISTORY_STATE_KEY]: true },
'',
'/creation',
);
replaceStateSpy.mockRestore();
});
test('带 projectid 的项目画布直达保持编辑器阶段', () => {
mockMatchMedia(false);
window.history.replaceState(
null,
'',
'/editor/canvas?projectid=project-from-url',
);
renderApp();
expect(screen.getByTestId('selection-stage').textContent).toBe(
'image-editor',
);
expect(window.location.pathname).toBe('/editor/canvas');
expect(window.location.search).toBe('?projectid=project-from-url');
});
test('历史回到缺少 projectid 的项目画布时会替换回创作页', async () => {
mockMatchMedia(false);
window.history.replaceState(null, '', '/creation');
renderApp();
window.history.pushState(null, '', '/editor/canvas');
await act(async () => {
window.dispatchEvent(new PopStateEvent('popstate'));
});
expect(screen.getByTestId('selection-stage').textContent).toBe(
'creation-home',
);
expect(window.location.pathname).toBe('/creation');
expect(window.location.search).toBe('');
});
test('项目画布导航只写入一次带 projectid 的历史记录', async () => {
mockMatchMedia(true);
window.history.replaceState(null, '', '/creation');
+32 -5
View File
@@ -25,6 +25,7 @@ import {
readPublicWorkCodeFromLocationSearch,
resolveInitialSelectionStageFromPath,
resolvePathForSelectionStage,
shouldRedirectEditorCanvasWithoutProject,
} from './routing/appPageRoutes';
import type { RpgRuntimeAppIntent } from './RpgRuntimeApp';
import {
@@ -62,6 +63,23 @@ function isRpgRuntimeRoute(pathname: string) {
);
}
function resolveInitialAppSelectionStage() {
if (
shouldRedirectEditorCanvasWithoutProject(
window.location.pathname,
window.location.search,
)
) {
replaceAppHistoryPath('/creation');
return 'creation-home';
}
return resolveInitialSelectionStageFromPath(
window.location.pathname,
getInitialPlatformDesktopLayout(),
);
}
export default function App() {
const authUi = useAuthUi();
const runtimeIntentTokenRef = useRef(0);
@@ -75,11 +93,8 @@ export default function App() {
const [isRuntimeActive, setIsRuntimeActive] = useState(() =>
isRpgRuntimeRoute(window.location.pathname),
);
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(() =>
resolveInitialSelectionStageFromPath(
window.location.pathname,
getInitialPlatformDesktopLayout(),
),
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(
resolveInitialAppSelectionStage,
);
const [runtimeReturnStage, setRuntimeReturnStage] =
useState<SelectionStage>('platform');
@@ -111,6 +126,18 @@ export default function App() {
window.history.state,
);
if (
shouldRedirectEditorCanvasWithoutProject(
window.location.pathname,
window.location.search,
)
) {
replaceAppHistoryPath('/creation');
setIsRuntimeActive(false);
setRawSelectionStage('creation-home');
return;
}
if (isRpgRuntimeRoute(window.location.pathname)) {
setIsRuntimeActive(true);
return;
+30 -8
View File
@@ -6,9 +6,13 @@ import {StrictMode, Suspense} from 'react';
import {createRoot} from 'react-dom/client';
import {FloatingFeedbackEntry} from './components/common/FloatingFeedbackEntry';
import {getInitialPlatformDesktopLayout} from './components/platform-entry/platformEntryResponsive';
import {stabilizeMobileViewportKeyboardFocus} from './mobileViewportKeyboardFocus';
import {lockMobileViewportZoom} from './mobileViewportZoomLock';
import {resolveAppRoute} from './routing/appRoutes';
import {
resolveAppRoute,
shouldUseRecommendationRouteLoading,
} from './routing/appRoutes';
import {RouteImageReadyGate} from './routing/RouteImageReadyGate';
import {RouteLoadingScreen} from './routing/RouteLoadingScreen';
import {
@@ -39,6 +43,11 @@ function markWechatMiniProgramRuntime() {
const root = window.__tavernRealmsRoot__ ??= createRoot(rootElement);
const RouteComponent = route.Component;
const routeElement = <RouteComponent {...(route.componentProps ?? {})} />;
const shouldGateRouteImages = shouldUseRecommendationRouteLoading(
window.location.pathname,
getInitialPlatformDesktopLayout(),
);
lockMobileViewportZoom();
stabilizeMobileViewportKeyboardFocus();
@@ -47,13 +56,26 @@ void refreshNativeAppHostRuntime();
root.render(
<StrictMode>
<Suspense fallback={<RouteLoadingScreen eyebrow={route.loadingEyebrow} text={route.loadingText} />}>
<RouteImageReadyGate
eyebrow={route.loadingEyebrow}
text={route.loadingText}
>
<RouteComponent {...(route.componentProps ?? {})} />
</RouteImageReadyGate>
<Suspense
fallback={
shouldGateRouteImages ? (
<RouteLoadingScreen
eyebrow={route.loadingEyebrow}
text={route.loadingText}
/>
) : null
}
>
{shouldGateRouteImages ? (
<RouteImageReadyGate
eyebrow={route.loadingEyebrow}
text={route.loadingText}
>
{routeElement}
</RouteImageReadyGate>
) : (
routeElement
)}
</Suspense>
<FloatingFeedbackEntry />
</StrictMode>,
+23
View File
@@ -5,9 +5,11 @@ import { describe, expect, it } from 'vitest';
import {
isAppHistoryState,
pushAppHistoryPath,
readEditorCanvasProjectIdFromLocationSearch,
replaceAppHistoryPath,
resolvePathForSelectionStage,
resolveSelectionStageFromPath,
shouldRedirectEditorCanvasWithoutProject,
} from './appPageRoutes';
describe('appPageRoutes', () => {
@@ -39,6 +41,27 @@ describe('appPageRoutes', () => {
expect(window.location.search).toBe('?projectid=project-from-route-test');
});
it('detects editor canvas routes that are missing project id', () => {
expect(readEditorCanvasProjectIdFromLocationSearch('?projectid=abc')).toBe(
'abc',
);
expect(shouldRedirectEditorCanvasWithoutProject('/editor/canvas', '')).toBe(
true,
);
expect(
shouldRedirectEditorCanvasWithoutProject('/EDITOR/CANVAS/', '?foo=bar'),
).toBe(true);
expect(
shouldRedirectEditorCanvasWithoutProject(
'/editor/canvas',
'?projectid=project-from-route-test',
),
).toBe(false);
expect(shouldRedirectEditorCanvasWithoutProject('/creation', '')).toBe(
false,
);
});
it('resolves the project route', () => {
expect(resolveSelectionStageFromPath('/project')).toBe('project');
expect(resolveSelectionStageFromPath('/PROJECT/')).toBe('project');
+16
View File
@@ -9,6 +9,7 @@ import {
export type RuntimePageRoute = 'rpg-character-select' | 'rpg-adventure';
export const PUBLIC_WORK_QUERY_PARAM = 'work';
export const EDITOR_CANVAS_PROJECT_ID_QUERY_PARAM = 'projectid';
const STAGE_ROUTE_ENTRIES = [
['platform', '/'],
@@ -122,6 +123,21 @@ export function readPublicWorkCodeFromLocationSearch(search: string) {
return params.get(PUBLIC_WORK_QUERY_PARAM)?.trim() || null;
}
export function readEditorCanvasProjectIdFromLocationSearch(search: string) {
const params = new URLSearchParams(search);
return params.get(EDITOR_CANVAS_PROJECT_ID_QUERY_PARAM)?.trim() || null;
}
export function shouldRedirectEditorCanvasWithoutProject(
pathname: string,
search: string,
) {
return (
normalizeAppPath(pathname) === APP_STAGE_ROUTES['image-editor'] &&
!readEditorCanvasProjectIdFromLocationSearch(search)
);
}
export function buildPublicWorkDetailPath(publicWorkCode: string) {
return buildPublicWorkStagePath('work-detail', publicWorkCode);
}
+13 -1
View File
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { matchAppRoute } from './appRoutes';
import { matchAppRoute, shouldUseRecommendationRouteLoading } from './appRoutes';
afterEach(() => {
vi.unstubAllEnvs();
@@ -89,3 +89,15 @@ describe('matchAppRoute', () => {
});
});
});
describe('shouldUseRecommendationRouteLoading', () => {
it('only enables the dark recommendation loading screen for mobile root route', () => {
expect(shouldUseRecommendationRouteLoading('/', false)).toBe(true);
expect(shouldUseRecommendationRouteLoading('/', true)).toBe(false);
expect(shouldUseRecommendationRouteLoading('/creation', false)).toBe(false);
expect(shouldUseRecommendationRouteLoading('/project', false)).toBe(false);
expect(shouldUseRecommendationRouteLoading('/editor/canvas', false)).toBe(
false,
);
});
});
+7
View File
@@ -54,6 +54,13 @@ function normalizeRoutePath(pathname: string) {
return normalizeAppPath(pathname);
}
export function shouldUseRecommendationRouteLoading(
pathname: string,
isDesktopLayout: boolean,
) {
return normalizeRoutePath(pathname) === '/' && !isDesktopLayout;
}
export function matchAppRoute(pathname: string): AppRouteMatch {
const normalizedPath = normalizeRoutePath(pathname);