继续收口平台空态与动作按钮

作品架异步状态切换复用 PlatformAsyncStatePanel
复制反馈动作外观改为组合 PlatformActionButton
结果页与调试面板空态继续收口到 PlatformEmptyState
暗色私聊与工坊按钮改为复用 PlatformActionButton
更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-11 01:41:15 +08:00
parent 0a4ccdf45c
commit 06bf03a28c
15 changed files with 202 additions and 130 deletions

View File

@@ -847,6 +847,31 @@ test('creation hub works-only tab filters bark battle draft and published works'
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(barkBattlePublishedItem);
});
test('creation hub keeps filtered empty copy when selected tab has no works', async () => {
const user = userEvent.setup();
render(
<CustomWorldCreationHub
mode="works-only"
items={[]}
barkBattleItems={[barkBattlePublishedItem]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
await user.click(screen.getByRole('tab', { name: '草稿 0' }));
expect(screen.getByText('当前筛选下没有内容')).toBeTruthy();
expect(screen.queryByText('还没有作品')).toBeNull();
});
test('creation hub published work delete action stays in revealed side actions', async () => {
const user = userEvent.setup();
const onDeletePuzzle = vi.fn();

View File

@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
import { resolveSelectionStageFromPath } from '../../routing/appPageRoutes';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import type { PublishShareModalPayload } from '../common/publishShareModalModel';
@@ -206,6 +207,7 @@ export function CustomWorldCreationHub({
const recentCreationTypeIds = [
...new Set(recentWorkItems.map((item) => item.kind)),
];
const isWorkShelfEmpty = !loading && filteredItems.length === 0;
function handleOpenShelfItem(item: CreationWorkShelfItem) {
onOpenShelfItem?.(item);
@@ -238,6 +240,55 @@ export function CustomWorldCreationHub({
const showStartCard = mode !== 'works-only';
const showWorkShelf = mode !== 'start-only';
const workShelfLoadingState = (
<div className={WORK_GRID_CLASS}>
{Array.from({ length: 3 }).map((_, index) => (
<PlatformSubpanel
as="div"
key={`skeleton-${index}`}
padding="sm"
radius="md"
className="min-h-[10.5rem] sm:min-h-[12rem] sm:p-5"
>
<div className="h-4 w-20 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-5 h-6 w-24 rounded-full bg-[var(--platform-track-fill)] sm:mt-6 sm:h-8 sm:w-36" />
<div className="mt-3 h-3 w-full rounded-full bg-[var(--platform-track-fill)] sm:mt-4 sm:h-4" />
<div className="mt-2 h-4 w-4/5 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-6 flex flex-col gap-2 sm:mt-8 sm:flex-row">
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
</div>
</PlatformSubpanel>
))}
</div>
);
const workShelfEmptyState = (
<EmptyState
title={shelfItems.length === 0 ? '还没有作品' : '当前筛选下没有内容'}
/>
);
const workShelfContent = (
<div className={WORK_GRID_CLASS}>
{filteredItems.map((item) => (
<CustomWorldWorkCard
key={`${item.kind}-${item.id}`}
item={item}
previousMetricValues={metricSnapshot[buildWorkMetricCacheItemKey(item)]}
onOpen={() => {
handleOpenShelfItem(item);
}}
onDelete={buildDeleteAction(item)}
deleteBusy={deletingWorkId === item.id}
onShare={buildShareAction(item)}
onClaimPointIncentive={buildPointIncentiveAction(item)}
pointIncentiveBusy={
item.source.kind === 'puzzle' &&
claimingPuzzleProfileId === item.source.item.profileId
}
/>
))}
</div>
);
return (
<div className="platform-remap-surface w-full space-y-4 px-3 pb-4 pt-3 sm:px-4 sm:pt-4 xl:px-5 xl:pb-5 xl:pt-5">
@@ -276,55 +327,14 @@ export function CustomWorldCreationHub({
) : null}
{showWorkShelf ? (
loading ? (
<div className={WORK_GRID_CLASS}>
{Array.from({ length: 3 }).map((_, index) => (
<PlatformSubpanel
as="div"
key={`skeleton-${index}`}
padding="sm"
radius="md"
className="min-h-[10.5rem] sm:min-h-[12rem] sm:p-5"
>
<div className="h-4 w-20 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-5 h-6 w-24 rounded-full bg-[var(--platform-track-fill)] sm:mt-6 sm:h-8 sm:w-36" />
<div className="mt-3 h-3 w-full rounded-full bg-[var(--platform-track-fill)] sm:mt-4 sm:h-4" />
<div className="mt-2 h-4 w-4/5 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-6 flex flex-col gap-2 sm:mt-8 sm:flex-row">
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
</div>
</PlatformSubpanel>
))}
</div>
) : filteredItems.length > 0 ? (
<div className={WORK_GRID_CLASS}>
{filteredItems.map((item) => (
<CustomWorldWorkCard
key={`${item.kind}-${item.id}`}
item={item}
previousMetricValues={
metricSnapshot[buildWorkMetricCacheItemKey(item)]
}
onOpen={() => {
handleOpenShelfItem(item);
}}
onDelete={buildDeleteAction(item)}
deleteBusy={deletingWorkId === item.id}
onShare={buildShareAction(item)}
onClaimPointIncentive={buildPointIncentiveAction(item)}
pointIncentiveBusy={
item.source.kind === 'puzzle' &&
claimingPuzzleProfileId === item.source.item.profileId
}
/>
))}
</div>
) : shelfItems.length === 0 ? (
<EmptyState title="还没有作品" />
) : (
<EmptyState title="当前筛选下没有内容" />
)
<PlatformAsyncStatePanel
isLoading={loading}
loadingState={workShelfLoadingState}
isEmpty={isWorkShelfEmpty}
emptyState={workShelfEmptyState}
>
{workShelfContent}
</PlatformAsyncStatePanel>
) : null}
</div>
</div>