import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent'; import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime'; import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop'; import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish'; import { buildPublicWorkStagePath } from '../../routing/appPageRoutes'; import { buildBabyObjectMatchPublicWorkCode, buildCustomWorldPublicWorkCode, buildBarkBattlePublicWorkCode, buildBigFishPublicWorkCode, buildJumpHopPublicWorkCode, buildMatch3DPublicWorkCode, buildPuzzlePublicWorkCode, buildSquareHolePublicWorkCode, buildVisualNovelPublicWorkCode, buildWoodenFishPublicWorkCode, } from '../../services/publicWorkCode'; import type { CustomWorldProfile } from '../../types'; const MATCH3D_CONTAINER_REFERENCE_COVER_SRC = '/match3d-background-references/pot-fused-reference.png'; const BARK_BATTLE_REFERENCE_COVER_SRC = '/creation-type-references/bark-battle.webp'; const DEFAULT_CREATION_WORK_AUTHOR = '玩家'; export type CreationWorkShelfKind = | 'rpg' | 'big-fish' | 'match3d' | 'square-hole' | 'jump-hop' | 'wooden-fish' | 'puzzle' | 'baby-object-match' | 'bark-battle' | 'visual-novel'; export type CreationWorkShelfStatus = 'draft' | 'published'; export type CreationWorkShelfBadgeTone = 'warm' | 'success' | 'neutral'; export type CreationWorkShelfBadge = { id: string; label: string; tone: CreationWorkShelfBadgeTone; }; export type CreationWorkShelfMetricId = | 'play-count' | 'remix-count' | 'like-count'; export type CreationWorkShelfMetricTone = 'play' | 'remix' | 'like'; export type CreationWorkShelfMetric = { id: CreationWorkShelfMetricId; label: string; value: number; unit: string; tone: CreationWorkShelfMetricTone; }; export type CreationWorkShelfPointIncentive = { totalHalfPoints: number; totalPoints: number; claimablePoints: number; }; export type CreationWorkShelfSource = | { kind: 'rpg'; item: CustomWorldWorkSummary; } | { kind: 'big-fish'; item: BigFishWorkSummary; } | { kind: 'match3d'; item: Match3DWorkSummary; } | { kind: 'square-hole'; item: SquareHoleWorkSummary; } | { kind: 'jump-hop'; item: JumpHopWorkSummaryResponse; } | { kind: 'wooden-fish'; item: WoodenFishWorkSummaryResponse; } | { kind: 'puzzle'; item: PuzzleWorkSummary; } | { kind: 'visual-novel'; item: VisualNovelWorkSummary; } | { kind: 'bark-battle'; item: BarkBattleWorkSummary; } | { kind: 'baby-object-match'; item: BabyObjectMatchDraft; }; export type CreationWorkShelfActions = { open: () => void; delete?: () => void; claimPointIncentive?: () => void; }; export type CreationWorkShelfItem = { id: string; kind: CreationWorkShelfKind; status: CreationWorkShelfStatus; isGenerating?: boolean; hasGenerationFailure?: boolean; generationFailureSummary?: string; hasUnreadUpdate?: boolean; title: string; summary: string; authorDisplayName: string; updatedAt: string; coverImageSrc: string | null; coverRenderMode: 'image' | 'scene_with_roles'; coverCharacterImageSrcs: string[]; publicWorkCode: string | null; sharePath: string | null; openActionLabel: string; canDelete: boolean; canShare: boolean; badges: CreationWorkShelfBadge[]; metrics: CreationWorkShelfMetric[]; pointIncentive?: CreationWorkShelfPointIncentive; actions: CreationWorkShelfActions; source: CreationWorkShelfSource; }; export type CreationWorkShelfRuntimeState = { isGenerating?: boolean; hasGenerationFailure?: boolean; generationFailureSummary?: string; hasUnreadUpdate?: boolean; suppressPersistedGenerating?: boolean; titleOverride?: string; summaryOverride?: string; }; export function buildCreationWorkShelfItems(params: { rpgItems: CustomWorldWorkSummary[]; rpgLibraryEntries?: CustomWorldLibraryEntry[]; bigFishItems: BigFishWorkSummary[]; match3dItems?: Match3DWorkSummary[]; squareHoleItems?: SquareHoleWorkSummary[]; jumpHopItems?: JumpHopWorkSummaryResponse[]; woodenFishItems?: WoodenFishWorkSummaryResponse[]; puzzleItems: PuzzleWorkSummary[]; babyObjectMatchItems?: BabyObjectMatchDraft[]; barkBattleItems?: BarkBattleWorkSummary[]; visualNovelItems?: VisualNovelWorkSummary[]; canDeleteRpg?: boolean; canDeleteBigFish?: boolean; canDeleteMatch3D?: boolean; canDeleteSquareHole?: boolean; canDeleteJumpHop?: boolean; canDeleteWoodenFish?: boolean; canDeletePuzzle?: boolean; canDeleteBabyObjectMatch?: boolean; canDeleteBarkBattle?: boolean; canDeleteVisualNovel?: boolean; onOpenRpgDraft?: (item: CustomWorldWorkSummary) => void; onEnterRpgPublished?: (profileId: string) => void; onDeleteRpg?: (item: CustomWorldWorkSummary) => void; onOpenBigFishDetail?: (item: BigFishWorkSummary) => void; onDeleteBigFish?: (item: BigFishWorkSummary) => void; onOpenMatch3DDetail?: (item: Match3DWorkSummary) => void; onDeleteMatch3D?: (item: Match3DWorkSummary) => void; onOpenSquareHoleDetail?: (item: SquareHoleWorkSummary) => void; onDeleteSquareHole?: (item: SquareHoleWorkSummary) => void; onOpenJumpHopDetail?: (item: JumpHopWorkSummaryResponse) => void; onDeleteJumpHop?: (item: JumpHopWorkSummaryResponse) => void; onOpenWoodenFishDetail?: (item: WoodenFishWorkSummaryResponse) => void; onDeleteWoodenFish?: (item: WoodenFishWorkSummaryResponse) => void; onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void; onDeletePuzzle?: (item: PuzzleWorkSummary) => void; onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void; onOpenBabyObjectMatchDetail?: (item: BabyObjectMatchDraft) => void; onDeleteBabyObjectMatch?: (item: BabyObjectMatchDraft) => void; onOpenBarkBattleDetail?: (item: BarkBattleWorkSummary) => void; onDeleteBarkBattle?: (item: BarkBattleWorkSummary) => void; onOpenVisualNovelDetail?: (item: VisualNovelWorkSummary) => void; onDeleteVisualNovel?: (item: VisualNovelWorkSummary) => void; getItemState?: ( item: CreationWorkShelfItem, ) => CreationWorkShelfRuntimeState | null; }) { const { rpgItems, rpgLibraryEntries = [], bigFishItems, match3dItems = [], squareHoleItems = [], jumpHopItems = [], woodenFishItems = [], puzzleItems, babyObjectMatchItems = [], barkBattleItems = [], visualNovelItems = [], canDeleteRpg = false, canDeleteBigFish = false, canDeleteMatch3D = false, canDeleteSquareHole = false, canDeleteJumpHop = false, canDeleteWoodenFish = false, canDeletePuzzle = false, canDeleteBabyObjectMatch = false, canDeleteBarkBattle = false, canDeleteVisualNovel = false, onOpenRpgDraft, onEnterRpgPublished, onDeleteRpg, onOpenBigFishDetail, onDeleteBigFish, onOpenMatch3DDetail, onDeleteMatch3D, onOpenSquareHoleDetail, onDeleteSquareHole, onOpenJumpHopDetail, onDeleteJumpHop, onOpenWoodenFishDetail, onDeleteWoodenFish, onOpenPuzzleDetail, onDeletePuzzle, onClaimPuzzlePointIncentive, onOpenBabyObjectMatchDetail, onDeleteBabyObjectMatch, onOpenBarkBattleDetail, onDeleteBarkBattle, onOpenVisualNovelDetail, onDeleteVisualNovel, getItemState, } = params; return [ ...rpgItems.map((item) => mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries, { onOpenDraft: onOpenRpgDraft, onEnterPublished: onEnterRpgPublished, onDelete: onDeleteRpg, }), ), ...bigFishItems.map((item) => mapBigFishWorkToShelfItem(item, canDeleteBigFish, { onOpen: onOpenBigFishDetail, onDelete: onDeleteBigFish, }), ), ...match3dItems.map((item) => mapMatch3DWorkToShelfItem(item, canDeleteMatch3D, { onOpen: onOpenMatch3DDetail, onDelete: onDeleteMatch3D, }), ), ...squareHoleItems.map((item) => mapSquareHoleWorkToShelfItem(item, canDeleteSquareHole, { onOpen: onOpenSquareHoleDetail, onDelete: onDeleteSquareHole, }), ), ...jumpHopItems.map((item) => mapJumpHopWorkToShelfItem(item, canDeleteJumpHop, { onOpen: onOpenJumpHopDetail, onDelete: onDeleteJumpHop, }), ), ...woodenFishItems.map((item) => mapWoodenFishWorkToShelfItem(item, canDeleteWoodenFish, { onOpen: onOpenWoodenFishDetail, onDelete: onDeleteWoodenFish, }), ), ...puzzleItems.map((item) => mapPuzzleWorkToShelfItem(item, canDeletePuzzle, { onOpen: onOpenPuzzleDetail, onDelete: onDeletePuzzle, onClaimPointIncentive: onClaimPuzzlePointIncentive, }), ), ...babyObjectMatchItems.map((item) => mapBabyObjectMatchDraftToShelfItem(item, canDeleteBabyObjectMatch, { onOpen: onOpenBabyObjectMatchDetail, onDelete: onDeleteBabyObjectMatch, }), ), ...mergeBarkBattleShelfSourceItems(barkBattleItems).map((item) => mapBarkBattleWorkToShelfItem(item, canDeleteBarkBattle, { onOpen: onOpenBarkBattleDetail, onDelete: onDeleteBarkBattle, }), ), ...visualNovelItems.map((item) => mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel, { onOpen: onOpenVisualNovelDetail, onDelete: onDeleteVisualNovel, }), ), ] .map((item) => { const state = getItemState?.(item); const persistedIsGenerating = isPersistedCreationWorkGenerating(item); const isGenerating = Boolean( state?.isGenerating || (!state?.suppressPersistedGenerating && persistedIsGenerating), ); return state || isGenerating ? { ...item, title: state?.titleOverride ?? item.title, summary: state?.summaryOverride ?? item.summary, isGenerating, hasGenerationFailure: state?.hasGenerationFailure ?? item.hasGenerationFailure, generationFailureSummary: state?.generationFailureSummary ?? item.generationFailureSummary, hasUnreadUpdate: state?.hasUnreadUpdate, } : item; }) .sort( (left, right) => getCreationWorkShelfItemTime(right.updatedAt) - getCreationWorkShelfItemTime(left.updatedAt), ); } function mergeBarkBattleShelfSourceItems( items: readonly BarkBattleWorkSummary[], ): BarkBattleWorkSummary[] { const byWorkId = new Map(); for (const item of items) { const current = byWorkId.get(item.workId); if (!current) { byWorkId.set(item.workId, item); continue; } if (current.status !== 'published' && item.status === 'published') { byWorkId.set(item.workId, { ...current, ...item }); continue; } if (current.status === item.status) { byWorkId.set(item.workId, { ...current, ...item }); } } return Array.from(byWorkId.values()); } type RpgWorkShelfAdapter = { onOpenDraft?: (item: CustomWorldWorkSummary) => void; onEnterPublished?: (profileId: string) => void; onDelete?: (item: CustomWorldWorkSummary) => void; }; type WorkShelfAdapter = { onOpen?: (item: TItem) => void; onDelete?: (item: TItem) => void; }; type PuzzleWorkShelfAdapter = WorkShelfAdapter & { onClaimPointIncentive?: (item: PuzzleWorkSummary) => void; }; function mapRpgWorkToShelfItem( item: CustomWorldWorkSummary, canDelete: boolean, libraryEntries: CustomWorldLibraryEntry[], adapter: RpgWorkShelfAdapter, ): CreationWorkShelfItem { const isDraft = item.status === 'draft'; const libraryEntry = item.profileId ? libraryEntries.find((entry) => entry.profileId === item.profileId) : null; const publicWorkCode = item.status === 'published' ? libraryEntry?.publicWorkCode?.trim() || (item.profileId ? buildCustomWorldPublicWorkCode(item.profileId) : null) : null; const badges: CreationWorkShelfBadge[] = [ buildStatusBadge(item.status), { id: 'type', label: 'RPG', tone: 'neutral' }, ]; const metrics = buildPublishedMetrics({ playCount: libraryEntry?.playCount, remixCount: libraryEntry?.remixCount, likeCount: libraryEntry?.likeCount, }); return { id: item.workId, kind: 'rpg', status: item.status, title: item.title, summary: item.summary, authorDisplayName: resolveAuthorDisplayName(item, libraryEntry), updatedAt: item.updatedAt, coverImageSrc: item.coverImageSrc ?? null, coverRenderMode: item.coverRenderMode ?? 'image', coverCharacterImageSrcs: item.coverCharacterImageSrcs ?? [], publicWorkCode, sharePath: publicWorkCode && item.status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: isDraft ? item.playableNpcCount > 0 || item.landmarkCount > 0 ? '继续完善' : '继续创作' : '查看详情', canDelete, canShare: item.status === 'published' && Boolean(publicWorkCode), actions: buildRpgWorkShelfActions(item, adapter), badges, metrics: isDraft ? [] : metrics, source: { kind: 'rpg', item }, }; } function mapBigFishWorkToShelfItem( item: BigFishWorkSummary, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const isPublished = item.status === 'published'; const publicWorkCode = isPublished ? buildBigFishPublicWorkCode(item.sourceSessionId) : null; return { id: item.workId, kind: 'big-fish', status: item.status, title: item.title, summary: item.summary, authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc: item.coverImageSrc ?? null, coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && isPublished ? buildPublicWorkStagePath('big-fish-runtime', publicWorkCode) : null, openActionLabel: item.status === 'draft' ? '继续创作' : '查看详情', canDelete, canShare: isPublished && Boolean(publicWorkCode), actions: buildWorkShelfActions(item, adapter), badges: [ buildStatusBadge(item.status), { id: 'type', label: '大鱼', tone: 'neutral' }, ], metrics: isPublished ? buildPublishedMetrics({ playCount: item.playCount, remixCount: item.remixCount, likeCount: item.likeCount, }) : [], source: { kind: 'big-fish', item }, }; } function mapMatch3DWorkToShelfItem( item: Match3DWorkSummary, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const status = item.publicationStatus === 'published' ? 'published' : 'draft'; const publicWorkCode = status === 'published' ? buildMatch3DPublicWorkCode(item.profileId) : null; const coverImageSrc = resolveMatch3DWorkCoverImageSrc(item); return { id: item.workId, kind: 'match3d', status, title: item.gameName, summary: item.summary, authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc, coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: status === 'published' ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), actions: buildWorkShelfActions(item, adapter), badges: [ buildStatusBadge(status), { id: 'type', label: '抓鹅', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: item.playCount, remixCount: 0, likeCount: 0, }) : [], source: { kind: 'match3d', item }, }; } function mapPuzzleWorkToShelfItem( item: PuzzleWorkSummary, canDelete: boolean, adapter: PuzzleWorkShelfAdapter, ): CreationWorkShelfItem { const status = item.publicationStatus; const publicWorkCode = status === 'published' ? buildPuzzlePublicWorkCode(item.profileId) : null; const coverImageSrc = resolvePuzzleWorkCoverImageSrc(item); return { id: item.workId, kind: 'puzzle', status, title: item.workTitle?.trim() || item.levelName.trim() || '未命名拼图', summary: item.workDescription?.trim() || item.summary.trim() || (status === 'draft' ? '未填写作品描述' : ''), authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc, coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('puzzle-gallery-detail', publicWorkCode) : null, openActionLabel: status === 'published' && !item.sourceSessionId ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), actions: buildPuzzleWorkShelfActions(item, adapter), badges: [ buildStatusBadge(status), { id: 'type', label: '拼图', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: item.playCount, remixCount: item.remixCount, likeCount: item.likeCount, }) : [], pointIncentive: status === 'published' ? { totalHalfPoints: normalizeMetricCount( item.pointIncentiveTotalHalfPoints, ), totalPoints: normalizePointIncentiveTotal( item.pointIncentiveTotalPoints, item.pointIncentiveTotalHalfPoints, ), claimablePoints: normalizeMetricCount( item.pointIncentiveClaimablePoints, ), } : undefined, source: { kind: 'puzzle', item }, }; } function mapBabyObjectMatchDraftToShelfItem( item: BabyObjectMatchDraft, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const status = item.publicationStatus === 'published' ? 'published' : 'draft'; const publicWorkCode = status === 'published' ? buildBabyObjectMatchPublicWorkCode(item.profileId) : null; const coverImageSrc = item.itemAssets.find((asset) => asset.imageSrc.trim())?.imageSrc ?? null; return { id: item.profileId, kind: 'baby-object-match', status, title: item.workTitle.trim() || item.templateName, summary: item.workDescription.trim() || `${item.itemNames[0]}和${item.itemNames[1]}识物分类`, authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc, coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: status === 'published' ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), badges: [ buildStatusBadge(status), { id: 'type', label: '宝贝识物', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: 0, remixCount: 0, likeCount: 0, }) : [], actions: buildWorkShelfActions(item, adapter), source: { kind: 'baby-object-match', item }, }; } function mapVisualNovelWorkToShelfItem( item: VisualNovelWorkSummary, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const status = item.publishStatus === 'published' ? 'published' : 'draft'; const publicWorkCode = status === 'published' ? buildVisualNovelPublicWorkCode(item.profileId) : null; const title = item.title?.trim() || '未命名视觉小说'; const summary = item.description?.trim() || (status === 'draft' ? '未填写作品描述' : ''); return { id: item.profileId, kind: 'visual-novel', status, title, summary, authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc: item.coverImageSrc ?? null, coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: status === 'published' ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), badges: [ buildStatusBadge(status), { id: 'type', label: '视觉小说', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: item.playCount, remixCount: 0, likeCount: 0, }) : [], actions: buildWorkShelfActions(item, adapter), source: { kind: 'visual-novel', item }, }; } function mapBarkBattleWorkToShelfItem( item: BarkBattleWorkSummary, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const status = item.status; const publicWorkCode = status === 'published' ? buildBarkBattlePublicWorkCode(item.workId) : null; const playerCharacterImageSrc = normalizeCoverImageSrc( item.playerCharacterImageSrc, ); const opponentCharacterImageSrc = normalizeCoverImageSrc( item.opponentCharacterImageSrc, ); const coverImageSrc = normalizeCoverImageSrc(item.uiBackgroundImageSrc) ?? playerCharacterImageSrc ?? opponentCharacterImageSrc ?? BARK_BATTLE_REFERENCE_COVER_SRC; const coverCharacterImageSrcs = [ playerCharacterImageSrc, opponentCharacterImageSrc, ].filter((imageSrc): imageSrc is string => Boolean(imageSrc)); const canRenderSceneWithRoles = Boolean(normalizeCoverImageSrc(item.uiBackgroundImageSrc)) && coverCharacterImageSrcs.length >= 2; return { id: item.workId, kind: 'bark-battle', status, title: item.title.trim() || '汪汪声浪大作战', summary: item.summary.trim() || item.themeDescription.trim() || (status === 'draft' ? '未填写作品描述' : ''), authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc, coverRenderMode: canRenderSceneWithRoles ? 'scene_with_roles' : 'image', coverCharacterImageSrcs, publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: status === 'published' ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), badges: [ buildStatusBadge(status), { id: 'type', label: '汪汪', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: item.playCount, remixCount: 0, likeCount: 0, }) : [], actions: buildWorkShelfActions(item, adapter), source: { kind: 'bark-battle', item }, }; } function mapSquareHoleWorkToShelfItem( item: SquareHoleWorkSummary, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const status = item.publicationStatus === 'published' ? 'published' : 'draft'; const publicWorkCode = status === 'published' ? buildSquareHolePublicWorkCode(item.profileId) : null; const coverImageSrc = resolveSquareHoleWorkCoverImageSrc(item); return { id: item.workId, kind: 'square-hole', status, title: item.gameName, summary: item.summary, authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc, coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: status === 'published' ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), badges: [ buildStatusBadge(status), { id: 'type', label: '方洞', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: item.playCount, remixCount: 0, likeCount: 0, }) : [], actions: buildWorkShelfActions(item, adapter), source: { kind: 'square-hole', item }, }; } function mapJumpHopWorkToShelfItem( item: JumpHopWorkSummaryResponse, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const status = item.publicationStatus === 'published' ? 'published' : 'draft'; const publicWorkCode = status === 'published' ? buildJumpHopPublicWorkCode(item.profileId) : null; const coverImageSrc = normalizeCoverImageSrc(item.coverImageSrc); return { id: item.workId, kind: 'jump-hop', status, title: item.workTitle, summary: item.workDescription, authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc, coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: status === 'published' ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), badges: [ buildStatusBadge(status), { id: 'type', label: '跳一跳', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: item.playCount, remixCount: 0, likeCount: 0, }) : [], actions: buildWorkShelfActions(item, adapter), source: { kind: 'jump-hop', item }, }; } function mapWoodenFishWorkToShelfItem( item: WoodenFishWorkSummaryResponse, canDelete: boolean, adapter: WorkShelfAdapter, ): CreationWorkShelfItem { const status = item.publicationStatus === 'published' ? 'published' : 'draft'; const publicWorkCode = status === 'published' ? buildWoodenFishPublicWorkCode(item.profileId) : null; const title = item.workTitle.trim() || '敲木鱼'; const summary = item.workDescription.trim() || (status === 'draft' ? '未填写作品描述' : ''); return { id: item.workId, kind: 'wooden-fish', status, title, summary, authorDisplayName: resolveAuthorDisplayName(item), updatedAt: item.updatedAt, coverImageSrc: normalizeCoverImageSrc(item.coverImageSrc), coverRenderMode: 'image', coverCharacterImageSrcs: [], publicWorkCode, sharePath: publicWorkCode && status === 'published' ? buildPublicWorkStagePath('work-detail', publicWorkCode) : null, openActionLabel: status === 'published' ? '查看详情' : '继续创作', canDelete, canShare: status === 'published' && Boolean(publicWorkCode), badges: [ buildStatusBadge(status), { id: 'type', label: '敲木鱼', tone: 'neutral' }, ], metrics: status === 'published' ? buildPublishedMetrics({ playCount: item.playCount, remixCount: 0, likeCount: 0, }) : [], actions: buildWorkShelfActions(item, adapter), source: { kind: 'wooden-fish', item }, }; } function resolveAuthorDisplayName(...sources: Array) { for (const source of sources) { const authorDisplayName = source && typeof source === 'object' && 'authorDisplayName' in source && typeof source.authorDisplayName === 'string' ? source.authorDisplayName.trim() : ''; if (authorDisplayName) { return authorDisplayName; } } return DEFAULT_CREATION_WORK_AUTHOR; } function normalizeCoverImageSrc(value?: string | null) { return value?.trim() || null; } function isCreationTypeReferenceCoverImageSrc(value?: string | null) { const normalizedValue = normalizeCoverImageSrc(value); if (!normalizedValue) { return false; } // 中文注释:玩法参考图只做草稿页兜底,不应覆盖作品已经生成出来的真实关卡图或运行态背景图。 return /^\/?creation-type-references\/[^/?#]+(?:[?#].*)?$/u.test( normalizedValue, ); } export function resolvePuzzleWorkCoverImageSrc(item: PuzzleWorkSummary) { const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc); if ( directCoverImageSrc && !isCreationTypeReferenceCoverImageSrc(directCoverImageSrc) ) { return directCoverImageSrc; } for (const level of item.levels ?? []) { const levelImageSrc = resolvePuzzleLevelCoverImageSrc(level); if (levelImageSrc) { return levelImageSrc; } } return null; } export function resolvePuzzleLevelCoverImageSrc( level: NonNullable[number], ) { const levelCoverImageSrc = normalizeCoverImageSrc(level.coverImageSrc); if ( levelCoverImageSrc && !isCreationTypeReferenceCoverImageSrc(levelCoverImageSrc) ) { return levelCoverImageSrc; } const selectedCandidateImageSrc = level.selectedCandidateId && level.candidates.length > 0 ? normalizeCoverImageSrc( level.candidates.find( (candidate) => candidate.candidateId === level.selectedCandidateId, )?.imageSrc, ) : null; const fallbackCandidateImageSrc = normalizeCoverImageSrc( level.candidates[level.candidates.length - 1]?.imageSrc, ); const candidateImageSrc = selectedCandidateImageSrc || fallbackCandidateImageSrc; if ( candidateImageSrc && !isCreationTypeReferenceCoverImageSrc(candidateImageSrc) ) { return candidateImageSrc; } return null; } function resolveMatch3DWorkCoverImageSrc(item: Match3DWorkSummary) { const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc); if ( directCoverImageSrc && !isCreationTypeReferenceCoverImageSrc(directCoverImageSrc) ) { return directCoverImageSrc; } const topLevelContainerImageSrc = normalizeCoverImageSrc(item.generatedBackgroundAsset?.containerImageSrc) || normalizeCoverImageSrc( item.generatedBackgroundAsset?.containerImageObjectKey, ); if (topLevelContainerImageSrc) { return topLevelContainerImageSrc; } for (const asset of item.generatedItemAssets ?? []) { const assetContainerImageSrc = normalizeCoverImageSrc(asset.backgroundAsset?.containerImageSrc) || normalizeCoverImageSrc(asset.backgroundAsset?.containerImageObjectKey); if (assetContainerImageSrc) { return assetContainerImageSrc; } } const backgroundImageSrc = normalizeCoverImageSrc(item.backgroundImageSrc) || normalizeCoverImageSrc(item.backgroundImageObjectKey) || normalizeCoverImageSrc(item.generatedBackgroundAsset?.imageSrc) || normalizeCoverImageSrc(item.generatedBackgroundAsset?.imageObjectKey); if (backgroundImageSrc) { return backgroundImageSrc; } for (const asset of item.generatedItemAssets ?? []) { const assetBackgroundImageSrc = normalizeCoverImageSrc(asset.backgroundAsset?.imageSrc) || normalizeCoverImageSrc(asset.backgroundAsset?.imageObjectKey); if (assetBackgroundImageSrc) { return assetBackgroundImageSrc; } const imageView = asset.imageViews?.find( (view) => normalizeCoverImageSrc(view.imageSrc) || normalizeCoverImageSrc(view.imageObjectKey), ); const imageViewSrc = normalizeCoverImageSrc(imageView?.imageSrc) || normalizeCoverImageSrc(imageView?.imageObjectKey); const itemImageSrc = normalizeCoverImageSrc(asset.imageSrc) || normalizeCoverImageSrc(asset.imageObjectKey); const preferredImageSrc = imageViewSrc || itemImageSrc; if ( preferredImageSrc && !isCreationTypeReferenceCoverImageSrc(preferredImageSrc) ) { return preferredImageSrc; } } return MATCH3D_CONTAINER_REFERENCE_COVER_SRC; } function resolveSquareHoleWorkCoverImageSrc(item: SquareHoleWorkSummary) { const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc); if (directCoverImageSrc) { return directCoverImageSrc; } const backgroundImageSrc = normalizeCoverImageSrc(item.backgroundImageSrc); if (backgroundImageSrc) { return backgroundImageSrc; } for (const option of [...item.shapeOptions, ...item.holeOptions]) { const optionImageSrc = normalizeCoverImageSrc(option.imageSrc); if (optionImageSrc) { return optionImageSrc; } } return null; } function buildWorkShelfActions( item: TItem, adapter: WorkShelfAdapter, ): CreationWorkShelfActions { return { open: () => { adapter.onOpen?.(item); }, delete: adapter.onDelete ? () => { adapter.onDelete?.(item); } : undefined, }; } function buildPuzzleWorkShelfActions( item: PuzzleWorkSummary, adapter: PuzzleWorkShelfAdapter, ): CreationWorkShelfActions { return { ...buildWorkShelfActions(item, adapter), claimPointIncentive: adapter.onClaimPointIncentive ? () => { adapter.onClaimPointIncentive?.(item); } : undefined, }; } function isPersistedCreationWorkGenerating(item: CreationWorkShelfItem) { switch (item.source.kind) { case 'match3d': return item.source.item.generationStatus === 'generating'; case 'puzzle': return isPersistedPuzzleDraftGenerating(item.source.item); case 'wooden-fish': return item.source.item.generationStatus === 'generating'; case 'bark-battle': return isPersistedBarkBattleDraftGenerating(item.source.item); default: return false; } } export function isPersistedBarkBattleDraftGenerating( item: BarkBattleWorkSummary, ) { if (item.status === 'published') { return false; } // 中文注释:汪汪声浪生成失败后会回写 partial_failed 并进入结果页承接错误槽位, // 不能因为三图未齐就继续把作品架整卡锁成“生成中”。 return item.generationStatus === 'pending_assets'; } export function hasBarkBattleRequiredImages(item: BarkBattleWorkSummary) { return Boolean( normalizeCoverImageSrc(item.playerCharacterImageSrc) && normalizeCoverImageSrc(item.opponentCharacterImageSrc) && normalizeCoverImageSrc(item.uiBackgroundImageSrc), ); } export function isPersistedPuzzleDraftGenerating(item: PuzzleWorkSummary) { if (item.generationStatus !== 'generating') { return false; } const hasUsableCover = Boolean(resolvePuzzleWorkCoverImageSrc(item)); const hasReadyLevel = (item.levels ?? []).some((level) => Boolean(resolvePuzzleLevelCoverImageSrc(level)), ); // 中文注释:作品架“生成中”只表示初始草稿还没有可查看结果;结果页追加关卡或重绘局部图片不能锁住整张草稿卡。 return !hasUsableCover && !hasReadyLevel; } function buildRpgWorkShelfActions( item: CustomWorldWorkSummary, adapter: RpgWorkShelfAdapter, ): CreationWorkShelfActions { return { open: () => { if (item.status === 'draft') { adapter.onOpenDraft?.(item); return; } if (item.profileId) { adapter.onEnterPublished?.(item.profileId); } }, delete: adapter.onDelete ? () => { adapter.onDelete?.(item); } : undefined, }; } function buildPublishedMetrics(params: { playCount?: number | null; remixCount?: number | null; likeCount?: number | null; }): CreationWorkShelfMetric[] { return [ { id: 'play-count', label: '游玩', value: normalizeMetricCount(params.playCount), unit: '次', tone: 'play', }, { id: 'remix-count', label: '改造', value: normalizeMetricCount(params.remixCount), unit: '次', tone: 'remix', }, { id: 'like-count', label: '点赞', value: normalizeMetricCount(params.likeCount), unit: '赞', tone: 'like', }, ]; } export function normalizeMetricCount(value?: number | null) { return Math.max(0, Math.floor(value ?? 0)); } export function formatCreationMetricCount(value?: number | null) { const normalized = Math.max(0, Math.floor(value ?? 0)); if (normalized >= 10000) { const wanValue = normalized / 10000; return `${Number.isInteger(wanValue) ? wanValue.toFixed(0) : wanValue.toFixed(1)}万`; } return `${normalized}`; } export function formatCreationPointIncentiveTotal(value?: number | null) { const normalized = Math.max(0, value ?? 0); return Number.isInteger(normalized) ? normalized.toFixed(0) : normalized.toFixed(1); } function normalizePointIncentiveTotal( totalPoints?: number | null, totalHalfPoints?: number | null, ) { if (Number.isFinite(totalPoints)) { return Math.max(0, totalPoints ?? 0); } return normalizeMetricCount(totalHalfPoints) / 2; } function buildStatusBadge( status: CreationWorkShelfStatus, ): CreationWorkShelfBadge { return { id: 'status', label: status === 'draft' ? '草稿' : '已发布', tone: status === 'draft' ? 'warm' : 'success', }; } export function getCreationWorkShelfItemTime(value: string) { const normalized = value.trim(); const numericTimestamp = normalized.match(/^(-?\d+(?:\.\d+)?)(?:Z)?$/u); if (numericTimestamp?.[1]) { const rawTimestamp = Number(numericTimestamp[1]); if (Number.isFinite(rawTimestamp)) { const absoluteTimestamp = Math.abs(rawTimestamp); if (absoluteTimestamp >= 1_000_000_000_000_000) { return rawTimestamp / 1000; } if (absoluteTimestamp >= 1_000_000_000_000) { return rawTimestamp; } if (absoluteTimestamp >= 1_000_000_000) { return rawTimestamp * 1000; } } } const timestamp = new Date(normalized).getTime(); return Number.isNaN(timestamp) ? 0 : timestamp; }