调整创作主页顶部与左侧栏

更新 /creation 顶部栏和左侧功能栏的参考图规格

接入创作主页 image2 陶土风背景、图标和按钮素材

保留创作主页现有文案、搜索、登录、钱包、导航与项目创建逻辑

补充创作主页改版文档和移动端直达验证
This commit is contained in:
2026-06-19 17:35:40 +08:00
parent 98545d74d1
commit e13f7e99c2
28 changed files with 833 additions and 120 deletions
@@ -1,16 +1,3 @@
import {
Box,
Code2,
Gamepad2,
Image as ImageIcon,
Layers3,
Megaphone,
Music,
Palette,
Plus,
Sparkles,
Wand2,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
@@ -56,7 +43,7 @@ type ShowcaseTabId =
type CreationFeatureItem = {
title: string;
description: string;
icon: typeof Palette;
iconSrc: string;
};
type ShowcaseAssetItem = {
@@ -70,51 +57,57 @@ type ShowcaseAssetItem = {
cost: string;
};
const CREATION_HOME_ASSETS = {
heroCreate: '/creation-home/hero-create.png',
heroCommunity: '/creation-home/hero-community.png',
projectNew: '/creation-home/project-new.png',
} as const;
const CREATION_FEATURES: CreationFeatureItem[] = [
{
title: '游戏视觉规范',
description: '轻松约束多类素材视觉一致性',
icon: Palette,
iconSrc: '/creation-home/feature-visual-spec.png',
},
{
title: '游戏角色',
description: '整套高完成度2D/3D角色素材及动画',
icon: Box,
iconSrc: '/creation-home/feature-character.png',
},
{
title: '游戏UI',
description: '轻松生成高一致性的UI素材',
icon: Layers3,
iconSrc: '/creation-home/feature-ui.png',
},
{
title: '游戏音乐',
description: '轻松制作游戏背景音乐、音效',
icon: Music,
iconSrc: '/creation-home/feature-music.png',
},
{
title: '游戏特效',
description: '低成本生成精品游戏特效',
icon: Sparkles,
iconSrc: '/creation-home/feature-effects.png',
},
{
title: '游戏场景',
description: '轻松生成各类游戏场景素材',
icon: ImageIcon,
iconSrc: '/creation-home/feature-scene.png',
},
{
title: '游戏美宣',
description: '轻松生成游戏上架所需图片和视频',
icon: Megaphone,
iconSrc: '/creation-home/feature-marketing.png',
},
{
title: '游戏功能编码',
description: '自然语言驱动游戏生成',
icon: Code2,
iconSrc: '/creation-home/feature-coding.png',
},
{
title: '一键导出',
description: '支持导出为微抖小游戏、Godot、Unity、UE工程',
icon: Gamepad2,
iconSrc: '/creation-home/feature-export.png',
},
];
@@ -479,18 +472,35 @@ export function CreationLandingView({
<div className="creation-landing__hero-actions">
<PlatformActionButton
size="md"
shape="pill"
className="creation-landing__hero-action creation-landing__hero-action--primary"
onClick={createProject}
disabled={isCreatingProject}
>
<Wand2 className="h-4 w-4" />
<img
src={CREATION_HOME_ASSETS.heroCreate}
alt=""
aria-hidden="true"
draggable={false}
className="creation-landing__button-art"
/>
</PlatformActionButton>
{onOpenCommunity ? (
<PlatformActionButton
tone="secondary"
size="md"
shape="pill"
className="creation-landing__hero-action creation-landing__hero-action--secondary"
onClick={onOpenCommunity}
>
<img
src={CREATION_HOME_ASSETS.heroCommunity}
alt=""
aria-hidden="true"
draggable={false}
className="creation-landing__button-art"
/>
</PlatformActionButton>
) : null}
@@ -510,18 +520,26 @@ export function CreationLandingView({
</div>
</div>
<div className="creation-landing__feature-grid">
{CREATION_FEATURES.map((feature) => {
const Icon = feature.icon;
return (
<article key={feature.title} className="creation-landing__feature">
{CREATION_FEATURES.map((feature) => (
<article key={feature.title} className="creation-landing__feature">
<span className="creation-landing__feature-figure">
<span className="creation-landing__feature-icon">
<Icon className="h-5 w-5" />
<img
src={feature.iconSrc}
alt=""
aria-hidden="true"
loading="lazy"
draggable={false}
className="creation-landing__feature-art"
/>
</span>
</span>
<span className="creation-landing__feature-copy">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</article>
);
})}
</span>
</article>
))}
</div>
</section>
@@ -552,7 +570,13 @@ export function CreationLandingView({
disabled={isCreatingProject}
>
<span>
<Plus className="h-5 w-5" />
<img
src={CREATION_HOME_ASSETS.projectNew}
alt=""
aria-hidden="true"
draggable={false}
className="creation-landing__project-new-art"
/>
</span>
</button>
@@ -15268,6 +15268,7 @@ export function PlatformEntryFlowShellImpl({
? 'create'
: platformBootstrap.platformTab
}
allowHiddenActiveTab={selectionStage === 'creation-home'}
onTabChange={handlePlatformHomeTabChange}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
@@ -15428,7 +15429,7 @@ export function PlatformEntryFlowShellImpl({
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
className="creation-home-stage flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
{platformHomeView}
</motion.div>
@@ -9169,6 +9169,23 @@ test('creation home desktop rail can return to platform tabs', async () => {
expect(window.location.pathname).toBe('/');
});
test('direct mobile creation home keeps landing content without mobile nav entry', async () => {
window.history.replaceState(null, '', '/creation');
const { container } = render(<TestWrapper withAuth />);
expect(
await screen.findByRole('main', { name: '陶泥儿创作主页' }),
).toBeTruthy();
const bottomNav = container.querySelector('.platform-bottom-nav');
expect(bottomNav).toBeTruthy();
expect(
within(bottomNav as HTMLElement).queryByRole('button', { name: '创作' }),
).toBeNull();
expect(
within(bottomNav as HTMLElement).queryByRole('button', { name: '项目' }),
).toBeNull();
});
test('embedded puzzle form timeout exits busy state and shows a readable error', async () => {
const user = userEvent.setup();
+57 -3
View File
@@ -288,6 +288,7 @@ export interface RpgEntryHomeViewProps {
createTabContent?: ReactNode;
draftTabContent?: ReactNode;
hasUnreadDraftUpdate?: boolean;
allowHiddenActiveTab?: boolean;
}
const PANEL_SURFACE_CLASS = 'platform-surface platform-surface--soft';
@@ -319,6 +320,19 @@ const PLATFORM_DESKTOP_NAV_TABS: PlatformNavTab[] = [
'projects',
'profile',
];
const CREATION_HOME_NAV_ICON_SRC: Record<PlatformNavTab, string> = {
home: '/creation-home/nav-recommend.png',
category: '/creation-home/nav-discover.png',
create: '/creation-home/nav-create.png',
projects: '/creation-home/nav-projects.png',
saves: '/creation-home/nav-projects.png',
profile: '/creation-home/nav-profile.png',
};
const CREATION_HOME_TOPBAR_ASSETS = {
brandMascot: '/creation-home/brand-mascot.png',
search: '/creation-home/topbar-search.png',
wallet: '/creation-home/topbar-wallet.png',
} as const;
const AVATAR_MAX_FILE_SIZE = 5 * 1024 * 1024;
const AVATAR_OUTPUT_SIZE = 256;
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
@@ -1011,6 +1025,13 @@ function PublicCodeSearchBar({
<div
className={`platform-desktop-search flex min-w-0 items-center gap-3 px-4 py-3 text-[var(--platform-text-soft)] ${className ?? ''}`}
>
<img
src={CREATION_HOME_TOPBAR_ASSETS.search}
alt=""
aria-hidden="true"
draggable={false}
className="creation-home-topbar-art creation-home-topbar-art--search"
/>
<Search className="h-4 w-4 shrink-0" />
<input
value={value}
@@ -1065,6 +1086,13 @@ function TopbarWalletShortcutButton({
.filter(Boolean)
.join(' ')}
>
<img
src={CREATION_HOME_TOPBAR_ASSETS.wallet}
alt=""
aria-hidden="true"
draggable={false}
className="creation-home-topbar-art creation-home-topbar-art--wallet"
/>
<PlatformIconBadge
icon={<Coins className="h-3.5 w-3.5" />}
size="xs"
@@ -1868,6 +1896,7 @@ function DesktopTabButton({
emphasized = false,
showDot = false,
disabled = false,
creationHomeIconSrc,
}: {
active: boolean;
label: string;
@@ -1876,6 +1905,7 @@ function DesktopTabButton({
emphasized?: boolean;
showDot?: boolean;
disabled?: boolean;
creationHomeIconSrc?: string;
}) {
const ariaLabel = showDot ? `${label},有新草稿` : label;
@@ -1888,6 +1918,15 @@ function DesktopTabButton({
className={`platform-desktop-rail__button ${emphasized ? 'platform-desktop-rail__button--primary' : ''} ${active ? 'platform-desktop-rail__button--active' : ''} disabled:cursor-not-allowed disabled:opacity-55`}
>
<span className="platform-desktop-rail__icon-shell">
{creationHomeIconSrc ? (
<img
src={creationHomeIconSrc}
alt=""
aria-hidden="true"
draggable={false}
className="creation-home-nav-art"
/>
) : null}
<Icon className="platform-desktop-rail__icon h-[1.1rem] w-[1.1rem]" />
{showDot ? (
<span aria-hidden="true" className="platform-nav-unread-dot" />
@@ -2597,6 +2636,7 @@ export function RpgEntryHomeView({
createTabContent,
draftTabContent,
hasUnreadDraftUpdate = false,
allowHiddenActiveTab = false,
}: RpgEntryHomeViewProps) {
const authUi = useAuthUi();
const showRechargeEntry = shouldShowRechargeEntry();
@@ -2935,11 +2975,15 @@ export function RpgEntryHomeView({
} as const satisfies Record<PlatformNavTab, string>;
useEffect(() => {
if (allowHiddenActiveTab) {
return;
}
if (!visibleMobileTabs.includes(activeTab) && !isDesktopLayout) {
onTabChange(isAuthenticated ? 'home' : 'category');
}
}, [
activeTab,
allowHiddenActiveTab,
isAuthenticated,
isDesktopLayout,
onTabChange,
@@ -5096,8 +5140,17 @@ export function RpgEntryHomeView({
<div className="flex h-full min-h-0 flex-col">
<div className="platform-desktop-shell flex h-full min-h-0 flex-col p-5 xl:p-6">
<div className="platform-desktop-topbar flex items-center gap-4 px-5 py-4">
<div className="flex min-w-0 flex-1 items-center gap-5">
<RpgEntryBrandLogo className="shrink-0" decorative />
<div className="platform-desktop-topbar__brand-search flex min-w-0 flex-1 items-center gap-5">
<span className="creation-home-brand-lockup shrink-0">
<img
src={CREATION_HOME_TOPBAR_ASSETS.brandMascot}
alt=""
aria-hidden="true"
draggable={false}
className="creation-home-brand-lockup__art"
/>
<RpgEntryBrandLogo className="shrink-0" decorative />
</span>
<PublicCodeSearchBar
value={desktopSearchKeyword}
onChange={updateDesktopSearchKeyword}
@@ -5109,7 +5162,7 @@ export function RpgEntryHomeView({
/>
</div>
<div className="flex items-center gap-3">
<div className="platform-desktop-topbar__actions flex items-center gap-3">
{isAuthenticated &&
(activeTab === 'create' || activeTab === 'saves') ? (
<TopbarWalletShortcutButton
@@ -5162,6 +5215,7 @@ export function RpgEntryHomeView({
active={isPlatformHomeTab(tab) && activeTab === tab}
label={tabLabels[tab]}
icon={tabIcons[tab]}
creationHomeIconSrc={CREATION_HOME_NAV_ICON_SRC[tab]}
emphasized={tab === 'create'}
showDot={false}
disabled={isRechargePaymentConfirmationPending}
+581 -83
View File
File diff suppressed because it is too large Load Diff