feat: complete reporting and filing workflows
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { BarChart3, CalendarClock, Eye, MapPin, Search, Send, Smartphone, StopCircle, TrendingUp } from 'lucide-react';
|
||||
import { adminApi, type SmsBatchTask, type SmsMessageRecord } from '@/api/adminApi';
|
||||
import { adminApi, type BatchTaskMessagePage, type SmsBatchTask, type SmsMessageRecord } from '@/api/adminApi';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -89,6 +89,21 @@ function formatTime(value?: string | null) {
|
||||
return formatDateTime(value);
|
||||
}
|
||||
|
||||
function messageStatusLabel(status: string) {
|
||||
return {
|
||||
pending_review: '待人工审核',
|
||||
queued: '已入队',
|
||||
scheduled: '等待定时发送',
|
||||
submitted: '供应商已受理',
|
||||
delivered: '送达成功',
|
||||
submit_failed: '提交失败',
|
||||
failed: '回执失败',
|
||||
rejected: '已拒绝',
|
||||
timeout: '超时',
|
||||
canceled: '已取消',
|
||||
}[status] ?? status;
|
||||
}
|
||||
|
||||
function normalizeTaskStatus(status: string): TaskStatus {
|
||||
if (['finished', 'completed', 'done'].includes(status)) return 'completed';
|
||||
if (['canceled', 'cancelled', 'terminated'].includes(status)) return 'terminated';
|
||||
@@ -359,6 +374,11 @@ export function AdminSmsTaskProgressPage() {
|
||||
const [hoveredTaskId, setHoveredTaskId] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [selectedTask, setSelectedTask] = useState<SmsTask | null>(null);
|
||||
const [phoneTarget, setPhoneTarget] = useState<SmsTask | null>(null);
|
||||
const [phoneKeyword, setPhoneKeyword] = useState('');
|
||||
const [phonePage, setPhonePage] = useState(1);
|
||||
const [phonePageSize, setPhonePageSize] = useState(20);
|
||||
const [phoneData, setPhoneData] = useState<BatchTaskMessagePage>({ items: [], total: 0, page: 1, pageSize: 20 });
|
||||
const [terminateTarget, setTerminateTarget] = useState<SmsTask | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
@@ -378,6 +398,17 @@ export function AdminSmsTaskProgressPage() {
|
||||
loadTasks();
|
||||
}, []);
|
||||
|
||||
function loadPhones(target = phoneTarget, page = phonePage, pageSize = phonePageSize) {
|
||||
if (!target) return;
|
||||
adminApi.listAdminBatchTaskMessages(target.backendId, { phone: phoneKeyword || undefined, page, pageSize })
|
||||
.then(setPhoneData)
|
||||
.catch((failure: Error) => setError(failure.message || '发送批次号码列表加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (phoneTarget) loadPhones(phoneTarget, phonePage, phonePageSize);
|
||||
}, [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 }))];
|
||||
@@ -498,7 +529,7 @@ export function AdminSmsTaskProgressPage() {
|
||||
<td><span>{formatTime(record.submittedAt)}</span></td>
|
||||
<td>
|
||||
<div className="admin-task-counts">
|
||||
<strong>{formatNumber(record.phoneCount)}</strong>
|
||||
<button className="table-link" onClick={() => { setPhoneTarget(record); setPhoneKeyword(''); setPhonePage(1); }} type="button">{formatNumber(record.phoneCount)} · 查看列表</button>
|
||||
<span>{record.wordCount}字</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -568,6 +599,36 @@ export function AdminSmsTaskProgressPage() {
|
||||
</div>
|
||||
|
||||
{selectedTask ? <TaskDetailModal onClose={() => setSelectedTask(null)} task={selectedTask} /> : null}
|
||||
{phoneTarget ? <Modal footer={<Button onClick={() => setPhoneTarget(null)}>关闭</Button>} onClose={() => setPhoneTarget(null)} open size="xl" title={`号码列表 · 发送批次号 ${phoneTarget.id}`}>
|
||||
<div className="page-stack">
|
||||
<div className="audit-filter-grid">
|
||||
<Input label="手机号码" onChange={(event) => setPhoneKeyword(event.target.value)} placeholder="输入完整或部分号码" value={phoneKeyword} />
|
||||
<Select label="每页条数" onChange={(event) => { setPhonePageSize(Number(event.target.value)); setPhonePage(1); }} options={[{ label: '10条/页', value: '10' }, { label: '20条/页', value: '20' }, { label: '50条/页', value: '50' }]} value={String(phonePageSize)} />
|
||||
<div className="audit-filter-actions"><Button icon={<Search size={16} />} onClick={() => { setPhonePage(1); loadPhones(phoneTarget, 1, phonePageSize); }}>查询</Button></div>
|
||||
</div>
|
||||
<Table
|
||||
columns={[
|
||||
{ key: 'phoneNumber', title: '手机号码', render: (item) => <strong>{item.phoneNumber}</strong> },
|
||||
{ key: 'province', title: '号码归属地', render: (item) => item.province || '-' },
|
||||
{ key: 'carrier', title: '运营商', render: (item) => carrierLabels[item.carrier ?? '']?.label ?? item.carrier ?? '-' },
|
||||
{ key: 'status', title: '短信记录状态', render: (item) => <Tag tone={item.status === 'delivered' ? 'success' : ['failed', 'submit_failed', 'rejected', 'timeout'].includes(item.status) ? 'danger' : 'info'}>{messageStatusLabel(item.status)}</Tag> },
|
||||
]}
|
||||
data={phoneData.items}
|
||||
emptyText="暂无号码记录"
|
||||
rowKey="id"
|
||||
/>
|
||||
<Pagination
|
||||
nextDisabled={phonePage * phonePageSize >= phoneData.total}
|
||||
onNext={() => setPhonePage((current) => current + 1)}
|
||||
onPageChange={setPhonePage}
|
||||
onPrevious={() => setPhonePage((current) => Math.max(1, current - 1))}
|
||||
page={phonePage}
|
||||
previousDisabled={phonePage <= 1}
|
||||
total={phoneData.total}
|
||||
totalPages={Math.max(1, Math.ceil(phoneData.total / phonePageSize))}
|
||||
/>
|
||||
</div>
|
||||
</Modal> : null}
|
||||
{terminateTarget ? (
|
||||
<Modal
|
||||
footer={(
|
||||
|
||||
Reference in New Issue
Block a user