1
This commit is contained in:
@@ -345,9 +345,9 @@ test('creation hub shows puzzle point incentive and claims without opening card'
|
||||
profileId: 'puzzle-profile-incentive',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '百梦灯塔',
|
||||
levelName: '陶泥儿灯塔',
|
||||
summary: '拼图作品会展示积分激励。',
|
||||
themeTags: ['灯塔', '百梦'],
|
||||
themeTags: ['灯塔', '陶泥儿'],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: new Date('2026-05-01T12:00:00.000Z').toISOString(),
|
||||
@@ -375,8 +375,8 @@ test('creation hub shows puzzle point incentive and claims without opening card'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('积分激励总数 2.5 光点')).toBeTruthy();
|
||||
expect(screen.getByLabelText('待领取积分 1 光点')).toBeTruthy();
|
||||
expect(screen.getByLabelText('积分激励总数 2.5 泥点')).toBeTruthy();
|
||||
expect(screen.getByLabelText('待领取积分 1 泥点')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '领取积分' }));
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ export function CustomWorldWorkCard({
|
||||
event.preventDefault();
|
||||
onOpen();
|
||||
}}
|
||||
className={`platform-surface platform-interactive-card relative min-h-[9.5rem] cursor-pointer overflow-hidden px-2.5 py-2.5 text-left sm:min-h-[12.5rem] sm:px-4 sm:py-4 xl:min-h-[11.25rem] xl:px-4 xl:py-3.5 ${isPublished ? 'col-span-2 sm:col-span-1' : ''}`}
|
||||
className={`creation-work-card platform-surface platform-interactive-card relative min-h-[9.5rem] cursor-pointer overflow-hidden px-2.5 py-2.5 text-left sm:min-h-[12.5rem] sm:px-4 sm:py-4 xl:min-h-[11.25rem] xl:px-4 xl:py-3.5 ${isPublished ? 'col-span-2 sm:col-span-1' : ''}`}
|
||||
>
|
||||
<CustomWorldCoverArtwork
|
||||
imageSrc={item.coverImageSrc}
|
||||
@@ -263,10 +263,9 @@ export function CustomWorldWorkCard({
|
||||
fallbackLabel="封面"
|
||||
renderMode={item.coverRenderMode}
|
||||
characterImageSrcs={item.coverCharacterImageSrcs}
|
||||
className="platform-cover-artwork absolute inset-0 opacity-70 saturate-[1.08]"
|
||||
className="platform-cover-artwork absolute inset-0"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_100%_100%,rgba(255,255,255,0.18),transparent_34%),linear-gradient(180deg,rgba(255,255,255,0.08),rgba(0,0,0,0.08))]" />
|
||||
<div className="creation-work-card__overlay absolute inset-0" />
|
||||
{item.hasUnreadUpdate ? (
|
||||
<span
|
||||
aria-label="新生成完成"
|
||||
@@ -288,7 +287,7 @@ export function CustomWorldWorkCard({
|
||||
disabled={deleteBusy}
|
||||
aria-label={deleteBusy ? '删除中' : '删除'}
|
||||
title={deleteBusy ? '删除中' : '删除作品'}
|
||||
className="grid h-7 w-7 place-items-center text-[var(--platform-text-soft)] transition hover:text-[var(--platform-button-danger-text)] disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:w-8"
|
||||
className="grid h-7 w-7 place-items-center rounded-full bg-black/22 text-white/78 transition hover:bg-red-500/22 hover:text-white disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:w-8"
|
||||
>
|
||||
{deleteBusy ? (
|
||||
<span className="text-xs leading-none">…</span>
|
||||
@@ -326,7 +325,7 @@ export function CustomWorldWorkCard({
|
||||
? '分享内容复制失败'
|
||||
: '分享'
|
||||
}
|
||||
className="inline-flex h-7 min-w-7 items-center justify-center gap-1 whitespace-nowrap px-1.5 text-[var(--platform-text-soft)] transition hover:text-[var(--platform-cool-text)] disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:min-w-8"
|
||||
className="inline-flex h-7 min-w-7 items-center justify-center gap-1 whitespace-nowrap rounded-full bg-black/22 px-1.5 text-white/78 transition hover:bg-white/18 hover:text-white disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:min-w-8"
|
||||
>
|
||||
{shareState === 'idle' ? (
|
||||
<Share2 aria-hidden="true" className="h-3.5 w-3.5" />
|
||||
@@ -358,10 +357,10 @@ export function CustomWorldWorkCard({
|
||||
</div>
|
||||
|
||||
<div className="mt-3 min-h-0 sm:mt-4 xl:mt-3">
|
||||
<div className="line-clamp-1 break-words text-base font-black leading-tight text-[var(--platform-text-strong)] sm:text-2xl xl:text-xl">
|
||||
<div className="line-clamp-1 break-words text-base font-black leading-tight text-white [text-shadow:0_2px_12px_rgba(0,0,0,0.52)] sm:text-2xl xl:text-xl">
|
||||
{displayTitle}
|
||||
</div>
|
||||
<div className="mt-2 line-clamp-2 break-words text-[11px] leading-4 text-[color:color-mix(in_srgb,var(--platform-text-base)_92%,transparent)] sm:mt-3 sm:text-sm sm:leading-6 xl:mt-2">
|
||||
<div className="mt-2 line-clamp-2 break-words text-[11px] leading-4 text-white/84 [text-shadow:0_1px_8px_rgba(0,0,0,0.5)] sm:mt-3 sm:text-sm sm:leading-6 xl:mt-2">
|
||||
{item.summary}
|
||||
</div>
|
||||
</div>
|
||||
@@ -371,7 +370,7 @@ export function CustomWorldWorkCard({
|
||||
{item.pointIncentive ? (
|
||||
<div className="creation-work-card-incentive">
|
||||
<div
|
||||
aria-label={`积分激励总数 ${formatCreationPointIncentiveTotal(item.pointIncentive.totalPoints)} 光点`}
|
||||
aria-label={`积分激励总数 ${formatCreationPointIncentiveTotal(item.pointIncentive.totalPoints)} 泥点`}
|
||||
className="creation-work-card-incentive__metric"
|
||||
>
|
||||
<span className="creation-work-card-incentive__label">
|
||||
@@ -384,7 +383,7 @@ export function CustomWorldWorkCard({
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label={`待领取积分 ${item.pointIncentive.claimablePoints} 光点`}
|
||||
aria-label={`待领取积分 ${item.pointIncentive.claimablePoints} 泥点`}
|
||||
className="creation-work-card-incentive__metric"
|
||||
>
|
||||
<span className="creation-work-card-incentive__label">
|
||||
|
||||
@@ -187,6 +187,109 @@ test('buildCreationWorkShelfItems sorts works by latest updatedAt across timesta
|
||||
]);
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems falls back to available gameplay images as covers', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [
|
||||
{
|
||||
workId: 'puzzle:level-cover',
|
||||
profileId: 'puzzle-profile-level-cover',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '关卡封面拼图',
|
||||
summary: '作品自身封面为空时使用关卡正式图。',
|
||||
themeTags: [],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '2026-05-08T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
levels: [
|
||||
{
|
||||
levelId: 'level-1',
|
||||
levelName: '第一关',
|
||||
pictureDescription: '港口雨夜。',
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle-candidate.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '港口雨夜',
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
match3dItems: [
|
||||
{
|
||||
workId: 'match3d:asset-cover',
|
||||
profileId: 'match3d-profile-asset-cover',
|
||||
ownerUserId: 'user-1',
|
||||
gameName: '素材封面抓鹅',
|
||||
themeText: '糖果厨房',
|
||||
summary: '作品自身封面为空时使用素材图。',
|
||||
tags: [],
|
||||
coverImageSrc: null,
|
||||
clearCount: 18,
|
||||
difficulty: 1,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-07T00:00:00.000Z',
|
||||
publishReady: false,
|
||||
generatedItemAssets: [
|
||||
{
|
||||
itemId: 'item-1',
|
||||
itemName: '糖果',
|
||||
imageSrc: '/match3d-item.png',
|
||||
status: 'image_ready',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
squareHoleItems: [
|
||||
{
|
||||
workId: 'square-hole:background-cover',
|
||||
profileId: 'square-hole-profile-background-cover',
|
||||
ownerUserId: 'user-1',
|
||||
gameName: '背景封面方洞',
|
||||
themeText: '星空玩具箱',
|
||||
twistRule: '旋转洞口',
|
||||
summary: '作品自身封面为空时使用背景图。',
|
||||
tags: [],
|
||||
coverImageSrc: null,
|
||||
backgroundPrompt: '星空玩具箱',
|
||||
backgroundImageSrc: '/square-hole-background.png',
|
||||
shapeOptions: [],
|
||||
holeOptions: [],
|
||||
shapeCount: 3,
|
||||
difficulty: 1,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-06T00:00:00.000Z',
|
||||
publishReady: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items.find((item) => item.kind === 'puzzle')?.coverImageSrc).toBe(
|
||||
'/puzzle-candidate.png',
|
||||
);
|
||||
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
|
||||
'/match3d-item.png',
|
||||
);
|
||||
expect(items.find((item) => item.kind === 'square-hole')?.coverImageSrc).toBe(
|
||||
'/square-hole-background.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('getCreationWorkShelfItemTime parses backend seconds.microsZ values', () => {
|
||||
expect(getCreationWorkShelfItemTime('1778457601.234567Z')).toBe(
|
||||
1778457601234.567,
|
||||
|
||||
@@ -364,6 +364,7 @@ function mapMatch3DWorkToShelfItem(
|
||||
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
||||
const publicWorkCode =
|
||||
status === 'published' ? buildMatch3DPublicWorkCode(item.profileId) : null;
|
||||
const coverImageSrc = resolveMatch3DWorkCoverImageSrc(item);
|
||||
|
||||
return {
|
||||
id: item.workId,
|
||||
@@ -372,7 +373,7 @@ function mapMatch3DWorkToShelfItem(
|
||||
title: item.gameName,
|
||||
summary: item.summary,
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc: item.coverImageSrc ?? null,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
publicWorkCode,
|
||||
@@ -408,6 +409,7 @@ function mapPuzzleWorkToShelfItem(
|
||||
const status = item.publicationStatus;
|
||||
const publicWorkCode =
|
||||
status === 'published' ? buildPuzzlePublicWorkCode(item.profileId) : null;
|
||||
const coverImageSrc = resolvePuzzleWorkCoverImageSrc(item);
|
||||
|
||||
return {
|
||||
id: item.workId,
|
||||
@@ -419,7 +421,7 @@ function mapPuzzleWorkToShelfItem(
|
||||
item.summary.trim() ||
|
||||
(status === 'draft' ? '未填写作品描述' : ''),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc: item.coverImageSrc ?? null,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
publicWorkCode,
|
||||
@@ -571,6 +573,7 @@ function mapSquareHoleWorkToShelfItem(
|
||||
status === 'published'
|
||||
? buildSquareHolePublicWorkCode(item.profileId)
|
||||
: null;
|
||||
const coverImageSrc = resolveSquareHoleWorkCoverImageSrc(item);
|
||||
|
||||
return {
|
||||
id: item.workId,
|
||||
@@ -579,7 +582,7 @@ function mapSquareHoleWorkToShelfItem(
|
||||
title: item.gameName,
|
||||
summary: item.summary,
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc: item.coverImageSrc ?? null,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
publicWorkCode,
|
||||
@@ -607,6 +610,90 @@ function mapSquareHoleWorkToShelfItem(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCoverImageSrc(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function resolvePuzzleWorkCoverImageSrc(item: PuzzleWorkSummary) {
|
||||
const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc);
|
||||
if (directCoverImageSrc) {
|
||||
return directCoverImageSrc;
|
||||
}
|
||||
|
||||
for (const level of item.levels ?? []) {
|
||||
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 levelCoverImageSrc =
|
||||
selectedCandidateImageSrc ||
|
||||
normalizeCoverImageSrc(level.coverImageSrc) ||
|
||||
fallbackCandidateImageSrc;
|
||||
|
||||
if (levelCoverImageSrc) {
|
||||
return levelCoverImageSrc;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveMatch3DWorkCoverImageSrc(item: Match3DWorkSummary) {
|
||||
const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc);
|
||||
if (directCoverImageSrc) {
|
||||
return directCoverImageSrc;
|
||||
}
|
||||
|
||||
const backgroundImageSrc =
|
||||
normalizeCoverImageSrc(item.backgroundImageSrc) ||
|
||||
normalizeCoverImageSrc(item.generatedBackgroundAsset?.imageSrc) ||
|
||||
normalizeCoverImageSrc(item.generatedBackgroundAsset?.containerImageSrc);
|
||||
if (backgroundImageSrc) {
|
||||
return backgroundImageSrc;
|
||||
}
|
||||
|
||||
for (const asset of item.generatedItemAssets ?? []) {
|
||||
const imageViewSrc = normalizeCoverImageSrc(
|
||||
asset.imageViews?.find((view) => normalizeCoverImageSrc(view.imageSrc))
|
||||
?.imageSrc,
|
||||
);
|
||||
const itemImageSrc = normalizeCoverImageSrc(asset.imageSrc);
|
||||
if (imageViewSrc || itemImageSrc) {
|
||||
return imageViewSrc || itemImageSrc;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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>,
|
||||
|
||||
Reference in New Issue
Block a user