继续收口首页公开分区异步状态壳

统一首页公开列表与我的创作分区的异步状态切换到 PlatformAsyncStatePanel
补充首页异步状态回归测试覆盖登录态作品分区与桌面公开空态
更新 PlatformUiKit 收口计划与共享决策记录
This commit is contained in:
2026-06-11 02:53:31 +08:00
parent 84ded19f11
commit ae1a15cee0
4 changed files with 132 additions and 65 deletions

View File

@@ -53,6 +53,7 @@
- 2026-06-10 追加:`PlatformStatusDialog` 支持自定义图标、图标可访问标签以及动作按钮 surface / size / className 透传,用来承接玩法结果页里保留品牌视觉但语义仍是“状态结果弹层”的场景;大鱼吃小鱼结果页的发布失败弹层已迁移到这套组件,业务页不再保留 `UnifiedConfirmDialog + PlatformIconBadge` 的专用组合。
- 2026-06-10 追加:`PlatformStatusDialog` 继续支持 header notice 布局、body content、close button、backdrop / Escape 关闭路径,用来承接“提示 / 规则阻断 / 作品不可用 / 泥点不足”这类带标题栏的状态 notice平台入口的 `draftGenerationPointNotice``workNotFoundRecoveryDialog` 和 RPG 大编辑器里的 `EditorNoticeDialog` 已迁移到这套共享组件,不再各自维护 `UnifiedConfirmDialog` 壳层和关闭策略。
- 2026-06-10 追加:`CustomWorldEntityCatalog``minimum-playable` 规则阻断提示也统一迁到 `PlatformStatusDialog`,不再和删除角色 / 批量删除共用 `UnifiedConfirmDialog` 配置;同日平台入口公开编号搜索把 error 分支从用户摘要 modal 中拆出,未命中结果单独走 `PlatformStatusDialog`,命中用户继续保留 `UnifiedModal + PlatformSubpanel` 信息布局。
- 2026-06-11 追加:`PlatformAsyncStatePanel` 继续从 profile modal 与作品架扩展到 RPG 首页公开分区;`RpgEntryHomeView.tsx` 的移动端排行、发现页寓教于乐 / 默认公开 feed、桌面首页“今日游戏 / 推荐”、桌面发现页寓教于乐 / 默认公开 feed以及“我的创作”分区已统一改成 `loadingState / emptyState / children` 三态 slot。页面级 `platformError` 继续留在状态壳外层,保证错误提示可以和内容并存;`recommend runtime`、分类筛选等含运行态或二级筛选语义的分支暂不硬并入这一轮。
- 2026-06-09 追加:通用输入 Composer 的上传参考图、发送和移除参考图已迁移到 `PlatformIconButton`;图标上传仍使用 `asChild="label"` 保留 label + file input 语义,公共组件会自动写入隐藏文本,确保内嵌 file input 继承可访问名称。
- 2026-06-10 追加creation-agent composer 的上传文档 / 上传参考图入口使用 `PlatformIconButton` 默认 `platformIcon`;工作台只保留动态 label、title、busy 状态和 picker 回调,发送按钮继续保留主题色动作布局。验证命令:`npm run test -- src/components/creation-agent/CreationAgentWorkspace.test.tsx src/components/common/PlatformIconButton.test.tsx`
- 2026-06-10 追加:作品详情顶部返回 / 分享和封面轮播上一张 / 下一张入口使用 `PlatformIconButton variant="platformIcon"`;详情页保留原 `platform-work-detail__*` 局部 class 控制位置和尺寸,点赞、复制三态等专用动作暂不迁移。验证命令:`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformIconButton.test.tsx`

View File

