e976a69f9f
- GameDetailPage 左上角「返回游戏广场」简化为「返回」,不再写死跳广场 - 详情/游玩页返回优先 window.history.back(),由 popstate 还原真实来源页(我的游戏/广场) - activeAppPageRoutes 给应用历史条目补深度标记并新增 hasAppHistoryBackEntry() - 深链直开等无可回退条目时用 replaceAppHistoryPath 兜底回广场或详情 - 新增 activeAppPageRoutes 深度语义用例与 PlatformEntryActiveFlowShell「游戏详情返回」两条用例 - 同步里程碑文档 2026-09-30 与 pitfalls 同日条目
160 lines
5.0 KiB
TypeScript
160 lines
5.0 KiB
TypeScript
import { HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS } from '../../packages/shared/src/contracts/hostBridge';
|
|
import type { SelectionStage } from '../components/platform-entry/platformEntryActiveTypes';
|
|
|
|
export const EDITOR_CANVAS_PROJECT_ID_QUERY_PARAM = 'projectid';
|
|
|
|
const STAGE_ROUTE_ENTRIES = [
|
|
['platform', '/'],
|
|
['creation-home', '/creation'],
|
|
['project', '/project'],
|
|
['profile', '/profile'],
|
|
['image-editor', '/editor/canvas'],
|
|
['games', '/games'],
|
|
['game-detail', '/games/detail'],
|
|
['game-play', '/games/play'],
|
|
['game-mine', '/games/mine'],
|
|
['game-publish', '/games/publish'],
|
|
] as const satisfies readonly (readonly [SelectionStage, string])[];
|
|
|
|
export const APP_STAGE_ROUTES: Record<SelectionStage, string> =
|
|
Object.fromEntries(STAGE_ROUTE_ENTRIES) as Record<SelectionStage, string>;
|
|
|
|
const ROUTE_STAGE_BY_PATH = new Map(
|
|
STAGE_ROUTE_ENTRIES.map(([stage, path]) => [path, stage] as const),
|
|
) as Map<string, SelectionStage>;
|
|
|
|
const APP_RUNTIME_CONTEXT_QUERY_KEYS =
|
|
HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS;
|
|
export const APP_HISTORY_STATE_KEY = '__genarrativeAppHistoryEntry';
|
|
const APP_HISTORY_STATE_DEPTH_KEY = '__genarrativeAppHistoryDepth';
|
|
|
|
export function normalizeAppPath(pathname: string) {
|
|
const trimmedPathname = pathname.trim().toLowerCase();
|
|
if (!trimmedPathname || trimmedPathname === '/') {
|
|
return '/';
|
|
}
|
|
return trimmedPathname.replace(/\/+$/u, '');
|
|
}
|
|
|
|
export function resolveSelectionStageFromPath(
|
|
pathname: string,
|
|
): SelectionStage {
|
|
return ROUTE_STAGE_BY_PATH.get(normalizeAppPath(pathname)) ?? 'platform';
|
|
}
|
|
|
|
export function resolveInitialSelectionStageFromPath(
|
|
pathname: string,
|
|
_isDesktopLayout: boolean,
|
|
): SelectionStage {
|
|
return resolveSelectionStageFromPath(pathname);
|
|
}
|
|
|
|
export function resolvePathForSelectionStage(stage: SelectionStage) {
|
|
return APP_STAGE_ROUTES[stage];
|
|
}
|
|
|
|
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 isKnownMainAppPagePath(pathname: string) {
|
|
return ROUTE_STAGE_BY_PATH.has(normalizeAppPath(pathname));
|
|
}
|
|
|
|
export function pushAppHistoryPath(path: string) {
|
|
writeAppHistoryPath(path, 'push');
|
|
}
|
|
|
|
export function replaceAppHistoryPath(path: string) {
|
|
writeAppHistoryPath(path, 'replace');
|
|
}
|
|
|
|
function writeAppHistoryPath(path: string, mode: 'push' | 'replace') {
|
|
const nextUrl = new URL(path, window.location.origin);
|
|
const normalizedPath = normalizeAppPath(nextUrl.pathname);
|
|
const nextSearch = buildPreservedAppSearch(
|
|
nextUrl.search,
|
|
window.location.search,
|
|
);
|
|
const nextRelativeUrl = `${normalizedPath}${nextSearch}`;
|
|
const currentRelativeUrl = `${normalizeAppPath(window.location.pathname)}${window.location.search}`;
|
|
if (currentRelativeUrl === nextRelativeUrl) {
|
|
return;
|
|
}
|
|
|
|
const state = buildAppHistoryState(mode);
|
|
if (mode === 'replace') {
|
|
window.history.replaceState(state, '', nextRelativeUrl);
|
|
return;
|
|
}
|
|
window.history.pushState(state, '', nextRelativeUrl);
|
|
}
|
|
|
|
export function isAppHistoryState(state: unknown) {
|
|
return Boolean(
|
|
state &&
|
|
typeof state === 'object' &&
|
|
(state as Record<string, unknown>)[APP_HISTORY_STATE_KEY] === true,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 当前历史条目是不是应用内导航写入的、且存在可回退的上一页。
|
|
* 直接打开深链时浏览器条目不是应用写入的,此时返回需要走兜底路径,
|
|
* 否则 `history.back()` 会直接退出应用。
|
|
*/
|
|
export function hasAppHistoryBackEntry() {
|
|
const state = window.history.state;
|
|
if (!isAppHistoryState(state)) {
|
|
return false;
|
|
}
|
|
return readAppHistoryDepth(state as Record<string, unknown>) > 0;
|
|
}
|
|
|
|
function readAppHistoryDepth(state: Record<string, unknown>) {
|
|
const depth = state[APP_HISTORY_STATE_DEPTH_KEY];
|
|
return typeof depth === 'number' && Number.isFinite(depth) ? depth : 0;
|
|
}
|
|
|
|
function buildAppHistoryState(mode: 'push' | 'replace') {
|
|
const currentState =
|
|
window.history.state && typeof window.history.state === 'object'
|
|
? window.history.state
|
|
: {};
|
|
const currentDepth = readAppHistoryDepth(
|
|
currentState as Record<string, unknown>,
|
|
);
|
|
return {
|
|
...currentState,
|
|
[APP_HISTORY_STATE_KEY]: true,
|
|
[APP_HISTORY_STATE_DEPTH_KEY]:
|
|
mode === 'push' ? currentDepth + 1 : currentDepth,
|
|
};
|
|
}
|
|
|
|
function buildPreservedAppSearch(
|
|
explicitSearch: string,
|
|
currentSearch: string,
|
|
) {
|
|
const params = new URLSearchParams(explicitSearch);
|
|
const currentParams = new URLSearchParams(currentSearch);
|
|
APP_RUNTIME_CONTEXT_QUERY_KEYS.forEach((key) => {
|
|
if (!params.has(key) && currentParams.has(key)) {
|
|
params.set(key, currentParams.get(key) ?? '');
|
|
}
|
|
});
|
|
const serialized = params.toString();
|
|
return serialized ? `?${serialized}` : '';
|
|
}
|