import { Ionicons } from '@expo/vector-icons' import { Block, ConfirmModal, 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 = 21 type MediaItem = { id: string type: 'image' | 'video' url: string poster?: 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) 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?.webpPreviewUrl ?? template?.previewUrl) : template.coverImageUrl || 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 // 标记首次(或第一页)加载已完成,避免 FlashList 首次渲染触发 onEndReached const currentPage = queryRef.current.page if (currentPage === 1) { firstLoadDoneRef.current = true } setRefreshing(false) setLoadingMore(false) } } /** ================= tab 切换(抽象后的重点) ================= */ const changeTab = useCallback( (tab: ActiveTab) => { // 重置加载状态,确保新的tab切换不会被旧的加载状态阻挡 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 // if (allItems.length === 0) return // 首次加载未完成时不触发加载更多 queryRef.current.page += 1 fetchList('loadMore') console.log('handleLoadMore-------------') }, [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) // 100ms延迟,避免刷新时的快速状态切换 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 } } // 缓存渲染函数以提升性能 const renderItem = useCallback( ({ item }: { item: MediaItem }) => ( setSelectedItem(item)} /> ), [selectedItem?.id], ) const keyExtractor = useCallback((item: MediaItem) => item.id, []) /** ================= UI ================= */ return ( setIsSearchOpen(true)} onQuickGen={handleQuickGen} selectedItem={selectedItem} /> { setActiveTab(tab) changeTab(tab) }} /> ) : null } numColumns={3} onEndReached={handleLoadMore} drawDistance={1200} onEndReachedThreshold={0.3} refreshControl={} renderItem={renderItem} keyExtractor={keyExtractor} ListEmptyComponent={renderListEmpty} showsVerticalScrollIndicator={false} data={allItems} // @ts-ignore estimatedItemSize={ITEM_WIDTH} /> 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) } // 增加防抖时间到500ms,减少频繁搜索 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 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 } // 优化GridItem组件,减少不必要的重渲染 const GridItem = memo( function GridItem({ item, isSelected, itemWidth, onSelect }) { // console.log('item-------------', item); return ( {isSelected && } {item.title} {item.authorName || '未知作者'} {item.likeCount} ) }, (prevProps, nextProps) => { // 自定义比较函数,只在关键属性变化时才重渲染 return ( prevProps.item.id === nextProps.item.id && prevProps.isSelected === nextProps.isSelected && prevProps.itemWidth === nextProps.itemWidth && prevProps.item.likeCount === nextProps.item.likeCount ) }, ) export default Index