177 lines
8.2 KiB
TypeScript
177 lines
8.2 KiB
TypeScript
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>
|
||
);
|
||
}
|