收口运行态阶段加载提示

RPG runtime 主阶段懒加载提示改用 PlatformSubpanel

新增阶段路由加载提示的公共子面板断言

同步 PlatformUiKit 文档和 Hermes 决策记录
This commit is contained in:
2026-06-10 13:07:20 +08:00
parent 6841b686d9
commit d7002929c3
4 changed files with 173 additions and 3 deletions
+1
View File
@@ -70,6 +70,7 @@
- 2026-06-10 追加:个人中心存档 / 玩过弹窗里的简单空态使用 `PlatformEmptyState surface="subpanel" size="inline"`,玩过弹窗的“可继续 / 玩过”分区标题使用 `PlatformFieldLabel variant="section"`,已玩作品白底按钮卡使用 `PlatformSubpanel as="button" surface="flat" radius="sm" padding="md" interactive``SaveArchiveCard` 因含图片遮罩和加载态暂不并入本轮。
- 2026-06-10 追加:平台入口壳纯 Suspense fallback 使用 `PlatformSubpanel radius="sm" padding="none"` 承接原 `platform-subpanel` 外壳;带恢复动作、错误语义或运行态遮罩的提示面板不和纯加载 fallback 同批迁移。
- 2026-06-10 追加:平台入口作品详情读取 / 错误提示、Agent 工作区恢复提示和生成结果恢复面板也迁移到 `PlatformSubpanel`;普通提示使用 `radius="sm" padding="none"`,带恢复动作的 `CreationResultRecoveryPanel` 使用 `radius="xl" padding="none"`,玩法 runtime overlay 继续保留专用层级语义。验证命令:`npm run test -- src/components/common/PlatformSubpanel.test.tsx src/components/platform-entry/PlatformEntryFlowShellImpl.test.ts`
- 2026-06-10 追加:RPG runtime 主阶段路由里的平台首页、角色选择和冒险面板懒加载提示使用 `PlatformSubpanel radius="sm" padding="none"`;路由器只保留 Suspense 分流和提示文案,运行态 HUD / overlay 不并入该普通提示面板规则。验证命令:`npm run test -- src/components/rpg-runtime-shell/RpgRuntimeStageRouter.test.tsx src/components/common/PlatformSubpanel.test.tsx`
- 2026-06-10 追加:个人中心钱包账单弹窗的“暂无账单记录”使用 `PlatformEmptyState surface="subpanel" size="inline"`,账单行使用 `PlatformSubpanel as="div" surface="flat" radius="xs" padding="none"`;业务 JSX 只保留来源、时间、收支色值、余额右对齐和局部间距 / 阴影。
- 2026-06-10 追加:个人中心邀请弹窗里的社区二维码卡、邀请码展示卡、成功邀请容器和邀请用户行使用 `PlatformSubpanel`,简单空态使用 `PlatformEmptyState`,小标题使用 `PlatformFieldLabel variant="section"`;外层弹窗、query 自动打开、复制邀请和提交邀请码状态机不随 UI chrome 收口改动。
- 2026-06-10 追加:个人中心任务中心任务条目使用 `PlatformSubpanel radius="sm" padding="md"` 承接原 `platform-subpanel` 外壳;业务组件只保留任务标题、进度、奖励、状态和领取按钮逻辑。
File diff suppressed because one or more lines are too long
@@ -0,0 +1,161 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { expect, test, vi } from 'vitest';
import {
AnimationState,
type GameState,
} from '../../types';
import {
RpgRuntimeStageRouter,
type RpgRuntimeStageRouterProps,
} from './RpgRuntimeStageRouter';
vi.mock('../platform-entry/PlatformEntryFlowShell', () => ({
PlatformEntryFlowShell: () => <div></div>,
}));
vi.mock('../rpg-entry/RpgEntryCharacterSelectView', () => ({
RpgEntryCharacterSelectView: () => <div></div>,
}));
vi.mock('../rpg-runtime-panels/RpgRuntimePanelRouter', () => ({
RpgRuntimePanelRouter: () => <div></div>,
}));
const noop = () => {};
function createGameState(overrides: Partial<GameState> = {}): GameState {
return {
worldType: null,
customWorldProfile: null,
playerCharacter: null,
runtimeStats: {
playTimeMs: 0,
lastPlayTickAt: null,
hostileNpcsDefeated: 0,
questsAccepted: 0,
itemsUsed: 0,
scenesTraveled: 0,
},
currentScene: 'test-scene',
storyHistory: [],
characterChats: {},
animationState: AnimationState.IDLE,
currentEncounter: null,
npcInteractionActive: false,
currentScenePreset: null,
sceneHostileNpcs: [],
playerX: 0,
playerOffsetY: 0,
playerFacing: 'right',
playerActionMode: 'idle',
scrollWorld: false,
inBattle: false,
playerHp: 100,
playerMaxHp: 100,
playerMana: 30,
playerMaxMana: 30,
playerSkillCooldowns: {},
activeCombatEffects: [],
playerCurrency: 0,
playerInventory: [],
playerEquipment: {
weapon: null,
armor: null,
relic: null,
},
npcStates: {},
quests: [],
roster: [],
companions: [],
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
...overrides,
};
}
function buildProps(
overrides: Partial<RpgRuntimeStageRouterProps> = {},
): RpgRuntimeStageRouterProps {
const gameState = createGameState();
return {
gameState,
visibleGameState: gameState,
visibleCurrentStory: null,
isLoading: false,
aiError: null,
bottomTab: 'adventure',
setBottomTab: noop,
selectionStage: 'platform',
setSelectionStage: noop,
isCharacterSelectionStage: false,
hasSavedGame: false,
savedSnapshot: null,
handleContinueGame: noop,
handleStartNewGame: noop,
handleCustomWorldSelect: noop,
handleBackToWorldSelect: noop,
handleCharacterSelect: noop,
displayedOptions: [],
hideStoryOptions: false,
canRefreshOptions: false,
handleRefreshOptions: noop,
refreshNpcChatOptions: () => false,
handleSceneTransitionChoice: noop,
handleNpcChatInput: () => false,
exitNpcChat: () => false,
characterChatUi: {} as RpgRuntimeStageRouterProps['characterChatUi'],
inventoryUi: {} as RpgRuntimeStageRouterProps['inventoryUi'],
battleRewardUi: {} as RpgRuntimeStageRouterProps['battleRewardUi'],
questUi: {} as RpgRuntimeStageRouterProps['questUi'],
npcChatQuestOfferUi:
{} as RpgRuntimeStageRouterProps['npcChatQuestOfferUi'],
goalUi: {} as RpgRuntimeStageRouterProps['goalUi'],
companionRenderStates: [],
characterChatSummaries: {},
openOverlayPanel: noop,
openCampModal: noop,
openPartyMemberDetails: noop,
adventureStatistics: {
playTimeMs: 0,
hostileNpcsDefeated: 0,
questsAccepted: 0,
questsCompleted: 0,
questsTurnedIn: 0,
itemsUsed: 0,
scenesTraveled: 0,
currentSceneName: '测试场景',
playerCurrency: 0,
inventoryItemCount: 0,
inventoryStackCount: 0,
activeCompanionCount: 0,
rosterCompanionCount: 0,
},
musicVolume: 0.5,
onMusicVolumeChange: noop,
resetForSaveAndExit: noop,
handleSaveAndExit: noop,
...overrides,
};
}
test('renders the main content loading fallback with PlatformSubpanel chrome', async () => {
render(<RpgRuntimeStageRouter {...buildProps()} />);
const loadingLabel = screen.getByText('正在加载平台首页...');
const panel = loadingLabel.closest('.platform-subpanel');
expect(panel).not.toBeNull();
expect(panel?.className).toContain('rounded-[1rem]');
expect(panel?.className).toContain('px-5 py-4');
expect(panel?.className).toContain('text-zinc-300');
await screen.findByText('平台首页');
});
@@ -19,6 +19,7 @@ import type {
StoryOption,
} from '../../types';
import { UI_CHROME } from '../../uiAssets';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import type { SelectionStage } from '../platform-entry/platformEntryTypes';
import type { RpgAdventureStatistics } from './types';
@@ -47,9 +48,14 @@ const RpgRuntimePanelRouter = lazy(async () => {
function MainContentLoadingFallback({ label }: { label: string }) {
return (
<div className="flex h-full min-h-0 items-center justify-center">
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-zinc-300">
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-5 py-4 text-sm text-zinc-300"
>
{label}
</div>
</PlatformSubpanel>
</div>
);
}