fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Check, 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 SmsBatchTask } from '@/api/adminApi';
|
||||
import { clientApi, type ClientSmsApplication, type ClientSmsSignature, type ClientSmsTemplate, type ImportPreviewResponse, type SmsBatchTask } from '@/api/adminApi';
|
||||
|
||||
type Recipient = {
|
||||
id: string;
|
||||
@@ -27,6 +27,10 @@ export function ClientSendPage() {
|
||||
const [scheduledAt, setScheduledAt] = useState('');
|
||||
const [receiverMode, setReceiverMode] = useState<ReceiverMode>('manual');
|
||||
const [recipients, setRecipients] = useState<Recipient[]>([{ id: '1', phone: '' }]);
|
||||
const [importContent, setImportContent] = useState('');
|
||||
const [importFileName, setImportFileName] = useState('');
|
||||
const [importPreview, setImportPreview] = useState<ImportPreviewResponse | null>(null);
|
||||
const [importLoading, setImportLoading] = useState(false);
|
||||
const [submittedRecord, setSubmittedRecord] = useState<SmsBatchTask | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,13 +56,16 @@ export function ClientSendPage() {
|
||||
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 = validRecipients.length * smsParts;
|
||||
const canSubmit = Boolean(taskName && applicationId && signatureId && templateId && validRecipients.length > 0 && (sendMode === 'now' || scheduledAt));
|
||||
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)));
|
||||
@@ -84,15 +91,28 @@ export function ClientSendPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
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('');
|
||||
@@ -100,6 +120,30 @@ export function ClientSendPage() {
|
||||
.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);
|
||||
setImportPreview(preview);
|
||||
} catch (reason) {
|
||||
setImportContent('');
|
||||
setImportFileName('');
|
||||
setImportPreview(null);
|
||||
setError(reason instanceof Error ? reason.message : '导入预览失败');
|
||||
} finally {
|
||||
setImportLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="sms-send-page">
|
||||
<div className="sms-send-title">
|
||||
@@ -233,12 +277,42 @@ export function ClientSendPage() {
|
||||
<FileText size={26} />
|
||||
</div>
|
||||
<strong>导入表格</strong>
|
||||
<span>支持 .xlsx / .csv 文件,当前原型仅展示上传入口。</span>
|
||||
<Button variant="ghost">选择文件</Button>
|
||||
<span>支持 CSV / TSV / TXT 文本文件。第一列为手机号,后续列名可对应模板变量。</span>
|
||||
<input
|
||||
accept=".csv,.tsv,.txt,text/csv,text/plain"
|
||||
id="sms-import-file"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
void previewImportFile(file);
|
||||
}
|
||||
event.currentTarget.value = '';
|
||||
}}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
/>
|
||||
<Button disabled={importLoading || !templateId} onClick={() => document.getElementById('sms-import-file')?.click()} variant="ghost">
|
||||
{importLoading ? '解析中...' : '选择文件'}
|
||||
</Button>
|
||||
{importFileName ? <span>文件:{importFileName}</span> : null}
|
||||
{importPreview ? (
|
||||
<div className="detail-grid">
|
||||
<div><span>总行数</span><strong>{importPreview.totalRows}</strong></div>
|
||||
<div><span>有效号码</span><strong>{importPreview.validCount}</strong></div>
|
||||
<div><span>错误行</span><strong>{importPreview.errorCount}</strong></div>
|
||||
<div><span>变量校验</span><strong>{requiredVariables.length ? requiredVariables.join(', ') : '无必填变量'}</strong></div>
|
||||
{importPreview.errors.length ? (
|
||||
<div className="detail-grid__wide">
|
||||
<span>错误明细</span>
|
||||
<strong>{importPreview.errors.slice(0, 5).map((item) => `第${item.rowNumber}行${item.phoneNumber ? ` ${item.phoneNumber}` : ''}:${item.reason}`).join(';')}</strong>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="send-tip">已添加 {validRecipients.length} 个接收号码</p>
|
||||
<p className="send-tip">已添加 {receiverCount} 个接收号码</p>
|
||||
</section>
|
||||
|
||||
<div className="send-submit-row">
|
||||
|
||||
Reference in New Issue
Block a user