gongxiangp2/hooks/useMaterials.ts
2026-05-15 12:48:36 +08:00

338 lines
11 KiB
TypeScript

/**
* 素材数据获取 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<React.SetStateAction<Material[]>>
setLoading: React.Dispatch<React.SetStateAction<boolean>>
setIsTransitionLoading: React.Dispatch<React.SetStateAction<boolean>>
setDisplayedCount: React.Dispatch<React.SetStateAction<number>>
setCurrentPage: React.Dispatch<React.SetStateAction<number>>
setHasMore: React.Dispatch<React.SetStateAction<boolean>>
setIsLoadingMore: React.Dispatch<React.SetStateAction<boolean>>
setTotalMaterialsCount: React.Dispatch<React.SetStateAction<number>>
setMonths: React.Dispatch<React.SetStateAction<MonthItem[]>>
setCategories: React.Dispatch<React.SetStateAction<CategoryItem[]>>
setScenes: React.Dispatch<React.SetStateAction<CategoryItem[]>>
setPlatforms: React.Dispatch<React.SetStateAction<CategoryItem[]>>
setProductCategories: React.Dispatch<React.SetStateAction<CategoryItem[]>>
setThemeColor: React.Dispatch<React.SetStateAction<string>>
// 当前状态
materials: Material[]
displayedCount: number
currentPage: number
hasMore: boolean
isLoadingMore: boolean
}
interface UseMaterialsReturn {
fetchMaterials: (page?: number, append?: boolean) => Promise<number>
handleLoadMore: () => Promise<void>
fetchCategories: () => Promise<void>
fetchScenes: (categoryName?: string) => Promise<void>
fetchPlatforms: () => Promise<void>
fetchProductCategories: () => Promise<void>
fetchThemeColor: () => Promise<void>
fetchMonthsAndTotal: () => Promise<void>
loadMoreRef: React.RefObject<HTMLDivElement>
fetchMaterialsRef: React.MutableRefObject<((page?: number, append?: boolean) => Promise<number>) | 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<HTMLDivElement>(null)
const fetchMaterialsRef = useRef<((page?: number, append?: boolean) => Promise<number>) | null>(null)
// 获取素材列表
const fetchMaterials = useCallback(
async (page = 1, append = false): Promise<number> => {
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,
}
}