import { Coins, LockKeyhole, Trophy } from 'lucide-react'; import { type UIEvent, useEffect, useMemo, useRef, useState } from 'react'; import type { CreationEntryConfig, CreationEntryEventBannerConfig, } from '../../services/creationEntryConfigService'; import { groupVisiblePlatformCreationTypes, type PlatformCreationTypeCard, type PlatformCreationTypeId, } from '../platform-entry/platformEntryCreationTypes'; /** 底部加号创作入口页的渲染参数,最近创作用作品架摘要推导模板入口。 */ type CustomWorldCreationStartCardProps = { busy?: boolean; entryConfig: CreationEntryConfig; creationTypes: readonly PlatformCreationTypeCard[]; recentCreationTypeIds?: readonly PlatformCreationTypeId[]; recentWindowDays?: number; onCreateType: (type: PlatformCreationTypeId) => void; }; /** 创作入口公告卡兼容结构化和 HTML 两种后台配置。 */ type CreationEventBannerCard = CreationEntryEventBannerConfig; const CREATION_ENTRY_BANNER_AUTOPLAY_MS = 4200; const CREATION_ENTRY_RECENT_TAB_ID = '__recent_creation__'; /** 判断模板 badge 是否需要展示,普通可创建态不额外占用卡片空间。 */ function shouldShowCreationBadge(badge: string) { const normalizedBadge = badge.trim(); return normalizedBadge !== '可创建' && normalizedBadge !== '可创作'; } /** 从后端入口配置中解析创作入口公告位,保留旧单条字段兜底。 */ function resolveCreationEntryEventBanners( entryConfig: CreationEntryConfig, ): CreationEventBannerCard[] { const configuredBanners = Array.isArray(entryConfig.eventBanners) ? entryConfig.eventBanners.filter((banner) => banner.title.trim()) : []; return configuredBanners.length > 0 ? configuredBanners : [entryConfig.eventBanner]; } /** 渲染创作入口公告位当前页指示点。 */ function CreationEntryBannerPager({ banners, activeBannerIndex, }: { banners: readonly CreationEventBannerCard[]; activeBannerIndex: number; }) { return ( ); } /** 渲染底部加号进入的创作入口页,包括后台公告位和模板分类。 */ export function CustomWorldCreationStartCard({ busy = false, entryConfig, creationTypes, recentCreationTypeIds = [], recentWindowDays = 7, onCreateType, }: CustomWorldCreationStartCardProps) { const creationTypeGroups = useMemo( () => groupVisiblePlatformCreationTypes(creationTypes), [creationTypes], ); const recentCreationTypes = useMemo(() => { const creationTypeById = new Map( creationTypes .filter((item) => !item.hidden) .map((item) => [item.id, item] as const), ); return [...new Set(recentCreationTypeIds)] .map((id) => creationTypeById.get(id)) .filter((item): item is PlatformCreationTypeCard => Boolean(item)); }, [creationTypes, recentCreationTypeIds]); const [activeCategoryId, setActiveCategoryId] = useState(null); const hasRecentCreationTypes = recentCreationTypes.length > 0; const activeTabId = activeCategoryId ?? (hasRecentCreationTypes ? CREATION_ENTRY_RECENT_TAB_ID : (creationTypeGroups[0]?.id ?? null)); const isRecentTabActive = hasRecentCreationTypes && activeTabId === CREATION_ENTRY_RECENT_TAB_ID; const activeGroup = isRecentTabActive ? null : (creationTypeGroups.find((group) => group.id === activeTabId) ?? creationTypeGroups[0] ?? null); const visibleCreationTypes = isRecentTabActive ? recentCreationTypes : (activeGroup?.items ?? []); const eventBanners = useMemo( () => resolveCreationEntryEventBanners(entryConfig), [entryConfig], ); const [activeBannerIndex, setActiveBannerIndex] = useState(0); const bannerTrackRef = useRef(null); useEffect(() => { setActiveBannerIndex(0); }, [eventBanners.length]); useEffect(() => { if (hasRecentCreationTypes) { return; } setActiveCategoryId((currentId) => currentId === CREATION_ENTRY_RECENT_TAB_ID ? null : currentId, ); }, [hasRecentCreationTypes]); useEffect(() => { if (eventBanners.length <= 1) { return undefined; } const intervalId = window.setInterval(() => { setActiveBannerIndex((currentIndex) => { const nextIndex = (currentIndex + 1) % eventBanners.length; const track = bannerTrackRef.current; if (track && typeof track.scrollTo === 'function') { track.scrollTo({ left: track.clientWidth * nextIndex, behavior: 'smooth', }); } return nextIndex; }); }, CREATION_ENTRY_BANNER_AUTOPLAY_MS); return () => window.clearInterval(intervalId); }, [eventBanners.length]); /** 同步手势滑动后的 banner 页码,避免自动轮播和手动滑动状态错位。 */ function handleBannerScroll(event: UIEvent) { const { clientWidth, scrollLeft } = event.currentTarget; if (clientWidth <= 0) { return; } const nextIndex = Math.max( 0, Math.min(eventBanners.length - 1, Math.round(scrollLeft / clientWidth)), ); setActiveBannerIndex((currentIndex) => currentIndex === nextIndex ? currentIndex : nextIndex, ); } return (
{eventBanners.map((banner, index) => { const prizePoolText = banner.prizePoolMudPoints.toLocaleString('zh-CN'); const shouldRenderHtmlBanner = banner.renderMode === 'html' && Boolean(banner.htmlCode?.trim()); return (
{shouldRenderHtmlBanner ? (