import { useEffect, useMemo, useState } from 'react'; import { ChevronDown, ChevronRight, Edit3, FileText, Info, Plus, Search, Trash2, Upload } from 'lucide-react'; import { adminApi, type ApplicationReportField, type ClientSmsApplication, type ClientSmsSignature, type FileRef, type TenantOption } from '@/api/adminApi'; import { Breadcrumb, Button, FileActions, Input, Modal, Pagination, Select, Tabs, Tag, Textarea } from '@/components/ui'; import { displayFileName } from '@/utils/fileName'; import { formatDateTime } from '@/utils/dateTime'; type CarrierStatus = 'approved' | 'pending' | 'rejected' | 'filing'; type DrainageInfo = { id: string; siteName: string; url: string; field1File?: UploadedFileRef | null; field2?: string; field3?: string; field4?: string; field5?: string; field6?: string; field7File?: UploadedFileRef | null; field8?: string; field9?: string; field10?: string; mobile: CarrierStatus; unicom: CarrierStatus; telecom: CarrierStatus; submittedAt: string; remark: string; reportValues: ReportValues; auditStatus?: string; rejectReason?: string | null; }; type UploadedFileRef = FileRef; type ReportValues = Record; type SignatureFormState = { tenantId: string; applicationId: string; name: string; purpose: string; mobile: CarrierStatus; unicom: CarrierStatus; telecom: CarrierStatus; reportValues: ReportValues; }; type CarrierReportSummary = { status: string; approved: number; total: number }; type SignatureCardTone = 'green' | 'blue' | 'amber' | 'red' | 'gray'; function CarrierReportTag({ summary }: { summary?: CarrierReportSummary }) { if (!summary || summary.status === 'not_applicable' || summary.total === 0) return 不适用; let label = '未报备'; let tone: 'success' | 'danger' | 'warning' | 'info' | 'neutral' = 'neutral'; if (summary.status === 'approved') { label = '全部通过'; tone = 'success'; } else if (summary.status === 'failed' || summary.status === 'rejected') { label = '报备失败'; tone = 'danger'; } else if (summary.status === 'waiting_material') { label = '资料待补充'; tone = 'warning'; } else if (summary.approved > 0) { label = '部分通过'; tone = 'info'; } else if (summary.status === 'reporting' || summary.status === 'exporting') { label = '报备中'; tone = 'warning'; } return {label}({summary.approved}/{summary.total}); } function signatureCardVisual(auditStatus: string, summaries?: Record) { if (auditStatus === 'rejected') return { label: '签名审核已驳回', tone: 'red' as SignatureCardTone }; if (auditStatus === 'pending') return { label: '签名待审核', tone: 'amber' as SignatureCardTone }; if (auditStatus !== 'approved') return { label: '签名尚未提交审核', tone: 'gray' as SignatureCardTone }; const values = Object.values(summaries ?? {}); const applicable = values.filter((summary) => summary.total > 0 && summary.status !== 'not_applicable'); if (applicable.some((summary) => ['failed', 'rejected'].includes(summary.status))) return { label: '存在报备失败', tone: 'red' as SignatureCardTone }; if (applicable.some((summary) => summary.approved > 0 && summary.approved < summary.total)) return { label: '部分通道报备通过', tone: 'blue' as SignatureCardTone }; if (applicable.some((summary) => summary.status === 'waiting_material')) return { label: '报备资料待补充', tone: 'amber' as SignatureCardTone }; if (applicable.some((summary) => ['reporting', 'exporting'].includes(summary.status))) return { label: '通道报备处理中', tone: 'amber' as SignatureCardTone }; if (applicable.length > 0 && applicable.every((summary) => summary.status === 'approved')) return { label: '所有目标通道报备通过', tone: 'green' as SignatureCardTone }; if (applicable.some((summary) => summary.approved > 0)) return { label: '部分运营商报备通过', tone: 'blue' as SignatureCardTone }; return { label: applicable.length > 0 ? '目标通道尚未报备' : '没有适用的目标通道', tone: 'gray' as SignatureCardTone }; } function AuditStatusTag({ status }: { status: string }) { const meta: Record = { draft: { label: '草稿', tone: 'neutral' }, pending: { label: '待审核', tone: 'info' }, approved: { label: '已通过', tone: 'success' }, rejected: { label: '已驳回', tone: 'danger' }, }; const current = meta[status] ?? { label: status || '-', tone: 'neutral' as const }; return {current.label}; } function readDrainagePayload(signature: ClientSmsSignature) { const payload = signature.drainageInfo && typeof signature.drainageInfo === 'object' ? signature.drainageInfo : {}; const carrierStatus = typeof payload.carrierStatus === 'object' && payload.carrierStatus ? payload.carrierStatus as Record : {}; const profile = typeof payload.signatureProfile === 'object' && payload.signatureProfile ? payload.signatureProfile as Record : {}; const links = Array.isArray(payload.links) ? payload.links as Array> : []; const fallbackStatus = normalizeCarrierStatus(signature.auditStatus); return { carrierStatus: { mobile: normalizeCarrierStatus(carrierStatus.mobile, fallbackStatus), unicom: normalizeCarrierStatus(carrierStatus.unicom, fallbackStatus), telecom: normalizeCarrierStatus(carrierStatus.telecom, fallbackStatus), }, signatureProfile: profile, signatureReportValues: normalizeReportValues(payload.signatureReportValues), links: links.map((item) => ({ id: String(item.id ?? `drain-${Date.now()}`), siteName: String(item.siteName ?? ''), url: String(item.url ?? ''), field1File: normalizeUploadedFile(item.field1File), field2: String(item.field2 ?? ''), field3: String(item.field3 ?? ''), field4: String(item.field4 ?? ''), field5: String(item.field5 ?? ''), field6: String(item.field6 ?? ''), field7File: normalizeUploadedFile(item.field7File), field8: String(item.field8 ?? ''), field9: String(item.field9 ?? ''), field10: String(item.field10 ?? ''), mobile: normalizeCarrierStatus(item.mobile, 'filing'), unicom: normalizeCarrierStatus(item.unicom, 'filing'), telecom: normalizeCarrierStatus(item.telecom, 'filing'), submittedAt: String(item.submittedAt ?? ''), remark: String(item.remark ?? ''), reportValues: normalizeReportValues(item.reportValues), auditStatus: String(item.auditStatus ?? 'pending'), rejectReason: item.rejectReason ? String(item.rejectReason) : null, })), }; } function buildDrainagePayload(carrierStatus: { mobile: CarrierStatus; unicom: CarrierStatus; telecom: CarrierStatus }, links: DrainageInfo[], signatureProfile?: Record, signatureReportValues?: ReportValues) { return { carrierStatus, links, signatureProfile, signatureReportValues }; } function normalizeReportValues(value: unknown): ReportValues { if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; return Object.fromEntries(Object.entries(value as Record).map(([key, item]) => [key, normalizeUploadedFile(item) ?? String(item ?? '')])); } function hasMissingRequiredReportValue(fields: ApplicationReportField[], values: ReportValues) { return fields.some((field) => field.required && !values[field.code]); } function normalizeUploadedFile(value: unknown): UploadedFileRef | null { if (!value || typeof value !== 'object') return null; const item = value as Record; const fileObjectId = String(item.fileObjectId ?? ''); const fileName = String(item.fileName ?? ''); const contentType = typeof item.contentType === 'string' ? item.contentType : undefined; return fileObjectId || fileName ? { contentType, fileObjectId, fileName } : null; } function normalizeCarrierStatus(value: unknown, fallback: CarrierStatus = 'filing'): CarrierStatus { return value === 'approved' || value === 'pending' || value === 'rejected' || value === 'filing' ? value : fallback; } function formatDate(value?: string) { return formatDateTime(value); } function SignatureUploadBox({ compact = false, file, label, onUploaded, }: { compact?: boolean; file?: UploadedFileRef | null; label: string; onUploaded: (file: UploadedFileRef) => void; }) { const [uploading, setUploading] = useState(false); const [error, setError] = useState(''); async function uploadFile(fileInput: File | undefined) { if (!fileInput) return; setUploading(true); setError(''); try { const fileObject = await adminApi.uploadFileObject(fileInput, { purpose: 'signature_report_material', prefix: 'signature-report-materials' }); onUploaded({ contentType: fileObject.contentType, fileObjectId: fileObject.id, fileName: fileObject.fileName }); } catch (failure) { setError(failure instanceof Error ? failure.message : '文件上传失败'); } finally { setUploading(false); } } return ( ); } function DynamicReportFields({ fields, onChange, title, values }: { fields: ApplicationReportField[]; onChange: (code: string, value: string | UploadedFileRef | null) => void; title: string; values: ReportValues }) { const [explanationOpen, setExplanationOpen] = useState(false); if (fields.length === 0) return null; const channels = Array.from(new Map(fields.flatMap((field) => field.channels).map((channel) => [channel.id, channel])).values()); const groups = Array.from(new Map(channels.map((channel) => [channel.groupId, channel.groupName])).entries()); const requiredCount = fields.filter((field) => field.required).length; const commonCount = fields.filter((field) => (field.commonReportTypes?.length ?? 0) > 0).length; return (

{title}

当前要求由 {commonCount} 项通用字段及 {groups.length} 个通道组、{channels.length} 个通道配置合并生成,共 {fields.length} 项,其中 {requiredCount} 项必填。保存时会固化本次要求快照。
{fields.map((field) => { const channelHint = field.channels.map((channel) => channel.name).join('、'); const requiredChannels = field.required ? field.channels.filter((channel) => channel.required).map((channel) => channel.name).join('、') : ''; const isCommon = (field.commonReportTypes?.length ?? 0) > 0; const label = `${field.required ? '* ' : ''}${field.name}`; const hint = isCommon ? `平台通用${field.required ? '必填' : '选填'}资料${channelHint ? `,适用于:${channelHint}` : ''}` : field.required ? `由 ${requiredChannels} 要求,至少一个通道配置为必填` : `适用通道:${channelHint}`; return field.fieldType === 'file' || field.fieldType === 'image' ? (
onChange(field.code, file)} /> {hint}
) : (
onChange(field.code, event.target.value)} placeholder={field.description ?? `请输入${field.name}`} required={field.required} value={typeof values[field.code] === 'string' ? values[field.code] as string : ''} /> {hint}
); })}
setExplanationOpen(false)}>我知道了} onClose={() => setExplanationOpen(false)} open={explanationOpen} size="xl" title="这些资料从哪里来?">

