Files
Genarrative/src/components/puzzle-clear-result/PuzzleClearResultView.tsx
kdletters 0d9259b762 继续沉淀结果页返回按钮
新增共享 PlatformBackActionButton 承接结果页轻量返回入口
将拼图方洞拼消消视觉小说等结果页返回按钮收口到共享组件
将拼消消跳一跳敲木鱼宝贝识物结果页返回按钮收口到共享组件
补充对应测试并更新 PlatformUiKit 收口计划与共享决策记录
2026-06-11 04:52:48 +08:00

216 lines
7.2 KiB
TypeScript

import { Loader2, Play, RefreshCcw, Send } from 'lucide-react';
import { useState } from 'react';
import type {
PuzzleClearDraftResponse,
PuzzleClearWorkProfileResponse,
} from '../../../packages/shared/src/contracts/puzzleClear';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformBackActionButton } from '../common/PlatformBackActionButton';
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
import { PlatformMediaTileGrid } from '../common/PlatformMediaTileGrid';
import { PlatformStatGrid } from '../common/PlatformStatGrid';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
type PuzzleClearResultViewProps = {
profile: PuzzleClearDraftResponse | PuzzleClearWorkProfileResponse;
isBusy?: boolean;
error?: string | null;
onBack: () => void;
onEdit: () => void;
onStartTestRun: () => void;
onPublish: () => void;
onRegenerateAtlas: () => void;
};
function isPuzzleClearWorkProfile(
profile: PuzzleClearResultViewProps['profile'],
): profile is PuzzleClearWorkProfileResponse {
return 'summary' in profile;
}
function getDraft(profile: PuzzleClearResultViewProps['profile']) {
return isPuzzleClearWorkProfile(profile) ? profile.draft : profile;
}
export function PuzzleClearResultView({
profile,
isBusy = false,
error = null,
onBack,
onEdit,
onStartTestRun,
onPublish,
onRegenerateAtlas,
}: PuzzleClearResultViewProps) {
const [isPublishing, setIsPublishing] = useState(false);
const isWorkProfile = isPuzzleClearWorkProfile(profile);
const draft = getDraft(profile);
const summary = isWorkProfile ? profile.summary : null;
const title =
summary?.workTitle?.trim() || draft.workTitle.trim() || '拼消消';
const description =
summary?.workDescription?.trim() || draft.workDescription.trim();
const boardBackgroundAsset = isWorkProfile
? (profile.boardBackgroundAsset ?? draft.boardBackgroundAsset)
: draft.boardBackgroundAsset;
const atlasAsset = isWorkProfile ? profile.atlasAsset : draft.atlasAsset;
const patternGroups = isWorkProfile
? profile.patternGroups
: draft.patternGroups;
const cardAssets = isWorkProfile ? profile.cardAssets : draft.cardAssets;
const previewCards = cardAssets.slice(0, 24);
const canPublish = Boolean(isWorkProfile && summary?.publishReady);
const handlePublish = async () => {
setIsPublishing(true);
try {
await Promise.resolve(onPublish());
} finally {
setIsPublishing(false);
}
};
return (
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4">
<div className="mb-3 flex items-center justify-between gap-3">
<PlatformBackActionButton
onClick={onBack}
variant="regular"
/>
<PlatformActionButton
onClick={onRegenerateAtlas}
disabled={isBusy}
tone="ghost"
size="xs"
className="min-h-0 gap-2 py-2 text-sm"
>
<RefreshCcw className="h-4 w-4" />
</PlatformActionButton>
</div>
<div className="grid min-h-0 flex-1 gap-3 lg:grid-cols-[minmax(0,1.05fr)_minmax(19rem,0.95fr)]">
<PlatformSubpanel className="flex min-h-0 flex-col" radius="md">
<div className="text-2xl font-black text-[var(--platform-text-strong)]">
{title}
</div>
{description ? (
<div className="mt-2 text-sm leading-6 text-[var(--platform-text-base)]">
{description}
</div>
) : null}
<div className="mt-4 grid min-h-0 flex-1 gap-3 sm:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="overflow-hidden bg-white/80"
>
<PlatformMediaFrame
src={boardBackgroundAsset?.imageSrc}
alt="场地底图"
fallbackLabel="底图"
aspect="portrait"
surface="none"
className="rounded-none"
fallbackClassName="tracking-normal text-[var(--platform-text-soft)]"
/>
</PlatformSubpanel>
<div className="flex min-h-0 flex-col gap-3">
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="overflow-hidden bg-white/80"
>
<PlatformMediaFrame
src={atlasAsset?.imageSrc}
alt="素材图集"
fallbackLabel="图集"
aspect="square"
surface="none"
className="rounded-none"
fallbackClassName="tracking-normal text-[var(--platform-text-soft)]"
/>
</PlatformSubpanel>
<PlatformMediaTileGrid
items={previewCards.map((card) => ({
id: card.cardId,
src: card.imageSrc,
fallbackLabel: '卡片',
}))}
/>
</div>
</div>
</PlatformSubpanel>
<aside className="flex min-h-0 flex-col gap-3 overflow-y-auto">
<PlatformSubpanel>
<PlatformStatGrid
items={[
{ label: '图案组', value: patternGroups.length },
{ label: '卡片', value: cardAssets.length },
{ label: '状态', value: draft.generationStatus },
]}
density="compact"
itemClassName="rounded-[0.9rem] py-3"
/>
</PlatformSubpanel>
{error ? (
<PlatformStatusMessage tone="error" surface="platform" size="md">
{error}
</PlatformStatusMessage>
) : null}
<PlatformSubpanel className="mt-auto">
<div className="grid gap-2">
<PlatformActionButton
onClick={onStartTestRun}
disabled={isBusy || !isWorkProfile}
size="md"
className="min-h-11 gap-2"
>
<Play className="h-4 w-4" />
</PlatformActionButton>
<PlatformActionButton
onClick={handlePublish}
disabled={isBusy || isPublishing || !canPublish}
tone="secondary"
size="md"
className="min-h-11 gap-2"
>
{isPublishing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</PlatformActionButton>
<PlatformActionButton
onClick={onEdit}
disabled={isBusy}
tone="ghost"
size="md"
className="min-h-11"
>
</PlatformActionButton>
</div>
</PlatformSubpanel>
</aside>
</div>
</div>
);
}
export default PuzzleClearResultView;