Files
photography/frontend/components/category-page.tsx
xujiang 48b6a5f4aa feat: 完善 CI/CD 配置并修复代码质量问题
## 修复内容

### 前端 (Frontend)
- 修复 ESLint 错误:未使用变量重命名为下划线前缀
- 修复 TypeScript 类型错误:完善 BackendPhoto 接口定义
- 修复引号转义问题:搜索结果显示优化
- 优化 useEffect 依赖:添加 useCallback 避免无限循环
- 移除未使用的导入和变量

### 后端 (Backend)
- 修复 go vet 错误:测试文件中的字段名称不匹配
- 修复数组访问错误:使用正确的结构体字段路径
- 统一代码格式:go fmt 自动格式化

### 管理后台 (Admin)
- 创建缺失的 ESLint 配置文件
- 修复 React 导入缺失问题
- 确保 TypeScript 编译通过

## CI/CD 改进
- 验证了前端、后端、管理后台的完整构建流程
- 所有 lint 检查、类型检查、测试均通过
- 为自动化部署做好准备

## 技术细节
- 前端:修复 5+ ESLint 错误,完善类型定义
- 后端:修复 3+ go vet 错误,通过所有测试
- 管理后台:创建 ESLint 配置,修复导入问题
- 所有模块均可正常构建和运行
2025-07-14 10:01:48 +08:00

