196 lines
7.2 KiB
TypeScript
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>
|
|
);
|
|
}
|