1
This commit is contained in:
242
src/components/platform-entry/PlatformWorkDetailView.tsx
Normal file
242
src/components/platform-entry/PlatformWorkDetailView.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import { ArrowLeft, Copy, GitFork, Play, Share2 } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import {
|
||||
buildPlatformWorldTags,
|
||||
formatPlatformWorldTime,
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldCoverImage,
|
||||
resolvePlatformWorldStats,
|
||||
type PlatformPublicGalleryCard,
|
||||
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||
|
||||
export interface PlatformWorkDetailViewProps {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
isBusy: boolean;
|
||||
error: string | null;
|
||||
onBack: () => void;
|
||||
onStart: () => void;
|
||||
onRemix: () => void;
|
||||
}
|
||||
|
||||
function formatCompactCount(value: number) {
|
||||
if (value >= 10000) {
|
||||
const normalized = value / 10000;
|
||||
return `${Number.isInteger(normalized) ? normalized.toFixed(0) : normalized.toFixed(1)}万`;
|
||||
}
|
||||
return `${value}`;
|
||||
}
|
||||
|
||||
function getSourceLabel(entry: PlatformPublicGalleryCard) {
|
||||
if ('sourceType' in entry && entry.sourceType === 'puzzle') {
|
||||
return '拼图';
|
||||
}
|
||||
if ('sourceType' in entry && entry.sourceType === 'big-fish') {
|
||||
return '大鱼吃小鱼';
|
||||
}
|
||||
return 'RPG';
|
||||
}
|
||||
|
||||
export function PlatformWorkDetailView({
|
||||
entry,
|
||||
isBusy,
|
||||
error,
|
||||
onBack,
|
||||
onStart,
|
||||
onRemix,
|
||||
}: PlatformWorkDetailViewProps) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const publicWorkCode = resolvePlatformPublicWorkCode(entry);
|
||||
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>(
|
||||
'idle',
|
||||
);
|
||||
const [shareState, setShareState] = useState<'idle' | 'copied' | 'failed'>(
|
||||
'idle',
|
||||
);
|
||||
const tags = useMemo(
|
||||
() =>
|
||||
[
|
||||
getSourceLabel(entry),
|
||||
...buildPlatformWorldTags(entry).map((tag) => tag.trim()),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.slice(0, 4),
|
||||
[entry],
|
||||
);
|
||||
const stats = resolvePlatformWorldStats(entry);
|
||||
const statItems = [
|
||||
{ label: '改造次数', value: formatCompactCount(stats.remixCount) },
|
||||
{ label: '游玩次数', value: formatCompactCount(stats.playCount) },
|
||||
{ label: '点赞次数', value: formatCompactCount(stats.likeCount) },
|
||||
{
|
||||
label: '上线日期',
|
||||
value: formatPlatformWorldTime(stats.publishedAt),
|
||||
},
|
||||
];
|
||||
|
||||
const copyPublicWorkCode = () => {
|
||||
if (!publicWorkCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
void copyTextToClipboard(publicWorkCode).then((copied) => {
|
||||
setCopyState(copied ? 'copied' : 'failed');
|
||||
window.setTimeout(() => setCopyState('idle'), 1400);
|
||||
});
|
||||
};
|
||||
|
||||
const sharePublicWork = () => {
|
||||
if (!publicWorkCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shareText = `邀请你来玩《${entry.worldName}》\n作品号:${publicWorkCode}\n${buildPublicWorkDetailUrl(publicWorkCode)}`;
|
||||
void copyTextToClipboard(shareText).then((copied) => {
|
||||
setShareState(copied ? 'copied' : 'failed');
|
||||
window.setTimeout(() => setShareState('idle'), 1400);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="platform-work-detail">
|
||||
<div className="platform-work-detail__topbar">
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__icon-button"
|
||||
onClick={onBack}
|
||||
aria-label="返回"
|
||||
title="返回"
|
||||
>
|
||||
<ArrowLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<div className="platform-work-detail__title">详情</div>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__icon-button"
|
||||
onClick={sharePublicWork}
|
||||
disabled={!publicWorkCode}
|
||||
aria-label="分享"
|
||||
title="分享"
|
||||
>
|
||||
<Share2 className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="platform-work-detail__scroll">
|
||||
<section className="platform-work-detail__cover">
|
||||
{coverImage ? (
|
||||
<>
|
||||
<ResolvedAssetImage
|
||||
src={coverImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="platform-work-detail__cover-blur"
|
||||
/>
|
||||
<ResolvedAssetImage
|
||||
src={coverImage}
|
||||
alt={entry.worldName}
|
||||
className="platform-work-detail__cover-image"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="platform-work-detail__cover-fallback" />
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="platform-work-detail__summary">
|
||||
<div className="platform-work-detail__meta-row">
|
||||
<div className="platform-work-detail__app-icon">
|
||||
{coverImage ? (
|
||||
<ResolvedAssetImage
|
||||
src={coverImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
entry.worldName.slice(0, 1)
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="platform-work-detail__name">
|
||||
{entry.worldName}
|
||||
</div>
|
||||
<div className="platform-work-detail__author">
|
||||
{entry.authorDisplayName}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__remix"
|
||||
onClick={onRemix}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<GitFork className="h-5 w-5" />
|
||||
Remix
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="platform-work-detail__stats">
|
||||
{statItems.map((item) => (
|
||||
<div key={item.label} className="platform-work-detail__stat">
|
||||
<div className="platform-work-detail__stat-label">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="platform-work-detail__stat-value">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-work-detail__body">
|
||||
<div className="platform-work-detail__chips">
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className="platform-work-detail__chip">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="platform-work-detail__copy">{entry.summaryText}</p>
|
||||
{publicWorkCode ? (
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__code"
|
||||
onClick={copyPublicWorkCode}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span>{publicWorkCode}</span>
|
||||
{copyState !== 'idle' ? (
|
||||
<span>{copyState === 'copied' ? '已复制' : '复制失败'}</span>
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
{shareState !== 'idle' ? (
|
||||
<div className="platform-work-detail__toast">
|
||||
{shareState === 'copied' ? '分享内容已复制' : '分享失败'}
|
||||
</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
<div className="platform-work-detail__error">{error}</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="platform-work-detail__bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__start"
|
||||
onClick={onStart}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Play className="h-5 w-5 fill-current" />
|
||||
启动
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user