feat(admin-web): 主列表页改页码分页(usePagedList + antd pagination)
用户/提现/上报/审计日志四页从「加载更多」改为页码分页: - 新增 usePagedList hook:页码→offset、暴露 total,配 antd Table pagination; 与 useCursorList(加载更多)并存,详情子表/反馈页仍用后者 - CursorPage 类型加 total? - 四页接入 pagination(页码/跳页/共 N 条/showSizeChanger);提现页移除自定义 「每次加载」选择器,统一用 showSizeChanger(20/50/100) - 反馈页本轮不动(后端反馈接口在 feat/feedback-iteration 迭代中) tsc --noEmit 通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Button, Input, Space, Table, Tag } from 'antd';
|
||||
import { formatUtcTime } from '@/lib/format';
|
||||
import { useCursorList } from '@/lib/useCursorList';
|
||||
import { usePagedList } from '@/lib/usePagedList';
|
||||
import type { AuditLog } from '@/lib/types';
|
||||
|
||||
// 审计 created_at 为 UTC 口径(server_default now()),按北京显示(勿用 new Date().toLocaleString)
|
||||
@@ -13,11 +13,8 @@ const dt = (v: string) => formatUtcTime(v, 'YYYY-MM-DD HH:mm:ss');
|
||||
export default function AuditLogsPage() {
|
||||
const [action, setAction] = useState('');
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>({});
|
||||
const { items, nextCursor, loading, loadMore } = useCursorList<AuditLog>(
|
||||
'/admin/api/audit-logs',
|
||||
filters,
|
||||
50,
|
||||
);
|
||||
const { items, total, page, pageSize, loading, onChange: onPageChange } =
|
||||
usePagedList<AuditLog>('/admin/api/audit-logs', filters, 50);
|
||||
|
||||
const search = () => setFilters({ action: action || undefined });
|
||||
|
||||
@@ -61,14 +58,16 @@ export default function AuditLogsPage() {
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
<Button onClick={loadMore} disabled={!nextCursor} loading={loading}>
|
||||
{nextCursor ? '加载更多' : '没有更多了'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { canDo } from '@/lib/auth';
|
||||
import { useCursorList } from '@/lib/useCursorList';
|
||||
import { usePagedList } from '@/lib/usePagedList';
|
||||
import type { PriceReport, PriceReportSummary } from '@/lib/types';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -65,10 +65,8 @@ export default function PriceReportsPage() {
|
||||
const filters: Record<string, unknown> = {};
|
||||
if (activeStatus !== 'all') filters.status = activeStatus;
|
||||
|
||||
const { items, nextCursor, loading, loadMore, reload } = useCursorList<PriceReport>(
|
||||
'/admin/api/price-reports',
|
||||
filters,
|
||||
);
|
||||
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
||||
usePagedList<PriceReport>('/admin/api/price-reports', filters);
|
||||
|
||||
const canReview = canDo(['operator']);
|
||||
|
||||
@@ -277,14 +275,16 @@ export default function PriceReportsPage() {
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
scroll={{ x: 1100 }}
|
||||
/>
|
||||
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
<Button onClick={loadMore} disabled={!nextCursor} loading={loading}>
|
||||
{nextCursor ? '加载更多' : '没有更多了'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -10,7 +10,7 @@ import dayjs, { type Dayjs } from 'dayjs';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { canDo } from '@/lib/auth';
|
||||
import { formatUtcTime } from '@/lib/format';
|
||||
import { useCursorList } from '@/lib/useCursorList';
|
||||
import { usePagedList } from '@/lib/usePagedList';
|
||||
import { AdjustCashModal, AdjustCoinModal } from '@/components/AdjustBalanceModals';
|
||||
import type { UserListItem } from '@/lib/types';
|
||||
|
||||
@@ -37,10 +37,8 @@ export default function UsersPage() {
|
||||
|
||||
const filters: Record<string, unknown> = { ...applied, sort_by: sortBy, sort_order: sortOrder };
|
||||
const filterKey = JSON.stringify(filters);
|
||||
const { items, nextCursor, loading, loadMore, reload } = useCursorList<UserListItem>(
|
||||
'/admin/api/users',
|
||||
filters,
|
||||
);
|
||||
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
||||
usePagedList<UserListItem>('/admin/api/users', filters);
|
||||
|
||||
const canCoins = canDo(['finance']);
|
||||
const canStatus = canDo(['operator']);
|
||||
@@ -339,15 +337,17 @@ export default function UsersPage() {
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 个用户`,
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
scroll={{ x: 1160 }}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
<Button onClick={loadMore} disabled={!nextCursor} loading={loading}>
|
||||
{nextCursor ? '加载更多' : '没有更多了'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AdjustCoinModal user={coinUser} open={!!coinUser} onClose={() => setCoinUser(null)} />
|
||||
<AdjustCashModal user={cashUser} open={!!cashUser} onClose={() => setCashUser(null)} />
|
||||
|
||||
@@ -39,7 +39,7 @@ import dayjs, { type Dayjs } from 'dayjs';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { canDo } from '@/lib/auth';
|
||||
import { formatUtcTime, formatWallTime, utcDayjs, utcFromNow, yuan } from '@/lib/format';
|
||||
import { useCursorList } from '@/lib/useCursorList';
|
||||
import { usePagedList } from '@/lib/usePagedList';
|
||||
import type {
|
||||
AuditLog,
|
||||
CashTxn,
|
||||
@@ -181,7 +181,6 @@ export default function WithdrawsPage() {
|
||||
const [bulkRejecting, setBulkRejecting] = useState<WithdrawOrder[]>([]);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([]);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [searchDraft, setSearchDraft] = useState('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(null);
|
||||
@@ -202,11 +201,8 @@ export default function WithdrawsPage() {
|
||||
filters.sort_by = sortBy;
|
||||
filters.sort_order = sortOrder;
|
||||
const filterKey = JSON.stringify(filters);
|
||||
const { items, nextCursor, loading, loadMore, reload } = useCursorList<WithdrawOrder>(
|
||||
'/admin/api/withdraws',
|
||||
filters,
|
||||
pageSize,
|
||||
);
|
||||
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
||||
usePagedList<WithdrawOrder>('/admin/api/withdraws', filters, 20);
|
||||
const canManage = canDo(['finance']);
|
||||
const selectedOrders = items.filter((item) => selectedRowKeys.includes(item.id));
|
||||
const selectedReviewing = selectedOrders.filter((item) => item.status === 'reviewing');
|
||||
@@ -290,7 +286,7 @@ export default function WithdrawsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRowKeys([]);
|
||||
}, [filterKey, pageSize]);
|
||||
}, [filterKey, page, pageSize]);
|
||||
|
||||
const refreshAfterChange = async (outBillNo?: string) => {
|
||||
setSelectedRowKeys([]);
|
||||
@@ -887,31 +883,6 @@ export default function WithdrawsPage() {
|
||||
</Space>
|
||||
</Space>
|
||||
)}
|
||||
<Space
|
||||
wrap
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<Text type="secondary">
|
||||
当前已加载 {items.length} 条{nextCursor ? ',还有更多' : ',已到最后一页'}
|
||||
</Text>
|
||||
<Space>
|
||||
<Text type="secondary">每次加载</Text>
|
||||
<Select
|
||||
value={pageSize}
|
||||
style={{ width: 96 }}
|
||||
onChange={(value: number) => setPageSize(value)}
|
||||
options={[
|
||||
{ value: 20, label: '20 条' },
|
||||
{ value: 50, label: '50 条' },
|
||||
{ value: 100, label: '100 条' },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
rowSelection={
|
||||
@@ -928,18 +899,21 @@ export default function WithdrawsPage() {
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [20, 50, 100],
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
scroll={{ x: 1350 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => openDetail(record),
|
||||
style: { cursor: 'pointer' },
|
||||
})}
|
||||
/>
|
||||
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
<Button onClick={loadMore} disabled={!nextCursor} loading={loading}>
|
||||
{nextCursor ? `继续加载 ${pageSize} 条` : '没有更多了'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface LoginResponse {
|
||||
export interface CursorPage<T> {
|
||||
items: T[];
|
||||
next_cursor: number | null;
|
||||
total?: number | null; // offset 分页时为符合筛选条件的总条数(页码分页用);加载更多接口可能没有
|
||||
}
|
||||
|
||||
export interface UserListItem {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { api } from './api';
|
||||
import type { CursorPage } from './types';
|
||||
|
||||
/**
|
||||
* 页码分页列表 hook(对接后端 offset 分页 + total)。
|
||||
* 与 useCursorList(「加载更多」)并存:需要页码/跳页/共 N 条的主列表页用本 hook。
|
||||
*
|
||||
* 约定:后端 cursor 即 offset,本 hook 按 (page-1)*pageSize 计算;CursorPage.total 为符合
|
||||
* 筛选条件的总条数(供 antd Table pagination 渲染)。filters 变化自动回第 1 页重载。
|
||||
*/
|
||||
export function usePagedList<T>(
|
||||
url: string,
|
||||
filters: Record<string, unknown>,
|
||||
initialPageSize = 20,
|
||||
) {
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(initialPageSize);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const requestSeq = useRef(0);
|
||||
|
||||
const filtersKey = JSON.stringify(filters);
|
||||
|
||||
const load = useCallback(
|
||||
async (p: number, ps: number) => {
|
||||
const seq = requestSeq.current + 1;
|
||||
requestSeq.current = seq;
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = { ...JSON.parse(filtersKey), limit: ps, cursor: (p - 1) * ps };
|
||||
const { data } = await api.get<CursorPage<T>>(url, { params });
|
||||
if (requestSeq.current !== seq) return; // 丢弃过期响应(快速切页/筛选)
|
||||
setItems(data.items);
|
||||
setTotal(data.total ?? 0);
|
||||
} finally {
|
||||
if (requestSeq.current === seq) setLoading(false);
|
||||
}
|
||||
},
|
||||
[url, filtersKey],
|
||||
);
|
||||
|
||||
// 筛选变化:回第 1 页重载
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
load(1, pageSize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [load]);
|
||||
|
||||
// antd Table pagination.onChange(page, pageSize):页变或每页条数变都走这里
|
||||
const onChange = (p: number, ps: number) => {
|
||||
if (ps !== pageSize) {
|
||||
// 每页条数变 → 回第 1 页,避免越界空页
|
||||
setPageSize(ps);
|
||||
setPage(1);
|
||||
load(1, ps);
|
||||
} else {
|
||||
setPage(p);
|
||||
load(p, ps);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
loading,
|
||||
onChange,
|
||||
reload: () => load(page, pageSize), // 写操作后刷新当前页
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user