feat: build reporting workbench workflow

This commit is contained in:
hectorzhao
2026-09-02 14:30:15 +08:00
parent 53d3668306
commit 9b8196ecab
31 changed files with 7651 additions and 1973 deletions
+193 -51
View File
@@ -1,22 +1,36 @@
import { useEffect, 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, Tabs } from '@/components/ui';
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 {
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 [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 [drainageStatusTarget, setDrainageStatusTarget] = useState<{
signature: ClientSmsSignature;
item: DrainageInfo;
} | null>(null);
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
const [applicationKeyword, setApplicationKeyword] = useState('');
@@ -36,10 +50,20 @@ export function AdminEnterpriseSignaturesPage() {
const [page, setPage] = useState(1);
const [importOpen, setImportOpen] = useState(false);
const [message, setMessage] = useState('');
const [materialChangedSignature, setMaterialChangedSignature] = useState<ClientSmsSignature | null>(null);
const pageSize = 10;
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, signatureKeyword: appliedSignatureKeyword, drainageKeyword: appliedDrainageKeyword }, targetPage = page, targetSort = signatureSort) {
async function loadData(
filters = {
enterpriseKeyword: appliedEnterpriseKeyword,
applicationKeyword: appliedApplicationKeyword,
signatureKeyword: appliedSignatureKeyword,
drainageKeyword: appliedDrainageKeyword,
},
targetPage = page,
targetSort = signatureSort,
) {
try {
const [signatureResult, tenantItems, applicationItems] = await Promise.all([
adminApi.listEnterpriseSignaturesPage({ ...filters, signatureSort: targetSort, page: targetPage, pageSize }),
@@ -57,7 +81,7 @@ export function AdminEnterpriseSignaturesPage() {
}
useEffect(() => {
void loadData(undefined, page);
queueMicrotask(() => void loadData(undefined, page));
}, [page]);
const filteredSignatures = signatures;
@@ -68,21 +92,27 @@ export function AdminEnterpriseSignaturesPage() {
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);
const drainageInfo = buildDrainagePayload(
{
mobile: state.mobile,
unicom: state.unicom,
telecom: state.telecom,
},
existingPayload.links,
existingPayload.signatureProfile,
state.reportValues,
);
try {
let saved: ClientSmsSignature;
if (existing) {
await adminApi.updateEnterpriseSignature(existing.id, {
saved = await adminApi.updateEnterpriseSignature(existing.id, {
applicationId: state.applicationId || null,
drainageInfo,
name: state.name,
purpose: state.purpose,
});
} else {
await adminApi.createEnterpriseSignature({
saved = await adminApi.createEnterpriseSignature({
applicationId: state.applicationId || undefined,
drainageInfo,
name: state.name,
@@ -91,6 +121,7 @@ export function AdminEnterpriseSignaturesPage() {
});
}
setSignatureModal(null);
if (saved.reportMaterialChanged) setMaterialChangedSignature(saved);
await loadData();
} catch (failure) {
setError(failure instanceof Error ? failure.message : '企业签名保存失败');
@@ -154,39 +185,87 @@ export function AdminEnterpriseSignaturesPage() {
<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>
<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} />
<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);
setPage(1);
void loadData(filters, 1);
}}></Button>
<Button onClick={() => {
const filters = { enterpriseKeyword: '', applicationKeyword: '', signatureKeyword: '', drainageKeyword: '' };
setEnterpriseKeyword('');
setApplicationKeyword('');
setSignatureKeyword('');
setDrainageKeyword('');
setAppliedEnterpriseKeyword('');
setAppliedApplicationKeyword('');
setAppliedSignatureKeyword('');
setAppliedDrainageKeyword('');
setPage(1);
void loadData(filters, 1);
}} variant="ghost"></Button>
<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);
setPage(1);
void loadData(filters, 1);
}}
>
</Button>
<Button
onClick={() => {
const filters = {
enterpriseKeyword: '',
applicationKeyword: '',
signatureKeyword: '',
drainageKeyword: '',
};
setEnterpriseKeyword('');
setApplicationKeyword('');
setSignatureKeyword('');
setDrainageKeyword('');
setAppliedEnterpriseKeyword('');
setAppliedApplicationKeyword('');
setAppliedSignatureKeyword('');
setAppliedDrainageKeyword('');
setPage(1);
void loadData(filters, 1);
}}
variant="ghost"
>
</Button>
</div>
</div>
@@ -199,7 +278,12 @@ export function AdminEnterpriseSignaturesPage() {
value={activeTab}
items={[
{ label: '短信签名', value: 'sms', content: smsSignatureContent },
{ label: '彩信签名', pending: true, value: 'mms', content: <div className="ui-table__empty"></div> },
{
label: '彩信签名',
pending: true,
value: 'mms',
content: <div className="ui-table__empty"></div>,
},
]}
/>
</div>
@@ -209,29 +293,87 @@ export function AdminEnterpriseSignaturesPage() {
applications={applications}
item={signatureModal === 'new' ? undefined : signatureModal}
onClose={() => setSignatureModal(null)}
onSubmit={(state) => { void saveSignature(state); }}
onSubmit={(state) => {
void saveSignature(state);
}}
tenants={tenants}
/>
) : null}
{importOpen ? <ReportMaterialImportModal onClose={() => setImportOpen(false)} onCompleted={() => {
setMessage('导入解析完成,合格资料已进入审核中心的导入批次');
void loadData();
}} /> : null}
{reportStatusTarget ? <ChannelReportStatusModal item={reportStatusTarget} onClose={() => setReportStatusTarget(null)} onSaved={() => { setReportStatusTarget(null); void loadData(); }} /> : 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={() => {
setMessage('导入解析完成,合格资料已进入审核中心的导入批次');
void loadData();
}}
/>
) : 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); }}
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}
{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(); }}
onConfirm={() => {
void confirmDelete();
}}
/>
) : null}
</section>