This commit is contained in:
imeepos
2025-11-12 13:19:06 +08:00
parent a7d922b535
commit 3ebcc24e9b
14 changed files with 483 additions and 265 deletions

View File

@@ -1,6 +1,6 @@
import { router } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ScrollView, StyleSheet, View, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native';
import {
CategoryTabs,
@@ -69,16 +69,18 @@ function translateActivity(activity: Activity): FeatureItem | null {
export default function ExploreScreen() {
const { isAuthenticated } = useAuth();
const { requireAuth } = useAuthGuard();
const [activeCategory, setActiveCategory] = useState<string>();
const [activeCategory, setActiveCategory] = useState<string>('');
const [activityFeatures, setActivityFeatures] = useState<FeatureItem[]>([]);
const [categoryCollection, setCategoryCollection] = useState<CategoryWithChildren[]>([]);
const scrollViewRef = useRef<ScrollView>(null);
const categoryPositionsRef = useRef<Map<string, number>>(new Map());
const isScrollingProgrammaticallyRef = useRef(false);
useEffect(() => {
const hydrateFeatureCarousel = async () => {
try {
const activities = await getActivities({ isActive: true });
console.log({ activities })
const curatedFeatures = activities
.map(translateActivity)
.filter((feature): feature is FeatureItem => feature !== null);
@@ -92,9 +94,6 @@ export default function ExploreScreen() {
};
hydrateFeatureCarousel();
return () => {
};
}, []);
useEffect(() => {
@@ -157,65 +156,115 @@ export default function ExploreScreen() {
return options.length > 0 ? options : [];
}, [categoryCollection]);
const allCategoriesWithTemplates = useMemo(() => {
return categoryCollection.map((category) => {
const templates = (category.templates ?? [])
.map(translateTemplateToCommunity)
.filter((item): item is CommunityItem => item !== null);
const activeCategoryRecord = useMemo(() => {
return categoryCollection.find((category) => category.id === activeCategory) ?? null;
}, [categoryCollection, activeCategory]);
return {
id: category.id,
name: coalesceText(category.nameEn, category.name),
templates,
};
});
}, [categoryCollection]);
const templateCommunityItems = useMemo(() => {
if (!activeCategoryRecord) {
return [];
const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (isScrollingProgrammaticallyRef.current) {
return;
}
return (activeCategoryRecord.templates ?? [])
.map(translateTemplateToCommunity)
.filter((item): item is CommunityItem => item !== null);
}, [activeCategoryRecord]);
const scrollY = event.nativeEvent.contentOffset.y;
const positions = Array.from(categoryPositionsRef.current.entries());
const featureItems = useMemo(() => {
return activityFeatures.map((item, index) => ({
...item,
id: `${activeCategory}-${item.id || index}`,
}));
}, [activityFeatures]);
let newActiveCategory = activeCategory;
for (let i = positions.length - 1; i >= 0; i--) {
const [categoryId, position] = positions[i];
if (scrollY >= position - 100) {
newActiveCategory = categoryId;
break;
}
}
const communityItems = useMemo(() => {
const source: CommunityItem[] =
templateCommunityItems.length > 0 ? templateCommunityItems : [];
if (newActiveCategory !== activeCategory) {
setActiveCategory(newActiveCategory);
}
}, [activeCategory]);
return source.map((item, index) => ({
...item,
id: `${activeCategory}-${item.id || index}`,
}));
}, [activeCategory, templateCommunityItems]);
const handleCategoryChange = useCallback((categoryId: string) => {
const targetPosition = categoryPositionsRef.current.get(categoryId);
if (targetPosition === undefined) {
return;
}
const handleGeneratePress = (item: CommunityItem) => {
isScrollingProgrammaticallyRef.current = true;
scrollViewRef.current?.scrollTo({
y: targetPosition,
animated: true,
});
setTimeout(() => {
isScrollingProgrammaticallyRef.current = false;
}, 500);
setActiveCategory(categoryId);
}, []);
const handleGeneratePress = useCallback((item: CommunityItem) => {
requireAuth(() => {
const templateId = item.id.split('-').pop() || item.id;
router.push(`/templates/${templateId}`);
});
};
}, [requireAuth]);
const handleFeaturePress = (item: FeatureItemType) => {
const handleFeaturePress = useCallback((item: FeatureItemType) => {
requireAuth(() => {
console.log('用户已登录,使用功能:', item.title);
// TODO: 实现功能逻辑
});
};
}, [requireAuth]);
return (
<PageLayout backgroundColor="#050505">
<PageLayout backgroundColor="#050505" topInset="none">
<StatusBarSpacer />
<Header />
<CategoryTabs
categories={categoryOptions}
activeId={activeCategory}
onChange={handleCategoryChange}
/>
<ScrollView
ref={scrollViewRef}
style={styles.scroll}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
scrollEventThrottle={16}
>
<StatusBarSpacer />
<Header />
<CategoryTabs categories={categoryOptions} activeId={activeCategory || ``} onChange={setActiveCategory} />
{featureItems.length > 0 && <FeatureCarousel items={featureItems} onPress={handleFeaturePress} />}
<SectionHeader title={activeCategoryRecord?.name || ``} />
<CommunityGrid items={communityItems} onPressAction={handleGeneratePress} />
<View style={styles.bottomSpacer} />
{activityFeatures.length > 0 && (
<FeatureCarousel
items={activityFeatures}
onPress={handleFeaturePress}
/>
)}
{allCategoriesWithTemplates.map((category, index) => (
<View
key={category.id}
onLayout={(event) => {
const layout = event.nativeEvent.layout;
categoryPositionsRef.current.set(category.id, layout.y);
}}
>
<SectionHeader title={category.name} />
<CommunityGrid
items={category.templates.map((template, idx) => ({
...template,
id: `${category.id}-${template.id || idx}`,
}))}
onPressAction={handleGeneratePress}
/>
</View>
))}
</ScrollView>
</PageLayout>
);
@@ -226,10 +275,6 @@ const styles = StyleSheet.create({
flex: 1,
},
contentContainer: {
paddingTop: 16,
paddingBottom: 48,
},
bottomSpacer: {
height: 32,
paddingBottom: 16,
},
});