资料要求由“报备字段库通用配置”和“企业应用 → 通道组 → 通道 → 通道报备字段”实时合并;相同字段只填写一次,但会按目标通道分别用于报备。

{commonCount > 0 ? (

平台通用字段

    {fields.filter((field) => (field.commonReportTypes?.length ?? 0) > 0).map((field) =>
  • {field.name} · {field.commonReportTypes?.includes('signature') ? '签名报备' : '引流信息报备'} · {field.required ? '必填' : '选填'}
  • )}
) : null} {groups.map(([groupId, groupName]) => (

通道组:{groupName}

{channels.filter((channel) => channel.groupId === groupId).map((channel) => (
{channel.name}({channel.code})
    {fields.filter((field) => field.channels.some((source) => source.id === channel.id)).map((field) => (
  • {field.name} · {field.channels.find((source) => source.id === channel.id)?.reportType === 'both' ? '签名和引流共用' : field.channels.find((source) => source.id === channel.id)?.reportType === 'signature' ? '签名报备' : '引流信息报备'} · {field.channels.find((source) => source.id === channel.id)?.required ? '必填' : '选填'}
  • ))}
))}
))}
); } function SignatureFormModal({ applications, item, onClose, onSubmit, tenants, }: { applications: ClientSmsApplication[]; item?: ClientSmsSignature; onClose: () => void; onSubmit: (state: SignatureFormState) => void; tenants: TenantOption[]; }) { const payload = item ? readDrainagePayload(item) : null; const [form, setForm] = useState({ tenantId: item?.tenantId ?? '', applicationId: item?.applicationId ?? '', name: item?.name ?? '', purpose: item?.purpose ?? '', mobile: payload?.carrierStatus.mobile ?? 'filing', unicom: payload?.carrierStatus.unicom ?? 'filing', telecom: payload?.carrierStatus.telecom ?? 'filing', reportValues: payload?.signatureReportValues ?? {}, }); const [reportFields, setReportFields] = useState([]); const tenantApplications = applications.filter((application) => application.tenantId === form.tenantId && application.status !== 'deleted'); useEffect(() => { const request = form.applicationId ? adminApi.listApplicationReportFields(form.applicationId, 'signature') : adminApi.listCommonApplicationReportFields('signature'); request.then(setReportFields).catch(() => setReportFields([])); }, [form.applicationId]); function update(key: Key, value: SignatureFormState[Key]) { setForm((current) => ({ ...current, [key]: value })); } function updateReportValue(code: string, value: string | UploadedFileRef | null) { setForm((current) => ({ ...current, reportValues: { ...current.reportValues, [code]: value } })); } return ( )} onClose={onClose} open size="xl" title={(

{item ? '编辑签名' : '添加签名'}

{item ? '修改短信签名的相关信息' : '新增短信签名的相关信息'}

)} >

