feat: add phone frequency controls and modularize codebase

This commit is contained in:
hectorzhao
2026-07-31 22:25:23 +08:00
parent 0af671b4ed
commit ca4f591a13
216 changed files with 41579 additions and 23694 deletions
+76 -9
View File
@@ -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>
);
}