Allow multi-file live clip uploads with shared date/host/platform and per-clip titles, filter materials by live platform and date range, and fix the migration to alter the mapped materials table.
426 lines
22 KiB
TypeScript
426 lines
22 KiB
TypeScript
'use client'
|
||
|
||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } 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
|
||
selectedLivePlatform?: string
|
||
setSelectedLivePlatform?: (value: string) => void
|
||
liveDateFrom?: string
|
||
setLiveDateFrom?: (value: string) => void
|
||
liveDateTo?: string
|
||
setLiveDateTo?: (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
|
||
facetsLoading?: boolean
|
||
}
|
||
|
||
function FilterPanel(props: FilterPanelProps) {
|
||
const {
|
||
selectedMaterialType, setSelectedMaterialType,
|
||
selectedCategory, setSelectedCategory,
|
||
selectedScene, setSelectedScene,
|
||
selectedPlatform, setSelectedPlatform,
|
||
selectedLivePlatform = '', setSelectedLivePlatform,
|
||
liveDateFrom = '', setLiveDateFrom,
|
||
liveDateTo = '', setLiveDateTo,
|
||
selectedProductCategory, setSelectedProductCategory,
|
||
selectedMonth, setSelectedMonth,
|
||
sortBy, setSortBy, sortOrder, setSortOrder,
|
||
materialTypes, categories, scenes, platforms, productCategories, months,
|
||
totalMaterialsCount, materialsCount, facetTotals,
|
||
isFilterCollapsed, setIsFilterCollapsed,
|
||
facetsLoading = false,
|
||
} = 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 tabBase = 'relative shrink-0 whitespace-nowrap pb-3 text-[15px] tracking-[-0.02em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 rounded-sm'
|
||
const tabActive = 'text-[#1a1c1d]'
|
||
const tabInactive = 'text-[#6b7075] hover:text-[#1a1c1d]'
|
||
const chipBase = 'text-[14px] tracking-[-0.02em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 rounded-sm'
|
||
const chipActive = 'text-[#1a1c1d]'
|
||
const chipInactive = 'text-[#6b7075] hover:text-[#1a1c1d]'
|
||
const countClass = 'font-en text-[12px] font-medium tabular-nums tracking-[0.08em] text-[#86898e]'
|
||
const countText = (value: number) => (facetsLoading ? '…' : value.toLocaleString('zh-CN'))
|
||
const rankedTypes = useMemo(() => {
|
||
const hasCounts = materialTypes.some((type) => (type.count || 0) > 0)
|
||
if (!hasCounts) return materialTypes
|
||
return [...materialTypes].sort((left, right) => (right.count || 0) - (left.count || 0))
|
||
}, [materialTypes])
|
||
const rankByCount = useCallback(<T extends { count: number }>(items: T[]) => {
|
||
if (!items.some((item) => (item.count || 0) > 0)) return items
|
||
return [...items].sort((left, right) => (right.count || 0) - (left.count || 0))
|
||
}, [])
|
||
const rankedCategories = useMemo(() => rankByCount(categories), [categories, rankByCount])
|
||
const rankedScenes = useMemo(() => rankByCount(scenes), [scenes, rankByCount])
|
||
const rankedPlatforms = useMemo(() => rankByCount(platforms), [platforms, rankByCount])
|
||
const rankedProductCategories = useMemo(() => rankByCount(productCategories), [productCategories, rankByCount])
|
||
const yearFacets = useMemo(() => {
|
||
const yearMap = new Map<string, number>()
|
||
for (const item of months) {
|
||
const year = item.value.slice(0, 4)
|
||
if (!/^\d{4}$/.test(year)) continue
|
||
yearMap.set(year, (yearMap.get(year) || 0) + item.count)
|
||
}
|
||
return Array.from(yearMap.entries())
|
||
.map(([year, count]) => ({ year, count }))
|
||
.sort((left, right) => right.year.localeCompare(left.year))
|
||
}, [months])
|
||
const focusedYear = selectedMonth.slice(0, 4) || yearFacets[0]?.year || ''
|
||
const monthsInFocusedYear = useMemo(
|
||
() => months.filter((item) => item.value.startsWith(`${focusedYear}-`)).sort((left, right) => right.value.localeCompare(left.value)),
|
||
[months, focusedYear],
|
||
)
|
||
const rowRef = useRef<HTMLDivElement>(null)
|
||
const measureRef = useRef<HTMLDivElement>(null)
|
||
const moreWrapRef = useRef<HTMLDivElement>(null)
|
||
const [fitCount, setFitCount] = useState<number | null>(null)
|
||
const [moreOpen, setMoreOpen] = useState(false)
|
||
|
||
const measureFit = useCallback(() => {
|
||
const row = rowRef.current
|
||
const measure = measureRef.current
|
||
if (!row || !measure) return
|
||
const available = row.clientWidth
|
||
if (available < 8) {
|
||
setFitCount(null)
|
||
return
|
||
}
|
||
const kids = Array.from(measure.children) as HTMLElement[]
|
||
if (kids.length < 2) {
|
||
setFitCount(rankedTypes.length)
|
||
return
|
||
}
|
||
const moreWidth = kids[kids.length - 1].offsetWidth
|
||
const typeEls = kids.slice(1, -1)
|
||
const gap = 32
|
||
let used = kids[0].offsetWidth
|
||
let count = 0
|
||
for (let index = 0; index < typeEls.length; index += 1) {
|
||
const remaining = typeEls.length - (index + 1)
|
||
const extra = remaining > 0 ? gap + moreWidth : 0
|
||
if (used + gap + typeEls[index].offsetWidth + extra <= available) {
|
||
used += gap + typeEls[index].offsetWidth
|
||
count += 1
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
setFitCount(count)
|
||
}, [rankedTypes.length])
|
||
|
||
useLayoutEffect(() => {
|
||
measureFit()
|
||
const row = rowRef.current
|
||
if (!row || typeof ResizeObserver === 'undefined') return
|
||
const observer = new ResizeObserver(() => measureFit())
|
||
observer.observe(row)
|
||
return () => observer.disconnect()
|
||
}, [measureFit, rankedTypes, totals.materialType, facetsLoading])
|
||
|
||
const { visibleTypes, overflowTypes } = useMemo(() => {
|
||
if (fitCount === null || fitCount >= rankedTypes.length) {
|
||
return { visibleTypes: rankedTypes, overflowTypes: [] as typeof rankedTypes }
|
||
}
|
||
let visible = rankedTypes.slice(0, fitCount)
|
||
let overflow = rankedTypes.slice(fitCount)
|
||
if (selectedMaterialType) {
|
||
const hiddenSelected = overflow.find((type) => type.value === selectedMaterialType)
|
||
if (hiddenSelected && visible.length > 0) {
|
||
const displaced = visible[visible.length - 1]
|
||
visible = [...visible.slice(0, -1), hiddenSelected]
|
||
overflow = [displaced, ...overflow.filter((type) => type.value !== selectedMaterialType)]
|
||
}
|
||
}
|
||
return { visibleTypes: visible, overflowTypes: overflow }
|
||
}, [fitCount, rankedTypes, selectedMaterialType])
|
||
|
||
useEffect(() => {
|
||
if (!moreOpen) return
|
||
const onPointer = (event: MouseEvent) => {
|
||
if (!moreWrapRef.current?.contains(event.target as Node)) setMoreOpen(false)
|
||
}
|
||
const onKey = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') setMoreOpen(false)
|
||
}
|
||
document.addEventListener('mousedown', onPointer)
|
||
document.addEventListener('keydown', onKey)
|
||
return () => {
|
||
document.removeEventListener('mousedown', onPointer)
|
||
document.removeEventListener('keydown', onKey)
|
||
}
|
||
}, [moreOpen])
|
||
|
||
useEffect(() => {
|
||
setMoreOpen(false)
|
||
}, [selectedMaterialType])
|
||
|
||
const renderTypeTab = (label: string, count: number, active: boolean, onClick: () => void) => (
|
||
<button onClick={onClick} className={`${tabBase} ${active ? tabActive : tabInactive}`}>
|
||
{label}
|
||
<span className={`ml-2 ${countClass}`}>{countText(count)}</span>
|
||
{active && <span aria-hidden="true" className="absolute inset-x-0 -bottom-px h-px bg-[#1a1c1d]" />}
|
||
</button>
|
||
)
|
||
|
||
const moreActive = overflowTypes.some((type) => type.value === selectedMaterialType)
|
||
|
||
return (
|
||
<>
|
||
<div className="mb-6">
|
||
<div className="flex items-end justify-between gap-6 border-b border-[#e7e8ea]">
|
||
<div ref={rowRef} className="relative min-w-0 flex-1">
|
||
<div className="flex items-end gap-x-8" aria-busy={facetsLoading}>
|
||
{renderTypeTab('全部', totals.materialType, selectedMaterialType === '', () => setSelectedMaterialType(''))}
|
||
{visibleTypes.map((type) => (
|
||
<span key={type.value}>
|
||
{renderTypeTab(type.label, type.count || 0, selectedMaterialType === type.value, () => setSelectedMaterialType(type.value === selectedMaterialType ? '' : type.value))}
|
||
</span>
|
||
))}
|
||
{overflowTypes.length > 0 && (
|
||
<div ref={moreWrapRef} className="relative shrink-0">
|
||
<button
|
||
type="button"
|
||
onClick={() => setMoreOpen((open) => !open)}
|
||
aria-expanded={moreOpen}
|
||
aria-haspopup="listbox"
|
||
className={`${tabBase} ${moreActive || moreOpen ? tabActive : tabInactive}`}
|
||
>
|
||
更多
|
||
<span className={`ml-2 ${countClass}`}>{overflowTypes.length}</span>
|
||
{(moreActive || moreOpen) && <span aria-hidden="true" className="absolute inset-x-0 -bottom-px h-px bg-[#1a1c1d]" />}
|
||
</button>
|
||
{moreOpen && (
|
||
<div role="listbox" className="absolute right-0 top-full z-40 mt-2 max-h-[320px] min-w-[240px] overflow-y-auto border border-[#e7e8ea] bg-white py-1">
|
||
{overflowTypes.map((type) => {
|
||
const active = selectedMaterialType === type.value
|
||
return (
|
||
<button
|
||
key={type.value}
|
||
role="option"
|
||
aria-selected={active}
|
||
onClick={() => setSelectedMaterialType(active ? '' : type.value)}
|
||
className={`flex w-full items-baseline justify-between gap-6 px-4 py-2.5 text-left text-[14px] tracking-[-0.02em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 ${active ? 'text-[#1a1c1d]' : 'text-[#6b7075] hover:bg-[#fafafa] hover:text-[#1a1c1d]'}`}
|
||
>
|
||
<span>{type.label}</span>
|
||
<span className={countClass}>{countText(type.count || 0)}</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{facetsLoading && materialTypes.length === 0 && [0, 1, 2, 3].map((item) => (
|
||
<span key={item} className="mb-2 h-4 w-16 bg-[#eceef0]" aria-hidden="true" />
|
||
))}
|
||
</div>
|
||
<div aria-hidden="true" className="pointer-events-none absolute left-0 top-0 h-0 w-0 overflow-hidden">
|
||
<div ref={measureRef} className="flex w-max items-end gap-x-8 whitespace-nowrap">
|
||
<span className={tabBase}>全部<span className={`ml-2 ${countClass}`}>{countText(totals.materialType)}</span></span>
|
||
{rankedTypes.map((type) => (
|
||
<span key={type.value} className={tabBase}>{type.label}<span className={`ml-2 ${countClass}`}>{countText(type.count || 0)}</span></span>
|
||
))}
|
||
<span className={tabBase}>更多<span className={`ml-2 ${countClass}`}>00</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => setIsFilterCollapsed(!isFilterCollapsed)}
|
||
aria-label={isFilterCollapsed ? '展开更多筛选' : '收起筛选'}
|
||
className="mb-2 shrink-0 whitespace-nowrap bg-[#fafafa] pl-4 text-[13px] tracking-[-0.02em] text-[#6b7075] hover:text-[#1a1c1d] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 rounded-sm"
|
||
>
|
||
{isFilterCollapsed ? '展开更多筛选' : '收起筛选'}
|
||
{isFilterCollapsed && activeFilterCount > 0 && <span className="ml-1.5 font-en text-[11px] text-[#5a66b8]">{activeFilterCount}</span>}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="mb-8 flex flex-wrap items-baseline justify-between gap-3">
|
||
<p className="text-[13px] text-[#86898e]">
|
||
共 <span className="font-en text-[15px] font-medium tabular-nums tracking-[0.06em] text-[#1a1c1d]">{facetsLoading ? '…' : materialsCount.toLocaleString('zh-CN')}</span> 条
|
||
{selectedMaterialType && <span className="sr-only">{facetsLoading ? '计数加载中' : `${materialsCount} 个素材`}</span>}
|
||
</p>
|
||
<div className="flex items-center gap-4 text-[13px]" role="group" aria-labelledby="materials-sort-label">
|
||
<h2 id="materials-sort-label" className="sr-only">排序方式</h2>
|
||
<button onClick={() => handleSortChange('createdAt')} className={`flex items-center gap-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 rounded-sm ${sortBy === 'createdAt' ? 'text-[#1a1c1d]' : 'text-[#86898e] hover:text-[#1a1c1d]'}`}>
|
||
{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={`flex items-center gap-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 rounded-sm ${sortBy === 'downloadCount' ? 'text-[#1a1c1d]' : 'text-[#86898e] hover:text-[#1a1c1d]'}`}>
|
||
{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>
|
||
<div className={`overflow-hidden transition-all duration-300 ${isFilterCollapsed ? 'max-h-0 opacity-0' : 'max-h-[2000px] opacity-100'}`}>
|
||
<div className="mb-6">
|
||
<h2 className="mb-3 text-[13px] text-[#6b7075]">使用店铺</h2>
|
||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||
<button onClick={() => setSelectedCategory('')} className={`${chipBase} ${selectedCategory === '' ? chipActive : chipInactive}`}>全部<span className={`ml-1.5 ${countClass}`}>{countText(totals.category)}</span></button>
|
||
{rankedCategories.map((item) => <button key={item.id} onClick={() => setSelectedCategory(item.name === selectedCategory ? '' : item.name)} className={`${chipBase} ${selectedCategory === item.name ? chipActive : chipInactive}`}>{item.name}<span className={`ml-1.5 ${countClass}`}>{countText(item.count || 0)}</span></button>)}
|
||
</div>
|
||
</div>
|
||
<div className="mb-6">
|
||
<h2 className="mb-3 text-[13px] text-[#6b7075]">场景类别</h2>
|
||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||
<button onClick={() => setSelectedScene('')} className={`${chipBase} ${selectedScene === '' ? chipActive : chipInactive}`}>全部<span className={`ml-1.5 ${countClass}`}>{countText(totals.scene)}</span></button>
|
||
{rankedScenes.map((item) => <button key={item.id} onClick={() => setSelectedScene(item.name === selectedScene ? '' : item.name)} className={`${chipBase} ${selectedScene === item.name ? chipActive : chipInactive}`}>{item.name}<span className={`ml-1.5 ${countClass}`}>{countText(item.count || 0)}</span></button>)}
|
||
</div>
|
||
</div>
|
||
{(rankedPlatforms.length > 0 || facetsLoading) && <div className="mb-6">
|
||
<h2 className="mb-3 text-[13px] text-[#6b7075]">平台分类</h2>
|
||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||
<button onClick={() => setSelectedPlatform('')} className={`${chipBase} ${selectedPlatform === '' ? chipActive : chipInactive}`}>全部<span className={`ml-1.5 ${countClass}`}>{countText(totals.platform)}</span></button>
|
||
{rankedPlatforms.map((item) => <button key={item.id} onClick={() => setSelectedPlatform(item.name === selectedPlatform ? '' : item.name)} className={`${chipBase} ${selectedPlatform === item.name ? chipActive : chipInactive}`}>{item.name}<span className={`ml-1.5 ${countClass}`}>{countText(item.count || 0)}</span></button>)}
|
||
</div>
|
||
</div>}
|
||
{setSelectedLivePlatform && (
|
||
<div className="mb-6">
|
||
<h2 className="mb-3 text-[13px] text-[#6b7075]">直播平台</h2>
|
||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||
{['', '抖音', '快手', '视频号', '淘宝直播', 'B站'].map((name) => (
|
||
<button
|
||
key={name || 'all'}
|
||
type="button"
|
||
onClick={() => setSelectedLivePlatform(name)}
|
||
className={`${chipBase} ${selectedLivePlatform === name ? chipActive : chipInactive}`}
|
||
>
|
||
{name || '全部'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
{(setLiveDateFrom || setLiveDateTo) && (
|
||
<div className="mb-6">
|
||
<h2 className="mb-3 text-[13px] text-[#6b7075]">直播日期</h2>
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<input
|
||
type="date"
|
||
aria-label="直播日期起"
|
||
value={liveDateFrom}
|
||
onChange={(e) => setLiveDateFrom?.(e.target.value)}
|
||
className={`${chipBase} cursor-text`}
|
||
/>
|
||
<span className="text-[12px] text-[#86898e]">至</span>
|
||
<input
|
||
type="date"
|
||
aria-label="直播日期止"
|
||
value={liveDateTo}
|
||
onChange={(e) => setLiveDateTo?.(e.target.value)}
|
||
className={`${chipBase} cursor-text`}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{(rankedProductCategories.length > 0 || facetsLoading) && <div className="mb-6">
|
||
<h2 className="mb-3 text-[13px] text-[#6b7075]">产品类别</h2>
|
||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||
<button onClick={() => setSelectedProductCategory('')} className={`${chipBase} ${selectedProductCategory === '' ? chipActive : chipInactive}`}>全部<span className={`ml-1.5 ${countClass}`}>{countText(totals.productCategory)}</span></button>
|
||
{rankedProductCategories.map((item) => <button key={item.id} onClick={() => setSelectedProductCategory(item.name === selectedProductCategory ? '' : item.name)} className={`${chipBase} ${selectedProductCategory === item.name ? chipActive : chipInactive}`}>{item.name}<span className={`ml-1.5 ${countClass}`}>{countText(item.count || 0)}</span></button>)}
|
||
</div>
|
||
</div>}
|
||
<div className="mb-6">
|
||
<h2 className="mb-3 text-[13px] text-[#6b7075]">上传时间</h2>
|
||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||
<button onClick={() => setSelectedMonth('')} className={`${chipBase} ${selectedMonth === '' ? chipActive : chipInactive}`}>全部<span className={`ml-1.5 ${countClass}`}>{countText(totals.month)}</span></button>
|
||
{yearFacets.map((item) => (
|
||
<button
|
||
key={item.year}
|
||
onClick={() => setSelectedMonth(selectedMonth === item.year ? '' : item.year)}
|
||
className={`${chipBase} ${selectedMonth === item.year || selectedMonth.startsWith(`${item.year}-`) ? chipActive : chipInactive}`}
|
||
>
|
||
{item.year}
|
||
<span className={`ml-1.5 ${countClass}`}>{countText(item.count)}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
{monthsInFocusedYear.length > 0 && (
|
||
<div className="mt-3 flex flex-wrap gap-x-5 gap-y-2">
|
||
{monthsInFocusedYear.map((item) => {
|
||
const monthNumber = Number.parseInt(item.value.slice(5), 10)
|
||
return (
|
||
<button
|
||
key={item.value}
|
||
onClick={() => setSelectedMonth(item.value === selectedMonth ? '' : item.value)}
|
||
className={`${chipBase} ${selectedMonth === item.value ? chipActive : chipInactive}`}
|
||
>
|
||
{monthNumber}月
|
||
<span className={`ml-1.5 ${countClass}`}>{countText(item.count)}</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<p className="mb-6 text-xs text-[#86898e]">选了一个条件后,其它条件只保留还有结果的选项,数量会跟着变。</p>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default memo(FilterPanel)
|