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:
xujiang
2025-07-11 11:42:14 +08:00
parent b26a05f089
commit af222afc33
20 changed files with 1760 additions and 258 deletions

View File

@ -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>
)
}