feat: support WPS report material workbooks

This commit is contained in:
hectorzhao
2026-09-04 17:10:04 +08:00
parent 48d0363920
commit fb39c8b606
29 changed files with 2737 additions and 734 deletions
+132 -111
View File
@@ -1,20 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import {
BarChart3,
DollarSign,
FileCheck2,
ShieldCheck,
} from 'lucide-react';
import { BarChart3, DollarSign, FileCheck2, ShieldCheck } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import {
Breadcrumb,
Button,
Modal,
MoneyText,
Table,
Tag,
type TableColumn,
} from '@/components/ui';
import { Breadcrumb, Button, Modal, MoneyText, Table, Tag, type TableColumn } from '@/components/ui';
import { Chart } from '@/components/ui/Chart';
import { adminApi, type DashboardResponse, type SendQualityResponse } from '@/api/adminApi';
import { createDualAxisBarLineOption, createLineOption } from '@/theme/chartOptions';
@@ -70,7 +57,11 @@ export function AdminHome() {
enterprise: account.tenantName,
todaySpend,
availableBalance,
balanceStatus: (availableBalance <= 0 ? '欠费' : availableBalance < 100 ? '紧张' : '充足') as EnterpriseSpendRank['balanceStatus'],
balanceStatus: (availableBalance <= 0
? '欠费'
: availableBalance < 100
? '紧张'
: '充足') as EnterpriseSpendRank['balanceStatus'],
};
});
}, [dashboard]);
@@ -83,33 +74,43 @@ export function AdminHome() {
const todayProfit = moneyUnitsToYuan(dashboard?.today.profitCents);
const activeSignatureCount = new Set(quality?.signatures.map((item) => item.signatureId) ?? []).size;
const downstreamAlertCount = dashboard?.downstreamDeliverySummary?.alertCount ?? 0;
const pendingAudits = dashboard?.pendingAudits ?? { enterpriseCertifications: 0, smsAudits: 0, templates: 0, signatures: 0, drainageInfos: 0, total: 0 };
const pendingAudits = dashboard?.pendingAudits ?? {
enterpriseCertifications: 0,
smsAudits: 0,
templates: 0,
signatures: 0,
drainageInfos: 0,
total: 0,
};
const sendTrendOption = useMemo(
() => createLineOption({
labels: dashboard?.hourlySendTrend.map((item) => item.label) ?? [],
series: [
{ name: '提交总条数', data: dashboard?.hourlySendTrend.map((item) => item.submittedCount) ?? [] },
{ name: '成功条数', data: dashboard?.hourlySendTrend.map((item) => item.successCount) ?? [] },
],
}),
() =>
createLineOption({
labels: dashboard?.hourlySendTrend.map((item) => item.label) ?? [],
series: [
{ name: '提交总条数', data: dashboard?.hourlySendTrend.map((item) => item.submittedCount) ?? [] },
{ name: '成功条数', data: dashboard?.hourlySendTrend.map((item) => item.successCount) ?? [] },
],
}),
[dashboard],
);
const auditSpeedOption = useMemo(
() => createDualAxisBarLineOption({
labels: dashboard?.auditProcessingSpeed.map((item) => item.label) ?? [],
bar: {
name: '审核数量',
data: dashboard?.auditProcessingSpeed.map((item) => item.count) ?? [],
},
line: {
name: '平均处理时长(分钟)',
data: dashboard?.auditProcessingSpeed.map((item) => (
item.averageProcessingMs == null ? null : Number((item.averageProcessingMs / 60_000).toFixed(1))
)) ?? [],
},
}),
() =>
createDualAxisBarLineOption({
labels: dashboard?.auditProcessingSpeed.map((item) => item.label) ?? [],
bar: {
name: '审核数量',
data: dashboard?.auditProcessingSpeed.map((item) => item.count) ?? [],
},
line: {
name: '平均处理时长(分钟)',
data:
dashboard?.auditProcessingSpeed.map((item) =>
item.averageProcessingMs == null ? null : Number((item.averageProcessingMs / 60_000).toFixed(1)),
) ?? [],
},
}),
[dashboard],
);
@@ -125,9 +126,23 @@ export function AdminHome() {
</div>
),
},
{ key: 'todaySpend', title: '今日消费(元)', align: 'right', render: (record) => <MoneyText>¥{formatCurrency(record.todaySpend)}</MoneyText> },
{ key: 'availableBalance', title: '可用余额', align: 'right', render: (record) => formatCount(record.availableBalance) },
{ key: 'balanceStatus', title: '余额状态', render: (record) => <Tag tone={balanceTone[record.balanceStatus]}>{record.balanceStatus}</Tag> },
{
key: 'todaySpend',
title: '今日消费(元)',
align: 'right',
render: (record) => <MoneyText>¥{formatCurrency(record.todaySpend)}</MoneyText>,
},
{
key: 'availableBalance',
title: '可用余额',
align: 'right',
render: (record) => formatCount(record.availableBalance),
},
{
key: 'balanceStatus',
title: '余额状态',
render: (record) => <Tag tone={balanceTone[record.balanceStatus]}>{record.balanceStatus}</Tag>,
},
{
key: 'actions',
title: '操作',
@@ -151,9 +166,7 @@ export function AdminHome() {
<Button icon={<FileCheck2 size={16} />} onClick={() => navigate('/admin/templates')} variant="ghost">
</Button>
<Button onClick={() => navigate('/admin/monitor')}>
</Button>
<Button onClick={() => navigate('/admin/monitor')}></Button>
</div>
</div>
@@ -163,15 +176,20 @@ export function AdminHome() {
<strong>{formatCount(totalSend)} </strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{formatCount(dashboard?.today.segmentCount ?? 0)} </strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{averageSuccessRate.toFixed(1)}%</strong>
<small>delivered / </small>
</div>
<div className="surface metric-card">
<span></span>
<strong>¥{formatCurrency(todaySpend)}</strong>
<small></small>
<span></span>
<strong>{(dashboard?.today.arrivalRate ?? 0).toFixed(1)}%</strong>
<small> / </small>
</div>
<div className="surface metric-card">
<span></span>
@@ -179,14 +197,9 @@ export function AdminHome() {
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{formatCount(dashboard?.today.segmentCount ?? 0)} </strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{(dashboard?.today.arrivalRate ?? 0).toFixed(1)}%</strong>
<small> / </small>
<span></span>
<strong>¥{formatCurrency(todaySpend)}</strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
@@ -200,12 +213,16 @@ export function AdminHome() {
</div>
<div className="surface metric-card">
<span></span>
<strong className={todayProfit < 0 ? 'metric-card__value--danger' : undefined}>¥{formatCurrency(todayProfit)}</strong>
<strong className={todayProfit < 0 ? 'metric-card__value--danger' : undefined}>
¥{formatCurrency(todayProfit)}
</strong>
<small> - </small>
</div>
<div className="surface metric-card">
<span></span>
<strong className={(dashboard?.today.profitRate ?? 0) < 0 ? 'metric-card__value--danger' : undefined}>{(dashboard?.today.profitRate ?? 0).toFixed(1)}%</strong>
<strong className={(dashboard?.today.profitRate ?? 0) < 0 ? 'metric-card__value--danger' : undefined}>
{(dashboard?.today.profitRate ?? 0).toFixed(1)}%
</strong>
<small> / </small>
</div>
</div>
@@ -214,12 +231,12 @@ export function AdminHome() {
<div className="chart-grid">
<div className="surface chart-card">
<h2></h2>
<p className="muted"></p>
<p className="muted"></p>
<Chart height={300} option={sendTrendOption} />
</div>
<div className="surface chart-card">
<h2></h2>
<p className="muted"></p>
<p className="muted"></p>
<Chart height={300} option={auditSpeedOption} />
</div>
</div>
@@ -238,73 +255,75 @@ export function AdminHome() {
</div>
<div className="surface section-stack">
<div className="section-heading">
<div className="section-heading">
<div>
<h2></h2>
<p className="muted"></p>
</div>
<BarChart3 size={20} className="status-info" />
</div>
<div className="overview-grid overview-grid--three">
<Button className="mini-status-card" onClick={() => navigate('/admin/enterprise-audit')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.enterpriseCertifications} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/sms-audit')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.smsAudits} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/templates')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.templates} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/signatures')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.signatures} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/drainage-audits')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.drainageInfos} </strong>
</Button>
<div className="mini-status-card">
<ShieldCheck size={22} />
<div>
<h2></h2>
<p className="muted"></p>
</div>
<BarChart3 size={20} className="status-info" />
</div>
<div className="overview-grid overview-grid--three">
<Button className="mini-status-card" onClick={() => navigate('/admin/enterprise-audit')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.enterpriseCertifications} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/sms-audit')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.smsAudits} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/templates')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.templates} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/signatures')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.signatures} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/drainage-audits')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.drainageInfos} </strong>
</Button>
<div className="mini-status-card">
<ShieldCheck size={22} />
<div>
<span></span>
<strong>{dashboard?.taskCount ?? 0} </strong>
<small></small>
</div>
</div>
<div className="mini-status-card">
<ShieldCheck size={22} />
<div>
<span></span>
<strong>{downstreamAlertCount} </strong>
<small></small>
</div>
<span></span>
<strong>{dashboard?.taskCount ?? 0} </strong>
<small></small>
</div>
</div>
<div className="mini-status-card">
<ShieldCheck size={22} />
<div>
<span></span>
<strong>{downstreamAlertCount} </strong>
<small></small>
</div>
</div>
</div>
</div>
<Modal
footer={(
footer={
<>
<Button onClick={() => setSelectedEnterprise(null)} variant="ghost"></Button>
<Button onClick={() => setSelectedEnterprise(null)} variant="ghost">
</Button>
<Button onClick={() => navigate('/admin/recharge-records')}></Button>
</>
)}
}
onClose={() => setSelectedEnterprise(null)}
open={Boolean(selectedEnterprise)}
title={(
title={
<div className="ui-detail-title">
<h2></h2>
<p>{selectedEnterprise?.id}</p>
</div>
)}
}
>
{selectedEnterprise ? (
<div className="ui-detail-info-grid">
@@ -324,7 +343,9 @@ export function AdminHome() {
</div>
<div className="ui-detail-info-grid__item">
<span></span>
<strong><MoneyText>¥{formatCurrency(selectedEnterprise.todaySpend)}</MoneyText></strong>
<strong>
<MoneyText>¥{formatCurrency(selectedEnterprise.todaySpend)}</MoneyText>
</strong>
</div>
<div className="ui-detail-info-grid__item">
<span></span>