继续收口首页分类异步状态

扩展 PlatformAsyncStatePanel 接入首页分类分支的外层与筛选空态
合并桌面分类 section 并补充首页分类状态回归测试
修正 FlowShell 发现频道切换测试的 tab 语义断言
更新 PlatformUiKit 收口计划与共享决策记录
This commit is contained in:
2026-06-11 03:31:35 +08:00
parent 051fd6156c
commit 58a3c6eb97
5 changed files with 210 additions and 223 deletions

View File

@@ -7561,7 +7561,7 @@ test('published puzzle works appear on home and mobile game category channel', a
});
await clickFirstButtonByName(user, '发现');
await user.click(screen.getByRole('button', { name: '分类' }));
await user.click(screen.getByRole('tab', { name: '分类' }));
const discoverPanel = getPlatformTabPanel('category');
expect(within(discoverPanel).getAllByText('星桥机关').length).toBeGreaterThan(
@@ -8533,7 +8533,7 @@ test('published big fish works stay hidden from platform home and game category
expect(screen.queryByText('机械深海 大鱼吃小鱼')).toBeNull();
await clickFirstButtonByName(user, '发现');
await user.click(screen.getByRole('button', { name: '分类' }));
await user.click(screen.getByRole('tab', { name: '分类' }));
const discoverPanel = getPlatformTabPanel('category');
expect(within(discoverPanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
@@ -8573,7 +8573,7 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
render(<TestWrapper withAuth />);
await clickFirstButtonByName(user, '发现');
await user.click(await screen.findByRole('button', { name: '排行' }));
await user.click(await screen.findByRole('tab', { name: '排行' }));
await waitFor(() => {
expect(document.getElementById('platform-tab-panel-category')).toBeTruthy();
});

View File

@@ -911,6 +911,7 @@ function renderLoggedOutHomeView(
| 'recommendRuntimeError'
| 'onSelectNextRecommendEntry'
| 'onSelectPreviousRecommendEntry'
| 'isLoadingPlatform'
>
> = {},
activeTab: RpgEntryHomeViewProps['activeTab'] = 'home',
@@ -949,7 +950,7 @@ function renderLoggedOutHomeView(
myEntries={[]}
historyEntries={overrides.historyEntries ?? []}
profileDashboard={null}
isLoadingPlatform={false}
isLoadingPlatform={overrides.isLoadingPlatform ?? false}
isLoadingDashboard={false}
isResumingSaveWorldKey={null}
platformError={null}
@@ -5488,6 +5489,52 @@ test('mobile game category filter dialog filters by play type', async () => {
});
});
test('mobile game category keeps filter controls when current filter becomes empty', async () => {
const user = userEvent.setup();
renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
});
await user.click(screen.getByRole('button', { name: '发现' }));
await user.click(screen.getByRole('tab', { name: '分类' }));
await user.click(screen.getByRole('button', { name: '奇幻' }));
await user.click(screen.getByRole('button', { name: //u }));
await user.click(screen.getByRole('button', { name: '抓鹅' }));
expect(screen.getByText('当前筛选下没有作品。')).toBeTruthy();
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
expect(screen.getByRole('button', { name: '奇幻' })).toBeTruthy();
expect(document.querySelector('.platform-category-sort-button')).toBeTruthy();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(document.querySelector('.platform-category-game-item')).toBeNull();
});
test('desktop discover category shows loading state before category data is ready', async () => {
const user = userEvent.setup();
mockDesktopLayout();
renderLoggedOutHomeView(
vi.fn(),
{
isLoadingPlatform: true,
},
'category',
true,
);
const channelBar = document.querySelector('.platform-mobile-home-channelbar');
if (!channelBar) {
throw new Error('缺少发现频道栏');
}
await user.click(within(channelBar as HTMLElement).getByRole('tab', { name: '分类' }));
expect(screen.getByText('正在读取作品分类...')).toBeTruthy();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(document.querySelector('.platform-category-game-item')).toBeNull();
});
test('bottom category tab becomes ranking and switches ranking metrics', async () => {
const user = userEvent.setup();

View File

@@ -3239,6 +3239,149 @@ export function RpgEntryHomeView({
);
const desktopFeaturedGrid = desktopRecommendEntries.slice(0, 4);
const desktopCategoryGrid = activeCategoryEntries.slice(0, 6);
const mobileCategoryPanelContent = activeCategoryGroup ? (
<>
<div className="platform-category-filter-row">
<button
type="button"
onClick={() => setIsCategoryFilterPanelOpen(true)}
aria-haspopup="dialog"
className="platform-category-filter-button"
>
<SlidersHorizontal className="h-4 w-4" />
<span>{categoryFilterApplied ? activeCategoryFilterLabel : '筛选'}</span>
<span className="platform-category-filter-button__count">
{activeCategoryFilterCount}
</span>
</button>
<span className="platform-category-filter-divider" />
<PlatformSegmentedTabs
items={categoryGroupTabs}
activeId={activeCategoryGroup.tag}
onChange={handleCategoryGroupChange}
layout="scroll"
gap="md"
frame="bare"
surface="transparent"
size="sm"
tone="neutral"
className="platform-category-chip-scroll min-w-0 flex-1"
itemClassName={(_, active) =>
[
'platform-category-chip shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
active ? 'platform-category-chip--active' : null,
]
.filter(Boolean)
.join(' ')
}
/>
</div>
<button
type="button"
onClick={cycleCategorySortMode}
className="platform-category-sort-button"
>
<span>{activeCategorySortLabel}</span>
<ChevronDown className="h-3.5 w-3.5" />
</button>
<PlatformAsyncStatePanel
isEmpty={activeCategoryEntries.length === 0}
emptyState={<PlatformEmptyState></PlatformEmptyState>}
>
<div className="platform-category-game-list">
{activeCategoryEntries.map((entry) => (
<PlatformCategoryGameItem
key={`${buildPublicGalleryCardKey(entry)}:mobile-category:${activeCategoryGroup.tag}:${categoryKindFilter}:${categorySortMode}`}
entry={entry}
categoryTag={activeCategoryGroup.tag}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
</PlatformAsyncStatePanel>
</>
) : null;
const renderDesktopCategorySection = (cardKeyPrefix: string) => {
const desktopCategoryPanelContent = activeCategoryGroup ? (
<>
<div className="mb-4 flex min-w-0 items-center gap-2">
<button
type="button"
onClick={() => setIsCategoryFilterPanelOpen(true)}
aria-haspopup="dialog"
className="platform-category-filter-button"
>
<SlidersHorizontal className="h-4 w-4" />
<span>{categoryFilterApplied ? activeCategoryFilterLabel : '筛选'}</span>
<span className="platform-category-filter-button__count">
{activeCategoryFilterCount}
</span>
</button>
<PlatformSegmentedTabs
items={categoryGroupTabs}
activeId={activeCategoryGroup.tag}
onChange={handleCategoryGroupChange}
layout="scroll"
gap="md"
frame="bare"
surface="transparent"
size="sm"
tone="neutral"
className="min-w-0 flex-1 pb-1"
itemClassName={(_, active) =>
[
'platform-category-chip shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
active ? 'platform-category-chip--active' : null,
]
.filter(Boolean)
.join(' ')
}
/>
<button
type="button"
onClick={cycleCategorySortMode}
className="platform-category-sort-button shrink-0"
>
<span>{activeCategorySortLabel}</span>
<ChevronDown className="h-3.5 w-3.5" />
</button>
</div>
<PlatformAsyncStatePanel
isEmpty={desktopCategoryGrid.length === 0}
emptyState={<PlatformEmptyState></PlatformEmptyState>}
>
<div className="grid gap-4 xl:grid-cols-3">
{desktopCategoryGrid.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:${cardKeyPrefix}:${activeCategoryGroup.tag}:${categoryKindFilter}:${categorySortMode}`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
</div>
</PlatformAsyncStatePanel>
</>
) : null;
return (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="作品分类" detail="GAME CATEGORY" />
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={!activeCategoryGroup || activeCategoryRawCount === 0}
emptyState={<PlatformEmptyState></PlatformEmptyState>}
>
{desktopCategoryPanelContent}
</PlatformAsyncStatePanel>
</section>
);
};
const desktopLibraryPreview = myEntries.slice(0, 2);
const resolvedRecommendCoverUrls = useResolvedRecommendCoverImages(
recommendedFeedEntries,
@@ -3753,81 +3896,18 @@ export function RpgEntryHomeView({
mobileRankingPanel
) : discoverChannel === 'category' ? (
<section className="platform-category-list-panel">
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : categoryGroups.length > 0 && activeCategoryGroup ? (
<>
<div className="platform-category-filter-row">
<button
type="button"
onClick={() => setIsCategoryFilterPanelOpen(true)}
aria-haspopup="dialog"
className="platform-category-filter-button"
>
<SlidersHorizontal className="h-4 w-4" />
<span>
{categoryFilterApplied
? activeCategoryFilterLabel
: '筛选'}
</span>
<span className="platform-category-filter-button__count">
{activeCategoryFilterCount}
</span>
</button>
<span className="platform-category-filter-divider" />
<PlatformSegmentedTabs
items={categoryGroupTabs}
activeId={activeCategoryGroup.tag}
onChange={handleCategoryGroupChange}
layout="scroll"
gap="md"
frame="bare"
surface="transparent"
size="sm"
tone="neutral"
className="platform-category-chip-scroll min-w-0 flex-1"
itemClassName={(_, active) =>
[
'platform-category-chip shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
active ? 'platform-category-chip--active' : null,
]
.filter(Boolean)
.join(' ')
}
/>
</div>
<button
type="button"
onClick={cycleCategorySortMode}
className="platform-category-sort-button"
>
<span>{activeCategorySortLabel}</span>
<ChevronDown className="h-3.5 w-3.5" />
</button>
{activeCategoryEntries.length > 0 ? (
<div className="platform-category-game-list">
{activeCategoryEntries.map((entry) => (
<PlatformCategoryGameItem
key={`${buildPublicGalleryCardKey(entry)}:mobile-category:${activeCategoryGroup.tag}:${categoryKindFilter}:${categorySortMode}`}
entry={entry}
categoryTag={activeCategoryGroup.tag}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<PlatformEmptyState>
</PlatformEmptyState>
)}
</>
) : (
<PlatformEmptyState>
广
</PlatformEmptyState>
)}
<PlatformAsyncStatePanel
isLoading={isLoadingPlatform}
loadingState={<PlatformEmptyState>...</PlatformEmptyState>}
isEmpty={!activeCategoryGroup || categoryGroups.length === 0}
emptyState={
<PlatformEmptyState>
广
</PlatformEmptyState>
}
>
{mobileCategoryPanelContent}
</PlatformAsyncStatePanel>
</section>
) : discoverChannel === 'edutainment' ? (
<section className="platform-mobile-home-feed">
@@ -3983,77 +4063,7 @@ export function RpgEntryHomeView({
{discoverChannel === 'ranking' ? (
mobileRankingPanel
) : discoverChannel === 'category' ? (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="作品分类" detail="GAME CATEGORY" />
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : activeCategoryGroup && activeCategoryRawCount > 0 ? (
<>
<div className="mb-4 flex min-w-0 items-center gap-2">
<button
type="button"
onClick={() => setIsCategoryFilterPanelOpen(true)}
aria-haspopup="dialog"
className="platform-category-filter-button"
>
<SlidersHorizontal className="h-4 w-4" />
<span>
{categoryFilterApplied ? activeCategoryFilterLabel : '筛选'}
</span>
<span className="platform-category-filter-button__count">
{activeCategoryFilterCount}
</span>
</button>
<PlatformSegmentedTabs
items={categoryGroupTabs}
activeId={activeCategoryGroup.tag}
onChange={handleCategoryGroupChange}
layout="scroll"
gap="md"
frame="bare"
surface="transparent"
size="sm"
tone="neutral"
className="min-w-0 flex-1 pb-1"
itemClassName={(_, active) =>
[
'platform-category-chip shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
active ? 'platform-category-chip--active' : null,
]
.filter(Boolean)
.join(' ')
}
/>
<button
type="button"
onClick={cycleCategorySortMode}
className="platform-category-sort-button shrink-0"
>
<span>{activeCategorySortLabel}</span>
<ChevronDown className="h-3.5 w-3.5" />
</button>
</div>
{desktopCategoryGrid.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-3">
{desktopCategoryGrid.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-discover-category:${activeCategoryGroup.tag}:${categoryKindFilter}:${categorySortMode}`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
</div>
) : (
<PlatformEmptyState></PlatformEmptyState>
)}
</>
) : (
<PlatformEmptyState></PlatformEmptyState>
)}
</section>
renderDesktopCategorySection('desktop-discover-category')
) : discoverChannel === 'edutainment' ? (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title={EDUTAINMENT_WORK_TAG} detail="EDUTAINMENT" />
@@ -4816,79 +4826,7 @@ export function RpgEntryHomeView({
) : null}
</div>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="作品分类" detail="GAME CATEGORY" />
{isLoadingPlatform ? (
<PlatformEmptyState>...</PlatformEmptyState>
) : activeCategoryGroup && activeCategoryRawCount > 0 ? (
<>
<div className="mb-4 flex min-w-0 items-center gap-2">
<button
type="button"
onClick={() => setIsCategoryFilterPanelOpen(true)}
aria-haspopup="dialog"
className="platform-category-filter-button"
>
<SlidersHorizontal className="h-4 w-4" />
<span>
{categoryFilterApplied
? activeCategoryFilterLabel
: '筛选'}
</span>
<span className="platform-category-filter-button__count">
{activeCategoryFilterCount}
</span>
</button>
<PlatformSegmentedTabs
items={categoryGroupTabs}
activeId={activeCategoryGroup.tag}
onChange={handleCategoryGroupChange}
layout="scroll"
gap="md"
frame="bare"
surface="transparent"
size="sm"
tone="neutral"
className="min-w-0 flex-1 pb-1"
itemClassName={(_, active) =>
[
'platform-category-chip shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
active ? 'platform-category-chip--active' : null,
]
.filter(Boolean)
.join(' ')
}
/>
<button
type="button"
onClick={cycleCategorySortMode}
className="platform-category-sort-button shrink-0"
>
<span>{activeCategorySortLabel}</span>
<ChevronDown className="h-3.5 w-3.5" />
</button>
</div>
{desktopCategoryGrid.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-3">
{desktopCategoryGrid.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-category:${activeCategoryGroup.tag}:${categoryKindFilter}:${categorySortMode}`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
authorSummary={getPublicEntryAuthorSummary(entry)}
/>
))}
</div>
) : (
<PlatformEmptyState></PlatformEmptyState>
)}
</>
) : (
<PlatformEmptyState></PlatformEmptyState>
)}
</section>
{renderDesktopCategorySection('desktop-category')}
</>
)}
</div>