import { useEffect, useMemo, useState } from 'react'; import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent'; import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime'; import type { CustomWorldProfile } from '../../types'; import type { PlatformCreationTypeId } from '../platform-entry/platformEntryCreationTypes'; import { buildCreationWorkShelfItems, type CreationWorkShelfItem, type CreationWorkShelfMetricId, } from './creationWorkShelf'; import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard'; import { CustomWorldWorkCard } from './CustomWorldWorkCard'; import { type CustomWorldWorkFilter, CustomWorldWorkTabs, } from './CustomWorldWorkTabs'; // 中文注释:草稿在手机端保持双列,已发布卡片由卡片自身跨两列展示公开指标。 const WORK_GRID_CLASS = 'grid grid-cols-2 gap-2.5 sm:gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4'; const WORK_METRIC_CACHE_KEY = 'genarrative.creationHub.publishedMetrics.v1'; type WorkMetricSnapshot = Record< string, Partial> >; type CustomWorldCreationHubProps = { items: CustomWorldWorkSummary[]; loading: boolean; error: string | null; onRetry: () => void; createError?: string | null; createBusy?: boolean; onCreateType: (type: PlatformCreationTypeId) => void; onOpenDraft: (item: CustomWorldWorkSummary) => void; onEnterPublished: (profileId: string) => void; onDeletePublished?: ((item: CustomWorldWorkSummary) => void) | null; deletingWorkId?: string | null; rpgLibraryEntries?: CustomWorldLibraryEntry[]; bigFishItems?: BigFishWorkSummary[]; onOpenBigFishDetail?: (item: BigFishWorkSummary) => void; onDeleteBigFish?: ((item: BigFishWorkSummary) => void) | null; match3dItems?: Match3DWorkSummary[]; onOpenMatch3DDetail?: (item: Match3DWorkSummary) => void; onDeleteMatch3D?: ((item: Match3DWorkSummary) => void) | null; squareHoleItems?: SquareHoleWorkSummary[]; onOpenSquareHoleDetail?: (item: SquareHoleWorkSummary) => void; onDeleteSquareHole?: ((item: SquareHoleWorkSummary) => void) | null; puzzleItems?: PuzzleWorkSummary[]; onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void; onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null; onClaimPuzzlePointIncentive?: ((item: PuzzleWorkSummary) => void) | null; claimingPuzzleProfileId?: string | null; }; function EmptyState({ title }: { title: string }) { return (
{title}
); } function buildWorkMetricCacheItemKey(item: CreationWorkShelfItem) { return `${item.kind}:${item.id}`; } function readWorkMetricSnapshot(): WorkMetricSnapshot { if (typeof window === 'undefined') { return {}; } try { const rawSnapshot = window.sessionStorage.getItem(WORK_METRIC_CACHE_KEY); if (!rawSnapshot) { return {}; } const parsed = JSON.parse(rawSnapshot) as WorkMetricSnapshot; return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } } function writeWorkMetricSnapshot(items: CreationWorkShelfItem[]) { if (typeof window === 'undefined') { return; } const snapshot: WorkMetricSnapshot = {}; for (const item of items) { if (item.status !== 'published' || item.metrics.length === 0) { continue; } snapshot[buildWorkMetricCacheItemKey(item)] = Object.fromEntries( item.metrics.map((metric) => [metric.id, metric.value]), ); } // 中文注释:缓存只作为下一次进入创作页的数字动画起点,真实展示值仍以接口返回为准。 if (Object.keys(snapshot).length === 0) { return; } try { window.sessionStorage.setItem( WORK_METRIC_CACHE_KEY, JSON.stringify(snapshot), ); } catch { // 中文注释:浏览器禁用 sessionStorage 时降级为无缓存动画,不影响作品列表使用。 } } export function CustomWorldCreationHub({ items, loading, error, onRetry, createError = null, createBusy = false, onCreateType, onOpenDraft, onEnterPublished, onDeletePublished = null, deletingWorkId = null, rpgLibraryEntries = [], bigFishItems = [], onOpenBigFishDetail, onDeleteBigFish = null, match3dItems = [], onOpenMatch3DDetail, onDeleteMatch3D = null, squareHoleItems = [], onOpenSquareHoleDetail, onDeleteSquareHole = null, puzzleItems = [], onOpenPuzzleDetail, onDeletePuzzle = null, onClaimPuzzlePointIncentive = null, claimingPuzzleProfileId = null, }: CustomWorldCreationHubProps) { const [activeFilter, setActiveFilter] = useState('all'); const shelfItems = useMemo( () => buildCreationWorkShelfItems({ rpgItems: items, rpgLibraryEntries, bigFishItems, match3dItems, squareHoleItems, puzzleItems, canDeleteRpg: Boolean(onDeletePublished), canDeleteBigFish: Boolean(onDeleteBigFish), canDeleteMatch3D: Boolean(onDeleteMatch3D), canDeleteSquareHole: Boolean(onDeleteSquareHole), canDeletePuzzle: Boolean(onDeletePuzzle), }), [ bigFishItems, items, match3dItems, onDeleteBigFish, onDeleteMatch3D, onDeleteSquareHole, onDeletePublished, onDeletePuzzle, puzzleItems, rpgLibraryEntries, squareHoleItems, ], ); const [metricSnapshot] = useState(() => readWorkMetricSnapshot(), ); useEffect(() => { writeWorkMetricSnapshot(shelfItems); }, [shelfItems]); const draftCount = shelfItems.filter( (entry) => entry.status === 'draft', ).length; const publishedCount = shelfItems.filter( (entry) => entry.status === 'published', ).length; const filteredItems = useMemo( () => shelfItems.filter((entry) => activeFilter === 'all' ? true : entry.status === activeFilter, ), [activeFilter, shelfItems], ); function handleOpenShelfItem(item: CreationWorkShelfItem) { switch (item.source.kind) { case 'puzzle': onOpenPuzzleDetail?.(item.source.item); return; case 'big-fish': onOpenBigFishDetail?.(item.source.item); return; case 'match3d': onOpenMatch3DDetail?.(item.source.item); return; case 'square-hole': onOpenSquareHoleDetail?.(item.source.item); return; case 'rpg': if (item.status === 'draft') { onOpenDraft(item.source.item); return; } if (item.source.item.profileId) { onEnterPublished(item.source.item.profileId); } } } function buildDeleteAction(item: CreationWorkShelfItem) { if (!item.canDelete) { return null; } switch (item.source.kind) { case 'puzzle': { const sourceItem = item.source.item; return () => { onDeletePuzzle?.(sourceItem); }; } case 'big-fish': { const sourceItem = item.source.item; return () => { onDeleteBigFish?.(sourceItem); }; } case 'match3d': { const sourceItem = item.source.item; return () => { onDeleteMatch3D?.(sourceItem); }; } case 'square-hole': { const sourceItem = item.source.item; return () => { onDeleteSquareHole?.(sourceItem); }; } case 'rpg': { const sourceItem = item.source.item; return () => { onDeletePublished?.(sourceItem); }; } } } function buildPointIncentiveAction(item: CreationWorkShelfItem) { if (item.source.kind !== 'puzzle' || !onClaimPuzzlePointIncentive) { return null; } const sourceItem = item.source.item; return () => { onClaimPuzzlePointIncentive(sourceItem); }; } return (
{error ? (
{error}
) : null} {loading ? (
{Array.from({ length: 3 }).map((_, index) => (
))}
) : filteredItems.length > 0 ? (
{filteredItems.map((item) => ( handleOpenShelfItem(item)} onDelete={buildDeleteAction(item)} deleteBusy={deletingWorkId === item.id} onClaimPointIncentive={buildPointIncentiveAction(item)} pointIncentiveBusy={ item.source.kind === 'puzzle' && claimingPuzzleProfileId === item.source.item.profileId } /> ))}
) : shelfItems.length === 0 ? ( ) : ( )}
); } export type { CustomWorldWorkFilter };