import { Ionicons } from '@expo/vector-icons' import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components' import Img from '@share/components/Img' import { FlashList } from '@shopify/flash-list' import { LinearGradient } from 'expo-linear-gradient' import { router } from 'expo-router' import React, { memo, 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 { useAuth } from '@/hooks/core/use-auth' import { useUserBalance } from '@/hooks/core/use-user-balance' import { useTemplates } from '@/hooks/data' import { useFavoriteTemplates } from '@/hooks/data/use-favorite-templates' 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 } type ActiveTab = 'gen' | '' | 'new' | 'like' /** ========================= * Entry page * ========================= */ export default function Sync() { const { user, isAuthenticated, signOut } = useAuth() const { balance, load: loadBalance } = useUserBalance() 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 isDeleted?: boolean } const transformTemplateToMediaItem = (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 || '', poster: isVideo ? template.coverImageUrl : undefined, likeCount: template.likeCount || 0, price: template.price || 2, } } /** ================= 统一请求函数 ================= */ 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') { 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) } setAllItems((prev) => (mode === 'loadMore' ? [...prev, ...newItems] : newItems)) } 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 = (tab: ActiveTab) => { queryRef.current = { ...queryRef.current, tab, page: 1, } setHasMore(true) firstLoadDoneRef.current = false fetchList('init') } /** ================= 搜索 ================= */ const handleSearch = (text: string) => { queryRef.current = { ...queryRef.current, search: text, page: 1, } setHasMore(true) firstLoadDoneRef.current = false fetchList('refresh') } /** ================= 刷新 / 加载更多 ================= */ const handleRefresh = () => { queryRef.current.page = 1 setHasMore(true) firstLoadDoneRef.current = false fetchList('refresh') } const handleLoadMore = () => { // 防止重复请求或首次渲染时误触发 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-------------') } /** ================= selectedItem 修正 ================= */ useEffect(() => { if (!allItems.length) { setSelectedItem(null) return } if (!allItems.find((i) => i.id === selectedItem?.id)) { setSelectedItem(allItems[0]) } }, [allItems, selectedItem?.id]) /** ================= balance ================= */ useEffect(() => { loadBalance() fetchList('init') }, []) /** ================= 快速生成 ================= */ const handleQuickGen = async () => { if (!isAuthenticated) { Toast.show({ title: '请先登录' }) return } if (!selectedItem) { Toast.show({ title: '请先选择一个模板' }) return } if (balance < selectedItem.price) { Toast.show({ title: '余额不足,请充值' }) return } Toast.showModal( 生成将消耗 {selectedItem.price} Goo } onCancel={Toast.hideModal} onConfirm={async () => { Toast.hideModal() const { generationId, error } = await runTemplate({ templateId: selectedItem.id, data: {}, originalUrl: selectedItem.url, }) if (generationId && user?.id) loadBalance() else Toast.show({ title: error?.message || '生成失败' }) }} />, ) } const renderListEmpty = () => { if (activeTab === 'like' && !isAuthenticated) { return ( 查看您收藏的模板 登录后即可查看和管理收藏 router.push('/auth')} > 立即登录 ) } } /** ================= UI ================= */ return ( router.push('/pointList')} onOpenSearch={() => setIsSearchOpen(true)} /> } selectedItem={selectedItem} /> { setActiveTab(tab) changeTab(tab) }} /> ) : null } numColumns={3} onEndReached={handleLoadMore} drawDistance={1200} onEndReachedThreshold={0.3} refreshControl={} renderItem={({ item }) => ( setSelectedItem(item)} /> )} 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 = (text: string) => { setSearchText(text) onChange && onChange(text) if (timerRef.current) { clearTimeout(timerRef.current) } timerRef.current = setTimeout(() => { onSearch(text) }, 2000) } useEffect(() => { // 删除定时器 return () => { if (timerRef.current) { clearTimeout(timerRef.current) } } }, []) const handleClose = () => { setSearchText('') onSearch('') onClose && onClose() } if (!isOpen) return null return ( ) }) type GooActionsProps = { gooPoints: number onAddGoo: () => void onOpenSearch: () => void } const GooActions = memo(function GooActions({ gooPoints, onAddGoo, onOpenSearch }) { return ( {gooPoints} Goo ) }) type HeroCircleProps = { selectedItem: MediaItem | null onQuickGen: () => void rightSlot: React.ReactNode } const HeroCircle = memo(function HeroCircle({ selectedItem, onQuickGen, rightSlot }) { if (!selectedItem) { return ( 加载中... {rightSlot} ) } return ( {selectedItem.type === 'video' ? ( ) : ( )} {selectedItem?.id} GET 同款 {rightSlot} ) }) 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 } const GridItem = memo(function GridItem({ item, isSelected, itemWidth, onSelect }) { // console.log('item-------------', item); return ( {isSelected && } {item.id} {item.id} {item.likeCount} ) })