Files
lislgosms/src/apps/client/ClientSignaturesPage.tsx
T

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>支持图片、PDFWord 等真实材料文件</small>
<input
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
style={{ display: 'none' }}
type="file"
/>
</label>
</div>
</Modal>
</section>
);
}