import { useEffect, useMemo, useState } from 'react'; import { resolveSelectionStageFromPath } from '../../routing/appPageRoutes'; import type { CreationEntryConfig } from '../../services/creationEntryConfigService'; import type { PublishShareModalPayload } from '../common/publishShareModalModel'; import type { PlatformCreationTypeCard, PlatformCreationTypeId, } from '../platform-entry/platformEntryCreationTypes'; import { type CreationWorkShelfItem, type CreationWorkShelfMetricId, getCreationWorkShelfItemTime, } from './creationWorkShelf'; import { CustomWorldCreationStartCard, } from './CustomWorldCreationStartCard'; import { CustomWorldWorkCard } from './CustomWorldWorkCard'; import { type CustomWorldWorkFilter, CustomWorldWorkTabs, } from './CustomWorldWorkTabs'; const WORK_GRID_CLASS = 'creation-work-list grid min-w-0 gap-3 sm:gap-3.5 xl:gap-4'; const WORK_METRIC_CACHE_KEY = 'genarrative.creationHub.publishedMetrics.v1'; const RECENT_CREATION_WINDOW_DAYS = 7; const RECENT_CREATION_WINDOW_MS = RECENT_CREATION_WINDOW_DAYS * 24 * 60 * 60 * 1000; type WorkMetricSnapshot = Record< string, Partial> >; type CustomWorldCreationHubProps = { shelfItems: CreationWorkShelfItem[]; loading: boolean; error: string | null; onRetry: () => void; createBusy?: boolean; entryConfig: CreationEntryConfig; creationTypes: readonly PlatformCreationTypeCard[]; onCreateType: (type: PlatformCreationTypeId) => void; deletingWorkId?: string | null; claimingPuzzleProfileId?: string | null; onOpenShelfItem?: (item: CreationWorkShelfItem) => void; onShareWork?: ((payload: PublishShareModalPayload) => void) | null; // 中文注释:底部加号入口可传入后端作品架摘要,用于推导最近使用过的模板。 recentWorkItems?: CreationWorkShelfItem[]; mode?: 'full' | 'start-only' | 'works-only'; }; 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 时降级为无缓存动画,不影响作品列表使用。 } } function resolveShelfShareStage( sharePath: string, ): PublishShareModalPayload['stage'] | null { let pathname = ''; try { pathname = new URL(sharePath, 'https://genarrative.local').pathname; } catch { pathname = sharePath.split(/[?#]/u)[0] ?? ''; } const stage = resolveSelectionStageFromPath(pathname); return stage === 'platform' ? null : stage; } function buildCreationWorkShelfSharePayload( item: CreationWorkShelfItem, ): PublishShareModalPayload | null { const publicWorkCode = item.publicWorkCode?.trim(); const sharePath = item.sharePath?.trim(); if (!publicWorkCode || !sharePath) { return null; } const stage = resolveShelfShareStage(sharePath); if (!stage) { return null; } return { title: item.title, publicWorkCode, stage, }; } /** 渲染底部加号创作入口页与草稿作品架,最近创作复用最近使用过的模板入口。 */ export function CustomWorldCreationHub({ shelfItems, loading, error, onRetry, createBusy = false, entryConfig, creationTypes, onCreateType, deletingWorkId = null, claimingPuzzleProfileId = null, onOpenShelfItem, onShareWork = null, recentWorkItems: recentWorkSourceItems, mode = 'full', }: CustomWorldCreationHubProps) { const [activeFilter, setActiveFilter] = useState('all'); 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], ); // 中文注释:最近创作只取 7 天内作品架摘要,再推导模板 ID 复用模板入口卡片。 const recentCreationCutoffMs = Date.now() - RECENT_CREATION_WINDOW_MS; const recentWorkItems = mode === 'start-only' ? (recentWorkSourceItems ?? shelfItems) .filter( (item) => getCreationWorkShelfItemTime(item.updatedAt) >= recentCreationCutoffMs, ) .slice(0, 4) : []; const recentCreationTypeIds = [ ...new Set(recentWorkItems.map((item) => item.kind)), ]; function handleOpenShelfItem(item: CreationWorkShelfItem) { onOpenShelfItem?.(item); // 中文注释:玩法差异由 Work Shelf Adapter 承载,Hub 只负责响应卡片点击。 item.actions.open(); } function buildDeleteAction(item: CreationWorkShelfItem) { if (!item.canDelete) { return null; } return item.actions.delete ?? null; } function buildShareAction(item: CreationWorkShelfItem) { const payload = buildCreationWorkShelfSharePayload(item); if (!payload) { return null; } return () => { onShareWork?.(payload); }; } function buildPointIncentiveAction(item: CreationWorkShelfItem) { return item.actions.claimPointIncentive ?? null; } const showStartCard = mode !== 'works-only'; const showWorkShelf = mode !== 'start-only'; return (
{showStartCard ? ( ) : null} {showWorkShelf ? ( ) : null} {showWorkShelf && error ? (
) : null} {showWorkShelf ? ( loading ? (
{Array.from({ length: 3 }).map((_, index) => (
))}
) : filteredItems.length > 0 ? (
{filteredItems.map((item) => ( { 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 } /> ))}
) : shelfItems.length === 0 ? ( ) : ( ) ) : null}
); } export type { CustomWorldWorkFilter };