338 lines
11 KiB
TypeScript
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,
|
|
}
|
|
}
|