fix: connect remaining sms pages to real backend
This commit is contained in:
@@ -1,175 +1,88 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronRight, Edit3, FilePenLine, Info, Plus, Search, Trash2, Upload } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FilePenLine, Plus, Search, Trash2, Upload } from 'lucide-react';
|
||||
import { Button, Input, Modal, Select, Tag } from '@/components/ui';
|
||||
import { clientApi, type ClientSmsApplication, type ClientSmsSignature } from '@/api/adminApi';
|
||||
|
||||
type CarrierStatus = 'approved' | 'pending' | 'rejected';
|
||||
|
||||
type DrainageInfo = {
|
||||
id: string;
|
||||
siteName: string;
|
||||
content: string;
|
||||
mobile: CarrierStatus;
|
||||
unicom: CarrierStatus;
|
||||
telecom: CarrierStatus;
|
||||
submittedAt: string;
|
||||
};
|
||||
|
||||
type SignatureItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
application: string;
|
||||
accent: 'green' | 'blue' | 'red' | 'gray';
|
||||
mobile: CarrierStatus;
|
||||
unicom: CarrierStatus;
|
||||
telecom: CarrierStatus;
|
||||
drainage: DrainageInfo[];
|
||||
};
|
||||
|
||||
const statusLabelMap: Record<CarrierStatus, string> = {
|
||||
approved: '已通过',
|
||||
pending: '审核中',
|
||||
rejected: '已驳回',
|
||||
};
|
||||
|
||||
const statusToneMap: Record<CarrierStatus, 'success' | 'info' | 'danger'> = {
|
||||
const statusTone: Record<string, 'success' | 'info' | 'danger' | 'warning'> = {
|
||||
approved: 'success',
|
||||
pending: 'info',
|
||||
rejected: 'danger',
|
||||
draft: 'warning',
|
||||
};
|
||||
|
||||
const initialSignatures: SignatureItem[] = [
|
||||
{
|
||||
id: 'sig-1',
|
||||
name: '【科技公司】',
|
||||
application: '营销推广',
|
||||
accent: 'green',
|
||||
mobile: 'approved',
|
||||
unicom: 'approved',
|
||||
telecom: 'approved',
|
||||
drainage: [
|
||||
{ id: 'drain-1', siteName: '官方网站', content: 'https://www.example.com', mobile: 'approved', unicom: 'approved', telecom: 'approved', submittedAt: '2024-01-08 11:00:00' },
|
||||
{ id: 'drain-2', siteName: '促销活动页', content: 'https://promo.example.com', mobile: 'approved', unicom: 'pending', telecom: 'pending', submittedAt: '2024-01-09 10:30:00' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sig-2',
|
||||
name: '【客户服务】',
|
||||
application: '通知服务',
|
||||
accent: 'blue',
|
||||
mobile: 'approved',
|
||||
unicom: 'pending',
|
||||
telecom: 'approved',
|
||||
drainage: [
|
||||
{ id: 'drain-3', siteName: '客服入口', content: 'https://service.example.com', mobile: 'approved', unicom: 'pending', telecom: 'approved', submittedAt: '2024-01-10 09:12:00' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sig-3',
|
||||
name: '【验证码】',
|
||||
application: '验证码',
|
||||
accent: 'green',
|
||||
mobile: 'approved',
|
||||
unicom: 'approved',
|
||||
telecom: 'approved',
|
||||
drainage: [],
|
||||
},
|
||||
{
|
||||
id: 'sig-4',
|
||||
name: '【促销活动】',
|
||||
application: '百<>会员推广',
|
||||
accent: 'red',
|
||||
mobile: 'rejected',
|
||||
unicom: 'approved',
|
||||
telecom: 'pending',
|
||||
drainage: [
|
||||
{ id: 'drain-4', siteName: '会员活动页', content: 'https://vip.example.com', mobile: 'rejected', unicom: 'approved', telecom: 'pending', submittedAt: '2024-01-11 13:42:00' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sig-5',
|
||||
name: '【会员中心】',
|
||||
application: '会员服务',
|
||||
accent: 'gray',
|
||||
mobile: 'pending',
|
||||
unicom: 'pending',
|
||||
telecom: 'pending',
|
||||
drainage: [],
|
||||
},
|
||||
];
|
||||
|
||||
function UploadBox({ label, compact = false }: { label?: string; compact?: boolean }) {
|
||||
return (
|
||||
<div className={compact ? 'signature-upload signature-upload--compact' : 'signature-upload'}>
|
||||
{label ? <span>{label}</span> : null}
|
||||
<Upload size={compact ? 30 : 42} />
|
||||
<strong>{compact ? '上传文件' : '点击上传 或拖拽文件到此处'}</strong>
|
||||
{!compact ? <small>支持 PNG、JPG、JPEG 格式,大小不超过 3M</small> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SignatureForm({ signature }: { signature?: SignatureItem }) {
|
||||
return (
|
||||
<div className="signature-form">
|
||||
<section>
|
||||
<h3>基本信息</h3>
|
||||
<div className="signature-alert">
|
||||
<Info size={18} />
|
||||
<span>签名需履行报备,并遵照管理部门审核结果方可使用。请用PNG、JPG或JPEG格式的正版文件,且大小不超过3M。</span>
|
||||
</div>
|
||||
<div className="signature-form-grid">
|
||||
<Select label="* 签名依据" options={[{ label: '请选择签名依据', value: '' }, { label: '企事业单位证明', value: 'company' }]} defaultValue={signature ? 'company' : ''} />
|
||||
<Input label="* 短信签名" defaultValue={signature?.name ?? ''} placeholder="请输入短信签名,如【XXXX公司】" />
|
||||
</div>
|
||||
<UploadBox label="* 资质凭证" />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>公司信息</h3>
|
||||
<div className="signature-form-grid">
|
||||
<Input label="* 公司名称" defaultValue={signature?.application ?? ''} placeholder="请输入公司名称" />
|
||||
<Input label="* 统一社会信用代码" placeholder="请输入统一社会信用代码" />
|
||||
<Input label="* 法人姓名" placeholder="请输入法人姓名" />
|
||||
<Input label="法人身份证号" placeholder="请输入法人身份证号" />
|
||||
<UploadBox compact label="法人身份证照片-人像面" />
|
||||
<UploadBox compact label="法人身份证照片-国徽面" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>责任人信息</h3>
|
||||
<div className="signature-form-grid">
|
||||
<Input label="* 责任人姓名" placeholder="请输入责任人姓名" />
|
||||
<Input label="* 责任人手机号" placeholder="请输入责任人手机号" />
|
||||
<Input label="* 责任人身份证号" placeholder="请输入责任人身份证号" />
|
||||
<Input label="责任人邮箱" placeholder="请输入责任人邮箱" />
|
||||
<UploadBox compact label="责任人身份证照片-人像面" />
|
||||
<UploadBox compact label="责任人身份证照片-国徽面" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const statusLabel: Record<string, string> = {
|
||||
approved: '已通过',
|
||||
pending: '审核中',
|
||||
rejected: '已驳回',
|
||||
draft: '草稿',
|
||||
disabled: '已禁用',
|
||||
};
|
||||
|
||||
export function ClientSignaturesPage() {
|
||||
const [signatures, setSignatures] = useState(initialSignatures);
|
||||
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
|
||||
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [expandedId, setExpandedId] = useState('');
|
||||
const [signatureModal, setSignatureModal] = useState<{ mode: 'add' | 'edit'; signature?: SignatureItem } | null>(null);
|
||||
const [editingDrainage, setEditingDrainage] = useState<{ signature: SignatureItem; drainage?: DrainageInfo } | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [applicationId, setApplicationId] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [purpose, setPurpose] = useState('');
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const filteredSignatures = signatures.filter((item) => (
|
||||
item.name.includes(keyword) || item.application.includes(keyword)
|
||||
));
|
||||
|
||||
function deleteSignature(id: string) {
|
||||
setSignatures((items) => items.filter((item) => item.id !== id));
|
||||
function loadData() {
|
||||
setLoading(true);
|
||||
Promise.all([clientApi.listApplications(), clientApi.listSignatures()])
|
||||
.then(([applicationItems, signatureItems]) => {
|
||||
setApplications(applicationItems.filter((item) => item.status === 'active'));
|
||||
setSignatures(signatureItems.filter((item) => item.auditStatus !== 'disabled' && item.auditStatus !== 'deleted'));
|
||||
setError('');
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '签名数据加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
function deleteDrainage(signatureId: string, drainageId: string) {
|
||||
setSignatures((items) => items.map((item) => (
|
||||
item.id === signatureId ? { ...item, drainage: item.drainage.filter((drainage) => drainage.id !== drainageId) } : item
|
||||
)));
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const filteredSignatures = useMemo(() => signatures.filter((item) => (
|
||||
!keyword || [item.name, item.purpose, item.applicationId].join(' ').includes(keyword)
|
||||
)), [keyword, signatures]);
|
||||
|
||||
async function createSignature() {
|
||||
try {
|
||||
const signature = await clientApi.createSignature({ applicationId: applicationId || undefined, name, purpose });
|
||||
if (file) {
|
||||
const fileObject = await clientApi.createFileObject({
|
||||
objectKey: `signature-materials/${signature.id}/${Date.now()}-${file.name}`,
|
||||
fileName: file.name,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
sizeBytes: file.size,
|
||||
purpose: 'signature_material',
|
||||
});
|
||||
await clientApi.createSignatureMaterial(signature.id, {
|
||||
fileObjectId: fileObject.id,
|
||||
materialType: file.type.startsWith('image/') ? 'image' : 'file',
|
||||
title: file.name,
|
||||
});
|
||||
}
|
||||
await clientApi.submitSignature(signature.id);
|
||||
setModalOpen(false);
|
||||
setApplicationId('');
|
||||
setName('');
|
||||
setPurpose('');
|
||||
setFile(null);
|
||||
loadData();
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : '签名提交失败');
|
||||
}
|
||||
}
|
||||
|
||||
function disableSignature(id: string) {
|
||||
clientApi.changeSignatureStatus(id, 'disabled')
|
||||
.then(loadData)
|
||||
.catch((reason: Error) => setError(reason.message || '签名禁用失败'));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -179,154 +92,83 @@ export function ClientSignaturesPage() {
|
||||
<span className="sms-send-title__icon">
|
||||
<FilePenLine size={22} />
|
||||
</span>
|
||||
<h1>签名与引流信息</h1>
|
||||
<h1>签名与报备材料</h1>
|
||||
</div>
|
||||
<Button icon={<Plus size={17} />} onClick={() => setSignatureModal({ mode: 'add' })}>添加签名</Button>
|
||||
<Button icon={<Plus size={17} />} onClick={() => setModalOpen(true)}>添加签名</Button>
|
||||
</div>
|
||||
|
||||
<div className="signature-search-row">
|
||||
<Input
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder="搜索签名名称或用途"
|
||||
placeholder="搜索签名名称、用途或应用"
|
||||
prefix={<Search size={17} />}
|
||||
value={keyword}
|
||||
/>
|
||||
</div>
|
||||
{loading ? <p className="muted">正在加载签名...</p> : null}
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<div className="signature-list">
|
||||
{filteredSignatures.map((signature) => {
|
||||
const expanded = expandedId === signature.id;
|
||||
return (
|
||||
<article className={`signature-card signature-card--${signature.accent}`} key={signature.id}>
|
||||
<div className="signature-summary">
|
||||
<button aria-label="展开签名" onClick={() => setExpandedId(expanded ? '' : signature.id)} type="button">
|
||||
{expanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
||||
</button>
|
||||
<div>
|
||||
<span>签名名称</span>
|
||||
<strong>{signature.name}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>应用</span>
|
||||
<strong>{signature.application}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>移动</span>
|
||||
<Tag tone={statusToneMap[signature.mobile]}>{statusLabelMap[signature.mobile]}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span>联通</span>
|
||||
<Tag tone={statusToneMap[signature.unicom]}>{statusLabelMap[signature.unicom]}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span>电信</span>
|
||||
<Tag tone={statusToneMap[signature.telecom]}>{statusLabelMap[signature.telecom]}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span>引流信息</span>
|
||||
<strong>{signature.drainage.length} 条</strong>
|
||||
</div>
|
||||
<div className="signature-actions">
|
||||
<Button icon={<Edit3 size={16} />} onClick={() => setSignatureModal({ mode: 'edit', signature })} size="sm" variant="ghost">编辑</Button>
|
||||
<Button icon={<Trash2 size={16} />} onClick={() => deleteSignature(signature.id)} size="sm" variant="danger">删除</Button>
|
||||
</div>
|
||||
{filteredSignatures.map((signature) => (
|
||||
<article className="signature-card signature-card--green" key={signature.id}>
|
||||
<div className="signature-summary">
|
||||
<div>
|
||||
<span>签名名称</span>
|
||||
<strong>{signature.name}</strong>
|
||||
</div>
|
||||
|
||||
{expanded ? (
|
||||
<div className="drainage-panel">
|
||||
<h2>引流信息列表</h2>
|
||||
<div className="drainage-table">
|
||||
<div className="drainage-table__head">
|
||||
<span>站名称</span>
|
||||
<span>引内容</span>
|
||||
<span>移动</span>
|
||||
<span>联通</span>
|
||||
<span>电信</span>
|
||||
<span>提交时间</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
{signature.drainage.map((item) => (
|
||||
<div className="drainage-table__row" key={item.id}>
|
||||
<strong>{item.siteName}</strong>
|
||||
<a href={item.content}>{item.content}</a>
|
||||
<Tag tone={statusToneMap[item.mobile]}>{statusLabelMap[item.mobile]}</Tag>
|
||||
<Tag tone={statusToneMap[item.unicom]}>{statusLabelMap[item.unicom]}</Tag>
|
||||
<Tag tone={statusToneMap[item.telecom]}>{statusLabelMap[item.telecom]}</Tag>
|
||||
<span className="muted">{item.submittedAt}</span>
|
||||
<span className="drainage-row-actions">
|
||||
<Button onClick={() => setEditingDrainage({ signature, drainage: item })} size="sm" variant="ghost">编辑</Button>
|
||||
<Button onClick={() => deleteDrainage(signature.id, item.id)} size="sm" variant="danger">删除</Button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="drainage-panel__footer">
|
||||
<Button icon={<Plus size={16} />} onClick={() => setEditingDrainage({ signature })} size="sm" variant="ghost">
|
||||
添加引流信息
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
<span>用途</span>
|
||||
<strong>{signature.purpose ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>审核状态</span>
|
||||
<Tag tone={statusTone[signature.auditStatus] ?? 'info'}>{statusLabel[signature.auditStatus] ?? signature.auditStatus}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span>材料</span>
|
||||
<strong>{signature.materials?.length ?? 0} 份</strong>
|
||||
</div>
|
||||
<div className="signature-actions">
|
||||
<Button icon={<Trash2 size={16} />} onClick={() => disableSignature(signature.id)} size="sm" variant="danger">删除</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{!loading && !error && filteredSignatures.length === 0 ? <p className="muted">暂无签名记录。</p> : null}
|
||||
|
||||
<Modal
|
||||
footer={
|
||||
footer={(
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setSignatureModal(null)}>取消</Button>
|
||||
<Button onClick={() => setSignatureModal(null)}>确认</Button>
|
||||
<Button onClick={() => setModalOpen(false)} variant="ghost">取消</Button>
|
||||
<Button disabled={!name} onClick={createSignature}>提交审核</Button>
|
||||
</>
|
||||
}
|
||||
onClose={() => setSignatureModal(null)}
|
||||
open={Boolean(signatureModal)}
|
||||
)}
|
||||
onClose={() => setModalOpen(false)}
|
||||
open={modalOpen}
|
||||
size="xl"
|
||||
title={<div className="signature-modal-title"><h2>{signatureModal?.mode === 'edit' ? '编辑签名' : '添加签名'}</h2><p>修改短信签名的相关信息</p></div>}
|
||||
title="添加签名"
|
||||
>
|
||||
<SignatureForm signature={signatureModal?.signature} />
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setEditingDrainage(null)}>取消</Button>
|
||||
<Button onClick={() => setEditingDrainage(null)}>确认</Button>
|
||||
</>
|
||||
}
|
||||
onClose={() => setEditingDrainage(null)}
|
||||
open={Boolean(editingDrainage)}
|
||||
size="xl"
|
||||
title={<div className="signature-modal-title"><h2>编辑引流信息</h2><p>所属签名:{editingDrainage?.signature.name}</p></div>}
|
||||
>
|
||||
{editingDrainage ? (
|
||||
<div className="signature-form">
|
||||
<section>
|
||||
<h3>基本信息</h3>
|
||||
<Input label="* 引流信息:" defaultValue={editingDrainage.drainage?.content ?? 'https://www.example.com'} />
|
||||
<div className="signature-alert">
|
||||
<Info size={18} />
|
||||
<span>1 本页面中所填的信息需与您使用的包含的网站或服务保持一致;2 图片仅支持PNG、JPG或JPEG格式的正版文件,且大小不超过3M;3 文件格式支持pdf格式或者图片,且大小不超过10M。</span>
|
||||
</div>
|
||||
<div className="signature-form-grid">
|
||||
<UploadBox compact label="* 字段名称1:" />
|
||||
<Input label="* 字段名称2:" defaultValue={editingDrainage.drainage?.siteName ?? '官方网站'} />
|
||||
<Input label="* 字段名称3:" placeholder="请输入公司名称" />
|
||||
<Input label="* 字段名称4:" placeholder="请输入统一社会信用代码" />
|
||||
<Input label="* 字段名称5:" placeholder="请输入法人姓名" />
|
||||
<Input label="* 字段名称6:" placeholder="请输入法人身份证号" />
|
||||
<div className="signature-file-line">
|
||||
<span>字段名称7:</span>
|
||||
<Button size="sm">选择文件</Button>
|
||||
<small>未选择文件</small>
|
||||
</div>
|
||||
<Input label="* 字段名称8:" placeholder="请输入责任人身份证号" />
|
||||
<Input label="* 字段名称9:" placeholder="请输入责任人姓名" />
|
||||
<Input label="* 字段名称10:" placeholder="请输入责任人手机号" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="signature-form">
|
||||
<Select
|
||||
label="短信应用"
|
||||
onChange={(event) => setApplicationId(event.target.value)}
|
||||
options={[{ label: '不绑定应用', value: '' }, ...applications.map((item) => ({ label: item.name, value: item.id }))]}
|
||||
value={applicationId}
|
||||
/>
|
||||
<Input label="短信签名" onChange={(event) => setName(event.target.value)} placeholder="请输入短信签名,如【某某科技】" value={name} />
|
||||
<Input label="用途" onChange={(event) => setPurpose(event.target.value)} placeholder="请输入签名用途" value={purpose} />
|
||||
<label className="signature-upload">
|
||||
<Upload size={36} />
|
||||
<strong>{file ? file.name : '上传资质图片或文件'}</strong>
|
||||
<small>支持图片、PDF、Word 等真实材料文件</small>
|
||||
<input
|
||||
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user