From 8f00d4644accc489ac2e2fa32d3998bf5b75642a Mon Sep 17 00:00:00 2001 From: imeepos Date: Tue, 27 Jan 2026 17:08:11 +0800 Subject: [PATCH] feat: implement pull-to-refresh and load more functionality in "My" page, add WebP image support --- .gitignore | 2 +- app/(tabs)/my.tsx | 68 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 96fdb49..57f950e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ expo-env.d.ts tmpclaude-* *empclaude* -tmpclaude-* +*tmpclaude-* # Native .kotlin/ diff --git a/app/(tabs)/my.tsx b/app/(tabs)/my.tsx index efde3ec..31c7eee 100644 --- a/app/(tabs)/my.tsx +++ b/app/(tabs)/my.tsx @@ -7,6 +7,10 @@ import { Dimensions, Pressable, StatusBar as RNStatusBar, + RefreshControl, + ActivityIndicator, + NativeScrollEvent, + NativeSyntheticEvent, } from 'react-native' import { StatusBar } from 'expo-status-bar' import { SafeAreaView } from 'react-native-safe-area-context' @@ -17,10 +21,10 @@ import { PointsIcon, SearchIcon, SettingsIcon } from '@/components/icon' import EditProfileDrawer from '@/components/drawer/EditProfileDrawer' import Dropdown from '@/components/ui/dropdown' import Toast from '@/components/ui/Toast' -import { signOut } from '@/lib/auth' +import { signOut , useSession } from '@/lib/auth' import { useTemplateGenerations, type TemplateGeneration } from '@/hooks' import { MySkeleton } from '@/components/skeleton/MySkeleton' -import { useSession } from '@/lib/auth' + import { useUserBalance } from '@/hooks/use-user-balance' const { width: screenWidth } = Dimensions.get('window') @@ -30,9 +34,9 @@ const GALLERY_ITEM_SIZE = Math.floor( (screenWidth - GALLERY_HORIZONTAL_PADDING * 2 - GALLERY_GAP * 2) / 3 ) -// 获取作品封面图 +// 获取作品封面图 - Webp优先 const getCoverUrl = (item: TemplateGeneration) => - item.resultUrl?.[0] || item.template?.coverImageUrl + item.webpPreviewUrl || item.resultUrl?.[0] || item.template?.coverImageUrl export default function My() { const router = useRouter() @@ -58,16 +62,41 @@ export default function My() { const { generations, loading, - error, - execute: loadGenerations, + loadingMore, refetch, + loadMore, + hasMore, } = useTemplateGenerations() // 初始化加载作品列表 useEffect(() => { - loadGenerations({ page: 1, limit: 50 }) + refetch({ page: 1, limit: 50 }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + // 下拉刷新状态 + const [refreshing, setRefreshing] = useState(false) + + // 下拉刷新处理 + const onRefresh = useCallback(async () => { + setRefreshing(true) + await refetch({ page: 1, limit: 50 }) + setRefreshing(false) + }, [refetch]) + + // 加载更多处理 + const handleEndReached = useCallback((event: NativeSyntheticEvent) => { + const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent + const paddingToBottom = 100 // 距离底部100px时触发加载更多 + if ( + layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom && + !loadingMore && + hasMore + ) { + loadMore() + } + }, [loadingMore, hasMore, loadMore]) + // 处理设置菜单选择 const handleSettingsSelect = async (value: string) => { if (value === 'changePassword') { @@ -193,6 +222,18 @@ export default function My() { style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} + onScroll={handleEndReached} + scrollEventThrottle={400} + refreshControl={ + + } > {generations.map((item, index) => ( @@ -247,6 +288,13 @@ export default function My() { ))} + {/* 加载更多指示器 */} + {loadingMore && ( + + + + )} + {/* 空状态提示 */} {!loading && generations.length === 0 && ( @@ -419,6 +467,12 @@ const styles = StyleSheet.create({ fontSize: 9, fontWeight: '500', }, + loadingMoreContainer: { + width: '100%', + paddingVertical: 20, + alignItems: 'center', + justifyContent: 'center', + }, emptyState: { width: '100%', alignItems: 'center',