合并泥点弹窗透明修复
# Conflicts: # src/components/common/PublishShareModal.test.tsx # src/components/common/PublishShareModal.tsx # src/index.test.ts
This commit is contained in:
@@ -547,7 +547,14 @@ test('creation hub shows puzzle point incentive and claims without opening card'
|
||||
expect(screen.getByLabelText('积分激励总数 2.5 泥点')).toBeTruthy();
|
||||
expect(screen.getByLabelText('待领取积分 1 泥点')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '领取积分' }));
|
||||
const claimButton = screen.getByRole('button', { name: '领取积分' });
|
||||
|
||||
expect(claimButton.className).toContain('platform-button');
|
||||
expect(claimButton.className).toContain(
|
||||
'creation-work-card-incentive__button',
|
||||
);
|
||||
|
||||
await user.click(claimButton);
|
||||
|
||||
expect(onClaimPuzzlePointIncentive).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ profileId: 'puzzle-profile-incentive' }),
|
||||
@@ -840,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();
|
||||
@@ -1135,7 +1167,13 @@ test('creation hub published share icon is shown directly on the card header', (
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '分享' })).toBeTruthy();
|
||||
const shareButton = screen.getByRole('button', { name: '分享' });
|
||||
|
||||
expect(shareButton).toBeTruthy();
|
||||
expect(shareButton.className).toContain('platform-icon-button');
|
||||
expect(shareButton.className).toContain(
|
||||
'creation-work-card__quick-action-button',
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -191,6 +191,14 @@ test('creation hub draft card renders compiled work summary fields', () => {
|
||||
onEnterPublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
getWorkState={(item) =>
|
||||
item.kind === 'rpg'
|
||||
? {
|
||||
hasGenerationFailure: true,
|
||||
generationFailureSummary: '生成失败',
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -960,9 +968,128 @@ test('creation hub published work uses unified list card layout', () => {
|
||||
expect(html).toContain('creation-work-list');
|
||||
expect(html).toContain('platform-category-game-item');
|
||||
expect(html).toContain('creation-work-card__side-cover');
|
||||
expect(html).toContain('creation-work-card__badge');
|
||||
expect(html).toContain('border-[var(--platform-subpanel-border)]');
|
||||
expect(html).not.toContain('platform-pill');
|
||||
expect(html).not.toContain('col-span-2 sm:col-span-1');
|
||||
});
|
||||
|
||||
test('creation hub failed draft badge reuses PlatformPillBadge danger chrome', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
mode="works-only"
|
||||
shelfItems={[
|
||||
{
|
||||
id: 'failed-card',
|
||||
kind: 'bark-battle',
|
||||
status: 'draft',
|
||||
hasGenerationFailure: true,
|
||||
generationFailureSummary: '生成失败',
|
||||
title: '失败但仍可恢复的草稿',
|
||||
summary: '失败草稿也来自真实作品架摘要。',
|
||||
authorDisplayName: '测试作者',
|
||||
updatedAt: buildUpdatedAtDaysAgo(1),
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
publicWorkCode: null,
|
||||
sharePath: null,
|
||||
openActionLabel: '继续创作',
|
||||
canDelete: false,
|
||||
canShare: false,
|
||||
badges: [
|
||||
{ id: 'status', label: '草稿', tone: 'neutral' },
|
||||
{ id: 'type', label: '汪汪', tone: 'neutral' },
|
||||
],
|
||||
metrics: [],
|
||||
actions: { open: () => {} },
|
||||
source: {
|
||||
kind: 'bark-battle',
|
||||
item: {
|
||||
workId: 'failed-card',
|
||||
draftId: 'failed-draft',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
title: '失败但仍可恢复的草稿',
|
||||
summary: '失败草稿也来自真实作品架摘要。',
|
||||
themeDescription: '公园舞台',
|
||||
playerImageDescription: '柯基选手',
|
||||
opponentImageDescription: '哈士奇对手',
|
||||
playerCharacterImageSrc: null,
|
||||
opponentCharacterImageSrc: null,
|
||||
uiBackgroundImageSrc: null,
|
||||
difficultyPreset: 'normal',
|
||||
status: 'draft',
|
||||
generationStatus: 'failed',
|
||||
publishReady: false,
|
||||
playCount: 0,
|
||||
updatedAt: buildUpdatedAtDaysAgo(1),
|
||||
publishedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
items={[]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain('creation-work-card__failure-status');
|
||||
expect(html).toContain('border-[var(--platform-button-danger-border)]');
|
||||
expect(html).toContain('失败草稿也来自真实作品架摘要。');
|
||||
});
|
||||
|
||||
test('creation hub empty shelf reuses PlatformEmptyState chrome', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
mode="works-only"
|
||||
items={[]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain('还没有作品');
|
||||
expect(html).toContain('platform-surface platform-surface--soft');
|
||||
expect(html).toContain('min-h-[14rem]');
|
||||
expect(html).not.toContain('platform-subpanel flex min-h-[14rem]');
|
||||
});
|
||||
|
||||
test('creation hub loading shelf reuses PlatformSubpanel skeleton cards', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
mode="works-only"
|
||||
items={[]}
|
||||
loading
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html.match(/platform-subpanel/g)?.length).toBe(3);
|
||||
expect(html).toContain('min-h-[10.5rem]');
|
||||
expect(html).toContain('rounded-[1.25rem]');
|
||||
expect(html).not.toContain('rounded-[1.2rem]');
|
||||
});
|
||||
|
||||
test('creation hub draft cards use cover background and hide updated time', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
|
||||
@@ -2,6 +2,10 @@ 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';
|
||||
import type {
|
||||
PlatformCreationTypeCard,
|
||||
@@ -12,9 +16,7 @@ import {
|
||||
type CreationWorkShelfMetricId,
|
||||
getCreationWorkShelfItemTime,
|
||||
} from './creationWorkShelf';
|
||||
import {
|
||||
CustomWorldCreationStartCard,
|
||||
} from './CustomWorldCreationStartCard';
|
||||
import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard';
|
||||
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
|
||||
import {
|
||||
type CustomWorldWorkFilter,
|
||||
@@ -53,11 +55,14 @@ type CustomWorldCreationHubProps = {
|
||||
|
||||
function EmptyState({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="platform-subpanel flex min-h-[14rem] flex-col items-center justify-center rounded-[1.6rem] px-6 py-8 text-center">
|
||||
<div className="text-lg font-semibold text-[var(--platform-text-strong)]">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<PlatformEmptyState
|
||||
surface="soft"
|
||||
size="panel"
|
||||
tone="base"
|
||||
className="font-semibold text-[var(--platform-text-strong)]"
|
||||
>
|
||||
{title}
|
||||
</PlatformEmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,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);
|
||||
@@ -234,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">
|
||||
@@ -260,63 +315,26 @@ export function CustomWorldCreationHub({
|
||||
|
||||
{showWorkShelf && error ? (
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onRetry}
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full px-4 py-2 text-sm"
|
||||
tone="ghost"
|
||||
shape="pill"
|
||||
className="min-h-0 py-2"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showWorkShelf ? (
|
||||
loading ? (
|
||||
<div className={WORK_GRID_CLASS}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
key={`skeleton-${index}`}
|
||||
className="platform-subpanel min-h-[10.5rem] rounded-[1.2rem] p-3 sm:min-h-[12rem] sm:rounded-[1.6rem] 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>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
CreationEntryConfig,
|
||||
CreationEntryEventBannerConfig,
|
||||
} from '../../services/creationEntryConfigService';
|
||||
import { PlatformPillTabRail } from '../common/PlatformSegmentedTabPresets';
|
||||
import {
|
||||
groupVisiblePlatformCreationTypes,
|
||||
type PlatformCreationTypeCard,
|
||||
@@ -111,6 +112,23 @@ export function CustomWorldCreationStartCard({
|
||||
const visibleCreationTypes = isRecentTabActive
|
||||
? recentCreationTypes
|
||||
: (activeGroup?.items ?? []);
|
||||
const categoryTabs = useMemo(
|
||||
() => [
|
||||
...(hasRecentCreationTypes
|
||||
? [
|
||||
{
|
||||
id: CREATION_ENTRY_RECENT_TAB_ID,
|
||||
label: '最近创作',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...creationTypeGroups.map((group) => ({
|
||||
id: group.id,
|
||||
label: group.label,
|
||||
})),
|
||||
],
|
||||
[creationTypeGroups, hasRecentCreationTypes],
|
||||
);
|
||||
const eventBanners = useMemo(
|
||||
() => resolveCreationEntryEventBanners(entryConfig),
|
||||
[entryConfig],
|
||||
@@ -262,52 +280,12 @@ export function CustomWorldCreationStartCard({
|
||||
</section>
|
||||
|
||||
<section className="creation-template-list -mx-1 px-1 sm:-mx-2 sm:px-2">
|
||||
<div
|
||||
className="-mx-0.5 flex snap-x items-center gap-2 overflow-x-auto px-0.5 pb-1 scrollbar-hide scroll-px-2 sm:gap-3"
|
||||
role="tablist"
|
||||
aria-label="创作入口页签"
|
||||
>
|
||||
{hasRecentCreationTypes ? (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={isRecentTabActive}
|
||||
onClick={() => setActiveCategoryId(CREATION_ENTRY_RECENT_TAB_ID)}
|
||||
className={`relative min-h-8 shrink-0 rounded-full px-2.5 text-xs font-black transition sm:min-h-9 sm:px-3.5 sm:text-sm ${
|
||||
isRecentTabActive
|
||||
? 'text-[#6f2f21]'
|
||||
: 'text-[#7a6558] hover:text-[#6f2f21]'
|
||||
}`}
|
||||
>
|
||||
<span>最近创作</span>
|
||||
{isRecentTabActive ? (
|
||||
<span className="absolute bottom-0 left-3 right-3 h-1 rounded-full bg-[#d9793f]" />
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
{creationTypeGroups.map((group) => {
|
||||
const selected = group.id === activeGroup?.id;
|
||||
return (
|
||||
<button
|
||||
key={group.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={selected}
|
||||
onClick={() => setActiveCategoryId(group.id)}
|
||||
className={`relative min-h-8 shrink-0 rounded-full px-2.5 text-xs font-black transition sm:min-h-9 sm:px-3.5 sm:text-sm ${
|
||||
selected
|
||||
? 'text-[#6f2f21]'
|
||||
: 'text-[#7a6558] hover:text-[#6f2f21]'
|
||||
}`}
|
||||
>
|
||||
<span>{group.label}</span>
|
||||
{selected ? (
|
||||
<span className="absolute bottom-0 left-3 right-3 h-1 rounded-full bg-[#d9793f]" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<PlatformPillTabRail
|
||||
items={categoryTabs}
|
||||
activeId={activeTabId ?? ''}
|
||||
onChange={setActiveCategoryId}
|
||||
ariaLabel="创作入口页签"
|
||||
/>
|
||||
|
||||
{isRecentTabActive ? (
|
||||
<div className="creation-template-list__recent-window mt-2 text-[11px] font-bold leading-4 text-[#8b6654] sm:text-xs">
|
||||
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
|
||||
import {
|
||||
formatPlatformWorkDisplayName,
|
||||
@@ -44,10 +47,17 @@ type CustomWorldWorkCardProps = {
|
||||
pointIncentiveBusy?: boolean;
|
||||
};
|
||||
|
||||
const BADGE_TONE_CLASS: Record<CreationWorkShelfBadgeTone, string> = {
|
||||
warm: 'platform-pill--warm',
|
||||
success: 'platform-pill--success',
|
||||
neutral: 'platform-pill--neutral',
|
||||
type WorkCardPillBadgeTone = React.ComponentProps<
|
||||
typeof PlatformPillBadge
|
||||
>['tone'];
|
||||
|
||||
const BADGE_TONE_CLASS: Record<
|
||||
CreationWorkShelfBadgeTone,
|
||||
NonNullable<WorkCardPillBadgeTone>
|
||||
> = {
|
||||
warm: 'warning',
|
||||
success: 'success',
|
||||
neutral: 'neutral',
|
||||
};
|
||||
|
||||
const METRIC_ANIMATION_DURATION_MS = 820;
|
||||
@@ -627,8 +637,9 @@ export function CustomWorldWorkCard({
|
||||
</div>
|
||||
{canUseShareAction ? (
|
||||
<div className="creation-work-card__quick-actions">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformIconButton
|
||||
label="分享"
|
||||
icon={<Share2 aria-hidden="true" className="h-4 w-4" />}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
suppressOpenRef.current = false;
|
||||
@@ -645,23 +656,22 @@ export function CustomWorldWorkCard({
|
||||
event.stopPropagation();
|
||||
}}
|
||||
title="分享作品"
|
||||
aria-label="分享"
|
||||
className="creation-work-card__quick-action-button"
|
||||
>
|
||||
<Share2 aria-hidden="true" className="h-4 w-4" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="creation-work-card__meta platform-category-game-item__meta">
|
||||
{item.badges.slice(1).map((badge) => (
|
||||
<span
|
||||
<PlatformPillBadge
|
||||
key={`${item.id}-${badge.id}`}
|
||||
className={`creation-work-card__badge platform-pill ${BADGE_TONE_CLASS[badge.tone]}`}
|
||||
tone={BADGE_TONE_CLASS[badge.tone]}
|
||||
size="xs"
|
||||
className="creation-work-card__badge"
|
||||
>
|
||||
{formatPlatformWorkDisplayTag(badge.label)}
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -670,13 +680,15 @@ export function CustomWorldWorkCard({
|
||||
</div>
|
||||
|
||||
{item.hasGenerationFailure ? (
|
||||
<div
|
||||
<PlatformPillBadge
|
||||
tone="danger"
|
||||
size="xs"
|
||||
aria-label={item.generationFailureSummary ?? '生成失败'}
|
||||
className="creation-work-card__failure-status"
|
||||
icon={<CircleAlert aria-hidden="true" className="h-3.5 w-3.5" />}
|
||||
>
|
||||
<CircleAlert aria-hidden="true" className="h-3.5 w-3.5" />
|
||||
<span>{item.generationFailureSummary ?? '生成失败'}</span>
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
) : null}
|
||||
|
||||
{isPublished ? (
|
||||
@@ -709,8 +721,9 @@ export function CustomWorldWorkCard({
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="xxs"
|
||||
disabled={!canClaimPointIncentive || pointIncentiveBusy}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
@@ -722,7 +735,7 @@ export function CustomWorldWorkCard({
|
||||
className="creation-work-card-incentive__button"
|
||||
>
|
||||
{pointIncentiveBusy ? '领取中' : '领取积分'}
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PlatformUnderlineTabRail } from '../common/PlatformSegmentedTabPresets';
|
||||
|
||||
export type CustomWorldWorkFilter = 'all' | 'draft' | 'published';
|
||||
|
||||
const FILTER_OPTIONS: Array<{
|
||||
@@ -22,33 +24,27 @@ export function CustomWorldWorkTabs({
|
||||
publishedCount,
|
||||
onChange,
|
||||
}: CustomWorldWorkTabsProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex min-w-0 items-center gap-4 overflow-x-auto pb-1 scrollbar-hide xl:pb-0"
|
||||
role="tablist"
|
||||
aria-label="作品筛选"
|
||||
>
|
||||
{FILTER_OPTIONS.map((option) => {
|
||||
const count =
|
||||
option.id === 'draft'
|
||||
? draftCount
|
||||
: option.id === 'published'
|
||||
? publishedCount
|
||||
: draftCount + publishedCount;
|
||||
const filterTabs = FILTER_OPTIONS.map((option) => {
|
||||
const count =
|
||||
option.id === 'draft'
|
||||
? draftCount
|
||||
: option.id === 'published'
|
||||
? publishedCount
|
||||
: draftCount + publishedCount;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeFilter === option.id}
|
||||
onClick={() => onChange(option.id)}
|
||||
className={`platform-mobile-home-channel shrink-0 ${activeFilter === option.id ? 'platform-mobile-home-channel--active' : ''}`}
|
||||
>
|
||||
{option.label} {count}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return {
|
||||
id: option.id,
|
||||
label: `${option.label} ${count}`,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PlatformUnderlineTabRail
|
||||
items={filterTabs}
|
||||
activeId={activeFilter}
|
||||
onChange={onChange}
|
||||
ariaLabel="作品筛选"
|
||||
className="pb-1 !gap-4 xl:pb-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user