Initial CMPP frontend prototype

This commit is contained in:
hectorzhao
2026-06-30 16:09:46 +08:00
commit 2f3c274a30
98 changed files with 25255 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
import { useMemo, useState } from 'react';
import { Check, FileText, Plus, Search, Send, Trash2 } from 'lucide-react';
import { Button, DateTimeInput, Input, Modal, Select, Tag } from '@/components/ui';
import { clientService, type RecentMessage } from '@/mock';
type Recipient = {
id: string;
phone: string;
};
type SendMode = 'now' | 'scheduled';
type ReceiverMode = 'manual' | 'import';
const smsApplications = [
{ label: '会员营销平台', value: 'member' },
{ label: '订单通知系统', value: 'order' },
{ label: '登录认证服务', value: 'auth' },
];
export function ClientSendPage() {
const templates = clientService.getTemplates();
const signatures = clientService.getSignatures();
const approvedTemplates = templates.filter((item) => item.status === 'approved');
const approvedSignatures = signatures.filter((item) => item.status === 'approved');
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 [sendMode, setSendMode] = useState<SendMode>('now');
const [scheduledAt, setScheduledAt] = useState('');
const [receiverMode, setReceiverMode] = useState<ReceiverMode>('manual');
const [recipients, setRecipients] = useState<Recipient[]>([{ id: '1', phone: '' }]);
const [submittedRecord, setSubmittedRecord] = useState<RecentMessage | null>(null);
const selectedSignature = useMemo(
() => approvedSignatures.find((item) => item.id === signatureId),
[approvedSignatures, signatureId],
);
const selectedTemplate = useMemo(
() => approvedTemplates.find((item) => item.id === templateId),
[approvedTemplates, templateId],
);
const filteredTemplates = approvedTemplates.filter((item) => (
item.name.includes(templateKeyword) || item.content.includes(templateKeyword)
));
const validRecipients = recipients.filter((item) => item.phone.trim());
const previewText = selectedSignature && selectedTemplate
? `${selectedSignature.name}${selectedTemplate.content}`
: '请选择签名和模板';
const wordCount = previewText.length;
const smsParts = Math.max(1, Math.ceil(wordCount / 70));
const estimatedCount = validRecipients.length * smsParts;
const canSubmit = Boolean(taskName && applicationId && signatureId && templateId && validRecipients.length > 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) {
setTemplateId(id);
setTemplatePickerOpen(false);
}
function submitTask() {
if (!canSubmit) {
return;
}
const nextRecord: RecentMessage = {
id: `MSG-${Date.now().toString().slice(-6)}`,
scene: selectedTemplate?.name ?? taskName,
count: estimatedCount,
channel: '华东主通道',
status: sendMode === 'now' ? 'info' : 'warning',
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
};
clientService.addRecentMessage(nextRecord);
setSubmittedRecord(nextRecord);
}
return (
<section className="sms-send-page">
<div className="sms-send-title">
<span className="sms-send-title__icon">
<Send size={22} />
</span>
<h1></h1>
{submittedRecord ? <Tag tone="success"> {submittedRecord.id}</Tag> : null}
</div>
<div className="sms-send-layout">
<div className="sms-send-main">
<section className="send-card">
<div className="send-card__title">
<span>1</span>
<h2></h2>
</div>
<Input
label="任务名称"
onChange={(event) => setTaskName(event.target.value)}
placeholder="请输入任务名称,便于后续查找和管理"
value={taskName}
/>
<div className="send-form-row">
<Select
label="短信应用"
onChange={(event) => setApplicationId(event.target.value)}
options={[{ label: '选择应用', value: '' }, ...smsApplications]}
value={applicationId}
/>
<Select
label="短信签名"
onChange={(event) => setSignatureId(event.target.value)}
options={[
{ label: '选择签名', value: '' },
...approvedSignatures.map((item) => ({ label: item.name, value: item.id })),
]}
value={signatureId}
/>
<label className="ui-field">
<span className="ui-field__label"></span>
<button className="template-trigger" onClick={() => setTemplatePickerOpen(true)} type="button">
<span className={selectedTemplate ? '' : 'template-trigger__placeholder'}>
{selectedTemplate?.name ?? '选择模板'}
</span>
<FileText size={17} />
</button>
</label>
</div>
</section>
<section className="send-card">
<div className="send-card__title">
<span>2</span>
<h2></h2>
</div>
<div className="radio-row">
<label>
<input checked={sendMode === 'now'} onChange={() => setSendMode('now')} type="radio" />
<span></span>
</label>
<label>
<input checked={sendMode === 'scheduled'} onChange={() => setSendMode('scheduled')} type="radio" />
<span></span>
</label>
{sendMode === 'scheduled' ? (
<DateTimeInput onChange={setScheduledAt} value={scheduledAt} />
) : null}
</div>
</section>
<section className="send-card">
<div className="send-card__title">
<span>3</span>
<h2></h2>
</div>
<div className="receiver-tabs">
<button
className={receiverMode === 'manual' ? 'active' : ''}
onClick={() => setReceiverMode('manual')}
type="button"
>
</button>
<button
className={receiverMode === 'import' ? 'active' : ''}
onClick={() => setReceiverMode('import')}
type="button"
>
</button>
</div>
{receiverMode === 'manual' ? (
<>
<div className="receiver-table">
<div className="receiver-table__head">
<span></span>
<span></span>
<span></span>
</div>
{recipients.map((item, index) => (
<div className="receiver-table__row" key={item.id}>
<span>{index + 1}</span>
<input
inputMode="tel"
onChange={(event) => updateRecipient(item.id, event.target.value)}
placeholder="请输入手机号"
value={item.phone}
/>
<button
aria-label="删除接收人"
disabled={recipients.length === 1}
onClick={() => removeRecipient(item.id)}
type="button"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
<button className="add-recipient" onClick={addRecipient} type="button">
<Plus size={16} />
</button>
</>
) : (
<div className="import-panel">
<div className="import-panel__icon">
<FileText size={26} />
</div>
<strong></strong>
<span> .xlsx / .csv </span>
<Button variant="ghost"></Button>
</div>
)}
<p className="send-tip"> {validRecipients.length} </p>
</section>
<div className="send-submit-row">
<Button disabled={!canSubmit} icon={<Send size={18} />} onClick={submitTask}>
</Button>
</div>
</div>
<aside className="sms-preview-card">
<div className="preview-title">
<FileText size={19} />
<h2></h2>
</div>
<div className="phone-preview">
<div>{previewText}</div>
</div>
<div className="preview-stats">
<div>
<span></span>
<strong>{wordCount} </strong>
</div>
<div>
<span></span>
<strong>{estimatedCount || 1} /</strong>
</div>
<div>
<span></span>
<strong>¥0.05 / </strong>
</div>
</div>
<div className="preview-note"> 70 / 67 /</div>
</aside>
</div>
<Modal
footer={<Button variant="ghost" onClick={() => setTemplatePickerOpen(false)}></Button>}
onClose={() => setTemplatePickerOpen(false)}
open={templatePickerOpen}
title="选择短信模板"
>
<div className="template-picker">
<Input
prefix={<Search size={16} />}
onChange={(event) => setTemplateKeyword(event.target.value)}
placeholder="搜索模板名称或内容"
value={templateKeyword}
/>
<div className="template-picker__list">
{filteredTemplates.map((template) => (
<button
className={template.id === templateId ? 'active' : ''}
key={template.id}
onClick={() => chooseTemplate(template.id)}
type="button"
>
<span>
<strong>{template.name}</strong>
<small>{template.content}</small>
</span>
{template.id === templateId ? <Check size={18} /> : null}
</button>
))}
</div>
</div>
</Modal>
</section>
);
}