Files
Genarrative/src/components/rpg-entry/RpgEntryWorldDetailView.tsx
T
kdletters 7c47ad3358 继续收口详情页头部动作组合
沉淀 PlatformDetailTopbar 与 PlatformDetailShareActions 共享骨架
接入 RPG 世界详情与公开作品详情的返回复制分享动作组合
补充测试护栏与文档决策记录
2026-06-11 06:32:20 +08:00

352 lines
13 KiB
TypeScript

import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
import type { CustomWorldProfile } from '../../types';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformDetailShareActions } from '../common/PlatformDetailShareActions';
import { PlatformDetailTopbar } from '../common/PlatformDetailTopbar';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { useCopyFeedback } from '../common/useCopyFeedback';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import {
buildPlatformWorldDisplayTags,
describePlatformThemeLabel,
formatPlatformWorkDisplayName,
formatPlatformWorkDisplayTag,
formatPlatformWorldTime,
resolvePlatformPublicWorkCode,
resolvePlatformWorldCoverImage,
resolvePlatformWorldLeadPortrait,
} from './rpgEntryWorldPresentation';
export interface RpgEntryWorldDetailViewProps {
entry: CustomWorldLibraryEntry<CustomWorldProfile>;
isMutating: boolean;
error: string | null;
onBack: () => void;
onStartGame: () => void;
onContinueEdit?: (() => void) | null;
onPublish?: (() => void) | null;
onDelete?: (() => void) | null;
onUnpublish?: (() => void) | null;
}
export function RpgEntryWorldDetailView({
entry,
isMutating,
error,
onBack,
onStartGame,
onContinueEdit,
onPublish,
onDelete,
onUnpublish,
}: RpgEntryWorldDetailViewProps) {
const coverImage = resolvePlatformWorldCoverImage(entry);
const leadPortrait = resolvePlatformWorldLeadPortrait(entry);
const publicWorkCode = resolvePlatformPublicWorkCode(entry);
const { copyState, copyText: copyWorkCodeText } = useCopyFeedback();
const { copyState: shareState, copyText: copyShareText } = useCopyFeedback();
const canStartGame = entry.visibility === 'published';
const previewCharacters = buildCustomWorldPlayableCharacters(
entry.profile,
).slice(0, 3);
const previewLandmarks = entry.profile.landmarks.slice(0, 3);
const displayName = formatPlatformWorkDisplayName(entry.worldName);
const tags = buildPlatformWorldDisplayTags(entry, 3);
const copyPublicWorkCode = () => {
if (!publicWorkCode) {
return;
}
void copyWorkCodeText(publicWorkCode);
};
const sharePublicWork = () => {
if (!publicWorkCode) {
return;
}
const shareUrl = buildPublicWorkDetailUrl(publicWorkCode);
const shareText = `邀请你来玩《${entry.worldName}》\n作品号:${publicWorkCode}\n${shareUrl}`;
void copyShareText(shareText);
};
return (
<div className="flex h-full min-h-0 flex-col">
<PlatformDetailTopbar
onBack={onBack}
className="mb-4"
backButtonClassName="px-3"
trailing={
<PlatformPillBadge
tone="neutral"
size="xs"
className="px-3 py-1.5 tracking-[0.08em]"
>
{entry.visibility === 'published' ? '已发布' : '草稿'}
</PlatformPillBadge>
}
/>
<div className="min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide">
<div className="space-y-4 pb-2">
<div className="platform-surface platform-surface--hero relative overflow-hidden px-[18px] py-4">
{coverImage ? (
<ResolvedAssetImage
src={coverImage}
alt={entry.worldName}
className="absolute inset-0 h-full w-full object-cover opacity-38"
/>
) : null}
{leadPortrait ? (
<ResolvedAssetImage
src={leadPortrait}
alt=""
aria-hidden="true"
className="absolute bottom-0 right-2 h-32 w-32 object-contain opacity-25"
/>
) : null}
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
<div className="relative z-10">
<PlatformDetailShareActions
workCode={publicWorkCode}
copyState={copyState}
onCopyWorkCode={copyPublicWorkCode}
shareState={shareState}
onShare={sharePublicWork}
shareAriaLabel={`分享作品 ${entry.worldName}`}
leading={
<>
<PlatformPillBadge
tone="warning"
size="xxs"
className="tracking-[0.18em]"
>
{formatPlatformWorkDisplayTag(
describePlatformThemeLabel(entry.themeMode),
)}
</PlatformPillBadge>
<PlatformPillBadge
tone="neutral"
size="xxs"
className="px-3 tracking-[0.18em]"
>
{entry.authorDisplayName}
</PlatformPillBadge>
<PlatformPillBadge
tone="neutral"
size="xxs"
className="px-3 tracking-[0.18em]"
>
{entry.visibility === 'published'
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
: '仅自己可见'}
</PlatformPillBadge>
</>
}
variant="overlay"
/>
<div className="mt-4 text-3xl font-black text-white">
{displayName}
</div>
{entry.subtitle ? (
<div className="mt-2 text-sm tracking-[0.18em] text-zinc-300/88">
{entry.subtitle}
</div>
) : null}
<div className="mt-4 max-w-[36rem] text-sm leading-7 text-zinc-200/88">
{entry.summaryText || '等待补充世界摘要。'}
</div>
<div className="mt-4 flex flex-wrap gap-2">
{tags.map((tag, index) => (
<PlatformPillBadge
key={`world-detail-tag-${index}-${tag || 'empty'}`}
tone="neutral"
size="xxs"
className="px-3 tracking-[0.18em]"
>
{tag}
</PlatformPillBadge>
))}
</div>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
<div className="platform-surface platform-surface--soft px-4 py-3.5">
<PlatformFieldLabel variant="section">
世界信息
</PlatformFieldLabel>
<div className="mt-3 grid grid-cols-2 gap-3 text-sm text-[var(--platform-text-strong)] sm:grid-cols-4">
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-3 py-3"
>
<PlatformFieldLabel variant="section">
可玩角色
</PlatformFieldLabel>
<div className="mt-2 text-lg font-bold">
{entry.playableNpcCount}
</div>
</PlatformSubpanel>
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-3 py-3"
>
<PlatformFieldLabel variant="section">
地标
</PlatformFieldLabel>
<div className="mt-2 text-lg font-bold">
{entry.landmarkCount}
</div>
</PlatformSubpanel>
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-3 py-3"
>
<PlatformFieldLabel variant="section">
阵营
</PlatformFieldLabel>
<div className="mt-2 text-lg font-bold">
{entry.profile.majorFactions.length}
</div>
</PlatformSubpanel>
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-3 py-3"
>
<PlatformFieldLabel variant="section">
冲突
</PlatformFieldLabel>
<div className="mt-2 text-lg font-bold">
{entry.profile.coreConflicts.length}
</div>
</PlatformSubpanel>
</div>
<div className="mt-5">
<PlatformFieldLabel variant="section">
关键角色
</PlatformFieldLabel>
<div className="mt-3 grid gap-3 sm:grid-cols-3">
{previewCharacters.map((character, index) => (
<PlatformSubpanel
as="div"
key={character.id || `preview-character-${index}`}
radius="sm"
padding="none"
className="px-3 py-3"
>
<div className="line-clamp-1 text-sm font-bold text-[var(--platform-text-strong)]">
{character.title}
</div>
<div className="mt-1 line-clamp-2 text-xs leading-5 text-[var(--platform-text-base)]">
{character.description}
</div>
</PlatformSubpanel>
))}
</div>
</div>
<div className="mt-5">
<PlatformFieldLabel variant="section">
关键场景
</PlatformFieldLabel>
<div className="mt-3 grid gap-3 sm:grid-cols-3">
{previewLandmarks.map((landmark, index) => (
<PlatformSubpanel
as="div"
key={landmark.id || `preview-landmark-${index}`}
radius="sm"
padding="none"
className="px-3 py-3"
>
<div className="line-clamp-1 text-sm font-bold text-[var(--platform-text-strong)]">
{landmark.name}
</div>
<div className="mt-1 line-clamp-2 text-xs leading-5 text-[var(--platform-text-base)]">
{landmark.description}
</div>
</PlatformSubpanel>
))}
</div>
</div>
</div>
<div className="platform-surface platform-surface--soft px-4 py-3.5">
<PlatformFieldLabel variant="section">操作</PlatformFieldLabel>
<div className="mt-4 flex flex-col gap-3">
<PlatformActionButton
onClick={onStartGame}
tone="primary"
disabled={!canStartGame || isMutating}
>
{canStartGame ? '开始游戏' : '请先发布作品'}
</PlatformActionButton>
{onContinueEdit ? (
<PlatformActionButton
onClick={onContinueEdit}
tone="secondary"
disabled={isMutating}
>
继续创作
</PlatformActionButton>
) : null}
{onPublish ? (
<PlatformActionButton
onClick={onPublish}
tone="primary"
disabled={isMutating}
>
发布到广场
</PlatformActionButton>
) : null}
{onUnpublish ? (
<PlatformActionButton
onClick={onUnpublish}
tone="danger"
disabled={isMutating}
>
下架作品
</PlatformActionButton>
) : null}
{onDelete ? (
<PlatformActionButton
onClick={onDelete}
tone="danger"
disabled={isMutating}
>
删除作品
</PlatformActionButton>
) : null}
</div>
{error ? (
<PlatformStatusMessage
tone="error"
className="mt-4 rounded-2xl border-rose-400/20 bg-rose-500/10 px-4 py-3 leading-6"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</div>
</div>
</div>
</div>
);
}
export const PlatformWorldDetailView = RpgEntryWorldDetailView;