ce0765b298
H5 新增 navigation.canGoBack hook 并在直达二级页时补齐返回锚点 路由导航保留完整原生宿主上下文并标记应用历史状态 补齐 HostBridge 返回栈消费测试、门禁和文档
232 lines
8.1 KiB
TypeScript
232 lines
8.1 KiB
TypeScript
import type { SelectionStage } from '../components/platform-entry';
|
|
import { HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS } from '../../packages/shared/src/contracts/hostBridge';
|
|
import {
|
|
buildCreationUrlSearchFromParams,
|
|
isCreationRestorePath,
|
|
isSameCreationFlowPath,
|
|
} from '../services/creationUrlState';
|
|
|
|
export type RuntimePageRoute = 'rpg-character-select' | 'rpg-adventure';
|
|
|
|
export const PUBLIC_WORK_QUERY_PARAM = 'work';
|
|
|
|
const STAGE_ROUTE_ENTRIES = [
|
|
['platform', '/'],
|
|
['profile-feedback', '/profile/feedback'],
|
|
['work-detail', '/works/detail'],
|
|
['detail', '/worlds/detail'],
|
|
['agent-workspace', '/creation/rpg'],
|
|
['big-fish-agent-workspace', '/creation/big-fish'],
|
|
['custom-world-generating', '/creation/rpg/generating'],
|
|
['custom-world-result', '/creation/rpg/result'],
|
|
['big-fish-generating', '/creation/big-fish/generating'],
|
|
['big-fish-result', '/creation/big-fish/result'],
|
|
['big-fish-runtime', '/runtime/big-fish'],
|
|
['match3d-agent-workspace', '/creation/match3d'],
|
|
['match3d-generating', '/creation/match3d/generating'],
|
|
['match3d-result', '/creation/match3d/result'],
|
|
['match3d-runtime', '/runtime/match3d'],
|
|
['square-hole-agent-workspace', '/creation/square-hole'],
|
|
['square-hole-generating', '/creation/square-hole/generating'],
|
|
['square-hole-result', '/creation/square-hole/result'],
|
|
['square-hole-runtime', '/runtime/square-hole'],
|
|
['jump-hop-workspace', '/creation/jump-hop'],
|
|
['jump-hop-generating', '/creation/jump-hop/generating'],
|
|
['jump-hop-result', '/creation/jump-hop/result'],
|
|
['jump-hop-gallery-detail', '/gallery/jump-hop/detail'],
|
|
['jump-hop-runtime', '/runtime/jump-hop'],
|
|
['puzzle-clear-workspace', '/creation/puzzle-clear'],
|
|
['puzzle-clear-generating', '/creation/puzzle-clear/generating'],
|
|
['puzzle-clear-result', '/creation/puzzle-clear/result'],
|
|
['puzzle-clear-runtime', '/runtime/puzzle-clear'],
|
|
['bark-battle-workspace', '/creation/bark-battle'],
|
|
['bark-battle-generating', '/creation/bark-battle/generating'],
|
|
['bark-battle-result', '/creation/bark-battle/result'],
|
|
['bark-battle-runtime', '/runtime/bark-battle'],
|
|
['wooden-fish-workspace', '/creation/wooden-fish'],
|
|
['wooden-fish-generating', '/creation/wooden-fish/generating'],
|
|
['wooden-fish-result', '/creation/wooden-fish/result'],
|
|
['wooden-fish-runtime', '/runtime/wooden-fish'],
|
|
['creative-agent-workspace', '/creation/creative-agent'],
|
|
['visual-novel-agent-workspace', '/creation/visual-novel'],
|
|
['visual-novel-generating', '/creation/visual-novel/generating'],
|
|
['visual-novel-result', '/creation/visual-novel/result'],
|
|
['visual-novel-gallery-detail', '/gallery/visual-novel/detail'],
|
|
['visual-novel-runtime', '/runtime/visual-novel'],
|
|
['baby-object-match-workspace', '/creation/baby-object-match'],
|
|
['baby-object-match-generating', '/creation/baby-object-match/generating'],
|
|
['baby-object-match-result', '/creation/baby-object-match/result'],
|
|
['baby-object-match-runtime', '/runtime/baby-object-match'],
|
|
['baby-love-drawing-runtime', '/runtime/baby-love-drawing'],
|
|
['puzzle-agent-workspace', '/creation/puzzle'],
|
|
['puzzle-generating', '/creation/puzzle/generating'],
|
|
['puzzle-result', '/creation/puzzle/result'],
|
|
['puzzle-gallery-detail', '/gallery/puzzle/detail'],
|
|
['puzzle-runtime', '/runtime/puzzle'],
|
|
] as const satisfies readonly (readonly [SelectionStage, string])[];
|
|
|
|
export const APP_STAGE_ROUTES: Record<SelectionStage, string> =
|
|
Object.fromEntries(STAGE_ROUTE_ENTRIES) as Record<SelectionStage, string>;
|
|
|
|
export const APP_RUNTIME_ROUTES: Record<RuntimePageRoute, string> = {
|
|
'rpg-character-select': '/runtime/rpg/characters',
|
|
'rpg-adventure': '/runtime/rpg/adventure',
|
|
};
|
|
|
|
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';
|
|
|
|
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 resolvePathForSelectionStage(stage: SelectionStage) {
|
|
return APP_STAGE_ROUTES[stage] ?? APP_STAGE_ROUTES.platform;
|
|
}
|
|
|
|
export function readPublicWorkCodeFromLocationSearch(search: string) {
|
|
const params = new URLSearchParams(search);
|
|
return params.get(PUBLIC_WORK_QUERY_PARAM)?.trim() || null;
|
|
}
|
|
|
|
export function buildPublicWorkDetailPath(publicWorkCode: string) {
|
|
return buildPublicWorkStagePath('work-detail', publicWorkCode);
|
|
}
|
|
|
|
export function buildPublicWorkStagePath(
|
|
stage: SelectionStage,
|
|
publicWorkCode: string,
|
|
) {
|
|
const code = publicWorkCode.trim();
|
|
const stagePath = resolvePathForSelectionStage(stage);
|
|
if (!code) {
|
|
return stagePath;
|
|
}
|
|
|
|
const params = new URLSearchParams();
|
|
params.set(PUBLIC_WORK_QUERY_PARAM, code);
|
|
return `${stagePath}?${params.toString()}`;
|
|
}
|
|
|
|
export function buildPublicWorkDetailUrl(
|
|
publicWorkCode: string,
|
|
origin = window.location.origin,
|
|
) {
|
|
return new URL(buildPublicWorkDetailPath(publicWorkCode), origin).href;
|
|
}
|
|
|
|
export function isKnownMainAppPagePath(pathname: string) {
|
|
const normalizedPath = normalizeAppPath(pathname);
|
|
const runtimePaths: readonly string[] = Object.values(APP_RUNTIME_ROUTES);
|
|
return (
|
|
ROUTE_STAGE_BY_PATH.has(normalizedPath) ||
|
|
runtimePaths.includes(normalizedPath)
|
|
);
|
|
}
|
|
|
|
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.pathname,
|
|
normalizedPath,
|
|
window.location.search,
|
|
);
|
|
const nextRelativeUrl = `${normalizedPath}${nextSearch}`;
|
|
const currentRelativeUrl = `${normalizeAppPath(window.location.pathname)}${window.location.search}`;
|
|
if (currentRelativeUrl === nextRelativeUrl) {
|
|
return;
|
|
}
|
|
|
|
// 页面阶段变化是用户可感知导航,写入 history 以支持前进后退。
|
|
if (mode === 'replace') {
|
|
window.history.replaceState(buildAppHistoryState(), '', nextRelativeUrl);
|
|
return;
|
|
}
|
|
|
|
window.history.pushState(buildAppHistoryState(), '', nextRelativeUrl);
|
|
}
|
|
|
|
export function isAppHistoryState(state: unknown) {
|
|
return Boolean(
|
|
state &&
|
|
typeof state === 'object' &&
|
|
(state as Record<string, unknown>)[APP_HISTORY_STATE_KEY] === true,
|
|
);
|
|
}
|
|
|
|
function buildAppHistoryState() {
|
|
const currentState =
|
|
window.history.state && typeof window.history.state === 'object'
|
|
? window.history.state
|
|
: {};
|
|
return {
|
|
...currentState,
|
|
[APP_HISTORY_STATE_KEY]: true,
|
|
};
|
|
}
|
|
|
|
function buildPreservedAppSearch(
|
|
explicitNextSearch: string,
|
|
currentPathname: string,
|
|
normalizedPath: string,
|
|
search: string,
|
|
) {
|
|
const preservedParams = new URLSearchParams(explicitNextSearch);
|
|
|
|
if (
|
|
!explicitNextSearch &&
|
|
isCreationRestorePath(normalizedPath) &&
|
|
isSameCreationFlowPath(currentPathname, normalizedPath)
|
|
) {
|
|
const creationParams = new URLSearchParams(
|
|
buildCreationUrlSearchFromParams(search),
|
|
);
|
|
creationParams.forEach((value, key) => {
|
|
preservedParams.set(key, value);
|
|
});
|
|
}
|
|
|
|
const currentParams = new URLSearchParams(search);
|
|
// 中文注释:小程序 WebView 依赖这些宿主上下文判断登录和充值通道,不能被前端阶段导航清掉。
|
|
APP_RUNTIME_CONTEXT_QUERY_KEYS.forEach((key) => {
|
|
if (preservedParams.has(key)) {
|
|
return;
|
|
}
|
|
|
|
const value = currentParams.get(key)?.trim();
|
|
if (value) {
|
|
preservedParams.set(key, value);
|
|
}
|
|
});
|
|
|
|
const queryString = preservedParams.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|