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

177 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import { CalendarDays, Check, Search, X } from 'lucide-react';
import { adminApi, type RiskReviewTask } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, Textarea, type TableColumn } from '@/components/ui';
const statusLabel: Record<string, string> = {
pending_review: '待审核',
approved: '已通过',
rejected: '已驳回',
};
const statusTone: Record<string, 'warning' | 'success' | 'danger'> = {
pending_review: 'warning',
approved: 'success',
rejected: 'danger',
};
function sourceLabel(sourceType?: string) {
return sourceType === 'cmpp_template_mismatch' ? 'CMPP模板不匹配聚合' : '风控审核';
}
export function AdminSmsAuditPage() {
const [records, setRecords] = useState<RiskReviewTask[]>([]);
const [keyword, setKeyword] = useState('');
const [status, setStatus] = useState('pending_review');
const [date, setDate] = useState('');
const [error, setError] = useState('');
const [approveTarget, setApproveTarget] = useState<RiskReviewTask | 'batch' | null>(null);
const [rejectTarget, setRejectTarget] = useState<RiskReviewTask | null>(null);
const [rejectReason, setRejectReason] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
function loadData() {
adminApi.listRiskReviewTasks({ status: status === 'all' ? undefined : status })
.then((items) => {
setRecords(items);
setSelectedIds((current) => current.filter((id) => items.some((item) => item.id === id && item.status === 'pending_review')));
setError('');
})
.catch((failure: Error) => setError(failure.message || '短信审核任务加载失败'));
}
useEffect(() => {
loadData();
}, [status]);
const filteredRecords = useMemo(
() => records.filter((record) => {
const matchesKeyword = !keyword || [record.taskNo, record.content, record.reviewReason, record.rejectReason].join(' ').includes(keyword);
const matchesDate = !date || record.createdAt.startsWith(date);
return matchesKeyword && matchesDate;
}),
[date, keyword, records],
);
async function approveRecord(record: RiskReviewTask) {
await adminApi.approveRiskReviewTask(record.id, '运营审核通过');
setApproveTarget(null);
loadData();
}
async function approveBatch() {
await Promise.all(selectedIds.map((id) => adminApi.approveRiskReviewTask(id, '运营批量审核通过')));
setApproveTarget(null);
setSelectedIds([]);
loadData();
}
async function rejectRecord() {
if (!rejectTarget) return;
await adminApi.rejectRiskReviewTask(rejectTarget.id, rejectReason || '运营审核驳回');
setRejectTarget(null);
setRejectReason('');
loadData();
}
const selectableIds = filteredRecords.filter((item) => item.status === 'pending_review').map((item) => item.id);
const allSelected = selectableIds.length > 0 && selectableIds.every((id) => selectedIds.includes(id));
const columns: Array<TableColumn<RiskReviewTask>> = [
{
key: 'selection',
title: <input aria-label="全选当前筛选结果" checked={allSelected} disabled={selectableIds.length === 0} onChange={(event) => setSelectedIds(event.target.checked ? selectableIds : [])} type="checkbox" />,
width: '54px',
render: (record) => <input aria-label={`选择审核任务${record.taskNo}`} checked={selectedIds.includes(record.id)} disabled={record.status !== 'pending_review'} onChange={(event) => setSelectedIds((current) => event.target.checked ? [...new Set([...current, record.id])] : current.filter((id) => id !== record.id))} type="checkbox" />,
},
{ key: 'taskNo', title: '任务编号', width: '180px', render: (record) => <strong>{record.taskNo}</strong> },
{ key: 'sourceType', title: '审核来源', width: '180px', render: (record) => <Tag tone={record.sourceType === 'cmpp_template_mismatch' ? 'warning' : 'info'}>{sourceLabel(record.sourceType)}</Tag> },
{ key: 'content', title: '短信内容', render: (record) => <span className="table-long-text">{record.content}</span> },
{ key: 'phoneTotal', title: '聚合号码数', width: '140px', render: (record) => (record._count?.messageRecords ?? record.phoneTotal).toLocaleString('zh-CN') },
{ key: 'createdAt', title: '提交时间', width: '190px', render: (record) => record.createdAt },
{ key: 'reason', title: '审核原因', render: (record) => record.reviewReason ?? record.rejectReason ?? record.riskHits?.map((item) => item.reason).join('') ?? '-' },
{
key: 'status',
title: '状态',
width: '130px',
render: (record) => <Tag tone={statusTone[record.status] ?? 'warning'}>{statusLabel[record.status] ?? record.status}</Tag>,
},
{
key: 'actions',
title: '操作',
width: '160px',
align: 'right',
render: (record) => record.status === 'pending_review' ? (
<div className="audit-actions">
<Button icon={<Check size={15} />} onClick={() => setApproveTarget(record)} size="sm" variant="success">通过</Button>
<Button icon={<X size={15} />} onClick={() => setRejectTarget(record)} size="sm" variant="danger">驳回</Button>
</div>
) : null,
},
];
return (
<section className="page-stack admin-audit-page">
<Breadcrumb items={['审核中心', '短信审核']} />
{error ? <p className="form-error">{error}</p> : null}
<div className="surface sms-audit-filter">
<h2>筛选条件</h2>
<div className="audit-filter-grid audit-filter-grid--sms">
<Select
label="审核状态"
onChange={(event) => setStatus(event.target.value)}
options={[
{ label: '全部状态', value: 'all' },
{ label: '待审核', value: 'pending_review' },
{ label: '已通过', value: 'approved' },
{ label: '已驳回', value: 'rejected' },
]}
value={status}
/>
<Input label="短信内容" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入任务编号、内容或审核原因" value={keyword} />
<Input label="提交日期" onChange={(event) => setDate(event.target.value)} placeholder="yyyy-mm-dd" prefix={<CalendarDays size={16} />} value={date} />
<div className="audit-filter-actions">
<Button icon={<Search size={17} />} onClick={loadData}>查询</Button>
<Button onClick={() => { setKeyword(''); setDate(''); setStatus('pending_review'); }} variant="ghost">重置</Button>
</div>
</div>
<div className="sms-bulk-actions">
<span>已选择 {selectedIds.length} 条待审核任务</span>
<Button disabled={selectedIds.length === 0} icon={<Check size={16} />} onClick={() => setApproveTarget('batch')} variant="success">通过已选</Button>
</div>
</div>
<div className="surface sms-audit-list">
<Table columns={columns} data={filteredRecords} emptyText="暂无短信审核任务" rowKey="id" />
</div>
<Modal
footer={(
<>
<Button onClick={() => setApproveTarget(null)} variant="ghost">取消</Button>
<Button onClick={() => approveTarget === 'batch' ? void approveBatch() : approveTarget ? void approveRecord(approveTarget) : undefined} variant="success">确认通过</Button>
</>
)}
onClose={() => setApproveTarget(null)}
open={Boolean(approveTarget)}
title="确认通过"
>
<p>{approveTarget === 'batch' ? `确认通过已选择的 ${selectedIds.length} 条待审核任务?` : '确认通过该短信审核任务?'}</p>
</Modal>
<Modal
footer={(
<>
<Button onClick={() => setRejectTarget(null)} variant="ghost">取消</Button>
<Button disabled={!rejectReason.trim()} onClick={() => void rejectRecord()} variant="danger">确认驳回</Button>
</>
)}
onClose={() => setRejectTarget(null)}
open={Boolean(rejectTarget)}
title="确认驳回"
>
<Textarea label="驳回原因" onChange={(event) => setRejectReason(event.target.value)} rows={4} value={rejectReason} />
</Modal>
</section>
);
}