2025-06-24 10:56:21 +08:00

285 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
);
}