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:
OuYingJun1024
2026-06-14 15:37:38 +08:00
parent a84c4570ba
commit de58e69933
6 changed files with 122 additions and 73 deletions
+11 -12
View File
@@ -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>
);
}
+11 -11
View File
@@ -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
+11 -11
View File
@@ -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)} />
+13 -39
View File
@@ -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
+1
View File
@@ -19,6 +19,7 @@ export interface LoginResponse {
export interface CursorPage<T> {
items: T[];
next_cursor: number | null;
total?: number | null; // offset 分页时为符合筛选条件的总条数(页码分页用);加载更多接口可能没有
}
export interface UserListItem {
+75
View File
@@ -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), // 写操作后刷新当前页
};
}