feat: 订单列表支持分页查询,分页大小可选 20/50/100
- /api/orders/my 新增 page/page_size 参数,返回分页信息和全局状态统计 - 用户订单页:分页控件(首/上/下/末页)+ 分页大小选择,摘要卡片改为全局统计 - 管理员订单页:分页大小选择器(20/50/100)+ 首尾页跳转按钮
This commit is contained in:
@@ -43,6 +43,7 @@ function AdminContent() {
|
|||||||
const [orders, setOrders] = useState<AdminOrder[]>([]);
|
const [orders, setOrders] = useState<AdminOrder[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -54,7 +55,7 @@ function AdminContent() {
|
|||||||
if (!token) return;
|
if (!token) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ token, page: String(page), page_size: '20' });
|
const params = new URLSearchParams({ token, page: String(page), page_size: String(pageSize) });
|
||||||
if (statusFilter) params.set('status', statusFilter);
|
if (statusFilter) params.set('status', statusFilter);
|
||||||
|
|
||||||
const res = await fetch(`/api/admin/orders?${params}`);
|
const res = await fetch(`/api/admin/orders?${params}`);
|
||||||
@@ -75,7 +76,7 @@ function AdminContent() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [token, page, statusFilter]);
|
}, [token, page, pageSize, statusFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOrders();
|
fetchOrders();
|
||||||
@@ -198,14 +199,36 @@ function AdminContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 text-sm text-gray-500">
|
||||||
<div className="mt-4 flex items-center justify-between text-sm text-gray-500">
|
<div className="flex items-center gap-2">
|
||||||
<span>共 {total} 条记录</span>
|
<span>共 {total} 条,每页</span>
|
||||||
<div className="flex gap-2">
|
{[20, 50, 100].map((s) => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
onClick={() => { setPageSize(s); setPage(1); }}
|
||||||
|
className={`rounded border px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||||
|
pageSize === s
|
||||||
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||||
|
: 'border-gray-300 text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(1)}
|
||||||
|
disabled={page <= 1}
|
||||||
|
className="rounded border px-2.5 py-1 disabled:opacity-40 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
««
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={page <= 1}
|
disabled={page <= 1}
|
||||||
className="rounded border px-3 py-1 disabled:opacity-50"
|
className="rounded border px-3 py-1 disabled:opacity-40 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
上一页
|
上一页
|
||||||
</button>
|
</button>
|
||||||
@@ -215,13 +238,20 @@ function AdminContent() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||||
disabled={page >= totalPages}
|
disabled={page >= totalPages}
|
||||||
className="rounded border px-3 py-1 disabled:opacity-50"
|
className="rounded border px-3 py-1 disabled:opacity-40 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
下一页
|
下一页
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(totalPages)}
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
className="rounded border px-2.5 py-1 disabled:opacity-40 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
»»
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Order Detail */}
|
{/* Order Detail */}
|
||||||
{detailOrder && <OrderDetail order={detailOrder} onClose={() => setDetailOrder(null)} />}
|
{detailOrder && <OrderDetail order={detailOrder} onClose={() => setDetailOrder(null)} />}
|
||||||
|
|||||||
@@ -3,28 +3,44 @@ import { prisma } from '@/lib/db';
|
|||||||
import { getCurrentUserByToken } from '@/lib/sub2api/client';
|
import { getCurrentUserByToken } from '@/lib/sub2api/client';
|
||||||
import { deriveOrderState, isRechargeRetryable } from '@/lib/order/status';
|
import { deriveOrderState, isRechargeRetryable } from '@/lib/order/status';
|
||||||
|
|
||||||
|
const VALID_PAGE_SIZES = [20, 50, 100];
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const token = request.nextUrl.searchParams.get('token')?.trim();
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const token = searchParams.get('token')?.trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return NextResponse.json({ error: 'token is required' }, { status: 400 });
|
return NextResponse.json({ error: 'token is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const page = Math.max(1, Number(searchParams.get('page') || '1'));
|
||||||
|
const rawPageSize = Number(searchParams.get('page_size') || '20');
|
||||||
|
const pageSize = VALID_PAGE_SIZES.includes(rawPageSize) ? rawPageSize : 20;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await getCurrentUserByToken(token);
|
const user = await getCurrentUserByToken(token);
|
||||||
const orders = await prisma.order.findMany({
|
const where = { userId: user.id };
|
||||||
where: { userId: user.id },
|
|
||||||
orderBy: { createdAt: 'desc' },
|
const [orders, total, statusGroups] = await Promise.all([
|
||||||
take: 20,
|
prisma.order.findMany({
|
||||||
select: {
|
where,
|
||||||
id: true,
|
orderBy: { createdAt: 'desc' },
|
||||||
amount: true,
|
skip: (page - 1) * pageSize,
|
||||||
status: true,
|
take: pageSize,
|
||||||
paymentType: true,
|
select: {
|
||||||
createdAt: true,
|
id: true,
|
||||||
paidAt: true,
|
amount: true,
|
||||||
completedAt: true,
|
status: true,
|
||||||
},
|
paymentType: true,
|
||||||
});
|
createdAt: true,
|
||||||
|
paidAt: true,
|
||||||
|
completedAt: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.order.count({ where }),
|
||||||
|
prisma.order.groupBy({ by: ['status'], where, _count: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sc = Object.fromEntries(statusGroups.map((g) => [g.status, g._count]));
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
user: {
|
user: {
|
||||||
@@ -48,6 +64,15 @@ export async function GET(request: NextRequest) {
|
|||||||
rechargeRetryable: isRechargeRetryable(item),
|
rechargeRetryable: isRechargeRetryable(item),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
summary: {
|
||||||
|
total,
|
||||||
|
pending: sc['PENDING'] || 0,
|
||||||
|
completed: (sc['COMPLETED'] || 0) + (sc['PAID'] || 0) + (sc['RECHARGING'] || 0),
|
||||||
|
failed: (sc['FAILED'] || 0) + (sc['CANCELLED'] || 0) + (sc['EXPIRED'] || 0),
|
||||||
|
},
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
total_pages: Math.ceil(total / pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get my orders error:', error);
|
console.error('Get my orders error:', error);
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
import PayPageLayout from '@/components/PayPageLayout';
|
import PayPageLayout from '@/components/PayPageLayout';
|
||||||
import OrderFilterBar from '@/components/OrderFilterBar';
|
import OrderFilterBar from '@/components/OrderFilterBar';
|
||||||
import OrderSummaryCards from '@/components/OrderSummaryCards';
|
import OrderSummaryCards from '@/components/OrderSummaryCards';
|
||||||
import OrderTable from '@/components/OrderTable';
|
import OrderTable from '@/components/OrderTable';
|
||||||
import { detectDeviceIsMobile, type UserInfo, type MyOrder, type OrderStatusFilter } from '@/lib/pay-utils';
|
import { detectDeviceIsMobile, type UserInfo, type MyOrder, type OrderStatusFilter } from '@/lib/pay-utils';
|
||||||
|
|
||||||
|
const PAGE_SIZE_OPTIONS = [20, 50, 100];
|
||||||
|
|
||||||
|
interface Summary {
|
||||||
|
total: number;
|
||||||
|
pending: number;
|
||||||
|
completed: number;
|
||||||
|
failed: number;
|
||||||
|
}
|
||||||
|
|
||||||
function OrdersContent() {
|
function OrdersContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const userId = Number(searchParams.get('user_id'));
|
const userId = Number(searchParams.get('user_id'));
|
||||||
@@ -20,18 +29,22 @@ function OrdersContent() {
|
|||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||||
const [orders, setOrders] = useState<MyOrder[]>([]);
|
const [orders, setOrders] = useState<MyOrder[]>([]);
|
||||||
|
const [summary, setSummary] = useState<Summary>({ total: 0, pending: 0, completed: 0, failed: 0 });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [activeFilter, setActiveFilter] = useState<OrderStatusFilter>('ALL');
|
const [activeFilter, setActiveFilter] = useState<OrderStatusFilter>('ALL');
|
||||||
const [resolvedUserId, setResolvedUserId] = useState<number | null>(null);
|
const [resolvedUserId, setResolvedUserId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(20);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
const isEmbedded = uiMode === 'embedded' && isIframeContext;
|
const isEmbedded = uiMode === 'embedded' && isIframeContext;
|
||||||
const hasToken = token.length > 0;
|
const hasToken = token.length > 0;
|
||||||
const effectiveUserId = resolvedUserId || userId;
|
const effectiveUserId = resolvedUserId || userId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
setIsIframeContext(window.self !== window.top);
|
setIsIframeContext(window.self !== window.top);
|
||||||
setIsMobile(detectDeviceIsMobile());
|
setIsMobile(detectDeviceIsMobile());
|
||||||
}, []);
|
}, []);
|
||||||
@@ -52,7 +65,7 @@ function OrdersContent() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isMobile, isEmbedded, userId, token, theme, uiMode]);
|
}, [isMobile, isEmbedded, userId, token, theme, uiMode]);
|
||||||
|
|
||||||
const loadOrders = async () => {
|
const loadOrders = async (targetPage = page, targetPageSize = pageSize) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
@@ -70,7 +83,12 @@ function OrdersContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const meRes = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}`);
|
const params = new URLSearchParams({
|
||||||
|
token,
|
||||||
|
page: String(targetPage),
|
||||||
|
page_size: String(targetPageSize),
|
||||||
|
});
|
||||||
|
const meRes = await fetch(`/api/orders/my?${params}`);
|
||||||
if (!meRes.ok) {
|
if (!meRes.ok) {
|
||||||
if (meRes.status === 401) {
|
if (meRes.status === 401) {
|
||||||
setError('登录态已失效,请从 Sub2API 重新进入支付页。');
|
setError('登录态已失效,请从 Sub2API 重新进入支付页。');
|
||||||
@@ -84,9 +102,7 @@ function OrdersContent() {
|
|||||||
const meData = await meRes.json();
|
const meData = await meRes.json();
|
||||||
const meUser = meData.user || {};
|
const meUser = meData.user || {};
|
||||||
const meId = Number(meUser.id);
|
const meId = Number(meUser.id);
|
||||||
if (Number.isInteger(meId) && meId > 0) {
|
if (Number.isInteger(meId) && meId > 0) setResolvedUserId(meId);
|
||||||
setResolvedUserId(meId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUserInfo({
|
setUserInfo({
|
||||||
id: Number.isInteger(meId) && meId > 0 ? meId : userId,
|
id: Number.isInteger(meId) && meId > 0 ? meId : userId,
|
||||||
@@ -97,11 +113,10 @@ function OrdersContent() {
|
|||||||
balance: typeof meUser.balance === 'number' ? meUser.balance : 0,
|
balance: typeof meUser.balance === 'number' ? meUser.balance : 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Array.isArray(meData.orders)) {
|
setOrders(Array.isArray(meData.orders) ? meData.orders : []);
|
||||||
setOrders(meData.orders);
|
setSummary(meData.summary ?? { total: 0, pending: 0, completed: 0, failed: 0 });
|
||||||
} else {
|
setPage(meData.page ?? targetPage);
|
||||||
setOrders([]);
|
setTotalPages(meData.total_pages ?? 1);
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
setOrders([]);
|
setOrders([]);
|
||||||
setError('网络错误,请稍后重试。');
|
setError('网络错误,请稍后重试。');
|
||||||
@@ -112,22 +127,23 @@ function OrdersContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile && !isEmbedded) return;
|
if (isMobile && !isEmbedded) return;
|
||||||
loadOrders();
|
loadOrders(1, pageSize);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [userId, token, isMobile, isEmbedded]);
|
}, [userId, token, isMobile, isEmbedded]);
|
||||||
|
|
||||||
const filteredOrders = useMemo(() => {
|
const handlePageSizeChange = (newSize: number) => {
|
||||||
if (activeFilter === 'ALL') return orders;
|
setPageSize(newSize);
|
||||||
return orders.filter((item) => item.status === activeFilter);
|
setPage(1);
|
||||||
}, [orders, activeFilter]);
|
loadOrders(1, newSize);
|
||||||
|
};
|
||||||
|
|
||||||
const summary = useMemo(() => {
|
const handlePageChange = (newPage: number) => {
|
||||||
const total = orders.length;
|
setPage(newPage);
|
||||||
const pending = orders.filter((item) => item.status === 'PENDING').length;
|
loadOrders(newPage, pageSize);
|
||||||
const completed = orders.filter((item) => item.status === 'COMPLETED' || item.status === 'PAID').length;
|
};
|
||||||
const failed = orders.filter((item) => ['FAILED', 'CANCELLED', 'EXPIRED'].includes(item.status)).length;
|
|
||||||
return { total, pending, completed, failed };
|
const filteredOrders =
|
||||||
}, [orders]);
|
activeFilter === 'ALL' ? orders : orders.filter((item) => item.status === activeFilter);
|
||||||
|
|
||||||
const buildScopedUrl = (path: string) => {
|
const buildScopedUrl = (path: string) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -140,6 +156,13 @@ function OrdersContent() {
|
|||||||
|
|
||||||
const payUrl = buildScopedUrl('/pay');
|
const payUrl = buildScopedUrl('/pay');
|
||||||
|
|
||||||
|
const btnClass = [
|
||||||
|
'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors',
|
||||||
|
isDark
|
||||||
|
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
|
||||||
|
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -169,27 +192,10 @@ function OrdersContent() {
|
|||||||
subtitle={userInfo?.username || `用户 #${effectiveUserId}`}
|
subtitle={userInfo?.username || `用户 #${effectiveUserId}`}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<button
|
<button type="button" onClick={() => loadOrders(page, pageSize)} className={btnClass}>
|
||||||
type="button"
|
|
||||||
onClick={loadOrders}
|
|
||||||
className={[
|
|
||||||
'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors',
|
|
||||||
isDark
|
|
||||||
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
|
|
||||||
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
刷新
|
刷新
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a href={payUrl} className={btnClass}>
|
||||||
href={payUrl}
|
|
||||||
className={[
|
|
||||||
'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors',
|
|
||||||
isDark
|
|
||||||
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
|
|
||||||
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
返回充值
|
返回充值
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
@@ -197,11 +203,90 @@ function OrdersContent() {
|
|||||||
>
|
>
|
||||||
<OrderSummaryCards isDark={isDark} summary={summary} />
|
<OrderSummaryCards isDark={isDark} summary={summary} />
|
||||||
|
|
||||||
<div className="mb-4">
|
{/* 过滤 + 分页大小 */}
|
||||||
<OrderFilterBar isDark={isDark} activeFilter={activeFilter} onChange={setActiveFilter} />
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<OrderFilterBar isDark={isDark} activeFilter={activeFilter} onChange={(f) => setActiveFilter(f)} />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>每页</span>
|
||||||
|
{PAGE_SIZE_OPTIONS.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePageSizeChange(s)}
|
||||||
|
className={[
|
||||||
|
'rounded border px-2 py-1 text-xs font-medium transition-colors',
|
||||||
|
pageSize === s
|
||||||
|
? isDark
|
||||||
|
? 'border-indigo-400 bg-indigo-500/20 text-indigo-200'
|
||||||
|
: 'border-indigo-400 bg-indigo-50 text-indigo-700'
|
||||||
|
: isDark
|
||||||
|
? 'border-slate-600 text-slate-300 hover:bg-slate-800'
|
||||||
|
: 'border-slate-300 text-slate-600 hover:bg-slate-100',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrderTable isDark={isDark} loading={loading} error={error} orders={filteredOrders} />
|
<OrderTable isDark={isDark} loading={loading} error={error} orders={filteredOrders} />
|
||||||
|
|
||||||
|
{/* 分页控件 */}
|
||||||
|
{!loading && !error && totalPages > 1 && (
|
||||||
|
<div className="mt-4 flex items-center justify-between text-xs">
|
||||||
|
<span className={isDark ? 'text-slate-400' : 'text-slate-500'}>
|
||||||
|
共 {summary.total} 条,第 {page} / {totalPages} 页
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePageChange(1)}
|
||||||
|
disabled={page <= 1}
|
||||||
|
className={[
|
||||||
|
'rounded border px-2 py-1 transition-colors disabled:opacity-40',
|
||||||
|
isDark ? 'border-slate-600 text-slate-300 hover:bg-slate-800' : 'border-slate-300 text-slate-600 hover:bg-slate-100',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
««
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePageChange(page - 1)}
|
||||||
|
disabled={page <= 1}
|
||||||
|
className={[
|
||||||
|
'rounded border px-2 py-1 transition-colors disabled:opacity-40',
|
||||||
|
isDark ? 'border-slate-600 text-slate-300 hover:bg-slate-800' : 'border-slate-300 text-slate-600 hover:bg-slate-100',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePageChange(page + 1)}
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
className={[
|
||||||
|
'rounded border px-2 py-1 transition-colors disabled:opacity-40',
|
||||||
|
isDark ? 'border-slate-600 text-slate-300 hover:bg-slate-800' : 'border-slate-300 text-slate-600 hover:bg-slate-100',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePageChange(totalPages)}
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
className={[
|
||||||
|
'rounded border px-2 py-1 transition-colors disabled:opacity-40',
|
||||||
|
isDark ? 'border-slate-600 text-slate-300 hover:bg-slate-800' : 'border-slate-300 text-slate-600 hover:bg-slate-100',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
»»
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PayPageLayout>
|
</PayPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user