Files
lislgosms/src/apps/admin/AdminSmsRecordsPage.tsx
T

196 lines
7.2 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { adminApi, type SmsMessageRecord, type SmsMessageSegmentAudit } from '@/api/adminApi';
import { Breadcrumb, type DateRangeValue } from '@/components/ui';
import { SendDetailModal } from './sms-records/SendDetailModal';
import { SmsRecordFilter } from './sms-records/SmsRecordFilter';
import { SmsRecordList } from './sms-records/SmsRecordList';
import { defaultSmsRecordDateRange } from './sms-records/smsRecordModel';
import type { ApplicationOption, MessageFilters, TenantOption } from './sms-records/smsRecordTypes';
import './sms-records/AdminSmsRecordsPage.css';
const pageSize = 25;
export function AdminSmsRecordsPage() {
const [records, setRecords] = useState<SmsMessageRecord[]>([]);
const [enterprise, setEnterprise] = useState('all');
const [application, setApplication] = useState('all');
const [dateRange, setDateRange] = useState<DateRangeValue>(defaultSmsRecordDateRange);
const [phoneKeyword, setPhoneKeyword] = useState('');
const [contentKeyword, setContentKeyword] = useState('');
const [channelKeyword, setChannelKeyword] = useState('');
const [carrier, setCarrier] = useState('all');
const [status, setStatus] = useState('all');
const [selectedRecord, setSelectedRecord] = useState<SmsMessageRecord | null>(null);
const [segmentAudits, setSegmentAudits] = useState<SmsMessageSegmentAudit[]>([]);
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<TenantOption[]>([]);
const [filterApplications, setFilterApplications] = useState<ApplicationOption[]>([]);
function currentFilters(): MessageFilters {
return {
tenantId: enterprise === 'all' ? undefined : enterprise,
applicationId: application === 'all' ? undefined : application,
phoneNumber: phoneKeyword || undefined,
contentKeyword: contentKeyword || undefined,
channelKeyword: channelKeyword || undefined,
carrier: carrier === 'all' ? undefined : carrier,
queuedAtFrom: dateRange.start,
queuedAtTo: dateRange.end,
status: status === 'all' ? undefined : status,
};
}
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 || '短信记录加载失败'))
.finally(() => setLoading(false));
}
useEffect(() => {
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(() => {
if (!selectedRecord) {
setSegmentAudits([]);
return;
}
setSegmentLoading(true);
adminApi.listMessageSegmentAudits({ messageRecordId: selectedRecord.id })
.then((items) => {
setSegmentAudits(items);
setError('');
})
.catch((failure: Error) => setError(failure.message || '分片审计加载失败'))
.finally(() => setSegmentLoading(false));
}, [selectedRecord]);
const enterpriseOptions = useMemo(
() => [{ label: '全部企业', value: 'all' }, ...filterTenants.map((item) => ({ label: item.name, value: item.id }))],
[filterTenants],
);
const applicationOptions = useMemo(
() => [{ label: '全部应用', value: 'all' }, ...filterApplications
.filter((item) => enterprise === 'all' || item.tenantId === enterprise)
.map((item) => ({ label: item.name, value: item.id }))],
[enterprise, filterApplications],
);
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
function resetFilters() {
const defaultDateRange = defaultSmsRecordDateRange();
setEnterprise('all');
setApplication('all');
setDateRange(defaultDateRange);
setPhoneKeyword('');
setContentKeyword('');
setChannelKeyword('');
setCarrier('all');
setStatus('all');
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 (
<section className="page-stack admin-sms-records-page">
<div className="page-heading">
<div>
<Breadcrumb items={['数据详单', '短信记录']} />
<h1>短信记录</h1>
</div>
</div>
{error ? <p className="form-error">{error}</p> : null}
<SmsRecordFilter
application={application}
applicationOptions={applicationOptions}
carrier={carrier}
channelKeyword={channelKeyword}
contentKeyword={contentKeyword}
dateRange={dateRange}
enterprise={enterprise}
enterpriseOptions={enterpriseOptions}
phoneKeyword={phoneKeyword}
status={status}
onApplicationChange={setApplication}
onCarrierChange={setCarrier}
onChannelKeywordChange={setChannelKeyword}
onContentKeywordChange={setContentKeyword}
onDateRangeChange={setDateRange}
onEnterpriseChange={(value) => {
setEnterprise(value);
setApplication('all');
}}
onPhoneKeywordChange={setPhoneKeyword}
onQuery={() => {
if (page !== 1) setPage(1);
else loadData(currentFilters(), 1);
}}
onReset={resetFilters}
onStatusChange={setStatus}
/>
<SmsRecordList
currentPage={currentPage}
loading={loading}
records={records}
total={total}
totalPages={totalPages}
onExport={() => void exportRecords()}
onOpenDetail={setSelectedRecord}
onPageChange={setPage}
/>
{selectedRecord ? (
<SendDetailModal
onClose={() => setSelectedRecord(null)}
record={selectedRecord}
segmentAudits={segmentAudits}
segmentLoading={segmentLoading}
/>
) : null}
</section>
);
}