1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 15:45:14 +08:00
parent 8a7bd90458
commit 1c72066bab
73 changed files with 7814 additions and 1018 deletions

View File

@@ -117,12 +117,90 @@ test('custom world character selection stays stable when character ids are empty
render(
<CharacterSelectionFlow
worldType={WorldType.CUSTOM}
customWorldProfile={{} as CustomWorldProfile}
customWorldProfile={{
attributeSchema: {
id: 'schema:custom:test',
worldId: 'custom:test',
schemaVersion: 1,
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '潮城',
settingSummary: '潮水与迷雾交织的港城。',
tone: '潮湿、危险、带着试探。',
conflictCore: '在涨落之间抢先一步。',
},
slots: [
{
slotId: 'axis_a',
name: '潮骨',
definition: '扛住潮压与正面冲击的底子。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '顶住正面浪涌。',
socialUseText: '给人能扛事的可靠感。',
explorationUseText: '在风浪里稳住自己。',
},
{
slotId: 'axis_b',
name: '浪步',
definition: '顺潮借势、换位穿行的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '借势切线。',
socialUseText: '谈吐灵活。',
explorationUseText: '穿越复杂地形。',
},
{
slotId: 'axis_c',
name: '舟识',
definition: '辨流向、识潮眼的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '抓住变化时机。',
socialUseText: '看懂局势留白。',
explorationUseText: '辨认水路与遗痕。',
},
{
slotId: 'axis_d',
name: '潮魄',
definition: '在剧烈变化中仍敢推进的胆气。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '顶着压力推进。',
socialUseText: '在冲突里压住场子。',
explorationUseText: '面对异变继续前探。',
},
{
slotId: 'axis_e',
name: '契汐',
definition: '与人和约定形成牵引的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '借协同形成连锁。',
socialUseText: '结盟、安抚与交换。',
explorationUseText: '从旧约中打开局面。',
},
{
slotId: 'axis_f',
name: '回澜',
definition: '在漫长消耗中回稳状态的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '久战不乱。',
socialUseText: '遇事沉静。',
explorationUseText: '在恶劣天气里保有余力。',
},
],
},
} as unknown as CustomWorldProfile}
onBack={() => {}}
onConfirm={handleConfirm}
/>,
);
expect(screen.getByText(/:/u)).toBeTruthy();
expect(screen.queryByText(/:/u)).toBeNull();
await user.click(screen.getByRole('button', { name: //u }));
await waitFor(() => {

View File

@@ -1,5 +1,12 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {
buildCharacterAttributeProfile,
} from '../../data/attributeProfileGenerator';
import {
resolveAttributeSchema,
resolveCharacterAttributeProfile,
} from '../../data/attributeResolver';
import {
buildCustomWorldPlayableCharacters,
ROLE_TEMPLATE_CHARACTERS,
@@ -32,13 +39,6 @@ const CHARACTER_DISPLAY: Record<string, {name: string; title: string; role: stri
'fighter-4': {name: '装甲长矛手', title: '重装先锋', role: '前线', tags: ['守护', '稳定', '突破']},
};
const ATTRIBUTE_LABELS: Record<keyof Character['attributes'], string> = {
strength: '力量',
agility: '敏捷',
intelligence: '智力',
spirit: '精神',
};
function getGenderLabel(gender: Character['gender']) {
if (gender === 'female') return '女性';
if (gender === 'male') return '男性';
@@ -211,6 +211,22 @@ export function CharacterSelectionFlow({
const selectedCharacterMeta = selectedCharacter
? getCharacterMeta(selectedCharacter, {name: selectedCharacterDraft?.name})
: null;
const attributeSchema = useMemo(
() => resolveAttributeSchema(worldType, customWorldProfile),
[customWorldProfile, worldType],
);
const selectedAttributeProfile = useMemo(
() =>
selectedCharacter
? resolveCharacterAttributeProfile(
selectedCharacter,
worldType,
customWorldProfile,
)
?? buildCharacterAttributeProfile(selectedCharacter, attributeSchema)
: null,
[attributeSchema, customWorldProfile, selectedCharacter, worldType],
);
const selectedCharacterPersonalityTags = useMemo(
() => (selectedCharacterPreview ? getPersonalityTags(selectedCharacterPreview.personality) : []),
[selectedCharacterPreview],
@@ -363,10 +379,10 @@ export function CharacterSelectionFlow({
</span>
</div>
</div>
<div className="grid grid-cols-4 gap-1 text-[11px] text-zinc-300 sm:gap-1.5 sm:text-[13px]">
{Object.entries(selectedCharacter.attributes).map(([key, value]) => (
<div key={key} className="rounded-lg border border-white/6 bg-black/20 px-2 py-1.5 text-center sm:px-2.5">
{ATTRIBUTE_LABELS[key as keyof Character['attributes']]}: {value}
<div className="grid grid-cols-2 gap-1 text-[11px] text-zinc-300 sm:grid-cols-3 sm:gap-1.5 sm:text-[13px]">
{attributeSchema.slots.map((slot) => (
<div key={slot.slotId} className="rounded-lg border border-white/6 bg-black/20 px-2 py-1.5 text-center sm:px-2.5">
{slot.name}: {selectedAttributeProfile?.values?.[slot.slotId] ?? 0}
</div>
))}
</div>

View File

@@ -141,11 +141,15 @@ export function GameShellMainContent({
<div
className={`${isPlatformShell ? 'platform-main-shell' : 'pixel-app-shell'} flex min-h-0 flex-1 flex-col ${isCharacterSelectionStage ? 'justify-center p-4 sm:p-5' : 'p-3 sm:p-4'}`}
style={{
background: isPlatformShell
backgroundColor: isPlatformShell
? 'transparent'
: isCharacterSelectionStage
? '#0d1016'
: `linear-gradient(rgba(10, 12, 18, 0.55), rgba(10, 12, 18, 0.55)), url("${UI_CHROME.appBackground}")`,
? '#0d1016'
: undefined,
backgroundImage:
isPlatformShell || isCharacterSelectionStage
? undefined
: `linear-gradient(rgba(10, 12, 18, 0.55), rgba(10, 12, 18, 0.55)), url("${UI_CHROME.appBackground}")`,
backgroundPosition:
isPlatformShell || isCharacterSelectionStage ? undefined : 'center',
backgroundRepeat:

View File

@@ -1,9 +1,13 @@
import { AnimatePresence, motion } from 'motion/react';
import { lazy, Suspense } from 'react';
import type { CharacterChatUi, InventoryFlowUi, StoryGenerationNpcUi } from '../../hooks/useStoryGeneration';
import type {
CharacterChatUi,
InventoryFlowUi,
StoryGenerationNpcUi,
} from '../../hooks/useStoryGeneration';
import type { CompanionRenderState, GameState } from '../../types';
import { CHROME_ICONS, getNineSliceStyle,UI_CHROME } from '../../uiAssets';
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../../uiAssets';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import { PixelIcon } from '../PixelIcon';
import { ModalLoadingFallback, PanelLoadingFallback } from './GameShellLoaders';
@@ -120,7 +124,14 @@ export function GameShellOverlays({
return (
<>
{shouldMountAdventureEntityModal && (
<Suspense fallback={<ModalLoadingFallback label="正在加载冒险详情..." onClose={closeAdventureEntityModal} />}>
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载冒险详情..."
onClose={closeAdventureEntityModal}
/>
}
>
<AdventureEntityModal
selection={selectedSceneEntity}
gameState={gameState}
@@ -146,10 +157,12 @@ export function GameShellOverlays({
transition={{ duration: 0.18, ease: 'easeOut' }}
className="pixel-nine-slice pixel-modal-shell flex max-h-[min(92vh,60rem)] w-full max-w-5xl flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
style={getNineSliceStyle(UI_CHROME.modalPanel)}
onClick={event => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
<div className="relative border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
<div className="min-w-0 pr-10 text-sm font-semibold text-white">{overlayPanel === 'character' ? '队伍' : '背包'}</div>
<div className="min-w-0 pr-10 text-sm font-semibold text-white">
{overlayPanel === 'character' ? '队伍' : '背包'}
</div>
<button
type="button"
onClick={closeOverlayPanel}
@@ -160,11 +173,14 @@ export function GameShellOverlays({
</div>
<div className="flex min-h-0 flex-1 p-5">
{overlayPanel === 'character' ? (
<Suspense fallback={<PanelLoadingFallback label="正在加载队伍面板" />}>
<Suspense
fallback={<PanelLoadingFallback label="正在加载队伍面板" />}
>
<CharacterPanel
worldType={gameState.worldType}
customWorldProfile={gameState.customWorldProfile}
playerCharacter={gameState.playerCharacter}
playerProgression={gameState.playerProgression ?? null}
playerHp={gameState.playerHp}
playerMaxHp={gameState.playerMaxHp}
playerMana={gameState.playerMana}
@@ -178,7 +194,7 @@ export function GameShellOverlays({
closeOverlayPanel();
openCampModal();
}}
onOpenCharacterChat={target => {
onOpenCharacterChat={(target) => {
closeOverlayPanel();
characterChatUi.openChat(target);
}}
@@ -187,7 +203,9 @@ export function GameShellOverlays({
/>
</Suspense>
) : (
<Suspense fallback={<PanelLoadingFallback label="正在加载背包面板" />}>
<Suspense
fallback={<PanelLoadingFallback label="正在加载背包面板" />}
>
<InventoryPanel
playerCharacter={gameState.playerCharacter}
worldType={gameState.worldType}
@@ -214,7 +232,14 @@ export function GameShellOverlays({
</AnimatePresence>
{shouldMountCampModal && (
<Suspense fallback={<ModalLoadingFallback label="正在加载队伍营地..." onClose={closeCampModal} />}>
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载队伍营地..."
onClose={closeCampModal}
/>
}
>
<CompanionCampModal
isOpen={showTeamModal}
playerCharacter={gameState.playerCharacter}
@@ -229,13 +254,20 @@ export function GameShellOverlays({
)}
{shouldMountMapModal && (
<Suspense fallback={<ModalLoadingFallback label="正在加载地图..." onClose={() => setIsMapOpen(false)} />}>
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载地图..."
onClose={() => setIsMapOpen(false)}
/>
}
>
<MapModal
isOpen={isMapOpen}
currentScenePreset={gameState.currentScenePreset}
worldType={gameState.worldType}
canTravel={!gameState.inBattle && !isLoading}
onTravelToScene={scene => {
onTravelToScene={(scene) => {
const triggered = handleMapTravelToScene(scene.id);
if (triggered) {
setIsMapOpen(false);
@@ -248,7 +280,14 @@ export function GameShellOverlays({
)}
{shouldMountCharacterChatModal && (
<Suspense fallback={<ModalLoadingFallback label="正在加载角色聊天..." onClose={characterChatUi.closeChat} />}>
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载角色聊天..."
onClose={characterChatUi.closeChat}
/>
}
>
<CharacterChatModal
modal={characterChatUi.modal}
onClose={characterChatUi.closeChat}
@@ -261,7 +300,9 @@ export function GameShellOverlays({
)}
{shouldMountNpcModals && (
<Suspense fallback={<ModalLoadingFallback label="正在加载角色交互..." />}>
<Suspense
fallback={<ModalLoadingFallback label="正在加载角色交互..." />}
>
<NpcModals gameState={gameState} npcUi={npcUi} />
</Suspense>
)}

View File

@@ -1,5 +1,6 @@
import { lazy, Suspense, useEffect } from 'react';
import { lazy, Suspense } from 'react';
import { normalizePlayerProgressionState } from '../../data/playerProgression';
import { UI_CHROME } from '../../uiAssets';
import { useAuthUi } from '../auth/AuthUiContext';
import { GameShellMainContent } from './GameShellMainContent';
@@ -19,7 +20,13 @@ const GameShellCanvasStage = lazy(async () => {
};
});
export function GameShellRuntime({session, story, entry, companions, audio}: GameShellProps) {
export function GameShellRuntime({
session,
story,
entry,
companions,
audio,
}: GameShellProps) {
const authUi = useAuthUi();
const isPlatformShell = !session.gameState.worldType;
const platformThemeClass =
@@ -60,12 +67,9 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
handleBackToWorldSelect,
handleCharacterSelect,
} = entry;
const {
companionRenderStates,
onBenchCompanion,
onActivateRosterCompanion,
} = companions;
const {musicVolume, onMusicVolumeChange} = audio;
const { companionRenderStates, onBenchCompanion, onActivateRosterCompanion } =
companions;
const { musicVolume, onMusicVolumeChange } = audio;
const {
selectionStage,
setSelectionStage,
@@ -103,24 +107,26 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
story,
companions,
});
useEffect(() => {
authUi?.setGlobalAccountActionsVisible(false);
return () => {
authUi?.setGlobalAccountActionsVisible(true);
};
}, [authUi]);
const playerProgression = normalizePlayerProgressionState(
visibleGameState.playerProgression ?? null,
);
const playerProgressionRatio =
playerProgression.xpToNextLevel <= 0
? 1
: Math.max(
0,
Math.min(
1,
playerProgression.currentLevelXp / playerProgression.xpToNextLevel,
),
);
return (
<div
className={`${isPlatformShell ? `platform-ui-shell platform-theme ${platformThemeClass} text-[var(--platform-text-strong)]` : 'fusion-pixel-app pixel-root-shell text-zinc-100'} flex h-screen max-h-screen flex-col overflow-hidden font-sans`}
style={{
background: isPlatformShell
? 'var(--platform-body-fill)'
: undefined,
backgroundImage: isPlatformShell
? undefined
? 'var(--platform-body-fill)'
: `linear-gradient(rgba(8, 10, 14, 0.82), rgba(8, 10, 14, 0.82)), url("${UI_CHROME.appBackground}")`,
backgroundPosition: isPlatformShell ? undefined : 'center',
backgroundRepeat: isPlatformShell ? undefined : 'repeat',
@@ -141,6 +147,36 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
/>
</Suspense>
{visibleGameState.playerCharacter && (
<div
className="pointer-events-none fixed z-[26] w-[4.5rem] drop-shadow-[0_2px_8px_rgba(0,0,0,0.75)]"
style={{
top: 'calc(env(safe-area-inset-top, 0px) + 0.65rem)',
left: 'calc(env(safe-area-inset-left, 0px) + 0.7rem)',
}}
>
<div className="flex items-end gap-1.5 text-amber-50">
<span className="text-[10px] font-semibold uppercase leading-none tracking-[0.14em] text-amber-100/80">
Lv
</span>
<span className="text-2xl font-black leading-none tracking-[-0.08em] text-white">
{playerProgression.level}
</span>
</div>
<div className="mt-1 h-1 overflow-hidden rounded-full bg-black/45">
<div
className="h-full rounded-full bg-[linear-gradient(90deg,rgba(251,191,36,0.65),rgba(254,240,138,0.95))]"
style={{
width:
playerProgressionRatio <= 0
? '0%'
: `${Math.max(8, playerProgressionRatio * 100)}%`,
}}
/>
</div>
</div>
)}
<GameShellMainContent
gameState={gameState}
visibleGameState={visibleGameState}

View File

@@ -9,8 +9,13 @@ import type {
NpcChatQuestOfferUi,
QuestFlowUi,
} from '../../hooks/useStoryGeneration';
import type { CompanionRenderState, GameState, StoryMoment, StoryOption } from '../../types';
import { getNineSliceStyle,TAB_ICONS, UI_CHROME } from '../../uiAssets';
import type {
CompanionRenderState,
GameState,
StoryMoment,
StoryOption,
} from '../../types';
import { getNineSliceStyle, TAB_ICONS, UI_CHROME } from '../../uiAssets';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import { PixelIcon } from '../PixelIcon';
import { PanelLoadingFallback } from './GameShellLoaders';
@@ -110,11 +115,20 @@ export function GameShellStoryPanels({
<button
onClick={() => setBottomTab('character')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'character' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(bottomTab === 'character' ? UI_CHROME.tabActive : UI_CHROME.tabInactive, { paddingX: 10, paddingY: 8 })}
style={getNineSliceStyle(
bottomTab === 'character'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={bottomTab === 'character' ? TAB_ICONS.character.active : TAB_ICONS.character.inactive}
src={
bottomTab === 'character'
? TAB_ICONS.character.active
: TAB_ICONS.character.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'character' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
@@ -123,11 +137,20 @@ export function GameShellStoryPanels({
<button
onClick={() => setBottomTab('adventure')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'adventure' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(bottomTab === 'adventure' ? UI_CHROME.tabActive : UI_CHROME.tabInactive, { paddingX: 10, paddingY: 8 })}
style={getNineSliceStyle(
bottomTab === 'adventure'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={bottomTab === 'adventure' ? TAB_ICONS.adventure.active : TAB_ICONS.adventure.inactive}
src={
bottomTab === 'adventure'
? TAB_ICONS.adventure.active
: TAB_ICONS.adventure.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'adventure' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
@@ -136,11 +159,20 @@ export function GameShellStoryPanels({
<button
onClick={() => setBottomTab('inventory')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'inventory' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(bottomTab === 'inventory' ? UI_CHROME.tabActive : UI_CHROME.tabInactive, { paddingX: 10, paddingY: 8 })}
style={getNineSliceStyle(
bottomTab === 'inventory'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={bottomTab === 'inventory' ? TAB_ICONS.inventory.active : TAB_ICONS.inventory.inactive}
src={
bottomTab === 'inventory'
? TAB_ICONS.inventory.active
: TAB_ICONS.inventory.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'inventory' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
@@ -154,6 +186,7 @@ export function GameShellStoryPanels({
worldType={visibleGameState.worldType}
customWorldProfile={visibleGameState.customWorldProfile}
playerCharacter={playerCharacter}
playerProgression={visibleGameState.playerProgression ?? null}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
playerMana={visibleGameState.playerMana}

View File

@@ -48,8 +48,10 @@ export type PlatformHomeTab = 'home' | 'create' | 'saves' | 'profile';
const PANEL_SURFACE_CLASS = 'platform-surface platform-surface--soft';
const HERO_SURFACE_CLASS =
'platform-surface platform-surface--hero platform-interactive-card';
const MOBILE_PAGE_STAGE_CLASS = 'platform-page-stage space-y-4 pb-2';
const DESKTOP_PAGE_STAGE_CLASS = 'platform-page-stage space-y-5 pb-4';
const MOBILE_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface space-y-4 pb-2';
const DESKTOP_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface space-y-5 pb-4';
function SectionHeader({ title, detail }: { title: string; detail: string }) {
return (
@@ -57,14 +59,18 @@ function SectionHeader({ title, detail }: { title: string; detail: string }) {
<div className="text-[10px] font-semibold tracking-[0.26em] text-zinc-500">
{detail}
</div>
<div className="mt-1 text-base font-semibold text-white">{title}</div>
<div className="mt-1 text-base font-semibold text-[var(--platform-text-strong)]">
{title}
</div>
</div>
);
}
function EmptyShelf({ text }: { text: string }) {
return (
<div className={`${PANEL_SURFACE_CLASS} rounded-[1.35rem] px-4 py-3 text-sm leading-6 text-zinc-300`}>
<div
className={`${PANEL_SURFACE_CLASS} rounded-[1.35rem] px-4 py-3 text-sm leading-6 text-zinc-300`}
>
{text}
</div>
);
@@ -82,7 +88,7 @@ function SaveArchivePreview({
return (
<div
aria-hidden="true"
className={`relative shrink-0 overflow-hidden rounded-[1.35rem] border border-white/12 bg-black/18 shadow-[0_16px_36px_rgba(15,23,42,0.18)] ${className}`}
className={`platform-remap-surface relative shrink-0 overflow-hidden rounded-[1.35rem] border border-white/12 bg-black/18 shadow-[var(--platform-desktop-hover-shadow)] ${className}`}
>
{entry.coverImageSrc ? (
<img
@@ -93,7 +99,7 @@ function SaveArchivePreview({
) : (
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.18),transparent_34%),linear-gradient(145deg,rgba(255,94,125,0.92),rgba(255,150,116,0.88))]" />
)}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,14,0.06),rgba(8,10,14,0.74))]" />
<div className="absolute inset-0 bg-[var(--platform-card-overlay-soft)]" />
<div className="absolute inset-x-0 bottom-0 px-2.5 py-2">
<span className="inline-flex max-w-full items-center rounded-full border border-white/15 bg-black/24 px-2.5 py-1 text-[9px] font-semibold tracking-[0.14em] text-white/88">
{label}
@@ -147,12 +153,10 @@ function WorldCard({
className="absolute bottom-2 right-2 h-24 w-24 object-contain opacity-25"
/>
) : null}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,14,0.14),rgba(8,10,14,0.9))]" />
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
<div className="relative z-10 flex h-full flex-col">
<div className="flex items-start justify-between gap-3">
<span className="platform-pill platform-pill--warm">
{badge}
</span>
<span className="platform-pill platform-pill--warm">{badge}</span>
<span className="platform-pill platform-pill--neutral px-2.5">
{metaLabel}
</span>
@@ -233,7 +237,7 @@ function CreationLibraryCard({
className="absolute bottom-1.5 right-1.5 h-16 w-16 object-contain opacity-24 sm:h-20 sm:w-20"
/>
) : null}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,14,0.1),rgba(8,10,14,0.92))]" />
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
<div className="relative z-10 flex h-full min-w-0 flex-col">
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
<span
@@ -267,7 +271,9 @@ function CreationLibraryCard({
<span className="truncate">{primaryTag}</span>
</span>
<span className="inline-flex items-center gap-1 text-[11px] font-semibold text-zinc-200">
<span>{entry.visibility === 'published' ? '进入世界' : '继续创作'}</span>
<span>
{entry.visibility === 'published' ? '进入世界' : '继续创作'}
</span>
<ArrowRight className="h-3.5 w-3.5 shrink-0" />
</span>
</div>
@@ -286,7 +292,8 @@ function SaveArchiveCard({
onClick: () => void;
loading?: boolean;
}) {
const summaryText = entry.summaryText || entry.subtitle || '继续推进上一次保存的故事。';
const summaryText =
entry.summaryText || entry.subtitle || '继续推进上一次保存的故事。';
return (
<button
@@ -295,7 +302,7 @@ function SaveArchiveCard({
disabled={loading}
className={`platform-surface platform-surface--soft platform-interactive-card relative flex min-h-[13rem] w-full overflow-hidden p-3.5 text-left sm:min-h-[12.5rem] sm:p-4 ${loading ? 'opacity-80' : ''}`}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.14),transparent_30%),radial-gradient(circle_at_right,rgba(255,205,178,0.14),transparent_28%),linear-gradient(180deg,rgba(8,10,14,0.22),rgba(8,10,14,0.9))]" />
<div className="absolute inset-0 bg-[var(--platform-card-overlay-deep)]" />
<div className="relative z-10 flex h-full w-full flex-col gap-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="platform-pill platform-pill--cool">ARCHIVE</span>
@@ -422,7 +429,7 @@ function DesktopTrendingItem({
<span>{`${rank}`.padStart(2, '0')}</span>
<span>{formatPlatformWorldTime(entry.publishedAt)}</span>
</div>
<div className="mt-2 line-clamp-1 text-lg font-semibold text-white">
<div className="mt-2 line-clamp-1 text-lg font-semibold text-[var(--platform-text-strong)]">
{entry.worldName}
</div>
<div className="mt-1 line-clamp-2 text-sm leading-6 text-zinc-300/86">
@@ -581,7 +588,9 @@ function ProfileStatCard({
<Icon className="h-4 w-4" />
<span className="text-[11px] tracking-[0.16em]">{label}</span>
</div>
<div className="mt-3 text-lg font-black text-white">{value}</div>
<div className="mt-3 text-lg font-black text-[var(--platform-text-strong)]">
{value}
</div>
</button>
);
}
@@ -615,7 +624,9 @@ function ProfileShortcutButton({
<div className="platform-profile-chip flex h-10 w-10 items-center justify-center rounded-full">
<Icon className="h-[1.125rem] w-[1.125rem]" />
</div>
<div className="text-sm font-semibold text-white">{label}</div>
<div className="text-sm font-semibold text-[var(--platform-text-strong)]">
{label}
</div>
</button>
);
}
@@ -756,7 +767,7 @@ export function PlatformHomeView({
? `${snapshotCharacterName} 的进度已保存,点这里回到上一次停下来的故事节点。`
: '从设定、角色到场景网络,先生成一版可玩的世界底稿,再继续精修和发布。'}
</div>
<div className="mt-4 flex items-center gap-2 text-sm font-semibold text-violet-100">
<div className="mt-4 flex items-center gap-2 text-sm font-semibold text-white/90">
<span>{hasSavedGame ? '继续推进故事' : '进入创作工作台'}</span>
<ArrowRight className="h-4 w-4" />
</div>
@@ -832,7 +843,7 @@ export function PlatformHomeView({
<div className="mt-2 max-w-[28rem] text-sm leading-6 text-zinc-200/88">
</div>
<div className="mt-4 flex items-center gap-2 text-sm font-semibold text-cyan-100">
<div className="mt-4 flex items-center gap-2 text-sm font-semibold text-white/90">
<span></span>
<ArrowRight className="h-4 w-4" />
</div>
@@ -904,7 +915,9 @@ export function PlatformHomeView({
) : (
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<div className="platform-subpanel rounded-[1.35rem] px-4 py-4">
<div className="text-base font-semibold text-white"></div>
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
</div>
<button
type="button"
onClick={() => authUi?.openLoginModal()}
@@ -1074,7 +1087,9 @@ export function PlatformHomeView({
<Settings className="h-[1.125rem] w-[1.125rem]" />
</div>
<div>
<div className="text-base font-semibold text-white"></div>
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
</div>
<div className="text-xs text-zinc-400"></div>
</div>
</div>
@@ -1085,7 +1100,9 @@ export function PlatformHomeView({
) : (
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<div className="platform-subpanel rounded-[1.35rem] px-4 py-4">
<div className="text-base font-semibold text-white"></div>
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
</div>
<button
type="button"
onClick={() => authUi?.openLoginModal()}
@@ -1144,15 +1161,19 @@ export function PlatformHomeView({
<div className="max-w-[35rem]">
<div className="text-5xl font-semibold leading-[1.08] text-white">
{hasSavedGame ? snapshotWorldName : '把你的世界观直接变成可游玩的舞台'}
{hasSavedGame
? snapshotWorldName
: '把你的世界观直接变成可游玩的舞台'}
</div>
<div className="mt-4 text-base leading-8 text-zinc-200/86">
{hasSavedGame
? `${snapshotCharacterName} 的进度已经保存,桌面端可以直接从这里回到上一次停下来的关键节点。`
: '从设定、角色、世界结构到可玩流程,一次生成创作底稿,再继续精修并发布到平台广场。'}
</div>
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-violet-300/18 bg-violet-500/14 px-4 py-2 text-sm font-semibold text-violet-100">
<span>{hasSavedGame ? '继续推进故事' : '进入创作工作台'}</span>
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-white/18 bg-white/18 px-4 py-2 text-sm font-semibold text-white/92">
<span>
{hasSavedGame ? '继续推进故事' : '进入创作工作台'}
</span>
<ArrowRight className="h-4 w-4" />
</div>
</div>
@@ -1181,7 +1202,9 @@ export function PlatformHomeView({
<span className="text-zinc-500">
{`${index + 1}`.padStart(2, '0')}
</span>
<span className="line-clamp-1">{entry.worldName}</span>
<span className="line-clamp-1">
{entry.worldName}
</span>
</div>
</div>
);
@@ -1405,8 +1428,11 @@ export function PlatformHomeView({
</div>
<div
className="mt-4 border-t border-white/5 pt-3"
style={{ paddingBottom: 'calc(env(safe-area-inset-bottom) + 0.2rem)' }}
className="mt-4 border-t pt-3"
style={{
borderColor: 'var(--platform-line-soft)',
paddingBottom: 'calc(env(safe-area-inset-bottom) + 0.2rem)',
}}
>
<div className="platform-bottom-nav grid h-14 grid-cols-4 gap-1 rounded-[1.2rem] px-1 py-1">
<PlatformTabButton
@@ -1441,10 +1467,7 @@ export function PlatformHomeView({
<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">
<PlatformBrandLogo
className="shrink-0"
decorative
/>
<PlatformBrandLogo className="shrink-0" decorative />
<div className="platform-desktop-search flex min-w-0 max-w-[34rem] flex-1 items-center gap-3 px-4 py-3 text-zinc-400">
<Search className="h-4 w-4 shrink-0" />
<span className="truncate text-sm">
@@ -1467,11 +1490,17 @@ export function PlatformHomeView({
onClick={openUserSurface}
className="platform-desktop-search flex items-center gap-3 px-3 py-2.5 text-left"
>
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-[linear-gradient(135deg,rgba(91,108,255,0.9),rgba(61,217,255,0.82))] text-base font-black text-white shadow-[0_10px_22px_rgba(91,108,255,0.24)]">
<span
className="flex h-11 w-11 items-center justify-center rounded-full text-base font-black text-white"
style={{
background: 'var(--platform-profile-avatar-fill)',
boxShadow: 'var(--platform-profile-avatar-shadow)',
}}
>
{avatarLabel}
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-semibold text-white">
<span className="block truncate text-sm font-semibold text-[var(--platform-text-strong)]">
{authUi?.user?.displayName || '进入账户'}
</span>
<span className="block truncate text-xs text-zinc-400">

View File

@@ -211,7 +211,6 @@ type TestAuthValue = {
openSettingsModal: (section?: PlatformSettingsSection) => void;
openAccountModal: () => void;
logout: () => Promise<void>;
setGlobalAccountActionsVisible: (visible: boolean) => void;
musicVolume: number;
setMusicVolume: (value: number) => void;
platformTheme: 'light' | 'dark';
@@ -229,7 +228,6 @@ function createAuthValue(overrides: Partial<TestAuthValue> = {}): TestAuthValue
openSettingsModal: () => {},
openAccountModal: () => {},
logout: async () => {},
setGlobalAccountActionsVisible: () => {},
musicVolume: 0.42,
setMusicVolume: () => {},
platformTheme: 'light',