16
src/routing/RouteLoadingScreen.tsx
Normal file
16
src/routing/RouteLoadingScreen.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export function RouteLoadingScreen({
|
||||
eyebrow,
|
||||
text,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
text: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#0d1016] px-6 text-zinc-200">
|
||||
<div className="text-center">
|
||||
<div className="text-sm tracking-[0.26em] text-zinc-500">{eyebrow}</div>
|
||||
<div className="mt-3 text-lg font-semibold text-white">{text}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/routing/appRoutes.test.ts
Normal file
38
src/routing/appRoutes.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { matchAppRoute } from './appRoutes';
|
||||
|
||||
describe('matchAppRoute', () => {
|
||||
it('routes the main app by default', () => {
|
||||
expect(matchAppRoute('/')).toEqual({
|
||||
kind: 'game',
|
||||
});
|
||||
});
|
||||
|
||||
it('routes item editor paths to the preset editor items tab', () => {
|
||||
expect(matchAppRoute('/item-editor/tools')).toEqual({
|
||||
kind: 'preset-editor',
|
||||
initialTab: 'items',
|
||||
});
|
||||
});
|
||||
|
||||
it('routes behavior editor paths to the functions tab', () => {
|
||||
expect(matchAppRoute('/behavior-editor')).toEqual({
|
||||
kind: 'preset-editor',
|
||||
initialTab: 'functions',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts nested preset editor paths with trailing slashes', () => {
|
||||
expect(matchAppRoute('/NPC-EDITOR/profiles/')).toEqual({
|
||||
kind: 'preset-editor',
|
||||
initialTab: 'npcs',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not treat unrelated prefixes as preset editor routes', () => {
|
||||
expect(matchAppRoute('/npc-editorial')).toEqual({
|
||||
kind: 'game',
|
||||
});
|
||||
});
|
||||
});
|
||||
122
src/routing/appRoutes.tsx
Normal file
122
src/routing/appRoutes.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
|
||||
import { type ComponentType, lazy, type LazyExoticComponent } from 'react';
|
||||
|
||||
import type { PresetEditorTab } from '../components/PresetEditor';
|
||||
|
||||
type AppRouteComponent = LazyExoticComponent<
|
||||
ComponentType<Record<string, unknown>>
|
||||
>;
|
||||
|
||||
export type AppRouteMatch =
|
||||
| {
|
||||
kind: 'game';
|
||||
}
|
||||
| {
|
||||
kind: 'preset-editor';
|
||||
initialTab: PresetEditorTab;
|
||||
};
|
||||
|
||||
export type ResolvedAppRoute = {
|
||||
kind: AppRouteMatch['kind'];
|
||||
loadingEyebrow: string;
|
||||
loadingText: string;
|
||||
Component: AppRouteComponent;
|
||||
componentProps?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const GameApp = lazy(() => import('../App')) as AppRouteComponent;
|
||||
const PresetEditorApp = lazy(async () => {
|
||||
const module = await import('../components/PresetEditor');
|
||||
|
||||
return {
|
||||
default: module.PresetEditor,
|
||||
};
|
||||
}) as AppRouteComponent;
|
||||
|
||||
const PRESET_EDITOR_ROUTES: Array<{
|
||||
prefixes: string[];
|
||||
initialTab: PresetEditorTab;
|
||||
}> = [
|
||||
{
|
||||
prefixes: ['/character-asset-studio', '/asset-studio'],
|
||||
initialTab: 'assets',
|
||||
},
|
||||
{
|
||||
prefixes: ['/function-editor', '/behavior-editor'],
|
||||
initialTab: 'functions',
|
||||
},
|
||||
{
|
||||
prefixes: ['/item-editor'],
|
||||
initialTab: 'items',
|
||||
},
|
||||
{
|
||||
prefixes: ['/npc-editor'],
|
||||
initialTab: 'npcs',
|
||||
},
|
||||
{
|
||||
prefixes: ['/preset-editor'],
|
||||
initialTab: 'characters',
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeRoutePath(pathname: string) {
|
||||
const trimmedPathname = pathname.trim().toLowerCase();
|
||||
|
||||
if (!trimmedPathname || trimmedPathname === '/') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return trimmedPathname.replace(/\/+$/u, '');
|
||||
}
|
||||
|
||||
function matchesRoutePrefix(pathname: string, prefix: string) {
|
||||
const normalizedPrefix = normalizeRoutePath(prefix);
|
||||
|
||||
return (
|
||||
pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`)
|
||||
);
|
||||
}
|
||||
|
||||
export function matchAppRoute(pathname: string): AppRouteMatch {
|
||||
const normalizedPathname = normalizeRoutePath(pathname);
|
||||
const presetRoute = PRESET_EDITOR_ROUTES.find((route) =>
|
||||
route.prefixes.some((prefix) =>
|
||||
matchesRoutePrefix(normalizedPathname, prefix),
|
||||
),
|
||||
);
|
||||
|
||||
if (presetRoute) {
|
||||
return {
|
||||
kind: 'preset-editor',
|
||||
initialTab: presetRoute.initialTab,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'game',
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAppRoute(pathname: string): ResolvedAppRoute {
|
||||
const matchedRoute = matchAppRoute(pathname);
|
||||
|
||||
if (matchedRoute.kind === 'preset-editor') {
|
||||
return {
|
||||
kind: matchedRoute.kind,
|
||||
loadingEyebrow: '正在载入编辑器',
|
||||
loadingText: '正在载入编辑器...',
|
||||
Component: PresetEditorApp,
|
||||
componentProps: {
|
||||
initialTab: matchedRoute.initialTab,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'game',
|
||||
loadingEyebrow: '正在载入游戏',
|
||||
loadingText: '正在载入冒险...',
|
||||
Component: GameApp,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user