7c47ad3358
沉淀 PlatformDetailTopbar 与 PlatformDetailShareActions 共享骨架 接入 RPG 世界详情与公开作品详情的返回复制分享动作组合 补充测试护栏与文档决策记录
352 lines
13 KiB
TypeScript
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;
|