1277 lines
39 KiB
TypeScript
1277 lines
39 KiB
TypeScript
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<CustomWorldProfile>[];
|
|
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<string, BarkBattleWorkSummary>();
|
|
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<TItem> = {
|
|
onOpen?: (item: TItem) => void;
|
|
onDelete?: (item: TItem) => void;
|
|
};
|
|
|
|
type PuzzleWorkShelfAdapter = WorkShelfAdapter<PuzzleWorkSummary> & {
|
|
onClaimPointIncentive?: (item: PuzzleWorkSummary) => void;
|
|
};
|
|
|
|
function mapRpgWorkToShelfItem(
|
|
item: CustomWorldWorkSummary,
|
|
canDelete: boolean,
|
|
libraryEntries: CustomWorldLibraryEntry<CustomWorldProfile>[],
|
|
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<BigFishWorkSummary>,
|
|
): 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<Match3DWorkSummary>,
|
|
): 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<BabyObjectMatchDraft>,
|
|
): 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<VisualNovelWorkSummary>,
|
|
): 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<BarkBattleWorkSummary>,
|
|
): 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<SquareHoleWorkSummary>,
|
|
): 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<JumpHopWorkSummaryResponse>,
|
|
): 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<WoodenFishWorkSummaryResponse>,
|
|
): 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<unknown>) {
|
|
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<PuzzleWorkSummary['levels']>[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<TItem>(
|
|
item: TItem,
|
|
adapter: WorkShelfAdapter<TItem>,
|
|
): 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;
|
|
}
|