743 lines
34 KiB
TypeScript
743 lines
34 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
||
import { ChevronDown, ChevronRight, Edit3, FileText, Info, Plus, Search, Trash2, Upload } from 'lucide-react';
|
||
import { adminApi, type ClientSmsApplication, type ClientSmsSignature, type FileRef, type TenantOption } from '@/api/adminApi';
|
||
import { Breadcrumb, Button, FileActions, Input, Modal, Pagination, Select, Tabs, Tag, Textarea } from '@/components/ui';
|
||
import { displayFileName } from '@/utils/fileName';
|
||
import { formatDateTime } from '@/utils/dateTime';
|
||
|
||
type CarrierStatus = 'approved' | 'pending' | 'rejected' | 'filing';
|
||
|
||
type DrainageInfo = {
|
||
id: string;
|
||
siteName: string;
|
||
url: string;
|
||
field1File?: UploadedFileRef | null;
|
||
field2?: string;
|
||
field3?: string;
|
||
field4?: string;
|
||
field5?: string;
|
||
field6?: string;
|
||
field7File?: UploadedFileRef | null;
|
||
field8?: string;
|
||
field9?: string;
|
||
field10?: string;
|
||
mobile: CarrierStatus;
|
||
unicom: CarrierStatus;
|
||
telecom: CarrierStatus;
|
||
submittedAt: string;
|
||
remark: string;
|
||
};
|
||
|
||
type UploadedFileRef = FileRef;
|
||
|
||
type SignatureProfile = {
|
||
basis: string;
|
||
companyName: string;
|
||
creditCode: string;
|
||
legalPersonName: string;
|
||
legalPersonIdCard: string;
|
||
responsibleName: string;
|
||
responsiblePhone: string;
|
||
responsibleIdCard: string;
|
||
credentialFile?: UploadedFileRef | null;
|
||
legalFrontFile?: UploadedFileRef | null;
|
||
legalBackFile?: UploadedFileRef | null;
|
||
responsibleFrontFile?: UploadedFileRef | null;
|
||
responsibleBackFile?: UploadedFileRef | null;
|
||
};
|
||
|
||
type SignatureFormState = {
|
||
tenantId: string;
|
||
applicationId: string;
|
||
name: string;
|
||
purpose: string;
|
||
profile: SignatureProfile;
|
||
mobile: CarrierStatus;
|
||
unicom: CarrierStatus;
|
||
telecom: CarrierStatus;
|
||
};
|
||
|
||
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',
|
||
};
|
||
|
||
function StatusTag({ status }: { status: CarrierStatus }) {
|
||
return <Tag tone={statusToneMap[status]}>{statusLabelMap[status]}</Tag>;
|
||
}
|
||
|
||
function readDrainagePayload(signature: ClientSmsSignature) {
|
||
const payload = signature.drainageInfo && typeof signature.drainageInfo === 'object' ? signature.drainageInfo : {};
|
||
const carrierStatus = typeof payload.carrierStatus === 'object' && payload.carrierStatus ? payload.carrierStatus as Record<string, unknown> : {};
|
||
const profile = typeof payload.signatureProfile === 'object' && payload.signatureProfile ? payload.signatureProfile as Record<string, unknown> : {};
|
||
const links = Array.isArray(payload.links) ? payload.links as Array<Record<string, unknown>> : [];
|
||
const fallbackStatus = normalizeCarrierStatus(signature.auditStatus);
|
||
return {
|
||
carrierStatus: {
|
||
mobile: normalizeCarrierStatus(carrierStatus.mobile, fallbackStatus),
|
||
unicom: normalizeCarrierStatus(carrierStatus.unicom, fallbackStatus),
|
||
telecom: normalizeCarrierStatus(carrierStatus.telecom, fallbackStatus),
|
||
},
|
||
signatureProfile: normalizeSignatureProfile(profile, signature),
|
||
links: links.map((item) => ({
|
||
id: String(item.id ?? `drain-${Date.now()}`),
|
||
siteName: String(item.siteName ?? ''),
|
||
url: String(item.url ?? ''),
|
||
field1File: normalizeUploadedFile(item.field1File),
|
||
field2: String(item.field2 ?? ''),
|
||
field3: String(item.field3 ?? ''),
|
||
field4: String(item.field4 ?? ''),
|
||
field5: String(item.field5 ?? ''),
|
||
field6: String(item.field6 ?? ''),
|
||
field7File: normalizeUploadedFile(item.field7File),
|
||
field8: String(item.field8 ?? ''),
|
||
field9: String(item.field9 ?? ''),
|
||
field10: String(item.field10 ?? ''),
|
||
mobile: normalizeCarrierStatus(item.mobile, 'filing'),
|
||
unicom: normalizeCarrierStatus(item.unicom, 'filing'),
|
||
telecom: normalizeCarrierStatus(item.telecom, 'filing'),
|
||
submittedAt: String(item.submittedAt ?? ''),
|
||
remark: String(item.remark ?? ''),
|
||
})),
|
||
};
|
||
}
|
||
|
||
function buildDrainagePayload(carrierStatus: { mobile: CarrierStatus; unicom: CarrierStatus; telecom: CarrierStatus }, links: DrainageInfo[], signatureProfile?: SignatureProfile) {
|
||
return { carrierStatus, links, signatureProfile };
|
||
}
|
||
|
||
function normalizeUploadedFile(value: unknown): UploadedFileRef | null {
|
||
if (!value || typeof value !== 'object') return null;
|
||
const item = value as Record<string, unknown>;
|
||
const fileObjectId = String(item.fileObjectId ?? '');
|
||
const fileName = String(item.fileName ?? '');
|
||
const contentType = typeof item.contentType === 'string' ? item.contentType : undefined;
|
||
return fileObjectId || fileName ? { contentType, fileObjectId, fileName } : null;
|
||
}
|
||
|
||
function emptySignatureProfile(signature?: ClientSmsSignature): SignatureProfile {
|
||
return {
|
||
basis: '',
|
||
companyName: signature?.tenant?.name ?? '',
|
||
creditCode: '',
|
||
legalPersonName: '',
|
||
legalPersonIdCard: '',
|
||
responsibleName: '',
|
||
responsiblePhone: '',
|
||
responsibleIdCard: '',
|
||
credentialFile: null,
|
||
legalFrontFile: null,
|
||
legalBackFile: null,
|
||
responsibleFrontFile: null,
|
||
responsibleBackFile: null,
|
||
};
|
||
}
|
||
|
||
function normalizeSignatureProfile(value: Record<string, unknown>, signature?: ClientSmsSignature): SignatureProfile {
|
||
return {
|
||
...emptySignatureProfile(signature),
|
||
basis: String(value.basis ?? ''),
|
||
companyName: String(value.companyName ?? signature?.tenant?.name ?? ''),
|
||
creditCode: String(value.creditCode ?? ''),
|
||
legalPersonName: String(value.legalPersonName ?? ''),
|
||
legalPersonIdCard: String(value.legalPersonIdCard ?? ''),
|
||
responsibleName: String(value.responsibleName ?? ''),
|
||
responsiblePhone: String(value.responsiblePhone ?? ''),
|
||
responsibleIdCard: String(value.responsibleIdCard ?? ''),
|
||
credentialFile: normalizeUploadedFile(value.credentialFile),
|
||
legalFrontFile: normalizeUploadedFile(value.legalFrontFile),
|
||
legalBackFile: normalizeUploadedFile(value.legalBackFile),
|
||
responsibleFrontFile: normalizeUploadedFile(value.responsibleFrontFile),
|
||
responsibleBackFile: normalizeUploadedFile(value.responsibleBackFile),
|
||
};
|
||
}
|
||
|
||
function normalizeCarrierStatus(value: unknown, fallback: CarrierStatus = 'filing'): CarrierStatus {
|
||
return value === 'approved' || value === 'pending' || value === 'rejected' || value === 'filing' ? value : fallback;
|
||
}
|
||
|
||
function signatureCardTone(statuses: { mobile: CarrierStatus; unicom: CarrierStatus; telecom: CarrierStatus }) {
|
||
const values = Object.values(statuses);
|
||
if (values.includes('rejected')) return 'red';
|
||
if (values.every((status) => status === 'approved')) return 'green';
|
||
if (values.includes('pending')) return 'blue';
|
||
return 'gray';
|
||
}
|
||
|
||
function formatDate(value?: string) {
|
||
return formatDateTime(value);
|
||
}
|
||
|
||
function SignatureUploadBox({
|
||
compact = false,
|
||
file,
|
||
label,
|
||
onUploaded,
|
||
}: {
|
||
compact?: boolean;
|
||
file?: UploadedFileRef | null;
|
||
label: string;
|
||
onUploaded: (file: UploadedFileRef) => void;
|
||
}) {
|
||
const [uploading, setUploading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
async function uploadFile(fileInput: File | undefined) {
|
||
if (!fileInput) return;
|
||
setUploading(true);
|
||
setError('');
|
||
try {
|
||
const fileObject = await adminApi.uploadFileObject(fileInput, { purpose: 'signature_report_material', prefix: 'signature-report-materials' });
|
||
onUploaded({ contentType: fileObject.contentType, fileObjectId: fileObject.id, fileName: fileObject.fileName });
|
||
} catch (failure) {
|
||
setError(failure instanceof Error ? failure.message : '文件上传失败');
|
||
} finally {
|
||
setUploading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<label className={compact ? 'signature-upload signature-upload--compact' : 'signature-upload'}>
|
||
<span>{label}</span>
|
||
<Upload size={compact ? 30 : 42} />
|
||
<strong>{uploading ? '上传中...' : (file ? displayFileName(file.fileName) : '') || (compact ? '上传文件' : '点击上传 或拖拽文件到此处')}</strong>
|
||
<FileActions file={file} />
|
||
{!compact ? <small>支持 PNG、JPG、JPEG、PDF,文件大小不超过 10M</small> : null}
|
||
{error ? <small className="form-error">{error}</small> : null}
|
||
<input
|
||
accept="image/png,image/jpeg,application/pdf"
|
||
disabled={uploading}
|
||
onChange={(event) => { void uploadFile(event.target.files?.[0]); }}
|
||
style={{ display: 'none' }}
|
||
type="file"
|
||
/>
|
||
</label>
|
||
);
|
||
}
|
||
|
||
function SignatureFormModal({
|
||
applications,
|
||
item,
|
||
onClose,
|
||
onSubmit,
|
||
tenants,
|
||
}: {
|
||
applications: ClientSmsApplication[];
|
||
item?: ClientSmsSignature;
|
||
onClose: () => void;
|
||
onSubmit: (state: SignatureFormState) => void;
|
||
tenants: TenantOption[];
|
||
}) {
|
||
const payload = item ? readDrainagePayload(item) : null;
|
||
const [form, setForm] = useState<SignatureFormState>({
|
||
tenantId: item?.tenantId ?? '',
|
||
applicationId: item?.applicationId ?? '',
|
||
name: item?.name ?? '',
|
||
purpose: item?.purpose ?? '',
|
||
profile: payload?.signatureProfile ?? emptySignatureProfile(item),
|
||
mobile: payload?.carrierStatus.mobile ?? 'filing',
|
||
unicom: payload?.carrierStatus.unicom ?? 'filing',
|
||
telecom: payload?.carrierStatus.telecom ?? 'filing',
|
||
});
|
||
const tenantApplications = applications.filter((application) => application.tenantId === form.tenantId && application.status !== 'deleted');
|
||
|
||
function update<Key extends keyof SignatureFormState>(key: Key, value: SignatureFormState[Key]) {
|
||
setForm((current) => ({ ...current, [key]: value }));
|
||
}
|
||
|
||
function updateProfile<Key extends keyof SignatureProfile>(key: Key, value: SignatureProfile[Key]) {
|
||
setForm((current) => ({ ...current, profile: { ...current.profile, [key]: value } }));
|
||
}
|
||
|
||
return (
|
||
<Modal
|
||
footer={(
|
||
<>
|
||
<Button onClick={onClose} variant="ghost">取消</Button>
|
||
<Button disabled={!form.tenantId || !form.name} onClick={() => onSubmit(form)}>保存</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>签名需履行报备,并遵照管理部门审核结果方可使用。请用 PNG、JPG、JPEG 或 PDF 格式上传真实材料。</span>
|
||
</div>
|
||
<div className="signature-form-grid">
|
||
<Select
|
||
disabled={Boolean(item)}
|
||
label="所属企业"
|
||
onChange={(event) => update('tenantId', event.target.value)}
|
||
options={[
|
||
{ label: '请选择企业', value: '' },
|
||
...tenants.map((tenant) => ({ label: `${tenant.name}(${tenant.code})`, value: tenant.id })),
|
||
]}
|
||
required
|
||
value={form.tenantId}
|
||
/>
|
||
<Select
|
||
label="* 应用名称"
|
||
onChange={(event) => update('applicationId', event.target.value)}
|
||
options={[
|
||
{ label: '不绑定应用', value: '' },
|
||
...tenantApplications.map((application) => ({ label: application.name, value: application.id })),
|
||
]}
|
||
value={form.applicationId}
|
||
/>
|
||
<Select
|
||
label="* 签名依据"
|
||
onChange={(event) => updateProfile('basis', event.target.value)}
|
||
options={[
|
||
{ label: '请选择签名依据', value: '' },
|
||
{ label: '企事业单位证明', value: 'company' },
|
||
{ label: '商标注册证', value: 'trademark' },
|
||
{ label: '授权委托书', value: 'authorization' },
|
||
]}
|
||
value={form.profile.basis}
|
||
/>
|
||
<Input label="* 短信签名" onChange={(event) => update('name', event.target.value)} placeholder="请输入短信签名,如【XXXX公司】" required value={form.name} />
|
||
</div>
|
||
<SignatureUploadBox
|
||
file={form.profile.credentialFile}
|
||
label="* 资质凭证"
|
||
onUploaded={(file) => updateProfile('credentialFile', file)}
|
||
/>
|
||
</section>
|
||
|
||
<section>
|
||
<h3>公司信息</h3>
|
||
<div className="signature-form-grid">
|
||
<Input label="* 公司名称" onChange={(event) => updateProfile('companyName', event.target.value)} placeholder="请输入公司名称" value={form.profile.companyName} />
|
||
<Input label="* 统一社会信用代码" onChange={(event) => updateProfile('creditCode', event.target.value)} placeholder="请输入统一社会信用代码" value={form.profile.creditCode} />
|
||
<Input label="* 法人姓名" onChange={(event) => updateProfile('legalPersonName', event.target.value)} placeholder="请输入法人姓名" value={form.profile.legalPersonName} />
|
||
<Input label="法人身份证号" onChange={(event) => updateProfile('legalPersonIdCard', event.target.value)} placeholder="请输入法人身份证号" value={form.profile.legalPersonIdCard} />
|
||
<SignatureUploadBox compact file={form.profile.legalFrontFile} label="法人身份证照片-人像面" onUploaded={(file) => updateProfile('legalFrontFile', file)} />
|
||
<SignatureUploadBox compact file={form.profile.legalBackFile} label="法人身份证照片-国徽面" onUploaded={(file) => updateProfile('legalBackFile', file)} />
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h3>责任人信息</h3>
|
||
<div className="signature-form-grid">
|
||
<Input label="* 责任人姓名" onChange={(event) => updateProfile('responsibleName', event.target.value)} placeholder="请输入责任人姓名" value={form.profile.responsibleName} />
|
||
<Input label="* 责任人手机号" onChange={(event) => updateProfile('responsiblePhone', event.target.value)} placeholder="请输入责任人手机号" value={form.profile.responsiblePhone} />
|
||
<Input className="signature-form-grid__wide" label="* 责任人身份证号" onChange={(event) => updateProfile('responsibleIdCard', event.target.value)} placeholder="请输入责任人身份证号" value={form.profile.responsibleIdCard} />
|
||
<SignatureUploadBox compact file={form.profile.responsibleFrontFile} label="责任人身份证照片-人像面" onUploaded={(file) => updateProfile('responsibleFrontFile', file)} />
|
||
<SignatureUploadBox compact file={form.profile.responsibleBackFile} label="责任人身份证照片-国徽面" onUploaded={(file) => updateProfile('responsibleBackFile', file)} />
|
||
</div>
|
||
</section>
|
||
|
||
</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: '',
|
||
field1File: null,
|
||
field2: '',
|
||
field3: '',
|
||
field4: '',
|
||
field5: '',
|
||
field6: '',
|
||
field7File: null,
|
||
field8: '',
|
||
field9: '',
|
||
field10: '',
|
||
mobile: 'filing',
|
||
unicom: 'filing',
|
||
telecom: 'filing',
|
||
submittedAt: formatDateTime(new Date()),
|
||
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 disabled={!form.url} 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="请输入引流网址"
|
||
required
|
||
value={form.url}
|
||
/>
|
||
<div className="signature-alert drainage-form-note">
|
||
<Info size={18} />
|
||
<ol>
|
||
<li>本页面中所填的信息需与短信内容应用所包含的网站或服务保持一致;</li>
|
||
<li>图片仅支持 PNG、JPG 或 JPEG 格式的正版文件,且大小不超过 3M;</li>
|
||
<li>文件格式支持 PDF 格式或者图片,且大小不超过 10M。</li>
|
||
</ol>
|
||
</div>
|
||
<div className="signature-form-grid">
|
||
<SignatureUploadBox compact file={form.field1File} label="* 字段名称1" onUploaded={(file) => update('field1File', file)} />
|
||
<Input label="* 字段名称2" onChange={(event) => update('field2', event.target.value)} placeholder="请输入字段2内容" value={form.field2 ?? ''} />
|
||
<Input label="* 字段名称3" onChange={(event) => { update('field3', event.target.value); update('siteName', event.target.value); }} placeholder="请输入公司名称" value={form.field3 ?? form.siteName} />
|
||
<Input label="字段名称4" onChange={(event) => update('field4', event.target.value)} placeholder="请输入统一社会信用代码" value={form.field4 ?? ''} />
|
||
<Input label="* 字段名称5" onChange={(event) => update('field5', event.target.value)} placeholder="请输入法人姓名" value={form.field5 ?? ''} />
|
||
<Input label="字段名称6" onChange={(event) => update('field6', event.target.value)} placeholder="请输入法人身份证号" value={form.field6 ?? ''} />
|
||
<SignatureUploadBox compact file={form.field7File} label="字段名称7" onUploaded={(file) => update('field7File', file)} />
|
||
<Input label="* 字段名称8" onChange={(event) => update('field8', event.target.value)} placeholder="请输入责任人身份证号" value={form.field8 ?? ''} />
|
||
<Input label="* 字段名称9" onChange={(event) => update('field9', event.target.value)} placeholder="请输入责任人姓名" value={form.field9 ?? ''} />
|
||
<Input label="* 字段名称10" onChange={(event) => update('field10', event.target.value)} placeholder="请输入责任人手机号" value={form.field10 ?? ''} />
|
||
<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 SignatureReportModal({ item, onClose }: { item: ClientSmsSignature; onClose: () => void }) {
|
||
const payload = readDrainagePayload(item);
|
||
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.tenant?.name ?? item.tenantId}</strong></div>
|
||
<div><span>应用名称</span><strong>{item.application?.name ?? '-'}</strong></div>
|
||
<div><span>签名名称</span><strong>{item.name}</strong></div>
|
||
<div><span>更新时间</span><strong>{formatDate(item.updatedAt)}</strong></div>
|
||
</div>
|
||
<div className="admin-report-tabs">
|
||
<button className="admin-report-carrier--mobile active" type="button"><strong>移动</strong><span><StatusTag status={payload.carrierStatus.mobile} /></span></button>
|
||
<button className="admin-report-carrier--unicom active" type="button"><strong>联通</strong><span><StatusTag status={payload.carrierStatus.unicom} /></span></button>
|
||
<button className="admin-report-carrier--telecom active" type="button"><strong>电信</strong><span><StatusTag status={payload.carrierStatus.telecom} /></span></button>
|
||
</div>
|
||
</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>
|
||
);
|
||
}
|
||
|
||
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>
|
||
);
|
||
}
|
||
|
||
export function AdminEnterpriseSignaturesPage() {
|
||
const [activeTab, setActiveTab] = useState<'sms' | 'mms'>('sms');
|
||
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
|
||
const [deleteTarget, setDeleteTarget] = useState<{ kind: 'signature'; id: string; name: string } | { kind: 'drainage'; signatureId: string; id: string; name: string } | null>(null);
|
||
const [drainageModal, setDrainageModal] = useState<{ signatureId: string; item?: DrainageInfo } | null>(null);
|
||
const [drainageReport, setDrainageReport] = useState<DrainageInfo | null>(null);
|
||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||
const [error, setError] = useState('');
|
||
const [expandedSignatureId, setExpandedSignatureId] = useState('');
|
||
const [signatureKeyword, setSignatureKeyword] = useState('');
|
||
const [signatureModal, setSignatureModal] = useState<ClientSmsSignature | 'new' | null>(null);
|
||
const [signatureReport, setSignatureReport] = useState<ClientSmsSignature | null>(null);
|
||
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
|
||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||
const [page, setPage] = useState(1);
|
||
|
||
async function loadData() {
|
||
try {
|
||
const [signatureItems, tenantItems, applicationItems] = await Promise.all([
|
||
adminApi.listEnterpriseSignatures({ keyword: [enterpriseKeyword, signatureKeyword].filter(Boolean).join(' ') }),
|
||
adminApi.listTenants(),
|
||
adminApi.listEnterpriseApplications(),
|
||
]);
|
||
setSignatures(signatureItems);
|
||
setTenants(tenantItems.filter((tenant) => tenant.status !== 'deleted'));
|
||
setApplications(applicationItems);
|
||
setError('');
|
||
} catch (failure) {
|
||
setError(failure instanceof Error ? failure.message : '企业签名加载失败');
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
void loadData();
|
||
}, []);
|
||
|
||
const filteredSignatures = useMemo(() => signatures.filter((item) => {
|
||
const enterprise = item.tenant?.name ?? item.tenantId;
|
||
const application = item.application?.name ?? '';
|
||
return (!enterpriseKeyword || enterprise.includes(enterpriseKeyword))
|
||
&& (!signatureKeyword || item.name.includes(signatureKeyword) || application.includes(signatureKeyword) || (item.purpose ?? '').includes(signatureKeyword));
|
||
}), [enterpriseKeyword, signatureKeyword, signatures]);
|
||
const pageSize = 10;
|
||
const totalPages = Math.max(1, Math.ceil(filteredSignatures.length / pageSize));
|
||
const currentPage = Math.min(page, totalPages);
|
||
const visibleSignatures = filteredSignatures.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||
|
||
useEffect(() => {
|
||
setPage(1);
|
||
}, [enterpriseKeyword, filteredSignatures.length, signatureKeyword]);
|
||
|
||
async function saveSignature(state: SignatureFormState) {
|
||
const existing = signatureModal && signatureModal !== 'new' ? signatureModal : null;
|
||
const existingPayload = existing ? readDrainagePayload(existing) : { links: [] };
|
||
const drainageInfo = buildDrainagePayload({
|
||
mobile: state.mobile,
|
||
unicom: state.unicom,
|
||
telecom: state.telecom,
|
||
}, existingPayload.links, state.profile);
|
||
try {
|
||
if (existing) {
|
||
await adminApi.updateEnterpriseSignature(existing.id, {
|
||
applicationId: state.applicationId || null,
|
||
drainageInfo,
|
||
name: state.name,
|
||
purpose: state.purpose,
|
||
});
|
||
} else {
|
||
await adminApi.createEnterpriseSignature({
|
||
applicationId: state.applicationId || undefined,
|
||
drainageInfo,
|
||
name: state.name,
|
||
purpose: state.purpose,
|
||
tenantId: state.tenantId,
|
||
});
|
||
}
|
||
setSignatureModal(null);
|
||
await loadData();
|
||
} catch (failure) {
|
||
setError(failure instanceof Error ? failure.message : '企业签名保存失败');
|
||
}
|
||
}
|
||
|
||
async function saveDrainage(signatureId: string, item: DrainageInfo) {
|
||
const signature = signatures.find((current) => current.id === signatureId);
|
||
if (!signature) {
|
||
return;
|
||
}
|
||
const payload = readDrainagePayload(signature);
|
||
const links = payload.links.some((current) => current.id === item.id)
|
||
? payload.links.map((current) => current.id === item.id ? item : current)
|
||
: [item, ...payload.links];
|
||
await adminApi.updateEnterpriseSignature(signatureId, {
|
||
drainageInfo: buildDrainagePayload(payload.carrierStatus, links, payload.signatureProfile),
|
||
});
|
||
setDrainageModal(null);
|
||
setExpandedSignatureId(signatureId);
|
||
await loadData();
|
||
}
|
||
|
||
async function confirmDelete() {
|
||
if (!deleteTarget) {
|
||
return;
|
||
}
|
||
if (deleteTarget.kind === 'signature') {
|
||
await adminApi.changeEnterpriseSignatureStatus(deleteTarget.id, 'deleted', '运营端删除签名');
|
||
} else {
|
||
const signature = signatures.find((item) => item.id === deleteTarget.signatureId);
|
||
if (signature) {
|
||
const payload = readDrainagePayload(signature);
|
||
await adminApi.updateEnterpriseSignature(signature.id, {
|
||
drainageInfo: buildDrainagePayload(payload.carrierStatus, payload.links.filter((item) => item.id !== deleteTarget.id), payload.signatureProfile),
|
||
});
|
||
}
|
||
}
|
||
setDeleteTarget(null);
|
||
await loadData();
|
||
}
|
||
|
||
const smsSignatureContent = (
|
||
<div className="signature-list admin-enterprise-signature-list">
|
||
{visibleSignatures.map((signature) => {
|
||
const payload = readDrainagePayload(signature);
|
||
const expanded = expandedSignatureId === signature.id;
|
||
return (
|
||
<article className={`signature-card signature-card--${signatureCardTone(payload.carrierStatus)}`} 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.tenant?.name ?? signature.tenantId}</strong></div>
|
||
<div><span>应用</span><strong>{signature.application?.name ?? '-'}</strong></div>
|
||
<div><span>移动</span><StatusTag status={payload.carrierStatus.mobile} /></div>
|
||
<div><span>联通</span><StatusTag status={payload.carrierStatus.unicom} /></div>
|
||
<div><span>电信</span><StatusTag status={payload.carrierStatus.telecom} /></div>
|
||
<div><span>引流信息</span><strong>{payload.links.length} 条</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(signature)} size="sm" variant="ghost">编辑</Button>
|
||
<Button icon={<Trash2 size={16} />} onClick={() => setDeleteTarget({ kind: 'signature', id: signature.id, name: signature.name })} size="sm" variant="danger">删除</Button>
|
||
</div>
|
||
</div>
|
||
{expanded ? (
|
||
<div className="drainage-panel">
|
||
<h2>引流信息列表</h2>
|
||
{payload.links.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>
|
||
{payload.links.map((item) => (
|
||
<div className="drainage-table__row" key={item.id}>
|
||
<strong>{item.siteName}</strong>
|
||
<span className="drainage-table__url" title={item.url}>{item.url}</span>
|
||
<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>
|
||
);
|
||
})}
|
||
<Pagination
|
||
nextDisabled={currentPage >= totalPages}
|
||
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
|
||
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
|
||
page={currentPage}
|
||
totalPages={totalPages}
|
||
onPageChange={setPage}
|
||
previousDisabled={currentPage <= 1}
|
||
total={filteredSignatures.length}
|
||
/>
|
||
{filteredSignatures.length === 0 ? <div className="ui-table__empty">暂无企业签名</div> : null}
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<section className="page-stack admin-customer-split-page">
|
||
<div className="page-heading">
|
||
<div>
|
||
<Breadcrumb items={['客户管理', '企业签名管理']} />
|
||
<h1>企业签名管理</h1>
|
||
</div>
|
||
<Button icon={<Plus size={16} />} onClick={() => setSignatureModal(activeTab === 'sms' ? 'new' : null)}>添加签名</Button>
|
||
</div>
|
||
|
||
<div className="surface admin-split-filter">
|
||
<Input label="企业名称" onChange={(event) => setEnterpriseKeyword(event.target.value)} placeholder="请输入企业名称" prefix={<Search size={16} />} value={enterpriseKeyword} />
|
||
<Input label="签名/应用" onChange={(event) => setSignatureKeyword(event.target.value)} placeholder="请输入签名或应用名称" prefix={<Search size={16} />} value={signatureKeyword} />
|
||
<Button onClick={() => { setEnterpriseKeyword(''); setSignatureKeyword(''); void loadData(); }} variant="ghost">重置</Button>
|
||
<Button icon={<Search size={16} />} onClick={() => { void loadData(); }}>查询</Button>
|
||
</div>
|
||
|
||
{error ? <p className="form-error">{error}</p> : null}
|
||
|
||
<div className="surface section-stack">
|
||
<Tabs
|
||
onChange={(value) => setActiveTab(value as 'sms' | 'mms')}
|
||
value={activeTab}
|
||
items={[
|
||
{ label: '短信签名', value: 'sms', content: smsSignatureContent },
|
||
{ label: '彩信签名', pending: true, value: 'mms', content: <div className="ui-table__empty">彩信签名待后端能力确认,本页不展示演示数据。</div> },
|
||
]}
|
||
/>
|
||
</div>
|
||
|
||
{signatureModal ? (
|
||
<SignatureFormModal
|
||
applications={applications}
|
||
item={signatureModal === 'new' ? undefined : signatureModal}
|
||
onClose={() => setSignatureModal(null)}
|
||
onSubmit={(state) => { void saveSignature(state); }}
|
||
tenants={tenants}
|
||
/>
|
||
) : null}
|
||
{signatureReport ? <SignatureReportModal item={signatureReport} onClose={() => setSignatureReport(null)} /> : null}
|
||
{drainageModal ? (
|
||
<DrainageFormModal
|
||
item={drainageModal.item}
|
||
onClose={() => setDrainageModal(null)}
|
||
onSubmit={(item) => { void saveDrainage(drainageModal.signatureId, item); }}
|
||
/>
|
||
) : null}
|
||
{drainageReport ? <DrainageReportModal item={drainageReport} onClose={() => setDrainageReport(null)} /> : null}
|
||
{deleteTarget ? (
|
||
<ConfirmModal
|
||
message={`确认删除“${deleteTarget.name}”吗?删除后会写入真实后台。`}
|
||
onCancel={() => setDeleteTarget(null)}
|
||
onConfirm={() => { void confirmDelete(); }}
|
||
/>
|
||
) : null}
|
||
</section>
|
||
);
|
||
}
|