fix: connect remaining sms pages to real backend
This commit is contained in:
@@ -1,630 +1,53 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Edit3, Eye, FileText, ImageIcon, Info, Music, Plus, Search, Trash2, Video } from 'lucide-react';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Tabs, Tag, Textarea } from '@/components/ui';
|
||||
|
||||
type TemplateKind = 'sms' | 'mms';
|
||||
|
||||
type SmsTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
enterprise: string;
|
||||
application: string;
|
||||
hash: string;
|
||||
content: string;
|
||||
variables: string[];
|
||||
updatedAt: string;
|
||||
accent: 'green' | 'blue' | 'red';
|
||||
};
|
||||
|
||||
type MmsTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
enterprise: string;
|
||||
application: string;
|
||||
code: string;
|
||||
title: string;
|
||||
image: string;
|
||||
content: string;
|
||||
status: 'approved' | 'pending' | 'rejected';
|
||||
updatedAt: string;
|
||||
accent: 'green' | 'blue' | 'gray';
|
||||
frames?: MmsFrame[];
|
||||
};
|
||||
|
||||
type FrameType = 'text' | 'image' | 'video' | 'audio';
|
||||
|
||||
type MmsFrame = {
|
||||
id: string;
|
||||
type: FrameType;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const initialSmsTemplates: SmsTemplate[] = [
|
||||
{ id: 'tpl-1', name: '喜约领券', enterprise: '上海XXXXX科技有限公司', application: '营销推广平台', hash: '1c37fb4a7c4a4a63', content: '【喜约领券】尊宾您好!您于${time}备存物品(${item})过期时间${expireTime}。', variables: ['time', 'item', 'expireTime'], updatedAt: '2026-01-04 17:45:36', accent: 'green' },
|
||||
{ id: 'tpl-2', name: '南通仲裁委', enterprise: '重庆进载数智', application: '客户服务系统', hash: '8e0a32c9d7b14c6a', content: '【南通仲裁委】尊敬的仲裁员,${caseNumber}号件请前往小程序或PC端查看本案信息。', variables: ['caseNumber'], updatedAt: '2026-01-04 17:45:36', accent: 'blue' },
|
||||
{ id: 'tpl-3', name: '派件通知', enterprise: '超感世纪互三网', application: '验证码服务', hash: '6e9f23a4b5c7d8e1', content: '【派件通知】${name}您的快递已到达${station},请保持电话畅通。', variables: ['name', 'station'], updatedAt: '2026-01-04 11:20:18', accent: 'red' },
|
||||
];
|
||||
|
||||
const initialMmsTemplates: MmsTemplate[] = [
|
||||
{ id: 'mms-tpl-1', name: '春节祝福', enterprise: '上海XXXXX科技有限公司', application: '营销活动彩信', code: 'MMS_1a2b3c4d5e6f', title: '新春佳节,福气满满', image: 'https://images.unsplash.com/photo-1519671482749-fd09be7ccebf?auto=format&fit=crop&w=900&q=80', content: '【活动推广】尊敬的客户,新春佳节来临之际,祝您新春快乐,万事如意!', status: 'approved', updatedAt: '2026-01-15 10:30:00', accent: 'green', frames: [{ id: 'mms-frame-1', type: 'text' }, { id: 'mms-frame-2', type: 'image' }] },
|
||||
{ id: 'mms-tpl-2', name: '新品发布', enterprise: '重庆进载数智', application: '营销活动彩信', code: 'MMS_2b3c4d5e6f7a', title: '重磅新品震撼来袭', image: 'https://images.unsplash.com/photo-1434494878577-86c23bcb06b9?auto=format&fit=crop&w=900&q=80', content: '【新品发布】优品商城重磅新品无线蓝牙耳机震撼来袭!', status: 'pending', updatedAt: '2026-01-16 14:20:00', accent: 'blue', frames: [{ id: 'mms-frame-3', type: 'text' }, { id: 'mms-frame-4', type: 'image' }] },
|
||||
{ id: 'mms-tpl-3', name: '促销活动', enterprise: '超感世纪互三网', application: '节日祝福彩信', code: 'MMS_6f7a8b9c0d1e', title: '限时抢购,低至3折', image: 'https://images.unsplash.com/photo-1607083206968-13611e3d76db?auto=format&fit=crop&w=900&q=80', content: '【活动推广】年中大促火热进行中!精选商品限时抢购。', status: 'rejected', updatedAt: '2026-01-11 15:30:00', accent: 'gray', frames: [{ id: 'mms-frame-5', type: 'video' }, { id: 'mms-frame-6', type: 'text' }] },
|
||||
];
|
||||
|
||||
const frameTypeOptions = [
|
||||
{ label: '文字', value: 'text' },
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '音频', value: 'audio' },
|
||||
];
|
||||
|
||||
const frameIconMap: Record<FrameType, typeof FileText> = {
|
||||
text: FileText,
|
||||
image: ImageIcon,
|
||||
video: Video,
|
||||
audio: Music,
|
||||
};
|
||||
|
||||
const frameFormatMap: Record<FrameType, string> = {
|
||||
text: '',
|
||||
image: '支持格式:jpg, jpeg, png, gif',
|
||||
video: '支持格式:mp4, mpg, 3gp, 3gpp',
|
||||
audio: '支持格式:mp3, mpeg3',
|
||||
};
|
||||
|
||||
const applicationOptions = [
|
||||
{ label: '请选择', value: '' },
|
||||
{ label: '营销推广平台', value: '营销推广平台' },
|
||||
{ label: '客户服务系统', value: '客户服务系统' },
|
||||
{ label: '验证码服务', value: '验证码服务' },
|
||||
];
|
||||
|
||||
const signatureOptions = [
|
||||
{ label: '请选择', value: '' },
|
||||
{ label: '【科技公司】', value: '【科技公司】' },
|
||||
{ label: '【客户服务】', value: '【客户服务】' },
|
||||
{ label: '【促销活动】', value: '【促销活动】' },
|
||||
];
|
||||
|
||||
const recommendedVariables = [
|
||||
['验证码', 'code'],
|
||||
['手机号', 'phone'],
|
||||
['姓名', 'name'],
|
||||
['日期', 'date'],
|
||||
['金额', 'amount'],
|
||||
['时间', 'time'],
|
||||
['余额', 'balance'],
|
||||
['地址', 'address'],
|
||||
['天数', 'days'],
|
||||
['快递单号', 'trackingNumber'],
|
||||
['案件号', 'caseNumber'],
|
||||
['课程名称', 'courseName'],
|
||||
['链接', 'link'],
|
||||
['站点', 'station'],
|
||||
];
|
||||
|
||||
const statusToneMap = {
|
||||
approved: 'success',
|
||||
pending: 'info',
|
||||
rejected: 'danger',
|
||||
} as const;
|
||||
|
||||
const statusLabelMap = {
|
||||
approved: '已通过',
|
||||
pending: '审核中',
|
||||
rejected: '已驳回',
|
||||
};
|
||||
|
||||
function extractVariables(content: string) {
|
||||
return Array.from(content.matchAll(/\$\{([^}]+)\}/g)).map((match) => match[1]);
|
||||
}
|
||||
|
||||
function paginate<T>(items: T[], page: number, pageSize: number) {
|
||||
return items.slice((page - 1) * pageSize, page * pageSize);
|
||||
}
|
||||
|
||||
function Pagination({ page, pageSize, total, onPageChange }: { page: number; pageSize: number; total: number; onPageChange: (page: number) => void }) {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
return (
|
||||
<div className="admin-split-pagination">
|
||||
<span>{pageSize}条/页</span>
|
||||
<Button disabled={page <= 1} icon={<ChevronLeft size={16} />} iconOnly onClick={() => onPageChange(page - 1)} variant="ghost">上一页</Button>
|
||||
<Button size="sm">{page}</Button>
|
||||
<span>/ {totalPages}</span>
|
||||
<Button disabled={page >= totalPages} icon={<ChevronRight size={16} />} iconOnly onClick={() => onPageChange(page + 1)} variant="ghost">下一页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmModal({ message, onCancel, onConfirm }: { message: string; onCancel: () => void; onConfirm: () => void }) {
|
||||
return (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onCancel} variant="ghost">取消</Button>
|
||||
<Button onClick={onConfirm} variant="danger">确认删除</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={onCancel}
|
||||
open
|
||||
title="删除确认"
|
||||
>
|
||||
<p className="admin-confirm-text">{message}</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function SmsTemplateModal({ item, onClose, onSubmit }: { item?: SmsTemplate; onClose: () => void; onSubmit: (item: SmsTemplate) => void }) {
|
||||
const [application, setApplication] = useState(item?.application ?? '');
|
||||
const [signature, setSignature] = useState('');
|
||||
const [content, setContent] = useState(item?.content ?? '');
|
||||
const [customVariable, setCustomVariable] = useState('');
|
||||
const [variablesOpen, setVariablesOpen] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const variables = extractVariables(content);
|
||||
const wordCount = content.length;
|
||||
const billingCount = Math.max(1, Math.ceil(wordCount / 70));
|
||||
|
||||
function insertVariable(name: string) {
|
||||
if (!name.trim()) {
|
||||
return;
|
||||
}
|
||||
setContent((current) => `${current}\${${name.trim()}}`);
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!application || !signature || !content.trim()) {
|
||||
setError('请填写应用、签名和模板内容');
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
id: item?.id ?? `tpl-${Date.now()}`,
|
||||
name: content.replace(/^【([^】]+)】.*$/, '$1').slice(0, 10) || '新建模板',
|
||||
enterprise: item?.enterprise ?? '上海XXXXX科技有限公司',
|
||||
application,
|
||||
hash: item?.hash ?? Math.random().toString(16).slice(2, 18),
|
||||
content,
|
||||
variables,
|
||||
updatedAt: '2026-06-30 10:00:00',
|
||||
accent: item?.accent ?? 'green',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onClose} variant="ghost">取消</Button>
|
||||
<Button onClick={submit}>确认</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>{item ? '编辑模板' : '添加模板'}</h2><p>请填写模板信息</p></div>}
|
||||
>
|
||||
<div className="template-form">
|
||||
<Select
|
||||
error={error && !application ? '请选择应用' : undefined}
|
||||
label="* 应用:"
|
||||
onChange={(event) => setApplication(event.target.value)}
|
||||
options={applicationOptions}
|
||||
value={application}
|
||||
/>
|
||||
<Select
|
||||
error={error && !signature ? '请选择签名' : undefined}
|
||||
label="* 签名:"
|
||||
onChange={(event) => setSignature(event.target.value)}
|
||||
options={signatureOptions}
|
||||
value={signature}
|
||||
/>
|
||||
<Textarea
|
||||
error={error && !content.trim() ? '请输入模板内容' : undefined}
|
||||
label="* 模板内容:"
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
placeholder="请输入模板内容"
|
||||
rows={9}
|
||||
value={content}
|
||||
/>
|
||||
<div className="template-form-meta">
|
||||
<button onClick={() => setVariablesOpen((current) => !current)} type="button">
|
||||
<Plus size={16} /> {variablesOpen ? '收起变量面板' : '插入变量'}
|
||||
</button>
|
||||
<span>{wordCount} 字符(不含变量),计费 {billingCount} 条</span>
|
||||
</div>
|
||||
{variablesOpen ? (
|
||||
<div className="template-variable-panel">
|
||||
<h3>推荐变量</h3>
|
||||
<div className="template-variable-buttons">
|
||||
{recommendedVariables.map(([label, value]) => (
|
||||
<button key={value} onClick={() => insertVariable(value)} type="button">
|
||||
{label} ({value})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<h3>自定义变量</h3>
|
||||
<div className="template-custom-variable">
|
||||
<Input
|
||||
onChange={(event) => setCustomVariable(event.target.value)}
|
||||
placeholder="英文字符或数字"
|
||||
value={customVariable}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
insertVariable(customVariable || 'custom');
|
||||
setCustomVariable('');
|
||||
}}
|
||||
>
|
||||
插入
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="template-info-tip">
|
||||
<Info size={18} />
|
||||
<span>短信字数=签名+模板内容+变量内容,普通短信 70 字符计费 1 条,长短信每 67 字符计算为 1 条短信(包含标点符号和空格)</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function FrameEditor({
|
||||
frame,
|
||||
index,
|
||||
onRemove,
|
||||
onTypeChange,
|
||||
}: {
|
||||
frame: MmsFrame;
|
||||
index: number;
|
||||
onRemove: () => void;
|
||||
onTypeChange: (type: FrameType) => void;
|
||||
}) {
|
||||
const Icon = frameIconMap[frame.type];
|
||||
|
||||
return (
|
||||
<div className="mms-frame">
|
||||
<div className="mms-frame__top">
|
||||
<strong>第 {index + 1} 帧</strong>
|
||||
<Select
|
||||
className="mms-frame-type"
|
||||
onChange={(event) => onTypeChange(event.target.value as FrameType)}
|
||||
options={frameTypeOptions}
|
||||
value={frame.type}
|
||||
/>
|
||||
<button aria-label="删除帧" onClick={onRemove} type="button">
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{frame.type === 'text' ? (
|
||||
<Textarea defaultValue={frame.text} placeholder="请输入文字内容" />
|
||||
) : (
|
||||
<div className="mms-file-drop">
|
||||
<Icon size={22} />
|
||||
<div>
|
||||
<strong>选择文件</strong>
|
||||
<span>未选择任何文件</span>
|
||||
</div>
|
||||
<small>{frameFormatMap[frame.type]}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MmsTemplateModal({ item, onClose, onSubmit }: { item?: MmsTemplate; onClose: () => void; onSubmit: (item: MmsTemplate) => void }) {
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [name, setName] = useState(item?.name ?? '');
|
||||
const [enterprise] = useState(item?.enterprise ?? '上海XXXXX科技有限公司');
|
||||
const [application, setApplication] = useState(item?.application ?? '营销活动彩信');
|
||||
const [title, setTitle] = useState(item?.title ?? '');
|
||||
const [content] = useState(item?.content ?? '这里展示当前彩信模板的文字、图片、视频或音频帧内容。');
|
||||
const [frames, setFrames] = useState<MmsFrame[]>(item?.frames ?? [
|
||||
{ id: 'new-mms-frame-1', type: 'text' },
|
||||
{ id: 'new-mms-frame-2', type: 'image' },
|
||||
]);
|
||||
|
||||
const totalSize = useMemo(() => {
|
||||
const textSize = frames.filter((frame) => frame.type === 'text').length * 0.2;
|
||||
const mediaSize = frames.filter((frame) => frame.type !== 'text').length * 180;
|
||||
return Math.min(2000, textSize + mediaSize).toFixed(1);
|
||||
}, [frames]);
|
||||
|
||||
function addFrame() {
|
||||
if (frames.length >= 9) {
|
||||
return;
|
||||
}
|
||||
setFrames((items) => [...items, { id: `new-mms-frame-${Date.now()}`, type: 'text' }]);
|
||||
}
|
||||
|
||||
function removeFrame(id: string) {
|
||||
setFrames((items) => items.filter((frame) => frame.id !== id));
|
||||
}
|
||||
|
||||
function changeFrameType(id: string, type: FrameType) {
|
||||
setFrames((items) => items.map((frame) => (frame.id === id ? { ...frame, type } : frame)));
|
||||
}
|
||||
|
||||
function submit() {
|
||||
onSubmit({
|
||||
id: item?.id ?? `mms-tpl-${Date.now()}`,
|
||||
name: name || '新建彩信模板',
|
||||
enterprise,
|
||||
application,
|
||||
code: item?.code ?? `MMS_${Math.random().toString(16).slice(2, 14)}`,
|
||||
title,
|
||||
image: item?.image ?? 'https://images.unsplash.com/photo-1607083206968-13611e3d76db?auto=format&fit=crop&w=900&q=80',
|
||||
content,
|
||||
status: item?.status ?? 'pending',
|
||||
updatedAt: '2026-06-30 10:00:00',
|
||||
accent: item?.accent ?? 'blue',
|
||||
frames,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={() => setPreviewOpen(true)} variant="secondary">预览</Button>
|
||||
<Button onClick={onClose} variant="ghost">取消</Button>
|
||||
<Button className="mms-save-button" onClick={submit}>确认</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={(
|
||||
<div className="mms-template-modal-title">
|
||||
<h2>{item ? '编辑彩信模板' : '创建彩信模板'}</h2>
|
||||
<p>彩信最多支持9帧,每帧可以是文字、图片、视频或者音频,内容总大小不超过2000KB。提交后需三大运营商审核。</p>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="mms-template-form">
|
||||
<div className="mms-template-form-grid">
|
||||
<Input label="彩信模板名称 *" onChange={(event) => setName(event.target.value)} placeholder="春节祝福" value={name} />
|
||||
<Select
|
||||
label="彩信应用 *"
|
||||
onChange={(event) => setApplication(event.target.value)}
|
||||
options={[
|
||||
{ label: '营销活动彩信', value: '营销活动彩信' },
|
||||
{ label: '节日祝福彩信', value: '节日祝福彩信' },
|
||||
]}
|
||||
value={application}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
defaultValue="【活动推广】"
|
||||
label="签名 *"
|
||||
options={[
|
||||
{ label: '【活动推广】', value: '【活动推广】' },
|
||||
{ label: '【优品发布】', value: '【优品发布】' },
|
||||
{ label: '【节日祝福】', value: '【节日祝福】' },
|
||||
]}
|
||||
/>
|
||||
<Input label="彩信标题 *" onChange={(event) => setTitle(event.target.value)} placeholder="新春佳节,福气满满" value={title} />
|
||||
|
||||
<div className="mms-frame-header">
|
||||
<div>
|
||||
<strong>彩信内容 *</strong>
|
||||
<span>({frames.length}/9 帧,已使用 {totalSize}KB/2000KB)</span>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={addFrame} variant="ghost">添加帧</Button>
|
||||
</div>
|
||||
|
||||
<div className="mms-frame-list">
|
||||
{frames.map((frame, index) => (
|
||||
<FrameEditor
|
||||
frame={frame}
|
||||
index={index}
|
||||
key={frame.id}
|
||||
onRemove={() => removeFrame(frame.id)}
|
||||
onTypeChange={(type) => changeFrameType(frame.id, type)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
footer={<Button onClick={() => setPreviewOpen(false)}>关闭</Button>}
|
||||
onClose={() => setPreviewOpen(false)}
|
||||
open={previewOpen}
|
||||
size="md"
|
||||
title={<div className="template-modal-title"><h2>当前模板预览</h2><p>{name || '新建彩信模板'}</p></div>}
|
||||
>
|
||||
<div className="mms-preview">
|
||||
{item?.image ? <img alt={item.name} src={item.image} /> : null}
|
||||
<h3>{title || '新春佳节,福气满满'}</h3>
|
||||
<p>{content}</p>
|
||||
<div className="mms-preview-frames">
|
||||
{frames.map((frame, index) => {
|
||||
const Icon = frameIconMap[frame.type];
|
||||
return (
|
||||
<span key={frame.id}>
|
||||
<Icon size={15} />
|
||||
第 {index + 1} 帧 · {frameTypeOptions.find((option) => option.value === frame.type)?.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { adminApi, type ClientSmsTemplate } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
|
||||
export function AdminEnterpriseTemplatesPage() {
|
||||
const [activeTab, setActiveTab] = useState<TemplateKind>('sms');
|
||||
const [smsTemplates, setSmsTemplates] = useState(initialSmsTemplates);
|
||||
const [mmsTemplates, setMmsTemplates] = useState(initialMmsTemplates);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [smsModal, setSmsModal] = useState<SmsTemplate | null | undefined>(undefined);
|
||||
const [mmsModal, setMmsModal] = useState<MmsTemplate | null | undefined>(undefined);
|
||||
const [preview, setPreview] = useState<MmsTemplate | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ kind: TemplateKind; id: string; name: string } | null>(null);
|
||||
const pageSize = 2;
|
||||
const [templates, setTemplates] = useState<ClientSmsTemplate[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const filteredSmsTemplates = useMemo(
|
||||
() => smsTemplates.filter((template) => (
|
||||
(!enterpriseKeyword || template.enterprise.includes(enterpriseKeyword))
|
||||
&& (!templateKeyword || template.name.includes(templateKeyword) || template.application.includes(templateKeyword) || template.content.includes(templateKeyword))
|
||||
)),
|
||||
[enterpriseKeyword, smsTemplates, templateKeyword],
|
||||
);
|
||||
const filteredMmsTemplates = useMemo(
|
||||
() => mmsTemplates.filter((template) => (
|
||||
(!enterpriseKeyword || template.enterprise.includes(enterpriseKeyword))
|
||||
&& (!templateKeyword || template.name.includes(templateKeyword) || template.application.includes(templateKeyword) || template.title.includes(templateKeyword) || template.content.includes(templateKeyword))
|
||||
)),
|
||||
[enterpriseKeyword, mmsTemplates, templateKeyword],
|
||||
);
|
||||
|
||||
const pagedSmsTemplates = paginate(filteredSmsTemplates, page, pageSize);
|
||||
const pagedMmsTemplates = paginate(filteredMmsTemplates, page, pageSize);
|
||||
|
||||
function upsertSmsTemplate(nextTemplate: SmsTemplate) {
|
||||
setSmsTemplates((current) => current.some((item) => item.id === nextTemplate.id)
|
||||
? current.map((item) => item.id === nextTemplate.id ? nextTemplate : item)
|
||||
: [nextTemplate, ...current]);
|
||||
setSmsModal(undefined);
|
||||
function loadData() {
|
||||
adminApi.listEnterpriseTemplates({ keyword })
|
||||
.then((items) => {
|
||||
setTemplates(items);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '企业模板加载失败'));
|
||||
}
|
||||
|
||||
function upsertMmsTemplate(nextTemplate: MmsTemplate) {
|
||||
setMmsTemplates((current) => current.some((item) => item.id === nextTemplate.id)
|
||||
? current.map((item) => item.id === nextTemplate.id ? nextTemplate : item)
|
||||
: [nextTemplate, ...current]);
|
||||
setMmsModal(undefined);
|
||||
}
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
function confirmDelete() {
|
||||
if (!deleteTarget) {
|
||||
return;
|
||||
}
|
||||
if (deleteTarget.kind === 'sms') {
|
||||
setSmsTemplates((current) => current.filter((item) => item.id !== deleteTarget.id));
|
||||
} else {
|
||||
setMmsTemplates((current) => current.filter((item) => item.id !== deleteTarget.id));
|
||||
}
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
const filtered = useMemo(() => templates.filter((item) => !keyword || [item.name, item.content, item.auditStatus, item.application?.name].join(' ').includes(keyword)), [keyword, templates]);
|
||||
|
||||
const smsContent = (
|
||||
<>
|
||||
<div className="template-card-grid">
|
||||
{pagedSmsTemplates.map((template) => (
|
||||
<article className={`template-card template-card--${template.accent}`} key={template.id}>
|
||||
<h2>{template.name}</h2>
|
||||
<p className="muted">{template.enterprise} / {template.application}</p>
|
||||
<p className="template-hash">{template.hash}</p>
|
||||
<p className="template-content">{template.content}</p>
|
||||
<div className="template-vars">
|
||||
<span>变量:</span>
|
||||
{template.variables.map((item) => <strong key={item}>${`{${item}}`}</strong>)}
|
||||
</div>
|
||||
<div className="template-card-footer">
|
||||
<span>{template.updatedAt}</span>
|
||||
<div>
|
||||
<button onClick={() => setSmsModal(template)} type="button"><Edit3 size={15} />编辑</button>
|
||||
<button onClick={() => setDeleteTarget({ kind: 'sms', id: template.id, name: template.name })} type="button"><Trash2 size={15} />删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<Pagination onPageChange={setPage} page={page} pageSize={pageSize} total={filteredSmsTemplates.length} />
|
||||
</>
|
||||
);
|
||||
|
||||
const mmsContent = (
|
||||
<>
|
||||
<div className="mms-template-grid">
|
||||
{pagedMmsTemplates.map((template) => (
|
||||
<article className={`mms-template-card mms-template-card--${template.accent}`} key={template.id}>
|
||||
<span className="mms-template-app-tag">{template.enterprise}</span>
|
||||
<div className="mms-template-card__body">
|
||||
<div className="mms-template-meta">
|
||||
<h2>{template.name}</h2>
|
||||
<p className="mms-template-code">{template.code}</p>
|
||||
<h3>{template.title}</h3>
|
||||
</div>
|
||||
<img alt={template.name} src={template.image} />
|
||||
<p className="mms-template-content">{template.content}</p>
|
||||
<div className="mms-template-status">
|
||||
<span>应用:{template.application}</span>
|
||||
<Tag tone={statusToneMap[template.status]}>{statusLabelMap[template.status]}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="mms-template-card__footer">
|
||||
<span>{template.updatedAt}</span>
|
||||
<div>
|
||||
<button onClick={() => setPreview(template)} type="button"><Eye size={17} />预览</button>
|
||||
<button onClick={() => setMmsModal(template)} type="button"><Edit3 size={15} />编辑</button>
|
||||
<button onClick={() => setDeleteTarget({ kind: 'mms', id: template.id, name: template.name })} type="button"><Trash2 size={15} />删除</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<Pagination onPageChange={setPage} page={page} pageSize={pageSize} total={filteredMmsTemplates.length} />
|
||||
</>
|
||||
);
|
||||
const columns: Array<TableColumn<ClientSmsTemplate>> = [
|
||||
{ key: 'name', title: '模板名称', render: (record) => <strong>{record.name}</strong> },
|
||||
{ key: 'tenant', title: '企业', render: (record) => record.tenantId },
|
||||
{ key: 'application', title: '应用', render: (record) => record.application?.name ?? record.applicationId },
|
||||
{ key: 'content', title: '内容', render: (record) => <span className="table-long-text">{record.content}</span> },
|
||||
{ key: 'status', title: '审核状态', render: (record) => <Tag tone={record.auditStatus === 'approved' ? 'success' : record.auditStatus === 'rejected' ? 'danger' : 'info'}>{record.auditStatus}</Tag> },
|
||||
{ key: 'updatedAt', title: '更新时间', render: (record) => record.updatedAt },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-customer-split-page">
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['客户管理', '企业模板管理']} />
|
||||
<h1>企业模板管理</h1>
|
||||
<Breadcrumb items={['企业配置', '企业模板']} />
|
||||
<h1>企业模板</h1>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => (activeTab === 'sms' ? setSmsModal(null) : setMmsModal(null))}>添加模板</Button>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-split-filter">
|
||||
<Input label="企业名称" onChange={(event) => { setEnterpriseKeyword(event.target.value); setPage(1); }} placeholder="请输入企业名称" prefix={<Search size={16} />} value={enterpriseKeyword} />
|
||||
<Input label="模板/应用" onChange={(event) => { setTemplateKeyword(event.target.value); setPage(1); }} placeholder="请输入模板、应用或内容关键词" prefix={<Search size={16} />} value={templateKeyword} />
|
||||
<Button onClick={() => { setEnterpriseKeyword(''); setTemplateKeyword(''); setPage(1); }} variant="ghost">重置</Button>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
<div className="surface admin-security-filter">
|
||||
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索模板、应用、内容或状态" prefix={<Search size={16} />} value={keyword} />
|
||||
<Button icon={<Search size={16} />} onClick={loadData}>查询</Button>
|
||||
</div>
|
||||
|
||||
<div className="surface section-stack">
|
||||
<Tabs
|
||||
onChange={(value) => { setActiveTab(value as TemplateKind); setPage(1); }}
|
||||
value={activeTab}
|
||||
items={[
|
||||
{ label: '短信模板', value: 'sms', content: smsContent },
|
||||
{ label: '彩信模板', value: 'mms', pending: true, content: mmsContent },
|
||||
]}
|
||||
/>
|
||||
<div className="surface">
|
||||
<Table columns={columns} data={filtered} emptyText="暂无企业模板" rowKey="id" />
|
||||
</div>
|
||||
|
||||
{smsModal !== undefined ? <SmsTemplateModal item={smsModal ?? undefined} onClose={() => setSmsModal(undefined)} onSubmit={upsertSmsTemplate} /> : null}
|
||||
{mmsModal !== undefined ? <MmsTemplateModal item={mmsModal ?? undefined} onClose={() => setMmsModal(undefined)} onSubmit={upsertMmsTemplate} /> : null}
|
||||
|
||||
<Modal
|
||||
footer={<Button onClick={() => setPreview(null)}>关闭</Button>}
|
||||
onClose={() => setPreview(null)}
|
||||
open={Boolean(preview)}
|
||||
title="彩信预览"
|
||||
>
|
||||
{preview ? (
|
||||
<div className="mms-preview">
|
||||
<img alt={preview.name} src={preview.image} />
|
||||
<h3>{preview.title}</h3>
|
||||
<p>{preview.content}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
{deleteTarget ? (
|
||||
<ConfirmModal
|
||||
message={`确认删除“${deleteTarget.name}”吗?删除后仅影响当前本地 mock 数据。`}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user