Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-28 02:05:13 +08:00
33 changed files with 1498 additions and 290 deletions

View File

@@ -1,8 +1,9 @@
import { ArrowLeft, Copy } from 'lucide-react';
import { ArrowLeft, Copy, Share2 } from 'lucide-react';
import { useState } from 'react';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
import { copyTextToClipboard } from '../../services/clipboard';
import type { CustomWorldProfile } from '../../types';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
@@ -74,6 +75,9 @@ export function RpgEntryWorldDetailView({
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>(
'idle',
);
const [shareState, setShareState] = useState<'idle' | 'copied' | 'failed'>(
'idle',
);
const canStartGame = entry.visibility === 'published';
const previewCharacters = buildCustomWorldPlayableCharacters(
entry.profile,
@@ -96,6 +100,19 @@ export function RpgEntryWorldDetailView({
window.setTimeout(() => setCopyState('idle'), 1400);
});
};
const sharePublicWork = () => {
if (!publicWorkCode) {
return;
}
const shareUrl = buildPublicWorkDetailUrl(publicWorkCode);
const shareText = `邀请你来玩《${entry.worldName}\n作品号${publicWorkCode}\n${shareUrl}`;
void copyTextToClipboard(shareText).then((copied) => {
setShareState(copied ? 'copied' : 'failed');
window.setTimeout(() => setShareState('idle'), 1400);
});
};
return (
<div className="flex h-full min-h-0 flex-col">
@@ -146,21 +163,38 @@ export function RpgEntryWorldDetailView({
: '仅自己可见'}
</span>
{publicWorkCode ? (
<button
type="button"
onClick={copyPublicWorkCode}
className="platform-pill platform-pill--neutral flex items-center gap-1 px-3"
aria-label={`复制作品号 ${publicWorkCode}`}
title="复制作品号"
>
<span> {publicWorkCode}</span>
<Copy className="h-3 w-3" />
{copyState !== 'idle' ? (
<span className="text-xs">
{copyState === 'copied' ? '已复制' : '复制失败'}
</span>
) : null}
</button>
<>
<button
type="button"
onClick={copyPublicWorkCode}
className="platform-pill platform-pill--neutral flex items-center gap-1 px-3"
aria-label={`复制作品号 ${publicWorkCode}`}
title="复制作品号"
>
<span> {publicWorkCode}</span>
<Copy className="h-3 w-3" />
{copyState !== 'idle' ? (
<span className="text-xs">
{copyState === 'copied' ? '已复制' : '复制失败'}
</span>
) : null}
</button>
<button
type="button"
onClick={sharePublicWork}
className="platform-pill platform-pill--neutral flex items-center gap-1 px-3"
aria-label={`分享作品 ${entry.worldName}`}
title="分享作品"
>
<Share2 className="h-3 w-3" />
<span></span>
{shareState !== 'idle' ? (
<span className="text-xs">
{shareState === 'copied' ? '已复制' : '复制失败'}
</span>
) : null}
</button>
</>
) : null}
</div>
<div className="mt-4 text-3xl font-black text-white">

View File

@@ -9,6 +9,11 @@ import type {
CustomWorldLibraryEntry,
PlatformBrowseHistoryWriteEntry,
} from '../../../packages/shared/src/contracts/runtime';
import {
buildPublicWorkDetailPath,
pushAppHistoryPath,
} from '../../routing/appPageRoutes';
import { ApiClientError } from '../../services/apiClient';
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetail,
@@ -16,7 +21,6 @@ import {
publishRpgEntryWorldProfile,
unpublishRpgEntryWorldProfile,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import { ApiClientError } from '../../services/apiClient';
import type { CustomWorldProfile } from '../../types';
import {
normalizeRpgEntryAgentBackedProfile,
@@ -167,6 +171,9 @@ export function useRpgEntryLibraryDetail(
setSelectedDetailEntry(entry);
setDetailError(null);
setSelectionStage('detail');
if (entry.publicWorkCode?.trim()) {
pushAppHistoryPath(buildPublicWorkDetailPath(entry.publicWorkCode));
}
},
[appendBrowseHistoryEntry, setSelectedDetailEntry, setSelectionStage],
);
@@ -183,6 +190,11 @@ export function useRpgEntryLibraryDetail(
entry.profileId,
);
setSelectedDetailEntry(detailEntry);
if (detailEntry.publicWorkCode?.trim()) {
pushAppHistoryPath(
buildPublicWorkDetailPath(detailEntry.publicWorkCode),
);
}
void appendBrowseHistoryEntry({
ownerUserId: detailEntry.ownerUserId,
profileId: detailEntry.profileId,