超管后台 - 整体页面错误调整、及菜单栏设置颜色
This commit is contained in:
@@ -18,6 +18,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { getTrafficPoolList } from "@/lib/traffic-pool-api"
|
import { getTrafficPoolList } from "@/lib/traffic-pool-api"
|
||||||
import { Customer } from "@/lib/traffic-pool-api"
|
import { Customer } from "@/lib/traffic-pool-api"
|
||||||
|
import { PaginationControls } from "@/components/ui/pagination-controls"
|
||||||
|
|
||||||
// Sample customer data
|
// Sample customer data
|
||||||
const customersData = [
|
const customersData = [
|
||||||
@@ -123,8 +124,7 @@ export default function CustomersPage() {
|
|||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [totalPages, setTotalPages] = useState(1)
|
const [totalPages, setTotalPages] = useState(1)
|
||||||
const [totalItems, setTotalItems] = useState(0)
|
const [totalItems, setTotalItems] = useState(0)
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(100)
|
||||||
const [jumpToPage, setJumpToPage] = useState("")
|
|
||||||
|
|
||||||
// 获取客户列表数据
|
// 获取客户列表数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -140,10 +140,14 @@ export default function CustomersPage() {
|
|||||||
} else {
|
} else {
|
||||||
setError(response.msg || "获取客户列表失败");
|
setError(response.msg || "获取客户列表失败");
|
||||||
setCustomers([]);
|
setCustomers([]);
|
||||||
|
setTotalItems(0); // Reset totals on error
|
||||||
|
setTotalPages(0);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || "获取客户列表失败");
|
setError(err.message || "获取客户列表失败");
|
||||||
setCustomers([]);
|
setCustomers([]);
|
||||||
|
setTotalItems(0); // Reset totals on error
|
||||||
|
setTotalPages(0);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -152,27 +156,10 @@ export default function CustomersPage() {
|
|||||||
fetchCustomers();
|
fetchCustomers();
|
||||||
}, [currentPage, pageSize, searchTerm]);
|
}, [currentPage, pageSize, searchTerm]);
|
||||||
|
|
||||||
// 切换页码
|
// 修改后的页面大小处理函数
|
||||||
const goToPage = (page: number) => {
|
const handlePageSizeChange = (newSize: number) => {
|
||||||
if (page >= 1 && page <= totalPages) {
|
|
||||||
setCurrentPage(page);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理页码跳转
|
|
||||||
const handleJumpToPage = () => {
|
|
||||||
const page = parseInt(jumpToPage);
|
|
||||||
if (!isNaN(page) && page >= 1 && page <= totalPages) {
|
|
||||||
setCurrentPage(page);
|
|
||||||
setJumpToPage("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理每页显示条数变化
|
|
||||||
const handlePageSizeChange = (size: string) => {
|
|
||||||
const newSize = parseInt(size);
|
|
||||||
setPageSize(newSize);
|
setPageSize(newSize);
|
||||||
setCurrentPage(1); // 重置为第一页
|
setCurrentPage(1); // Reset to first page when page size changes
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter customers based on search and filters (兼容示例数据)
|
// Filter customers based on search and filters (兼容示例数据)
|
||||||
@@ -308,12 +295,12 @@ export default function CustomersPage() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="h-24 text-center">
|
<TableCell colSpan={8} className="h-24 text-center">
|
||||||
正在加载...
|
加载中...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="h-24 text-center text-red-500">
|
<TableCell colSpan={8} className="h-24 text-center text-red-600">
|
||||||
{error}
|
{error}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -379,106 +366,15 @@ export default function CustomersPage() {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分页控件 */}
|
{/* 使用新的分页组件 */}
|
||||||
{!isLoading && !error && customers.length > 0 && (
|
<PaginationControls
|
||||||
<div className="flex items-center justify-between px-2">
|
currentPage={currentPage}
|
||||||
<div className="flex items-center space-x-4">
|
totalPages={totalPages}
|
||||||
<div className="text-sm text-muted-foreground">
|
pageSize={pageSize}
|
||||||
共 {totalItems} 条记录,当前第 {currentPage}/{totalPages} 页
|
totalItems={totalItems}
|
||||||
</div>
|
onPageChange={setCurrentPage} // 直接传递setCurrentPage
|
||||||
<div className="flex items-center space-x-2">
|
onPageSizeChange={handlePageSizeChange}
|
||||||
<span className="text-sm">每页显示:</span>
|
/>
|
||||||
<Select value={pageSize.toString()} onValueChange={handlePageSizeChange}>
|
|
||||||
<SelectTrigger className="h-8 w-[70px]">
|
|
||||||
<SelectValue placeholder={pageSize} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="30">30</SelectItem>
|
|
||||||
<SelectItem value="50">50</SelectItem>
|
|
||||||
<SelectItem value="100">100</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<span className="text-sm">条</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => goToPage(currentPage - 1)}
|
|
||||||
disabled={currentPage <= 1}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* 数字分页按钮 */}
|
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => {
|
|
||||||
// 显示当前页码前后2页,以及第一页和最后一页
|
|
||||||
const shouldShow =
|
|
||||||
page === 1 ||
|
|
||||||
page === totalPages ||
|
|
||||||
(page >= currentPage - 2 && page <= currentPage + 2);
|
|
||||||
|
|
||||||
if (!shouldShow) {
|
|
||||||
// 显示省略号
|
|
||||||
if (page === currentPage - 3 || page === currentPage + 3) {
|
|
||||||
return (
|
|
||||||
<span key={page} className="px-2">
|
|
||||||
...
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={page}
|
|
||||||
variant={page === currentPage ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => goToPage(page)}
|
|
||||||
className="min-w-[2.5rem]"
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => goToPage(currentPage + 1)}
|
|
||||||
disabled={currentPage >= totalPages}
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* 页码跳转 */}
|
|
||||||
<div className="flex items-center space-x-2 ml-2">
|
|
||||||
<span className="text-sm">跳转到</span>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={jumpToPage}
|
|
||||||
onChange={(e) => setJumpToPage(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleJumpToPage()}
|
|
||||||
className="h-8 w-16 text-center"
|
|
||||||
min={1}
|
|
||||||
max={totalPages}
|
|
||||||
/>
|
|
||||||
<span className="text-sm">页</span>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleJumpToPage}
|
|
||||||
disabled={!jumpToPage || isNaN(parseInt(jumpToPage)) || parseInt(jumpToPage) < 1 || parseInt(jumpToPage) > totalPages}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import { Plus, Search, MoreHorizontal, Edit, Eye, Trash, ChevronLeft, ChevronRight } from "lucide-react"
|
import { Plus, Search, MoreHorizontal, Edit, Eye, Trash } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { PaginationControls } from "@/components/ui/pagination-controls"
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: number
|
id: number
|
||||||
@@ -36,7 +37,8 @@ export default function ProjectsPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [totalPages, setTotalPages] = useState(1)
|
const [totalPages, setTotalPages] = useState(1)
|
||||||
const [pageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
const [totalItems, setTotalItems] = useState(0)
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
const [deletingProjectId, setDeletingProjectId] = useState<number | null>(null)
|
const [deletingProjectId, setDeletingProjectId] = useState<number | null>(null)
|
||||||
const [isDeleting, setIsDeleting] = useState(false)
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
@@ -51,12 +53,19 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
setProjects(data.data.list)
|
setProjects(data.data.list)
|
||||||
|
setTotalItems(data.data.total)
|
||||||
setTotalPages(Math.ceil(data.data.total / pageSize))
|
setTotalPages(Math.ceil(data.data.total / pageSize))
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.msg || "获取项目列表失败")
|
toast.error(data.msg || "获取项目列表失败")
|
||||||
|
setProjects([])
|
||||||
|
setTotalItems(0)
|
||||||
|
setTotalPages(0)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("获取项目列表失败")
|
toast.error("获取项目列表失败")
|
||||||
|
setProjects([])
|
||||||
|
setTotalItems(0)
|
||||||
|
setTotalPages(0)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@@ -65,11 +74,10 @@ export default function ProjectsPage() {
|
|||||||
fetchProjects()
|
fetchProjects()
|
||||||
}, [currentPage, pageSize])
|
}, [currentPage, pageSize])
|
||||||
|
|
||||||
// 切换页码
|
// 处理页面大小变化
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageSizeChange = (newSize: number) => {
|
||||||
if (page >= 1 && page <= totalPages) {
|
setPageSize(newSize)
|
||||||
setCurrentPage(page)
|
setCurrentPage(1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteClick = (projectId: number) => {
|
const handleDeleteClick = (projectId: number) => {
|
||||||
@@ -96,8 +104,26 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
toast.success("删除成功")
|
toast.success("删除成功")
|
||||||
// 刷新项目列表
|
// Fetch projects again after delete
|
||||||
window.location.reload()
|
const fetchProjects = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://yishi.com/company/list?page=${currentPage}&limit=${pageSize}`)
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.code === 200) {
|
||||||
|
setProjects(data.data.list)
|
||||||
|
setTotalItems(data.data.total)
|
||||||
|
setTotalPages(Math.ceil(data.data.total / pageSize))
|
||||||
|
if (currentPage > Math.ceil(data.data.total / pageSize) && Math.ceil(data.data.total / pageSize) > 0) {
|
||||||
|
setCurrentPage(Math.ceil(data.data.total / pageSize));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setProjects([]); setTotalItems(0); setTotalPages(0);
|
||||||
|
}
|
||||||
|
} catch (error) { setProjects([]); setTotalItems(0); setTotalPages(0); }
|
||||||
|
finally { setIsLoading(false); }
|
||||||
|
}
|
||||||
|
fetchProjects();
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.msg || "删除失败")
|
toast.error(data.msg || "删除失败")
|
||||||
}
|
}
|
||||||
@@ -206,41 +232,15 @@ export default function ProjectsPage() {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分页控件 */}
|
<PaginationControls
|
||||||
{!isLoading && projects.length > 0 && (
|
currentPage={currentPage}
|
||||||
<div className="flex items-center justify-center gap-2">
|
totalPages={totalPages}
|
||||||
<Button
|
pageSize={pageSize}
|
||||||
variant="outline"
|
totalItems={totalItems}
|
||||||
size="icon"
|
onPageChange={setCurrentPage}
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
disabled={currentPage === 1}
|
/>
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
|
||||||
<Button
|
|
||||||
key={page}
|
|
||||||
variant={page === currentPage ? "default" : "outline"}
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handlePageChange(page)}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
3
SuperAdmin/components/layout/header.module.css
Normal file
3
SuperAdmin/components/layout/header.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.contentHeader {
|
||||||
|
height: 3.81rem;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import styles from './header.module.css';
|
||||||
|
|
||||||
interface AdminInfo {
|
interface AdminInfo {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -39,7 +40,7 @@ export function Header() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-16 border-b px-6 flex items-center justify-between bg-background">
|
<header className={`${styles.contentHeader} border-b px-6 flex items-center justify-between bg-background`}>
|
||||||
<div className="flex-1"></div>
|
<div className="flex-1"></div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export function Sidebar() {
|
|||||||
onClick={() => toggleMenu(item.id)}
|
onClick={() => toggleMenu(item.id)}
|
||||||
className={`flex items-center justify-between px-4 py-2 rounded-md text-sm w-full text-left ${
|
className={`flex items-center justify-between px-4 py-2 rounded-md text-sm w-full text-left ${
|
||||||
isActive || isChildActive
|
isActive || isChildActive
|
||||||
? "bg-primary text-primary-foreground"
|
? "text-white font-semibold"
|
||||||
: "hover:bg-accent hover:text-accent-foreground"
|
: "hover:bg-blue-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -125,8 +125,8 @@ export function Sidebar() {
|
|||||||
href={child.path}
|
href={child.path}
|
||||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${
|
className={`flex items-center px-4 py-2 rounded-md text-sm ${
|
||||||
isChildItemActive
|
isChildItemActive
|
||||||
? "text-primary font-medium"
|
? "text-white font-semibold bg-blue-700"
|
||||||
: "hover:bg-accent hover:text-accent-foreground"
|
: "hover:bg-blue-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{child.icon && getLucideIcon(child.icon)}
|
{child.icon && getLucideIcon(child.icon)}
|
||||||
@@ -143,8 +143,8 @@ export function Sidebar() {
|
|||||||
href={item.path}
|
href={item.path}
|
||||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${
|
className={`flex items-center px-4 py-2 rounded-md text-sm ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary text-primary-foreground"
|
? "text-white font-semibold"
|
||||||
: "hover:bg-accent hover:text-accent-foreground"
|
: "hover:bg-blue-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.icon && getLucideIcon(item.icon)}
|
{item.icon && getLucideIcon(item.icon)}
|
||||||
@@ -156,8 +156,8 @@ export function Sidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-64 border-r bg-background h-full flex flex-col">
|
<div className="w-64 border-r bg-[#2563eb] h-full flex flex-col text-white">
|
||||||
<div className="p-4 border-b">
|
<div className="p-4 border-b border-blue-500">
|
||||||
<h2 className="text-lg font-bold">超级管理员</h2>
|
<h2 className="text-lg font-bold">超级管理员</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ export function Sidebar() {
|
|||||||
// 加载状态
|
// 加载状态
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<div key={i} className="h-10 rounded animate-pulse bg-gray-200"></div>
|
<div key={i} className="h-10 rounded animate-pulse bg-blue-400"></div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : menus.length > 0 ? (
|
) : menus.length > 0 ? (
|
||||||
@@ -176,7 +176,7 @@ export function Sidebar() {
|
|||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
// 无菜单数据
|
// 无菜单数据
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-blue-200">
|
||||||
<p>暂无菜单数据</p>
|
<p>暂无菜单数据</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
175
SuperAdmin/components/ui/pagination-controls.tsx
Normal file
175
SuperAdmin/components/ui/pagination-controls.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
|
|
||||||
|
interface PaginationControlsProps {
|
||||||
|
currentPage: number
|
||||||
|
totalPages: number
|
||||||
|
pageSize: number
|
||||||
|
totalItems: number
|
||||||
|
onPageChange: (page: number) => void
|
||||||
|
onPageSizeChange: (size: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PaginationControls({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
pageSize,
|
||||||
|
totalItems,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
}: PaginationControlsProps) {
|
||||||
|
const [jumpToPage, setJumpToPage] = useState("")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset jump input when page changes externally
|
||||||
|
setJumpToPage(currentPage.toString());
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
|
const handleJumpToPage = () => {
|
||||||
|
const page = parseInt(jumpToPage)
|
||||||
|
if (!isNaN(page) && page >= 1 && page <= totalPages) {
|
||||||
|
onPageChange(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJumpInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setJumpToPage(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJumpInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleJumpToPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInternalPageSizeChange = (size: string) => {
|
||||||
|
const newSize = parseInt(size)
|
||||||
|
onPageSizeChange(newSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Page Number Logic --- (Same as customer pool)
|
||||||
|
const MAX_VISIBLE_PAGES = 5;
|
||||||
|
let startPage = 1;
|
||||||
|
let endPage = totalPages;
|
||||||
|
|
||||||
|
if (totalPages > MAX_VISIBLE_PAGES) {
|
||||||
|
const halfVisible = Math.floor(MAX_VISIBLE_PAGES / 2);
|
||||||
|
if (currentPage <= halfVisible + 1) {
|
||||||
|
endPage = MAX_VISIBLE_PAGES;
|
||||||
|
} else if (currentPage >= totalPages - halfVisible) {
|
||||||
|
startPage = totalPages - MAX_VISIBLE_PAGES + 1;
|
||||||
|
} else {
|
||||||
|
startPage = currentPage - halfVisible;
|
||||||
|
endPage = currentPage + halfVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageNumbers = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
|
||||||
|
// --- End Page Number Logic ---
|
||||||
|
|
||||||
|
if (totalPages <= 0) {
|
||||||
|
return null; // Don't render pagination if there are no pages
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between flex-wrap gap-4 mt-4 px-2 py-2 border-t">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
共 {totalItems} 条记录
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap justify-center">
|
||||||
|
<Select value={pageSize.toString()} onValueChange={handleInternalPageSizeChange}>
|
||||||
|
<SelectTrigger className="w-[100px] h-9">
|
||||||
|
<SelectValue placeholder="每页条数" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="10">10 条/页</SelectItem>
|
||||||
|
<SelectItem value="30">30 条/页</SelectItem>
|
||||||
|
<SelectItem value="50">50 条/页</SelectItem>
|
||||||
|
<SelectItem value="100">100 条/页</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Page Numbers */}
|
||||||
|
{startPage > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() => onPageChange(1)}
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{startPage > 2 && (
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pageNumbers.map((page) => (
|
||||||
|
<Button
|
||||||
|
key={page}
|
||||||
|
variant={page === currentPage ? "default" : "outline"}
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() => onPageChange(page)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{endPage < totalPages -1 && (
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
)}
|
||||||
|
{endPage < totalPages && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() => onPageChange(totalPages)}
|
||||||
|
>
|
||||||
|
{totalPages}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* End Page Numbers */}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max={totalPages}
|
||||||
|
className="h-9 w-16"
|
||||||
|
placeholder="页码"
|
||||||
|
value={jumpToPage}
|
||||||
|
onChange={handleJumpInputChange}
|
||||||
|
onKeyDown={handleJumpInputKeyDown}
|
||||||
|
/>
|
||||||
|
<Button variant="outline" size="sm" className="h-9" onClick={handleJumpToPage}>跳转</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user