Files
Genarrative/src/components/edutainment-result/BabyObjectMatchResultView.tsx
kdletters 1ad25e30f8 收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
2026-06-10 10:24:18 +08:00

253 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
ArrowLeft,
CheckCircle2,
Loader2,
Play,
RefreshCw,
Save,
Tag,
} from 'lucide-react';
import { useMemo } from 'react';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import {
BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
hasBabyObjectMatchRequiredTag,
normalizeBabyObjectMatchTags,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
import { PlatformOverlayBadge } from '../common/PlatformOverlayBadge';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
type BabyObjectMatchResultViewProps = {
draft: BabyObjectMatchDraft;
isBusy?: boolean;
error?: string | null;
onBack: () => void;
onSaveDraft?: (draft: BabyObjectMatchDraft) => void;
onPublish?: (draft: BabyObjectMatchDraft) => void;
onStartTestRun?: (draft: BabyObjectMatchDraft) => void;
onRegenerateAssets?: (draft: BabyObjectMatchDraft) => void;
};
function normalizeDraftForAction(draft: BabyObjectMatchDraft) {
return {
...draft,
themeTags: normalizeBabyObjectMatchTags(draft.themeTags),
updatedAt: new Date().toISOString(),
};
}
const REQUIRED_VISUAL_ASSET_KINDS = [
'background',
'gift-box',
'basket',
] as const;
export function BabyObjectMatchResultView({
draft,
isBusy = false,
error = null,
onBack,
onSaveDraft,
onPublish,
onStartTestRun,
onRegenerateAssets,
}: BabyObjectMatchResultViewProps) {
const normalizedDraft = useMemo(
() => normalizeDraftForAction(draft),
[draft],
);
const hasGeneratedAssets =
normalizedDraft.itemAssets.every(
(asset) =>
asset.generationProvider === 'vector-engine-gpt-image-2' &&
asset.imageSrc.startsWith('data:image/png;base64,'),
) &&
Boolean(normalizedDraft.visualPackage) &&
REQUIRED_VISUAL_ASSET_KINDS.every((kind) =>
normalizedDraft.visualPackage!.assets.some(
(asset) =>
asset.assetKind === kind &&
asset.generationProvider === 'vector-engine-gpt-image-2' &&
asset.imageSrc.startsWith('data:image/png;base64,'),
),
);
const publishReady =
normalizedDraft.itemNames.every((itemName) => itemName.trim()) &&
normalizedDraft.itemAssets.every((asset) => asset.imageSrc.trim()) &&
hasBabyObjectMatchRequiredTag(normalizedDraft.themeTags) &&
hasGeneratedAssets;
const isPublished = normalizedDraft.publicationStatus === 'published';
return (
<div className="platform-page-stage platform-remap-surface flex h-full min-h-0 flex-col overflow-hidden px-3 pb-3 pt-3 sm:px-4 sm:pt-4 xl:px-5 xl:pb-4 xl:pt-4">
<div className="mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col">
<div className="mb-3 flex shrink-0 items-center justify-between gap-3">
<PlatformActionButton
onClick={onBack}
disabled={isBusy}
tone="ghost"
size="xs"
className="min-h-0 gap-2 px-3 py-1.5 text-[11px]"
>
<ArrowLeft className="h-3.5 w-3.5" />
</PlatformActionButton>
<div className="flex min-w-0 items-center gap-2">
<PlatformPillBadge
tone={isPublished ? 'success' : 'neutral'}
size="xs"
>
{isPublished ? '已发布' : '草稿'}
</PlatformPillBadge>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto pr-0.5">
<section className="grid gap-3 lg:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
title="模板"
titleVariant="strong"
className="bg-white/68 shadow-[inset_0_1px_0_rgba(255,255,255,0.74)]"
>
<h1 className="mt-2 m-0 text-3xl font-black leading-tight tracking-normal text-[var(--platform-text-strong)] sm:text-5xl">
{normalizedDraft.workTitle}
</h1>
<div className="mt-4 flex flex-wrap gap-2">
{normalizedDraft.themeTags.map((tag) => (
<PlatformPillBadge
key={tag}
tone={
tag === BABY_OBJECT_MATCH_EDUTAINMENT_TAG
? 'success'
: 'neutral'
}
icon={<Tag className="h-3 w-3" />}
>
{tag}
</PlatformPillBadge>
))}
</div>
</PlatformSubpanel>
<div className="grid gap-3 sm:grid-cols-2">
{normalizedDraft.itemAssets.map((asset) => (
<PlatformSubpanel
as="article"
key={asset.itemId}
surface="flat"
radius="sm"
padding="none"
className="overflow-hidden shadow-[inset_0_1px_0_rgba(255,255,255,0.76)]"
>
<PlatformMediaFrame
src={asset.imageSrc}
alt={asset.itemName}
fallbackLabel={asset.itemName}
aspect="square"
surface="none"
loading="lazy"
imageClassName="absolute inset-0 h-full w-full object-cover"
className="rounded-none bg-[linear-gradient(145deg,rgba(236,253,245,0.92),rgba(255,247,237,0.86))]"
fallbackClassName="tracking-normal text-[var(--platform-text-soft)]"
previewOverlay={
asset.generationProvider === 'placeholder' ? (
<PlatformOverlayBadge
placement="topRight"
offset="tight"
tone="muted"
size="compact"
>
</PlatformOverlayBadge>
) : null
}
/>
<div className="p-3">
<div className="truncate text-lg font-black text-[var(--platform-text-strong)]">
{asset.itemName}
</div>
</div>
</PlatformSubpanel>
))}
</div>
</section>
{error ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="md"
className="mt-3"
>
{error}
</PlatformStatusMessage>
) : null}
{!hasGeneratedAssets ? (
<PlatformStatusMessage
tone="neutral"
surface="platform"
size="md"
className="mt-3 rounded-2xl"
>
</PlatformStatusMessage>
) : null}
</div>
<div className="mt-3 grid shrink-0 gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:grid-cols-4">
<PlatformActionButton
disabled={isBusy || !onSaveDraft}
onClick={() => onSaveDraft?.(normalizedDraft)}
tone="secondary"
>
<Save className="h-4 w-4" />
稿
</PlatformActionButton>
<PlatformActionButton
disabled={isBusy || !onRegenerateAssets}
onClick={() => onRegenerateAssets?.(normalizedDraft)}
tone="secondary"
>
{isBusy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</PlatformActionButton>
<PlatformActionButton
disabled={isBusy || !hasGeneratedAssets || !onStartTestRun}
onClick={() => onStartTestRun?.(normalizedDraft)}
tone="secondary"
>
<Play className="h-4 w-4" />
</PlatformActionButton>
<PlatformActionButton
disabled={isBusy || !publishReady || !onPublish}
onClick={() => onPublish?.(normalizedDraft)}
>
{isBusy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<CheckCircle2 className="h-4 w-4" />
)}
</PlatformActionButton>
</div>
</div>
</div>
);
}
export default BabyObjectMatchResultView;