feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Check, Download, FileText, Plus, Search, Send, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, DateTimeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
|
||||
import { clientApi, type ClientSmsApplication, type ClientSmsSignatureView, type ClientSmsTemplate, type ImportPreviewResponse, type SmsBatchTask } from '@/api/adminApi';
|
||||
import { formatCents } from '@/utils/currency';
|
||||
@@ -14,6 +15,7 @@ type SendMode = 'now' | 'scheduled';
|
||||
type ReceiverMode = 'manual' | 'import';
|
||||
|
||||
export function ClientSendPage() {
|
||||
const navigate = useNavigate();
|
||||
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
|
||||
const [templates, setTemplates] = useState<ClientSmsTemplate[]>([]);
|
||||
const [signatures, setSignatures] = useState<ClientSmsSignatureView[]>([]);
|
||||
@@ -35,6 +37,8 @@ export function ClientSendPage() {
|
||||
const [importPreview, setImportPreview] = useState<ImportPreviewResponse | null>(null);
|
||||
const [importLoading, setImportLoading] = useState(false);
|
||||
const [submittedRecord, setSubmittedRecord] = useState<SmsBatchTask | null>(null);
|
||||
const [submitError, setSubmitError] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([clientApi.listApplications(), clientApi.listTemplates({ status: 'approved' }), clientApi.listSignatures()])
|
||||
@@ -74,8 +78,8 @@ export function ClientSendPage() {
|
||||
const previewText = selectedSignature && messageContent
|
||||
? replaceLeadingSmsSignature(messageContent, selectedSignature.name)
|
||||
: messageContent;
|
||||
const wordCount = previewText.length;
|
||||
const smsParts = wordCount > 0 ? Math.max(1, Math.ceil(wordCount / 70)) : 0;
|
||||
const wordCount = [...previewText].length;
|
||||
const smsParts = wordCount === 0 ? 0 : wordCount <= 70 ? 1 : Math.ceil(wordCount / 67);
|
||||
const estimatedCount = receiverCount * smsParts;
|
||||
const requiredVariables = selectedTemplate?.variables?.map((item) => item.name) ?? [];
|
||||
const canSubmit = Boolean(taskName && applicationId && signatureId && templateId && receiverCount > 0 && (sendMode === 'now' || scheduledAt));
|
||||
@@ -100,10 +104,15 @@ export function ClientSendPage() {
|
||||
}
|
||||
|
||||
function submitTask() {
|
||||
if (!canSubmit) {
|
||||
if (!canSubmit || submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
setSubmitError('');
|
||||
const normalizedScheduledAt = sendMode === 'scheduled' && scheduledAt
|
||||
? `${scheduledAt}:00+08:00`
|
||||
: undefined;
|
||||
const submitRequest = receiverMode === 'manual'
|
||||
? clientApi.createBatchTask({
|
||||
applicationId,
|
||||
@@ -112,7 +121,7 @@ export function ClientSendPage() {
|
||||
category: selectedTemplate?.category ?? taskName,
|
||||
phones: validRecipients.map((item) => item.phone.trim()),
|
||||
sendMode: sendMode === 'now' ? 'immediate' : 'scheduled',
|
||||
scheduledAt: sendMode === 'scheduled' ? scheduledAt : undefined,
|
||||
scheduledAt: normalizedScheduledAt,
|
||||
})
|
||||
: clientApi.confirmImport({
|
||||
applicationId,
|
||||
@@ -122,7 +131,7 @@ export function ClientSendPage() {
|
||||
importContent,
|
||||
requiredVariables,
|
||||
sendMode: sendMode === 'now' ? 'immediate' : 'scheduled',
|
||||
scheduledAt: sendMode === 'scheduled' ? scheduledAt : undefined,
|
||||
scheduledAt: normalizedScheduledAt,
|
||||
});
|
||||
|
||||
submitRequest
|
||||
@@ -130,7 +139,33 @@ export function ClientSendPage() {
|
||||
setSubmittedRecord(task);
|
||||
setError('');
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '发送任务提交失败'));
|
||||
.catch((reason: Error) => setSubmitError(reason.message || '发送任务提交失败'))
|
||||
.finally(() => setSubmitting(false));
|
||||
}
|
||||
|
||||
function resetSendForm() {
|
||||
setTaskName('');
|
||||
setApplicationId('');
|
||||
setSignatureId('');
|
||||
setTemplateId('');
|
||||
setTemplateKeyword('');
|
||||
setMessageContent('');
|
||||
setSendMode('now');
|
||||
setScheduledAt('');
|
||||
setReceiverMode('manual');
|
||||
setRecipients([{ id: Date.now().toString(), phone: '' }]);
|
||||
setImportContent('');
|
||||
setImportFileName('');
|
||||
setImportFileUrl((current) => {
|
||||
if (current) {
|
||||
URL.revokeObjectURL(current);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
setImportPreview(null);
|
||||
setSubmittedRecord(null);
|
||||
setSubmitError('');
|
||||
setError('');
|
||||
}
|
||||
|
||||
async function previewImportFile(file: File) {
|
||||
@@ -352,8 +387,8 @@ export function ClientSendPage() {
|
||||
</section>
|
||||
|
||||
<div className="send-submit-row">
|
||||
<Button disabled={!canSubmit} icon={<Send size={18} />} onClick={submitTask}>
|
||||
提交发送任务
|
||||
<Button disabled={!canSubmit || submitting} icon={<Send size={18} />} onClick={submitTask}>
|
||||
{submitting ? '提交中...' : '提交发送任务'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,7 +419,7 @@ export function ClientSendPage() {
|
||||
</div>
|
||||
<div>
|
||||
<span>单价</span>
|
||||
<strong>¥{formatCents(selectedApplication?.customerUnitPrice)} / 人</strong>
|
||||
<strong>¥{formatCents(selectedApplication?.customerUnitPrice)} / 条</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="preview-note">短信按 70 字/条计费,超出部分按 67 字/条计算</div>
|
||||
@@ -422,6 +457,38 @@ export function ClientSendPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{submitError ? (
|
||||
<Modal
|
||||
footer={<Button onClick={() => setSubmitError('')}>我知道了</Button>}
|
||||
onClose={() => setSubmitError('')}
|
||||
open
|
||||
title="提交发送任务失败"
|
||||
>
|
||||
<p className="form-error">{submitError}</p>
|
||||
</Modal>
|
||||
) : null}
|
||||
|
||||
{submittedRecord ? (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={resetSendForm} variant="ghost">继续发送短信</Button>
|
||||
<Button onClick={() => navigate(`/client/batch-tasks?taskNo=${encodeURIComponent(submittedRecord.taskNo)}`)}>
|
||||
查看任务进度
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={() => setSubmittedRecord(null)}
|
||||
open
|
||||
title="发送任务提交成功"
|
||||
>
|
||||
<div className="detail-grid">
|
||||
<div><span>任务编号</span><strong>{submittedRecord.taskNo}</strong></div>
|
||||
<div><span>发送号码数</span><strong>{submittedRecord.phoneTotal.toLocaleString('zh-CN')} 个</strong></div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user