Merge remote-tracking branch 'origin/master'
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user