完善运营端报备流程与页面标题

This commit is contained in:
hectorzhao
2026-06-30 18:32:18 +08:00
parent b2e42ebd82
commit 47e603d7d3
40 changed files with 917 additions and 83 deletions
+83 -6
View File
@@ -6,6 +6,7 @@ import {
ChevronUp,
Eye,
FileSliders,
FileUp,
GripVertical,
Pencil,
Plus,
@@ -14,7 +15,7 @@ import {
Trash2,
} from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
import { Button, DateRangeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
import type { DateRangeValue } from '@/components/ui';
type ReportStatus = 'success' | 'failed' | 'reporting' | 'unreported' | 'withdrawn' | 'abandoned';
@@ -129,11 +130,34 @@ const drainageFieldPool: ReportField[] = [
{ id: 'authorization', label: '授权证明', type: '文件' },
];
const signatureFieldPool: ReportField[] = [
{ id: 'signatureName', label: '短信签名', type: '文本' },
{ id: 'signatureBasis', label: '签名依据', type: '文本' },
{ id: 'qualificationFile', label: '资质凭证', type: '文件' },
{ id: 'companyName', label: '公司名称', type: '文本' },
{ id: 'creditCode', label: '统一社会信用代码', type: '文本' },
{ id: 'legalName', label: '法人姓名', type: '文本' },
{ id: 'legalId', label: '法人身份证号', type: '文本' },
{ id: 'legalIdFront', label: '法人身份证人像面', type: '图片' },
{ id: 'legalIdBack', label: '法人身份证国徽面', type: '图片' },
{ id: 'managerName', label: '责任人姓名', type: '文本' },
{ id: 'managerPhone', label: '责任人手机号', type: '文本' },
{ id: 'managerId', label: '责任人身份证号', type: '文本' },
{ id: 'authorization', label: '授权委托书', type: '文件' },
];
const initialSelectedDrainageFields: SelectedReportField[] = [
{ id: 'companyName', label: '公司名称', type: '文本', required: false, mapping: '' },
{ id: 'legalId', label: '法人身份证号', type: '文本', required: false, mapping: '' },
];
const initialSelectedSignatureFields: SelectedReportField[] = [
{ id: 'signatureName', label: '短信签名', type: '文本', required: true, mapping: 'sign_name' },
{ id: 'qualificationFile', label: '资质凭证', type: '文件', required: true, mapping: 'license_file' },
{ id: 'companyName', label: '公司名称', type: '文本', required: true, mapping: 'enterprise_name' },
{ id: 'creditCode', label: '统一社会信用代码', type: '文本', required: true, mapping: 'credit_code' },
];
const emptyStats: DeliveryStats = {
successRate: 0,
successCount: 0,
@@ -269,19 +293,25 @@ function SignatureDetailModal({ report, onClose }: { report: SignatureReport; on
function DrainageFieldConfigModal({
fields,
fieldPool,
onChange,
onClose,
title = '配置报备字段',
description = '从字段池中选择字段,并配置是否必填',
}: {
fields: SelectedReportField[];
fieldPool: ReportField[];
onChange: (fields: SelectedReportField[]) => void;
onClose: () => void;
title?: string;
description?: string;
}) {
const [draft, setDraft] = useState(fields);
const [searchText, setSearchText] = useState('');
const availableFields = useMemo(() => drainageFieldPool.filter((field) => (
const availableFields = useMemo(() => fieldPool.filter((field) => (
!draft.some((item) => item.id === field.id)
&& (!searchText || field.label.includes(searchText))
)), [draft, searchText]);
)), [draft, fieldPool, searchText]);
function addField(field: ReportField) {
setDraft((items) => [...items, { ...field, required: false, mapping: '' }]);
@@ -312,7 +342,7 @@ function DrainageFieldConfigModal({
onClose={onClose}
open
size="xl"
title={<div className="channel-field-config-title"><h2></h2><p></p></div>}
title={<div className="channel-field-config-title"><h2>{title}</h2><p>{description}</p></div>}
>
<div className="channel-field-config">
<section className="channel-field-pool">
@@ -366,6 +396,33 @@ function DrainageFieldConfigModal({
);
}
function ReceiptImportModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: () => void }) {
return (
<Modal
footer={<><Button onClick={onClose} variant="ghost"></Button><Button onClick={onSubmit}></Button></>}
onClose={onClose}
open
size="xl"
title={<div className="template-modal-title"><h2></h2><p></p></div>}
>
<div className="report-receipt-modal">
<div className="report-upload-drop">
<FileUp size={38} />
<strong></strong>
<span> ExcelCSV</span>
</div>
<div className="report-receipt-preview">
<h3></h3>
<div><span></span><strong>12 </strong></div>
<div><span></span><strong>5 </strong></div>
<div><span></span><strong>2 </strong></div>
</div>
<Textarea label="导入备注" placeholder="记录运营商工单号、回执来源或人工说明" rows={4} />
</div>
</Modal>
);
}
export function AdminChannelReportPage() {
const navigate = useNavigate();
const { channelId = '88827' } = useParams();
@@ -380,7 +437,10 @@ export function AdminChannelReportPage() {
const [nextRemark, setNextRemark] = useState('');
const [detail, setDetail] = useState<ReportDetail | null>(null);
const [fieldConfigOpen, setFieldConfigOpen] = useState(false);
const [signatureFieldConfigOpen, setSignatureFieldConfigOpen] = useState(false);
const [receiptOpen, setReceiptOpen] = useState(false);
const [drainageFields, setDrainageFields] = useState(initialSelectedDrainageFields);
const [signatureFields, setSignatureFields] = useState(initialSelectedSignatureFields);
const filteredReports = useMemo(() => reports.filter((report) => {
const matchesKeyword = !keyword || report.name.includes(keyword) || report.drainage.some((item) => item.value.includes(keyword));
@@ -433,14 +493,15 @@ export function AdminChannelReportPage() {
return (
<section className="page-stack channel-report-page">
<div className="surface channel-report-hero">
<div className="breadcrumb-line"><span></span><span>/</span><strong></strong></div>
<Breadcrumb items={[channelNames[channelId] ?? `短信通道 ${channelId}`]} />
<div className="channel-report-heading">
<Button icon={<ChevronLeft size={16} />} onClick={() => navigate('/admin/channels')} variant="ghost"></Button>
<h1>{channelNames[channelId] ?? `短信通道 ${channelId}`}</h1>
<Button icon={<ChevronRight size={16} />} variant="ghost"></Button>
<div className="channel-report-config-actions">
<Button icon={<FileUp size={16} />} onClick={() => setReceiptOpen(true)} variant="secondary"></Button>
<Button icon={<Settings2 size={16} />} onClick={() => setFieldConfigOpen(true)} variant="ghost"></Button>
<Button icon={<FileSliders size={16} />} variant="ghost"></Button>
<Button icon={<FileSliders size={16} />} onClick={() => setSignatureFieldConfigOpen(true)} variant="ghost"></Button>
</div>
</div>
</div>
@@ -543,12 +604,28 @@ export function AdminChannelReportPage() {
{fieldConfigOpen ? (
<DrainageFieldConfigModal
description="这些字段会驱动客户端引流资料补充,并映射到当前通道的导出模板。"
fieldPool={drainageFieldPool}
fields={drainageFields}
onChange={setDrainageFields}
onClose={() => setFieldConfigOpen(false)}
title="个性化引流信息报备字段"
/>
) : null}
{signatureFieldConfigOpen ? (
<DrainageFieldConfigModal
description="这些字段会驱动客户端签名资料补充,并映射到当前通道的签名报备导出模板。"
fieldPool={signatureFieldPool}
fields={signatureFields}
onChange={setSignatureFields}
onClose={() => setSignatureFieldConfigOpen(false)}
title="个性化签名报备字段"
/>
) : null}
{receiptOpen ? <ReceiptImportModal onClose={() => setReceiptOpen(false)} onSubmit={() => setReceiptOpen(false)} /> : null}
{detail?.kind === 'drainage' ? (
<Modal footer={<Button onClick={() => setDetail(null)} variant="ghost"></Button>} onClose={() => setDetail(null)} open size="md" title="引流信息报备详情">
<div className="channel-report-detail">