110 lines
8.3 KiB
TypeScript
110 lines
8.3 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Eye, Search, X } from 'lucide-react';
|
|
import { adminApi, type ClientSmsSignature, type FileRef } from '@/api/adminApi';
|
|
import { Breadcrumb, Button, FileActions, Input, Modal, RiskAction, Select, Table, Tag, Textarea, type TableColumn } from '@/components/ui';
|
|
import { formatDateTime } from '@/utils/dateTime';
|
|
|
|
const statusMeta: Record<string, { label: string; tone: 'neutral' | 'info' | 'success' | 'danger' }> = {
|
|
draft: { label: '草稿', tone: 'neutral' },
|
|
pending: { label: '待审核', tone: 'info' },
|
|
approved: { label: '已通过', tone: 'success' },
|
|
rejected: { label: '已驳回', tone: 'danger' },
|
|
};
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function fileRef(value: unknown): FileRef | null {
|
|
const item = asRecord(value);
|
|
return item.fileObjectId && item.fileName ? {
|
|
fileObjectId: String(item.fileObjectId),
|
|
fileName: String(item.fileName),
|
|
contentType: item.contentType ? String(item.contentType) : undefined,
|
|
} : null;
|
|
}
|
|
|
|
function canReviewSignature(item: ClientSmsSignature) {
|
|
return ['draft', 'pending'].includes(item.auditStatus);
|
|
}
|
|
|
|
function SignatureDetail({ item, onClose }: { item: ClientSmsSignature; onClose: () => void }) {
|
|
const payload = asRecord(item.drainageInfo);
|
|
const profile = asRecord(payload.signatureProfile);
|
|
const values = asRecord(payload.signatureReportValues);
|
|
const profileFiles = ['credentialFile', 'legalFrontFile', 'legalBackFile', 'responsibleFrontFile', 'responsibleBackFile']
|
|
.map((key) => fileRef(profile[key]))
|
|
.filter((value): value is FileRef => Boolean(value));
|
|
const materialFiles: FileRef[] = (item.materials ?? []).flatMap((value) => {
|
|
const material = asRecord(value);
|
|
return material.fileObjectId ? [{
|
|
fileObjectId: String(material.fileObjectId),
|
|
fileName: String(material.title ?? material.fileName ?? material.fileObjectId),
|
|
contentType: material.contentType ? String(material.contentType) : undefined,
|
|
}] : [];
|
|
});
|
|
const files = [...new Map([...profileFiles, ...materialFiles].map((file) => [file.fileObjectId, file])).values()];
|
|
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.name}</strong></div>
|
|
<div><span>审核状态</span><Tag tone={(statusMeta[item.auditStatus] ?? statusMeta.draft).tone}>{(statusMeta[item.auditStatus] ?? statusMeta.draft).label}</Tag></div>
|
|
<div><span>签名依据</span><strong>{String(profile.basis ?? '-')}</strong></div>
|
|
<div><span>公司名称</span><strong>{String(profile.companyName ?? '-')}</strong></div>
|
|
<div><span>统一社会信用代码</span><strong>{String(profile.creditCode ?? '-')}</strong></div>
|
|
<div><span>法人</span><strong>{String(profile.legalPersonName ?? '-')}</strong></div>
|
|
<div><span>责任人</span><strong>{String(profile.responsibleName ?? '-')}</strong></div>
|
|
<div><span>责任人手机</span><strong>{String(profile.responsiblePhone ?? '-')}</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="table-actions" style={{ marginTop: 12 }}>{files.length ? files.map((file) => <FileActions file={file} key={file.fileObjectId} />) : <span className="muted">无</span>}</div></div>
|
|
<div className="surface" style={{ padding: 16 }}><strong>通道动态报备资料</strong><div className="detail-grid" style={{ marginTop: 12 }}>{Object.entries(values).length ? Object.entries(values).map(([key, value]) => <div key={key}><span>{key}</span><strong>{fileRef(value) ? <FileActions file={fileRef(value)} /> : String(value ?? '-')}</strong></div>) : <span className="muted">无</span>}</div></div>
|
|
</div>
|
|
</Modal>;
|
|
}
|
|
|
|
export function AdminSignatureAuditPage() {
|
|
const [items, setItems] = useState<ClientSmsSignature[]>([]);
|
|
const [keyword, setKeyword] = useState('');
|
|
const [status, setStatus] = useState('pending');
|
|
const [detail, setDetail] = useState<ClientSmsSignature>();
|
|
const [rejectTarget, setRejectTarget] = useState<ClientSmsSignature>();
|
|
const [reason, setReason] = useState('');
|
|
const [error, setError] = useState('');
|
|
|
|
function loadData() {
|
|
adminApi.listEnterpriseSignatures({ keyword, status: status === 'all' ? undefined : status })
|
|
.then((records) => { setItems(records); setError(''); })
|
|
.catch((failure: Error) => setError(failure.message || '签名审核列表加载失败'));
|
|
}
|
|
|
|
useEffect(loadData, [keyword, status]);
|
|
|
|
const visible = useMemo(() => items.filter((item) => status === 'all' || item.auditStatus === status), [items, status]);
|
|
|
|
async function reject() {
|
|
if (!rejectTarget || !reason.trim()) return;
|
|
try { await adminApi.rejectSignature(rejectTarget.id, reason.trim()); setRejectTarget(undefined); setReason(''); loadData(); } catch (failure) { setError(failure instanceof Error ? failure.message : '签名审核驳回失败'); }
|
|
}
|
|
|
|
const columns = useMemo<Array<TableColumn<ClientSmsSignature>>>(() => [
|
|
{ key: 'name', title: '签名', render: (record) => <strong>{record.name}</strong> },
|
|
{ key: 'tenant', title: '企业', render: (record) => record.tenant?.name ?? record.tenantId },
|
|
{ key: 'application', title: '应用', render: (record) => record.application?.name ?? '-' },
|
|
{ key: 'submittedAt', title: '提交时间', render: (record) => formatDateTime(record.updatedAt) },
|
|
{ key: 'status', title: '状态', render: (record) => <Tag tone={(statusMeta[record.auditStatus] ?? statusMeta.draft).tone}>{(statusMeta[record.auditStatus] ?? statusMeta.draft).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><RiskAction disabled={!canReviewSignature(record)} onCompleted={loadData} targetId={record.id} targetType="signature" /><Button disabled={!canReviewSignature(record)} 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}
|
|
<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' }, { label: '草稿', value: 'draft' }]} 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={visible} emptyText="暂无签名审核记录" rowKey="id" /></div>
|
|
{detail ? <SignatureDetail 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>;
|
|
}
|