1
This commit is contained in:
@@ -67,6 +67,7 @@ import {
|
||||
} from '../../services/match3d-works';
|
||||
import {
|
||||
createPuzzleAgentSession,
|
||||
executePuzzleAgentAction,
|
||||
getPuzzleAgentSession,
|
||||
} from '../../services/puzzle-agent';
|
||||
import {
|
||||
@@ -463,9 +464,17 @@ vi.mock('../puzzle-agent/PuzzleAgentWorkspace', () => ({
|
||||
|
||||
vi.mock('../puzzle-result/PuzzleResultView', () => ({
|
||||
PuzzleResultView: ({
|
||||
isBusy,
|
||||
onExecuteAction,
|
||||
session,
|
||||
onBack,
|
||||
}: {
|
||||
isBusy?: boolean;
|
||||
onExecuteAction: (payload: {
|
||||
action: string;
|
||||
levelId?: string;
|
||||
promptText?: string;
|
||||
}) => void;
|
||||
session: { draft?: { levelName: string } | null };
|
||||
onBack: () => void;
|
||||
}) => (
|
||||
@@ -475,6 +484,21 @@ vi.mock('../puzzle-result/PuzzleResultView', () => ({
|
||||
关卡名
|
||||
<input readOnly value={session.draft?.levelName ?? ''} />
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onExecuteAction({
|
||||
action: 'generate_puzzle_images',
|
||||
levelId: 'puzzle-level-1',
|
||||
promptText: '重新生成猫街',
|
||||
});
|
||||
}}
|
||||
>
|
||||
重新生成画面
|
||||
</button>
|
||||
<button type="button" disabled={isBusy}>
|
||||
新增关卡
|
||||
</button>
|
||||
<button type="button" onClick={onBack}>
|
||||
返回
|
||||
</button>
|
||||
@@ -550,14 +574,36 @@ vi.mock('../big-fish-result/BigFishResultView', () => ({
|
||||
vi.mock('../match3d-creation/Match3DAgentWorkspace', () => ({
|
||||
Match3DAgentWorkspace: ({
|
||||
session,
|
||||
onCreateFromForm,
|
||||
}: {
|
||||
session: { sessionId: string; messages: Array<{ text: string }> } | null;
|
||||
onCreateFromForm?: (payload: {
|
||||
seedText: string;
|
||||
themeText: string;
|
||||
referenceImageSrc: string | null;
|
||||
clearCount: number;
|
||||
difficulty: number;
|
||||
}) => void;
|
||||
}) => (
|
||||
<div className="match3d-agent-workspace-mock">
|
||||
<div>抓大鹅工作区:{session?.sessionId ?? 'missing-session'}</div>
|
||||
{session?.messages.map((message) => (
|
||||
<div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onCreateFromForm?.({
|
||||
seedText: '赛博水果摊题材,消除9次,难度6',
|
||||
themeText: '赛博水果摊',
|
||||
referenceImageSrc: null,
|
||||
clearCount: 9,
|
||||
difficulty: 6,
|
||||
});
|
||||
}}
|
||||
>
|
||||
生成抓大鹅草稿
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -2355,6 +2401,9 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'AIRP' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/airp.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '抓大鹅' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/match3d.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('.text-white'),
|
||||
).toBeTruthy();
|
||||
@@ -2364,12 +2413,29 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
expect(screen.queryByRole('button', { name: /智能创作/u })).toBeNull();
|
||||
expect(screen.queryByPlaceholderText('问一问百梦')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /角色扮演/u })).toBeNull();
|
||||
expect(screen.queryByRole('tab', { name: /抓大鹅/u })).toBeNull();
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('create tab switches match3d into the embedded entry form', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '抓大鹅' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(
|
||||
await screen.findByText('抓大鹅工作区:missing-session'),
|
||||
).toBeTruthy();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('embedded puzzle form routes through requireAuth while logged out', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
@@ -2826,6 +2892,159 @@ test('owned public puzzle detail edits original draft instead of remixing', asyn
|
||||
expect(getPuzzleAgentSession).toHaveBeenCalledWith('puzzle-session-1');
|
||||
expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
|
||||
expect(await screen.findByText('拼图结果页')).toBeTruthy();
|
||||
|
||||
vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
|
||||
operation: {
|
||||
operationId: 'puzzle-image-generation-1',
|
||||
type: 'generate_puzzle_images',
|
||||
status: 'running',
|
||||
phaseLabel: '生成中',
|
||||
phaseDetail: '正在生成拼图画面',
|
||||
progress: 0.3,
|
||||
},
|
||||
session: {
|
||||
sessionId: 'puzzle-session-1',
|
||||
currentTurn: 3,
|
||||
progressPercent: 88,
|
||||
stage: 'ready_to_publish',
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'theme_promise',
|
||||
label: '主题承诺',
|
||||
value: '雨夜猫街',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visual_subject',
|
||||
label: '视觉主体',
|
||||
value: '屋檐下的猫',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visual_mood',
|
||||
label: '视觉气质',
|
||||
value: '温暖',
|
||||
status: 'confirmed',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'composition_hooks',
|
||||
label: '构图钩子',
|
||||
value: '雨滴与灯牌',
|
||||
status: 'confirmed',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tags_and_forbidden',
|
||||
label: '标签与禁区',
|
||||
value: '猫咪、雨夜',
|
||||
status: 'confirmed',
|
||||
},
|
||||
},
|
||||
draft: {
|
||||
workTitle: '暖灯猫街作品',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
levelName: '雨夜猫街',
|
||||
summary: '屋檐下的猫与暖灯街角。',
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
forbiddenDirectives: [],
|
||||
creatorIntent: null,
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'theme_promise',
|
||||
label: '主题承诺',
|
||||
value: '雨夜猫街',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visual_subject',
|
||||
label: '视觉主体',
|
||||
value: '屋檐下的猫',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visual_mood',
|
||||
label: '视觉气质',
|
||||
value: '温暖',
|
||||
status: 'confirmed',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'composition_hooks',
|
||||
label: '构图钩子',
|
||||
value: '雨滴与灯牌',
|
||||
status: 'confirmed',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tags_and_forbidden',
|
||||
label: '标签与禁区',
|
||||
value: '猫咪、雨夜',
|
||||
status: 'confirmed',
|
||||
},
|
||||
},
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/candidate-1.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '雨夜猫咪',
|
||||
actualPrompt: null,
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: '/puzzle/candidate-1.png',
|
||||
coverAssetId: 'asset-1',
|
||||
generationStatus: 'generating',
|
||||
levels: [
|
||||
{
|
||||
levelId: 'puzzle-level-1',
|
||||
levelName: '雨夜猫街',
|
||||
pictureDescription: '屋檐下的猫与暖灯街角。',
|
||||
pictureReference: null,
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/candidate-1.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '雨夜猫咪',
|
||||
actualPrompt: null,
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: '/puzzle/candidate-1.png',
|
||||
coverAssetId: 'asset-1',
|
||||
generationStatus: 'generating',
|
||||
},
|
||||
],
|
||||
metadata: null,
|
||||
},
|
||||
messages: [],
|
||||
lastAssistantReply: '大鱼结果页草稿已经生成,可以补正式资产。',
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: {
|
||||
draft: null,
|
||||
publishReady: false,
|
||||
blockers: [],
|
||||
qualityFindings: [],
|
||||
},
|
||||
updatedAt: '2026-04-26T10:10:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '重新生成画面' }));
|
||||
|
||||
expect(executePuzzleAgentAction).toHaveBeenCalledWith(
|
||||
'puzzle-session-1',
|
||||
expect.objectContaining({
|
||||
action: 'generate_puzzle_images',
|
||||
}),
|
||||
);
|
||||
expect(screen.getByRole('button', { name: '新增关卡' })).toHaveProperty(
|
||||
'disabled',
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('logged out public detail gates big fish start before local runtime', async () => {
|
||||
@@ -3027,7 +3246,6 @@ test('published puzzle works appear on home and mobile game category channel', a
|
||||
});
|
||||
|
||||
test('home recommendation starts embedded puzzle without global auth reset on local failure', async () => {
|
||||
const user = userEvent.setup();
|
||||
const publishedPuzzleWork = {
|
||||
workId: 'puzzle-work-public-1',
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
@@ -3396,7 +3614,7 @@ test('embedded puzzle form timeout exits busy state and shows a readable error',
|
||||
expect(streamCreativeAgentMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('hidden match3d creation card stays closed even when public galleries fail', async () => {
|
||||
test('match3d creation tab stays usable even when public galleries fail', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listRpgEntryWorldGallery).mockRejectedValueOnce(
|
||||
@@ -3411,9 +3629,7 @@ test('hidden match3d creation card stays closed even when public galleries fail'
|
||||
await openCreateTemplateHub(user);
|
||||
expect(screen.queryByText('读取作品广场失败')).toBeNull();
|
||||
expect(screen.queryByText('读取抓大鹅广场失败')).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('tab', { name: /抓大鹅.*经典消除玩法/u }),
|
||||
).toBeNull();
|
||||
expect(screen.getByRole('tab', { name: '抓大鹅' })).toBeTruthy();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1312,6 +1312,8 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
|
||||
vi.useFakeTimers();
|
||||
const onSelectNextRecommendEntry = vi.fn();
|
||||
const onSelectPreviousRecommendEntry = vi.fn();
|
||||
const onLikeRecommendEntry = vi.fn();
|
||||
const onRemixRecommendEntry = vi.fn();
|
||||
const firstEntry = {
|
||||
...puzzlePublicEntry,
|
||||
workId: 'puzzle-work-feed-1',
|
||||
@@ -1397,6 +1399,8 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
|
||||
activeRecommendEntryKey="puzzle:user-feed-1:puzzle-profile-feed-1"
|
||||
onSelectNextRecommendEntry={onSelectNextRecommendEntry}
|
||||
onSelectPreviousRecommendEntry={onSelectPreviousRecommendEntry}
|
||||
onLikeRecommendEntry={onLikeRecommendEntry}
|
||||
onRemixRecommendEntry={onRemixRecommendEntry}
|
||||
onOpenLibraryDetail={vi.fn()}
|
||||
onSearchPublicCode={vi.fn()}
|
||||
/>
|
||||
@@ -1412,8 +1416,38 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
|
||||
).toHaveLength(3);
|
||||
expect(screen.getAllByText('下一拼图').length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.getAllByText('上一拼图').length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.queryByText('评论')).toBeNull();
|
||||
expect(screen.queryByLabelText(/游玩/u)).toBeNull();
|
||||
const clipboardWriteText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText: clipboardWriteText },
|
||||
});
|
||||
|
||||
const meta = screen.getByLabelText('当前拼图 作品信息') as HTMLElement;
|
||||
const activeRecommendCard = within(meta);
|
||||
const likeButton = activeRecommendCard.getByRole('button', {
|
||||
name: '点赞 12',
|
||||
});
|
||||
expect(likeButton).toBeTruthy();
|
||||
expect(activeRecommendCard.getByLabelText('12 个赞')).toBeTruthy();
|
||||
const shareButton = activeRecommendCard.getByRole('button', { name: '分享' });
|
||||
const remixButton = activeRecommendCard.getByRole('button', {
|
||||
name: '改造 5',
|
||||
});
|
||||
expect(shareButton).toBeTruthy();
|
||||
expect(remixButton).toBeTruthy();
|
||||
|
||||
fireEvent.click(likeButton);
|
||||
fireEvent.click(shareButton);
|
||||
fireEvent.click(remixButton);
|
||||
|
||||
expect(onLikeRecommendEntry).toHaveBeenCalledWith(firstEntry);
|
||||
expect(onRemixRecommendEntry).toHaveBeenCalledWith(firstEntry);
|
||||
expect(clipboardWriteText).toHaveBeenCalledWith(
|
||||
expect.stringContaining('作品号:PZ-FEED1'),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent(meta, 'pointerdown', { pointerId: 1, clientY: 300 });
|
||||
dispatchPointerEvent(meta, 'pointermove', { pointerId: 1, clientY: 210 });
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Coins,
|
||||
Compass,
|
||||
Copy,
|
||||
GitFork,
|
||||
Gamepad2,
|
||||
Heart,
|
||||
LogIn,
|
||||
@@ -16,10 +17,12 @@ import {
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
Share2,
|
||||
Settings,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Star,
|
||||
ThumbsUp,
|
||||
Ticket,
|
||||
UserPlus,
|
||||
UserRound,
|
||||
@@ -54,6 +57,7 @@ import type {
|
||||
RedeemProfileRewardCodeResponse,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import {
|
||||
getPublicAuthUserByCode,
|
||||
@@ -86,6 +90,7 @@ import {
|
||||
isVisualNovelGalleryEntry,
|
||||
type PlatformPublicGalleryCard,
|
||||
type PlatformWorldCardLike,
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldCoverImage,
|
||||
resolvePlatformWorldCoverSlides,
|
||||
resolvePlatformWorldLeadPortrait,
|
||||
@@ -126,6 +131,8 @@ export interface RpgEntryHomeViewProps {
|
||||
recommendRuntimeError?: string | null;
|
||||
onSelectNextRecommendEntry?: () => void;
|
||||
onSelectPreviousRecommendEntry?: () => void;
|
||||
onLikeRecommendEntry?: (entry: PlatformPublicGalleryCard) => void;
|
||||
onRemixRecommendEntry?: (entry: PlatformPublicGalleryCard) => void;
|
||||
onOpenLibraryDetail: (
|
||||
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
||||
) => void;
|
||||
@@ -772,19 +779,27 @@ function RecommendSwipeCard({
|
||||
authorAvatarUrl,
|
||||
isActive,
|
||||
visual,
|
||||
shareState,
|
||||
onDragPointerDown,
|
||||
onDragPointerMove,
|
||||
onDragPointerUp,
|
||||
onDragPointerCancel,
|
||||
onLike,
|
||||
onShare,
|
||||
onRemix,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
isActive: boolean;
|
||||
visual: ReactNode;
|
||||
shareState?: 'idle' | 'copied' | 'failed';
|
||||
onDragPointerDown?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerCancel?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onLike?: () => void;
|
||||
onShare?: () => void;
|
||||
onRemix?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -797,10 +812,14 @@ function RecommendSwipeCard({
|
||||
entry={entry}
|
||||
authorAvatarUrl={authorAvatarUrl}
|
||||
isActive={isActive}
|
||||
shareState={shareState}
|
||||
onDragPointerDown={onDragPointerDown}
|
||||
onDragPointerMove={onDragPointerMove}
|
||||
onDragPointerUp={onDragPointerUp}
|
||||
onDragPointerCancel={onDragPointerCancel}
|
||||
onLike={onLike}
|
||||
onShare={onShare}
|
||||
onRemix={onRemix}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -814,6 +833,10 @@ function RecommendRuntimeMeta({
|
||||
onDragPointerMove,
|
||||
onDragPointerUp,
|
||||
onDragPointerCancel,
|
||||
shareState = 'idle',
|
||||
onLike,
|
||||
onShare,
|
||||
onRemix,
|
||||
isActive = true,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
@@ -822,20 +845,21 @@ function RecommendRuntimeMeta({
|
||||
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerCancel?: (event: PointerEvent<HTMLElement>) => void;
|
||||
shareState?: 'idle' | 'copied' | 'failed';
|
||||
onLike?: () => void;
|
||||
onShare?: () => void;
|
||||
onRemix?: () => void;
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const playCount = getPlatformWorldPlayCount(entry);
|
||||
const remixCount = getPlatformWorldRemixCount(entry);
|
||||
const likeCount = getPlatformWorldLikeCount(entry);
|
||||
const remixCount = getPlatformWorldRemixCount(entry);
|
||||
const authorName = entry.authorDisplayName.trim() || '玩家';
|
||||
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
const statItems = [
|
||||
{ label: '游玩', value: playCount, icon: Gamepad2 },
|
||||
{ label: '点赞', value: likeCount, icon: Heart },
|
||||
{ label: '改造', value: remixCount, icon: MessageCircle },
|
||||
];
|
||||
const stopActionPointer = (event: PointerEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -854,19 +878,6 @@ function RecommendRuntimeMeta({
|
||||
onPointerUp={onDragPointerUp}
|
||||
onPointerCancel={onDragPointerCancel}
|
||||
>
|
||||
<div className="platform-recommend-work-meta__stats">
|
||||
{statItems.map(({ label, value, icon: Icon }) => (
|
||||
<span
|
||||
key={label}
|
||||
className="platform-recommend-work-meta__stat"
|
||||
aria-label={`${label} ${formatCompactCount(value)}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
<span>{formatCompactCount(value)}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="platform-recommend-work-meta__row">
|
||||
<div
|
||||
className="platform-recommend-work-meta__identity"
|
||||
@@ -894,6 +905,62 @@ function RecommendRuntimeMeta({
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="platform-recommend-work-meta__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="platform-recommend-work-meta__action platform-recommend-work-meta__action--icon platform-recommend-work-meta__action--like"
|
||||
onPointerDown={stopActionPointer}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onLike?.();
|
||||
}}
|
||||
disabled={!isActive || !onLike}
|
||||
aria-label={`点赞 ${formatCompactCount(likeCount)}`}
|
||||
title="点赞"
|
||||
>
|
||||
<ThumbsUp className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<span
|
||||
className="platform-recommend-work-meta__like-count"
|
||||
aria-label={`${formatCompactCount(likeCount)} 个赞`}
|
||||
>
|
||||
{formatCompactCount(likeCount)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-recommend-work-meta__action platform-recommend-work-meta__action--icon"
|
||||
onPointerDown={stopActionPointer}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onShare?.();
|
||||
}}
|
||||
disabled={!isActive || !onShare}
|
||||
aria-label={
|
||||
shareState === 'copied'
|
||||
? '分享内容已复制'
|
||||
: shareState === 'failed'
|
||||
? '分享内容复制失败'
|
||||
: '分享'
|
||||
}
|
||||
title="分享"
|
||||
>
|
||||
<Share2 className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-recommend-work-meta__action platform-recommend-work-meta__action--icon platform-recommend-work-meta__action--remix"
|
||||
onPointerDown={stopActionPointer}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onRemix?.();
|
||||
}}
|
||||
disabled={!isActive || !onRemix}
|
||||
aria-label={`改造 ${formatCompactCount(remixCount)}`}
|
||||
title="改造"
|
||||
>
|
||||
<GitFork className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -2977,6 +3044,8 @@ export function RpgEntryHomeView({
|
||||
recommendRuntimeError = null,
|
||||
onSelectNextRecommendEntry,
|
||||
onSelectPreviousRecommendEntry,
|
||||
onLikeRecommendEntry,
|
||||
onRemixRecommendEntry,
|
||||
onOpenLibraryDetail,
|
||||
onDeleteLibraryEntry,
|
||||
deletingLibraryEntryId = null,
|
||||
@@ -3863,6 +3932,10 @@ export function RpgEntryHomeView({
|
||||
const [recommendDragOffsetY, setRecommendDragOffsetY] = useState(0);
|
||||
const [recommendDragCommitDirection, setRecommendDragCommitDirection] =
|
||||
useState<1 | -1 | null>(null);
|
||||
const [recommendShareState, setRecommendShareState] = useState<
|
||||
'idle' | 'copied' | 'failed'
|
||||
>('idle');
|
||||
const recommendShareResetTimerRef = useRef<number | null>(null);
|
||||
const recommendCardStageRef = useRef<HTMLDivElement | null>(null);
|
||||
const recommendDragStartRef = useRef<{
|
||||
pointerId: number;
|
||||
@@ -4005,6 +4078,36 @@ export function RpgEntryHomeView({
|
||||
onSelectNextRecommendEntry,
|
||||
recommendedFeedEntries.length,
|
||||
]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (recommendShareResetTimerRef.current !== null) {
|
||||
window.clearTimeout(recommendShareResetTimerRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
setRecommendShareState('idle');
|
||||
}, [activeRecommendEntryKey]);
|
||||
const shareRecommendEntry = useCallback((entry: PlatformPublicGalleryCard) => {
|
||||
const publicWorkCode = resolvePlatformPublicWorkCode(entry)?.trim();
|
||||
if (!publicWorkCode) {
|
||||
setRecommendShareState('failed');
|
||||
return;
|
||||
}
|
||||
|
||||
const shareText = `邀请你来玩《${entry.worldName}》\n作品号:${publicWorkCode}\n${buildPublicWorkDetailUrl(publicWorkCode)}`;
|
||||
void copyTextToClipboard(shareText).then((copied) => {
|
||||
setRecommendShareState(copied ? 'copied' : 'failed');
|
||||
if (recommendShareResetTimerRef.current !== null) {
|
||||
window.clearTimeout(recommendShareResetTimerRef.current);
|
||||
}
|
||||
recommendShareResetTimerRef.current = window.setTimeout(() => {
|
||||
recommendShareResetTimerRef.current = null;
|
||||
setRecommendShareState('idle');
|
||||
}, 1400);
|
||||
});
|
||||
}, []);
|
||||
const openActiveRecommendEntry = useCallback(() => {
|
||||
if (!activeRecommendEntry) {
|
||||
return;
|
||||
@@ -4168,6 +4271,10 @@ export function RpgEntryHomeView({
|
||||
onDragPointerMove={moveRecommendDrag}
|
||||
onDragPointerUp={endRecommendDrag}
|
||||
onDragPointerCancel={cancelRecommendDrag}
|
||||
shareState={recommendShareState}
|
||||
onLike={() => onLikeRecommendEntry?.(activeRecommendEntry)}
|
||||
onShare={() => shareRecommendEntry(activeRecommendEntry)}
|
||||
onRemix={() => onRemixRecommendEntry?.(activeRecommendEntry)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user