import { Ionicons } from '@expo/vector-icons' import { useIsFocused } from '@react-navigation/native' import { Block, ConfirmModal, Img, ListEmpty, Text, Toast, VideoBox } from '@share/components' import { FlashList } from '@shopify/flash-list' import { LinearGradient } from 'expo-linear-gradient' import { router, useFocusEffect } from 'expo-router' import { observer } from 'mobx-react-lite' import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ActivityIndicator, RefreshControl, TextInput } from 'react-native' import BannerSection from '@/components/BannerSection' import { useTemplateActions } from '@/hooks/actions/use-template-actions' import { useTemplates } from '@/hooks/data' import { useFavoriteTemplates } from '@/hooks/data/use-favorite-templates' import { userBalanceStore, userStore } from '@/stores' import { screenWidth } from '@/utils' import { cn } from '@/utils/cn' const CATEGORY_ID = process.env.EXPO_PUBLIC_INDEX_GROUP_ID const ITEM_WIDTH = Math.floor((screenWidth - 24 - 12 * 2) / 3) const PAGE_SIZE = 12 type MediaItem = { id: string type: 'image' | 'video' url: string | '' poster?: string webpPreviewUrl?: string webpHighPreviewUrl?: string mp4Url?: string likeCount?: number price: number title?: string authorName?: string } type ActiveTab = 'gen' | '' | 'new' | 'like' /** ========================= * Entry page * ========================= */ const Index = observer(function Index() { // 从MobX Store获取用户信息 const { user, isAuthenticated } = userStore as typeof userStore const { execute: loadTemplates } = useTemplates() const { execute: loadFavorites } = useFavoriteTemplates() const { runTemplate } = useTemplateActions() /** ================= 状态 ================= */ const [activeTab, setActiveTab] = useState('') const [isSearchOpen, setIsSearchOpen] = useState(false) const [allItems, setAllItems] = useState([]) const [selectedItem, setSelectedItem] = useState(null) // 直接使用 useIsFocused hook,无需手动管理状态 const isFocused = useIsFocused() const [refreshing, setRefreshing] = useState(false) const [loadingMore, setLoadingMore] = useState(false) const [hasMore, setHasMore] = useState(true) /** ================= refs(核心) ================= */ const queryRef = useRef({ page: 1, pageSize: PAGE_SIZE, search: '', tab: '' as ActiveTab, }) const loadingRef = useRef(false) const firstLoadDoneRef = useRef(false) /** ================= 工具函数 ================= */ type TemplateData = { id: string previewUrl?: string coverImageUrl?: string likeCount?: number price?: number title?: string isDeleted?: boolean } const transformTemplateToMediaItem = useCallback((template: TemplateData): MediaItem => { const isVideo = template.previewUrl?.includes('.mp4') return { id: template.id, type: isVideo ? 'video' : 'image', url: isVideo ? template?.previewUrl || '' : template.coverImageUrl || template.previewUrl || '', webpPreviewUrl: template?.webpPreviewUrl, webpHighPreviewUrl: template?.webpHighPreviewUrl, mp4Url: template?.previewUrl, poster: isVideo ? template.coverImageUrl : undefined, likeCount: template.likeCount || 0, price: template.price || 0, authorName: template?.user?.name || '未知作者', title: template.title || '', } }, []) /** ================= 统一请求函数 ================= */ const fetchList = async (mode: 'init' | 'refresh' | 'loadMore') => { if (loadingRef.current) return if (mode === 'loadMore' && !hasMore) return loadingRef.current = true if (mode === 'loadMore') { setLoadingMore(true) } else { setRefreshing(true) } try { const { page, pageSize, search, tab } = queryRef.current let newItems: MediaItem[] = [] if (tab === 'like') { console.log('加载收藏列表,isAuthenticated=', isAuthenticated) if (!isAuthenticated) { setHasMore(false) newItems = [] } else { const { data } = await loadFavorites({ page, limit: pageSize }) newItems = data?.favorites ?.filter((f) => f.template && !(f.template as TemplateData).isDeleted) .map((f) => transformTemplateToMediaItem(f.template as TemplateData)) || [] } } else { const sortBy = tab === 'new' ? 'createdAt' : 'likeCount' const { data } = await loadTemplates({ page, limit: pageSize, sortBy, sortOrder: 'desc', search, categoryId: CATEGORY_ID, }) newItems = data?.templates?.map(transformTemplateToMediaItem) || [] } if (newItems.length < pageSize) { setHasMore(false) } const updatedItems = mode === 'loadMore' ? [...allItems, ...newItems] : newItems setAllItems(updatedItems) } catch (e) { console.error('fetchList error:', e) setHasMore(false) } finally { loadingRef.current = false const currentPage = queryRef.current.page if (currentPage === 1) { firstLoadDoneRef.current = true } setRefreshing(false) setLoadingMore(false) } } /** ================= tab 切换 ================= */ const changeTab = useCallback( (tab: ActiveTab) => { loadingRef.current = false queryRef.current = { ...queryRef.current, tab, page: 1, } setHasMore(true) firstLoadDoneRef.current = false fetchList('init') }, [isAuthenticated], ) /** ================= 搜索 ================= */ const handleSearch = useCallback((text: string) => { queryRef.current = { ...queryRef.current, search: text, page: 1, } setHasMore(true) firstLoadDoneRef.current = false fetchList('refresh') }, []) /** ================= 刷新 / 加载更多 ================= */ const handleRefresh = useCallback(() => { queryRef.current.page = 1 setHasMore(true) firstLoadDoneRef.current = false fetchList('refresh') }, []) const handleLoadMore = useCallback(() => { if (loadingRef.current || !hasMore || refreshing || loadingMore) return if (!firstLoadDoneRef.current) return queryRef.current.page += 1 fetchList('loadMore') }, [hasMore, refreshing, loadingMore]) /** ================= selectedItem 修正 ================= */ useEffect(() => { if (!allItems.length) { setSelectedItem(null) return } if (!allItems.find((i) => i.id === selectedItem?.id)) { setSelectedItem(allItems[0]) } }, [allItems, selectedItem?.id]) useEffect(() => { fetchList('init') userBalanceStore.load(true) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { const timer = setTimeout(() => { if (isAuthenticated && user?.id) { console.log('用户已登录,启动余额轮询') userBalanceStore.startPolling() } else { console.log('用户未登录,停止余额轮询') userBalanceStore.stopPolling() } }, 100) return () => { clearTimeout(timer) } }, [isAuthenticated, user?.id]) useEffect(() => { return () => { userBalanceStore.stopPolling() } }, []) useFocusEffect(() => { if (isAuthenticated && user?.id) { userBalanceStore.load(true) } }) /** ================= 快速生成 ================= */ const handleQuickGen = async () => { if (!isAuthenticated) { router.push('/auth') return } if (!selectedItem) { Toast.show({ title: '请先选择一个模板' }) return } Toast.showLoading() try { await userBalanceStore.load(true) const currentBalance = userBalanceStore.balance if (currentBalance < selectedItem.price) { Toast.show({ title: '余额不足,请充值' }) return } } catch (error) { Toast.show({ title: '余额加载失败,请重试' }) return } finally { Toast.hideLoading() } Toast.showModal( 生成将消耗 {selectedItem.price} } onCancel={Toast.hideModal} onConfirm={async () => { Toast.hideModal() Toast.showLoading() try { userBalanceStore.deductBalance(selectedItem.price) const { generationId, error } = await runTemplate({ templateId: selectedItem.id, data: {}, }) if (generationId && user?.id) { await userBalanceStore.load(true) Toast.show({ title: '生成任务开启,请在我的生成中查看' }) } else { userBalanceStore.setBalance(userBalanceStore.balance + selectedItem.price) Toast.show({ title: error?.message || '生成失败' }) } } catch (error) { userBalanceStore.setBalance(userBalanceStore.balance + selectedItem.price) Toast.show({ title: '网络异常,请重试' }) } finally { Toast.hideLoading() } }} />, ) } const renderListEmpty = () => { if (activeTab === 'like' && !isAuthenticated) { return ( 查看您收藏的模板 登录后即可查看和管理收藏 router.push('/auth')} > 立即登录 ) } else { return } } // 简化的 selectedId,避免对象引用变化 const selectedId = selectedItem?.id // 可见性状态 const visibleIdsRef = useRef>(new Set()) const [visibilityVersion, setVisibilityVersion] = useState(0) // 可见性变化回调 - 简化版 const onViewableItemsChanged = useCallback( (info: { viewableItems: { isViewable: boolean; key: string; index: number | null }[] }) => { const { viewableItems } = info const currentVisible = new Set(viewableItems.filter((v) => v.isViewable).map((v) => v.key)) // 快速滑动时可能为空,保留上次状态 if (currentVisible.size === 0 && visibleIdsRef.current.size > 0) return // 添加前后缓冲(减少到3个以降低内存占用) if (viewableItems.length > 0 && allItems.length > 0) { const first = viewableItems[0]?.index ?? 0 const last = viewableItems[viewableItems.length - 1]?.index ?? first if (first !== null && last !== null) { for (let i = Math.max(0, first - 6); i < first; i++) { if (allItems[i]) currentVisible.add(allItems[i].id) } for (let i = last + 1; i < Math.min(allItems.length, last + 7); i++) { currentVisible.add(allItems[i].id) } } } visibleIdsRef.current = currentVisible setVisibilityVersion((v) => v + 1) }, [allItems], ) const renderItem = useCallback( ({ item, index }: { item: MediaItem; index: number }) => ( setSelectedItem(item)} /> ), [selectedId, isFocused], ) const keyExtractor = useCallback((item: MediaItem) => item.id, []) /** ================= UI ================= */ return ( setIsSearchOpen(true)} onQuickGen={handleQuickGen} selectedItem={selectedItem} /> { setActiveTab(tab) changeTab(tab) }} /> ) : null } numColumns={3} onEndReached={handleLoadMore} maxItemsInRecyclePool={0} removeClippedSubviews={true} drawDistance={300} onEndReachedThreshold={0.3} refreshControl={} renderItem={renderItem} keyExtractor={keyExtractor} ListEmptyComponent={renderListEmpty} onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={{ itemVisiblePercentThreshold: 10, minimumViewTime: 50, waitForInteraction: false, }} showsVerticalScrollIndicator={false} data={allItems} extraData={visibilityVersion} /> setIsSearchOpen(false)} onSearch={handleSearch} /> ) }) type SearchOverlayProps = { isOpen: boolean searchText?: string onChange?: (v: string) => void onClose: () => void onSearch: (v: string) => void } const SearchOverlay = memo(function SearchOverlay({ isOpen, onChange, onClose, onSearch }) { const timerRef = useRef | null>(null) const [searchText, setSearchText] = useState('') const handleTextChange = useCallback( (text: string) => { setSearchText(text) onChange?.(text) if (timerRef.current) { clearTimeout(timerRef.current) } timerRef.current = setTimeout(() => { onSearch(text) }, 500) }, [onChange, onSearch], ) const handleClose = useCallback(() => { setSearchText('') onSearch('') onClose?.() }, [onClose, onSearch]) if (!isOpen) return null return ( ) }) type GooActionsProps = { gooPoints: number onAddGoo: () => void onOpenSearch: () => void } const GooActions = observer(function GooActions({ gooPoints, onAddGoo, onOpenSearch }) { const { isPolling } = userBalanceStore const isDev = __DEV__ const isAuthenticated = userStore.isAuthenticated return ( {!!isAuthenticated && ( {gooPoints} {isDev && isPolling && } )} ) }) type HeroCircleProps = { selectedItem: MediaItem | null onQuickGen: () => void onOpenSearch: () => void } const HeroCircle = observer(function HeroCircle({ selectedItem, onQuickGen, onOpenSearch }) { const isAuthenticated = userStore.isAuthenticated const { balance } = userBalanceStore const Width = 216 const previewUrl = selectedItem?.webpHighPreviewUrl || selectedItem?.url || '' const existItem = !!selectedItem?.url return ( {existItem && ( )} {!existItem && ( 加载中... )} {selectedItem?.title} GET 同款 { if (!isAuthenticated) { router.push('/auth') } else { router.push('/pointList') } }} onOpenSearch={onOpenSearch} /> ) }) type FilterSectionProps = { activeTab: ActiveTab onChange: (t: ActiveTab) => void } const FilterSection = memo(function FilterSection({ activeTab, onChange }) { const tabs = useMemo( () => [ { label: '最热', state: '' as const }, { label: '最新', state: 'new' as const }, { label: '喜欢', state: 'like' as const }, ], [], ) return ( {tabs.map(({ label, state }) => { const isActive = activeTab === state return ( onChange(state)} style={{ transform: [{ skewX: '-6deg' }], }} > {label} ) })} ) }) type GridItemProps = { item: MediaItem isSelected: boolean itemWidth: number onSelect: () => void isVisible: boolean } const GridItem = memo(function GridItem({ item, isSelected, itemWidth, onSelect, isVisible }) { // console.log('item-------------', item); const previewUrl = item?.webpPreviewUrl || item?.url || '' // console.log('previewUrl-------------', isVisible) // 使用压缩好了webp预览,不可见情况下不渲染图片 return ( {isVisible && ( )} {isSelected && } {item.title} {item.authorName || '未知作者'} {item.likeCount} ) }) export default Index