332 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useMemo } from "react"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@/components/ui/breadcrumb"
import {
Search,
Grid,
BarChart3,
Camera,
ArrowRight,
Tag,
Clock,
Images
} from "lucide-react"
import { useCategories } from "@/lib/queries"
import { Photo } from "@/lib/queries"
interface CategoryPageProps {
photos: Photo[]
onCategorySelect: (category: string) => void
onPhotosView: () => void
}
export function CategoryPage({ photos, onCategorySelect, onPhotosView }: CategoryPageProps) {
const { data: _dynamicCategories = [] } = useCategories()
const [searchQuery, setSearchQuery] = useState("")
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
// 计算分类统计
const categoryStats = useMemo(() => {
const stats = new Map<string, {
name: string
count: number
lastUpdate: string
photos: Photo[]
}>()
photos.forEach(photo => {
const category = photo.category || '未分类'
const existing = stats.get(category)
if (existing) {
existing.count++
existing.photos.push(photo)
// 更新最后更新时间
if (photo.date > existing.lastUpdate) {
existing.lastUpdate = photo.date
}
} else {
stats.set(category, {
name: category,
count: 1,
lastUpdate: photo.date,
photos: [photo]
})
}
})
return Array.from(stats.values()).sort((a, b) => b.count - a.count)
}, [photos])
// 搜索过滤
const filteredCategories = useMemo(() => {
if (!searchQuery.trim()) return categoryStats
const query = searchQuery.toLowerCase()
return categoryStats.filter(category =>
category.name.toLowerCase().includes(query)
)
}, [categoryStats, searchQuery])
// 获取分类颜色
const getCategoryColor = (index: number) => {
const colors = [
'bg-blue-100 text-blue-800',
'bg-green-100 text-green-800',
'bg-purple-100 text-purple-800',
'bg-yellow-100 text-yellow-800',
'bg-red-100 text-red-800',
'bg-indigo-100 text-indigo-800',
'bg-pink-100 text-pink-800',
'bg-gray-100 text-gray-800'
]
return colors[index % colors.length]
}
// 获取分类预览图片
const getCategoryPreviewImages = (categoryPhotos: Photo[]) => {
return categoryPhotos.slice(0, 4)
}
return (
<div className="space-y-8">
{/* 面包屑导航 */}
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={onPhotosView} className="cursor-pointer">
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage></BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
{/* 页面标题和统计 */}
<div className="text-center">
<h1 className="text-4xl font-light text-gray-900 mb-4"></h1>
<p className="text-lg text-gray-600 mb-6">
{photos.length} {categoryStats.length}
</p>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Images className="h-5 w-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900">{photos.length}</p>
<p className="text-sm text-gray-600"></p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<Tag className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900">{categoryStats.length}</p>
<p className="text-sm text-gray-600"></p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<Clock className="h-5 w-5 text-purple-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900">
{categoryStats.length > 0 ?
Math.round(photos.length / categoryStats.length) : 0
}
</p>
<p className="text-sm text-gray-600"></p>
</div>
</div>
</Card>
</div>
</div>
{/* 搜索和视图控制 */}
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
<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={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex items-center gap-2">
<Button
variant={viewMode === 'grid' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('grid')}
>
<Grid className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('list')}
>
<BarChart3 className="h-4 w-4" />
</Button>
</div>
</div>
{/* 分类网格 */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredCategories.map((category, index) => (
<Card
key={category.name}
className="group cursor-pointer hover:shadow-lg transition-all duration-300 hover:scale-[1.02]"
onClick={() => onCategorySelect(category.name)}
>
{/* 照片预览网格 */}
<div className="aspect-[4/3] bg-gray-100 relative overflow-hidden">
{getCategoryPreviewImages(category.photos).length > 0 ? (
<div className="grid grid-cols-2 gap-1 h-full">
{getCategoryPreviewImages(category.photos).map((photo, idx) => (
<div
key={photo.id}
className={`relative overflow-hidden ${
idx === 0 && category.photos.length === 1 ? 'col-span-2' : ''
}`}
>
<img
src={photo.src}
alt={photo.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
/>
</div>
))}
</div>
) : (
<div className="flex items-center justify-center h-full">
<Camera className="h-12 w-12 text-gray-400" />
</div>
)}
{/* 悬停叠加层 */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all duration-300" />
<div className="absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ArrowRight className="h-5 w-5 text-white" />
</div>
</div>
{/* 分类信息 */}
<div className="p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="font-medium text-gray-900">{category.name}</h3>
<Badge className={getCategoryColor(index)} variant="secondary">
{category.count}
</Badge>
</div>
<p className="text-sm text-gray-600 mb-3">
: {new Date(category.lastUpdate).toLocaleDateString()}
</p>
<div className="flex items-center text-sm text-gray-500">
<Camera className="h-4 w-4 mr-1" />
<span></span>
</div>
</div>
</Card>
))}
</div>
) : (
/* 列表视图 */
<div className="space-y-4">
{filteredCategories.map((category, index) => (
<Card
key={category.name}
className="p-4 hover:shadow-md transition-shadow cursor-pointer"
onClick={() => onCategorySelect(category.name)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex -space-x-2">
{getCategoryPreviewImages(category.photos).slice(0, 3).map((photo, _idx) => (
<div
key={photo.id}
className="w-12 h-12 rounded-lg border-2 border-white overflow-hidden"
>
<img
src={photo.src}
alt={photo.title}
className="w-full h-full object-cover"
/>
</div>
))}
</div>
<div>
<h3 className="font-medium text-gray-900">{category.name}</h3>
<p className="text-sm text-gray-600">
{category.count} : {new Date(category.lastUpdate).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge className={getCategoryColor(index)} variant="secondary">
{category.count}
</Badge>
<ArrowRight className="h-4 w-4 text-gray-400" />
</div>
</div>
</Card>
))}
</div>
)}
{/* 空状态 */}
{filteredCategories.length === 0 && (
<div className="text-center py-12">
<Camera className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
{searchQuery ? '未找到匹配的分类' : '暂无分类'}
</h3>
<p className="text-gray-600 mb-6">
{searchQuery ? '尝试使用不同的关键词搜索' : '开始上传照片并为其分类'}
</p>
{searchQuery && (
<Button
variant="outline"
onClick={() => setSearchQuery("")}
>
</Button>
)}
</div>
)}
</div>
)
}