173 lines
6.5 KiB
TypeScript
173 lines
6.5 KiB
TypeScript
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';
|
|
|
|
const statusTone: Record<string, 'success' | 'info' | 'danger' | 'warning'> = {
|
|
approved: 'success',
|
|
pending: 'info',
|
|
rejected: 'danger',
|
|
draft: 'warning',
|
|
};
|
|
|
|
const statusLabel: Record<string, string> = {
|
|
approved: '已通过',
|
|
pending: '审核中',
|
|
rejected: '已驳回',
|
|
draft: '草稿',
|
|
disabled: '已禁用',
|
|
};
|
|
|
|
export function ClientSignaturesPage() {
|
|
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
|
|
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
|
|
const [keyword, setKeyword] = useState('');
|
|
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);
|
|
|
|
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));
|
|
}
|
|
|
|
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.uploadFileObject(file, {
|
|
purpose: 'signature_material',
|
|
prefix: `signature-materials/${signature.id}`,
|
|
});
|
|
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 (
|
|
<section className="page-stack">
|
|
<div className="signature-page-header">
|
|
<div className="sms-send-title">
|
|
<span className="sms-send-title__icon">
|
|
<FilePenLine size={22} />
|
|
</span>
|
|
<h1>签名与报备材料</h1>
|
|
</div>
|
|
<Button icon={<Plus size={17} />} onClick={() => setModalOpen(true)}>添加签名</Button>
|
|
</div>
|
|
|
|
<div className="signature-search-row">
|
|
<Input
|
|
onChange={(event) => setKeyword(event.target.value)}
|
|
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) => (
|
|
<article className="signature-card signature-card--green" key={signature.id}>
|
|
<div className="signature-summary">
|
|
<div>
|
|
<span>签名名称</span>
|
|
<strong>{signature.name}</strong>
|
|
</div>
|
|
<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={(
|
|
<>
|
|
<Button onClick={() => setModalOpen(false)} variant="ghost">取消</Button>
|
|
<Button disabled={!name} onClick={createSignature}>提交审核</Button>
|
|
</>
|
|
)}
|
|
onClose={() => setModalOpen(false)}
|
|
open={modalOpen}
|
|
size="xl"
|
|
title="添加签名"
|
|
>
|
|
<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>
|
|
);
|
|
}
|