fix: paginate operational list pages

This commit is contained in:
hectorzhao
2026-07-28 23:20:13 +08:00
parent 87c5b1eccc
commit b8560372cc
40 changed files with 1208 additions and 422 deletions
+11 -13
View File
@@ -69,12 +69,15 @@ export function ClientApplicationsPage() {
const [paramsError, setParamsError] = useState('');
const [copied, setCopied] = useState(false);
const [copyError, setCopyError] = useState('');
const [total, setTotal] = useState(0);
const pageSize = 10;
function loadApplications() {
setLoading(true);
clientApi.listApplications()
.then((items) => {
setApplications(items.filter((item) => item.status !== 'deleted'));
clientApi.listApplicationsPage({ page, pageSize })
.then((result) => {
setApplications(result.items.filter((item) => item.status !== 'deleted'));
setTotal(result.total);
setError('');
})
.catch((reason: Error) => setError(reason.message || '短信应用加载失败'))
@@ -83,7 +86,7 @@ export function ClientApplicationsPage() {
useEffect(() => {
loadApplications();
}, []);
}, [page]);
function openParams(application: ClientSmsApplication) {
if (application.interfaceEnabled === false) return;
@@ -102,14 +105,9 @@ export function ClientApplicationsPage() {
}
const selectedRows = useMemo(() => params ? mapParams(params) : [], [params]);
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(applications.length / pageSize));
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleApplications = applications.slice((currentPage - 1) * pageSize, currentPage * pageSize);
useEffect(() => {
setPage(1);
}, [applications.length]);
const visibleApplications = applications;
function copyParams() {
if (selectedRows.length === 0) {
@@ -128,7 +126,7 @@ export function ClientApplicationsPage() {
<FileText size={22} />
</span>
<h1></h1>
<span className="muted"> {applications.length} </span>
<span className="muted"> {total} </span>
</div>
{loading ? <p className="muted">...</p> : null}
@@ -180,7 +178,7 @@ export function ClientApplicationsPage() {
totalPages={totalPages}
onPageChange={setPage}
previousDisabled={currentPage <= 1}
total={applications.length}
total={total}
/>
<Modal
+41 -33
View File
@@ -1,4 +1,4 @@
import { Fragment, useEffect, useMemo, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import { Clock3, Eye, FileText, Search, StopCircle } from 'lucide-react';
import {
Button,
@@ -99,6 +99,8 @@ export function ClientBatchTasksPage() {
const [tasks, setTasks] = useState<BatchTask[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [total, setTotal] = useState(0);
const [applications, setApplications] = useState<Array<{ id: string; name: string }>>([]);
const [keyword, setKeyword] = useState('');
const [application, setApplication] = useState('all');
const [submittedDateRange, setSubmittedDateRange] = useState<DateRangeValue>({});
@@ -106,11 +108,21 @@ export function ClientBatchTasksPage() {
const [page, setPage] = useState(1);
const [selectedTask, setSelectedTask] = useState<BatchTask | null>(null);
function loadTasks() {
const pageSize = 10;
function loadTasks(targetPage = page) {
setLoading(true);
clientApi.listBatchTasks()
.then((items) => {
setTasks(items.map(mapTask));
clientApi.listBatchTasksPage({
keyword: keyword.trim() || undefined,
applicationKeyword: application === 'all' ? undefined : application,
createdAtFrom: submittedDateRange.start,
createdAtTo: submittedDateRange.end,
page: targetPage,
pageSize,
})
.then((result) => {
setTasks(result.items.map(mapTask));
setTotal(result.total);
setError('');
})
.catch((reason: Error) => setError(reason.message || '批量任务加载失败'))
@@ -118,39 +130,28 @@ export function ClientBatchTasksPage() {
}
useEffect(() => {
loadTasks();
}, []);
const applicationOptions = useMemo(() => {
const names = Array.from(new Set(tasks.map((item) => item.applicationName)));
return [
{ label: '全部应用', value: 'all' },
...names.map((name) => ({ label: name, value: name })),
];
}, [tasks]);
const filteredTasks = tasks.filter((item) => {
const matchesKeyword = !keyword || item.id.includes(keyword);
const matchesApplication = application === 'all' || item.applicationName === application;
const submittedDate = item.submittedAt.slice(0, 10);
const matchesStartDate = !submittedDateRange.start || submittedDate >= submittedDateRange.start;
const matchesEndDate = !submittedDateRange.end || submittedDate <= submittedDateRange.end;
return matchesKeyword && matchesApplication && matchesStartDate && matchesEndDate;
});
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(filteredTasks.length / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleTasks = filteredTasks.slice((currentPage - 1) * pageSize, currentPage * pageSize);
loadTasks(page);
}, [page]);
useEffect(() => {
setPage(1);
}, [application, keyword, submittedDateRange.end, submittedDateRange.start, tasks.length]);
clientApi.listApplicationOptions()
.then((items) => setApplications(items.map((item) => ({ id: item.id, name: item.name }))))
.catch(() => undefined);
}, []);
const applicationOptions = [
{ label: '全部应用', value: 'all' },
...applications.map((item) => ({ label: item.name, value: item.name })),
];
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleTasks = tasks;
function terminateTask(id: string) {
const source = tasks.find((item) => item.id === id);
if (!source) return;
clientApi.cancelBatchTask(source.backendId)
.then(loadTasks)
.then(() => loadTasks(page))
.catch((reason: Error) => setError(reason.message || '发送批次终止失败'));
}
@@ -236,17 +237,24 @@ export function ClientBatchTasksPage() {
<QueryPanel
title="查询条件"
summary={<> <strong>{filteredTasks.length}</strong> </>}
summary={<> <strong>{total}</strong> </>}
>
<Input
label="发送批次号"
onChange={(event) => setKeyword(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
setPage(1);
loadTasks(1);
}
}}
placeholder="输入发送批次号搜索"
prefix={<Search size={16} />}
value={keyword}
/>
<Select label="选择应用" onChange={(event) => setApplication(event.target.value)} options={applicationOptions} value={application} />
<DateRangeInput label="提交时间" onChange={setSubmittedDateRange} value={submittedDateRange} />
<Button onClick={() => { setPage(1); loadTasks(1); }} variant="primary"></Button>
</QueryPanel>
<div className="surface batch-table-card">
@@ -301,7 +309,7 @@ export function ClientBatchTasksPage() {
totalPages={totalPages}
onPageChange={setPage}
previousDisabled={currentPage <= 1}
total={filteredTasks.length}
total={total}
/>
</div>
+10 -8
View File
@@ -12,23 +12,25 @@ export function ClientBillingPage() {
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [total, setTotal] = useState(0);
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(orders.length / pageSize));
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleOrders = orders.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const visibleOrders = orders;
useEffect(() => {
setLoading(true);
Promise.all([clientApi.getDashboard(), clientApi.listOrders()])
.then(([dashboard, nextOrders]) => {
Promise.all([clientApi.getDashboard(), clientApi.listOrdersPage({ page, pageSize })])
.then(([dashboard, result]) => {
setBalanceCents(dashboard.accounts[0]?.balanceCents ?? 0);
setCreditCents(dashboard.accounts[0]?.creditCents ?? 0);
setOrders(nextOrders);
setOrders(result.items);
setTotal(result.total);
setError('');
})
.catch((reason: Error) => setError(reason.message || '账户信息加载失败'))
.finally(() => setLoading(false));
}, []);
}, [page]);
return (
<section className="page-stack">
@@ -46,7 +48,7 @@ export function ClientBillingPage() {
</div>
</div>
<div className="surface section-stack">
<div className="section-heading"><div><h2></h2><p className="muted"></p></div><Tag tone="info">{orders.length} </Tag></div>
<div className="section-heading"><div><h2></h2><p className="muted"></p></div><Tag tone="info">{total} </Tag></div>
{loading ? <p className="muted">...</p> : null}
{error ? <p className="form-error">{error}</p> : null}
{!loading && !error ? (
@@ -67,7 +69,7 @@ export function ClientBillingPage() {
</table>
</div>
) : null}
<Pagination nextDisabled={currentPage >= totalPages} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} onPrevious={() => setPage((value) => Math.max(1, value - 1))} page={currentPage} totalPages={totalPages} onPageChange={setPage} previousDisabled={currentPage <= 1} total={orders.length} />
<Pagination nextDisabled={currentPage >= totalPages} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} onPrevious={() => setPage((value) => Math.max(1, value - 1))} page={currentPage} totalPages={totalPages} onPageChange={setPage} previousDisabled={currentPage <= 1} total={total} />
</div>
</section>
);
+27 -25
View File
@@ -60,18 +60,26 @@ export function ClientSendDetailPage() {
const [contentKeyword, setContentKeyword] = useState('');
const [phoneKeyword, setPhoneKeyword] = useState('');
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [applications, setApplications] = useState<Array<{ id: string; name: string }>>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
function loadData() {
function loadData(targetPage = page) {
setLoading(true);
clientApi.listMessages({
applicationId: applicationId === 'all' ? undefined : applicationId,
phoneNumber: phoneKeyword || undefined,
status: status === 'all' ? undefined : status,
contentKeyword: contentKeyword || undefined,
queuedAtFrom: dateRange.start || undefined,
queuedAtTo: dateRange.end || undefined,
page: targetPage,
pageSize: 10,
})
.then((items) => {
setRecords(items);
.then((result) => {
setRecords(result.items);
setTotal(result.total);
setError('');
})
.catch((reason: Error) => setError(reason.message || '短信发送详情加载失败'))
@@ -79,37 +87,31 @@ export function ClientSendDetailPage() {
}
useEffect(() => {
loadData();
}, [applicationId, phoneKeyword, status]);
loadData(page);
}, [applicationId, contentKeyword, dateRange.end, dateRange.start, page, phoneKeyword, status]);
useEffect(() => {
clientApi.listApplicationOptions()
.then((items) => setApplications(items.map((item) => ({ id: item.id, name: item.name }))))
.catch((reason: Error) => setError(reason.message || '应用列表加载失败'));
}, []);
const applicationOptions = useMemo(() => {
const applications = new Map<string, string>();
records.forEach((item) => {
if (item.applicationId) {
applications.set(item.applicationId, item.application?.name ?? item.applicationId);
}
});
return [
{ label: '全部应用', value: 'all' },
...Array.from(applications.entries()).map(([value, label]) => ({ label, value })),
...applications.map((item) => ({ label: item.name, value: item.id })),
];
}, [records]);
}, [applications]);
const filteredRows = records.filter((item) => {
const sentDate = getDate(item.queuedAt);
const matchesStartDate = !dateRange.start || sentDate >= dateRange.start;
const matchesEndDate = !dateRange.end || sentDate <= dateRange.end;
const matchesContent = !contentKeyword || item.content.includes(contentKeyword);
return matchesStartDate && matchesEndDate && matchesContent;
});
const filteredRows = records;
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleRows = filteredRows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const visibleRows = filteredRows;
useEffect(() => {
setPage(1);
}, [contentKeyword, dateRange.end, dateRange.start, records.length]);
}, [applicationId, contentKeyword, dateRange.end, dateRange.start, phoneKeyword, status]);
return (
<section className="page-stack">
@@ -122,7 +124,7 @@ export function ClientSendDetailPage() {
<QueryPanel
title="查询条件"
summary={<> <strong>{filteredRows.length}</strong> </>}
summary={<> <strong>{total}</strong> </>}
>
<Select label="应用名称" onChange={(event) => setApplicationId(event.target.value)} options={applicationOptions} value={applicationId} />
<DateRangeInput label="发送时间" onChange={setDateRange} value={dateRange} />
@@ -232,7 +234,7 @@ export function ClientSendDetailPage() {
totalPages={totalPages}
onPageChange={setPage}
previousDisabled={currentPage <= 1}
total={filteredRows.length}
total={total}
/>
</div>
</section>
+29 -19
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { ChevronDown, ChevronRight, Edit3, FileCheck2, Globe2, Plus, RotateCcw, Search, Trash2, Upload } from 'lucide-react';
import { Button, DeleteRiskAction, FileActions, Input, Modal, Pagination, Select, Tag, Textarea } from '@/components/ui';
import {
@@ -14,6 +14,9 @@ import { getSmsSignatureValidationError, hasForbiddenSmsSignatureCharacter, isCo
const EMPTY_WORKSPACE: ClientSignatureWorkspace = {
items: [],
summary: { total: 0, pending: 0, approved: 0, rejected: 0, draft: 0 },
total: 0,
page: 1,
pageSize: 10,
};
const statusTone: Record<string, 'success' | 'info' | 'danger' | 'warning'> = {
@@ -269,9 +272,20 @@ export function ClientSignaturesPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
function loadData() {
const pageSize = 10;
function loadData(targetPage = page) {
setLoading(true);
Promise.all([clientApi.listApplications(), clientApi.getSignatureWorkspace()])
Promise.all([
clientApi.listApplicationOptions(),
clientApi.getSignatureWorkspace({
keyword: keyword.trim() || undefined,
applicationId: applicationFilter || undefined,
status: statusFilter || undefined,
page: targetPage,
pageSize,
}),
])
.then(([applicationItems, signatureWorkspace]) => {
setApplications(applicationItems.filter((item) => item.status === 'active'));
setWorkspace(signatureWorkspace);
@@ -281,19 +295,15 @@ export function ClientSignaturesPage() {
.finally(() => setLoading(false));
}
useEffect(loadData, []);
useEffect(() => {
const timer = window.setTimeout(() => loadData(page), 300);
return () => window.clearTimeout(timer);
}, [applicationFilter, keyword, page, statusFilter]);
const filteredItems = useMemo(() => workspace.items.filter((item) => {
const matchesKeyword = !keyword.trim() || [item.name, item.purpose, item.application?.name].join(' ').toLowerCase().includes(keyword.trim().toLowerCase());
return matchesKeyword && (!applicationFilter || item.applicationId === applicationFilter) && (!statusFilter || item.auditStatus === statusFilter);
}), [applicationFilter, keyword, statusFilter, workspace.items]);
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(filteredItems.length / pageSize));
const filteredItems = workspace.items;
const totalPages = Math.max(1, Math.ceil(workspace.total / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleItems = filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize);
useEffect(() => setPage(1), [applicationFilter, keyword, statusFilter]);
const visibleItems = workspace.items;
function toggleExpanded(id: string) {
setExpandedIds((current) => {
@@ -314,7 +324,7 @@ export function ClientSignaturesPage() {
}
}
const resetFilters = () => { setKeyword(''); setApplicationFilter(''); setStatusFilter(''); };
const resetFilters = () => { setKeyword(''); setApplicationFilter(''); setStatusFilter(''); setPage(1); };
return <section className="page-stack client-signature-page">
<header className="client-signature-heading">
<div className="client-signature-title">
@@ -332,9 +342,9 @@ export function ClientSignaturesPage() {
</section>
<div className="client-signature-toolbar">
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索签名、用途或应用" prefix={<Search size={17} />} value={keyword} />
<Select onChange={(event) => setApplicationFilter(event.target.value)} options={[{ label: '全部应用', value: '' }, ...applications.map((item) => ({ label: item.name, value: item.id }))]} value={applicationFilter} />
<Select onChange={(event) => setStatusFilter(event.target.value)} options={[{ label: '全部状态', value: '' }, { label: '资料审核中', value: 'pending' }, { label: '审核通过', value: 'approved' }, { label: '需修改', value: 'rejected' }, { label: '待提交', value: 'draft' }]} value={statusFilter} />
<Input onChange={(event) => { setKeyword(event.target.value); setPage(1); }} placeholder="搜索签名、用途或应用" prefix={<Search size={17} />} value={keyword} />
<Select onChange={(event) => { setApplicationFilter(event.target.value); setPage(1); }} options={[{ label: '全部应用', value: '' }, ...applications.map((item) => ({ label: item.name, value: item.id }))]} value={applicationFilter} />
<Select onChange={(event) => { setStatusFilter(event.target.value); setPage(1); }} options={[{ label: '全部状态', value: '' }, { label: '资料审核中', value: 'pending' }, { label: '审核通过', value: 'approved' }, { label: '需修改', value: 'rejected' }, { label: '待提交', value: 'draft' }]} value={statusFilter} />
<Button icon={<RotateCcw size={15} />} onClick={resetFilters} variant="ghost"></Button>
</div>
@@ -377,7 +387,7 @@ export function ClientSignaturesPage() {
})}
</section>
<Pagination nextDisabled={currentPage >= totalPages} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} onPageChange={setPage} onPrevious={() => setPage((value) => Math.max(1, value - 1))} page={currentPage} previousDisabled={currentPage <= 1} total={filteredItems.length} totalPages={totalPages} />
<Pagination nextDisabled={currentPage >= totalPages} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} onPageChange={setPage} onPrevious={() => setPage((value) => Math.max(1, value - 1))} page={currentPage} previousDisabled={currentPage <= 1} total={workspace.total} totalPages={totalPages} />
{signatureModal ? <SignatureModal applications={applications} onClose={() => setSignatureModal(undefined)} onSaved={() => { setSignatureModal(undefined); loadData(); }} signature={signatureModal === 'new' ? undefined : signatureModal} /> : null}
{drainageModal ? <DrainageModal item={drainageModal.item} onClose={() => setDrainageModal(undefined)} onSaved={() => { setDrainageModal(undefined); loadData(); }} signature={drainageModal.signature} /> : null}
+16 -19
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Edit3, MessageSquare, Plus, Search, Trash2 } from 'lucide-react';
import { Button, DeleteRiskAction, Input, Modal, Pagination, Select, Tag, Textarea } from '@/components/ui';
import { clientApi, type ClientSmsApplication, type ClientSmsSignatureView, type ClientSmsTemplate } from '@/api/adminApi';
@@ -217,13 +217,16 @@ export function ClientTemplatesPage() {
const [loading, setLoading] = useState(true);
const [modalTemplate, setModalTemplate] = useState<ClientSmsTemplate | 'new' | null>(null);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 10;
function loadData() {
function loadData(targetPage = page) {
setLoading(true);
Promise.all([clientApi.listApplications(), clientApi.listTemplates({ includeHistory: true }), clientApi.listSignatures()])
.then(([applicationItems, templateItems, signatureItems]) => {
Promise.all([clientApi.listApplicationOptions(), clientApi.listTemplatesPage({ includeHistory: true, keyword: keyword.trim() || undefined, page: targetPage, pageSize }), clientApi.listSignatureOptions()])
.then(([applicationItems, templateResult, signatureItems]) => {
setApplications(applicationItems.filter((item) => item.status === 'active'));
setTemplates(templateItems.filter((item) => item.auditStatus !== 'deleted' && item.auditStatus !== 'disabled'));
setTemplates(templateResult.items.filter((item) => item.auditStatus !== 'deleted' && item.auditStatus !== 'disabled'));
setTotal(templateResult.total);
setSignatures(signatureItems);
setError('');
})
@@ -232,20 +235,14 @@ export function ClientTemplatesPage() {
}
useEffect(() => {
loadData();
}, []);
const timer = window.setTimeout(() => loadData(page), 300);
return () => window.clearTimeout(timer);
}, [page, keyword]);
const filteredTemplates = useMemo(() => templates.filter((item) => (
!keyword || [item.name, item.content, item.application?.name, item.signature?.name].join(' ').includes(keyword)
)), [keyword, templates]);
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(filteredTemplates.length / pageSize));
const filteredTemplates = templates;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleTemplates = filteredTemplates.slice((currentPage - 1) * pageSize, currentPage * pageSize);
useEffect(() => {
setPage(1);
}, [filteredTemplates.length, keyword]);
const visibleTemplates = templates;
async function saveTemplate(state: TemplateFormState) {
const existing = modalTemplate && modalTemplate !== 'new' ? modalTemplate : null;
@@ -282,7 +279,7 @@ export function ClientTemplatesPage() {
<div className="template-toolbar">
<Input
onChange={(event) => setKeyword(event.target.value)}
onChange={(event) => { setKeyword(event.target.value); setPage(1); }}
placeholder="搜索模板名称、应用、签名或内容"
prefix={<Search size={17} />}
value={keyword}
@@ -327,7 +324,7 @@ export function ClientTemplatesPage() {
totalPages={totalPages}
onPageChange={setPage}
previousDisabled={currentPage <= 1}
total={filteredTemplates.length}
total={total}
/>
{!loading && !error && filteredTemplates.length === 0 ? <p className="muted"></p> : null}
+20 -9
View File
@@ -8,6 +8,7 @@ import {
DetailSection,
Input,
Modal,
Pagination,
QueryPanel,
Table,
type DateRangeValue,
@@ -33,12 +34,16 @@ export function ClientUplinkMessagesPage() {
const [matching, setMatching] = useState(false);
const [error, setError] = useState('');
const [detailError, setDetailError] = useState('');
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const pageSize = 10;
function loadData() {
function loadData(targetPage = page) {
setLoading(true);
clientApi.listUplinkMessages({ phoneNumber: phoneKeyword || undefined, keyword: contentKeyword || undefined, startTime: dateRange.start ? `${dateRange.start}T00:00:00+08:00` : undefined, endTime: dateRange.end ? `${dateRange.end}T23:59:59+08:00` : undefined })
.then((items) => {
setMessages(items);
clientApi.listUplinkMessagesPage({ phoneNumber: phoneKeyword || undefined, keyword: contentKeyword || undefined, startTime: dateRange.start ? `${dateRange.start}T00:00:00+08:00` : undefined, endTime: dateRange.end ? `${dateRange.end}T23:59:59+08:00` : undefined, page: targetPage, pageSize })
.then((result) => {
setMessages(result.items);
setTotal(result.total);
setError('');
})
.catch((reason: Error) => setError(reason.message || '上行短信加载失败'))
@@ -55,17 +60,22 @@ export function ClientUplinkMessagesPage() {
}
setMatching(true);
clientApi.listMessages({ messageId: message.messageId })
.then((items) => setMatchedRecords(items))
clientApi.listMessages({ messageId: message.messageId, page: 1, pageSize: 10 })
.then((result) => setMatchedRecords(result.items))
.catch((reason: Error) => setDetailError(reason.message || '匹配发送记录加载失败'))
.finally(() => setMatching(false));
}
useEffect(() => {
const timer = window.setTimeout(loadData, 300);
setPage(1);
const timer = window.setTimeout(() => loadData(1), 300);
return () => window.clearTimeout(timer);
}, [phoneKeyword, contentKeyword, dateRange.start, dateRange.end]);
useEffect(() => {
if (page > 1) loadData(page);
}, [page]);
const columns = useMemo<Array<TableColumn<SmsUplinkMessage>>>(() => [
{ key: 'phoneNumber', title: '手机号码', width: '180px', render: (record) => <strong>{record.phoneNumber}</strong> },
{ key: 'receivedAt', title: '上行时间', width: '220px', render: (record) => <span className="muted">{getTime(record.receivedAt)}</span> },
@@ -92,7 +102,7 @@ export function ClientUplinkMessagesPage() {
<h1></h1>
</div>
<QueryPanel title="查询条件" summary={<> <strong>{messages.length}</strong> </>}>
<QueryPanel title="查询条件" summary={<> <strong>{total}</strong> </>}>
<Input
label="手机号码"
onChange={(event) => setPhoneKeyword(event.target.value)}
@@ -113,7 +123,8 @@ export function ClientUplinkMessagesPage() {
{error ? <p className="form-error">{error}</p> : null}
<div className="surface uplink-table-card">
<Table columns={columns} data={loading ? [] : messages} emptyText={loading ? '正在加载真实上行记录...' : '暂无上行记录'} rowKey="id" />
<Table columns={columns} data={loading ? [] : messages} emptyText={loading ? '正在加载真实上行记录...' : '暂无上行记录'} pagination={false} rowKey="id" />
<Pagination page={page} totalPages={Math.max(1, Math.ceil(total / pageSize))} total={total} onPageChange={setPage} onPrevious={() => setPage((value) => Math.max(1, value - 1))} onNext={() => setPage((value) => Math.min(Math.max(1, Math.ceil(total / pageSize)), value + 1))} previousDisabled={page <= 1} nextDisabled={page >= Math.max(1, Math.ceil(total / pageSize))} />
</div>
<Modal