Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
This commit is contained in:
@@ -75,6 +75,14 @@ import {
|
||||
} from '../../services/rpg-entry/rpgProfileClient';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import {
|
||||
canExposePublicWork,
|
||||
EDUTAINMENT_WORK_TAG,
|
||||
filterEdutainmentPublicWorks,
|
||||
filterGeneralPublicWorks,
|
||||
findPublicWorkForHistoryEntry,
|
||||
isEdutainmentEntryEnabled,
|
||||
} from '../platform-entry/platformEdutainmentVisibility';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
|
||||
import {
|
||||
@@ -183,7 +191,12 @@ const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
|
||||
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
||||
|
||||
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
||||
type DiscoverChannel = 'recommend' | 'today' | 'category' | 'ranking';
|
||||
type DiscoverChannel =
|
||||
| 'recommend'
|
||||
| 'today'
|
||||
| 'category'
|
||||
| 'ranking'
|
||||
| 'edutainment';
|
||||
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
|
||||
|
||||
const COMMUNITY_QR_CODES = [
|
||||
@@ -208,6 +221,10 @@ const DISCOVER_CHANNELS: Array<{
|
||||
{ id: 'category', label: '分类' },
|
||||
{ id: 'ranking', label: '排行' },
|
||||
];
|
||||
const EDUTAINMENT_DISCOVER_CHANNEL = {
|
||||
id: 'edutainment',
|
||||
label: EDUTAINMENT_WORK_TAG,
|
||||
} as const;
|
||||
|
||||
const PLATFORM_RANKING_TABS: Array<{
|
||||
id: PlatformRankingTab;
|
||||
@@ -1313,9 +1330,11 @@ function buildPublicCategoryGroups(
|
||||
) {
|
||||
const publicEntryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
|
||||
[...featuredEntries, ...latestEntries].forEach((entry) => {
|
||||
publicEntryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach(
|
||||
(entry) => {
|
||||
publicEntryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
},
|
||||
);
|
||||
|
||||
const categoryMap = new Map<string, PlatformPublicGalleryCard[]>();
|
||||
Array.from(publicEntryMap.values()).forEach((entry) => {
|
||||
@@ -1346,6 +1365,21 @@ function getPlatformPublicEntries(
|
||||
) {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
|
||||
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach(
|
||||
(entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
},
|
||||
);
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}
|
||||
|
||||
function getAllPlatformPublicEntries(
|
||||
featuredEntries: PlatformPublicGalleryCard[],
|
||||
latestEntries: PlatformPublicGalleryCard[],
|
||||
) {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
|
||||
[...featuredEntries, ...latestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
@@ -3148,21 +3182,62 @@ export function RpgEntryHomeView({
|
||||
const [avatarError, setAvatarError] = useState<string | null>(null);
|
||||
const [isSavingAvatar, setIsSavingAvatar] = useState(false);
|
||||
const isAuthenticated = Boolean(authUi?.user);
|
||||
const edutainmentEntryEnabled = isEdutainmentEntryEnabled();
|
||||
const isDesktopLayout = usePlatformDesktopLayout();
|
||||
const openRecommendGalleryDetail =
|
||||
onOpenRecommendGalleryDetail ?? onOpenGalleryDetail;
|
||||
const featuredShelf = useMemo(
|
||||
() => featuredEntries.slice(0, 6),
|
||||
const generalFeaturedEntries = useMemo(
|
||||
() => filterGeneralPublicWorks(featuredEntries),
|
||||
[featuredEntries],
|
||||
);
|
||||
const categoryGroups = useMemo(
|
||||
() => buildPublicCategoryGroups(featuredEntries, latestEntries),
|
||||
const featuredShelf = useMemo(
|
||||
() => generalFeaturedEntries.slice(0, 6),
|
||||
[generalFeaturedEntries],
|
||||
);
|
||||
const generalLatestEntries = useMemo(
|
||||
() => filterGeneralPublicWorks(latestEntries),
|
||||
[latestEntries],
|
||||
);
|
||||
const allEdutainmentEntries = useMemo(
|
||||
() => filterEdutainmentPublicWorks([...featuredEntries, ...latestEntries]),
|
||||
[featuredEntries, latestEntries],
|
||||
);
|
||||
const edutainmentEntries = useMemo(
|
||||
() => (edutainmentEntryEnabled ? allEdutainmentEntries : []),
|
||||
[allEdutainmentEntries, edutainmentEntryEnabled],
|
||||
);
|
||||
const visibleDiscoverChannels = useMemo(
|
||||
() =>
|
||||
edutainmentEntryEnabled
|
||||
? [...DISCOVER_CHANNELS, EDUTAINMENT_DISCOVER_CHANNEL]
|
||||
: DISCOVER_CHANNELS,
|
||||
[edutainmentEntryEnabled],
|
||||
);
|
||||
const categoryGroups = useMemo(
|
||||
() =>
|
||||
buildPublicCategoryGroups(generalFeaturedEntries, generalLatestEntries),
|
||||
[generalFeaturedEntries, generalLatestEntries],
|
||||
);
|
||||
const publicEntries = useMemo(
|
||||
() => getPlatformPublicEntries(featuredEntries, latestEntries),
|
||||
() =>
|
||||
getPlatformPublicEntries(generalFeaturedEntries, generalLatestEntries),
|
||||
[generalFeaturedEntries, generalLatestEntries],
|
||||
);
|
||||
const allPublicEntries = useMemo(
|
||||
() => getAllPlatformPublicEntries(featuredEntries, latestEntries),
|
||||
[featuredEntries, latestEntries],
|
||||
);
|
||||
const visibleHistoryEntries = useMemo(
|
||||
() =>
|
||||
historyEntries.filter((entry) => {
|
||||
const matchedPublicWork = findPublicWorkForHistoryEntry(
|
||||
entry,
|
||||
allPublicEntries,
|
||||
);
|
||||
return !matchedPublicWork || canExposePublicWork(matchedPublicWork);
|
||||
}),
|
||||
[allPublicEntries, historyEntries],
|
||||
);
|
||||
const workSearchResults = useMemo(
|
||||
() =>
|
||||
filterPlatformWorkSearchResults(publicEntries, activeWorkSearchKeyword),
|
||||
@@ -3257,6 +3332,12 @@ export function RpgEntryHomeView({
|
||||
}
|
||||
}, [activeTab, isAuthenticated, onTabChange, visibleTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleDiscoverChannels.some((channel) => channel.id === discoverChannel)) {
|
||||
setDiscoverChannel('recommend');
|
||||
}
|
||||
}, [discoverChannel, visibleDiscoverChannels]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisitedTabs((currentTabs) => {
|
||||
if (currentTabs.has(activeTab)) {
|
||||
@@ -3739,6 +3820,10 @@ export function RpgEntryHomeView({
|
||||
publicEntries,
|
||||
trimmedKeyword,
|
||||
);
|
||||
const hiddenEdutainmentMatches = filterPlatformWorkSearchResults(
|
||||
allEdutainmentEntries,
|
||||
trimmedKeyword,
|
||||
);
|
||||
if (
|
||||
matchedEntries.length > 0 &&
|
||||
isExactPublicWorkCodeSearch(matchedEntries, trimmedKeyword) &&
|
||||
@@ -3755,6 +3840,11 @@ export function RpgEntryHomeView({
|
||||
return;
|
||||
}
|
||||
|
||||
if (hiddenEdutainmentMatches.length > 0) {
|
||||
setActiveWorkSearchKeyword(trimmedKeyword);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveWorkSearchKeyword('');
|
||||
if (!onSearchPublicCode || isSearchingPublicCode) {
|
||||
return;
|
||||
@@ -3769,50 +3859,58 @@ export function RpgEntryHomeView({
|
||||
submitWorkSearch(mobileSearchKeyword);
|
||||
};
|
||||
const desktopHeroEntry =
|
||||
featuredShelf[0] ?? latestEntries[0] ?? myEntries[0] ?? null;
|
||||
featuredShelf[0] ?? generalLatestEntries[0] ?? myEntries[0] ?? null;
|
||||
const desktopHeroCover = desktopHeroEntry
|
||||
? resolvePlatformWorldCoverImage(desktopHeroEntry)
|
||||
: null;
|
||||
const desktopHeroStripEntries = (
|
||||
featuredShelf.length > 0 ? featuredShelf : latestEntries
|
||||
featuredShelf.length > 0 ? featuredShelf : generalLatestEntries
|
||||
).slice(0, 5);
|
||||
// 网页端保留原有宽屏布局,只把模块数据同步到移动端首页频道语义。
|
||||
const desktopRecommendEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
[...featuredShelf, ...latestEntries].forEach((entry) => {
|
||||
[...featuredShelf, ...generalLatestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, latestEntries]);
|
||||
}, [featuredShelf, generalLatestEntries]);
|
||||
const desktopTodayEntries = useMemo(
|
||||
() => filterTodayPublishedEntries(latestEntries),
|
||||
[latestEntries],
|
||||
() => filterTodayPublishedEntries(generalLatestEntries),
|
||||
[generalLatestEntries],
|
||||
);
|
||||
const desktopFeaturedGrid = desktopRecommendEntries.slice(0, 4);
|
||||
const desktopCategoryGrid = activeCategoryEntries.slice(0, 6);
|
||||
const desktopLibraryPreview = myEntries.slice(0, 2);
|
||||
const recommendedFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
[...featuredShelf, ...latestEntries].forEach((entry) => {
|
||||
[...featuredShelf, ...generalLatestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, latestEntries]);
|
||||
}, [featuredShelf, generalLatestEntries]);
|
||||
const discoverFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
const sourceEntries =
|
||||
discoverChannel === 'recommend'
|
||||
? recommendedFeedEntries
|
||||
: filterTodayPublishedEntries(latestEntries);
|
||||
: filterTodayPublishedEntries(generalLatestEntries);
|
||||
|
||||
sourceEntries.forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [discoverChannel, latestEntries, recommendedFeedEntries]);
|
||||
}, [discoverChannel, generalLatestEntries, recommendedFeedEntries]);
|
||||
const edutainmentFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
edutainmentEntries.forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [edutainmentEntries]);
|
||||
const mobileFeedCarouselEnabled =
|
||||
!isDesktopLayout &&
|
||||
activeTab === 'category' &&
|
||||
@@ -4125,7 +4223,7 @@ export function RpgEntryHomeView({
|
||||
isAuthenticated,
|
||||
openRecommendGalleryDetail,
|
||||
]);
|
||||
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
|
||||
const leadPublicEntry = featuredShelf[0] ?? generalLatestEntries[0] ?? null;
|
||||
const openLeadPublicEntry = () => {
|
||||
if (leadPublicEntry) {
|
||||
openRecommendGalleryDetail(leadPublicEntry);
|
||||
@@ -4324,7 +4422,7 @@ export function RpgEntryHomeView({
|
||||
) : (
|
||||
<>
|
||||
<div className="platform-mobile-home-channelbar flex min-w-0 gap-4 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{DISCOVER_CHANNELS.map((channel) => {
|
||||
{visibleDiscoverChannels.map((channel) => {
|
||||
const active = discoverChannel === channel.id;
|
||||
return (
|
||||
<button
|
||||
@@ -4403,6 +4501,31 @@ export function RpgEntryHomeView({
|
||||
<EmptyShelf text="公开广场暂时还没有可分类的作品。" />
|
||||
)}
|
||||
</section>
|
||||
) : discoverChannel === 'edutainment' ? (
|
||||
<section className="platform-mobile-home-feed">
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : edutainmentFeedEntries.length > 0 ? (
|
||||
<div className="grid min-w-0 gap-3">
|
||||
{edutainmentFeedEntries.map((entry) => {
|
||||
const cardKey = buildPublicGalleryCardKey(entry);
|
||||
|
||||
return (
|
||||
<WorldCard
|
||||
key={`${cardKey}:mobile-edutainment`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
feedCardKey={cardKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="暂时还没有可展示的作品。" />
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<section
|
||||
ref={mobileDiscoverFeedRef}
|
||||
@@ -4439,8 +4562,122 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
);
|
||||
|
||||
const desktopDiscoverContent: ReactNode = (
|
||||
<div className={DESKTOP_PAGE_STAGE_CLASS}>
|
||||
<div className="platform-mobile-home-channelbar flex min-w-0 gap-4 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{visibleDiscoverChannels.map((channel) => {
|
||||
const active = discoverChannel === channel.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`desktop-${channel.id}`}
|
||||
type="button"
|
||||
onClick={() => setDiscoverChannel(channel.id)}
|
||||
className={`platform-mobile-home-channel shrink-0 ${active ? 'platform-mobile-home-channel--active' : ''}`}
|
||||
>
|
||||
{channel.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{platformError ? (
|
||||
<div className="rounded-[1.5rem] border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{discoverChannel === 'ranking' ? (
|
||||
mobileRankingPanel
|
||||
) : discoverChannel === 'category' ? (
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader title="作品分类" detail="GAME CATEGORY" />
|
||||
{isLoadingPlatform ? (
|
||||
<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-discover-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-discover-category:${activeCategoryGroup.tag}`}
|
||||
entry={entry}
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyShelf text="暂时还没有可分类的作品。" />
|
||||
)}
|
||||
</section>
|
||||
) : discoverChannel === 'edutainment' ? (
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader title={EDUTAINMENT_WORK_TAG} detail="EDUTAINMENT" />
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : edutainmentFeedEntries.length > 0 ? (
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{edutainmentFeedEntries.map((entry) => (
|
||||
<WorldCard
|
||||
key={`${buildPublicGalleryCardKey(entry)}:desktop-edutainment`}
|
||||
entry={entry}
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="暂时还没有可展示的作品。" />
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader
|
||||
title={discoverChannel === 'today' ? '今日游戏' : '推荐'}
|
||||
detail={discoverChannel === 'today' ? 'TODAY GAMES' : 'RECOMMENDED'}
|
||||
/>
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : discoverFeedEntries.length > 0 ? (
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{discoverFeedEntries.map((entry) => (
|
||||
<WorldCard
|
||||
key={`${buildPublicGalleryCardKey(entry)}:desktop-discover-feed:${discoverChannel}`}
|
||||
entry={entry}
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const categoryContent: ReactNode = isDesktopLayout ? (
|
||||
<div className={DESKTOP_PAGE_STAGE_CLASS}>{mobileRankingPanel}</div>
|
||||
desktopDiscoverContent
|
||||
) : (
|
||||
mobileDiscoverContent
|
||||
);
|
||||
@@ -4880,7 +5117,7 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`grid gap-5 ${desktopLibraryPreview.length > 0 || historyEntries.length > 0 ? '2xl:grid-cols-[minmax(0,1.2fr)_minmax(22rem,0.8fr)]' : ''}`}
|
||||
className={`grid gap-5 ${desktopLibraryPreview.length > 0 || visibleHistoryEntries.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="RECOMMENDED" />
|
||||
@@ -4903,7 +5140,7 @@ export function RpgEntryHomeView({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{desktopLibraryPreview.length > 0 || historyEntries.length > 0 ? (
|
||||
{desktopLibraryPreview.length > 0 || visibleHistoryEntries.length > 0 ? (
|
||||
<section className="platform-desktop-panel px-5 py-5">
|
||||
<SectionHeader
|
||||
title={desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'}
|
||||
@@ -4948,7 +5185,7 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 space-y-3">
|
||||
{historyEntries.slice(0, 2).map((entry) => {
|
||||
{visibleHistoryEntries.slice(0, 2).map((entry) => {
|
||||
const displayName = formatPlatformWorkDisplayName(
|
||||
entry.worldName,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user