460 lines
13 KiB
TypeScript
460 lines
13 KiB
TypeScript
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 { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
|
import {
|
|
buildBigFishPublicWorkCode,
|
|
buildMatch3DPublicWorkCode,
|
|
buildPuzzlePublicWorkCode,
|
|
buildSquareHolePublicWorkCode,
|
|
} from '../../services/publicWorkCode';
|
|
import type { CustomWorldProfile } from '../../types';
|
|
|
|
export type CreationWorkShelfKind =
|
|
| 'rpg'
|
|
| 'big-fish'
|
|
| 'match3d'
|
|
| 'square-hole'
|
|
| 'puzzle';
|
|
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: 'puzzle';
|
|
item: PuzzleWorkSummary;
|
|
};
|
|
|
|
export type CreationWorkShelfItem = {
|
|
id: string;
|
|
kind: CreationWorkShelfKind;
|
|
status: CreationWorkShelfStatus;
|
|
title: string;
|
|
summary: 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;
|
|
source: CreationWorkShelfSource;
|
|
};
|
|
|
|
export function buildCreationWorkShelfItems(params: {
|
|
rpgItems: CustomWorldWorkSummary[];
|
|
rpgLibraryEntries?: CustomWorldLibraryEntry<CustomWorldProfile>[];
|
|
bigFishItems: BigFishWorkSummary[];
|
|
match3dItems?: Match3DWorkSummary[];
|
|
squareHoleItems?: SquareHoleWorkSummary[];
|
|
puzzleItems: PuzzleWorkSummary[];
|
|
canDeleteRpg?: boolean;
|
|
canDeleteBigFish?: boolean;
|
|
canDeleteMatch3D?: boolean;
|
|
canDeleteSquareHole?: boolean;
|
|
canDeletePuzzle?: boolean;
|
|
}) {
|
|
const {
|
|
rpgItems,
|
|
rpgLibraryEntries = [],
|
|
bigFishItems,
|
|
match3dItems = [],
|
|
squareHoleItems = [],
|
|
puzzleItems,
|
|
canDeleteRpg = false,
|
|
canDeleteBigFish = false,
|
|
canDeleteMatch3D = false,
|
|
canDeleteSquareHole = false,
|
|
canDeletePuzzle = false,
|
|
} = params;
|
|
|
|
return [
|
|
...rpgItems.map((item) =>
|
|
mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries),
|
|
),
|
|
...bigFishItems.map((item) =>
|
|
mapBigFishWorkToShelfItem(item, canDeleteBigFish),
|
|
),
|
|
...match3dItems.map((item) =>
|
|
mapMatch3DWorkToShelfItem(item, canDeleteMatch3D),
|
|
),
|
|
...squareHoleItems.map((item) =>
|
|
mapSquareHoleWorkToShelfItem(item, canDeleteSquareHole),
|
|
),
|
|
...puzzleItems.map((item) =>
|
|
mapPuzzleWorkToShelfItem(item, canDeletePuzzle),
|
|
),
|
|
].sort(
|
|
(left, right) =>
|
|
getShelfItemTime(right.updatedAt) - getShelfItemTime(left.updatedAt),
|
|
);
|
|
}
|
|
|
|
function mapRpgWorkToShelfItem(
|
|
item: CustomWorldWorkSummary,
|
|
canDelete: boolean,
|
|
libraryEntries: CustomWorldLibraryEntry<CustomWorldProfile>[],
|
|
): 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 ?? 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,
|
|
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),
|
|
badges,
|
|
metrics: isDraft ? [] : metrics,
|
|
source: { kind: 'rpg', item },
|
|
};
|
|
}
|
|
|
|
function mapBigFishWorkToShelfItem(
|
|
item: BigFishWorkSummary,
|
|
canDelete: boolean,
|
|
): 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,
|
|
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),
|
|
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,
|
|
): CreationWorkShelfItem {
|
|
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
|
const publicWorkCode =
|
|
status === 'published' ? buildMatch3DPublicWorkCode(item.profileId) : null;
|
|
|
|
return {
|
|
id: item.workId,
|
|
kind: 'match3d',
|
|
status,
|
|
title: item.gameName,
|
|
summary: item.summary,
|
|
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,
|
|
})
|
|
: [],
|
|
source: { kind: 'match3d', item },
|
|
};
|
|
}
|
|
|
|
function mapPuzzleWorkToShelfItem(
|
|
item: PuzzleWorkSummary,
|
|
canDelete: boolean,
|
|
): CreationWorkShelfItem {
|
|
const status = item.publicationStatus;
|
|
const publicWorkCode =
|
|
status === 'published' ? buildPuzzlePublicWorkCode(item.profileId) : null;
|
|
|
|
return {
|
|
id: item.workId,
|
|
kind: 'puzzle',
|
|
status,
|
|
title: item.workTitle?.trim() || item.levelName.trim() || '未命名拼图',
|
|
summary:
|
|
item.workDescription?.trim() ||
|
|
item.summary.trim() ||
|
|
(status === 'draft' ? '未填写作品描述' : ''),
|
|
updatedAt: item.updatedAt,
|
|
coverImageSrc: item.coverImageSrc ?? null,
|
|
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),
|
|
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 mapSquareHoleWorkToShelfItem(
|
|
item: SquareHoleWorkSummary,
|
|
canDelete: boolean,
|
|
): CreationWorkShelfItem {
|
|
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
|
const publicWorkCode =
|
|
status === 'published'
|
|
? buildSquareHolePublicWorkCode(item.profileId)
|
|
: null;
|
|
|
|
return {
|
|
id: item.workId,
|
|
kind: 'square-hole',
|
|
status,
|
|
title: item.gameName,
|
|
summary: item.summary,
|
|
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,
|
|
})
|
|
: [],
|
|
source: { kind: 'square-hole', item },
|
|
};
|
|
}
|
|
|
|
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',
|
|
};
|
|
}
|
|
|
|
function getShelfItemTime(value: string) {
|
|
const timestamp = new Date(value).getTime();
|
|
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
}
|