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

99 lines
7.7 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Check, Eye, Search, X } from 'lucide-react';
import { adminApi, type FileRef, type SmsDrainageInfo } from '@/api/adminApi';
import { AuditReviewInfo, Breadcrumb, Button, FileActions, Input, Modal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
import { ReportImportAuditPanel } from './ReportImportAuditPanel';
const statusMeta: Record<string, { label: string; tone: 'neutral' | 'info' | 'success' | 'danger' }> = {
pending: { label: '待审核', tone: 'info' },
approved: { label: '已通过', tone: 'success' },
rejected: { label: '已驳回', tone: 'danger' },
deleted: { label: '已删除', tone: 'neutral' },
};
function fileRef(value: unknown): FileRef | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const item = value as Record<string, unknown>;
const fileObjectId = String(item.fileObjectId ?? '');
const fileName = String(item.fileName ?? '');
return fileObjectId && fileName ? { fileObjectId, fileName, contentType: item.contentType ? String(item.contentType) : undefined } : null;
}
function DrainageDetail({ item, onClose }: { item: SmsDrainageInfo; onClose: () => void }) {
const values = item.reportValues && typeof item.reportValues === 'object' ? item.reportValues : {};
const meta = statusMeta[item.auditStatus] ?? { label: item.auditStatus, tone: 'neutral' as const };
return <Modal footer={<Button onClick={onClose}>关闭</Button>} onClose={onClose} open size="xl" title="引流信息审核详情">
<div className="page-stack">
<div className="detail-grid">
<div><span>企业</span><strong>{item.tenant?.name ?? item.tenantId}</strong></div>
<div><span>应用</span><strong>{item.application?.name ?? '-'}</strong></div>
<div><span>所属签名</span><strong>{item.signature?.name ?? item.signatureId}</strong></div>
<div><span>审核状态</span><Tag tone={meta.tone}>{meta.label}</Tag></div>
<div><span>站名称</span><strong>{item.siteName}</strong></div>
<div><span>引流地址</span><strong>{item.url}</strong></div>
<div><span>提交时间</span><strong>{formatDateTime(item.submittedAt)}</strong></div>
<AuditReviewInfo targetId={item.id} targetType="sms_drainage_info" />
<div className="detail-grid__wide"><span>备注</span><strong>{item.remark || '-'}</strong></div>
{item.rejectReason ? <div className="detail-grid__wide"><span>驳回原因</span><strong>{item.rejectReason}</strong></div> : null}
</div>
<div className="surface" style={{ padding: 16 }}>
<strong>通道动态报备资料</strong>
<div className="detail-grid" style={{ marginTop: 12 }}>
{Object.keys(values).length ? Object.entries(values).map(([key, value]) => {
const file = fileRef(value);
return <div key={key}><span>{key}</span><strong>{file ? <FileActions file={file} /> : String(value ?? '-')}</strong></div>;
}) : <span className="muted"></span>}
</div>
</div>
</div>
</Modal>;
}
export function AdminDrainageAuditPage() {
const [items, setItems] = useState<SmsDrainageInfo[]>([]);
const [keyword, setKeyword] = useState('');
const [status, setStatus] = useState('pending');
const [detail, setDetail] = useState<SmsDrainageInfo>();
const [rejectTarget, setRejectTarget] = useState<SmsDrainageInfo>();
const [reason, setReason] = useState('');
const [error, setError] = useState('');
function loadData() {
adminApi.listDrainageInfos({ keyword, status: status === 'all' ? undefined : status })
.then((records) => { setItems(records); setError(''); })
.catch((failure: Error) => setError(failure.message || '引流信息审核列表加载失败'));
}
useEffect(loadData, [keyword, status]);
async function approve(item: SmsDrainageInfo) {
try { await adminApi.approveDrainageInfo(item.id); loadData(); } catch (failure) { setError(failure instanceof Error ? failure.message : '引流信息审核通过失败'); }
}
async function reject() {
if (!rejectTarget || !reason.trim()) return;
try { await adminApi.rejectDrainageInfo(rejectTarget.id, reason.trim()); setRejectTarget(undefined); setReason(''); loadData(); } catch (failure) { setError(failure instanceof Error ? failure.message : '引流信息审核驳回失败'); }
}
const columns = useMemo<Array<TableColumn<SmsDrainageInfo>>>(() => [
{ key: 'siteName', title: '站名称/引流地址', render: (record) => <div className="admin-task-enterprise"><strong>{record.siteName}</strong><span>{record.url}</span></div> },
{ key: 'signature', title: '所属签名', render: (record) => record.signature?.name ?? record.signatureId },
{ key: 'tenant', title: '企业/应用', render: (record) => <div className="admin-task-enterprise"><strong>{record.tenant?.name ?? record.tenantId}</strong><span>{record.application?.name ?? '-'}</span></div> },
{ key: 'submittedAt', title: '提交时间', render: (record) => formatDateTime(record.submittedAt) },
{ key: 'status', title: '状态', render: (record) => { const meta = statusMeta[record.auditStatus] ?? { label: record.auditStatus, tone: 'neutral' as const }; return <Tag tone={meta.tone}>{meta.label}</Tag>; } },
{ key: 'actions', title: '操作', align: 'right', render: (record) => <div className="table-actions"><Button icon={<Eye size={15} />} onClick={() => setDetail(record)} size="sm" variant="ghost">详情</Button><Button disabled={record.auditStatus !== 'pending'} icon={<Check size={15} />} onClick={() => void approve(record)} size="sm" variant="success">通过</Button><Button disabled={record.auditStatus !== 'pending'} icon={<X size={15} />} onClick={() => setRejectTarget(record)} size="sm" variant="danger">驳回</Button></div> },
], []);
return <section className="page-stack admin-template-audit-page">
<div className="page-heading"><div><Breadcrumb items={['审核中心', '引流信息审核']} /><h1>引流信息审核</h1></div></div>
{error ? <p className="form-error">{error}</p> : null}
<Tabs items={[
{ label: '单条引流信息审核', value: 'single', content: <div className="page-stack"><div className="surface audit-filter-card"><div className="audit-filter-grid audit-filter-grid--template"><Input label="搜索" onChange={(event) => setKeyword(event.target.value)} placeholder="搜索企业、应用、签名、站点或网址" prefix={<Search size={16} />} value={keyword} /><Select label="审核状态" onChange={(event) => setStatus(event.target.value)} options={[{ label: '全部状态', value: 'all' }, { label: '待审核', value: 'pending' }, { label: '已通过', value: 'approved' }, { label: '已驳回', value: 'rejected' }]} value={status} /><div className="audit-filter-actions"><Button icon={<Search size={17} />} onClick={loadData}>查询</Button><Button onClick={() => { setKeyword(''); setStatus('pending'); }} variant="ghost">重置</Button></div></div></div><div className="surface"><Table columns={columns} data={items} emptyText="暂无引流信息审核记录" rowKey="id" /></div></div> },
{ label: '导入批次审核', value: 'import', content: <ReportImportAuditPanel reportType="drainage" /> },
]} />
{detail ? <DrainageDetail item={detail} onClose={() => setDetail(undefined)} /> : null}
<Modal footer={<><Button onClick={() => setRejectTarget(undefined)} variant="ghost">取消</Button><Button disabled={!reason.trim()} onClick={() => void reject()} variant="danger">确认驳回</Button></>} onClose={() => setRejectTarget(undefined)} open={Boolean(rejectTarget)} title="驳回引流信息"><Textarea label="驳回原因" onChange={(event) => setReason(event.target.value)} rows={4} value={reason} /></Modal>
</section>;
}