285 lines
11 KiB
TypeScript
285 lines
11 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { Search, Eye, Star, BookOpen } from 'lucide-react';
|
||
import Link from 'next/link';
|
||
import Image from 'next/image';
|
||
import { Book, BookSearchFilters } from '@/lib/types';
|
||
|
||
async function fetchBooks(filters: BookSearchFilters & { page: number; pageSize: number }) {
|
||
const params = new URLSearchParams();
|
||
|
||
Object.entries(filters).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== '') {
|
||
params.append(key, value.toString());
|
||
}
|
||
});
|
||
|
||
const response = await fetch(`/api/books?${params}`);
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch books');
|
||
}
|
||
return response.json();
|
||
}
|
||
|
||
export default function BooksPage() {
|
||
const [searchFilters, setSearchFilters] = useState<BookSearchFilters>({});
|
||
const [page, setPage] = useState(1);
|
||
const pageSize = 12;
|
||
|
||
const { data, isLoading, error } = useQuery({
|
||
queryKey: ['books', searchFilters, page, pageSize],
|
||
queryFn: () => fetchBooks({ ...searchFilters, page, pageSize }),
|
||
});
|
||
|
||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
setPage(1);
|
||
};
|
||
|
||
const handleInputChange = (field: keyof BookSearchFilters, value: string) => {
|
||
setSearchFilters(prev => ({
|
||
...prev,
|
||
[field]: value || undefined,
|
||
}));
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
<div className="container mx-auto px-4 py-8">
|
||
{/* Header */}
|
||
<div className="mb-8">
|
||
<h1 className="text-3xl font-bold text-gray-800 mb-2">图书检索</h1>
|
||
<p className="text-gray-600">搜索和浏览图书馆藏书</p>
|
||
</div>
|
||
|
||
{/* Search Filters */}
|
||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||
<form onSubmit={handleSearch} className="space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
书名
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="输入书名关键词"
|
||
value={searchFilters.title || ''}
|
||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
作者
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="输入作者姓名"
|
||
value={searchFilters.author || ''}
|
||
onChange={(e) => handleInputChange('author', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
ISBN
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="输入ISBN号"
|
||
value={searchFilters.isbn || ''}
|
||
onChange={(e) => handleInputChange('isbn', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
分类号
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="输入分类号"
|
||
value={searchFilters.classificationNo || ''}
|
||
onChange={(e) => handleInputChange('classificationNo', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col sm:flex-row gap-4 items-end">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
状态
|
||
</label>
|
||
<select
|
||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
value={searchFilters.status || ''}
|
||
onChange={(e) => handleInputChange('status', e.target.value)}
|
||
>
|
||
<option value="">全部状态</option>
|
||
<option value="normal">正常</option>
|
||
<option value="damaged">损坏</option>
|
||
<option value="removed">下架</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="available"
|
||
className="mr-2"
|
||
checked={searchFilters.available || false}
|
||
onChange={(e) => handleInputChange('available', e.target.checked ? 'true' : '')}
|
||
/>
|
||
<label htmlFor="available" className="text-sm font-medium text-gray-700">
|
||
仅显示可借图书
|
||
</label>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md flex items-center gap-2 transition-colors"
|
||
>
|
||
<Search className="h-4 w-4" />
|
||
搜索
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{/* Results */}
|
||
{isLoading ? (
|
||
<div className="text-center py-12">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||
<p className="mt-4 text-gray-600">加载中...</p>
|
||
</div>
|
||
) : error ? (
|
||
<div className="text-center py-12">
|
||
<p className="text-red-600">加载失败,请重试</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* Results Header */}
|
||
<div className="flex justify-between items-center mb-6">
|
||
<p className="text-gray-600">
|
||
找到 {data?.data?.total || 0} 本图书
|
||
</p>
|
||
</div>
|
||
|
||
{/* Books Grid */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||
{data?.data?.data?.map((book: Book) => (
|
||
<div key={book.bookId} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
|
||
<div className="h-48 bg-gray-200 flex items-center justify-center">
|
||
{book.coverUrl ? (
|
||
<img
|
||
src={`https://picsum.photos/300/300?random=${book.bookId}`}
|
||
alt={book.title}
|
||
width={192}
|
||
height={192}
|
||
className="h-full w-full object-cover"
|
||
/>
|
||
) : (
|
||
<BookOpen className="h-16 w-16 text-gray-400" />
|
||
)}
|
||
</div>
|
||
|
||
<div className="p-4">
|
||
<h3 className="font-semibold text-lg mb-2 line-clamp-2">
|
||
{book.title}
|
||
</h3>
|
||
|
||
<p className="text-gray-600 text-sm mb-2">
|
||
作者: {book.authors?.join(', ')}
|
||
</p>
|
||
|
||
<p className="text-gray-600 text-sm mb-2">
|
||
出版社: {book.publisher}
|
||
</p>
|
||
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm text-gray-500">
|
||
可借: {book.availableCopies}/{book.totalCopies}
|
||
</span>
|
||
<span className={`px-2 py-1 rounded-full text-xs ${
|
||
book.status === 'normal' ? 'bg-green-100 text-green-800' :
|
||
book.status === 'damaged' ? 'bg-yellow-100 text-yellow-800' :
|
||
'bg-red-100 text-red-800'
|
||
}`}>
|
||
{book.status === 'normal' ? '正常' :
|
||
book.status === 'damaged' ? '损坏' : '下架'}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-center">
|
||
<div className="flex items-center text-yellow-500">
|
||
<Star className="h-4 w-4 fill-current" />
|
||
<span className="text-sm ml-1">4.5</span>
|
||
</div>
|
||
|
||
<Link
|
||
href={`/books/${book.bookId}`}
|
||
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm flex items-center gap-1 transition-colors"
|
||
>
|
||
<Eye className="h-3 w-3" />
|
||
详情
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
{data?.data?.totalPages > 1 && (
|
||
<div className="flex justify-center mt-8">
|
||
<div className="flex gap-2">
|
||
{page > 1 && (
|
||
<button
|
||
onClick={() => setPage(page - 1)}
|
||
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
上一页
|
||
</button>
|
||
)}
|
||
|
||
{[...Array(Math.min(5, data.data.totalPages))].map((_, i) => {
|
||
const pageNum = i + 1;
|
||
return (
|
||
<button
|
||
key={pageNum}
|
||
onClick={() => setPage(pageNum)}
|
||
className={`px-4 py-2 border rounded-md ${
|
||
page === pageNum
|
||
? 'bg-blue-600 text-white border-blue-600'
|
||
: 'border-gray-300 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
{pageNum}
|
||
</button>
|
||
);
|
||
})}
|
||
|
||
{page < data.data.totalPages && (
|
||
<button
|
||
onClick={() => setPage(page + 1)}
|
||
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
下一页
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|