Merge remote-tracking branch 'origin/master'
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-01 22:14:49 +08:00
151 changed files with 3952 additions and 1299 deletions

View File

@@ -306,6 +306,22 @@ function mapPuzzleWorkToPublicWorkDetail(
return mapPuzzleWorkToPlatformGalleryCard(item);
}
function resolveVisiblePuzzleDetailCoverCount(
entry: PlatformPublicGalleryCard | null,
run: PuzzleRunSnapshot | null,
) {
if (!entry || !isPuzzleGalleryEntry(entry)) {
return 1;
}
if (run?.entryProfileId !== entry.profileId) {
return 1;
}
// 中文注释:封面首图永远公开,后续封面跟随当前玩家本次 run 的通关进度即时解锁。
return Math.max(1, run.clearedLevelCount + 1);
}
function mapMatch3DWorkToPublicWorkDetail(
item: Match3DWorkSummary,
): PlatformPublicGalleryCard {
@@ -3374,48 +3390,53 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isBigFishGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToBigFishWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前作品缺少会话信息,暂时无法进入玩法。');
return;
}
startBigFishRunFromWork(work);
return;
}
if (isPuzzleGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToPuzzleWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前拼图作品信息不完整,暂时无法进入玩法。');
return;
}
setPublicWorkDetailError(null);
void startPuzzleRunFromProfile(work.profileId, 'work-detail', work, true);
return;
}
if (isMatch3DGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToMatch3DWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前抓大鹅作品信息不完整,暂时无法进入玩法。');
return;
}
setPublicWorkDetailError(null);
void startMatch3DRunFromProfile(work, 'work-detail', true);
return;
}
const launchEntry =
selectedDetailEntry?.profileId === selectedPublicWorkDetail.profileId
? selectedDetailEntry
: null;
if (!launchEntry) {
setPublicWorkDetailError('作品详情尚未读取完成。');
return;
}
runProtectedAction(() => {
if (isBigFishGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToBigFishWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前作品缺少会话信息,暂时无法进入玩法。');
return;
}
startBigFishRunFromWork(work);
return;
}
if (isPuzzleGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToPuzzleWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前拼图作品信息不完整,暂时无法进入玩法。');
return;
}
setPublicWorkDetailError(null);
void startPuzzleRunFromProfile(
work.profileId,
'work-detail',
work,
true,
);
return;
}
if (isMatch3DGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToMatch3DWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前抓大鹅作品信息不完整,暂时无法进入玩法。');
return;
}
setPublicWorkDetailError(null);
void startMatch3DRunFromProfile(work, 'work-detail', true);
return;
}
const launchEntry =
selectedDetailEntry?.profileId === selectedPublicWorkDetail.profileId
? selectedDetailEntry
: null;
if (!launchEntry) {
setPublicWorkDetailError('作品详情尚未读取完成。');
return;
}
setIsPublicWorkDetailBusy(true);
void recordRpgEntryWorldGalleryPlay(
launchEntry.ownerUserId,
@@ -3700,7 +3721,7 @@ export function PlatformEntryFlowShellImpl({
setSearchedPublicUser(user);
} catch (error) {
setPublicSearchError(
resolveRpgCreationErrorMessage(error, '未找到对应的陶泥号或作品号。'),
resolveRpgCreationErrorMessage(error, '未找到对应的百梦号或作品号。'),
);
} finally {
setIsSearchingPublicCode(false);
@@ -4166,6 +4187,10 @@ export function PlatformEntryFlowShellImpl({
isMatch3DBusy
}
error={publicWorkDetailError}
visibleCoverCount={resolveVisiblePuzzleDetailCoverCount(
selectedPublicWorkDetail,
puzzleRun,
)}
onBack={() => {
setPublicWorkDetailError(null);
clearSelectedPublicWorkAuthor();
@@ -5126,7 +5151,7 @@ export function PlatformEntryFlowShellImpl({
{searchedPublicUser.displayName}
</div>
<div className="mt-2 text-sm text-[var(--platform-text-soft)]">
{searchedPublicUser.publicUserCode}
{searchedPublicUser.publicUserCode}
</div>
</div>
) : null}

View File

@@ -124,7 +124,7 @@ test('PlatformWorkDetailView calls like handler', () => {
test('PlatformWorkDetailView cycles puzzle level cover slides', () => {
vi.useFakeTimers();
render(
const { container } = render(
<PlatformWorkDetailView
entry={{
...createPuzzleEntry(),
@@ -154,12 +154,23 @@ test('PlatformWorkDetailView cycles puzzle level cover slides', () => {
expect(screen.getAllByAltText('关键词:逍遥游拼图')[0]?.getAttribute('src')).toBe(
'/level-1.png',
);
const appIconImage = container.querySelector(
'.platform-work-detail__app-icon img',
);
expect(appIconImage?.getAttribute('src')).toBe('/level-1.png');
fireEvent.click(screen.getByRole('button', { name: '下一张关卡图' }));
expect(screen.getAllByAltText('关键词:逍遥游拼图')[0]?.getAttribute('src')).toBe(
'/level-2.png',
);
expect(appIconImage?.getAttribute('src')).toBe('/level-1.png');
expect(
container.querySelector('.platform-work-detail__cover-image--locked'),
).toBeTruthy();
expect(
container.querySelector('.platform-work-detail__cover-lock-icon'),
).toBeTruthy();
act(() => {
vi.advanceTimersByTime(4200);
@@ -169,3 +180,44 @@ test('PlatformWorkDetailView cycles puzzle level cover slides', () => {
'/level-1.png',
);
});
test('PlatformWorkDetailView unlocks later puzzle covers by visible cover count', () => {
const { container } = render(
<PlatformWorkDetailView
entry={{
...createPuzzleEntry(),
coverSlides: [
{
id: 'level-1',
imageSrc: '/level-1.png',
label: '第一关',
},
{
id: 'level-2',
imageSrc: '/level-2.png',
label: '第二关',
},
],
}}
visibleCoverCount={2}
isBusy={false}
error={null}
onBack={vi.fn()}
onLike={vi.fn()}
onStart={vi.fn()}
onRemix={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '下一张关卡图' }));
expect(screen.getAllByAltText('关键词:逍遥游拼图')[0]?.getAttribute('src')).toBe(
'/level-2.png',
);
expect(
container.querySelector('.platform-work-detail__cover-image--locked'),
).toBeNull();
expect(
container.querySelector('.platform-work-detail__cover-lock-icon'),
).toBeNull();
});

View File

@@ -2,6 +2,7 @@ import {
ArrowLeft,
ChevronLeft,
ChevronRight,
CircleHelp,
Clock3,
Copy,
Gamepad2,
@@ -32,6 +33,7 @@ export interface PlatformWorkDetailViewProps {
authorDisplayName?: string | null;
isBusy: boolean;
error: string | null;
visibleCoverCount?: number;
onBack: () => void;
onLike: () => void;
onStart: () => void;
@@ -71,6 +73,7 @@ export function PlatformWorkDetailView({
authorDisplayName,
isBusy,
error,
visibleCoverCount = 1,
onBack,
onLike,
onStart,
@@ -84,6 +87,9 @@ export function PlatformWorkDetailView({
const activeCoverSlide =
coverSlides[activeCoverIndex] ?? coverSlides[0] ?? null;
const coverImage = activeCoverSlide?.imageSrc ?? '';
const unlockedCoverCount = Math.max(1, Math.floor(visibleCoverCount));
const isActiveCoverVisible = activeCoverIndex < unlockedCoverCount;
const appIconImage = coverSlides[0]?.imageSrc ?? '';
const hasCoverCarousel = coverSlides.length > 1;
const publicWorkCode = resolvePlatformPublicWorkCode(entry);
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
@@ -237,8 +243,20 @@ export function PlatformWorkDetailView({
<ResolvedAssetImage
src={coverImage}
alt={entry.worldName}
className="platform-work-detail__cover-image"
className={`platform-work-detail__cover-image${
isActiveCoverVisible
? ''
: ' platform-work-detail__cover-image--locked'
}`}
/>
{!isActiveCoverVisible ? (
<div
className="platform-work-detail__cover-lock"
aria-hidden="true"
>
<CircleHelp className="platform-work-detail__cover-lock-icon" />
</div>
) : null}
{hasCoverCarousel ? (
<>
<button
@@ -288,9 +306,9 @@ export function PlatformWorkDetailView({
<section className="platform-work-detail__summary">
<div className="platform-work-detail__meta-row">
<div className="platform-work-detail__app-icon">
{coverImage ? (
{appIconImage ? (
<ResolvedAssetImage
src={coverImage}
src={appIconImage}
alt=""
aria-hidden="true"
className="h-full w-full object-cover"