基本信息

签名需履行报备,并遵照管理部门审核结果方可使用。请用 PNG、JPG、JPEG 或 PDF 格式上传真实材料。
update('applicationId', event.target.value)} options={[ { label: '不绑定应用', value: '' }, ...tenantApplications.map((application) => ({ label: application.name, value: application.id })), ]} value={form.applicationId} /> update('name', event.target.value)} placeholder="请输入短信签名,如【XXXX公司】" required value={form.name} />
); } function DrainageFormModal({ applicationId, item, onClose, onSubmit }: { applicationId?: string | null; item?: DrainageInfo; onClose: () => void; onSubmit: (item: DrainageInfo) => void }) { const [reportFields, setReportFields] = useState([]); const [form, setForm] = useState(() => item ?? { id: `drain-${Date.now()}`, siteName: '', url: '', field1File: null, field2: '', field3: '', field4: '', field5: '', field6: '', field7File: null, field8: '', field9: '', field10: '', mobile: 'filing', unicom: 'filing', telecom: 'filing', submittedAt: formatDateTime(new Date()), remark: '', reportValues: {}, }); useEffect(() => { const request = applicationId ? adminApi.listApplicationReportFields(applicationId, 'drainage') : adminApi.listCommonApplicationReportFields('drainage'); request.then(setReportFields).catch(() => setReportFields([])); }, [applicationId]); function update(key: Key, value: DrainageInfo[Key]) { setForm((current) => ({ ...current, [key]: value })); } function updateReportValue(code: string, value: string | UploadedFileRef | null) { setForm((current) => ({ ...current, reportValues: { ...current.reportValues, [code]: value } })); } return ( )} onClose={onClose} open size="xl" title={item ? '编辑引流信息' : '添加引流信息'} >

基本信息

update('url', event.target.value)} placeholder="请输入引流网址" required value={form.url} />
  1. 本页面中所填的信息需与短信内容应用所包含的网站或服务保持一致;
  2. 图片仅支持 PNG、JPG 或 JPEG 格式的正版文件,且大小不超过 3M;
  3. 文件格式支持 PDF 格式或者图片,且大小不超过 10M。
update('siteName', event.target.value)} placeholder="请输入引流信息" value={form.siteName} />