## 修复内容 ### 依赖修复 - 安装缺失的 `prettier-plugin-organize-imports` 插件 - 修复 CI/CD 中的 "Cannot find package" 错误 - 更新 package.json 和 bun.lockb ### 代码格式化 - 对所有源文件运行 Prettier 自动格式化 - 统一 import 语句排序和组织 - 修复 49 个文件的代码风格问题 - 确保所有文件符合项目代码规范 ### 格式化改进 - Import 语句自动排序和分组 - 统一缩进和空格规范 - 标准化引号和分号使用 - 优化对象和数组格式 ## 验证结果 ✅ `bun run format` 通过 - 所有文件格式正确 ✅ `prettier-plugin-organize-imports` 正常工作 ✅ CI/CD 格式检查将通过 ## 技术细节 - 添加 prettier-plugin-organize-imports@^4.1.0 - 保持现有 .prettierrc 配置不变 - 格式化涉及 TS/TSX/JS/JSX/JSON/CSS/MD 文件 - 代码功能完全不受影响,仅调整格式
773 lines
27 KiB
TypeScript
773 lines
27 KiB
TypeScript
import { Badge } from '@/components/ui/badge'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Card, CardContent } from '@/components/ui/card'
|
||
import { Checkbox } from '@/components/ui/checkbox'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog'
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
} from '@/components/ui/dropdown-menu'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Label } from '@/components/ui/label'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select'
|
||
import { Skeleton } from '@/components/ui/skeleton'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { categoryService } from '@/services/categoryService'
|
||
import { Photo, photoService } from '@/services/photoService'
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||
import {
|
||
Camera,
|
||
Edit,
|
||
Eye,
|
||
Folder,
|
||
Grid,
|
||
ImageIcon,
|
||
List,
|
||
MoreVertical,
|
||
Plus,
|
||
RefreshCw,
|
||
Search,
|
||
Tag,
|
||
Trash,
|
||
} from 'lucide-react'
|
||
import { useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { toast } from 'sonner'
|
||
|
||
type ViewMode = 'grid' | 'list'
|
||
|
||
interface PhotoFilters {
|
||
search: string
|
||
status: string
|
||
categoryId: string
|
||
tagId: string
|
||
dateRange: string
|
||
}
|
||
|
||
export default function Photos() {
|
||
const navigate = useNavigate()
|
||
const queryClient = useQueryClient()
|
||
|
||
// 状态管理
|
||
const [viewMode, setViewMode] = useState<ViewMode>('grid')
|
||
const [selectedPhotos, setSelectedPhotos] = useState<number[]>([])
|
||
const [page, setPage] = useState(1)
|
||
const [filters, setFilters] = useState<PhotoFilters>({
|
||
search: '',
|
||
status: '',
|
||
categoryId: '',
|
||
tagId: '',
|
||
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 }],
|
||
queryFn: () =>
|
||
photoService.getPhotos({
|
||
page,
|
||
limit: 20,
|
||
search: filters.search || undefined,
|
||
status: filters.status || undefined,
|
||
category_id: filters.categoryId ? parseInt(filters.categoryId) : undefined,
|
||
sort_by: 'created_at',
|
||
sort_order: 'desc',
|
||
}),
|
||
})
|
||
|
||
// 获取分类列表
|
||
const { data: categories } = useQuery({
|
||
queryKey: ['categories-all'],
|
||
queryFn: () => categoryService.getCategories(),
|
||
})
|
||
|
||
// 删除照片
|
||
const deletePhotoMutation = useMutation({
|
||
mutationFn: photoService.deletePhoto,
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||
toast.success('照片删除成功')
|
||
},
|
||
onError: (error: any) => {
|
||
toast.error(error?.message || '删除失败')
|
||
},
|
||
})
|
||
|
||
// 批量删除照片
|
||
const batchDeleteMutation = useMutation({
|
||
mutationFn: photoService.batchDelete,
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||
setSelectedPhotos([])
|
||
toast.success('批量删除成功')
|
||
},
|
||
onError: (error: any) => {
|
||
toast.error(error?.message || '批量删除失败')
|
||
},
|
||
})
|
||
|
||
// 批量更新状态
|
||
const batchUpdateMutation = useMutation({
|
||
mutationFn: ({ ids, status }: { ids: number[]; status: string }) =>
|
||
photoService.batchUpdate(ids, { status }),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||
setSelectedPhotos([])
|
||
toast.success('状态更新成功')
|
||
},
|
||
onError: (error: any) => {
|
||
toast.error(error?.message || '状态更新失败')
|
||
},
|
||
})
|
||
|
||
// 编辑照片
|
||
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) || [])
|
||
} else {
|
||
setSelectedPhotos([])
|
||
}
|
||
}
|
||
|
||
const handleSelectPhoto = (photoId: number, checked: boolean) => {
|
||
if (checked) {
|
||
setSelectedPhotos([...selectedPhotos, photoId])
|
||
} else {
|
||
setSelectedPhotos(selectedPhotos.filter(id => id !== photoId))
|
||
}
|
||
}
|
||
|
||
const handleBatchAction = (action: string, value?: string) => {
|
||
if (selectedPhotos.length === 0) {
|
||
toast.error('请先选择照片')
|
||
return
|
||
}
|
||
|
||
if (action === 'delete') {
|
||
if (confirm('确定要删除选中的照片吗?')) {
|
||
batchDeleteMutation.mutate(selectedPhotos)
|
||
}
|
||
} else if (action === 'status' && value) {
|
||
batchUpdateMutation.mutate({ ids: selectedPhotos, status: value })
|
||
}
|
||
}
|
||
|
||
const handleDeletePhoto = (photoId: number) => {
|
||
if (confirm('确定要删除这张照片吗?')) {
|
||
deletePhotoMutation.mutate(photoId)
|
||
}
|
||
}
|
||
|
||
const getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'published':
|
||
return 'bg-green-100 text-green-800'
|
||
case 'draft':
|
||
return 'bg-yellow-100 text-yellow-800'
|
||
case 'archived':
|
||
return 'bg-gray-100 text-gray-800'
|
||
case 'processing':
|
||
return 'bg-blue-100 text-blue-800'
|
||
default:
|
||
return 'bg-gray-100 text-gray-800'
|
||
}
|
||
}
|
||
|
||
const getStatusText = (status: string) => {
|
||
switch (status) {
|
||
case 'published':
|
||
return '已发布'
|
||
case 'draft':
|
||
return '草稿'
|
||
case 'archived':
|
||
return '已归档'
|
||
case 'processing':
|
||
return '处理中'
|
||
default:
|
||
return status
|
||
}
|
||
}
|
||
|
||
const formatFileSize = (bytes: number) => {
|
||
if (bytes === 0) return '0 Bytes'
|
||
const k = 1024
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
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">
|
||
{/* 页面头部 */}
|
||
<div className="flex items-center justify-between mb-8">
|
||
<div>
|
||
<h1 className="text-3xl font-bold">照片管理</h1>
|
||
<p className="text-muted-foreground mt-1">共 {photosData?.total || 0} 张照片</p>
|
||
</div>
|
||
<Button onClick={() => navigate('/photos/upload')} className="flex items-center gap-2">
|
||
<Plus className="h-4 w-4" />
|
||
上传照片
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 工具栏 */}
|
||
<Card className="mb-6">
|
||
<CardContent className="pt-6">
|
||
<div className="flex flex-col lg:flex-row gap-4">
|
||
{/* 搜索 */}
|
||
<div className="flex-1 relative">
|
||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||
<Input
|
||
placeholder="搜索照片..."
|
||
value={filters.search}
|
||
onChange={e => setFilters({ ...filters, search: e.target.value })}
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
|
||
{/* 过滤器 */}
|
||
<div className="flex gap-2">
|
||
<Select
|
||
value={filters.status}
|
||
onValueChange={value => setFilters({ ...filters, status: value })}
|
||
>
|
||
<SelectTrigger className="w-32">
|
||
<SelectValue placeholder="状态" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="">全部状态</SelectItem>
|
||
<SelectItem value="published">已发布</SelectItem>
|
||
<SelectItem value="draft">草稿</SelectItem>
|
||
<SelectItem value="archived">已归档</SelectItem>
|
||
<SelectItem value="processing">处理中</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
|
||
<Select
|
||
value={filters.categoryId}
|
||
onValueChange={value => setFilters({ ...filters, categoryId: value })}
|
||
>
|
||
<SelectTrigger className="w-32">
|
||
<SelectValue placeholder="分类" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="">全部分类</SelectItem>
|
||
{categories?.data?.categories?.map(category => (
|
||
<SelectItem key={category.id} value={category.id.toString()}>
|
||
{category.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 视图模式 */}
|
||
<div className="flex gap-1 border rounded-md">
|
||
<Button
|
||
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||
size="sm"
|
||
onClick={() => setViewMode('grid')}
|
||
>
|
||
<Grid className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||
size="sm"
|
||
onClick={() => setViewMode('list')}
|
||
>
|
||
<List className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 批量操作栏 */}
|
||
{selectedPhotos.length > 0 && (
|
||
<Card className="mb-6 border-primary">
|
||
<CardContent className="pt-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4">
|
||
<span className="text-sm">已选择 {selectedPhotos.length} 张照片</span>
|
||
<Button variant="outline" size="sm" onClick={() => setSelectedPhotos([])}>
|
||
取消选择
|
||
</Button>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="outline" size="sm">
|
||
更改状态
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent>
|
||
<DropdownMenuItem onClick={() => handleBatchAction('status', 'published')}>
|
||
设为已发布
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleBatchAction('status', 'draft')}>
|
||
设为草稿
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleBatchAction('status', 'archived')}>
|
||
设为已归档
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
<Button
|
||
variant="destructive"
|
||
size="sm"
|
||
onClick={() => handleBatchAction('delete')}
|
||
disabled={batchDeleteMutation.isPending}
|
||
>
|
||
删除选中
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 照片列表 */}
|
||
{photosLoading ? (
|
||
<div
|
||
className={
|
||
viewMode === 'grid'
|
||
? 'grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6'
|
||
: 'space-y-4'
|
||
}
|
||
>
|
||
{[...Array(8)].map((_, i) => (
|
||
<Card key={i}>
|
||
<CardContent className="p-4">
|
||
<Skeleton
|
||
className={viewMode === 'grid' ? 'aspect-square mb-4' : 'h-20 w-20 mb-4'}
|
||
/>
|
||
<Skeleton className="h-4 w-3/4 mb-2" />
|
||
<Skeleton className="h-3 w-1/2" />
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
) : photosData?.photos?.length ? (
|
||
<>
|
||
{/* 全选复选框 */}
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Checkbox
|
||
checked={
|
||
selectedPhotos.length === photosData.photos.length && photosData.photos.length > 0
|
||
}
|
||
onCheckedChange={handleSelectAll}
|
||
/>
|
||
<span className="text-sm text-muted-foreground">全选</span>
|
||
</div>
|
||
|
||
{viewMode === 'grid' ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||
{photosData.photos.map(photo => (
|
||
<Card key={photo.id} className="group">
|
||
<CardContent className="p-4">
|
||
<div className="relative mb-4">
|
||
<div className="aspect-square bg-gray-100 rounded-lg flex items-center justify-center">
|
||
<Camera className="h-12 w-12 text-gray-400" />
|
||
</div>
|
||
|
||
{/* 复选框 */}
|
||
<div className="absolute top-2 left-2">
|
||
<Checkbox
|
||
checked={selectedPhotos.includes(photo.id)}
|
||
onCheckedChange={checked =>
|
||
handleSelectPhoto(photo.id, checked as boolean)
|
||
}
|
||
className="bg-white"
|
||
/>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="outline" size="sm" className="h-8 w-8 p-0 bg-white">
|
||
<MoreVertical className="h-4 w-4" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end">
|
||
<DropdownMenuItem onClick={() => handleViewPhoto(photo)}>
|
||
<Eye className="h-4 w-4 mr-2" />
|
||
查看详情
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleEditPhoto(photo)}>
|
||
<Edit className="h-4 w-4 mr-2" />
|
||
编辑
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleDeletePhoto(photo.id)}>
|
||
<Trash className="h-4 w-4 mr-2" />
|
||
删除
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className="font-medium truncate mb-2">{photo.title}</h3>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<Badge className={getStatusColor(photo.status)}>
|
||
{getStatusText(photo.status)}
|
||
</Badge>
|
||
<span className="text-xs text-muted-foreground">
|
||
{formatFileSize(photo.fileSize)}
|
||
</span>
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
{new Date(photo.createdAt).toLocaleDateString()}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{photosData.photos.map(photo => (
|
||
<Card key={photo.id}>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center gap-4">
|
||
<Checkbox
|
||
checked={selectedPhotos.includes(photo.id)}
|
||
onCheckedChange={checked => handleSelectPhoto(photo.id, checked as boolean)}
|
||
/>
|
||
|
||
<div className="h-16 w-16 bg-gray-100 rounded flex items-center justify-center flex-shrink-0">
|
||
<Camera className="h-8 w-8 text-gray-400" />
|
||
</div>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<h3 className="font-medium truncate">{photo.title}</h3>
|
||
<p className="text-sm text-muted-foreground truncate">
|
||
{photo.description}
|
||
</p>
|
||
<div className="flex items-center gap-4 mt-2">
|
||
<Badge className={getStatusColor(photo.status)}>
|
||
{getStatusText(photo.status)}
|
||
</Badge>
|
||
<span className="text-xs text-muted-foreground">
|
||
{formatFileSize(photo.fileSize)}
|
||
</span>
|
||
<span className="text-xs text-muted-foreground">
|
||
{new Date(photo.createdAt).toLocaleDateString()}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="outline" size="sm">
|
||
<MoreVertical className="h-4 w-4" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end">
|
||
<DropdownMenuItem onClick={() => handleViewPhoto(photo)}>
|
||
<Eye className="h-4 w-4 mr-2" />
|
||
查看详情
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleEditPhoto(photo)}>
|
||
<Edit className="h-4 w-4 mr-2" />
|
||
编辑
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleDeletePhoto(photo.id)}>
|
||
<Trash className="h-4 w-4 mr-2" />
|
||
删除
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 分页 */}
|
||
{photosData.pages > 1 && (
|
||
<div className="flex justify-center mt-8">
|
||
<div className="flex gap-2">
|
||
<Button variant="outline" disabled={page === 1} onClick={() => setPage(page - 1)}>
|
||
上一页
|
||
</Button>
|
||
<div className="flex items-center gap-2">
|
||
{[...Array(Math.min(5, photosData.pages))].map((_, i) => {
|
||
const pageNum = i + 1
|
||
return (
|
||
<Button
|
||
key={pageNum}
|
||
variant={page === pageNum ? 'default' : 'outline'}
|
||
size="sm"
|
||
onClick={() => setPage(pageNum)}
|
||
>
|
||
{pageNum}
|
||
</Button>
|
||
)
|
||
})}
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
disabled={page === photosData.pages}
|
||
onClick={() => setPage(page + 1)}
|
||
>
|
||
下一页
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
<Card>
|
||
<CardContent className="py-12">
|
||
<div className="text-center">
|
||
<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>
|
||
<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>
|
||
)
|
||
}
|