import { useEffect, useMemo, useState } from 'react'; import { Check, Download, FileText, Plus, Search, Send, Trash2 } from 'lucide-react'; import { Button, DateTimeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui'; import { clientApi, type ClientSmsApplication, type ClientSmsSignature, type ClientSmsTemplate, type ImportPreviewResponse, type SmsBatchTask } from '@/api/adminApi'; type Recipient = { id: string; phone: string; }; type SendMode = 'now' | 'scheduled'; type ReceiverMode = 'manual' | 'import'; export function ClientSendPage() { const [applications, setApplications] = useState([]); const [templates, setTemplates] = useState([]); const [signatures, setSignatures] = useState([]); const [error, setError] = useState(''); const [taskName, setTaskName] = useState(''); const [applicationId, setApplicationId] = useState(''); const [signatureId, setSignatureId] = useState(''); const [templateId, setTemplateId] = useState(''); const [templatePickerOpen, setTemplatePickerOpen] = useState(false); const [templateKeyword, setTemplateKeyword] = useState(''); const [messageContent, setMessageContent] = useState(''); const [sendMode, setSendMode] = useState('now'); const [scheduledAt, setScheduledAt] = useState(''); const [receiverMode, setReceiverMode] = useState('manual'); const [recipients, setRecipients] = useState([{ id: '1', phone: '' }]); const [importContent, setImportContent] = useState(''); const [importFileName, setImportFileName] = useState(''); const [importFileUrl, setImportFileUrl] = useState(''); const [importPreview, setImportPreview] = useState(null); const [importLoading, setImportLoading] = useState(false); const [submittedRecord, setSubmittedRecord] = useState(null); useEffect(() => { Promise.all([clientApi.listApplications(), clientApi.listTemplates({ status: 'approved' }), clientApi.listSignatures()]) .then(([applicationItems, templateItems, signatureItems]) => { setApplications(applicationItems.filter((item) => item.status === 'active')); setTemplates(templateItems); setSignatures(signatureItems.filter((item) => item.auditStatus === 'approved')); setError(''); }) .catch((reason: Error) => setError(reason.message || '短信发送基础数据加载失败')); }, []); useEffect(() => () => { if (importFileUrl) { URL.revokeObjectURL(importFileUrl); } }, [importFileUrl]); const selectedSignature = useMemo( () => signatures.find((item) => item.id === signatureId), [signatures, signatureId], ); const selectedTemplate = useMemo( () => templates.find((item) => item.id === templateId), [templates, templateId], ); const filteredTemplates = templates.filter((item) => ( item.name.includes(templateKeyword) || item.content.includes(templateKeyword) )); const validRecipients = recipients.filter((item) => item.phone.trim()); const importedValidCount = importPreview?.validCount ?? 0; const receiverCount = receiverMode === 'manual' ? validRecipients.length : importedValidCount; const previewText = selectedSignature && messageContent ? `【${selectedSignature.name}】${messageContent}` : messageContent; const wordCount = previewText.length; const smsParts = wordCount > 0 ? Math.max(1, Math.ceil(wordCount / 70)) : 0; const estimatedCount = receiverCount * smsParts; const requiredVariables = selectedTemplate?.variables?.map((item) => item.name) ?? []; const canSubmit = Boolean(taskName && applicationId && signatureId && templateId && receiverCount > 0 && (sendMode === 'now' || scheduledAt)); function updateRecipient(id: string, phone: string) { setRecipients((items) => items.map((item) => (item.id === id ? { ...item, phone } : item))); } function addRecipient() { setRecipients((items) => [...items, { id: Date.now().toString(), phone: '' }]); } function removeRecipient(id: string) { setRecipients((items) => (items.length === 1 ? items : items.filter((item) => item.id !== id))); } function chooseTemplate(id: string) { const template = templates.find((item) => item.id === id); setTemplateId(id); setMessageContent(template?.content ?? ''); setTemplatePickerOpen(false); } function submitTask() { if (!canSubmit) { return; } const submitRequest = receiverMode === 'manual' ? clientApi.createBatchTask({ applicationId, templateId, content: previewText, category: selectedTemplate?.category ?? taskName, phones: validRecipients.map((item) => item.phone.trim()), sendMode: sendMode === 'now' ? 'immediate' : 'scheduled', scheduledAt: sendMode === 'scheduled' ? scheduledAt : undefined, }) : clientApi.confirmImport({ applicationId, templateId, content: previewText, category: selectedTemplate?.category ?? taskName, importContent, requiredVariables, sendMode: sendMode === 'now' ? 'immediate' : 'scheduled', scheduledAt: sendMode === 'scheduled' ? scheduledAt : undefined, }); submitRequest .then((task) => { setSubmittedRecord(task); setError(''); }) .catch((reason: Error) => setError(reason.message || '发送任务提交失败')); } async function previewImportFile(file: File) { setImportLoading(true); setError(''); try { const content = await file.text(); const preview = await clientApi.previewImport({ content, fileName: file.name, delimiter: file.name.endsWith('.tsv') ? '\t' : ',', requiredVariables, }); setImportContent(content); setImportFileName(file.name); setImportFileUrl((current) => { if (current) { URL.revokeObjectURL(current); } return URL.createObjectURL(file); }); setImportPreview(preview); } catch (reason) { setImportContent(''); setImportFileName(''); setImportFileUrl((current) => { if (current) { URL.revokeObjectURL(current); } return ''; }); setImportPreview(null); setError(reason instanceof Error ? reason.message : '导入预览失败'); } finally { setImportLoading(false); } } return (

发送短信

{submittedRecord ? 已提交任务 {submittedRecord.taskNo} : null}
{error ?

{error}

: null}
1

基本信息

setTaskName(event.target.value)} placeholder="请输入任务名称,便于后续查找和管理" value={taskName} />
setSignatureId(event.target.value)} options={[ { label: '选择签名', value: '' }, ...signatures.map((item) => ({ label: item.name, value: item.id })), ]} value={signatureId} />
2

发送时间

{sendMode === 'scheduled' ? ( ) : null}
3

发送对象

{receiverMode === 'manual' ? ( <>
序号 手机号码 操作
{recipients.map((item, index) => (
{index + 1} updateRecipient(item.id, event.target.value)} placeholder="请输入手机号" value={item.phone} />
))}
) : (
导入表格 支持 CSV / TSV / TXT 文本文件。第一列为手机号,后续列名可对应模板变量。 { const file = event.target.files?.[0]; if (file) { void previewImportFile(file); } event.currentTarget.value = ''; }} style={{ display: 'none' }} type="file" /> {importFileName ? ( 文件:{importFileName} {importFileUrl ? ( 下载 ) : null} ) : null} {importPreview ? (
总行数{importPreview.totalRows}
有效号码{importPreview.validCount}
错误行{importPreview.errorCount}
变量校验{requiredVariables.length ? requiredVariables.join(', ') : '无必填变量'}
{importPreview.errors.length ? (
错误明细 {importPreview.errors.slice(0, 5).map((item) => `第${item.rowNumber}行${item.phoneNumber ? ` ${item.phoneNumber}` : ''}:${item.reason}`).join(';')}
) : null}
) : null}
)}

已添加 {receiverCount} 个接收号码