1
This commit is contained in:
@@ -76,6 +76,7 @@ import {
|
||||
type PlatformPublicGalleryCard,
|
||||
type PlatformWorldCardLike,
|
||||
resolvePlatformWorldCoverImage,
|
||||
resolvePlatformWorldCoverSlides,
|
||||
resolvePlatformWorldLeadPortrait,
|
||||
} from './rpgEntryWorldPresentation';
|
||||
|
||||
@@ -145,6 +146,7 @@ const PLATFORM_HOME_TABS: PlatformHomeTab[] = [
|
||||
const AVATAR_MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||
const AVATAR_OUTPUT_SIZE = 256;
|
||||
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
||||
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
||||
type MobileHomeChannel = 'recommend' | 'today' | 'category';
|
||||
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
|
||||
@@ -365,12 +367,38 @@ function WorldCard({
|
||||
entry,
|
||||
onClick,
|
||||
className,
|
||||
feedCardKey,
|
||||
enableCoverCarousel = false,
|
||||
isCoverCarouselActive = false,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
feedCardKey?: string;
|
||||
enableCoverCarousel?: boolean;
|
||||
isCoverCarouselActive?: boolean;
|
||||
}) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const fallbackCoverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const coverSlides = useMemo(() => {
|
||||
if (!enableCoverCarousel) {
|
||||
return fallbackCoverImage
|
||||
? [
|
||||
{
|
||||
id: 'cover',
|
||||
imageSrc: fallbackCoverImage,
|
||||
label: entry.worldName,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
return resolvePlatformWorldCoverSlides(entry);
|
||||
}, [enableCoverCarousel, entry, fallbackCoverImage]);
|
||||
const [activeCoverIndex, setActiveCoverIndex] = useState(0);
|
||||
const visibleCoverIndex = isCoverCarouselActive ? activeCoverIndex : 0;
|
||||
const activeCoverSlide =
|
||||
coverSlides[visibleCoverIndex] ?? coverSlides[0] ?? null;
|
||||
const coverImage = activeCoverSlide?.imageSrc ?? '';
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
const tags = buildPlatformWorldDisplayTags(entry, 3);
|
||||
const playCount = getPlatformWorldPlayCount(entry);
|
||||
@@ -398,11 +426,36 @@ function WorldCard({
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
setActiveCoverIndex(0);
|
||||
}, [entry.ownerUserId, entry.profileId, coverSlides.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCoverCarouselActive) {
|
||||
setActiveCoverIndex(0);
|
||||
}
|
||||
}, [isCoverCarouselActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCoverCarouselActive || coverSlides.length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timerId = window.setInterval(() => {
|
||||
setActiveCoverIndex((current) => (current + 1) % coverSlides.length);
|
||||
}, PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timerId);
|
||||
};
|
||||
}, [coverSlides.length, isCoverCarouselActive]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={cardLabel}
|
||||
data-mobile-feed-card-key={feedCardKey}
|
||||
className={`platform-public-work-card platform-surface platform-interactive-card relative flex w-[min(21rem,88vw)] shrink-0 flex-col overflow-hidden p-0 text-left ${className ?? ''}`}
|
||||
>
|
||||
<div className="platform-public-work-card__cover relative aspect-video overflow-hidden">
|
||||
@@ -1191,7 +1244,7 @@ function formatCompactPlayTime(playTimeMs: number) {
|
||||
return `${Math.max(0, totalMinutes)}分`;
|
||||
}
|
||||
|
||||
// “总游戏时长”固定使用小时,避免短时长切到分钟或长时长切到天。
|
||||
// “游戏时长”固定使用小时,避免短时长切到分钟或长时长切到天。
|
||||
function formatTotalPlayTimeHours(playTimeMs: number) {
|
||||
const roundedHours = Math.max(0, Math.round(playTimeMs / 360000) / 10);
|
||||
|
||||
@@ -2056,7 +2109,7 @@ function ProfilePlayedWorksModal({
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="absolute right-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/80 text-[#ff4056] shadow-sm"
|
||||
aria-label="关闭玩过作品"
|
||||
aria-label="关闭玩过"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -2065,7 +2118,7 @@ function ProfilePlayedWorksModal({
|
||||
<div className="text-[10px] font-black tracking-[0.22em] text-[#ff4056]">
|
||||
PLAYED
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-black">玩过作品</div>
|
||||
<div className="mt-1 text-2xl font-black">玩过</div>
|
||||
<div className="mt-2 inline-flex items-center gap-2 rounded-full border border-rose-100 bg-rose-50 px-3 py-1.5 text-xs font-bold text-zinc-600">
|
||||
<Clock3 className="h-3.5 w-3.5 text-[#ff4056]" />
|
||||
<span>
|
||||
@@ -2129,7 +2182,7 @@ function ProfilePlayedWorksModal({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-5 rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-4 text-sm text-zinc-600">
|
||||
暂无玩过作品
|
||||
暂无玩过
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2202,6 +2255,10 @@ export function RpgEntryHomeView({
|
||||
);
|
||||
const [mobileHomeChannel, setMobileHomeChannel] =
|
||||
useState<MobileHomeChannel>('recommend');
|
||||
const mobileFeedRef = useRef<HTMLElement | null>(null);
|
||||
const [mobileCenteredCardKey, setMobileCenteredCardKey] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [activeRankingTab, setActiveRankingTab] =
|
||||
useState<PlatformRankingTab>('hot');
|
||||
const [visitedTabs, setVisitedTabs] = useState<Set<PlatformHomeTab>>(
|
||||
@@ -2631,9 +2688,21 @@ export function RpgEntryHomeView({
|
||||
const desktopHeroStripEntries = (
|
||||
featuredShelf.length > 0 ? featuredShelf : latestEntries
|
||||
).slice(0, 5);
|
||||
const desktopTrendingEntries = latestEntries.slice(0, 3);
|
||||
const desktopFeaturedGrid = featuredShelf.slice(0, 4);
|
||||
const desktopReleaseGrid = latestEntries.slice(0, 6);
|
||||
// 网页端保留原有宽屏布局,只把模块数据同步到移动端首页频道语义。
|
||||
const desktopRecommendEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
[...featuredShelf, ...latestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, latestEntries]);
|
||||
const desktopTodayEntries = useMemo(
|
||||
() => filterTodayPublishedEntries(latestEntries),
|
||||
[latestEntries],
|
||||
);
|
||||
const desktopFeaturedGrid = desktopRecommendEntries.slice(0, 4);
|
||||
const desktopCategoryGrid = activeCategoryEntries.slice(0, 6);
|
||||
const desktopLibraryPreview = myEntries.slice(0, 2);
|
||||
const mobileFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
@@ -2648,6 +2717,86 @@ export function RpgEntryHomeView({
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, latestEntries, mobileHomeChannel]);
|
||||
const mobileFeedCarouselEnabled =
|
||||
!isDesktopLayout && activeTab === 'home' && mobileHomeChannel !== 'category';
|
||||
useEffect(() => {
|
||||
if (!mobileFeedCarouselEnabled) {
|
||||
setMobileCenteredCardKey(null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const feedElement = mobileFeedRef.current;
|
||||
const scrollElement = feedElement?.closest('.platform-tab-panel');
|
||||
if (!feedElement || !scrollElement) {
|
||||
setMobileCenteredCardKey(null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let frameId: number | null = null;
|
||||
const updateCenteredCard = () => {
|
||||
frameId = null;
|
||||
const cards = Array.from(
|
||||
feedElement.querySelectorAll<HTMLElement>('[data-mobile-feed-card-key]'),
|
||||
);
|
||||
const viewportRect = scrollElement.getBoundingClientRect();
|
||||
const viewportCenterY =
|
||||
viewportRect.top + Math.max(0, viewportRect.height) / 2;
|
||||
let closestKey: string | null = null;
|
||||
let closestDistance = Number.POSITIVE_INFINITY;
|
||||
|
||||
cards.forEach((card) => {
|
||||
const cardKey = card.dataset.mobileFeedCardKey;
|
||||
if (!cardKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
if (
|
||||
cardRect.bottom <= viewportRect.top ||
|
||||
cardRect.top >= viewportRect.bottom
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cardCenterY = cardRect.top + cardRect.height / 2;
|
||||
const distance = Math.abs(cardCenterY - viewportCenterY);
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestKey = cardKey;
|
||||
}
|
||||
});
|
||||
|
||||
setMobileCenteredCardKey((current) =>
|
||||
current === closestKey ? current : closestKey,
|
||||
);
|
||||
};
|
||||
const scheduleUpdate = () => {
|
||||
if (frameId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
frameId =
|
||||
typeof window.requestAnimationFrame === 'function'
|
||||
? window.requestAnimationFrame(updateCenteredCard)
|
||||
: window.setTimeout(updateCenteredCard, 0);
|
||||
};
|
||||
|
||||
scheduleUpdate();
|
||||
scrollElement.addEventListener('scroll', scheduleUpdate, { passive: true });
|
||||
window.addEventListener('resize', scheduleUpdate);
|
||||
|
||||
return () => {
|
||||
if (frameId !== null) {
|
||||
if (typeof window.cancelAnimationFrame === 'function') {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
} else {
|
||||
window.clearTimeout(frameId);
|
||||
}
|
||||
}
|
||||
scrollElement.removeEventListener('scroll', scheduleUpdate);
|
||||
window.removeEventListener('resize', scheduleUpdate);
|
||||
};
|
||||
}, [mobileFeedCarouselEnabled, mobileFeedEntries, mobileHomeChannel]);
|
||||
const activeRankingConfig = PLATFORM_RANKING_TABS.find(
|
||||
(tab) => tab.id === activeRankingTab,
|
||||
) as (typeof PLATFORM_RANKING_TABS)[number];
|
||||
@@ -2757,19 +2906,26 @@ export function RpgEntryHomeView({
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<section className="platform-mobile-home-feed">
|
||||
<section ref={mobileFeedRef} className="platform-mobile-home-feed">
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : mobileFeedEntries.length > 0 ? (
|
||||
<div className="grid min-w-0 gap-3">
|
||||
{mobileFeedEntries.map((entry: PlatformPublicGalleryCard) => (
|
||||
<WorldCard
|
||||
key={`${buildPublicGalleryCardKey(entry)}:mobile-feed:${mobileHomeChannel}`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
/>
|
||||
))}
|
||||
{mobileFeedEntries.map((entry: PlatformPublicGalleryCard) => {
|
||||
const cardKey = buildPublicGalleryCardKey(entry);
|
||||
|
||||
return (
|
||||
<WorldCard
|
||||
key={`${cardKey}:mobile-feed:${mobileHomeChannel}`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
feedCardKey={cardKey}
|
||||
enableCoverCarousel={mobileFeedCarouselEnabled}
|
||||
isCoverCarouselActive={mobileCenteredCardKey === cardKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
|
||||
@@ -3040,21 +3196,21 @@ export function RpgEntryHomeView({
|
||||
<>
|
||||
<ProfileStatCard
|
||||
cardKey="wallet"
|
||||
label="剩余陶泥币"
|
||||
label="陶泥币"
|
||||
value="暂不可用"
|
||||
icon={Coins}
|
||||
onClick={openWalletLedgerPanel}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playTime"
|
||||
label="总游戏时长"
|
||||
label="游戏时长"
|
||||
value="暂不可用"
|
||||
icon={Clock3}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playedWorks"
|
||||
label="玩过作品"
|
||||
label="玩过"
|
||||
value="暂不可用"
|
||||
icon={BookOpen}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
@@ -3064,21 +3220,21 @@ export function RpgEntryHomeView({
|
||||
<>
|
||||
<ProfileStatCard
|
||||
cardKey="wallet"
|
||||
label="剩余陶泥币"
|
||||
label="陶泥币"
|
||||
value={formatDashboardCount(remainingNarrativeCoins)}
|
||||
icon={Coins}
|
||||
onClick={openWalletLedgerPanel}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playTime"
|
||||
label="总游戏时长"
|
||||
label="游戏时长"
|
||||
value={totalPlayTime}
|
||||
icon={Clock3}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playedWorks"
|
||||
label="玩过作品"
|
||||
label="玩过"
|
||||
value={formatDashboardCount(playedWorkCount)}
|
||||
icon={BookOpen}
|
||||
onClick={onOpenProfileDashboardCard}
|
||||
@@ -3185,7 +3341,7 @@ export function RpgEntryHomeView({
|
||||
<span className="platform-pill platform-pill--neutral px-3">
|
||||
{leadPublicEntry
|
||||
? describePublicGalleryCardKind(leadPublicEntry)
|
||||
: '作品广场'}
|
||||
: '作品'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3196,7 +3352,7 @@ export function RpgEntryHomeView({
|
||||
<div className="mt-4 text-base leading-8 text-zinc-200/86">
|
||||
{leadPublicEntry?.summaryText ||
|
||||
leadPublicEntry?.subtitle ||
|
||||
'从公开广场进入作品详情,挑一个世界开始游玩。'}
|
||||
'挑一个玩家作品,开始今天的游玩。'}
|
||||
</div>
|
||||
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-white/18 bg-white/18 px-4 py-2 text-sm font-semibold text-white/92">
|
||||
<span>查看作品</span>
|
||||
@@ -3243,18 +3399,18 @@ export function RpgEntryHomeView({
|
||||
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<SectionHeader title="趋势关注" detail="TRENDING NOW" />
|
||||
<SectionHeader title="今日游戏" detail="TODAY GAMES" />
|
||||
<span className="platform-pill platform-pill--neutral px-3">
|
||||
LIVE
|
||||
TODAY
|
||||
</span>
|
||||
</div>
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在整理趋势作品..." />
|
||||
) : desktopTrendingEntries.length > 0 ? (
|
||||
<EmptyShelf text="正在读取今日游戏..." />
|
||||
) : desktopTodayEntries.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{desktopTrendingEntries.map((entry, index) => (
|
||||
{desktopTodayEntries.slice(0, 3).map((entry, index) => (
|
||||
<DesktopTrendingItem
|
||||
key={`${buildPublicGalleryCardKey(entry)}:desktop-trend`}
|
||||
key={`${buildPublicGalleryCardKey(entry)}:desktop-today`}
|
||||
entry={entry}
|
||||
rank={index + 1}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
@@ -3262,16 +3418,18 @@ export function RpgEntryHomeView({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="公开广场暂时还没有趋势作品。" />
|
||||
<EmptyShelf text="今天暂时还没有新游戏。" />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 2xl:grid-cols-[minmax(0,1.2fr)_minmax(22rem,0.8fr)]">
|
||||
<div
|
||||
className={`grid gap-5 ${desktopLibraryPreview.length > 0 || historyEntries.length > 0 ? '2xl:grid-cols-[minmax(0,1.2fr)_minmax(22rem,0.8fr)]' : ''}`}
|
||||
>
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader title="精选推荐" detail="CURATED WORLDS" />
|
||||
<SectionHeader title="推荐" detail="RECOMMENDED" />
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取精选作品..." />
|
||||
<EmptyShelf text="正在读取推荐作品..." />
|
||||
) : desktopFeaturedGrid.length > 0 ? (
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
{desktopFeaturedGrid.map((entry) => (
|
||||
@@ -3284,136 +3442,142 @@ export function RpgEntryHomeView({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="公开广场暂时还没有精选作品。" />
|
||||
<EmptyShelf text="暂时还没有推荐作品。" />
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader
|
||||
title={
|
||||
desktopLibraryPreview.length > 0
|
||||
? '最近作品'
|
||||
: historyEntries.length > 0
|
||||
? '最近浏览'
|
||||
: '作品广场'
|
||||
}
|
||||
detail="QUICK ACCESS"
|
||||
/>
|
||||
{desktopLibraryPreview.length > 0 || historyEntries.length > 0 ? (
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader
|
||||
title={desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'}
|
||||
detail="QUICK ACCESS"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold tracking-[0.24em] text-[var(--platform-text-soft)]">
|
||||
{desktopLibraryPreview.length > 0
|
||||
? '最近作品'
|
||||
: historyEntries.length > 0
|
||||
? '最近浏览'
|
||||
: '公开作品'}
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold tracking-[0.24em] text-[var(--platform-text-soft)]">
|
||||
{desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'}
|
||||
</div>
|
||||
|
||||
{desktopLibraryPreview.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{desktopLibraryPreview.map((entry) => {
|
||||
const displayName = formatPlatformWorkDisplayName(
|
||||
entry.worldName,
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${entry.ownerUserId}:${entry.profileId}:desktop-mine`}
|
||||
type="button"
|
||||
onClick={() => onOpenLibraryDetail(entry)}
|
||||
className="platform-desktop-trending-item flex w-full items-center justify-between gap-3 px-4 py-4 text-left"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-1 text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-[var(--platform-text-soft)]">
|
||||
{entry.visibility === 'published'
|
||||
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
|
||||
: '草稿待完善'}
|
||||
</div>
|
||||
</div>
|
||||
<span className="platform-pill platform-pill--neutral px-3">
|
||||
{entry.visibility === 'published' ? '已发布' : '草稿'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 space-y-3">
|
||||
{historyEntries.slice(0, 2).map((entry) => {
|
||||
const displayName = formatPlatformWorkDisplayName(
|
||||
entry.worldName,
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${entry.ownerUserId}:${entry.profileId}:desktop-history`}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onOpenGalleryDetail({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
publicWorkCode: null,
|
||||
authorPublicUserCode: null,
|
||||
visibility: 'published',
|
||||
publishedAt: entry.visitedAt,
|
||||
updatedAt: entry.visitedAt,
|
||||
worldName: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summaryText: entry.summaryText,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
themeMode: entry.themeMode,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
likeCount: 0,
|
||||
})
|
||||
}
|
||||
className="platform-desktop-trending-item flex w-full items-center justify-between gap-3 px-4 py-4 text-left"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-1 text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-[var(--platform-text-soft)]">
|
||||
作者:{entry.authorDisplayName}
|
||||
</div>
|
||||
</div>
|
||||
<span className="platform-pill platform-pill--neutral px-3">
|
||||
浏览
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{desktopLibraryPreview.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{desktopLibraryPreview.map((entry) => {
|
||||
const displayName = formatPlatformWorkDisplayName(
|
||||
entry.worldName,
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${entry.ownerUserId}:${entry.profileId}:desktop-mine`}
|
||||
type="button"
|
||||
onClick={() => onOpenLibraryDetail(entry)}
|
||||
className="platform-desktop-trending-item flex w-full items-center justify-between gap-3 px-4 py-4 text-left"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-1 text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-[var(--platform-text-soft)]">
|
||||
{entry.visibility === 'published'
|
||||
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
|
||||
: '草稿待完善'}
|
||||
</div>
|
||||
</div>
|
||||
<span className="platform-pill platform-pill--neutral px-3">
|
||||
{entry.visibility === 'published' ? '已发布' : '草稿'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : historyEntries.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{historyEntries.slice(0, 2).map((entry) => {
|
||||
const displayName = formatPlatformWorkDisplayName(
|
||||
entry.worldName,
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${entry.ownerUserId}:${entry.profileId}:desktop-history`}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onOpenGalleryDetail({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
publicWorkCode: null,
|
||||
authorPublicUserCode: null,
|
||||
visibility: 'published',
|
||||
publishedAt: entry.visitedAt,
|
||||
updatedAt: entry.visitedAt,
|
||||
worldName: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summaryText: entry.summaryText,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
themeMode: entry.themeMode,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
likeCount: 0,
|
||||
})
|
||||
}
|
||||
className="platform-desktop-trending-item flex w-full items-center justify-between gap-3 px-4 py-4 text-left"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-1 text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-[var(--platform-text-soft)]">
|
||||
作者:{entry.authorDisplayName}
|
||||
</div>
|
||||
</div>
|
||||
<span className="platform-pill platform-pill--neutral px-3">
|
||||
浏览
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="platform-subpanel mt-3 rounded-[1.35rem] px-4 py-4 text-sm leading-6 text-[var(--platform-text-base)]">
|
||||
公开广场暂时还没有可展示的作品。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader title="最新发布" detail="PLAYER SQUARE" />
|
||||
<SectionHeader title="作品分类" detail="GAME CATEGORY" />
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取最新发布..." />
|
||||
) : desktopReleaseGrid.length > 0 ? (
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{desktopReleaseGrid.map((entry) => (
|
||||
<WorldCard
|
||||
key={`${buildPublicGalleryCardKey(entry)}:desktop-latest`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<EmptyShelf text="正在读取作品分类..." />
|
||||
) : activeCategoryGroup && desktopCategoryGrid.length > 0 ? (
|
||||
<>
|
||||
<div className="mb-4 flex min-w-0 items-center gap-2 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{categoryGroups.map((group) => {
|
||||
const active = group.tag === activeCategoryGroup.tag;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${group.tag}:desktop-category`}
|
||||
type="button"
|
||||
onClick={() => setSelectedCategoryTag(group.tag)}
|
||||
className={`platform-category-chip shrink-0 ${active ? 'platform-category-chip--active' : ''}`}
|
||||
>
|
||||
{group.tag}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{desktopCategoryGrid.map((entry) => (
|
||||
<WorldCard
|
||||
key={`${buildPublicGalleryCardKey(entry)}:desktop-category:${activeCategoryGroup.tag}`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyShelf text="公开广场暂时还没有新作品。" />
|
||||
<EmptyShelf text="暂时还没有可分类的作品。" />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user