收口个人中心弹层组件
- 新增 PlatformProfileModalShell 统一个人中心主弹层与副弹层壳层 - 抽离 PlatformProfilePlayedWorksModal 与 PlatformProfileReferralModal 并移除首页内联历史与邀请弹层实现 - 让昵称充值任务兑换码账单等弹层复用共享壳层并补齐测试和文档
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import { ArrowRight, Clock3 } from 'lucide-react';
|
||||
|
||||
import type {
|
||||
ProfilePlayedWorkSummary,
|
||||
ProfilePlayStatsResponse,
|
||||
ProfileSaveArchiveSummary,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { PlatformProfileSecondaryModalShell } from './PlatformProfileModalShell';
|
||||
import {
|
||||
formatCompactPlayTime,
|
||||
formatPlayedWorkId,
|
||||
formatPlayedWorkType,
|
||||
formatSnapshotTime,
|
||||
formatTotalPlayTimeHours,
|
||||
} from '../rpg-entry/rpgEntryProfileDashboardPresentation';
|
||||
import { formatPlatformWorkDisplayName } from '../rpg-entry/rpgEntryWorldPresentation';
|
||||
|
||||
type PlatformProfilePlayedWorksModalProps = {
|
||||
stats: ProfilePlayStatsResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
saveEntries: ProfileSaveArchiveSummary[];
|
||||
saveError: string | null;
|
||||
isResumingSaveWorldKey: string | null;
|
||||
onClose: () => void;
|
||||
onOpenWork?: (work: ProfilePlayedWorkSummary) => void;
|
||||
onResumeSave: (entry: ProfileSaveArchiveSummary) => void;
|
||||
};
|
||||
|
||||
function SaveArchivePreview({
|
||||
entry,
|
||||
className,
|
||||
}: {
|
||||
entry: ProfileSaveArchiveSummary;
|
||||
className: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={`platform-remap-surface relative shrink-0 overflow-hidden rounded-[1.35rem] border border-white/12 bg-black/18 shadow-[var(--platform-desktop-hover-shadow)] ${className}`}
|
||||
>
|
||||
{entry.coverImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={entry.coverImageSrc}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.18),transparent_34%),linear-gradient(145deg,rgba(255,94,125,0.92),rgba(255,150,116,0.88))]" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-[var(--platform-card-overlay-soft)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SaveArchiveCard({
|
||||
entry,
|
||||
onClick,
|
||||
loading = false,
|
||||
}: {
|
||||
entry: ProfileSaveArchiveSummary;
|
||||
onClick: () => void;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const summaryText =
|
||||
entry.summaryText || entry.subtitle || '继续推进上一次保存的故事。';
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={loading}
|
||||
className={`platform-surface platform-surface--soft platform-interactive-card relative flex min-h-[13rem] w-full overflow-hidden p-3.5 text-left sm:min-h-[12.5rem] sm:p-4 ${loading ? 'opacity-80' : ''}`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[var(--platform-card-overlay-deep)]" />
|
||||
<div className="relative z-10 flex h-full w-full flex-col gap-3">
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<PlatformPillBadge
|
||||
tone="darkNeutral"
|
||||
size="xs"
|
||||
className="font-medium text-[var(--platform-text-base)]"
|
||||
>
|
||||
{loading ? '恢复中' : formatSnapshotTime(entry.lastPlayedAt)}
|
||||
</PlatformPillBadge>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 items-stretch gap-3 sm:gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="line-clamp-2 break-words text-[1.35rem] font-black leading-tight text-[var(--platform-text-strong)] sm:text-2xl">
|
||||
{displayName}
|
||||
</div>
|
||||
{entry.subtitle ? (
|
||||
<div className="mt-1 line-clamp-2 break-words text-sm font-semibold text-[var(--platform-text-base)]">
|
||||
{entry.subtitle}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-2 line-clamp-3 break-words text-xs leading-5 text-[var(--platform-text-soft)] sm:text-sm">
|
||||
{summaryText}
|
||||
</div>
|
||||
<div className="mt-4 inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-200">
|
||||
<span>{loading ? '正在恢复' : '继续游玩'}</span>
|
||||
<ArrowRight className="h-3.5 w-3.5 shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
<SaveArchivePreview
|
||||
entry={entry}
|
||||
className="aspect-square w-[6.5rem] self-start sm:w-[7.5rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformProfilePlayedWorksModal({
|
||||
stats,
|
||||
isLoading,
|
||||
error,
|
||||
saveEntries,
|
||||
saveError,
|
||||
isResumingSaveWorldKey,
|
||||
onClose,
|
||||
onOpenWork,
|
||||
onResumeSave,
|
||||
}: PlatformProfilePlayedWorksModalProps) {
|
||||
// 中文注释:个人中心“玩过”弹层同时承接“可继续”的存档列表,保持同一入口下的历史/恢复语义。
|
||||
const playedWorks = stats?.playedWorks ?? [];
|
||||
const hasArchiveEntries = saveEntries.length > 0;
|
||||
const hasPlayedWorks = playedWorks.length > 0;
|
||||
|
||||
return (
|
||||
<PlatformProfileSecondaryModalShell
|
||||
title="玩过"
|
||||
onClose={onClose}
|
||||
panelClassName="relative !max-h-[min(92vh,42rem)] !max-w-[38rem] bg-white text-zinc-950 shadow-2xl !rounded-[1.35rem] sm:!rounded-[1.35rem]"
|
||||
contentClassName="relative max-h-[min(92vh,42rem)] overflow-y-auto px-4 pb-5 pt-4 sm:px-5"
|
||||
>
|
||||
<div className="pr-10">
|
||||
<div className="text-[10px] font-black tracking-[0.22em] text-[#ff4056]">
|
||||
PLAYED
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-black">玩过</div>
|
||||
<PlatformPillBadge
|
||||
tone="profile"
|
||||
icon={<Clock3 className="h-3.5 w-3.5 text-[#ff4056]" />}
|
||||
className="mt-2"
|
||||
>
|
||||
{formatTotalPlayTimeHours(stats?.totalPlayTimeMs ?? 0)}
|
||||
</PlatformPillBadge>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<PlatformStatusMessage tone="error" className="mt-4">
|
||||
{error}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
{saveError ? (
|
||||
<PlatformStatusMessage tone="error" className="mt-4">
|
||||
{saveError}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="mt-5 space-y-3">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
|
||||
))}
|
||||
</div>
|
||||
) : hasArchiveEntries || hasPlayedWorks ? (
|
||||
<div className="mt-5 space-y-5">
|
||||
{hasArchiveEntries ? (
|
||||
<section>
|
||||
<PlatformFieldLabel variant="section" className="mb-2 block">
|
||||
可继续
|
||||
</PlatformFieldLabel>
|
||||
<div className="grid gap-3">
|
||||
{saveEntries.map((entry) => (
|
||||
<SaveArchiveCard
|
||||
key={`${entry.worldKey}:played-archive`}
|
||||
entry={entry}
|
||||
loading={isResumingSaveWorldKey === entry.worldKey}
|
||||
onClick={() => onResumeSave(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{hasPlayedWorks ? (
|
||||
<section>
|
||||
<PlatformFieldLabel variant="section" className="mb-2 block">
|
||||
玩过
|
||||
</PlatformFieldLabel>
|
||||
<div className="space-y-3">
|
||||
{playedWorks.map((work) => (
|
||||
<PlatformSubpanel
|
||||
as="button"
|
||||
type="button"
|
||||
key={`${work.worldKey}:${work.lastPlayedAt}`}
|
||||
onClick={() => onOpenWork?.(work)}
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="md"
|
||||
interactive
|
||||
className="w-full hover:border-[#ff4056]"
|
||||
>
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-1 text-base font-black text-zinc-950">
|
||||
{work.worldTitle}
|
||||
</div>
|
||||
{work.worldSubtitle ? (
|
||||
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
|
||||
{work.worldSubtitle}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<PlatformPillBadge
|
||||
tone="profileAccent"
|
||||
size="xs"
|
||||
className="shrink-0 border-transparent"
|
||||
>
|
||||
{formatPlayedWorkType(work.worldType)}
|
||||
</PlatformPillBadge>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 text-xs text-zinc-500 sm:grid-cols-3">
|
||||
<span className="truncate">作品号 {formatPlayedWorkId(work)}</span>
|
||||
<span className="truncate">
|
||||
最近 {formatSnapshotTime(work.lastPlayedAt)}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
时长 {formatCompactPlayTime(work.lastObservedPlayTimeMs)}
|
||||
</span>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<PlatformEmptyState
|
||||
surface="subpanel"
|
||||
size="inline"
|
||||
tone="base"
|
||||
className="mt-5 text-left"
|
||||
>
|
||||
暂无玩过
|
||||
</PlatformEmptyState>
|
||||
)}
|
||||
</PlatformProfileSecondaryModalShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user