Files
lislgosms/src/apps/admin/AdminEnterpriseSignaturesPage.tsx
T

437 lines
17 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { FileSpreadsheet, Plus, Search } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { adminApi, type ClientSmsApplication, type ClientSmsSignature, type TenantOption } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Modal, Tabs } from '@/components/ui';
import { ReportMaterialImportModal } from './ReportMaterialImportModal';
import { DrainageFormModal } from './enterprise-signatures/DrainageFormModal';
import { EnterpriseSignaturesTable } from './enterprise-signatures/EnterpriseSignaturesTable';
import { SignatureFormModal } from './enterprise-signatures/SignatureFormModal';
import {
ChannelReportStatusModal,
ConfirmModal,
DrainageReportStatusModal,
} from './enterprise-signatures/SignatureReportModals';
import { buildDrainagePayload, readDrainagePayload } from './enterprise-signatures/signature.helpers';
import type { DrainageInfo, SignatureFormState } from './enterprise-signatures/signature.types';
/** R4 page container: owns query state and coordinates focused presentation components. */
export function AdminEnterpriseSignaturesPage() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<'sms' | 'mms'>('sms');
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
const [deleteTarget, setDeleteTarget] = useState<{
kind: 'drainage';
signatureId: string;
id: string;
name: string;
} | null>(null);
const [drainageModal, setDrainageModal] = useState<{ signatureId: string; item?: DrainageInfo } | null>(null);
const [drainageStatusTarget, setDrainageStatusTarget] = useState<{
signature: ClientSmsSignature;
item: DrainageInfo;
} | null>(null);
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
const [applicationKeyword, setApplicationKeyword] = useState('');
const [appliedApplicationKeyword, setAppliedApplicationKeyword] = useState('');
const [drainageKeyword, setDrainageKeyword] = useState('');
const [appliedDrainageKeyword, setAppliedDrainageKeyword] = useState('');
const [error, setError] = useState('');
const [expandedSignatureId, setExpandedSignatureId] = useState('');
const [signatureKeyword, setSignatureKeyword] = useState('');
const [appliedSignatureKeyword, setAppliedSignatureKeyword] = useState('');
const [signatureModal, setSignatureModal] = useState<ClientSmsSignature | 'new' | null>(null);
const [reportStatusTarget, setReportStatusTarget] = useState<ClientSmsSignature | null>(null);
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
const [total, setTotal] = useState(0);
const [pendingReportMaterialTotal, setPendingReportMaterialTotal] = useState(0);
const [tenants, setTenants] = useState<TenantOption[]>([]);
const [page, setPage] = useState(1);
const [importOpen, setImportOpen] = useState(false);
const [materialChangedSignature, setMaterialChangedSignature] = useState<ClientSmsSignature | null>(null);
const listRequestSequence = useRef(0);
const pageSize = 10;
async function loadData(
filters = {
enterpriseKeyword: appliedEnterpriseKeyword,
applicationKeyword: appliedApplicationKeyword,
signatureKeyword: appliedSignatureKeyword,
drainageKeyword: appliedDrainageKeyword,
},
targetPage = page,
) {
const sequence = ++listRequestSequence.current;
try {
const signatureResult = await adminApi.listEnterpriseSignaturesPage({ ...filters, page: targetPage, pageSize });
if (sequence !== listRequestSequence.current) return;
setSignatures(signatureResult.items);
setTotal(signatureResult.total);
setPendingReportMaterialTotal(signatureResult.pendingReportMaterialTotal);
setError('');
} catch (failure) {
if (sequence !== listRequestSequence.current) return;
setError(failure instanceof Error ? failure.message : '企业签名加载失败');
}
}
useEffect(() => {
let cancelled = false;
void Promise.all([adminApi.listTenantOptions(), adminApi.listEnterpriseApplicationOptions()])
.then(([tenantItems, applicationItems]) => {
if (cancelled) return;
setTenants(tenantItems.filter((tenant) => tenant.status !== 'deleted'));
setApplications(applicationItems);
})
.catch((failure: Error) => {
if (!cancelled) setError(failure.message || '企业及应用选项加载失败');
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
queueMicrotask(() => void loadData(undefined, page));
}, [page]);
const filteredSignatures = signatures;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleSignatures = signatures;
async function saveSignature(state: SignatureFormState) {
const existing = signatureModal && signatureModal !== 'new' ? signatureModal : null;
const existingPayload = existing ? readDrainagePayload(existing) : { links: [], signatureProfile: undefined };
const drainageInfo = buildDrainagePayload(
{
mobile: state.mobile,
unicom: state.unicom,
telecom: state.telecom,
},
existingPayload.links,
existingPayload.signatureProfile,
state.reportValues,
);
try {
let saved: ClientSmsSignature;
if (existing) {
saved = await adminApi.updateEnterpriseSignature(existing.id, {
applicationId: state.applicationId || null,
drainageInfo,
name: state.name,
purpose: state.purpose,
});
} else {
saved = await adminApi.createEnterpriseSignature({
applicationId: state.applicationId || undefined,
drainageInfo,
name: state.name,
purpose: state.purpose,
tenantId: state.tenantId,
});
}
setSignatureModal(null);
if (saved.reportMaterialChanged) setMaterialChangedSignature(saved);
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 exists = readDrainagePayload(signature).links.some((current) => current.id === item.id);
const body = { url: item.url.trim(), remark: item.remark, reportValues: item.reportValues };
if (exists) await adminApi.updateDrainageInfo(item.id, body);
else await adminApi.createDrainageInfo(signatureId, body);
setDrainageModal(null);
setExpandedSignatureId(signatureId);
await loadData();
}
async function editSignature(signature: ClientSmsSignature) {
try {
setSignatureModal(await adminApi.getEnterpriseSignature(signature.id));
} catch (failure) {
setError(failure instanceof Error ? failure.message : '签名详情加载失败');
}
}
async function openSignatureReport(signature: ClientSmsSignature) {
try {
const reportTargets = await adminApi.getEnterpriseSignatureReportTargets(signature.id);
setReportStatusTarget({ ...signature, reportTargets });
} catch (failure) {
setError(failure instanceof Error ? failure.message : '签名报备状态加载失败');
}
}
async function editDrainage(signature: ClientSmsSignature, item: DrainageInfo) {
try {
const detail = await adminApi.getEnterpriseSignature(signature.id);
const detailItem = readDrainagePayload(detail).links.find((candidate) => candidate.id === item.id);
if (!detailItem) throw new Error('引流信息不存在或已删除');
setDrainageModal({ signatureId: signature.id, item: detailItem });
} catch (failure) {
setError(failure instanceof Error ? failure.message : '引流信息详情加载失败');
}
}
async function openDrainageReport(signature: ClientSmsSignature, item: DrainageInfo) {
try {
const targets = await adminApi.getDrainageInfoReportTargets(item.id);
setDrainageStatusTarget({
signature: { ...signature, drainageReportTargets: { [item.id]: targets } },
item,
});
} catch (failure) {
setError(failure instanceof Error ? failure.message : '引流报备状态加载失败');
}
}
async function confirmDelete() {
if (!deleteTarget) {
return;
}
await adminApi.changeDrainageInfoStatus(deleteTarget.id, 'deleted', '运营端删除引流信息');
setDeleteTarget(null);
await loadData();
}
const smsSignatureContent = (
<EnterpriseSignaturesTable
appliedDrainageKeyword={appliedDrainageKeyword}
currentPage={currentPage}
expandedSignatureId={expandedSignatureId}
filteredSignatures={filteredSignatures}
loadData={loadData}
onAddDrainage={(signature) => setDrainageModal({ signatureId: signature.id })}
setDeleteTarget={setDeleteTarget}
onEditDrainage={(signature, item) => void editDrainage(signature, item)}
onOpenDrainageReport={(signature, item) => void openDrainageReport(signature, item)}
setExpandedSignatureId={setExpandedSignatureId}
setPage={setPage}
onEditSignature={(signature) => void editSignature(signature)}
onOpenSignatureReport={(signature) => void openSignatureReport(signature)}
total={total}
totalPages={totalPages}
visibleSignatures={visibleSignatures}
/>
);
return (
<section className="page-stack admin-customer-split-page">
<div className="page-heading">
<div>
<Breadcrumb items={['客户管理', '企业签名管理']} />
<h1>企业签名管理</h1>
</div>
<div className="page-heading__actions">
<Button icon={<FileSpreadsheet size={16} />} onClick={() => setImportOpen(true)} variant="ghost">
批量导入签名及引流资料
</Button>
<Button icon={<Plus size={16} />} onClick={() => setSignatureModal(activeTab === 'sms' ? 'new' : null)}>
添加签名
</Button>
</div>
</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) => setApplicationKeyword(event.target.value)}
placeholder="请输入企业应用名称"
prefix={<Search size={16} />}
value={applicationKeyword}
/>
<Input
label="签名"
onChange={(event) => setSignatureKeyword(event.target.value)}
placeholder="请输入签名"
prefix={<Search size={16} />}
value={signatureKeyword}
/>
<Input
label="引流信息"
onChange={(event) => setDrainageKeyword(event.target.value)}
placeholder="请输入引流信息、URL 或备注"
prefix={<Search size={16} />}
value={drainageKeyword}
/>
<div className="admin-split-filter__actions">
<Button
icon={<Search size={16} />}
onClick={() => {
const filters = {
enterpriseKeyword: enterpriseKeyword.trim(),
applicationKeyword: applicationKeyword.trim(),
signatureKeyword: signatureKeyword.trim(),
drainageKeyword: drainageKeyword.trim(),
};
setAppliedEnterpriseKeyword(filters.enterpriseKeyword);
setAppliedApplicationKeyword(filters.applicationKeyword);
setAppliedSignatureKeyword(filters.signatureKeyword);
setAppliedDrainageKeyword(filters.drainageKeyword);
if (page !== 1) setPage(1);
else void loadData(filters, 1);
}}
>
查询
</Button>
<Button
onClick={() => {
const filters = {
enterpriseKeyword: '',
applicationKeyword: '',
signatureKeyword: '',
drainageKeyword: '',
};
setEnterpriseKeyword('');
setApplicationKeyword('');
setSignatureKeyword('');
setDrainageKeyword('');
setAppliedEnterpriseKeyword('');
setAppliedApplicationKeyword('');
setAppliedSignatureKeyword('');
setAppliedDrainageKeyword('');
if (page !== 1) setPage(1);
else void loadData(filters, 1);
}}
variant="ghost"
>
重置
</Button>
</div>
</div>
<div className="enterprise-signature-pending-summary" role="status">
<span>待生成报备资料</span>
<strong>{pendingReportMaterialTotal}</strong>
<span></span>
<Button onClick={() => navigate('/admin/report-materials')} size="sm" variant="ghost">
查看详情
</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}
{materialChangedSignature ? (
<Modal
footer={
<>
<Button onClick={() => setMaterialChangedSignature(null)} variant="ghost">
稍后处理
</Button>
<Button
onClick={() => {
const id = materialChangedSignature.id;
setMaterialChangedSignature(null);
navigate(`/admin/report-materials?signatureId=${encodeURIComponent(id)}`);
}}
>
前往报备资料池
</Button>
</>
}
onClose={() => setMaterialChangedSignature(null)}
open
title="签名资料已更新"
>
<div className="signature-alert">
<FileSpreadsheet size={20} />
<span>资料发生变化,如需提交至通道报备,请到“报备工作台-报备资料池”生成报备批次。</span>
</div>
</Modal>
) : null}
{importOpen ? (
<ReportMaterialImportModal
onClose={() => setImportOpen(false)}
onCompleted={(result) => {
navigate(`/admin/signatures?tab=import&batchId=${encodeURIComponent(result.id)}&imported=1`);
}}
/>
) : null}
{reportStatusTarget ? (
<ChannelReportStatusModal
item={reportStatusTarget}
onClose={() => setReportStatusTarget(null)}
onSaved={() => {
setReportStatusTarget(null);
void loadData();
}}
/>
) : null}
{drainageModal ? (
<DrainageFormModal
applicationId={signatures.find((item) => item.id === drainageModal.signatureId)?.applicationId}
item={drainageModal.item}
onClose={() => setDrainageModal(null)}
onSubmit={(item) => {
void saveDrainage(drainageModal.signatureId, item);
}}
/>
) : null}
{drainageStatusTarget ? (
<DrainageReportStatusModal
item={drainageStatusTarget.item}
onClose={() => setDrainageStatusTarget(null)}
onSaved={() => {
setDrainageStatusTarget(null);
void loadData();
}}
signature={drainageStatusTarget.signature}
/>
) : null}
{deleteTarget ? (
<ConfirmModal
message={`确认删除“${deleteTarget.name}”吗?删除后会写入真实后台。`}
onCancel={() => setDeleteTarget(null)}
onConfirm={() => {
void confirmDelete();
}}
/>
) : null}
</section>
);
}