mirror of
https://github.com/handsomezhuzhu/QQuiz.git
synced 2026-02-20 20:10:14 +00:00
翻页样式完善
This commit is contained in:
87
frontend/src/components/Pagination.jsx
Normal file
87
frontend/src/components/Pagination.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'
|
||||||
|
|
||||||
|
const Pagination = ({
|
||||||
|
currentPage,
|
||||||
|
totalItems,
|
||||||
|
pageSize,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
pageSizeOptions = [10, 20, 50, 100]
|
||||||
|
}) => {
|
||||||
|
const totalPages = Math.ceil(totalItems / pageSize)
|
||||||
|
const [inputPage, setInputPage] = useState(currentPage)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputPage(currentPage)
|
||||||
|
}, [currentPage])
|
||||||
|
|
||||||
|
const handlePageSubmit = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
let page = parseInt(inputPage)
|
||||||
|
if (isNaN(page)) page = 1
|
||||||
|
if (page < 1) page = 1
|
||||||
|
if (page > totalPages) page = totalPages
|
||||||
|
onPageChange(page)
|
||||||
|
setInputPage(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalItems === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 py-4 border-t border-gray-100 mt-4">
|
||||||
|
{/* Info */}
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
显示 {Math.min((currentPage - 1) * pageSize + 1, totalItems)} - {Math.min(currentPage * pageSize, totalItems)} 共 {totalItems} 条
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 sm:gap-4">
|
||||||
|
{/* Page Size Selector */}
|
||||||
|
<div className="relative group">
|
||||||
|
<select
|
||||||
|
value={pageSize}
|
||||||
|
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||||
|
className="appearance-none bg-white border border-gray-300 text-gray-700 py-2 pl-3 pr-8 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent cursor-pointer hover:border-gray-400 transition-colors"
|
||||||
|
>
|
||||||
|
{pageSizeOptions.map(size => (
|
||||||
|
<option key={size} value={size}>{size} 条/页</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="absolute right-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Manual Input */}
|
||||||
|
<form onSubmit={handlePageSubmit} className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputPage}
|
||||||
|
onChange={(e) => setInputPage(e.target.value)}
|
||||||
|
className="w-12 text-center py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent mx-1"
|
||||||
|
/>
|
||||||
|
<span className="text-gray-500 text-sm mx-1">/ {totalPages}</span>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Pagination
|
||||||
@@ -5,7 +5,8 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { mistakeAPI } from '../api/client'
|
import { mistakeAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
import Layout from '../components/Layout'
|
||||||
import { XCircle, Loader, Trash2, BookOpen, Play, ChevronLeft, ChevronRight } from 'lucide-react'
|
import Pagination from '../components/Pagination'
|
||||||
|
import { XCircle, Loader, Trash2, BookOpen, Play } from 'lucide-react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { getQuestionTypeText, formatRelativeTime } from '../utils/helpers'
|
import { getQuestionTypeText, formatRelativeTime } from '../utils/helpers'
|
||||||
|
|
||||||
@@ -185,48 +186,16 @@ export const MistakeList = () => {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{total > limit && (
|
<Pagination
|
||||||
<div className="flex items-center justify-between pt-4">
|
currentPage={page}
|
||||||
<div className="text-sm text-gray-600">
|
totalItems={total}
|
||||||
显示 {Math.min((page - 1) * limit + 1, total)} - {Math.min(page * limit, total)} 共 {total} 条
|
pageSize={limit}
|
||||||
</div>
|
onPageChange={setPage}
|
||||||
<div className="flex gap-2">
|
onPageSizeChange={(newLimit) => {
|
||||||
<button
|
setLimit(newLimit)
|
||||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
setPage(1)
|
||||||
disabled={page === 1}
|
}}
|
||||||
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
/>
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<span className="flex items-center px-4 border border-gray-300 rounded-lg bg-white">
|
|
||||||
{page}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(p => (p * limit < total ? p + 1 : p))}
|
|
||||||
disabled={page * limit >= total}
|
|
||||||
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Limit Selector */}
|
|
||||||
<div className="flex justify-end pt-2">
|
|
||||||
<select
|
|
||||||
value={limit}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLimit(Number(e.target.value))
|
|
||||||
setPage(1)
|
|
||||||
}}
|
|
||||||
className="text-sm border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
>
|
|
||||||
<option value={10}>10 条/页</option>
|
|
||||||
<option value={20}>20 条/页</option>
|
|
||||||
<option value={50}>50 条/页</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { questionAPI } from '../api/client'
|
import { questionAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
import Layout from '../components/Layout'
|
||||||
import { FileText, Loader, ChevronLeft, ChevronRight, Search } from 'lucide-react'
|
import Pagination from '../components/Pagination'
|
||||||
|
import { FileText, Loader, Search } from 'lucide-react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { getQuestionTypeText, formatRelativeTime } from '../utils/helpers'
|
import { getQuestionTypeText, formatRelativeTime } from '../utils/helpers'
|
||||||
|
|
||||||
@@ -140,48 +141,16 @@ export const QuestionBank = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{total > limit && (
|
<Pagination
|
||||||
<div className="flex items-center justify-between pt-4">
|
currentPage={page}
|
||||||
<div className="text-sm text-gray-600">
|
totalItems={total}
|
||||||
显示 {Math.min((page - 1) * limit + 1, total)} - {Math.min(page * limit, total)} 共 {total} 条
|
pageSize={limit}
|
||||||
</div>
|
onPageChange={setPage}
|
||||||
<div className="flex gap-2">
|
onPageSizeChange={(newLimit) => {
|
||||||
<button
|
setLimit(newLimit)
|
||||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
setPage(1)
|
||||||
disabled={page === 1}
|
}}
|
||||||
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
/>
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<span className="flex items-center px-4 border border-gray-300 rounded-lg bg-white">
|
|
||||||
{page}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(p => (p * limit < total ? p + 1 : p))}
|
|
||||||
disabled={page * limit >= total}
|
|
||||||
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Limit Selector */}
|
|
||||||
<div className="flex justify-end pt-2">
|
|
||||||
<select
|
|
||||||
value={limit}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLimit(Number(e.target.value))
|
|
||||||
setPage(1)
|
|
||||||
}}
|
|
||||||
className="text-sm border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
>
|
|
||||||
<option value={10}>10 条/页</option>
|
|
||||||
<option value={20}>20 条/页</option>
|
|
||||||
<option value={50}>50 条/页</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user