170 lines
13 KiB
TypeScript
170 lines
13 KiB
TypeScript
'use client'
|
||
|
||
import { memo, useCallback } from 'react'
|
||
|
||
export interface CategoryItem {
|
||
id: string
|
||
name: string
|
||
count: number
|
||
}
|
||
|
||
export interface MonthItem {
|
||
value: string
|
||
label: string
|
||
count: number
|
||
}
|
||
|
||
export interface FacetTotals {
|
||
materialType: number
|
||
category: number
|
||
scene: number
|
||
platform: number
|
||
productCategory: number
|
||
month: number
|
||
}
|
||
|
||
interface FilterPanelProps {
|
||
selectedMaterialType: string
|
||
setSelectedMaterialType: (value: string) => void
|
||
selectedCategory: string
|
||
setSelectedCategory: (value: string) => void
|
||
selectedScene: string
|
||
setSelectedScene: (value: string) => void
|
||
selectedPlatform: string
|
||
setSelectedPlatform: (value: string) => void
|
||
selectedProductCategory: string
|
||
setSelectedProductCategory: (value: string) => void
|
||
selectedMonth: string
|
||
setSelectedMonth: (value: string) => void
|
||
sortBy: string
|
||
setSortBy: (value: string) => void
|
||
sortOrder: 'desc' | 'asc'
|
||
setSortOrder: (value: 'desc' | 'asc') => void
|
||
materialTypes: { value: string; label: string; count?: number }[]
|
||
categories: CategoryItem[]
|
||
scenes: CategoryItem[]
|
||
platforms: CategoryItem[]
|
||
productCategories: CategoryItem[]
|
||
months: MonthItem[]
|
||
totalMaterialsCount: number
|
||
materialsCount: number
|
||
facetTotals?: FacetTotals
|
||
isFilterCollapsed: boolean
|
||
setIsFilterCollapsed: (value: boolean) => void
|
||
themeColor: string
|
||
}
|
||
|
||
function FilterPanel(props: FilterPanelProps) {
|
||
const {
|
||
selectedMaterialType, setSelectedMaterialType,
|
||
selectedCategory, setSelectedCategory,
|
||
selectedScene, setSelectedScene,
|
||
selectedPlatform, setSelectedPlatform,
|
||
selectedProductCategory, setSelectedProductCategory,
|
||
selectedMonth, setSelectedMonth,
|
||
sortBy, setSortBy, sortOrder, setSortOrder,
|
||
materialTypes, categories, scenes, platforms, productCategories, months,
|
||
totalMaterialsCount, materialsCount, facetTotals,
|
||
isFilterCollapsed, setIsFilterCollapsed, themeColor,
|
||
} = props
|
||
|
||
const handleSortChange = useCallback((newSortBy: string) => {
|
||
if (sortBy === newSortBy) {
|
||
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc')
|
||
} else {
|
||
setSortBy(newSortBy)
|
||
setSortOrder('desc')
|
||
}
|
||
}, [sortBy, sortOrder, setSortBy, setSortOrder])
|
||
|
||
const totals = {
|
||
materialType: facetTotals?.materialType ?? totalMaterialsCount,
|
||
category: facetTotals?.category ?? totalMaterialsCount,
|
||
scene: facetTotals?.scene ?? totalMaterialsCount,
|
||
platform: facetTotals?.platform ?? totalMaterialsCount,
|
||
productCategory: facetTotals?.productCategory ?? totalMaterialsCount,
|
||
month: facetTotals?.month ?? totalMaterialsCount,
|
||
}
|
||
const activeFilterCount = [selectedMaterialType, selectedCategory, selectedScene, selectedPlatform, selectedProductCategory, selectedMonth].filter(Boolean).length
|
||
const btnBase = 'px-4 py-2 rounded-full text-sm font-medium transition-all'
|
||
const btnActive = 'text-white'
|
||
const btnInactive = 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||
|
||
return (
|
||
<>
|
||
<div className="mb-4">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">类型分页</h2>
|
||
{selectedMaterialType && <span className="text-xs text-gray-500 dark:text-gray-400">{materialsCount} 个素材</span>}
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => setSelectedMaterialType('')} className={`${btnBase} ${selectedMaterialType === '' ? btnActive : btnInactive}`} style={selectedMaterialType === '' ? { backgroundColor: themeColor } : {}}>全部<span className={`ml-1.5 text-xs ${selectedMaterialType === '' ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({totals.materialType})</span></button>
|
||
{materialTypes.map((type) => (
|
||
<button key={type.value} onClick={() => setSelectedMaterialType(type.value === selectedMaterialType ? '' : type.value)} className={`${btnBase} ${selectedMaterialType === type.value ? btnActive : btnInactive}`} style={selectedMaterialType === type.value ? { backgroundColor: themeColor } : {}}>{type.label}<span className={`ml-1.5 text-xs ${selectedMaterialType === type.value ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({type.count || 0})</span></button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="mb-4 border-t border-gray-200 dark:border-gray-700 pt-4">
|
||
<button onClick={() => setIsFilterCollapsed(!isFilterCollapsed)} className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">
|
||
<svg className={`w-4 h-4 transition-transform duration-200 ${isFilterCollapsed ? '' : 'rotate-180'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||
{isFilterCollapsed ? '展开更多筛选' : '收起筛选'}
|
||
{isFilterCollapsed && activeFilterCount > 0 && <span className="ml-1 px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded text-xs">{activeFilterCount}</span>}
|
||
</button>
|
||
</div>
|
||
<div className={`overflow-hidden transition-all duration-300 ${isFilterCollapsed ? 'max-h-0 opacity-0' : 'max-h-[2000px] opacity-100'}`}>
|
||
<div className="mb-4">
|
||
<div className="flex items-center justify-between mb-3"><h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">使用店铺</h2>{selectedCategory && <span className="text-xs text-gray-500 dark:text-gray-400">{materialsCount} 个素材</span>}</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => setSelectedCategory('')} className={`${btnBase} ${selectedCategory === '' ? btnActive : btnInactive}`} style={selectedCategory === '' ? { backgroundColor: themeColor } : {}}>全部<span className={`ml-1.5 text-xs ${selectedCategory === '' ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({totals.category})</span></button>
|
||
{categories.map((c) => <button key={c.id} onClick={() => setSelectedCategory(c.name === selectedCategory ? '' : c.name)} className={`${btnBase} ${selectedCategory === c.name ? btnActive : btnInactive}`} style={selectedCategory === c.name ? { backgroundColor: themeColor } : {}}>{c.name}<span className={`ml-1.5 text-xs ${selectedCategory === c.name ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({c.count || 0})</span></button>)}
|
||
</div>
|
||
</div>
|
||
<div className="mb-4">
|
||
<div className="flex items-center justify-between mb-3"><h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">场景类别</h2>{selectedScene && <span className="text-xs text-gray-500 dark:text-gray-400">{materialsCount} 个素材</span>}</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => setSelectedScene('')} className={`${btnBase} ${selectedScene === '' ? btnActive : btnInactive}`} style={selectedScene === '' ? { backgroundColor: themeColor } : {}}>全部<span className={`ml-1.5 text-xs ${selectedScene === '' ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({totals.scene})</span></button>
|
||
{scenes.map((s) => <button key={s.id} onClick={() => setSelectedScene(s.name === selectedScene ? '' : s.name)} className={`${btnBase} ${selectedScene === s.name ? btnActive : btnInactive}`} style={selectedScene === s.name ? { backgroundColor: themeColor } : {}}>{s.name}<span className={`ml-1.5 text-xs ${selectedScene === s.name ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({s.count || 0})</span></button>)}
|
||
</div>
|
||
</div>
|
||
{platforms.length > 0 && <div className="mb-4">
|
||
<div className="flex items-center justify-between mb-3"><h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">平台分类</h2>{selectedPlatform && <span className="text-xs text-gray-500 dark:text-gray-400">{materialsCount} 个素材</span>}</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => setSelectedPlatform('')} className={`${btnBase} ${selectedPlatform === '' ? btnActive : btnInactive}`} style={selectedPlatform === '' ? { backgroundColor: themeColor } : {}}>全部<span className={`ml-1.5 text-xs ${selectedPlatform === '' ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({totals.platform})</span></button>
|
||
{platforms.map((p) => <button key={p.id} onClick={() => setSelectedPlatform(p.name === selectedPlatform ? '' : p.name)} className={`${btnBase} ${selectedPlatform === p.name ? btnActive : btnInactive}`} style={selectedPlatform === p.name ? { backgroundColor: themeColor } : {}}>{p.name}<span className={`ml-1.5 text-xs ${selectedPlatform === p.name ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({p.count || 0})</span></button>)}
|
||
</div>
|
||
</div>}
|
||
{productCategories.length > 0 && <div className="mb-4">
|
||
<div className="flex items-center justify-between mb-3"><h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">产品类别</h2>{selectedProductCategory && <span className="text-xs text-gray-500 dark:text-gray-400">{materialsCount} 个素材</span>}</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => setSelectedProductCategory('')} className={`${btnBase} ${selectedProductCategory === '' ? btnActive : btnInactive}`} style={selectedProductCategory === '' ? { backgroundColor: themeColor } : {}}>全部<span className={`ml-1.5 text-xs ${selectedProductCategory === '' ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({totals.productCategory})</span></button>
|
||
{productCategories.map((c) => <button key={c.id} onClick={() => setSelectedProductCategory(c.name === selectedProductCategory ? '' : c.name)} className={`${btnBase} ${selectedProductCategory === c.name ? btnActive : btnInactive}`} style={selectedProductCategory === c.name ? { backgroundColor: themeColor } : {}}>{c.name}<span className={`ml-1.5 text-xs ${selectedProductCategory === c.name ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({c.count || 0})</span></button>)}
|
||
</div>
|
||
</div>}
|
||
<div className="mb-4">
|
||
<div className="flex items-center justify-between mb-3"><h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">上传月份</h2></div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => setSelectedMonth('')} className={`${btnBase} ${selectedMonth === '' ? btnActive : btnInactive}`} style={selectedMonth === '' ? { backgroundColor: themeColor } : {}}>全部<span className={`ml-1.5 text-xs ${selectedMonth === '' ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({totals.month})</span></button>
|
||
{months.map((m) => <button key={m.value} onClick={() => setSelectedMonth(m.value === selectedMonth ? '' : m.value)} className={`${btnBase} ${selectedMonth === m.value ? btnActive : btnInactive}`} style={selectedMonth === m.value ? { backgroundColor: themeColor } : {}}>{m.label}<span className={`ml-1.5 text-xs ${selectedMonth === m.value ? 'text-gray-300' : 'text-gray-500 dark:text-gray-400'}`}>({m.count})</span></button>)}
|
||
</div>
|
||
</div>
|
||
<div className="mb-4">
|
||
<div className="flex items-center justify-between mb-3"><h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">排序方式</h2></div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => handleSortChange('createdAt')} className={`${btnBase} flex items-center gap-1 ${sortBy === 'createdAt' ? btnActive : btnInactive}`} style={sortBy === 'createdAt' ? { backgroundColor: themeColor } : {}}>
|
||
{sortBy === 'createdAt' && sortOrder === 'asc' ? '最早上传' : '最新上传'}
|
||
{sortBy === 'createdAt' && <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={sortOrder === 'desc' ? 'M19 9l-7 7-7-7' : 'M5 15l7-7 7 7'} /></svg>}
|
||
</button>
|
||
<button onClick={() => handleSortChange('downloadCount')} className={`${btnBase} flex items-center gap-1 ${sortBy === 'downloadCount' ? btnActive : btnInactive}`} style={sortBy === 'downloadCount' ? { backgroundColor: themeColor } : {}}>
|
||
{sortBy === 'downloadCount' && sortOrder === 'asc' ? '下载最少' : '下载最多'}
|
||
{sortBy === 'downloadCount' && <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={sortOrder === 'desc' ? 'M19 9l-7 7-7-7' : 'M5 15l7-7 7 7'} /></svg>}
|
||
</button>
|
||
</div>
|
||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">💡 提示:可以组合使用多个筛选条件,找到更精确的素材</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default memo(FilterPanel)
|