fix: 修复 Prettier 格式检查和依赖问题

## 修复内容

### 依赖修复
- 安装缺失的 `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 文件
- 代码功能完全不受影响,仅调整格式
This commit is contained in:
xujiang
2025-07-14 11:25:05 +08:00
parent e46d8f28d1
commit 0ff0a7e995
49 changed files with 1292 additions and 1386 deletions

View File

@ -1,35 +1,52 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
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 {
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 {
Camera,
Plus,
Search,
MoreVertical,
import { categoryService } from '@/services/categoryService'
import { Photo, photoService } from '@/services/photoService'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Camera,
Edit,
Trash,
Eye,
Folder,
Grid,
List,
RefreshCw,
ImageIcon,
List,
MoreVertical,
Plus,
RefreshCw,
Search,
Tag,
Folder
Trash,
} from 'lucide-react'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { photoService, Photo } from '@/services/photoService'
import { categoryService } from '@/services/categoryService'
type ViewMode = 'grid' | 'list'
@ -44,7 +61,7 @@ interface PhotoFilters {
export default function Photos() {
const navigate = useNavigate()
const queryClient = useQueryClient()
// 状态管理
const [viewMode, setViewMode] = useState<ViewMode>('grid')
const [selectedPhotos, setSelectedPhotos] = useState<number[]>([])
@ -54,43 +71,43 @@ export default function Photos() {
status: '',
categoryId: '',
tagId: '',
dateRange: ''
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'
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'
})
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()
queryFn: () => categoryService.getCategories(),
})
// 删除照片
const deletePhotoMutation = useMutation({
mutationFn: photoService.deletePhoto,
@ -100,9 +117,9 @@ export default function Photos() {
},
onError: (error: any) => {
toast.error(error?.message || '删除失败')
}
},
})
// 批量删除照片
const batchDeleteMutation = useMutation({
mutationFn: photoService.batchDelete,
@ -113,12 +130,12 @@ export default function Photos() {
},
onError: (error: any) => {
toast.error(error?.message || '批量删除失败')
}
},
})
// 批量更新状态
const batchUpdateMutation = useMutation({
mutationFn: ({ ids, status }: { ids: number[], status: string }) =>
mutationFn: ({ ids, status }: { ids: number[]; status: string }) =>
photoService.batchUpdate(ids, { status }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['photos'] })
@ -127,13 +144,12 @@ export default function Photos() {
},
onError: (error: any) => {
toast.error(error?.message || '状态更新失败')
}
},
})
// 编辑照片
const editPhotoMutation = useMutation({
mutationFn: ({ id, data }: { id: number, data: any }) =>
photoService.updatePhoto(id, data),
mutationFn: ({ id, data }: { id: number; data: any }) => photoService.updatePhoto(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['photos'] })
setIsEditDialogOpen(false)
@ -142,9 +158,9 @@ export default function Photos() {
},
onError: (error: any) => {
toast.error(error?.message || '更新失败')
}
},
})
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedPhotos(photosData?.photos.map(photo => photo.id) || [])
@ -152,7 +168,7 @@ export default function Photos() {
setSelectedPhotos([])
}
}
const handleSelectPhoto = (photoId: number, checked: boolean) => {
if (checked) {
setSelectedPhotos([...selectedPhotos, photoId])
@ -160,13 +176,13 @@ export default function Photos() {
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)
@ -175,33 +191,43 @@ export default function Photos() {
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'
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
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
@ -209,57 +235,54 @@ export default function Photos() {
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
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
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>
<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">
@ -270,14 +293,17 @@ export default function Photos() {
<Input
placeholder="搜索照片..."
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
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 })}>
<Select
value={filters.status}
onValueChange={value => setFilters({ ...filters, status: value })}
>
<SelectTrigger className="w-32">
<SelectValue placeholder="状态" />
</SelectTrigger>
@ -289,14 +315,17 @@ export default function Photos() {
<SelectItem value="processing"></SelectItem>
</SelectContent>
</Select>
<Select value={filters.categoryId} onValueChange={(value) => setFilters({ ...filters, categoryId: value })}>
<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) => (
{categories?.data?.categories?.map(category => (
<SelectItem key={category.id} value={category.id.toString()}>
{category.name}
</SelectItem>
@ -304,7 +333,7 @@ export default function Photos() {
</SelectContent>
</Select>
</div>
{/* 视图模式 */}
<div className="flex gap-1 border rounded-md">
<Button
@ -325,16 +354,14 @@ export default function Photos() {
</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>
<span className="text-sm"> {selectedPhotos.length} </span>
<Button variant="outline" size="sm" onClick={() => setSelectedPhotos([])}>
</Button>
@ -358,8 +385,8 @@ export default function Photos() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="destructive"
<Button
variant="destructive"
size="sm"
onClick={() => handleBatchAction('delete')}
disabled={batchDeleteMutation.isPending}
@ -371,14 +398,22 @@ export default function Photos() {
</CardContent>
</Card>
)}
{/* 照片列表 */}
{photosLoading ? (
<div className={viewMode === 'grid' ? 'grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6' : 'space-y-4'}>
<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={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>
@ -390,31 +425,35 @@ export default function Photos() {
{/* 全选复选框 */}
<div className="flex items-center gap-2 mb-4">
<Checkbox
checked={selectedPhotos.length === photosData.photos.length && photosData.photos.length > 0}
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) => (
{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)}
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>
@ -440,7 +479,7 @@ export default function Photos() {
</DropdownMenu>
</div>
</div>
<div>
<h3 className="font-medium truncate mb-2">{photo.title}</h3>
<div className="flex items-center justify-between mb-2">
@ -461,19 +500,19 @@ export default function Photos() {
</div>
) : (
<div className="space-y-4">
{photosData.photos.map((photo) => (
{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)}
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">
@ -491,7 +530,7 @@ export default function Photos() {
</span>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
@ -519,16 +558,12 @@ export default function Photos() {
))}
</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 variant="outline" disabled={page === 1} onClick={() => setPage(page - 1)}>
</Button>
<div className="flex items-center gap-2">
@ -565,9 +600,7 @@ export default function Photos() {
<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>
<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" />
@ -582,42 +615,43 @@ export default function Photos() {
</CardContent>
</Card>
)}
{/* 编辑对话框 */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
<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 })}
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 })}
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 })}>
<Select
value={editForm.status}
onValueChange={value => setEditForm({ ...editForm, status: value as any })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
@ -629,7 +663,7 @@ export default function Photos() {
</Select>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
@ -640,23 +674,21 @@ export default function Photos() {
</div>
</DialogContent>
</Dialog>
{/* 详情查看对话框 */}
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{viewingPhoto?.title}</DialogTitle>
<DialogDescription>
</DialogDescription>
<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>
@ -664,19 +696,21 @@ export default function Photos() {
{getStatusText(viewingPhoto.status)}
</Badge>
</div>
<div>
<Label className="text-sm font-medium"></Label>
<p className="text-sm text-muted-foreground">{formatFileSize(viewingPhoto.fileSize)}</p>
<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">
@ -684,19 +718,19 @@ export default function Photos() {
</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) => (
{viewingPhoto.categories.map(category => (
<Badge key={category.id} variant="secondary">
<Folder className="h-3 w-3 mr-1" />
{category.name}
@ -705,12 +739,12 @@ export default function Photos() {
</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) => (
{viewingPhoto.tags.map(tag => (
<Badge key={tag.id} variant="outline">
<Tag className="h-3 w-3 mr-1" />
{tag.name}
@ -721,7 +755,7 @@ export default function Photos() {
)}
</div>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
@ -735,4 +769,4 @@ export default function Photos() {
</Dialog>
</div>
)
}
}