/** * 素材数据获取 Hook * 提取自 app/page.tsx 的素材获取和分页逻辑 */ import { useCallback, useRef, useEffect } from 'react' import type { Material, CategoryItem, MonthItem } from './useHomeState' interface UseMaterialsParams { // 筛选条件 searchKeyword: string selectedCategory: string selectedScene: string selectedMaterialType: string selectedPlatform: string selectedProductCategory: string selectedMonth: string sortBy: string sortOrder: 'desc' | 'asc' pageSize: number // 状态更新函数 setMaterials: React.Dispatch> setLoading: React.Dispatch> setIsTransitionLoading: React.Dispatch> setDisplayedCount: React.Dispatch> setCurrentPage: React.Dispatch> setHasMore: React.Dispatch> setIsLoadingMore: React.Dispatch> setTotalMaterialsCount: React.Dispatch> setMonths: React.Dispatch> setCategories: React.Dispatch> setScenes: React.Dispatch> setPlatforms: React.Dispatch> setProductCategories: React.Dispatch> setThemeColor: React.Dispatch> // 当前状态 materials: Material[] displayedCount: number currentPage: number hasMore: boolean isLoadingMore: boolean } interface UseMaterialsReturn { fetchMaterials: (page?: number, append?: boolean) => Promise handleLoadMore: () => Promise fetchCategories: () => Promise fetchScenes: (categoryName?: string) => Promise fetchPlatforms: () => Promise fetchProductCategories: () => Promise fetchThemeColor: () => Promise fetchMonthsAndTotal: () => Promise loadMoreRef: React.RefObject fetchMaterialsRef: React.MutableRefObject<((page?: number, append?: boolean) => Promise) | null> } export function useMaterials(params: UseMaterialsParams): UseMaterialsReturn { const { searchKeyword, selectedCategory, selectedScene, selectedMaterialType, selectedPlatform, selectedProductCategory, selectedMonth, sortBy, sortOrder, pageSize, setMaterials, setLoading, setIsTransitionLoading, setDisplayedCount, setCurrentPage, setHasMore, setIsLoadingMore, setTotalMaterialsCount, setMonths, setCategories, setScenes, setPlatforms, setProductCategories, setThemeColor, materials, displayedCount, currentPage, hasMore, isLoadingMore, } = params const loadMoreRef = useRef(null) const fetchMaterialsRef = useRef<((page?: number, append?: boolean) => Promise) | null>(null) // 获取素材列表 const fetchMaterials = useCallback( async (page = 1, append = false): Promise => { try { const showFullLoading = !append && page === 1 && materials.length === 0 if (showFullLoading) { setLoading(true) } else if (!append) { setIsTransitionLoading(true) } if (!append) setHasMore(true) const params = new URLSearchParams() if (searchKeyword) params.append('search', searchKeyword) if (selectedCategory) params.append('category', selectedCategory) if (selectedScene) params.append('scene', selectedScene) if (selectedMaterialType) params.append('materialType', selectedMaterialType) if (selectedPlatform) params.append('platform', selectedPlatform) if (selectedProductCategory) params.append('productCategory', selectedProductCategory) if (selectedMonth) params.append('month', selectedMonth) if (sortBy) params.append('sortBy', sortBy) if (sortOrder) params.append('sortOrder', sortOrder) params.append('page', page.toString()) params.append('pageSize', pageSize.toString()) const url = `/api/materials${params.toString() ? '?' + params.toString() : ''}` const response = await fetch(url) if (response.ok) { const data = await response.json() const materialsList = ((data.materials || data) as Material[]) .map((e) => { if (!e.id || typeof e.id !== 'string') return null const trimmedId = e.id.trim() if (!trimmedId || trimmedId.length < 10 || trimmedId.length > 100) return null const previewUrl = e.previewUrl || e.videoCoverUrl || (e.displayImages && e.displayImages.length > 0 ? e.displayImages[0] : '') return { ...e, id: trimmedId, previewUrl } }) .filter((e): e is Material => e !== null) let appendedItems: Material[] = materialsList if (append) { setMaterials((prev) => { const existingIds = new Set(prev.map((item) => item.id)) const deduped = materialsList.filter((item) => !existingIds.has(item.id)) appendedItems = deduped if (deduped.length === 0) return prev return [...prev, ...deduped] }) } else { setMaterials(materialsList) setDisplayedCount(Math.min(24, materialsList.length)) } const appendedCount = append ? appendedItems.length : materialsList.length setCurrentPage(page) const totalPages = data.totalPages || 1 setHasMore(appendedCount > 0 && page < totalPages) setTotalMaterialsCount(data.total ?? data.totalCount ?? materialsList.length) fetchMonthsAndTotal() return appendedCount } } catch (error) { console.error('Error fetching materials:', error) if (!append) { setMaterials([]) setDisplayedCount(0) } } finally { if (!append) { setLoading(false) setIsTransitionLoading(false) } } return 0 }, [materials.length, searchKeyword, selectedCategory, selectedScene, selectedMaterialType, selectedPlatform, selectedProductCategory, selectedMonth, sortBy, sortOrder, pageSize, setMaterials, setLoading, setIsTransitionLoading, setDisplayedCount, setCurrentPage, setHasMore] ) // 更新fetchMaterialsRef useEffect(() => { fetchMaterialsRef.current = fetchMaterials }, [fetchMaterials]) // 加载更多 const handleLoadMore = useCallback(async () => { if (isLoadingMore) return if (displayedCount < materials.length) { setIsLoadingMore(true) setDisplayedCount((prev) => Math.min(prev + 24, materials.length)) setIsLoadingMore(false) return } if (!hasMore) return setIsLoadingMore(true) const appended = await fetchMaterials(currentPage + 1, true) if (appended > 0) { setDisplayedCount((prev) => Math.min(prev + 24, materials.length + appended)) } setIsLoadingMore(false) }, [isLoadingMore, displayedCount, materials.length, hasMore, currentPage, fetchMaterials, setIsLoadingMore, setDisplayedCount]) // IntersectionObserver实现自动懒加载 useEffect(() => { if (!loadMoreRef.current) return if (isLoadingMore) return if (!hasMore && displayedCount >= materials.length) return const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && !isLoadingMore) { handleLoadMore() } }, { threshold: 0.1, rootMargin: '100px' } ) observer.observe(loadMoreRef.current) return () => { observer.disconnect() } }, [handleLoadMore, displayedCount, materials.length, hasMore, isLoadingMore]) // 获取分类列表 const fetchCategories = useCallback(async () => { try { const response = await fetch('/api/categories') if (response.ok) { const data = await response.json() setCategories(data) } } catch (error) { console.error('Error fetching categories:', error) } }, [setCategories]) // 获取场景列表 const fetchScenes = useCallback(async (categoryName?: string) => { try { const url = categoryName ? `/api/scenes?category=${encodeURIComponent(categoryName)}` : '/api/scenes' const response = await fetch(url) if (response.ok) { const data = await response.json() setScenes(data) } } catch (error) { console.error('Error fetching scenes:', error) } }, [setScenes]) // 获取平台列表 const fetchPlatforms = useCallback(async () => { try { const response = await fetch('/api/platforms') if (response.ok) { const data = await response.json() setPlatforms(data) } } catch (error) { console.error('Error fetching platforms:', error) } }, [setPlatforms]) // 获取品类列表 const fetchProductCategories = useCallback(async () => { try { const response = await fetch('/api/product-categories') if (response.ok) { const data = await response.json() setProductCategories(data) } } catch (error) { console.error('Error fetching product categories:', error) } }, [setProductCategories]) // 获取主题颜色 const fetchThemeColor = useCallback(async () => { try { const response = await fetch('/api/system-settings') if (response.ok) { const data = await response.json() setThemeColor(data.headerBgColor || '#1f2937') } } catch (error) { console.error('获取主题颜色失败:', error) } }, [setThemeColor]) // 获取月份统计和总数 const fetchMonthsAndTotal = useCallback(async () => { try { const params = new URLSearchParams() if (searchKeyword) params.append('search', searchKeyword) if (selectedCategory) params.append('category', selectedCategory) if (selectedScene) params.append('scene', selectedScene) if (selectedMaterialType) params.append('materialType', selectedMaterialType) if (selectedPlatform) params.append('platform', selectedPlatform) if (selectedProductCategory) params.append('productCategory', selectedProductCategory) if (selectedMonth) params.append('month', selectedMonth) params.append('pageSize', '1') const response = await fetch(`/api/materials?${params.toString()}`) if (response.ok) { const data = await response.json() setTotalMaterialsCount(data.total || 0) } const statsResponse = await fetch('/api/materials/stats') if (statsResponse.ok) { const statsData = await statsResponse.json() setMonths(statsData.months || []) } } catch (error) { console.error('Error fetching months and total:', error) } }, [searchKeyword, selectedCategory, selectedScene, selectedMaterialType, selectedPlatform, selectedProductCategory, selectedMonth, setTotalMaterialsCount, setMonths]) return { fetchMaterials, handleLoadMore, fetchCategories, fetchScenes, fetchPlatforms, fetchProductCategories, fetchThemeColor, fetchMonthsAndTotal, loadMoreRef, fetchMaterialsRef, } }