修复作品架已发布卡片分享入口

为作品架分享按钮补充可见底色与回归测试
将已发布作品卡分享动作接入统一发布分享弹窗
同步玩法链路文档中的作品架分享口径
This commit is contained in:
2026-06-08 01:54:11 +08:00
parent ff2ed5a59d
commit 49e4d085b3
7 changed files with 97 additions and 70 deletions

View File

@@ -1043,14 +1043,10 @@ test('creation hub opens persisted rpg drafts by card click', async () => {
expect(openedItems).toEqual([persistedDraft]);
});
test('creation hub published share icon copies share text without opening the card', async () => {
test('creation hub published share icon opens unified share payload without opening the card', async () => {
const user = userEvent.setup();
const writeText = vi.fn(async () => undefined);
const onOpenPuzzleDetail = vi.fn();
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText },
});
const onShareWork = vi.fn();
render(
<CustomWorldCreationHub
@@ -1081,6 +1077,7 @@ test('creation hub published share icon copies share text without opening the ca
onOpenDraft={() => {}}
onEnterPublished={() => {}}
onOpenPuzzleDetail={onOpenPuzzleDetail}
onShareWork={onShareWork}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
@@ -1092,19 +1089,12 @@ test('creation hub published share icon copies share text without opening the ca
await user.click(shareButton);
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('邀请你来玩《沉钟拼图》'),
);
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('作品号PZ-PROFILE1'),
);
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('/gallery/puzzle/detail?work=PZ-PROFILE1'),
);
expect(onShareWork).toHaveBeenCalledWith({
title: '沉钟拼图',
publicWorkCode: 'PZ-PROFILE1',
stage: 'puzzle-gallery-detail',
});
expect(onOpenPuzzleDetail).not.toHaveBeenCalled();
expect(
await screen.findByRole('button', { name: '分享内容已复制' }),
).toBeTruthy();
});
test('creation hub published share icon is shown directly on the card header', () => {

View File

@@ -12,8 +12,10 @@ import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import { resolveSelectionStageFromPath } from '../../routing/appPageRoutes';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import type { CustomWorldProfile } from '../../types';
import type { PublishShareModalPayload } from '../common/publishShareModalModel';
import type {
PlatformCreationTypeCard,
PlatformCreationTypeId,
@@ -99,6 +101,7 @@ type CustomWorldCreationHubProps = {
item: CreationWorkShelfItem,
) => CreationWorkShelfRuntimeState | null;
onOpenShelfItem?: (item: CreationWorkShelfItem) => void;
onShareWork?: ((payload: PublishShareModalPayload) => void) | null;
// 中文注释:底部加号入口可传入后端作品架摘要,用于推导最近使用过的模板。
recentWorkItems?: CreationWorkShelfItem[];
mode?: 'full' | 'start-only' | 'works-only';
@@ -167,6 +170,41 @@ function writeWorkMetricSnapshot(items: CreationWorkShelfItem[]) {
}
}
function resolveShelfShareStage(
sharePath: string,
): PublishShareModalPayload['stage'] | null {
let pathname = '';
try {
pathname = new URL(sharePath, 'https://genarrative.local').pathname;
} catch {
pathname = sharePath.split(/[?#]/u)[0] ?? '';
}
const stage = resolveSelectionStageFromPath(pathname);
return stage === 'platform' ? null : stage;
}
function buildCreationWorkShelfSharePayload(
item: CreationWorkShelfItem,
): PublishShareModalPayload | null {
const publicWorkCode = item.publicWorkCode?.trim();
const sharePath = item.sharePath?.trim();
if (!publicWorkCode || !sharePath) {
return null;
}
const stage = resolveShelfShareStage(sharePath);
if (!stage) {
return null;
}
return {
title: item.title,
publicWorkCode,
stage,
};
}
/** 渲染底部加号创作入口页与草稿作品架,最近创作复用最近使用过的模板入口。 */
export function CustomWorldCreationHub({
items,
@@ -216,6 +254,7 @@ export function CustomWorldCreationHub({
onDeleteVisualNovel = null,
getWorkState,
onOpenShelfItem,
onShareWork = null,
recentWorkItems: recentWorkSourceItems,
mode = 'full',
}: CustomWorldCreationHubProps) {
@@ -406,6 +445,17 @@ export function CustomWorldCreationHub({
return item.actions.delete ?? null;
}
function buildShareAction(item: CreationWorkShelfItem) {
const payload = buildCreationWorkShelfSharePayload(item);
if (!payload) {
return null;
}
return () => {
onShareWork?.(payload);
};
}
function buildPointIncentiveAction(item: CreationWorkShelfItem) {
return item.actions.claimPointIncentive ?? null;
}
@@ -481,6 +531,7 @@ export function CustomWorldCreationHub({
}}
onDelete={buildDeleteAction(item)}
deleteBusy={deletingWorkId === item.id}
onShare={buildShareAction(item)}
onClaimPointIncentive={buildPointIncentiveAction(item)}
pointIncentiveBusy={
item.source.kind === 'puzzle' &&

View File

@@ -18,7 +18,6 @@ import {
useState,
} from 'react';
import { copyTextToClipboard } from '../../services/clipboard';
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
import {
formatPlatformWorkDisplayName,
@@ -40,6 +39,7 @@ type CustomWorldWorkCardProps = {
onOpen: () => void;
onDelete?: (() => void) | null;
deleteBusy?: boolean;
onShare?: (() => void) | null;
onClaimPointIncentive?: (() => void) | null;
pointIncentiveBusy?: boolean;
};
@@ -231,13 +231,10 @@ export function CustomWorldWorkCard({
onOpen,
onDelete = null,
deleteBusy = false,
onShare = null,
onClaimPointIncentive = null,
pointIncentiveBusy = false,
}: CustomWorldWorkCardProps) {
const [shareState, setShareState] = useState<'idle' | 'copied' | 'failed'>(
'idle',
);
const shareResetTimerRef = useRef<number | null>(null);
const suppressOpenResetTimerRef = useRef<number | null>(null);
const suppressOpenRef = useRef(false);
const swipeGestureRef = useRef<{
@@ -253,7 +250,7 @@ export function CustomWorldWorkCard({
const [swipeOffset, setSwipeOffset] = useState(0);
const isPublished = item.status === 'published';
const canUseShareAction =
isPublished && item.canShare && Boolean(item.sharePath);
isPublished && item.canShare && Boolean(item.sharePath) && Boolean(onShare);
const swipeActionCount = onDelete ? 1 : 0;
const swipeRevealWidth = swipeActionCount * SWIPE_ACTION_WIDTH_PX;
const canClaimPointIncentive =
@@ -289,34 +286,8 @@ export function CustomWorldWorkCard({
}`,
} as CSSProperties;
const copyShareText = () => {
const publicWorkCode = item.publicWorkCode?.trim();
const sharePath = item.sharePath?.trim();
if (!publicWorkCode || !sharePath) {
return;
}
const shareUrl =
typeof window === 'undefined'
? sharePath
: new URL(sharePath, window.location.origin).href;
const shareText = `邀请你来玩《${item.title}\n作品号${publicWorkCode}\n${shareUrl}`;
void copyTextToClipboard(shareText).then((copied) => {
setShareState(copied ? 'copied' : 'failed');
if (shareResetTimerRef.current !== null) {
window.clearTimeout(shareResetTimerRef.current);
}
shareResetTimerRef.current = window.setTimeout(() => {
shareResetTimerRef.current = null;
setShareState('idle');
}, 1400);
});
};
useEffect(() => {
return () => {
if (shareResetTimerRef.current !== null) {
window.clearTimeout(shareResetTimerRef.current);
}
if (suppressOpenResetTimerRef.current !== null) {
window.clearTimeout(suppressOpenResetTimerRef.current);
}
@@ -677,7 +648,7 @@ export function CustomWorldWorkCard({
event.stopPropagation();
suppressOpenRef.current = false;
closeSwipeActions();
copyShareText();
onShare?.();
}}
onKeyDown={(event) => {
event.stopPropagation();
@@ -688,20 +659,8 @@ export function CustomWorldWorkCard({
onTouchStart={(event) => {
event.stopPropagation();
}}
title={
shareState === 'copied'
? '已复制'
: shareState === 'failed'
? '复制失败'
: '分享作品'
}
aria-label={
shareState === 'copied'
? '分享内容已复制'
: shareState === 'failed'
? '分享内容复制失败'
: '分享'
}
title="分享作品"
aria-label="分享"
className="creation-work-card__quick-action-button"
>
<Share2 aria-hidden="true" className="h-4 w-4" />

View File

@@ -17681,6 +17681,9 @@ export function PlatformEntryFlowShellImpl({
onOpenShelfItem={(item) => {
markDraftNoticeSeen(getGenerationNoticeShelfKeys(item));
}}
onShareWork={(payload) => {
openPublishShareModal(payload);
}}
onOpenDraft={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();