fix: connect remaining sms pages to real backend

This commit is contained in:
hectorzhao
2026-07-02 19:30:04 +08:00
parent 06562e42dd
commit 131f344ac4
50 changed files with 2074 additions and 6400 deletions
+35 -589
View File
@@ -1,607 +1,53 @@
import { useMemo, useState } from 'react';
import { ChevronDown, ChevronLeft, ChevronRight, Edit3, FileText, Info, Plus, Search, Trash2, Upload } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui';
type CarrierStatus = 'approved' | 'pending' | 'rejected' | 'filing';
type SignatureKind = 'sms' | 'mms';
type DrainageInfo = {
id: string;
siteName: string;
url: string;
mobile: CarrierStatus;
unicom: CarrierStatus;
telecom: CarrierStatus;
submittedAt: string;
remark: string;
};
type SignatureItem = {
id: string;
name: string;
enterprise: string;
application: string;
mobile: CarrierStatus;
unicom: CarrierStatus;
telecom: CarrierStatus;
drainage?: DrainageInfo[];
updatedAt: string;
};
const statusLabelMap: Record<CarrierStatus, string> = {
approved: '已通过',
pending: '审核中',
rejected: '已驳回',
filing: '待报备',
};
const statusToneMap: Record<CarrierStatus, 'success' | 'info' | 'danger' | 'neutral'> = {
approved: 'success',
pending: 'info',
rejected: 'danger',
filing: 'neutral',
};
const statusOptions = [
{ label: '已通过', value: 'approved' },
{ label: '审核中', value: 'pending' },
{ label: '已驳回', value: 'rejected' },
{ label: '待报备', value: 'filing' },
];
const initialSmsSignatures: SignatureItem[] = [
{
id: 'sig-1',
name: '【科技公司】',
enterprise: '上海XXXXX科技有限公司',
application: '营销推广平台',
mobile: 'approved',
unicom: 'approved',
telecom: 'approved',
updatedAt: '2026-01-08 11:00:00',
drainage: [
{ id: 'drain-1', siteName: '官网入口', url: 'https://www.example.com', mobile: 'approved', unicom: 'approved', telecom: 'approved', submittedAt: '2026-01-08 11:00:00', remark: '官网首页引流链接,三网报备通过。' },
{ id: 'drain-2', siteName: '促销活动页', url: 'https://sale.example.com', mobile: 'approved', unicom: 'pending', telecom: 'pending', submittedAt: '2026-01-09 10:30:00', remark: '活动页待联通、电信回执。' },
],
},
{
id: 'sig-2',
name: '【客户服务】',
enterprise: '重庆进载数智',
application: '客户服务系统',
mobile: 'approved',
unicom: 'pending',
telecom: 'approved',
updatedAt: '2026-01-09 14:20:00',
drainage: [
{ id: 'drain-3', siteName: '客户服务中心', url: 'https://service.example.com', mobile: 'approved', unicom: 'filing', telecom: 'pending', submittedAt: '2026-01-09 14:20:00', remark: '服务入口链接报备中。' },
],
},
{
id: 'sig-3',
name: '【促销活动】',
enterprise: '超感世纪互三网',
application: '营销推广平台',
mobile: 'rejected',
unicom: 'approved',
telecom: 'pending',
updatedAt: '2026-01-07 10:00:00',
drainage: [
{ id: 'drain-4', siteName: '促销专区', url: 'https://sale.example.com', mobile: 'rejected', unicom: 'approved', telecom: 'approved', submittedAt: '2026-01-07 10:00:00', remark: '移动侧驳回,需补充页面备案信息。' },
],
},
];
const initialMmsSignatures: SignatureItem[] = [
{ id: 'mms-sig-1', name: '【活动推广】', enterprise: '上海XXXXX科技有限公司', application: '营销活动彩信', mobile: 'approved', unicom: 'approved', telecom: 'approved', updatedAt: '2026-01-12 09:10:00' },
{ id: 'mms-sig-2', name: '【节日祝福】', enterprise: '重庆进载数智', application: '节日祝福彩信', mobile: 'approved', unicom: 'pending', telecom: 'approved', updatedAt: '2026-01-13 16:35:00' },
{ id: 'mms-sig-3', name: '【优品发布】', enterprise: '四川骠骑企业管理', application: '营销活动彩信', mobile: 'pending', unicom: 'pending', telecom: 'pending', updatedAt: '2026-01-15 10:28:00' },
];
function StatusTag({ status }: { status: CarrierStatus }) {
return <Tag tone={statusToneMap[status]}>{statusLabelMap[status]}</Tag>;
}
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 SignatureUploadBox({ label, compact = false }: { label: string; compact?: boolean }) {
return (
<div className={compact ? 'signature-upload signature-upload--compact' : 'signature-upload'}>
<span>{label}</span>
<Upload size={compact ? 30 : 42} />
<strong>{compact ? '上传文件' : '点击上传 或拖拽文件到此处'}</strong>
{!compact ? <small> PNGJPGJPEG 3M</small> : null}
</div>
);
}
function SignatureFormModal({
item,
kind,
onClose,
onSubmit,
}: {
item?: SignatureItem;
kind: SignatureKind;
onClose: () => void;
onSubmit: (item: SignatureItem) => void;
}) {
const [name, setName] = useState(item?.name ?? '');
const [enterprise, setEnterprise] = useState(item?.enterprise ?? '');
const [application, setApplication] = useState(item?.application ?? '');
const [mobile, setMobile] = useState<CarrierStatus>(item?.mobile ?? 'filing');
const [unicom, setUnicom] = useState<CarrierStatus>(item?.unicom ?? 'filing');
const [telecom, setTelecom] = useState<CarrierStatus>(item?.telecom ?? 'filing');
function submit() {
onSubmit({
id: item?.id ?? `${kind}-sig-${Date.now()}`,
name,
enterprise,
application,
mobile,
unicom,
telecom,
drainage: kind === 'sms' ? (item?.drainage ?? []) : undefined,
updatedAt: '2026-06-30 10:00:00',
});
}
return (
<Modal
footer={(
<>
<Button onClick={onClose} variant="ghost"></Button>
<Button onClick={submit}></Button>
</>
)}
onClose={onClose}
open
size="xl"
title={(
<div className="signature-modal-title">
<h2>{item ? '编辑签名' : '添加签名'}</h2>
<p>{item ? '修改短信签名的相关信息' : '新增短信签名的相关信息'}</p>
</div>
)}
>
<div className="signature-form">
<section>
<h3></h3>
<div className="signature-alert">
<Info size={18} />
<span>使 PNGJPG JPEG 3M</span>
</div>
<div className="signature-form-grid">
<Select
defaultValue={item ? 'company' : ''}
label="* 签名依据"
options={[
{ label: '请选择签名依据', value: '' },
{ label: '企事业单位证明', value: 'company' },
{ label: '商标注册证', value: 'trademark' },
{ label: '授权委托书', value: 'authorization' },
]}
/>
<Input label="* 短信签名" onChange={(event) => setName(event.target.value)} placeholder="请输入短信签名,如【XXXX公司】" value={name} />
</div>
<SignatureUploadBox label="* 资质凭证" />
</section>
<section>
<h3></h3>
<div className="signature-form-grid">
<Input label="* 公司名称" onChange={(event) => setEnterprise(event.target.value)} placeholder="请输入公司名称" value={enterprise} />
<Input label="* 统一社会信用代码" placeholder="请输入统一社会信用代码" />
<Input label="* 法人姓名" placeholder="请输入法人姓名" />
<Input label="法人身份证号" placeholder="请输入法人身份证号" />
<SignatureUploadBox compact label="法人身份证照片-人像面" />
<SignatureUploadBox compact label="法人身份证照片-国徽面" />
</div>
</section>
<section>
<h3></h3>
<div className="signature-form-grid">
<Input label="* 应用名称" onChange={(event) => setApplication(event.target.value)} placeholder="请输入应用名称" value={application} />
<Input label="* 责任人手机号" placeholder="请输入责任人手机号" />
<Input className="signature-form-grid__wide" label="* 责任人身份证号" placeholder="请输入责任人身份证号" />
<SignatureUploadBox compact label="责任人身份证照片-人像面" />
<SignatureUploadBox compact label="责任人身份证照片-国徽面" />
</div>
</section>
<section>
<h3></h3>
<div className="signature-form-grid">
<Select label="移动状态" onChange={(event) => setMobile(event.target.value as CarrierStatus)} options={statusOptions} value={mobile} />
<Select label="联通状态" onChange={(event) => setUnicom(event.target.value as CarrierStatus)} options={statusOptions} value={unicom} />
<Select label="电信状态" onChange={(event) => setTelecom(event.target.value as CarrierStatus)} options={statusOptions} value={telecom} />
</div>
</section>
</div>
</Modal>
);
}
function SignatureReportModal({ item, onClose }: { item: SignatureItem; onClose: () => void }) {
return (
<Modal footer={<Button onClick={onClose}></Button>} onClose={onClose} open title="签名报备详情">
<div className="admin-report-detail">
<div className="detail-grid">
<div><span></span><strong>{item.enterprise}</strong></div>
<div><span></span><strong>{item.application}</strong></div>
<div><span></span><strong>{item.name}</strong></div>
<div><span></span><strong>{item.updatedAt}</strong></div>
</div>
<div className="admin-report-tabs">
<button className="admin-report-carrier--mobile active" type="button"><strong></strong><span><StatusTag status={item.mobile} /></span></button>
<button className="admin-report-carrier--unicom active" type="button"><strong></strong><span><StatusTag status={item.unicom} /></span></button>
<button className="admin-report-carrier--telecom active" type="button"><strong></strong><span><StatusTag status={item.telecom} /></span></button>
</div>
</div>
</Modal>
);
}
function DrainageFormModal({
item,
onClose,
onSubmit,
}: {
item?: DrainageInfo;
onClose: () => void;
onSubmit: (item: DrainageInfo) => void;
}) {
const [form, setForm] = useState<DrainageInfo>(() => item ?? {
id: `drain-${Date.now()}`,
siteName: '',
url: '',
mobile: 'filing',
unicom: 'filing',
telecom: 'filing',
submittedAt: '2026-06-30 10:00:00',
remark: '',
});
function update<Key extends keyof DrainageInfo>(key: Key, value: DrainageInfo[Key]) {
setForm((current) => ({ ...current, [key]: value }));
}
return (
<Modal
footer={(
<>
<Button onClick={onClose} variant="ghost"></Button>
<Button onClick={() => onSubmit(form)}></Button>
</>
)}
onClose={onClose}
open
size="xl"
title={item ? '编辑引流链接' : '添加引流链接'}
>
<div className="signature-form drainage-edit-form">
<section>
<h3></h3>
<Input
label="* 引流信息"
onChange={(event) => update('url', event.target.value)}
placeholder="请输入引流网址"
value={form.url}
/>
<div className="signature-alert drainage-form-note">
<Info size={18} />
<ol>
<li></li>
<li> PNGJPG JPEG 3M</li>
<li> pdf 10M</li>
</ol>
</div>
<div className="signature-form-grid">
<SignatureUploadBox compact label="* 字段名称1" />
<Input label="* 字段名称2" placeholder="请输入字段2内容" />
<Input label="* 字段名称3" onChange={(event) => update('siteName', event.target.value)} placeholder="请输入公司名称" value={form.siteName} />
<Input label="字段名称4" placeholder="请输入统一社会信用代码" />
<Input label="* 字段名称5" placeholder="请输入法人姓名" />
<Input label="字段名称6" placeholder="请输入法人身份证号" />
<div className="drainage-file-picker">
<span>7:</span>
<div>
<Button size="sm"></Button>
<em></em>
</div>
</div>
<Input label="* 字段名称8" placeholder="请输入责任人身份证号" />
<Input label="* 字段名称9" placeholder="请输入责任人姓名" />
<Input label="* 字段名称10" placeholder="请输入责任人手机号" />
<Select label="移动状态" onChange={(event) => update('mobile', event.target.value as CarrierStatus)} options={statusOptions} value={form.mobile} />
<Select label="联通状态" onChange={(event) => update('unicom', event.target.value as CarrierStatus)} options={statusOptions} value={form.unicom} />
<Select label="电信状态" onChange={(event) => update('telecom', event.target.value as CarrierStatus)} options={statusOptions} value={form.telecom} />
<Input label="提交时间" onChange={(event) => update('submittedAt', event.target.value)} value={form.submittedAt} />
<Textarea className="signature-form-grid__wide" label="备注" onChange={(event) => update('remark', event.target.value)} rows={4} value={form.remark} />
</div>
</section>
</div>
</Modal>
);
}
function DrainageReportModal({ item, onClose }: { item: DrainageInfo; onClose: () => void }) {
return (
<Modal footer={<Button onClick={onClose}></Button>} onClose={onClose} open title="引流信息报备详情">
<div className="detail-grid">
<div><span></span><strong>{item.siteName}</strong></div>
<div><span></span><strong>{item.url}</strong></div>
<div><span></span><StatusTag status={item.mobile} /></div>
<div><span></span><StatusTag status={item.unicom} /></div>
<div><span></span><StatusTag status={item.telecom} /></div>
<div><span></span><strong>{item.submittedAt}</strong></div>
<div className="detail-grid__wide"><span></span><strong>{item.remark || '-'}</strong></div>
</div>
</Modal>
);
}
import { useEffect, useMemo, useState } from 'react';
import { Search } from 'lucide-react';
import { adminApi, type ClientSmsSignature } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Table, Tag, type TableColumn } from '@/components/ui';
export function AdminEnterpriseSignaturesPage() {
const [activeTab, setActiveTab] = useState<SignatureKind>('sms');
const [smsSignatures, setSmsSignatures] = useState(initialSmsSignatures);
const [mmsSignatures, setMmsSignatures] = useState(initialMmsSignatures);
const [expandedSignatureId, setExpandedSignatureId] = useState('');
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
const [signatureKeyword, setSignatureKeyword] = useState('');
const [page, setPage] = useState(1);
const [signatureModal, setSignatureModal] = useState<{ kind: SignatureKind; item?: SignatureItem } | null>(null);
const [signatureReport, setSignatureReport] = useState<SignatureItem | null>(null);
const [drainageModal, setDrainageModal] = useState<{ signatureId: string; item?: DrainageInfo } | null>(null);
const [drainageReport, setDrainageReport] = useState<DrainageInfo | null>(null);
const [deleteTarget, setDeleteTarget] = useState<{ kind: 'signature'; signatureKind: SignatureKind; id: string; name: string } | { kind: 'drainage'; signatureId: string; id: string; name: string } | null>(null);
const pageSize = 2;
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
const [keyword, setKeyword] = useState('');
const [error, setError] = useState('');
const filteredSmsSignatures = useMemo(
() => smsSignatures.filter((item) => (
(!enterpriseKeyword || item.enterprise.includes(enterpriseKeyword))
&& (!signatureKeyword || item.name.includes(signatureKeyword) || item.application.includes(signatureKeyword))
)),
[enterpriseKeyword, signatureKeyword, smsSignatures],
);
const filteredMmsSignatures = useMemo(
() => mmsSignatures.filter((item) => (
(!enterpriseKeyword || item.enterprise.includes(enterpriseKeyword))
&& (!signatureKeyword || item.name.includes(signatureKeyword) || item.application.includes(signatureKeyword))
)),
[enterpriseKeyword, signatureKeyword, mmsSignatures],
);
const pagedSmsSignatures = paginate(filteredSmsSignatures, page, pageSize);
const pagedMmsSignatures = paginate(filteredMmsSignatures, page, pageSize);
function upsertSignature(kind: SignatureKind, nextItem: SignatureItem) {
const setter = kind === 'sms' ? setSmsSignatures : setMmsSignatures;
setter((current) => current.some((item) => item.id === nextItem.id)
? current.map((item) => item.id === nextItem.id ? nextItem : item)
: [nextItem, ...current]);
setSignatureModal(null);
function loadData() {
adminApi.listEnterpriseSignatures({ keyword })
.then((items) => {
setSignatures(items);
setError('');
})
.catch((failure: Error) => setError(failure.message || '企业签名加载失败'));
}
function upsertDrainage(signatureId: string, nextItem: DrainageInfo) {
setSmsSignatures((current) => current.map((signature) => {
if (signature.id !== signatureId) {
return signature;
}
const drainage = signature.drainage ?? [];
const nextDrainage = drainage.some((item) => item.id === nextItem.id)
? drainage.map((item) => item.id === nextItem.id ? nextItem : item)
: [nextItem, ...drainage];
return { ...signature, drainage: nextDrainage };
}));
setDrainageModal(null);
setExpandedSignatureId(signatureId);
}
useEffect(() => {
loadData();
}, []);
function confirmDelete() {
if (!deleteTarget) {
return;
}
if (deleteTarget.kind === 'signature') {
const setter = deleteTarget.signatureKind === 'sms' ? setSmsSignatures : setMmsSignatures;
setter((current) => current.filter((item) => item.id !== deleteTarget.id));
} else {
setSmsSignatures((current) => current.map((signature) => signature.id === deleteTarget.signatureId
? { ...signature, drainage: (signature.drainage ?? []).filter((item) => item.id !== deleteTarget.id) }
: signature));
}
setDeleteTarget(null);
}
const filtered = useMemo(() => signatures.filter((item) => !keyword || [item.name, item.purpose, item.auditStatus].join(' ').includes(keyword)), [keyword, signatures]);
const mmsColumns = useMemo<Array<TableColumn<SignatureItem>>>(() => [
{ key: 'name', title: '签名名称', width: '170px', render: (record) => <strong>{record.name}</strong> },
{ key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise },
{ key: 'application', title: '应用', width: '180px', render: (record) => record.application },
{ key: 'mobile', title: '移动', width: '110px', render: (record) => <StatusTag status={record.mobile} /> },
{ key: 'unicom', title: '联通', width: '110px', render: (record) => <StatusTag status={record.unicom} /> },
{ key: 'telecom', title: '电信', width: '110px', render: (record) => <StatusTag status={record.telecom} /> },
{ key: 'updatedAt', title: '更新时间', width: '180px', render: (record) => record.updatedAt },
{
key: 'actions',
title: '操作',
align: 'right',
width: '230px',
render: (record) => (
<div className="table-actions">
<Button icon={<FileText size={15} />} onClick={() => setSignatureReport(record)} size="sm" variant="ghost"></Button>
<Button icon={<Edit3 size={15} />} onClick={() => setSignatureModal({ kind: 'mms', item: record })} size="sm" variant="ghost"></Button>
<Button icon={<Trash2 size={15} />} onClick={() => setDeleteTarget({ kind: 'signature', signatureKind: 'mms', id: record.id, name: record.name })} size="sm" variant="danger"></Button>
</div>
),
},
], []);
const smsSignatureContent = (
<>
<div className="signature-list admin-enterprise-signature-list">
{pagedSmsSignatures.map((signature) => {
const expanded = expandedSignatureId === signature.id;
return (
<article className="signature-card signature-card--green" key={signature.id}>
<div className="signature-summary">
<button aria-label="展开签名" onClick={() => setExpandedSignatureId(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.enterprise}</strong></div>
<div><span></span><strong>{signature.application}</strong></div>
<div><span></span><StatusTag status={signature.mobile} /></div>
<div><span></span><StatusTag status={signature.unicom} /></div>
<div><span></span><StatusTag status={signature.telecom} /></div>
<div><span></span><strong>{signature.drainage?.length ?? 0} </strong></div>
<div className="signature-actions">
<Button icon={<FileText size={16} />} onClick={() => setSignatureReport(signature)} size="sm" variant="ghost"></Button>
<Button icon={<Edit3 size={16} />} onClick={() => setSignatureModal({ kind: 'sms', item: signature })} size="sm" variant="ghost"></Button>
<Button icon={<Trash2 size={16} />} onClick={() => setDeleteTarget({ kind: 'signature', signatureKind: 'sms', id: signature.id, name: signature.name })} size="sm" variant="danger"></Button>
</div>
</div>
{expanded ? (
<div className="drainage-panel">
<h2></h2>
{signature.drainage?.length ? (
<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.url}>{item.url}</a>
<StatusTag status={item.mobile} />
<StatusTag status={item.unicom} />
<StatusTag status={item.telecom} />
<span className="muted">{item.submittedAt}</span>
<span className="drainage-row-actions">
<Button onClick={() => setDrainageReport(item)} size="sm" variant="ghost"></Button>
<Button onClick={() => setDrainageModal({ signatureId: signature.id, item })} size="sm" variant="ghost"></Button>
<Button onClick={() => setDeleteTarget({ kind: 'drainage', signatureId: signature.id, id: item.id, name: item.siteName })} size="sm" variant="danger"></Button>
</span>
</div>
))}
</div>
) : (
<p className="muted"></p>
)}
<div className="drainage-panel__footer">
<Button icon={<Plus size={16} />} onClick={() => setDrainageModal({ signatureId: signature.id })} size="sm" variant="ghost"></Button>
</div>
</div>
) : null}
</article>
);
})}
</div>
<Pagination onPageChange={setPage} page={page} pageSize={pageSize} total={filteredSmsSignatures.length} />
</>
);
const columns: Array<TableColumn<ClientSmsSignature>> = [
{ key: 'name', title: '签名名称', render: (record) => <strong>{record.name}</strong> },
{ key: 'tenant', title: '企业', render: (record) => record.tenantId },
{ key: 'purpose', title: '用途', render: (record) => record.purpose ?? '-' },
{ key: 'materials', title: '材料', render: (record) => `${record.materials?.length ?? 0}` },
{ 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={() => setSignatureModal({ kind: activeTab })}></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) => { setSignatureKeyword(event.target.value); setPage(1); }} placeholder="请输入签名或应用名称" prefix={<Search size={16} />} value={signatureKeyword} />
<Button onClick={() => { setEnterpriseKeyword(''); setSignatureKeyword(''); 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 SignatureKind); setPage(1); }}
value={activeTab}
items={[
{ label: '短信签名', value: 'sms', content: smsSignatureContent },
{
label: '彩信签名',
pending: true,
value: 'mms',
content: (
<>
<Table columns={mmsColumns} data={pagedMmsSignatures} rowKey="id" />
<Pagination onPageChange={setPage} page={page} pageSize={pageSize} total={filteredMmsSignatures.length} />
</>
),
},
]}
/>
<div className="surface">
<Table columns={columns} data={filtered} emptyText="暂无企业签名" rowKey="id" />
</div>
{signatureModal ? (
<SignatureFormModal
item={signatureModal.item}
kind={signatureModal.kind}
onClose={() => setSignatureModal(null)}
onSubmit={(item) => upsertSignature(signatureModal.kind, item)}
/>
) : null}
{signatureReport ? <SignatureReportModal item={signatureReport} onClose={() => setSignatureReport(null)} /> : null}
{drainageModal ? (
<DrainageFormModal
item={drainageModal.item}
onClose={() => setDrainageModal(null)}
onSubmit={(item) => upsertDrainage(drainageModal.signatureId, item)}
/>
) : null}
{drainageReport ? <DrainageReportModal item={drainageReport} onClose={() => setDrainageReport(null)} /> : null}
{deleteTarget ? (
<ConfirmModal
message={`确认删除“${deleteTarget.name}”吗?删除后仅影响当前本地 mock 数据。`}
onCancel={() => setDeleteTarget(null)}
onConfirm={confirmDelete}
/>
) : null}
</section>
);
}