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

299 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 {
Calendar,
BookOpen,
Search,
CheckCircle,
Clock,
AlertTriangle
} from 'lucide-react';
// Mock data - replace with actual API
async function fetchBorrowHistory() {
return {
success: true,
data: [
{
borrowId: 1,
book: {
title: '深入理解计算机系统',
authors: ['Randal E. Bryant'],
isbn: '9787111544937'
},
borrowDate: '2024-12-01',
dueDate: '2024-12-31',
returnDate: '2024-12-28',
renewTimes: 1,
status: 'returned',
fineAmount: '0'
},
{
borrowId: 2,
book: {
title: 'Python编程实战',
authors: ['Mark Lutz'],
isbn: '9787111627295'
},
borrowDate: '2025-01-05',
dueDate: '2025-02-04',
returnDate: null,
renewTimes: 1,
status: 'borrowed',
fineAmount: '0'
},
{
borrowId: 3,
book: {
title: '数据结构与算法',
authors: ['Thomas H. Cormen'],
isbn: '9787111407010'
},
borrowDate: '2024-11-15',
dueDate: '2024-12-15',
returnDate: '2024-12-20',
renewTimes: 0,
status: 'returned',
fineAmount: '5.00'
}
]
};
}
export default function StudentHistoryPage() {
const [statusFilter, setStatusFilter] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const { data, isLoading, error } = useQuery({
queryKey: ['student-history', statusFilter, searchTerm],
queryFn: fetchBorrowHistory,
});
const getStatusBadge = (status: string, dueDate: string, returnDate: string | null) => {
if (status === 'returned') {
const wasOverdue = returnDate && new Date(returnDate) > new Date(dueDate);
return (
<span className={`inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${
wasOverdue ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800'
}`}>
<CheckCircle className="h-3 w-3 mr-1" />
{wasOverdue ? '逾期归还' : '正常归还'}
</span>
);
}
const isOverdue = new Date(dueDate) < new Date();
return (
<span className={`inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${
isOverdue ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800'
}`}>
{isOverdue ? <AlertTriangle className="h-3 w-3 mr-1" /> : <Clock className="h-3 w-3 mr-1" />}
{isOverdue ? '逾期' : '借阅中'}
</span>
);
};
const filteredData = data?.data?.filter((record: any) => {
const matchesStatus = !statusFilter || record.status === statusFilter;
const matchesSearch = !searchTerm ||
record.book.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
record.book.authors.some((author: string) =>
author.toLowerCase().includes(searchTerm.toLowerCase())
);
return matchesStatus && matchesSearch;
});
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>
{/* Statistics */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-2xl font-bold text-gray-900">
{data?.data?.length || 0}
</p>
</div>
<BookOpen className="h-8 w-8 text-blue-600" />
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-2xl font-bold text-blue-600">
{data?.data?.filter((r: any) => r.status === 'borrowed').length || 0}
</p>
</div>
<Clock className="h-8 w-8 text-blue-600" />
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-2xl font-bold text-green-600">
{data?.data?.filter((r: any) => r.status === 'returned').length || 0}
</p>
</div>
<CheckCircle className="h-8 w-8 text-green-600" />
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-2xl font-bold text-red-600">
{data?.data?.filter((r: any) =>
(r.status === 'returned' && r.returnDate && new Date(r.returnDate) > new Date(r.dueDate)) ||
(r.status === 'borrowed' && new Date(r.dueDate) < new Date())
).length || 0}
</p>
</div>
<AlertTriangle className="h-8 w-8 text-red-600" />
</div>
</div>
</div>
{/* Search and Filters */}
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="输入书名或作者"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value=""></option>
<option value="borrowed"></option>
<option value="returned"></option>
</select>
</div>
</div>
</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>
) : (
<>
{/* History List */}
<div className="space-y-4">
{filteredData?.map((record: any) => (
<div key={record.borrowId} className="bg-white rounded-lg shadow-md p-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
<div className="flex-1">
<div className="flex items-start justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900">
{record.book.title}
</h3>
{getStatusBadge(record.status, record.dueDate, record.returnDate)}
</div>
<p className="text-gray-600 mb-2">
: {record.book.authors.join(', ')}
</p>
<p className="text-gray-500 text-sm mb-2">
ISBN: {record.book.isbn}
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700">:</span>
<span className="ml-2 text-gray-600">
{new Date(record.borrowDate).toLocaleDateString('zh-CN')}
</span>
</div>
<div>
<span className="font-medium text-gray-700">:</span>
<span className="ml-2 text-gray-600">
{new Date(record.dueDate).toLocaleDateString('zh-CN')}
</span>
</div>
{record.returnDate && (
<div>
<span className="font-medium text-gray-700">:</span>
<span className="ml-2 text-gray-600">
{new Date(record.returnDate).toLocaleDateString('zh-CN')}
</span>
</div>
)}
</div>
<div className="flex items-center justify-between mt-4 text-sm">
<div>
<span className="font-medium text-gray-700">:</span>
<span className="ml-2 text-gray-600">{record.renewTimes}/2</span>
</div>
{record.fineAmount && parseFloat(record.fineAmount) > 0 && (
<div className="text-red-600">
<span className="font-medium">:</span>
<span className="ml-2">¥{parseFloat(record.fineAmount).toFixed(2)}</span>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
{/* No results */}
{filteredData?.length === 0 && (
<div className="text-center py-12">
<Calendar className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">
{data?.data?.length === 0 ? '暂无借阅记录' : '没有找到符合条件的记录'}
</p>
</div>
)}
</>
)}
</div>
</div>
);
}