1
This commit is contained in:
@@ -310,6 +310,16 @@ const originalMatchMedia = window.matchMedia;
|
||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||
const originalCancelAnimationFrame = window.cancelAnimationFrame;
|
||||
|
||||
function dispatchPointerEvent(
|
||||
target: HTMLElement,
|
||||
type: string,
|
||||
options: { pointerId: number; clientY: number },
|
||||
) {
|
||||
const event = new Event(type, { bubbles: true, cancelable: true });
|
||||
Object.assign(event, options);
|
||||
target.dispatchEvent(event);
|
||||
}
|
||||
|
||||
const puzzlePublicEntry = {
|
||||
sourceType: 'puzzle',
|
||||
workId: 'puzzle-work-public-1',
|
||||
@@ -1298,6 +1308,132 @@ test('mobile recommend loading state is themed instead of hardcoded black', () =
|
||||
expect(document.querySelector('.platform-recommend-cover-only')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('logged in recommend runtime preloads adjacent work previews and drag switches like video feed', () => {
|
||||
vi.useFakeTimers();
|
||||
const onSelectNextRecommendEntry = vi.fn();
|
||||
const onSelectPreviousRecommendEntry = vi.fn();
|
||||
const firstEntry = {
|
||||
...puzzlePublicEntry,
|
||||
workId: 'puzzle-work-feed-1',
|
||||
profileId: 'puzzle-profile-feed-1',
|
||||
ownerUserId: 'user-feed-1',
|
||||
publicWorkCode: 'PZ-FEED1',
|
||||
worldName: '当前拼图',
|
||||
coverImageSrc: 'current-cover.png',
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
const secondEntry = {
|
||||
...puzzlePublicEntry,
|
||||
workId: 'puzzle-work-feed-2',
|
||||
profileId: 'puzzle-profile-feed-2',
|
||||
ownerUserId: 'user-feed-2',
|
||||
publicWorkCode: 'PZ-FEED2',
|
||||
worldName: '下一拼图',
|
||||
coverImageSrc: 'next-cover.png',
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
const thirdEntry = {
|
||||
...puzzlePublicEntry,
|
||||
workId: 'puzzle-work-feed-3',
|
||||
profileId: 'puzzle-profile-feed-3',
|
||||
ownerUserId: 'user-feed-3',
|
||||
publicWorkCode: 'PZ-FEED3',
|
||||
worldName: '上一拼图',
|
||||
coverImageSrc: 'previous-cover.png',
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
|
||||
render(
|
||||
<AuthUiContext.Provider
|
||||
value={{
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: vi.fn(),
|
||||
requireAuth: (action) => action(),
|
||||
openSettingsModal: vi.fn(),
|
||||
openAccountModal: vi.fn(),
|
||||
setCurrentUser: vi.fn(),
|
||||
logout: vi.fn(async () => undefined),
|
||||
musicVolume: 0.42,
|
||||
setMusicVolume: vi.fn(),
|
||||
platformTheme: 'light',
|
||||
setPlatformTheme: vi.fn(),
|
||||
isHydratingSettings: false,
|
||||
isPersistingSettings: false,
|
||||
settingsError: null,
|
||||
}}
|
||||
>
|
||||
<RpgEntryHomeView
|
||||
activeTab="home"
|
||||
onTabChange={vi.fn()}
|
||||
hasSavedGame={false}
|
||||
savedSnapshot={null}
|
||||
saveEntries={[]}
|
||||
saveError={null}
|
||||
featuredEntries={[]}
|
||||
latestEntries={[firstEntry, secondEntry, thirdEntry]}
|
||||
myEntries={[]}
|
||||
historyEntries={[]}
|
||||
profileDashboard={null}
|
||||
isLoadingPlatform={false}
|
||||
isLoadingDashboard={false}
|
||||
isResumingSaveWorldKey={null}
|
||||
platformError={null}
|
||||
dashboardError={null}
|
||||
onContinueGame={vi.fn()}
|
||||
onResumeSave={vi.fn()}
|
||||
onOpenCreateWorld={vi.fn()}
|
||||
onOpenCreateTypePicker={vi.fn()}
|
||||
onOpenGalleryDetail={vi.fn()}
|
||||
recommendRuntimeContent={<div data-testid="recommend-runtime" />}
|
||||
activeRecommendEntryKey="puzzle:user-feed-1:puzzle-profile-feed-1"
|
||||
onSelectNextRecommendEntry={onSelectNextRecommendEntry}
|
||||
onSelectPreviousRecommendEntry={onSelectPreviousRecommendEntry}
|
||||
onOpenLibraryDetail={vi.fn()}
|
||||
onSearchPublicCode={vi.fn()}
|
||||
/>
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
expect(
|
||||
document.querySelectorAll('.platform-recommend-runtime-preview'),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
document.querySelectorAll('.platform-recommend-swipe-card'),
|
||||
).toHaveLength(3);
|
||||
expect(screen.getAllByText('下一拼图').length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.getAllByText('上一拼图').length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const meta = screen.getByLabelText('当前拼图 作品信息') as HTMLElement;
|
||||
act(() => {
|
||||
dispatchPointerEvent(meta, 'pointerdown', { pointerId: 1, clientY: 300 });
|
||||
dispatchPointerEvent(meta, 'pointermove', { pointerId: 1, clientY: 210 });
|
||||
});
|
||||
|
||||
const rail = document.querySelector(
|
||||
'.platform-recommend-swipe-rail',
|
||||
) as HTMLElement | null;
|
||||
expect(rail?.className).toContain('platform-recommend-swipe-rail');
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent(meta, 'pointerup', { pointerId: 1, clientY: 210 });
|
||||
vi.advanceTimersByTime(180);
|
||||
});
|
||||
|
||||
expect(onSelectNextRecommendEntry).toHaveBeenCalledTimes(1);
|
||||
expect(onSelectPreviousRecommendEntry).not.toHaveBeenCalled();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('logged out active recommend bottom tab selects next work without login', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelectNextRecommendEntry = vi.fn();
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
type ComponentType,
|
||||
type CSSProperties,
|
||||
type PointerEvent,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
@@ -171,6 +172,8 @@ const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
||||
const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
|
||||
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
|
||||
const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
|
||||
const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
|
||||
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
||||
|
||||
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
||||
type DiscoverChannel = 'recommend' | 'today' | 'category' | 'ranking';
|
||||
@@ -727,18 +730,100 @@ function CreationLibraryCard({
|
||||
);
|
||||
}
|
||||
|
||||
function RecommendRuntimeMeta({
|
||||
function RecommendRuntimePreviewCard({
|
||||
entry,
|
||||
position,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
position: 'previous' | 'next';
|
||||
}) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
const typeLabel = describePublicGalleryCardKind(entry);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="platform-recommend-runtime-preview"
|
||||
aria-hidden="true"
|
||||
data-preview-position={position}
|
||||
>
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
alt=""
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_16%,rgba(255,255,255,0.28),transparent_30%),linear-gradient(135deg,rgba(255,118,117,0.42),rgba(89,164,255,0.34))]" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(0,0,0,0.08),rgba(0,0,0,0.42))]" />
|
||||
<div className="platform-recommend-runtime-preview__body">
|
||||
<span className="platform-public-work-card__kind">{typeLabel}</span>
|
||||
<span className="platform-recommend-runtime-preview__title">
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RecommendSwipeCard({
|
||||
entry,
|
||||
authorAvatarUrl,
|
||||
onSelectNext,
|
||||
onSelectPrevious,
|
||||
isActive,
|
||||
visual,
|
||||
onDragPointerDown,
|
||||
onDragPointerMove,
|
||||
onDragPointerUp,
|
||||
onDragPointerCancel,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
onSelectNext?: () => void;
|
||||
onSelectPrevious?: () => void;
|
||||
isActive: boolean;
|
||||
visual: ReactNode;
|
||||
onDragPointerDown?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerCancel?: (event: PointerEvent<HTMLElement>) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`platform-recommend-swipe-card ${isActive ? 'platform-recommend-swipe-card--active' : 'platform-recommend-swipe-card--preview'}`}
|
||||
data-active={isActive ? 'true' : 'false'}
|
||||
>
|
||||
<div className="platform-recommend-swipe-card__visual">{visual}</div>
|
||||
<div className="platform-recommend-swipe-card__meta">
|
||||
<RecommendRuntimeMeta
|
||||
entry={entry}
|
||||
authorAvatarUrl={authorAvatarUrl}
|
||||
isActive={isActive}
|
||||
onDragPointerDown={onDragPointerDown}
|
||||
onDragPointerMove={onDragPointerMove}
|
||||
onDragPointerUp={onDragPointerUp}
|
||||
onDragPointerCancel={onDragPointerCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RecommendRuntimeMeta({
|
||||
entry,
|
||||
authorAvatarUrl,
|
||||
onDragPointerDown,
|
||||
onDragPointerMove,
|
||||
onDragPointerUp,
|
||||
onDragPointerCancel,
|
||||
isActive = true,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
onDragPointerDown?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerCancel?: (event: PointerEvent<HTMLElement>) => void;
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const swipeStartYRef = useRef<number | null>(null);
|
||||
const playCount = getPlatformWorldPlayCount(entry);
|
||||
const remixCount = getPlatformWorldRemixCount(entry);
|
||||
const likeCount = getPlatformWorldLikeCount(entry);
|
||||
@@ -751,37 +836,23 @@ function RecommendRuntimeMeta({
|
||||
{ label: '点赞', value: likeCount, icon: Heart },
|
||||
{ label: '改造', value: remixCount, icon: MessageCircle },
|
||||
];
|
||||
const handlePointerEnd = (clientY: number) => {
|
||||
const startY = swipeStartYRef.current;
|
||||
swipeStartYRef.current = null;
|
||||
if (startY === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaY = clientY - startY;
|
||||
if (Math.abs(deltaY) < RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deltaY < 0) {
|
||||
onSelectNext?.();
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectPrevious?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className="platform-recommend-work-meta"
|
||||
className={`platform-recommend-work-meta ${
|
||||
isActive
|
||||
? 'platform-recommend-work-meta--active'
|
||||
: 'platform-recommend-work-meta--preview'
|
||||
}`}
|
||||
aria-label={`${entry.worldName} 作品信息`}
|
||||
aria-hidden={!isActive}
|
||||
data-active={isActive ? 'true' : 'false'}
|
||||
onPointerDown={(event) => {
|
||||
swipeStartYRef.current = event.clientY;
|
||||
}}
|
||||
onPointerUp={(event) => handlePointerEnd(event.clientY)}
|
||||
onPointerCancel={() => {
|
||||
swipeStartYRef.current = null;
|
||||
onDragPointerDown?.(event);
|
||||
}}
|
||||
onPointerMove={onDragPointerMove}
|
||||
onPointerUp={onDragPointerUp}
|
||||
onPointerCancel={onDragPointerCancel}
|
||||
>
|
||||
<div className="platform-recommend-work-meta__stats">
|
||||
{statItems.map(({ label, value, icon: Icon }) => (
|
||||
@@ -3769,6 +3840,171 @@ export function RpgEntryHomeView({
|
||||
) ??
|
||||
recommendedFeedEntries[0] ??
|
||||
null;
|
||||
const activeRecommendIndex = activeRecommendEntry
|
||||
? recommendedFeedEntries.findIndex(
|
||||
(entry) =>
|
||||
buildPublicGalleryCardKey(entry) ===
|
||||
buildPublicGalleryCardKey(activeRecommendEntry),
|
||||
)
|
||||
: -1;
|
||||
const previousRecommendEntry =
|
||||
activeRecommendIndex >= 0 && recommendedFeedEntries.length > 1
|
||||
? recommendedFeedEntries[
|
||||
(activeRecommendIndex - 1 + recommendedFeedEntries.length) %
|
||||
recommendedFeedEntries.length
|
||||
]
|
||||
: null;
|
||||
const nextRecommendEntry =
|
||||
activeRecommendIndex >= 0 && recommendedFeedEntries.length > 1
|
||||
? recommendedFeedEntries[
|
||||
(activeRecommendIndex + 1) % recommendedFeedEntries.length
|
||||
]
|
||||
: null;
|
||||
const [recommendDragOffsetY, setRecommendDragOffsetY] = useState(0);
|
||||
const [recommendDragCommitDirection, setRecommendDragCommitDirection] =
|
||||
useState<1 | -1 | null>(null);
|
||||
const recommendCardStageRef = useRef<HTMLDivElement | null>(null);
|
||||
const recommendDragStartRef = useRef<{
|
||||
pointerId: number;
|
||||
startY: number;
|
||||
dragging: boolean;
|
||||
} | null>(null);
|
||||
const commitRecommendDrag = useCallback(
|
||||
(direction: 1 | -1) => {
|
||||
if (recommendDragCommitDirection) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRecommendDragCommitDirection(direction);
|
||||
const panelHeight =
|
||||
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
|
||||
const commitDistance =
|
||||
panelHeight > 0 ? panelHeight : window.innerHeight;
|
||||
setRecommendDragOffsetY(
|
||||
direction === 1 ? -commitDistance : commitDistance,
|
||||
);
|
||||
window.setTimeout(() => {
|
||||
if (direction === 1) {
|
||||
onSelectNextRecommendEntry?.();
|
||||
} else {
|
||||
onSelectPreviousRecommendEntry?.();
|
||||
}
|
||||
setRecommendDragOffsetY(0);
|
||||
setRecommendDragCommitDirection(null);
|
||||
}, RECOMMEND_ENTRY_COMMIT_ANIMATION_MS);
|
||||
},
|
||||
[
|
||||
onSelectNextRecommendEntry,
|
||||
onSelectPreviousRecommendEntry,
|
||||
recommendDragCommitDirection,
|
||||
],
|
||||
);
|
||||
const beginRecommendDrag = useCallback(
|
||||
(event: PointerEvent<HTMLElement>) => {
|
||||
if (
|
||||
recommendDragCommitDirection ||
|
||||
!isAuthenticated ||
|
||||
!activeRecommendEntry ||
|
||||
recommendedFeedEntries.length <= 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
recommendDragStartRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
startY: event.clientY,
|
||||
dragging: false,
|
||||
};
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
},
|
||||
[
|
||||
activeRecommendEntry,
|
||||
isAuthenticated,
|
||||
recommendDragCommitDirection,
|
||||
recommendedFeedEntries.length,
|
||||
],
|
||||
);
|
||||
const moveRecommendDrag = useCallback((event: PointerEvent<HTMLElement>) => {
|
||||
const drag = recommendDragStartRef.current;
|
||||
if (!drag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaY = event.clientY - drag.startY;
|
||||
drag.dragging =
|
||||
drag.dragging || Math.abs(deltaY) >= RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX / 2;
|
||||
if (!drag.dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const cardHeight =
|
||||
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
|
||||
const dragLimit =
|
||||
cardHeight > 0 ? cardHeight : RECOMMEND_ENTRY_DRAG_LIMIT_PX;
|
||||
setRecommendDragOffsetY(
|
||||
Math.max(
|
||||
-dragLimit,
|
||||
Math.min(dragLimit, deltaY),
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
const endRecommendDrag = useCallback(
|
||||
(event: PointerEvent<HTMLElement>) => {
|
||||
const drag = recommendDragStartRef.current;
|
||||
if (!drag) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.currentTarget.releasePointerCapture?.(drag.pointerId);
|
||||
recommendDragStartRef.current = null;
|
||||
const deltaY = event.clientY - drag.startY;
|
||||
if (Math.abs(deltaY) < RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX) {
|
||||
setRecommendDragOffsetY(0);
|
||||
return;
|
||||
}
|
||||
|
||||
commitRecommendDrag(deltaY < 0 ? 1 : -1);
|
||||
},
|
||||
[commitRecommendDrag],
|
||||
);
|
||||
const cancelRecommendDrag = useCallback(
|
||||
(event: PointerEvent<HTMLElement>) => {
|
||||
const drag = recommendDragStartRef.current;
|
||||
if (drag) {
|
||||
event.currentTarget.releasePointerCapture?.(drag.pointerId);
|
||||
}
|
||||
recommendDragStartRef.current = null;
|
||||
setRecommendDragOffsetY(0);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const recommendRailStyle = {
|
||||
transform: `translate3d(0, ${recommendDragOffsetY}px, 0)`,
|
||||
} satisfies CSSProperties;
|
||||
const recommendRailClassName = recommendDragCommitDirection
|
||||
? 'platform-recommend-swipe-rail--committing'
|
||||
: recommendDragOffsetY === 0
|
||||
? 'platform-recommend-swipe-rail--settled'
|
||||
: 'platform-recommend-swipe-rail--dragging';
|
||||
const selectNextRecommendEntry = useCallback(() => {
|
||||
if (
|
||||
isAuthenticated &&
|
||||
activeRecommendEntry &&
|
||||
recommendedFeedEntries.length > 1
|
||||
) {
|
||||
commitRecommendDrag(1);
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectNextRecommendEntry?.();
|
||||
}, [
|
||||
activeRecommendEntry,
|
||||
commitRecommendDrag,
|
||||
isAuthenticated,
|
||||
onSelectNextRecommendEntry,
|
||||
recommendedFeedEntries.length,
|
||||
]);
|
||||
const openActiveRecommendEntry = useCallback(() => {
|
||||
if (!activeRecommendEntry) {
|
||||
return;
|
||||
@@ -3786,12 +4022,6 @@ export function RpgEntryHomeView({
|
||||
isAuthenticated,
|
||||
openRecommendGalleryDetail,
|
||||
]);
|
||||
const selectNextRecommendEntry = useCallback(() => {
|
||||
onSelectNextRecommendEntry?.();
|
||||
}, [onSelectNextRecommendEntry]);
|
||||
const selectPreviousRecommendEntry = useCallback(() => {
|
||||
onSelectPreviousRecommendEntry?.();
|
||||
}, [onSelectPreviousRecommendEntry]);
|
||||
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
|
||||
const openLeadPublicEntry = () => {
|
||||
if (leadPublicEntry) {
|
||||
@@ -3863,18 +4093,22 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
{isLoadingPlatform ? (
|
||||
{isLoadingPlatform ? (
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
<div className="platform-recommend-runtime-state">
|
||||
正在读取公开作品...
|
||||
</div>
|
||||
) : !isAuthenticated && activeRecommendEntry ? (
|
||||
</section>
|
||||
) : !isAuthenticated && activeRecommendEntry ? (
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
<RecommendCoverOnlyCard
|
||||
entry={activeRecommendEntry}
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
|
||||
onClick={openActiveRecommendEntry}
|
||||
/>
|
||||
) : recommendRuntimeError ? (
|
||||
</section>
|
||||
) : recommendRuntimeError ? (
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -3886,25 +4120,81 @@ export function RpgEntryHomeView({
|
||||
>
|
||||
{recommendRuntimeError}
|
||||
</button>
|
||||
) : isStartingRecommendEntry || !recommendRuntimeContent ? (
|
||||
</section>
|
||||
) : isStartingRecommendEntry || !recommendRuntimeContent ? (
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
<div className="platform-recommend-runtime-state">加载中...</div>
|
||||
) : (
|
||||
<div className="platform-recommend-runtime-viewport">
|
||||
{recommendRuntimeContent}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
) : activeRecommendEntry ? (
|
||||
<div
|
||||
ref={recommendCardStageRef}
|
||||
className="platform-recommend-swipe-stage"
|
||||
>
|
||||
<div
|
||||
className={`platform-recommend-swipe-rail ${recommendRailClassName}`}
|
||||
style={recommendRailStyle}
|
||||
>
|
||||
{previousRecommendEntry ? (
|
||||
<div className="platform-recommend-swipe-page platform-recommend-swipe-page--previous">
|
||||
<RecommendSwipeCard
|
||||
entry={previousRecommendEntry}
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||
previousRecommendEntry,
|
||||
)}
|
||||
isActive={false}
|
||||
visual={
|
||||
<RecommendRuntimePreviewCard
|
||||
entry={previousRecommendEntry}
|
||||
position="previous"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeRecommendEntry && isAuthenticated ? (
|
||||
<RecommendRuntimeMeta
|
||||
entry={activeRecommendEntry}
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
|
||||
onSelectNext={selectNextRecommendEntry}
|
||||
onSelectPrevious={selectPreviousRecommendEntry}
|
||||
/>
|
||||
) : !isLoadingPlatform ? (
|
||||
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
|
||||
) : null}
|
||||
<div className="platform-recommend-swipe-page platform-recommend-swipe-page--current">
|
||||
<RecommendSwipeCard
|
||||
entry={activeRecommendEntry}
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||
activeRecommendEntry,
|
||||
)}
|
||||
isActive
|
||||
visual={
|
||||
<div className="platform-recommend-runtime-viewport">
|
||||
{recommendRuntimeContent}
|
||||
</div>
|
||||
}
|
||||
onDragPointerDown={beginRecommendDrag}
|
||||
onDragPointerMove={moveRecommendDrag}
|
||||
onDragPointerUp={endRecommendDrag}
|
||||
onDragPointerCancel={cancelRecommendDrag}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{nextRecommendEntry ? (
|
||||
<div className="platform-recommend-swipe-page platform-recommend-swipe-page--next">
|
||||
<RecommendSwipeCard
|
||||
entry={nextRecommendEntry}
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||
nextRecommendEntry,
|
||||
)}
|
||||
isActive={false}
|
||||
visual={
|
||||
<RecommendRuntimePreviewCard
|
||||
entry={nextRecommendEntry}
|
||||
position="next"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user