fix: paginate operational list pages
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CheckCircle2, Copy, Eye, ExternalLink, FileText, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { adminApi, type AdminChannel, type ChannelConnectionLogResponse, type ChannelQualityStat, type ChannelTestResponse, type CmppConnectionState } from '@/api/adminApi';
|
||||
@@ -497,42 +497,35 @@ export function AdminChannelsPage() {
|
||||
const [logState, setLogState] = useState<ChannelLogState | null>(null);
|
||||
const [logKeyword, setLogKeyword] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
function loadChannels() {
|
||||
Promise.all([adminApi.listChannels(), adminApi.getSendQuality()])
|
||||
.then(async ([items, quality]) => {
|
||||
const visibleChannels = items.filter((item) => item.status !== 'deleted');
|
||||
function loadChannels(targetPage = page, filters = { keyword, carrier, status }) {
|
||||
Promise.all([
|
||||
adminApi.listChannelsPage({ keyword: filters.keyword.trim() || undefined, carrier: filters.carrier, status: filters.status, page: targetPage, pageSize }),
|
||||
adminApi.getSendQuality(),
|
||||
])
|
||||
.then(async ([result, quality]) => {
|
||||
const visibleChannels = result.items;
|
||||
const connections = await Promise.all(visibleChannels.map((channel) =>
|
||||
adminApi.listChannelConnections(channel.id).catch(() => [] as CmppConnectionState[]),
|
||||
));
|
||||
const qualityByChannel = new Map(quality.channels.map((item) => [item.channelId, item]));
|
||||
setChannels(visibleChannels.map((item, index) => mapApiChannel(item, connections[index], qualityByChannel.get(item.id))));
|
||||
setTotal(result.total);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '通道列表加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadChannels();
|
||||
}, []);
|
||||
loadChannels(page);
|
||||
}, [page]);
|
||||
|
||||
const filteredChannels = useMemo(
|
||||
() => channels.filter((channel) => {
|
||||
const matchesKeyword = !keyword || channel.name.includes(keyword);
|
||||
const matchesCarrier = carrier === 'all' || channel.carrier === carrier;
|
||||
const matchesStatus = status === 'all' || channel.status === status;
|
||||
return matchesKeyword && matchesCarrier && matchesStatus;
|
||||
}),
|
||||
[carrier, channels, keyword, status],
|
||||
);
|
||||
const totalPages = Math.max(1, Math.ceil(filteredChannels.length / pageSize));
|
||||
const filteredChannels = channels;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const visibleChannels = filteredChannels.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [carrier, channels.length, keyword, status]);
|
||||
const visibleChannels = channels;
|
||||
|
||||
async function upsertChannel(nextChannel: SmsChannel) {
|
||||
try {
|
||||
@@ -617,8 +610,8 @@ export function AdminChannelsPage() {
|
||||
<Select label="运营商" onChange={(event) => setCarrier(event.target.value)} options={carrierOptions} value={carrier} />
|
||||
<Select label="当前状态" onChange={(event) => setStatus(event.target.value)} options={statusOptions} value={status} />
|
||||
<div className="audit-filter-actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => void loadChannels()}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setCarrier('all'); setStatus('all'); }} variant="ghost">重置</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { setPage(1); void loadChannels(1); }}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setCarrier('all'); setStatus('all'); setPage(1); void loadChannels(1, { keyword: '', carrier: 'all', status: 'all' }); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -677,7 +670,7 @@ export function AdminChannelsPage() {
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={filteredChannels.length}
|
||||
total={total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Copy, Edit3, Plus, Search, Settings2, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { adminApi, type ApplicationCmppParams, type ApplicationDeactivationPreview, type CmppDownstreamConnection, type EnterpriseApplication, type HttpApiConfigResponse, type TenantOption } from '@/api/adminApi';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import { formatAmount, moneyUnitsToYuan } from '@/utils/currency';
|
||||
@@ -316,6 +316,9 @@ function CmppConnectionModal({
|
||||
export function AdminEnterpriseApplicationsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [smsApps, setSmsApps] = useState<SmsApp[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
|
||||
const [applicationKeyword, setApplicationKeyword] = useState('');
|
||||
@@ -339,10 +342,11 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
>(null);
|
||||
const [deactivateAction, setDeactivateAction] = useState<{ app: SmsApp; preview: ApplicationDeactivationPreview } | null>(null);
|
||||
|
||||
async function loadSmsApps(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, status: appliedStatus }) {
|
||||
async function loadSmsApps(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, status: appliedStatus }, targetPage = page) {
|
||||
try {
|
||||
const applications = await adminApi.listEnterpriseApplications(filters);
|
||||
setSmsApps(applications.map(mapApplication));
|
||||
const result = await adminApi.listEnterpriseApplicationsPage({ ...filters, page: targetPage, pageSize });
|
||||
setSmsApps(result.items.map(mapApplication));
|
||||
setTotal(result.total);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setSmsApps([]);
|
||||
@@ -351,8 +355,8 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadSmsApps();
|
||||
}, [appliedApplicationKeyword, appliedEnterpriseKeyword, appliedStatus]);
|
||||
void loadSmsApps(undefined, page);
|
||||
}, [page]);
|
||||
|
||||
async function openAddModal() {
|
||||
setAddModalOpen(true);
|
||||
@@ -441,12 +445,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredSmsApps = useMemo(
|
||||
() => smsApps.filter((item) => (!appliedEnterpriseKeyword || item.enterprise.includes(appliedEnterpriseKeyword))
|
||||
&& (!appliedApplicationKeyword || item.name.includes(appliedApplicationKeyword))
|
||||
&& (appliedStatus === 'all' || item.status === appliedStatus)),
|
||||
[appliedApplicationKeyword, appliedEnterpriseKeyword, appliedStatus, smsApps],
|
||||
);
|
||||
const filteredSmsApps = smsApps;
|
||||
|
||||
const smsColumns = useMemo<Array<TableColumn<SmsApp>>>(() => [
|
||||
{ key: 'name', title: '应用名称', width: '180px', render: (record) => <strong>{record.name}</strong> },
|
||||
@@ -524,8 +523,8 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
value={status}
|
||||
/>
|
||||
<div className="admin-split-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => { const filters = { enterpriseKeyword: enterpriseKeyword.trim(), applicationKeyword: applicationKeyword.trim(), status }; setAppliedEnterpriseKeyword(filters.enterpriseKeyword); setAppliedApplicationKeyword(filters.applicationKeyword); setAppliedStatus(filters.status); void loadSmsApps(filters); }}>查询</Button>
|
||||
<Button onClick={() => { const filters = { enterpriseKeyword: '', applicationKeyword: '', status: 'all' }; setEnterpriseKeyword(''); setApplicationKeyword(''); setStatus('all'); setAppliedEnterpriseKeyword(''); setAppliedApplicationKeyword(''); setAppliedStatus('all'); void loadSmsApps(filters); }} variant="ghost">重置</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { const filters = { enterpriseKeyword: enterpriseKeyword.trim(), applicationKeyword: applicationKeyword.trim(), status }; setAppliedEnterpriseKeyword(filters.enterpriseKeyword); setAppliedApplicationKeyword(filters.applicationKeyword); setAppliedStatus(filters.status); setPage(1); void loadSmsApps(filters, 1); }}>查询</Button>
|
||||
<Button onClick={() => { const filters = { enterpriseKeyword: '', applicationKeyword: '', status: 'all' }; setEnterpriseKeyword(''); setApplicationKeyword(''); setStatus('all'); setAppliedEnterpriseKeyword(''); setAppliedApplicationKeyword(''); setAppliedStatus('all'); setPage(1); void loadSmsApps(filters, 1); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -534,7 +533,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
<div className="surface section-stack">
|
||||
<Tabs
|
||||
items={[
|
||||
{ label: '短信应用', value: 'sms', content: <Table columns={smsColumns} data={filteredSmsApps} rowKey="id" /> },
|
||||
{ label: '短信应用', value: 'sms', content: <><Table columns={smsColumns} data={filteredSmsApps} 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))} /></> },
|
||||
{ label: '彩信应用', value: 'mms', pending: true, content: <div className="ui-table__empty">彩信应用待后端能力确认,本页不展示演示数据。</div> },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ChevronDown, ChevronRight, Edit3, FileSpreadsheet, FileText, Info, Plus, Search, Trash2, Upload } from 'lucide-react';
|
||||
import { adminApi, type ApplicationReportField, type ClientSmsApplication, type ClientSmsSignature, type FileRef, type TenantOption } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DeleteRiskAction, FileActions, Input, Modal, Pagination, Select, Tabs, Tag, Textarea } from '@/components/ui';
|
||||
@@ -614,19 +614,23 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const [signatureReport, setSignatureReport] = useState<ClientSmsSignature | null>(null);
|
||||
const [reportStatusTarget, setReportStatusTarget] = useState<ClientSmsSignature | null>(null);
|
||||
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, signatureKeyword: appliedSignatureKeyword, drainageKeyword: appliedDrainageKeyword }) {
|
||||
const pageSize = 10;
|
||||
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, signatureKeyword: appliedSignatureKeyword, drainageKeyword: appliedDrainageKeyword }, targetPage = page) {
|
||||
try {
|
||||
const [signatureItems, tenantItems, applicationItems] = await Promise.all([
|
||||
adminApi.listEnterpriseSignatures(filters),
|
||||
const [signatureResult, tenantItems, applicationItems] = await Promise.all([
|
||||
adminApi.listEnterpriseSignaturesPage({ ...filters, page: targetPage, pageSize }),
|
||||
adminApi.listTenants(),
|
||||
adminApi.listEnterpriseApplications(),
|
||||
adminApi.listEnterpriseApplicationOptions(),
|
||||
]);
|
||||
setSignatures(signatureItems);
|
||||
setSignatures(signatureResult.items);
|
||||
setTotal(signatureResult.total);
|
||||
setTenants(tenantItems.filter((tenant) => tenant.status !== 'deleted'));
|
||||
setApplications(applicationItems);
|
||||
setError('');
|
||||
@@ -636,26 +640,13 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, []);
|
||||
void loadData(undefined, page);
|
||||
}, [page]);
|
||||
|
||||
const filteredSignatures = useMemo(() => signatures.filter((item) => {
|
||||
const enterprise = item.tenant?.name ?? item.tenantId;
|
||||
const application = item.application?.name ?? '';
|
||||
const drainageItems = readDrainagePayload(item).links;
|
||||
return (!appliedEnterpriseKeyword || enterprise.includes(appliedEnterpriseKeyword))
|
||||
&& (!appliedApplicationKeyword || application.includes(appliedApplicationKeyword))
|
||||
&& (!appliedSignatureKeyword || item.name.includes(appliedSignatureKeyword) || (item.purpose ?? '').includes(appliedSignatureKeyword))
|
||||
&& (!appliedDrainageKeyword || drainageItems.some((drainage) => `${drainage.siteName} ${drainage.url} ${drainage.remark}`.includes(appliedDrainageKeyword)));
|
||||
}), [appliedApplicationKeyword, appliedDrainageKeyword, appliedEnterpriseKeyword, appliedSignatureKeyword, signatures]);
|
||||
const pageSize = 10;
|
||||
const totalPages = Math.max(1, Math.ceil(filteredSignatures.length / pageSize));
|
||||
const filteredSignatures = signatures;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const visibleSignatures = filteredSignatures.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [appliedApplicationKeyword, appliedDrainageKeyword, appliedEnterpriseKeyword, appliedSignatureKeyword, filteredSignatures.length]);
|
||||
const visibleSignatures = signatures;
|
||||
|
||||
async function saveSignature(state: SignatureFormState) {
|
||||
const existing = signatureModal && signatureModal !== 'new' ? signatureModal : null;
|
||||
@@ -792,7 +783,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={filteredSignatures.length}
|
||||
total={total}
|
||||
/>
|
||||
{filteredSignatures.length === 0 ? <div className="ui-table__empty">暂无企业签名</div> : null}
|
||||
</div>
|
||||
@@ -823,7 +814,8 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
setAppliedApplicationKeyword(filters.applicationKeyword);
|
||||
setAppliedSignatureKeyword(filters.signatureKeyword);
|
||||
setAppliedDrainageKeyword(filters.drainageKeyword);
|
||||
void loadData(filters);
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}}>查询</Button>
|
||||
<Button onClick={() => {
|
||||
const filters = { enterpriseKeyword: '', applicationKeyword: '', signatureKeyword: '', drainageKeyword: '' };
|
||||
@@ -835,7 +827,8 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
setAppliedApplicationKeyword('');
|
||||
setAppliedSignatureKeyword('');
|
||||
setAppliedDrainageKeyword('');
|
||||
void loadData(filters);
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Edit3, Eye, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { adminApi, type ClientSmsApplication, type ClientSmsSignature, type ClientSmsTemplate, type TenantOption } from '@/api/adminApi';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
@@ -289,6 +289,7 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
const [templateModal, setTemplateModal] = useState<ClientSmsTemplate | 'new' | null>(null);
|
||||
const [templatePreview, setTemplatePreview] = useState<ClientSmsTemplate | null>(null);
|
||||
const [templates, setTemplates] = useState<ClientSmsTemplate[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [templateNameKeyword, setTemplateNameKeyword] = useState('');
|
||||
const [appliedTemplateNameKeyword, setAppliedTemplateNameKeyword] = useState('');
|
||||
const [templateContentKeyword, setTemplateContentKeyword] = useState('');
|
||||
@@ -296,15 +297,18 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, nameKeyword: appliedTemplateNameKeyword, contentKeyword: appliedTemplateContentKeyword }) {
|
||||
const pageSize = 10;
|
||||
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, nameKeyword: appliedTemplateNameKeyword, contentKeyword: appliedTemplateContentKeyword }, targetPage = page) {
|
||||
try {
|
||||
const [templateItems, tenantItems, applicationItems, signatureList] = await Promise.all([
|
||||
adminApi.listEnterpriseTemplates(filters),
|
||||
const [templateResult, tenantItems, applicationItems, signatureList] = await Promise.all([
|
||||
adminApi.listEnterpriseTemplatesPage({ ...filters, page: targetPage, pageSize }),
|
||||
adminApi.listTenants(),
|
||||
adminApi.listEnterpriseApplications(),
|
||||
adminApi.listEnterpriseSignatures(),
|
||||
adminApi.listEnterpriseApplicationOptions(),
|
||||
adminApi.listEnterpriseSignatureOptions(),
|
||||
]);
|
||||
setTemplates(templateItems);
|
||||
setTemplates(templateResult.items);
|
||||
setTotal(templateResult.total);
|
||||
setTenants(tenantItems.filter((tenant) => tenant.status !== 'deleted'));
|
||||
setApplications(applicationItems);
|
||||
setSignatureItems(signatureList);
|
||||
@@ -315,25 +319,13 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, []);
|
||||
void loadData(undefined, page);
|
||||
}, [page]);
|
||||
|
||||
const filteredTemplates = useMemo(() => templates.filter((item) => {
|
||||
const enterprise = item.tenant?.name ?? item.tenantId;
|
||||
const application = item.application?.name ?? '';
|
||||
return (!appliedEnterpriseKeyword || enterprise.includes(appliedEnterpriseKeyword))
|
||||
&& (!appliedApplicationKeyword || application.includes(appliedApplicationKeyword))
|
||||
&& (!appliedTemplateNameKeyword || item.name.includes(appliedTemplateNameKeyword))
|
||||
&& (!appliedTemplateContentKeyword || item.content.includes(appliedTemplateContentKeyword));
|
||||
}), [appliedApplicationKeyword, appliedEnterpriseKeyword, appliedTemplateContentKeyword, appliedTemplateNameKeyword, 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);
|
||||
}, [appliedApplicationKeyword, appliedEnterpriseKeyword, appliedTemplateContentKeyword, appliedTemplateNameKeyword]);
|
||||
const visibleTemplates = templates;
|
||||
|
||||
async function saveTemplate(state: TemplateFormState) {
|
||||
const existing = templateModal && templateModal !== 'new' ? templateModal : null;
|
||||
@@ -387,7 +379,8 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
setAppliedApplicationKeyword(filters.applicationKeyword);
|
||||
setAppliedTemplateNameKeyword(filters.nameKeyword);
|
||||
setAppliedTemplateContentKeyword(filters.contentKeyword);
|
||||
void loadData(filters);
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}}>查询</Button>
|
||||
<Button onClick={() => {
|
||||
const filters = { enterpriseKeyword: '', applicationKeyword: '', nameKeyword: '', contentKeyword: '' };
|
||||
@@ -399,7 +392,8 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
setAppliedApplicationKeyword('');
|
||||
setAppliedTemplateNameKeyword('');
|
||||
setAppliedTemplateContentKeyword('');
|
||||
void loadData(filters);
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -447,7 +441,7 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
|
||||
page={currentPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={filteredTemplates.length}
|
||||
total={total}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Plus, ReceiptText, Search } from 'lucide-react';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, ManualRechargeDialog, Pagination, RechargeReceiptDialog, Tag, type DateRangeValue } from '@/components/ui';
|
||||
import { adminApi, type RechargeOrder, type TenantAccount, type TenantOption } from '@/api/adminApi';
|
||||
@@ -28,19 +28,28 @@ export function AdminRechargeRecordsPage() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
async function loadData() {
|
||||
async function loadData(targetPage = page, filters = { enterpriseKeyword, dateRange }) {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const [nextTenants, nextAccounts, nextRecords] = await Promise.all([
|
||||
const [nextTenants, nextAccounts, result] = await Promise.all([
|
||||
adminApi.listTenants(),
|
||||
adminApi.listAccounts(),
|
||||
adminApi.listManualRecharges(),
|
||||
adminApi.listManualRechargesPage({
|
||||
enterpriseKeyword: filters.enterpriseKeyword.trim() || undefined,
|
||||
createdAtFrom: filters.dateRange.start,
|
||||
createdAtTo: filters.dateRange.end,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
}),
|
||||
]);
|
||||
setTenants(nextTenants);
|
||||
setAccounts(nextAccounts);
|
||||
setRecords(nextRecords);
|
||||
setRecords(result.items);
|
||||
setTotal(result.total);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '充值记录加载失败');
|
||||
setRecords([]);
|
||||
@@ -50,35 +59,22 @@ export function AdminRechargeRecordsPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, []);
|
||||
void loadData(page);
|
||||
}, [page]);
|
||||
|
||||
const filteredRows = useMemo(
|
||||
() => records.filter((item) => {
|
||||
const rechargeDate = getDate(item.paidAt ?? item.createdAt);
|
||||
const tenantName = item.tenant?.name ?? tenants.find((tenant) => tenant.id === item.tenantId)?.name ?? item.tenantId;
|
||||
const matchesEnterprise = !enterpriseKeyword || tenantName.includes(enterpriseKeyword);
|
||||
const matchesStartDate = !dateRange.start || rechargeDate >= dateRange.start;
|
||||
const matchesEndDate = !dateRange.end || rechargeDate <= dateRange.end;
|
||||
return matchesEnterprise && matchesStartDate && matchesEndDate;
|
||||
}),
|
||||
[dateRange.end, dateRange.start, enterpriseKeyword, records, tenants],
|
||||
);
|
||||
const pageSize = 10;
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const filteredRows = records;
|
||||
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 = records;
|
||||
const receiptTenant = receiptRecord
|
||||
? receiptRecord.tenant ?? tenants.find((tenant) => tenant.id === receiptRecord.tenantId)
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [dateRange.end, dateRange.start, enterpriseKeyword, records.length]);
|
||||
|
||||
function resetFilters() {
|
||||
setEnterpriseKeyword('');
|
||||
setDateRange({});
|
||||
setPage(1);
|
||||
void loadData(1, { enterpriseKeyword: '', dateRange: {} });
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -95,7 +91,7 @@ export function AdminRechargeRecordsPage() {
|
||||
<Input label="企业名称" onChange={(event) => setEnterpriseKeyword(event.target.value)} value={enterpriseKeyword} />
|
||||
<DateRangeInput label="充值日期" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-recharge-filter__actions">
|
||||
<Button icon={<Search size={16} />}>查询</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { setPage(1); void loadData(1); }}>查询</Button>
|
||||
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +152,7 @@ export function AdminRechargeRecordsPage() {
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={filteredRows.length}
|
||||
total={total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Clock3, Eye, Search } from 'lucide-react';
|
||||
import { adminApi, type ReportRecord } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Table, Tag, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
|
||||
const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
|
||||
pending: 'neutral',
|
||||
@@ -71,28 +71,30 @@ export function AdminReportRecordsPage() {
|
||||
const [reportType, setReportType] = useState('all');
|
||||
const [detail, setDetail] = useState<ReportRecord | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
function loadData() {
|
||||
adminApi.listReportRecords()
|
||||
.then((items) => {
|
||||
setRecords(items);
|
||||
function loadData(targetPage = page) {
|
||||
adminApi.listReportRecordsPage({
|
||||
keyword: keyword || undefined,
|
||||
reportType: reportType === 'all' ? undefined : reportType as 'signature' | 'drainage',
|
||||
createdAtFrom: dateRange.start || undefined,
|
||||
createdAtTo: dateRange.end || undefined,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
})
|
||||
.then((result) => {
|
||||
setRecords(result.items);
|
||||
setTotal(result.total);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '报备记录加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const filteredRecords = useMemo(() => records.filter((record) => {
|
||||
const text = `${record.taskId}${record.channel?.name ?? ''}${record.action}${actionLabel[record.action] ?? ''}${recordSource(record)}${record.reason ?? ''}${record.task?.signature?.name ?? ''}${record.task?.signature?.purpose ?? ''}${record.task?.drainageInfo?.siteName ?? ''}${record.task?.drainageInfo?.url ?? ''}${record.task?.drainageInfo?.remark ?? ''}`;
|
||||
const date = record.createdAt?.slice(0, 10) ?? '';
|
||||
return (!keyword || text.includes(keyword))
|
||||
&& (!dateRange.start || date >= dateRange.start)
|
||||
&& (!dateRange.end || date <= dateRange.end)
|
||||
&& (reportType === 'all' || record.task?.reportType === reportType);
|
||||
}), [dateRange.end, dateRange.start, keyword, records, reportType]);
|
||||
loadData(page);
|
||||
}, [page]);
|
||||
|
||||
const columns: Array<TableColumn<ReportRecord>> = [
|
||||
{ key: 'task', title: '报备任务号', width: '190px', render: (record) => <strong className="admin-task-id">{record.taskId}</strong> },
|
||||
@@ -122,14 +124,24 @@ export function AdminReportRecordsPage() {
|
||||
<Select label="报备类型" onChange={(event) => setReportType(event.target.value)} options={[{ label: '全部类型', value: 'all' }, { label: '签名报备', value: 'signature' }, { label: '引流信息报备', value: 'drainage' }]} value={reportType} />
|
||||
<DateRangeInput label="提交时间" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={loadData}>查询</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { if (page !== 1) setPage(1); else loadData(1); }}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setDateRange({}); setReportType('all'); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-table-card report-task-table-card">
|
||||
<Table columns={columns} data={filteredRecords} emptyText="暂无报备记录" rowKey="id" />
|
||||
<Table columns={columns} data={records} emptyText="暂无报备记录" pagination={false} rowKey="id" />
|
||||
</div>
|
||||
<Pagination
|
||||
nextDisabled={page * pageSize >= total}
|
||||
onNext={() => setPage((current) => current + 1)}
|
||||
onPageChange={setPage}
|
||||
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
|
||||
page={page}
|
||||
previousDisabled={page <= 1}
|
||||
total={total}
|
||||
totalPages={Math.max(1, Math.ceil(total / pageSize))}
|
||||
/>
|
||||
|
||||
{detail ? <RecordDetailModal onClose={() => setDetail(null)} record={detail} /> : null}
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Eye, Search } from 'lucide-react';
|
||||
import { adminApi, fileDownloadUrl, type ReportTask } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
const statusMeta: Record<string, { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'danger' }> = {
|
||||
@@ -74,25 +74,30 @@ export function AdminReportTasksPage() {
|
||||
const [statusReason, setStatusReason] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
function loadData() {
|
||||
adminApi.listReportTasks({ reportType: reportType === 'all' ? undefined : reportType as 'signature' | 'drainage' })
|
||||
.then((items) => {
|
||||
setTasks(items);
|
||||
function loadData(targetPage = page) {
|
||||
adminApi.listReportTasksPage({
|
||||
reportType: reportType === 'all' ? undefined : reportType as 'signature' | 'drainage',
|
||||
keyword: keyword || undefined,
|
||||
createdAtFrom: dateRange.start || undefined,
|
||||
createdAtTo: dateRange.end || undefined,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
})
|
||||
.then((result) => {
|
||||
setTasks(result.items);
|
||||
setTotal(result.total);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '报备明细加载失败'));
|
||||
}
|
||||
|
||||
useEffect(loadData, [reportType]);
|
||||
|
||||
const filteredTasks = useMemo(() => tasks.filter((task) => {
|
||||
const text = `${task.id}${task.channel?.name ?? task.channelId}${task.signature?.name ?? task.signatureId}${task.drainageInfo?.siteName ?? ''}${task.drainageInfo?.url ?? ''}${task.signature?.tenant?.name ?? ''}${task.signature?.application?.name ?? ''}`;
|
||||
const date = task.createdAt?.slice(0, 10) ?? '';
|
||||
return (!keyword || text.includes(keyword))
|
||||
&& (!dateRange.start || date >= dateRange.start)
|
||||
&& (!dateRange.end || date <= dateRange.end);
|
||||
}), [dateRange.end, dateRange.start, keyword, tasks]);
|
||||
useEffect(() => {
|
||||
loadData(page);
|
||||
}, [page]);
|
||||
|
||||
async function saveTaskStatus() {
|
||||
if (!statusTask) return;
|
||||
@@ -143,13 +148,14 @@ export function AdminReportTasksPage() {
|
||||
<Input label="企业/应用/通道/报备对象" onChange={(event) => setKeyword(event.target.value)} placeholder="搜索报备明细" value={keyword} />
|
||||
<Select label="报备类型" onChange={(event) => setReportType(event.target.value)} options={[{ label: '全部类型', value: 'all' }, { label: '签名报备', value: 'signature' }, { label: '引流信息报备', value: 'drainage' }]} value={reportType} />
|
||||
<DateRangeInput label="创建时间" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-task-filter__actions"><Button icon={<Search size={16} />} onClick={loadData}>查询</Button><Button onClick={() => {
|
||||
<div className="admin-task-filter__actions"><Button icon={<Search size={16} />} onClick={() => { if (page !== 1) setPage(1); else loadData(1); }}>查询</Button><Button onClick={() => {
|
||||
setKeyword('');
|
||||
setDateRange({});
|
||||
setReportType('all');
|
||||
}} variant="ghost">重置</Button></div>
|
||||
</div>
|
||||
<div className="surface report-task-table-card"><Table columns={columns} data={filteredTasks} emptyText="暂无报备明细" rowKey="id" /></div>
|
||||
<div className="surface report-task-table-card"><Table columns={columns} data={tasks} emptyText="暂无报备明细" pagination={false} rowKey="id" /></div>
|
||||
<Pagination nextDisabled={page * pageSize >= total} onNext={() => setPage((current) => current + 1)} onPageChange={setPage} onPrevious={() => setPage((current) => Math.max(1, current - 1))} page={page} previousDisabled={page <= 1} total={total} totalPages={Math.max(1, Math.ceil(total / pageSize))} />
|
||||
{detailTask ? <TaskDetailModal onClose={() => setDetailTask(null)} task={detailTask} /> : null}
|
||||
<Modal footer={<><Button disabled={busy} onClick={() => setStatusTask(null)} variant="ghost">取消</Button><Button disabled={busy} onClick={() => void saveTaskStatus()}>{busy ? '保存中…' : '保存'}</Button></>} onClose={() => setStatusTask(null)} open={Boolean(statusTask)} title="修改报备状态">
|
||||
{statusTask ? <div className="page-stack">
|
||||
|
||||
@@ -419,6 +419,10 @@ export function AdminSmsRecordsPage() {
|
||||
const [segmentLoading, setSegmentLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filterTenants, setFilterTenants] = useState<Array<{ id: string; name: string }>>([]);
|
||||
const [filterApplications, setFilterApplications] = useState<Array<{ id: string; tenantId: string; name: string }>>([]);
|
||||
|
||||
type MessageFilters = {
|
||||
tenantId?: string;
|
||||
@@ -444,19 +448,30 @@ export function AdminSmsRecordsPage() {
|
||||
};
|
||||
}
|
||||
|
||||
function loadData(filters = currentFilters()) {
|
||||
adminApi.listOperationMessages(filters)
|
||||
.then((items) => {
|
||||
setRecords(items);
|
||||
setSelectedRecord((current) => current ? items.find((item) => item.id === current.id) ?? null : null);
|
||||
setPage(1);
|
||||
function loadData(filters = currentFilters(), targetPage = page) {
|
||||
setLoading(true);
|
||||
adminApi.listOperationMessages({ ...filters, page: targetPage, pageSize })
|
||||
.then((result) => {
|
||||
setRecords(result.items);
|
||||
setTotal(result.total);
|
||||
setSelectedRecord((current) => current ? result.items.find((item) => item.id === current.id) ?? null : null);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '短信记录加载失败'));
|
||||
.catch((failure: Error) => setError(failure.message || '短信记录加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
loadData(currentFilters(), page);
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([adminApi.listTenants(), adminApi.listEnterpriseApplicationOptions()])
|
||||
.then(([tenants, applications]) => {
|
||||
setFilterTenants(tenants.filter((item) => item.status !== 'deleted').map((item) => ({ id: item.id, name: item.name })));
|
||||
setFilterApplications(applications.filter((item) => item.status !== 'deleted').map((item) => ({ id: item.id, tenantId: item.tenantId, name: item.name })));
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '短信记录筛选项加载失败'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -475,31 +490,19 @@ export function AdminSmsRecordsPage() {
|
||||
}, [selectedRecord]);
|
||||
|
||||
const enterpriseOptions = useMemo(() => {
|
||||
const tenants = new Map<string, string>();
|
||||
records.forEach((record) => {
|
||||
if (record.tenantId) {
|
||||
tenants.set(record.tenantId, record.tenant?.name ?? record.tenantId);
|
||||
}
|
||||
});
|
||||
return [{ label: '全部企业', value: 'all' }, ...Array.from(tenants, ([value, label]) => ({ label, value }))];
|
||||
}, [records]);
|
||||
return [{ label: '全部企业', value: 'all' }, ...filterTenants.map((item) => ({ label: item.name, value: item.id }))];
|
||||
}, [filterTenants]);
|
||||
|
||||
const applicationOptions = useMemo(() => {
|
||||
const applications = new Map<string, string>();
|
||||
records
|
||||
.filter((record) => enterprise === 'all' || record.tenantId === enterprise)
|
||||
.forEach((record) => {
|
||||
if (record.applicationId) {
|
||||
applications.set(record.applicationId, record.application?.name ?? record.applicationId);
|
||||
}
|
||||
});
|
||||
return [{ label: '全部应用', value: 'all' }, ...Array.from(applications, ([value, label]) => ({ label, value }))];
|
||||
}, [enterprise, records]);
|
||||
return [{ label: '全部应用', value: 'all' }, ...filterApplications
|
||||
.filter((item) => enterprise === 'all' || item.tenantId === enterprise)
|
||||
.map((item) => ({ label: item.name, value: item.id }))];
|
||||
}, [enterprise, filterApplications]);
|
||||
|
||||
const filteredRows = records;
|
||||
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;
|
||||
|
||||
function resetFilters() {
|
||||
const defaultDateRange = defaultSmsRecordDateRange();
|
||||
@@ -510,7 +513,22 @@ export function AdminSmsRecordsPage() {
|
||||
setContentKeyword('');
|
||||
setChannelKeyword('');
|
||||
setStatus('all');
|
||||
loadData({ queuedAtFrom: defaultDateRange.start, queuedAtTo: defaultDateRange.end });
|
||||
if (page !== 1) setPage(1);
|
||||
else loadData({ queuedAtFrom: defaultDateRange.start, queuedAtTo: defaultDateRange.end }, 1);
|
||||
}
|
||||
|
||||
async function exportRecords() {
|
||||
try {
|
||||
const blob = await adminApi.exportOperationMessages(currentFilters());
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `sms-records-${new Date().toISOString().slice(0, 10)}.csv`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '短信记录导出失败');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -551,17 +569,17 @@ export function AdminSmsRecordsPage() {
|
||||
value={status}
|
||||
/>
|
||||
<div className="admin-sms-record-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => loadData()}>查询</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { if (page !== 1) setPage(1); else loadData(currentFilters(), 1); }}>查询</Button>
|
||||
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-sms-record-table-card">
|
||||
<div className="admin-sms-record-toolbar">
|
||||
<Button icon={<Download size={16} />} onClick={() => downloadCsv(filteredRows)} variant="ghost">导出CSV</Button>
|
||||
<Button icon={<Download size={16} />} onClick={() => void exportRecords()} variant="ghost">导出CSV</Button>
|
||||
</div>
|
||||
<div className="admin-sms-record-list">
|
||||
{filteredRows.length === 0 ? <div className="ui-table__empty">暂无短信记录</div> : visibleRows.map((record) => (
|
||||
{loading ? <div className="ui-table__empty">正在加载真实短信记录...</div> : filteredRows.length === 0 ? <div className="ui-table__empty">暂无短信记录</div> : visibleRows.map((record) => (
|
||||
<article className="admin-sms-record-card" key={record.id}>
|
||||
<header>
|
||||
<div className="admin-sms-record-sender">
|
||||
@@ -588,7 +606,7 @@ export function AdminSmsRecordsPage() {
|
||||
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
|
||||
page={currentPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={filteredRows.length}
|
||||
total={total}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -382,12 +382,25 @@ export function AdminSmsTaskProgressPage() {
|
||||
const [terminateTarget, setTerminateTarget] = useState<SmsTask | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
const [filterTenants, setFilterTenants] = useState<string[]>([]);
|
||||
const [filterApplications, setFilterApplications] = useState<Array<{ tenantName: string; name: string }>>([]);
|
||||
const pageSize = 10;
|
||||
|
||||
function loadTasks() {
|
||||
function loadTasks(targetPage = page) {
|
||||
setLoading(true);
|
||||
adminApi.listAdminBatchTasks()
|
||||
.then((items) => {
|
||||
setTasks(items.map(mapTask));
|
||||
adminApi.listAdminBatchTasksPage({
|
||||
keyword: keyword || undefined,
|
||||
enterpriseKeyword: enterprise === 'all' ? undefined : enterprise,
|
||||
applicationKeyword: application === 'all' ? undefined : application,
|
||||
createdAtFrom: submittedDateRange.start || undefined,
|
||||
createdAtTo: submittedDateRange.end || undefined,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
})
|
||||
.then((result) => {
|
||||
setTasks(result.items.map(mapTask));
|
||||
setTotal(result.total);
|
||||
setError('');
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '短信任务进度加载失败'))
|
||||
@@ -395,7 +408,17 @@ export function AdminSmsTaskProgressPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
loadTasks(page);
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([adminApi.listTenants(), adminApi.listEnterpriseApplicationOptions()])
|
||||
.then(([tenants, applications]) => {
|
||||
const tenantNameById = new Map(tenants.map((item) => [item.id, item.name]));
|
||||
setFilterTenants(tenants.filter((item) => item.status !== 'deleted').map((item) => item.name));
|
||||
setFilterApplications(applications.filter((item) => item.status !== 'deleted').map((item) => ({ tenantName: tenantNameById.get(item.tenantId) ?? '', name: item.name })));
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '任务筛选项加载失败'));
|
||||
}, []);
|
||||
|
||||
function loadPhones(target = phoneTarget, page = phonePage, pageSize = phonePageSize) {
|
||||
@@ -410,35 +433,18 @@ export function AdminSmsTaskProgressPage() {
|
||||
}, [phoneTarget, phonePage, phonePageSize]);
|
||||
|
||||
const enterpriseOptions = useMemo(() => {
|
||||
const names = Array.from(new Set(tasks.map((item) => item.enterprise)));
|
||||
return [{ label: '全部企业', value: 'all' }, ...names.map((name) => ({ label: name, value: name }))];
|
||||
}, [tasks]);
|
||||
return [{ label: '全部企业', value: 'all' }, ...filterTenants.map((name) => ({ label: name, value: name }))];
|
||||
}, [filterTenants]);
|
||||
|
||||
const applicationOptions = useMemo(() => {
|
||||
const names = Array.from(new Set(tasks.filter((item) => enterprise === 'all' || item.enterprise === enterprise).map((item) => item.application)));
|
||||
const names = Array.from(new Set(filterApplications.filter((item) => enterprise === 'all' || item.tenantName === enterprise).map((item) => item.name)));
|
||||
return [{ label: '全部应用', value: 'all' }, ...names.map((name) => ({ label: name, value: name }))];
|
||||
}, [enterprise, tasks]);
|
||||
}, [enterprise, filterApplications]);
|
||||
|
||||
const filteredTasks = useMemo(
|
||||
() => tasks.filter((item) => {
|
||||
const submittedDate = item.submittedAt.slice(0, 10);
|
||||
const matchesKeyword = !keyword || item.id.includes(keyword);
|
||||
const matchesEnterprise = enterprise === 'all' || item.enterprise === enterprise;
|
||||
const matchesApplication = application === 'all' || item.application === application;
|
||||
const matchesStartDate = !submittedDateRange.start || submittedDate >= submittedDateRange.start;
|
||||
const matchesEndDate = !submittedDateRange.end || submittedDate <= submittedDateRange.end;
|
||||
return matchesKeyword && matchesEnterprise && matchesApplication && matchesStartDate && matchesEndDate;
|
||||
}),
|
||||
[application, enterprise, keyword, submittedDateRange.end, submittedDateRange.start, tasks],
|
||||
);
|
||||
const pageSize = 10;
|
||||
const totalPages = Math.max(1, Math.ceil(filteredTasks.length / pageSize));
|
||||
const filteredTasks = tasks;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const visibleTasks = filteredTasks.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [application, enterprise, keyword, submittedDateRange.end, submittedDateRange.start, tasks.length]);
|
||||
const visibleTasks = filteredTasks;
|
||||
|
||||
function resetFilters() {
|
||||
setKeyword('');
|
||||
@@ -480,7 +486,7 @@ export function AdminSmsTaskProgressPage() {
|
||||
<Select label="选择应用" onChange={(event) => setApplication(event.target.value)} options={applicationOptions} value={application} />
|
||||
<DateRangeInput label="提交时间" onChange={setSubmittedDateRange} value={submittedDateRange} />
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={loadTasks}>查询</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { if (page !== 1) setPage(1); else loadTasks(1); }}>查询</Button>
|
||||
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -594,7 +600,7 @@ export function AdminSmsTaskProgressPage() {
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={filteredTasks.length}
|
||||
total={total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Search, Smartphone } from 'lucide-react';
|
||||
import { adminApi, type SmsMessageRecord, type SmsUplinkMatchCandidate, type SmsUplinkMessage } from '@/api/adminApi';
|
||||
import {
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DateRangeInput,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Table,
|
||||
type DateRangeValue,
|
||||
type TableColumn,
|
||||
@@ -210,12 +211,23 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
const [error, setError] = useState('');
|
||||
const [detailError, setDetailError] = useState('');
|
||||
const [claimError, setClaimError] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
|
||||
function loadData() {
|
||||
function loadData(targetPage = page, filters = { phoneKeyword, contentKeyword, dateRange }) {
|
||||
setLoading(true);
|
||||
adminApi.listAdminUplinkMessages()
|
||||
.then((items) => {
|
||||
setMessages(items);
|
||||
adminApi.listAdminUplinkMessagesPage({
|
||||
phoneNumber: filters.phoneKeyword.trim() || undefined,
|
||||
keyword: filters.contentKeyword.trim() || undefined,
|
||||
startTime: filters.dateRange.start ? `${filters.dateRange.start}T00:00:00+08:00` : undefined,
|
||||
endTime: filters.dateRange.end ? `${filters.dateRange.end}T23:59:59.999+08:00` : undefined,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
})
|
||||
.then((result) => {
|
||||
setMessages(result.items);
|
||||
setTotal(result.total);
|
||||
setError('');
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '短信上行记录加载失败'))
|
||||
@@ -233,32 +245,22 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
}
|
||||
|
||||
setMatching(true);
|
||||
adminApi.listOperationMessages({ tenantId: message.tenantId ?? undefined, messageId: message.messageId })
|
||||
.then((items) => setMatchedRecords(items))
|
||||
adminApi.listOperationMessages({ tenantId: message.tenantId ?? undefined, messageId: message.messageId, page: 1, pageSize: 10 })
|
||||
.then((result) => setMatchedRecords(result.items))
|
||||
.catch((reason: Error) => setDetailError(reason.message || '匹配发送记录加载失败'))
|
||||
.finally(() => setMatching(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const filteredMessages = useMemo(
|
||||
() => messages.filter((item) => {
|
||||
const receivedDate = getDate(item.receivedAt);
|
||||
const matchesStartDate = !dateRange.start || receivedDate >= dateRange.start;
|
||||
const matchesEndDate = !dateRange.end || receivedDate <= dateRange.end;
|
||||
const matchesPhone = !phoneKeyword || item.phoneNumber.includes(phoneKeyword);
|
||||
const matchesContent = !contentKeyword || item.content.includes(contentKeyword);
|
||||
return matchesStartDate && matchesEndDate && matchesPhone && matchesContent;
|
||||
}),
|
||||
[contentKeyword, dateRange.end, dateRange.start, messages, phoneKeyword],
|
||||
);
|
||||
loadData(page);
|
||||
}, [page]);
|
||||
|
||||
function resetFilters() {
|
||||
setDateRange({});
|
||||
setPhoneKeyword('');
|
||||
setContentKeyword('');
|
||||
setPage(1);
|
||||
loadData(1, { phoneKeyword: '', contentKeyword: '', dateRange: {} });
|
||||
}
|
||||
|
||||
function handleClaim(candidate: SmsUplinkMatchCandidate) {
|
||||
@@ -271,7 +273,7 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
.then((updated) => {
|
||||
setMessages((items) => items.map((item) => (item.id === updated.id ? { ...item, ...updated } : item)));
|
||||
setSelectedMessage((current) => (current && current.id === updated.id ? { ...current, ...updated } : current));
|
||||
loadData();
|
||||
loadData(page);
|
||||
})
|
||||
.catch((reason: Error) => setClaimError(reason.message || '上行认领失败'))
|
||||
.finally(() => setClaimingId(''));
|
||||
@@ -316,7 +318,7 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
<Input label="手机号码" onChange={(event) => setPhoneKeyword(event.target.value)} prefix={<Smartphone size={16} />} value={phoneKeyword} />
|
||||
<Input label="上行内容" onChange={(event) => setContentKeyword(event.target.value)} value={contentKeyword} />
|
||||
<div className="admin-uplink-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={loadData}>查询</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { setPage(1); loadData(1); }}>查询</Button>
|
||||
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,7 +326,8 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<div className="surface admin-uplink-table-card">
|
||||
<Table columns={columns} data={loading ? [] : filteredMessages} 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>
|
||||
|
||||
{selectedMessage ? (
|
||||
|
||||
Reference in New Issue
Block a user