feat: 完成前后端API联调测试并修复配置问题
- 启动后端go-zero API服务 (端口8080) - 修复前端API配置中的端口号 (8888→8080) - 完善前端API状态监控组件 - 创建categoryService服务层 - 更新前端数据查询和转换逻辑 - 完成完整API集成测试,验证所有接口正常工作 - 验证用户认证、分类管理、照片管理等核心功能 - 创建API集成测试脚本 - 更新任务进度文档 测试结果: ✅ 后端健康检查正常 ✅ 用户认证功能正常 (admin/admin123) ✅ 分类API正常 (5个分类) ✅ 照片API正常 (0张照片,数据库为空) ✅ 前后端API连接完全正常 下一步: 实现照片展示页面和搜索过滤功能
This commit is contained in:
@ -8,21 +8,27 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Camera,
|
||||
Plus,
|
||||
Search,
|
||||
|
||||
MoreVertical,
|
||||
Edit,
|
||||
Trash,
|
||||
Eye,
|
||||
Grid,
|
||||
List
|
||||
List,
|
||||
RefreshCw,
|
||||
ImageIcon,
|
||||
Tag,
|
||||
Folder
|
||||
} from 'lucide-react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import { photoService } from '@/services/photoService'
|
||||
import { photoService, Photo } from '@/services/photoService'
|
||||
import { categoryService } from '@/services/categoryService'
|
||||
|
||||
type ViewMode = 'grid' | 'list'
|
||||
@ -51,6 +57,19 @@ export default function Photos() {
|
||||
dateRange: ''
|
||||
})
|
||||
|
||||
// 编辑对话框状态
|
||||
const [editingPhoto, setEditingPhoto] = useState<Photo | null>(null)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [editForm, setEditForm] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'draft' as 'draft' | 'published' | 'archived' | 'processing'
|
||||
})
|
||||
|
||||
// 详情对话框状态
|
||||
const [viewingPhoto, setViewingPhoto] = useState<Photo | null>(null)
|
||||
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
|
||||
|
||||
// 获取照片列表
|
||||
const { data: photosData, isLoading: photosLoading } = useQuery({
|
||||
queryKey: ['photos', { page, ...filters }],
|
||||
@ -111,6 +130,21 @@ export default function Photos() {
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑照片
|
||||
const editPhotoMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number, data: any }) =>
|
||||
photoService.updatePhoto(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||||
setIsEditDialogOpen(false)
|
||||
setEditingPhoto(null)
|
||||
toast.success('照片更新成功')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || '更新失败')
|
||||
}
|
||||
})
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedPhotos(photosData?.photos.map(photo => photo.id) || [])
|
||||
@ -176,6 +210,40 @@ export default function Photos() {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 处理编辑照片
|
||||
const handleEditPhoto = (photo: Photo) => {
|
||||
setEditingPhoto(photo)
|
||||
setEditForm({
|
||||
title: photo.title,
|
||||
description: photo.description,
|
||||
status: photo.status
|
||||
})
|
||||
setIsEditDialogOpen(true)
|
||||
}
|
||||
|
||||
// 处理查看照片详情
|
||||
const handleViewPhoto = (photo: Photo) => {
|
||||
setViewingPhoto(photo)
|
||||
setIsViewDialogOpen(true)
|
||||
}
|
||||
|
||||
// 提交编辑
|
||||
const handleSubmitEdit = () => {
|
||||
if (!editingPhoto) return
|
||||
|
||||
editPhotoMutation.mutate({
|
||||
id: editingPhoto.id,
|
||||
data: editForm
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||||
toast.success('列表已刷新')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* 页面头部 */}
|
||||
@ -228,7 +296,7 @@ export default function Photos() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">全部分类</SelectItem>
|
||||
{categories?.map((category) => (
|
||||
{categories?.data?.categories?.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
@ -356,11 +424,11 @@ export default function Photos() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => navigate(`/photos/${photo.id}`)}>
|
||||
<DropdownMenuItem onClick={() => handleViewPhoto(photo)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看详情
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate(`/photos/${photo.id}/edit`)}>
|
||||
<DropdownMenuItem onClick={() => handleEditPhoto(photo)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
@ -431,11 +499,11 @@ export default function Photos() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => navigate(`/photos/${photo.id}`)}>
|
||||
<DropdownMenuItem onClick={() => handleViewPhoto(photo)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看详情
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate(`/photos/${photo.id}/edit`)}>
|
||||
<DropdownMenuItem onClick={() => handleEditPhoto(photo)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
@ -493,19 +561,178 @@ export default function Photos() {
|
||||
<Card>
|
||||
<CardContent className="py-12">
|
||||
<div className="text-center">
|
||||
<Camera className="h-16 w-16 mx-auto mb-4 text-gray-300" />
|
||||
<h3 className="text-lg font-medium mb-2">暂无照片</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
开始上传您的第一张照片吧
|
||||
<div className="w-32 h-32 mx-auto mb-6 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<ImageIcon className="h-16 w-16 text-gray-300" />
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-2">暂无照片</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
开始上传您的第一张照片,构建精美的作品集
|
||||
</p>
|
||||
<Button onClick={() => navigate('/photos/upload')}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
上传照片
|
||||
</Button>
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button onClick={() => navigate('/photos/upload')}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
上传照片
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleRefresh}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 编辑对话框 */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑照片</DialogTitle>
|
||||
<DialogDescription>
|
||||
修改照片的基本信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label htmlFor="edit-title">标题</Label>
|
||||
<Input
|
||||
id="edit-title"
|
||||
value={editForm.title}
|
||||
onChange={(e) => setEditForm({ ...editForm, title: e.target.value })}
|
||||
placeholder="照片标题"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-description">描述</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
value={editForm.description}
|
||||
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
||||
placeholder="照片描述"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-status">状态</Label>
|
||||
<Select value={editForm.status} onValueChange={(value) => setEditForm({ ...editForm, status: value as any })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">草稿</SelectItem>
|
||||
<SelectItem value="published">已发布</SelectItem>
|
||||
<SelectItem value="archived">已归档</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmitEdit} disabled={editPhotoMutation.isPending}>
|
||||
{editPhotoMutation.isPending ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 详情查看对话框 */}
|
||||
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{viewingPhoto?.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
照片详细信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{viewingPhoto && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<ImageIcon className="h-16 w-16 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">状态</Label>
|
||||
<Badge className={getStatusColor(viewingPhoto.status)}>
|
||||
{getStatusText(viewingPhoto.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium">文件大小</Label>
|
||||
<p className="text-sm text-muted-foreground">{formatFileSize(viewingPhoto.fileSize)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium">创建时间</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{new Date(viewingPhoto.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium">更新时间</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{new Date(viewingPhoto.updatedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewingPhoto.description && (
|
||||
<div>
|
||||
<Label className="text-sm font-medium">描述</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">{viewingPhoto.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewingPhoto.categories && viewingPhoto.categories.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-sm font-medium">分类</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{viewingPhoto.categories.map((category) => (
|
||||
<Badge key={category.id} variant="secondary">
|
||||
<Folder className="h-3 w-3 mr-1" />
|
||||
{category.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewingPhoto.tags && viewingPhoto.tags.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-sm font-medium">标签</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{viewingPhoto.tags.map((tag) => (
|
||||
<Badge key={tag.id} variant="outline">
|
||||
<Tag className="h-3 w-3 mr-1" />
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button onClick={() => viewingPhoto && handleEditPhoto(viewingPhoto)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user