fix: paginate operational list pages
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user