This commit is contained in:
2026-05-14 14:21:17 +08:00
parent 7a75f5d612
commit d33c937ebc
191 changed files with 1916 additions and 1549 deletions

View File

@@ -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: '领取积分' }));

View File

@@ -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">

View File

@@ -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,

View File

@@ -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>,