@@ -259,6 +259,7 @@
19.3.35. 白底 / 暗色面板里的轻量空态和普通 CTA 继续按共享组件收口:`PuzzleResultView.tsx` 的“还没有可编辑的拼图草稿”、`RpgCreationAssetDebugPanel.tsx` 的“没有可诊断项”、`VisualNovelEntityGrid` 的空实体列表、`AccountModal.tsx` 里账号安全分区的“无安全限制 / 无登录设备 / 无操作记录”以及 `LoginScreen.tsx` 的“当前登录入口暂不可用”都改为 `PlatformEmptyState``Match3DResultView.tsx` 的引用素材列表直接交给 `PlatformAssetPickerGrid` 自己处理空态。`AdventureEntityModal.tsx` 的私聊按钮、`InventoryPanel.tsx` 的锻造 / 合成按钮、`RpgAdventurePanel.tsx` 底部 `队伍 / 背包 / 换一换 / 退出聊天` 按钮,以及 `RpgAdventurePanelOverlays.tsx` 里的“查看任务 / 保存并退出”都改为 `PlatformActionButton surface="editorDark"`,业务页只贴回局部 sky / emerald / runtime 皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel"`;暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`,若还需要 stopPropagation、局部字号或图标排版可保留薄包装层但不要再回退到原生 `<button>` 基础 chrome。验证命令`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/common/PlatformActionButton.test.tsx``npm run check:encoding`
19.3.36. `VisualNovelEntityGrid` 的空态也继续收口到 `PlatformEmptyState surface="subpanel" size="inline"`;角色 / 场景 / 剧情阶段共用这一网格组件后,白底实体列表里的“暂无角色 / 暂无场景 / 暂无剧情阶段”等同构空态不再回退成 `PlatformSubpanel`。同时,`RpgCreationRoleAssetStudioModalImpl.tsx``RpgCreationEntityEditorShared.tsx` 保留局部 `ActionButton` 语义壳,但按钮本体已统一委托给 `PlatformActionButton surface="editorDark"`,只在包装层补最小的 `stopPropagation`、tone 映射和局部 class 适配。后续类似“暗色编辑器局部包装按钮”优先沿用这种薄包装模式,不再直接手写原生 `<button>` 基础 chrome。验证命令`npm run test -- src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/common/PlatformActionButton.test.tsx``npm run check:encoding`
19.3.37. 暗色编辑器里的局部动作按钮继续往共享 `editorDark` button 收口:`CustomWorldNpcVisualEditor.tsx` 的本地 `ActionButton``SkillEffectPreview.tsx` 的“重新预览”按钮都改为委托 `PlatformActionButton surface="editorDark"`。这类局部包装仍可保留 `stopPropagation`、图标布局、`tone` 映射和少量局部视觉覆写,但按钮本体不再直接使用原生 `<button>` 承接边框 / 底色 / hover / disabled chrome。验证命令`npm run test -- src/components/common/PlatformActionButton.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.38. `PlatformAsyncStatePanel` 继续从 profile / 作品架扩展到首页公开分区:`RpgEntryHomeView.tsx` 的移动端排行、发现页寓教于乐 / 默认公开 feed、桌面首页“今日游戏 / 推荐”、桌面发现页寓教于乐 / 默认公开 feed以及“我的创作”分区都统一改成 `loadingState / emptyState / children` 三个 slot 切换。页面继续把 `platformError` 保留在状态壳外层,让错误提示可以和内容并存;`recommend runtime`、分类筛选和其它含二级筛选 / 运行态语义的分支暂不并入这次收口。后续首页、作品架或白底列表若只是纯 `loading / empty / content` 互斥状态,优先直接复用 `PlatformAsyncStatePanel`,不要再把空态与读取态分支手写回业务 JSX。验证命令`npx vitest run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3. creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作、首页激励 CTA 和 prompt suggestion 按钮迁移到 `PlatformIconButton` / `PlatformActionButton`;首页继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏、抽屉和品牌化胶囊视觉,不把视觉回收和语义收口绑成一次大改。`Beta` 徽标和历史记录纯文本行暂保留本地实现,等出现更多同构轻量列表行后再评估是否抽新的共享 row primitive。
19.4. 大鱼吃小鱼结果页 hero 的返回入口迁移到 `PlatformIconButton variant="darkMini"`,测试 / 发布动作迁移到 `PlatformActionButton surface="editorDark"`;结果页只保留测试运行、发布提交和文案状态语义,不再手写 hero 顶栏按钮壳。
19.4.1. 大鱼吃小鱼结果页的发布失败弹层迁移到 `src/components/common/PlatformStatusDialog.tsx``PlatformStatusDialog` 补充自定义图标、可访问标签和动作按钮样式透传后,`BigFishResultView` 不再保留 `BigFishResultErrorModal` 内联的 `UnifiedConfirmDialog + PlatformIconBadge` 组合。结果页只保留失败文案和关闭回调,发布失败的状态图标、遮罩、白底面板和“知道了”主动作统一由共享状态弹层承接。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx``npm run typecheck`

View File

@@ -983,7 +983,11 @@ function renderLoggedInHomeView(
overrides: Partial<
Pick<
RpgEntryHomeViewProps,
'activeTab' | 'hasUnreadDraftUpdate' | 'draftTabContent'
| 'activeTab'
| 'hasUnreadDraftUpdate'
| 'draftTabContent'
| 'myEntries'
| 'isLoadingPlatform'
>
> = {},
) {
@@ -1025,10 +1029,10 @@ function renderLoggedInHomeView(
saveError={null}
featuredEntries={[]}
latestEntries={[]}
myEntries={[]}
myEntries={overrides.myEntries ?? []}
historyEntries={[]}
profileDashboard={null}
isLoadingPlatform={false}
isLoadingPlatform={overrides.isLoadingPlatform ?? false}
isLoadingDashboard={false}
isResumingSaveWorldKey={null}
platformError={null}
@@ -3514,6 +3518,40 @@ test('logged in draft bottom tab shows unread marker', () => {
expect(draftButton.querySelector('.platform-nav-unread-dot')).toBeTruthy();
});
test('logged in saves tab shows loading state while fetching my entries', () => {
renderLoggedInHomeView({
activeTab: 'saves',
isLoadingPlatform: true,
});
expect(screen.getByText('我的创作')).toBeTruthy();
expect(screen.getByText('正在读取你的作品...')).toBeTruthy();
expect(
screen.queryByText('你还没有保存任何自定义世界,先创建一个草稿开始吧。'),
).toBeNull();
});
test('logged in saves tab shows empty state when my entries are missing', () => {
renderLoggedInHomeView({
activeTab: 'saves',
myEntries: [],
});
expect(screen.getByText('我的创作')).toBeTruthy();
expect(
screen.getByText('你还没有保存任何自定义世界,先创建一个草稿开始吧。'),
).toBeTruthy();
expect(screen.queryByText('正在读取你的作品...')).toBeNull();
});
test('desktop home shows empty states when public shelves are unavailable', () => {
mockDesktopLayout();
renderLoggedOutHomeView(vi.fn(), {}, 'home', true);
expect(screen.getByText('今天暂时还没有新游戏。')).toBeTruthy();
expect(screen.getByText('暂时还没有推荐作品。')).toBeTruthy();
});
test('logged in create tab shows real wallet balance beside the brand', () => {
mockNarrowMobileLayout();

View File

@@ -74,6 +74,7 @@ import { shouldShowRechargeEntry } from '../../services/payment/paymentPlatform'
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformAcknowledgeStatusDialog } from '../common/PlatformAcknowledgeStatusDialog';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
import { LegalDocumentModal } from '../common/LegalDocumentModal';
import {
@@ -3539,9 +3540,14 @@ export function RpgEntryHomeView({
}
/>
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : rankingEntries.length > 0 ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={rankingEntries.length === 0}
emptyState={
<PlatformEmptyState>{activeRankingConfig.emptyText}</PlatformEmptyState>
}
>
<div className="mt-3 grid min-w-0 gap-2.5">
{rankingEntries.map((entry, index) => (
<PlatformRankingItem
@@ -3553,9 +3559,7 @@ export function RpgEntryHomeView({
/>
))}
</div>
) : (
<PlatformEmptyState>{activeRankingConfig.emptyText}</PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
);
@@ -3823,11 +3827,18 @@ export function RpgEntryHomeView({
</section>
) : discoverChannel === 'edutainment' ? (
<section className="platform-mobile-home-feed">
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : edutainmentFeedEntries.length > 0 ||
onOpenChildMotionDemo ||
onOpenBabyLoveDrawing ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={
edutainmentFeedEntries.length === 0 &&
!onOpenChildMotionDemo &&
!onOpenBabyLoveDrawing
}
emptyState={
<PlatformEmptyState></PlatformEmptyState>
}
>
<div className="grid min-w-0 gap-3">
{edutainmentFeedEntries.map((entry) => {
const cardKey = buildPublicGalleryCardKey(entry);
@@ -3881,20 +3892,23 @@ export function RpgEntryHomeView({
</button>
) : null}
</div>
) : (
<PlatformEmptyState>
</PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
) : (
<section
ref={mobileDiscoverFeedRef}
className="platform-mobile-home-feed"
>
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : discoverFeedEntries.length > 0 ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={discoverFeedEntries.length === 0}
emptyState={
<PlatformEmptyState>
广
</PlatformEmptyState>
}
>
<div className="grid min-w-0 gap-3">
{discoverFeedEntries.map(
(entry: PlatformPublicGalleryCard) => {
@@ -3918,11 +3932,7 @@ export function RpgEntryHomeView({
},
)}
</div>
) : (
<PlatformEmptyState>
广
</PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
)}
</>
@@ -4043,11 +4053,18 @@ export function RpgEntryHomeView({
) : discoverChannel === 'edutainment' ? (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title={EDUTAINMENT_WORK_TAG} detail="EDUTAINMENT" />
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : edutainmentFeedEntries.length > 0 ||
onOpenChildMotionDemo ||
onOpenBabyLoveDrawing ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={
edutainmentFeedEntries.length === 0 &&
!onOpenChildMotionDemo &&
!onOpenBabyLoveDrawing
}
emptyState={
<PlatformEmptyState></PlatformEmptyState>
}
>
<div className="grid gap-4 xl:grid-cols-3">
{edutainmentFeedEntries.map((entry) => (
<WorldCard
@@ -4096,9 +4113,7 @@ export function RpgEntryHomeView({
</button>
) : null}
</div>
) : (
<PlatformEmptyState></PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
) : (
<section className="platform-desktop-panel px-5 py-5">
@@ -4106,9 +4121,16 @@ export function RpgEntryHomeView({
title={discoverChannel === 'today' ? '今日游戏' : '推荐'}
detail={discoverChannel === 'today' ? 'TODAY GAMES' : 'RECOMMENDED'}
/>
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : discoverFeedEntries.length > 0 ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={discoverFeedEntries.length === 0}
emptyState={
<PlatformEmptyState>
广
</PlatformEmptyState>
}
>
<div className="grid gap-4 xl:grid-cols-3">
{discoverFeedEntries.map((entry) => (
<WorldCard
@@ -4121,11 +4143,7 @@ export function RpgEntryHomeView({
/>
))}
</div>
) : (
<PlatformEmptyState>
广
</PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
)}
</div>
@@ -4181,9 +4199,18 @@ export function RpgEntryHomeView({
{platformError}
</PlatformStatusMessage>
) : null}
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : myEntries.length > 0 ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={myEntries.length === 0}
emptyState={
<PlatformEmptyState>
{isAuthenticated
? '你还没有保存任何自定义世界,先创建一个草稿开始吧。'
: '登录后查看你的作品。'}
</PlatformEmptyState>
}
>
<div className="grid grid-cols-2 gap-2.5 sm:gap-3 xl:grid-cols-3">
{myEntries.map(
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => (
@@ -4201,13 +4228,7 @@ export function RpgEntryHomeView({
),
)}
</div>
) : (
<PlatformEmptyState>
{isAuthenticated
? '你还没有保存任何自定义世界,先创建一个草稿开始吧。'
: '登录后查看你的作品。'}
</PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
</div>
);
@@ -4629,9 +4650,14 @@ export function RpgEntryHomeView({
TODAY
</PlatformPillBadge>
</div>
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : desktopTodayEntries.length > 0 ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={desktopTodayEntries.length === 0}
emptyState={
<PlatformEmptyState></PlatformEmptyState>
}
>
<div className="space-y-3">
{desktopTodayEntries.slice(0, 3).map((entry, index) => (
<DesktopTrendingItem
@@ -4642,9 +4668,7 @@ export function RpgEntryHomeView({
/>
))}
</div>
) : (
<PlatformEmptyState></PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
</div>
@@ -4653,9 +4677,14 @@ export function RpgEntryHomeView({
>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="推荐" detail="RECOMMENDED" />
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : desktopFeaturedGrid.length > 0 ? (
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={desktopFeaturedGrid.length === 0}
emptyState={
<PlatformEmptyState></PlatformEmptyState>
}
>
<div className="grid gap-4 xl:grid-cols-2">
{desktopFeaturedGrid.map((entry) => (
<WorldCard
@@ -4668,9 +4697,7 @@ export function RpgEntryHomeView({
/>
))}
</div>
) : (
<PlatformEmptyState></PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</section>
{desktopLibraryPreview.length > 0 ||