299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
'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>
|
||
);
|
||
}
|