11
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-16 21:47:20 +08:00
parent 2456c10c63
commit 09d4c0c31b
79 changed files with 11873 additions and 2341 deletions

View File

@@ -17,6 +17,8 @@ import { type ComponentType, useMemo } from 'react';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
ProfileDashboardCardKey,
ProfileDashboardSummary,
} from '../../../packages/shared/src/contracts/runtime';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { AuthUser } from '../../services/authService';
@@ -72,7 +74,11 @@ function WorldCard({
const coverImage = resolvePlatformWorldCoverImage(entry);
const leadPortrait = resolvePlatformWorldLeadPortrait(entry);
const tags = [
...new Set(buildPlatformWorldTags(entry).map((tag) => tag.trim()).filter(Boolean)),
...new Set(
buildPlatformWorldTags(entry)
.map((tag) => tag.trim())
.filter(Boolean),
),
].slice(0, 3);
return (
@@ -224,19 +230,53 @@ function describeBindingStatus(bindingStatus: AuthUser['bindingStatus']) {
return bindingStatus === 'pending_bind_phone' ? '待绑定手机号' : '正常';
}
function formatPlayTime(playTimeMs: number) {
const totalSeconds = Math.max(0, Math.floor(playTimeMs / 1000));
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
function formatCompactPlayTime(playTimeMs: number) {
const totalMinutes = Math.max(0, Math.floor(playTimeMs / 60000));
const days = totalMinutes / 1440;
if (days > 0) {
return `${days}${hours}小时`;
if (days >= 10) {
return `${Math.floor(days)}`;
}
if (hours > 0) {
return `${hours}小时 ${minutes}`;
if (days >= 1) {
return `${days.toFixed(days >= 3 ? 0 : 1)}`;
}
return `${minutes}`;
const hours = totalMinutes / 60;
if (hours >= 1) {
return `${hours.toFixed(hours >= 10 ? 0 : 1)}小时`;
}
return `${Math.max(0, totalMinutes)}`;
}
function formatDashboardCount(value: number) {
const normalizedValue = Math.max(0, Math.round(value));
if (normalizedValue >= 100000000) {
return `${(normalizedValue / 100000000).toFixed(1)}亿`;
}
if (normalizedValue >= 10000) {
return `${(normalizedValue / 10000).toFixed(1)}`;
}
return normalizedValue.toLocaleString('zh-CN');
}
function formatDashboardUpdatedAt(value: string | null | undefined) {
if (!value) {
return '暂无更新记录';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function buildPublicUserCode(user: AuthUser | null | undefined) {
@@ -249,7 +289,9 @@ function buildPublicUserCode(user: AuthUser | null | undefined) {
}
function getUserAvatarLabel(user: AuthUser | null | undefined) {
return (user?.displayName || user?.username || '叙').slice(0, 1).toUpperCase();
return (user?.displayName || user?.username || '叙')
.slice(0, 1)
.toUpperCase();
}
function copyText(value: string) {
@@ -261,23 +303,40 @@ function copyText(value: string) {
}
function ProfileStatCard({
cardKey,
label,
value,
onClick,
icon,
}: {
cardKey: ProfileDashboardCardKey;
label: string;
value: string;
onClick?: ((cardKey: ProfileDashboardCardKey) => void) | null;
icon: ComponentType<{ className?: string }>;
}) {
const Icon = icon;
return (
<div className="rounded-[1.35rem] border border-white/10 bg-black/18 px-4 py-3">
<button
type="button"
onClick={onClick ? () => onClick(cardKey) : undefined}
className="rounded-[1.35rem] border border-white/10 bg-black/18 px-4 py-3 text-left transition hover:border-white/20 hover:bg-white/6"
>
<div className="flex items-center gap-2 text-zinc-400">
<Icon className="h-4 w-4" />
<span className="text-[11px] tracking-[0.16em]">{label}</span>
</div>
<div className="mt-3 text-lg font-black text-white">{value}</div>
</button>
);
}
function ProfileStatCardSkeleton() {
return (
<div className="rounded-[1.35rem] border border-white/10 bg-black/18 px-4 py-3">
<div className="h-4 w-20 animate-pulse rounded-full bg-white/10" />
<div className="mt-3 h-7 w-16 animate-pulse rounded-full bg-white/12" />
</div>
);
}
@@ -316,13 +375,20 @@ export function PlatformHomeView({
latestEntries,
myEntries,
historyEntries,
historyError,
profileDashboard,
isLoadingPlatform,
isLoadingDashboard,
isClearingHistory,
platformError,
dashboardError,
onContinueGame,
onClearHistory,
onOpenCreateWorld,
onOpenCreateTypePicker,
onOpenGalleryDetail,
onOpenLibraryDetail,
onOpenProfileDashboardCard,
}: {
activeTab: PlatformHomeTab;
onTabChange: (tab: PlatformHomeTab) => void;
@@ -332,15 +398,22 @@ export function PlatformHomeView({
latestEntries: CustomWorldGalleryCard[];
myEntries: CustomWorldLibraryEntry<CustomWorldProfile>[];
historyEntries: PlatformBrowseHistoryEntry[];
historyError: string | null;
profileDashboard: ProfileDashboardSummary | null;
isLoadingPlatform: boolean;
isLoadingDashboard: boolean;
isClearingHistory: boolean;
platformError: string | null;
dashboardError: string | null;
onContinueGame: () => void;
onClearHistory: () => void;
onOpenCreateWorld: () => void;
onOpenCreateTypePicker: () => void;
onOpenGalleryDetail: (entry: CustomWorldGalleryCard) => void;
onOpenLibraryDetail: (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => void;
onOpenProfileDashboardCard?: (cardKey: ProfileDashboardCardKey) => void;
}) {
const authUi = useAuthUi();
const featuredShelf = useMemo(
@@ -362,11 +435,11 @@ export function PlatformHomeView({
'上一次冒险已经保存,可以从这里继续推进故事。';
const publicUserCode = buildPublicUserCode(authUi?.user);
const avatarLabel = getUserAvatarLabel(authUi?.user);
const remainingNarrativeCoins = savedSnapshot?.gameState.playerCurrency ?? 0;
const totalPlayTime = formatPlayTime(
savedSnapshot?.gameState.runtimeStats.playTimeMs ?? 0,
const remainingNarrativeCoins = profileDashboard?.walletBalance ?? 0;
const totalPlayTime = formatCompactPlayTime(
profileDashboard?.totalPlayTimeMs ?? 0,
);
const playedWorkCount = hasSavedGame ? 1 : 0;
const playedWorkCount = profileDashboard?.playedWorldCount ?? 0;
const tabIcons = {
home: "/Icons/Admurin's Pixel Items/Admurin's Pixel Items/Miscellaneous/Singles/192_RustyTrinket_House.png",
create: '/Icons/01_Scroll.png',
@@ -647,21 +720,66 @@ export function PlatformHomeView({
})}
>
<div className="grid grid-cols-3 gap-3">
<ProfileStatCard
label="剩余叙世币"
value={`${remainingNarrativeCoins}`}
icon={Coins}
/>
<ProfileStatCard
label="总游戏时长"
value={totalPlayTime}
icon={Clock3}
/>
<ProfileStatCard
label="玩过作品"
value={`${playedWorkCount}`}
icon={BookOpen}
/>
{isLoadingDashboard ? (
<>
<ProfileStatCardSkeleton />
<ProfileStatCardSkeleton />
<ProfileStatCardSkeleton />
</>
) : dashboardError ? (
<>
<ProfileStatCard
cardKey="wallet"
label="剩余叙世币"
value="暂不可用"
icon={Coins}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playTime"
label="总游戏时长"
value="暂不可用"
icon={Clock3}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playedWorks"
label="玩过作品"
value="暂不可用"
icon={BookOpen}
onClick={onOpenProfileDashboardCard}
/>
</>
) : (
<>
<ProfileStatCard
cardKey="wallet"
label="剩余叙世币"
value={formatDashboardCount(remainingNarrativeCoins)}
icon={Coins}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playTime"
label="总游戏时长"
value={totalPlayTime}
icon={Clock3}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playedWorks"
label="玩过作品"
value={formatDashboardCount(playedWorkCount)}
icon={BookOpen}
onClick={onOpenProfileDashboardCard}
/>
</>
)}
</div>
<div className="mt-3 text-[11px] text-zinc-500">
{dashboardError
? dashboardError
: `更新于 ${formatDashboardUpdatedAt(profileDashboard?.updatedAt)}`}
</div>
</section>
@@ -719,8 +837,27 @@ export function PlatformHomeView({
paddingY: 14,
})}
>
<SectionHeader title="历史浏览" detail="最近看过的作品" />
{historyEntries.length > 0 ? (
<div className="mb-3 flex items-start justify-between gap-3">
<SectionHeader title="历史浏览" detail="最近看过的作品" />
{historyEntries.length > 0 ? (
<button
type="button"
onClick={onClearHistory}
disabled={isClearingHistory}
className="shrink-0 rounded-full border border-white/10 bg-black/18 px-3 py-1.5 text-[11px] text-zinc-300 transition hover:border-white/20 hover:text-white disabled:cursor-not-allowed disabled:opacity-55"
>
{isClearingHistory ? '清空中' : '清空'}
</button>
) : null}
</div>
{historyError ? (
<div className="mb-3 rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
{historyError}
</div>
) : null}
{isLoadingPlatform && historyEntries.length === 0 ? (
<EmptyShelf text="正在读取浏览历史..." />
) : historyEntries.length > 0 ? (
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
{historyEntries.map((entry) => (
<button
@@ -771,7 +908,9 @@ export function PlatformHomeView({
{entry.authorDisplayName}
</div>
<div className="mt-2 line-clamp-3 text-xs leading-5 text-zinc-400">
{entry.summaryText || entry.subtitle || '等待补充世界摘要。'}
{entry.summaryText ||
entry.subtitle ||
'等待补充世界摘要。'}
</div>
</div>
</div>
@@ -815,7 +954,9 @@ export function PlatformHomeView({
<Settings className="h-[1.125rem] w-[1.125rem]" />
</div>
<div>
<div className="text-base font-semibold text-white"></div>
<div className="text-base font-semibold text-white">
</div>
<div className="text-xs text-zinc-400"></div>
</div>
</div>