恢复新版创作主页与平台侧栏

恢复 /creation 新版创作工具主页及创作、项目公共侧栏。

将 creation-home 重新纳入 Vite、TypeScript、ESLint 与 Vitest 活动编译链。

恢复 /creation 的 Nginx 与 Pingora SPA 白名单并保留旧子路径下线边界。

补充路由、标题、CSS 过滤和网关 smoke 回归验证。
This commit is contained in:
2026-07-18 15:41:55 +08:00
parent f625fe8e8b
commit c56da8dc26
18 changed files with 275 additions and 86 deletions
@@ -1,5 +1,18 @@
import { FolderKanban, LogIn, Settings, UserRound } from 'lucide-react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import {
FolderKanban,
LogIn,
Palette,
Settings,
UserRound,
} from 'lucide-react';
import {
type ComponentType,
lazy,
Suspense,
useCallback,
useEffect,
useState,
} from 'react';
import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry';
import type { ProfileDashboardSummary } from '../../../packages/shared/src/contracts/runtime';
@@ -25,11 +38,87 @@ const ImageCanvasEditorView = lazy(async () => {
return { default: module.ImageCanvasEditorView };
});
const CreationLandingView = lazy(async () => {
const module = await import('../creation-home/CreationLandingView');
return { default: module.CreationLandingView };
});
const ProjectGalleryView = lazy(async () => {
const module = await import('../project/ProjectGalleryView');
return { default: module.ProjectGalleryView };
});
type ActiveRailButtonProps = {
active: boolean;
emphasized?: boolean;
icon: ComponentType<{ className?: string }>;
iconSrc: string;
label: string;
onClick: () => void;
};
function ActiveRailButton({
active,
emphasized = false,
icon: Icon,
iconSrc,
label,
onClick,
}: ActiveRailButtonProps) {
return (
<button
type="button"
aria-current={active ? 'page' : undefined}
aria-label={label}
className={`platform-desktop-rail__button ${emphasized ? 'platform-desktop-rail__button--primary' : ''} ${active ? 'platform-desktop-rail__button--active' : ''}`}
onClick={onClick}
>
<span className="platform-desktop-rail__icon-shell">
<img
src={iconSrc}
alt=""
aria-hidden="true"
draggable={false}
className="creation-home-nav-art"
/>
<Icon
className="platform-desktop-rail__icon h-[1.1rem] w-[1.1rem]"
aria-hidden="true"
/>
</span>
<span className="platform-desktop-rail__label text-[11px] font-semibold">
{label}
</span>
</button>
);
}
function ActivePlatformBrand() {
return (
<span
className="creation-home-brand-lockup shrink-0"
role="img"
aria-label="陶泥儿 GENARRATIVE"
>
<span className="platform-brand-logo">
<img
src="/branding/taonier-product-ip.png"
alt=""
aria-hidden="true"
draggable={false}
className="platform-brand-logo__image"
/>
<span className="platform-brand-logo__copy">
<span className="platform-brand-logo__title">
<span className="platform-brand-logo__title-suffix"></span>
</span>
<span className="platform-brand-logo__subtitle">GENARRATIVE</span>
</span>
</span>
</span>
);
}
function LoadingPanel({ label }: { label: string }) {
return (
<div className="flex h-full min-h-[12rem] items-center justify-center text-sm font-semibold text-[var(--platform-text-soft)]">
@@ -76,18 +165,26 @@ export function PlatformEntryFlowShellImpl({
currentUser: authUi?.user,
});
const openCreation = useCallback(() => {
pushAppHistoryPath('/creation');
setSelectionStage('creation-home', { path: '/creation' });
}, [setSelectionStage]);
const openProjects = useCallback(() => {
pushAppHistoryPath('/project');
setSelectionStage('project', { path: '/project' });
}, [setSelectionStage]);
const openEditorProject = useCallback(
(projectId: string, options?: { guide?: boolean }) => {
(projectId: string, options?: { guide?: boolean; tool?: string }) => {
const params = new URLSearchParams();
params.set('projectid', projectId);
if (options?.guide) {
params.set('guide', 'toolbar');
}
if (options?.tool) {
params.set('tool', options.tool);
}
const path = `/editor/canvas?${params.toString()}`;
pushAppHistoryPath(path);
setSelectionStage('image-editor', { path });
@@ -118,76 +215,121 @@ export function PlatformEntryFlowShellImpl({
profileCenter.rechargeCenter?.walletBalance ??
dashboard?.walletBalance ??
null;
const isCreationStage =
selectionStage === 'platform' || selectionStage === 'creation-home';
return (
<>
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
<header className="flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-4 sm:px-6">
<button
type="button"
className="flex min-w-0 items-center gap-3 text-left"
onClick={openProjects}
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--platform-accent-strong)] text-white">
<FolderKanban className="h-5 w-5" aria-hidden="true" />
</span>
<span className="truncate text-base font-black text-[var(--platform-text-strong)]">
</span>
</button>
<div className="flex shrink-0 items-center gap-2">
{isAuthenticated ? (
<PlatformMudPointWalletEntry
variant="desktop"
balance={balance}
breakdown={
profileCenter.rechargeCenter?.mudPointBalance ?? null
}
isLoading={
isLoadingDashboard || profileCenter.isLoadingRechargeCenter
}
error={profileCenter.rechargeError}
onRequestDetails={profileCenter.loadRechargeCenter}
onRecharge={() => {
profileCenter.setIsRechargeOpen(true);
profileCenter.loadRechargeCenter();
}}
onOpenLedger={profileCenter.openWalletLedgerPanel}
/>
) : (
<PlatformActionButton
size="xs"
onClick={() => authUi?.openLoginModal()}
<div
className={`flex h-full min-h-0 min-w-0 flex-col overflow-hidden ${isCreationStage ? 'creation-home-stage--landing' : ''}`}
>
<div className="platform-desktop-shell platform-desktop-shell--workbench flex h-full min-h-0 flex-col">
<div className="platform-desktop-layout flex min-h-0 flex-1">
<aside className="platform-desktop-leftbar hidden shrink-0 flex-col lg:flex">
<ActivePlatformBrand />
<nav
className="platform-desktop-rail flex shrink-0 flex-col gap-3"
aria-label="平台导航"
>
<LogIn className="h-4 w-4" aria-hidden="true" />
</PlatformActionButton>
)}
<PlatformIconButton
label={isAuthenticated ? '账号' : '登录'}
icon={<UserRound className="h-5 w-5" aria-hidden="true" />}
onClick={() => {
if (isAuthenticated) {
authUi?.openAccountModal();
return;
}
authUi?.openLoginModal();
}}
/>
<PlatformIconButton
label="设置"
icon={<Settings className="h-5 w-5" aria-hidden="true" />}
onClick={() => authUi?.openSettingsModal()}
/>
</div>
</header>
<ActiveRailButton
active={isCreationStage}
emphasized={isCreationStage}
icon={Palette}
iconSrc="/creation-home/nav-create.png"
label="创作"
onClick={openCreation}
/>
<ActiveRailButton
active={selectionStage === 'project'}
icon={FolderKanban}
iconSrc="/creation-home/nav-projects.png"
label="项目"
onClick={openProjects}
/>
</nav>
</aside>
<main className="min-h-0 min-w-0 flex-1 overflow-auto px-3 py-4 sm:px-6 sm:py-6">
<Suspense fallback={<LoadingPanel label="正在加载项目..." />}>
<ProjectGalleryView onOpenProject={openEditorProject} />
</Suspense>
</main>
<div className="platform-desktop-main flex min-w-0 flex-1 flex-col">
<header className="platform-desktop-topbar flex min-h-16 shrink-0 items-center justify-between gap-3 px-4 sm:px-6">
<div className="min-w-0 lg:hidden">
<ActivePlatformBrand />
</div>
<span className="hidden truncate text-base font-black text-[var(--platform-text-strong)] lg:block">
{isCreationStage ? '创作工具' : '项目'}
</span>
<div className="platform-desktop-topbar__actions flex shrink-0 items-center gap-2">
{isAuthenticated ? (
<PlatformMudPointWalletEntry
variant="desktop"
balance={balance}
breakdown={
profileCenter.rechargeCenter?.mudPointBalance ?? null
}
isLoading={
isLoadingDashboard ||
profileCenter.isLoadingRechargeCenter
}
error={profileCenter.rechargeError}
onRequestDetails={profileCenter.loadRechargeCenter}
onRecharge={() => {
profileCenter.setIsRechargeOpen(true);
profileCenter.loadRechargeCenter();
}}
onOpenLedger={profileCenter.openWalletLedgerPanel}
/>
) : (
<PlatformActionButton
size="xs"
onClick={() => authUi?.openLoginModal()}
>
<LogIn className="h-4 w-4" aria-hidden="true" />
</PlatformActionButton>
)}
<PlatformIconButton
label={isAuthenticated ? '账号' : '登录'}
icon={
<UserRound className="h-5 w-5" aria-hidden="true" />
}
onClick={() => {
if (isAuthenticated) {
authUi?.openAccountModal();
return;
}
authUi?.openLoginModal();
}}
/>
<PlatformIconButton
label="设置"
icon={<Settings className="h-5 w-5" aria-hidden="true" />}
onClick={() => authUi?.openSettingsModal()}
/>
</div>
</header>
<main
id={isCreationStage ? 'platform-tab-panel-create' : undefined}
className={`platform-tab-panel min-h-0 min-w-0 flex-1 overflow-auto ${isCreationStage ? '' : 'px-3 py-4 sm:px-6 sm:py-6'}`}
>
{isCreationStage ? (
<Suspense
fallback={<LoadingPanel label="正在加载创作主页..." />}
>
<CreationLandingView
onOpenProject={openEditorProject}
onOpenProjects={openProjects}
/>
</Suspense>
) : (
<Suspense fallback={<LoadingPanel label="正在加载项目..." />}>
<ProjectGalleryView onOpenProject={openEditorProject} />
</Suspense>
)}
</main>
</div>
</div>
</div>
</div>
{profileCenter.isRechargeOpen ? (
@@ -8,7 +8,7 @@ export type { PlatformEntryFlowShellProps, SelectionStage };
/**
* 平台入口通用壳层。
* 平台运行入口承载项目、编辑器与账户公共能力。
* 平台运行入口承载新创作主页、项目、编辑器与账户公共能力。
*/
export function PlatformEntryFlowShell(props: PlatformEntryFlowShellProps) {
return <PlatformEntryFlowShellImpl {...props} />;
@@ -1,4 +1,8 @@
export type SelectionStage = 'platform' | 'project' | 'image-editor';
export type SelectionStage =
| 'platform'
| 'creation-home'
| 'project'
| 'image-editor';
export type PlatformEntryFlowShellProps = {
selectionStage: SelectionStage;
+4 -2
View File
@@ -14,17 +14,19 @@ import {
} from './activeAppPageRoutes';
describe('appPageRoutes', () => {
it('only resolves public platform, project, and editor stages', () => {
it('resolves the active creation, project, and editor stages', () => {
expect(resolveSelectionStageFromPath('/')).toBe('platform');
expect(resolveSelectionStageFromPath('/creation')).toBe('creation-home');
expect(resolveSelectionStageFromPath('/project')).toBe('project');
expect(resolveSelectionStageFromPath('/EDITOR/CANVAS/')).toBe(
'image-editor',
);
expect(resolveSelectionStageFromPath('/creation')).toBe('platform');
expect(resolveSelectionStageFromPath('/creation/rpg')).toBe('platform');
expect(resolveSelectionStageFromPath('/runtime/puzzle')).toBe('platform');
expect(isKnownMainAppPagePath('/creation')).toBe(true);
expect(isKnownMainAppPagePath('/creation/rpg')).toBe(false);
expect(isKnownMainAppPagePath('/runtime/puzzle')).toBe(false);
expect(resolvePathForSelectionStage('creation-home')).toBe('/creation');
expect(resolvePathForSelectionStage('project')).toBe('/project');
});
+1
View File
@@ -5,6 +5,7 @@ export const EDITOR_CANVAS_PROJECT_ID_QUERY_PARAM = 'projectid';
const STAGE_ROUTE_ENTRIES = [
['platform', '/'],
['creation-home', '/creation'],
['project', '/project'],
['image-editor', '/editor/canvas'],
] as const satisfies readonly (readonly [SelectionStage, string])[];
+3
View File
@@ -24,6 +24,9 @@ afterEach(() => {
describe('activeAppTitle', () => {
test('maps only active platform stages', () => {
expect(resolveAppTitleForSelectionStage('platform')).toBe(APP_HOME_TITLE);
expect(resolveAppTitleForSelectionStage('creation-home')).toBe(
APP_HOME_TITLE,
);
expect(resolveAppTitleForSelectionStage('project')).toBe('项目 - 陶泥儿');
expect(resolveAppTitleForSelectionStage('image-editor')).toBe(
'美术编辑器 - 陶泥儿',
+1
View File
@@ -7,6 +7,7 @@ export const APP_HOME_TITLE =
const APP_TITLE_BY_SELECTION_STAGE: Record<SelectionStage, string> = {
platform: APP_HOME_TITLE,
'creation-home': APP_HOME_TITLE,
project: '项目 - 陶泥儿',
'image-editor': '美术编辑器 - 陶泥儿',
};