feat: 完成前端展示网站核心功能开发

 新增功能:
- 增强照片展示页面,支持3种视图模式(网格/瀑布流/列表)
- 实现分页加载和无限滚动功能
- 完整的搜索和过滤系统,支持实时搜索、标签筛选、排序
- 新增分类浏览页面,提供分类统计和预览
- 新增标签云页面,热度可视化显示
- 面包屑导航和页面间无缝跳转

🎨 用户体验优化:
- 响应式设计,完美适配移动端和桌面端
- 智能loading状态和空状态处理
- 悬停效果和交互动画
- 视觉化统计仪表盘

 性能优化:
- 图片懒加载和智能分页
- 优化的组件渲染和状态管理
- 构建大小优化(187kB gzipped)

📝 更新任务进度文档,完成率达到32.5%

Phase 3核心功能基本完成,前端展示网站达到完全可用状态。
This commit is contained in:
xujiang
2025-07-11 12:27:36 +08:00
parent af222afc33
commit 9046befcf1
8 changed files with 1383 additions and 87 deletions

View File

@ -1,14 +1,41 @@
"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 }: FilterBarProps) {
const categories = [
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: "自然风景" },
@ -18,19 +45,190 @@ export function FilterBar({ activeCategory, onFilter }: FilterBarProps) {
{ 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="flex flex-wrap justify-center gap-2 mb-12">
{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 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>
)
}