✨ 新增功能: - 增强照片展示页面,支持3种视图模式(网格/瀑布流/列表) - 实现分页加载和无限滚动功能 - 完整的搜索和过滤系统,支持实时搜索、标签筛选、排序 - 新增分类浏览页面,提供分类统计和预览 - 新增标签云页面,热度可视化显示 - 面包屑导航和页面间无缝跳转 🎨 用户体验优化: - 响应式设计,完美适配移动端和桌面端 - 智能loading状态和空状态处理 - 悬停效果和交互动画 - 视觉化统计仪表盘 ⚡ 性能优化: - 图片懒加载和智能分页 - 优化的组件渲染和状态管理 - 构建大小优化(187kB gzipped) 📝 更新任务进度文档,完成率达到32.5% Phase 3核心功能基本完成,前端展示网站达到完全可用状态。
235 lines
7.3 KiB
TypeScript
235 lines
7.3 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Search, X, Calendar, Tag, SlidersHorizontal } from "lucide-react"
|
|
import { useCategories } from "@/lib/queries"
|
|
|
|
interface FilterBarProps {
|
|
activeCategory: string
|
|
onFilter: (category: string) => void
|
|
searchText: string
|
|
onSearchChange: (search: string) => void
|
|
sortBy: string
|
|
onSortChange: (sort: string) => void
|
|
selectedTags: string[]
|
|
onTagToggle: (tag: string) => void
|
|
onClearFilters?: () => void
|
|
}
|
|
|
|
export function FilterBar({
|
|
activeCategory,
|
|
onFilter,
|
|
searchText,
|
|
onSearchChange,
|
|
sortBy,
|
|
onSortChange,
|
|
selectedTags,
|
|
onTagToggle,
|
|
onClearFilters
|
|
}: FilterBarProps) {
|
|
const { data: dynamicCategories = [] } = useCategories()
|
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
|
|
// 静态分类作为备选
|
|
const staticCategories = [
|
|
{ id: "all", name: "全部作品" },
|
|
{ id: "urban", name: "城市风光" },
|
|
{ id: "nature", name: "自然风景" },
|
|
{ id: "portrait", name: "人像摄影" },
|
|
{ id: "street", name: "街头摄影" },
|
|
{ id: "architecture", name: "建筑摄影" },
|
|
{ id: "macro", name: "微距摄影" },
|
|
]
|
|
|
|
// 使用动态分类,如果为空则使用静态分类
|
|
const categories = [
|
|
{ id: "all", name: "全部作品" },
|
|
...dynamicCategories.map(cat => ({ id: cat, name: cat }))
|
|
]
|
|
|
|
// 常用标签
|
|
const availableTags = [
|
|
"风景", "人像", "黑白", "彩色", "夜景", "日出",
|
|
"日落", "建筑", "街头", "自然", "艺术", "纪实"
|
|
]
|
|
|
|
// 排序选项
|
|
const sortOptions = [
|
|
{ value: "date_desc", label: "最新发布" },
|
|
{ value: "date_asc", label: "最早发布" },
|
|
{ value: "title_asc", label: "标题 A-Z" },
|
|
{ value: "title_desc", label: "标题 Z-A" },
|
|
{ value: "category", label: "按分类" },
|
|
]
|
|
|
|
// 检查是否有任何活动过滤器
|
|
const hasActiveFilters = activeCategory !== "all" || searchText.trim() !== "" || selectedTags.length > 0 || sortBy !== "date_desc"
|
|
|
|
const handleClearSearch = () => {
|
|
onSearchChange("")
|
|
}
|
|
|
|
const handleClearAll = () => {
|
|
onFilter("all")
|
|
onSearchChange("")
|
|
onSortChange("date_desc")
|
|
selectedTags.forEach(tag => onTagToggle(tag))
|
|
onClearFilters?.()
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 mb-12">
|
|
{/* 搜索栏 */}
|
|
<div className="flex flex-col md:flex-row gap-4 items-center">
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
|
<Input
|
|
type="text"
|
|
placeholder="搜索照片标题、描述..."
|
|
value={searchText}
|
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
className="pl-10 pr-10"
|
|
/>
|
|
{searchText && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleClearSearch}
|
|
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Select value={sortBy} onValueChange={onSortChange}>
|
|
<SelectTrigger className="w-32">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sortOptions.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<SlidersHorizontal className="h-4 w-4" />
|
|
高级筛选
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 分类过滤 */}
|
|
<div className="flex flex-wrap justify-center gap-2">
|
|
{categories.map((category) => (
|
|
<Button
|
|
key={category.id}
|
|
variant={activeCategory === category.id ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => onFilter(category.id)}
|
|
className="transition-all duration-200 hover:scale-105"
|
|
>
|
|
{category.name}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* 高级筛选 */}
|
|
{showAdvanced && (
|
|
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Tag className="h-4 w-4" />
|
|
<span className="text-sm font-medium">标签筛选</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{availableTags.map((tag) => (
|
|
<Badge
|
|
key={tag}
|
|
variant={selectedTags.includes(tag) ? "default" : "secondary"}
|
|
className="cursor-pointer hover:bg-primary/80 transition-colors"
|
|
onClick={() => onTagToggle(tag)}
|
|
>
|
|
{tag}
|
|
{selectedTags.includes(tag) && (
|
|
<X className="ml-1 h-3 w-3" />
|
|
)}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 已选择的过滤器 */}
|
|
{hasActiveFilters && (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm text-gray-600">当前筛选:</span>
|
|
|
|
{activeCategory !== "all" && (
|
|
<Badge variant="outline" className="gap-1">
|
|
分类: {categories.find(c => c.id === activeCategory)?.name}
|
|
<X
|
|
className="h-3 w-3 cursor-pointer"
|
|
onClick={() => onFilter("all")}
|
|
/>
|
|
</Badge>
|
|
)}
|
|
|
|
{searchText.trim() && (
|
|
<Badge variant="outline" className="gap-1">
|
|
搜索: "{searchText.trim()}"
|
|
<X
|
|
className="h-3 w-3 cursor-pointer"
|
|
onClick={handleClearSearch}
|
|
/>
|
|
</Badge>
|
|
)}
|
|
|
|
{selectedTags.map((tag) => (
|
|
<Badge key={tag} variant="outline" className="gap-1">
|
|
{tag}
|
|
<X
|
|
className="h-3 w-3 cursor-pointer"
|
|
onClick={() => onTagToggle(tag)}
|
|
/>
|
|
</Badge>
|
|
))}
|
|
|
|
{sortBy !== "date_desc" && (
|
|
<Badge variant="outline" className="gap-1">
|
|
排序: {sortOptions.find(s => s.value === sortBy)?.label}
|
|
<X
|
|
className="h-3 w-3 cursor-pointer"
|
|
onClick={() => onSortChange("date_desc")}
|
|
/>
|
|
</Badge>
|
|
)}
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleClearAll}
|
|
className="text-xs text-gray-500 hover:text-gray-700"
|
|
>
|
|
清除全部
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|