437 lines
17 KiB
TypeScript
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>
|
|
);
|
|
}
|