fix: 补齐签名导入待审通知并展示通道组成本

This commit is contained in:
hectorzhao
2026-09-05 21:22:18 +08:00
parent 15a1f9d8ed
commit 839dba8d9b
13 changed files with 103 additions and 6 deletions
@@ -43,6 +43,7 @@ function createPrismaMock() {
rechargeOrder: { rechargeOrder: {
findMany: jest.fn().mockResolvedValue([{ id: 'order-1', tenantId: 'tenant-1', amountCents: 1000 }]), findMany: jest.fn().mockResolvedValue([{ id: 'order-1', tenantId: 'tenant-1', amountCents: 1000 }]),
}, },
reportMaterialImportItem: { count: jest.fn().mockResolvedValue(0) },
smsTemplate: { smsTemplate: {
count: jest.fn().mockResolvedValue(1), count: jest.fn().mockResolvedValue(1),
}, },
@@ -542,6 +543,7 @@ describe('OperationsService', () => {
enterpriseCertifications: 1, enterpriseCertifications: 1,
smsAudits: 2, smsAudits: 2,
signatures: 1, signatures: 1,
signatureImports: 0,
drainageInfos: 0, drainageInfos: 0,
templates: 1, templates: 1,
total: 5, total: 5,
@@ -729,6 +731,7 @@ describe('OperationsService', () => {
templates: 1, templates: 1,
signatures: 1, signatures: 1,
drainageInfos: 0, drainageInfos: 0,
signatureImports: 0,
total: 5, total: 5,
}); });
expect(prisma.smsTemplate.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', auditStatus: 'pending' } }); expect(prisma.smsTemplate.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', auditStatus: 'pending' } });
@@ -740,6 +743,16 @@ describe('OperationsService', () => {
expect(prisma.$queryRaw).not.toHaveBeenCalled(); expect(prisma.$queryRaw).not.toHaveBeenCalled();
}); });
it('counts pending signature import rows within the requested tenant', async () => {
const prisma = createPrismaMock();
prisma.reportMaterialImportItem.count.mockResolvedValue(19);
const service = new OperationsService(prisma as never);
await expect(service.pendingAudits('tenant-1')).resolves.toMatchObject({ signatureImports: 19, total: 24 });
expect(prisma.reportMaterialImportItem.count).toHaveBeenCalledWith({
where: { reportType: 'signature', status: 'pending_review', batch: { tenantId: 'tenant-1' } },
});
});
it('rejects invalid send quality dates', async () => { it('rejects invalid send quality dates', async () => {
const service = new OperationsService(createPrismaMock() as never); const service = new OperationsService(createPrismaMock() as never);
await expect(service.sendQuality('2026-02-31')).rejects.toThrow('统计日期无效'); await expect(service.sendQuality('2026-02-31')).rejects.toThrow('统计日期无效');
@@ -439,13 +439,17 @@ pendingAudits(tenantId?: string) {
this.prisma.smsDrainageInfo.count({ where: { tenantId, auditStatus: 'pending' } }), this.prisma.smsDrainageInfo.count({ where: { tenantId, auditStatus: 'pending' } }),
this.prisma.enterpriseCertification.count({ where: { tenantId, status: 'pending' } }), this.prisma.enterpriseCertification.count({ where: { tenantId, status: 'pending' } }),
this.prisma.smsSendTask.count({ where: { tenantId, status: 'pending_review' } }), this.prisma.smsSendTask.count({ where: { tenantId, status: 'pending_review' } }),
]).then(([templates, signatures, drainageInfos, enterpriseCertifications, smsAudits]) => ({ this.prisma.reportMaterialImportItem.count({
where: { reportType: 'signature', status: 'pending_review', batch: { tenantId } },
}),
]).then(([templates, signatures, drainageInfos, enterpriseCertifications, smsAudits, signatureImports]) => ({
templates, templates,
signatures, signatures,
drainageInfos, drainageInfos,
enterpriseCertifications, enterpriseCertifications,
smsAudits, smsAudits,
total: templates + signatures + drainageInfos + enterpriseCertifications + smsAudits, signatureImports,
total: templates + signatures + drainageInfos + enterpriseCertifications + smsAudits + signatureImports,
})); }));
} }
} }
@@ -2186,3 +2186,8 @@
- 引流信息的业务主字段统一为“引流 URL 或号码”,支持带 `http/https` 协议或不带协议的域名/URL、手机号码和固定电话号码。客户端新增/修改弹窗不再显示或要求“名称”,原“访问地址”统一改名为“引流 URL 或号码”。 - 引流信息的业务主字段统一为“引流 URL 或号码”,支持带 `http/https` 协议或不带协议的域名/URL、手机号码和固定电话号码。客户端新增/修改弹窗不再显示或要求“名称”,原“访问地址”统一改名为“引流 URL 或号码”。
- API 和服务端必须真实接受上述三种内容并拒绝无效任意文本。数据库既有 `SmsDrainageInfo.siteName` 仅作为兼容列保留,新建或修改时由后端同步写入规范化后的 URL/号码;客户端不得继续提交或依赖独立名称。 - API 和服务端必须真实接受上述三种内容并拒绝无效任意文本。数据库既有 `SmsDrainageInfo.siteName` 仅作为兼容列保留,新建或修改时由后端同步写入规范化后的 URL/号码;客户端不得继续提交或依赖独立名称。
- 运营端单条审核列表、审核详情、导入审核、报备任务和报备记录统一只展示 URL/号码,不再把兼容列作为独立名称。批量导入映射和官方模板去掉“站点名称”必填列,只要求所属短信签名及“引流 URL 或号码”。 - 运营端单条审核列表、审核详情、导入审核、报备任务和报备记录统一只展示 URL/号码,不再把兼容列作为独立名称。批量导入映射和官方模板去掉“站点名称”必填列,只要求所属短信签名及“引流 URL 或号码”。
### 2026-09-05 签名导入待审通知与通道成本展示
- 运营端右上角新增“签名导入待审”,按签名导入资料中 pending_review 的明细条数计入待审核总数,独立于正式签名待审;点击进入签名审核的导入页签。统计遵守批次tenantId范围,invalid/approved/rejected明细不计入;导入提交及审核成功后立即刷新,不预先改计数。
- 短信通道组新建/编辑的省网、全国列表展示成本价格(元/条),使用通道现有unitPrice及10000单位/元换算,保留4位小数;零成本显示0.0000,缺失通道/价格显示“—”。本次不改变通道选择、路由优先级和保存行为。
+10
View File
@@ -5093,3 +5093,13 @@ npm run verify:phase8
| TC-REPORT-MATERIAL-DETAIL-001 | 查看包含当前图片字段、未删除历史字段及旧图片引用的报备资料 | 当前字段展示名称、代码、导出名及图片预览;未删除历史字段继续展示且同时显示字段名称和代码;图片可内联查看并保留下载入口,缺失内容显示明确占位 | | TC-REPORT-MATERIAL-DETAIL-001 | 查看包含当前图片字段、未删除历史字段及旧图片引用的报备资料 | 当前字段展示名称、代码、导出名及图片预览;未删除历史字段继续展示且同时显示字段名称和代码;图片可内联查看并保留下载入口,缺失内容显示明确占位 |
| TC-REPORT-CHANNEL-IDENTITY-001 | 打开短信通道管理的报备详情 | 页面同时明确展示“通道名称”和“通道编号”,列表、筛选、状态修改和窄屏布局不受影响 | | TC-REPORT-CHANNEL-IDENTITY-001 | 打开短信通道管理的报备详情 | 页面同时明确展示“通道名称”和“通道编号”,列表、筛选、状态修改和窄屏布局不受影响 |
| TC-DASHBOARD-METRIC-ORDER-001 | 打开运营看板并按从左到右、从上到下读取指标 | 顺序为发送总量、消息分片数、总体成功率、到达率、活跃签名、消费金额、返还金额、计收金额、利润、利润率;所有数值继续来自真实API口径 | | TC-DASHBOARD-METRIC-ORDER-001 | 打开运营看板并按从左到右、从上到下读取指标 | 顺序为发送总量、消息分片数、总体成功率、到达率、活跃签名、消费金额、返还金额、计收金额、利润、利润率;所有数值继续来自真实API口径 |
## 2026-09-05 签名导入通知与通道组成本回归
| 编号 | 操作 | 预期 |
| --- | --- | --- |
| TC-IMPORT-NOTICE-001 | 提交签名批量导入,打开右上角待审核任务并点击签名导入待审 | 导入成功后即时重新请求真实统计;pending_review明细逐条计数,跳转签名审核tab=import;失败不发送成功刷新事件 |
| TC-IMPORT-NOTICE-002 | 部分通过/驳回导入资料,并按租户查询统计 | 完成明细不计数,剩余待审保留,成功审核即时刷新;租户隔离,空租户结果0,不把无效行算作任务 |
| TC-CHANNEL-COST-001 | 新建/编辑通道组,查看省网和全国列表,分别使用非零、零及缺失价格 | 成本价格按现有单位换算为元/条并保留4位;零显示0.0000,缺失显示—;三尺寸表格内部滚动,无整页溢出;不修改价格和路由 |
上述通知口径增加签名导入这一类审核任务,替代TC-ADMIN-NOTICE-0715-09中“仅五类”的数量限定;下游投递告警仍不计入待审核任务。
+14
View File
@@ -4550,3 +4550,17 @@ git diff --check
- 发布后12项服务ActiveState/MainPID/NRestarts与发布前完全一致;API、独立Callback、Gateway及MinIO健康正常。PostgreSQL只读查询确认96项已完成migration、未完成0项;三条短信Stream组状态逐字一致,pending/lag均为0。发布窗口warning及以上journal为0行,相对恢复归档日志末尾的新增应用错误匹配为0;所有既有dist文件摘要校验通过。 - 发布后12项服务ActiveState/MainPID/NRestarts与发布前完全一致;API、独立Callback、Gateway及MinIO健康正常。PostgreSQL只读查询确认96项已完成migration、未完成0项;三条短信Stream组状态逐字一致,pending/lag均为0。发布窗口warning及以上journal为0行,相对恢复归档日志末尾的新增应用错误匹配为0;所有既有dist文件摘要校验通过。
- 工作站从真实HTTP入口回读index.html、主CSS和主JSCSS/JS与本地构建字节摘要一致;index.html与服务器候选字节一致,相对本机构建仅有CR字符和末尾空白差异,去除CR并裁剪末尾空白后全文相同。发布后Playwright/Edge再次检查两端登录页三尺寸的首次进入、刷新和跨入口导航,共6个状态均非空、无横向溢出、无控制台/页面异常和失败HTTP响应。仅做未登录入口回归,未创建账号或绕过验证码,登录后业务交互仍未重跑;未以此冒充完整业务验收。 - 工作站从真实HTTP入口回读index.html、主CSS和主JSCSS/JS与本地构建字节摘要一致;index.html与服务器候选字节一致,相对本机构建仅有CR字符和末尾空白差异,去除CR并裁剪末尾空白后全文相同。发布后Playwright/Edge再次检查两端登录页三尺寸的首次进入、刷新和跨入口导航,共6个状态均非空、无横向溢出、无控制台/页面异常和失败HTTP响应。仅做未登录入口回归,未创建账号或绕过验证码,登录后业务交互仍未重跑;未以此冒充完整业务验收。
- 发布证据包含恢复点中的`release-manifest-verified.json`、候选及实际门禁日志、完整资源摘要、服务/Stream前后快照及journal,以及本机临时目录中的`browser-smoke.json`、6张截图和`http-after.json`。本段验收文档单独本地提交并同步测试机;不推送远端,不发送、补发、重投或重新入队短信,不修改余额、通道或客户配置。 - 发布证据包含恢复点中的`release-manifest-verified.json`、候选及实际门禁日志、完整资源摘要、服务/Stream前后快照及journal,以及本机临时目录中的`browser-smoke.json`、6张截图和`http-after.json`。本段验收文档单独本地提交并同步测试机;不推送远端,不发送、补发、重投或重新入队短信,不修改余额、通道或客户配置。
## 2026-09-05 签名导入待审通知与通道组成本(本地提交)
- 当前main基于15a1f9d,已有文档改动和未跟踪方案保留。本次仅授权修改并本地提交,不推送、不部署。需求见first-version-development-requirements.md末节,用例TC-IMPORT-NOTICE-001/002、TC-CHANNEL-COST-001。
- 根因:导入资料先进入ReportMaterialImportItem,原pendingAudits仅统计正式签名等5类表,漏计导入待审;提交导入和审核完成未触发顶部刷新。真实测试库signature pending_review=19、approved=20、invalid=27、rejected=19,正式签名pending=0,解释原通知缺失。
- 修复:新增signatureImports可兼容旧响应的计数字段,按reportType=signature/status=pending_review并经batch.tenantId限定计数,计入total;右上角新增“签名导入待审”直达/admin/signatures?tab=import。导入及审核成功时触发既有刷新事件;不改正式审核流程、不重复创建任务。通道组省网/全国列表展示unitPrice/10000、四位小数成本,缺失显示—,零显示0.0000;无CSS、数据库结构、队列、路由或价格写入改动。
- 自动验证:前端13套65项通过,补充导入成功刷新事件断言后该2项定向测试通过;API全量55套执行,新增测试后619项通过、1项因既有dashboard响应断言未包含新字段而失败,更新该断言后operations定向32项全部通过(累计620项通过)。前端TypeScript/Vite和API构建通过;Stylelint、CSS治理15项通过。Chart既有包体告警仍在。
- 质量门禁:format:check和lint已执行,未宣称全绿。HEAD与本轮逐文件ESLint对照确认dashboard.queries.ts原有39条unused等错误、通道组表单原有1条set-state-in-effect错误,另有2条既有依赖警告,本轮相同。Prettier基线证明API查询、API测试、通道组表单3文件原已不通过;AdminLayout新增格式已修正。为保持最小范围未夹带整文件格式化或历史Hooks重构。git diff --check通过。
- 真实验证:从候选构建提取实际pendingAudits函数,在测试机用真实Prisma/PostgreSQL只读执行;全平台和所选QA租户signatureImports=19/total=21,不存在租户全0。没有替换服务器应用文件或启动新业务服务。
- 浏览器:本地生产预览http://127.0.0.1:4173代理测试环境真实API,正常算术验证码登录临时运营账号。1600×1000、1366×768、390×844进入及刷新通道组编辑页,两个成本列存在、真实零成本显示0.0000、无整页溢出;右上角新入口在三尺寸均进入导入审核页签;pageerror和console error均0。Browser技能不可用,按前端测试技能用Playwright/Edge。证据在本机临时目录cmpp-notice-cost-qa/browser.json、截图和lint-baseline.json。
- 验收边界:测试机API仍是15a1f9d,故候选页面上的新计数按旧响应兼容显示0;新统计用真实数据库函数单独验证,未把分段证据冒充新版本端到端部署验收。未执行新的文件上传/审核写入、通道组保存、非零/缺失价格真实夹具及失败注入;不发送短信,不改余额或通道配置。临时用户精确删除后0残留,logout后无剩余Redis会话,本机凭据/storageState及服务器凭据已清理。
- 后续交互建议(未实施):现有历史优先级10/20与编辑下拉1—5不兼容;同优先级/同省份会过滤原行。建议先修正历史值回显和冲突处理,再加入全国路由上下移动/拖拽、明确顺序预览、可搜索通道选择及未保存提示;与本次成本展示分开实施。
- 提交前补充:签名审核页页签由URL直接派生,修正已停留在该页时通知只改query而未切换页签的问题;新增同路由query切换回归测试通过。该文件另有HEAD已存在的1条preserve-manual-memoization错误和1条依赖警告,未扩展整改。
- 最终前端全量14套66项通过,最终TypeScript/Vite生产构建通过;本轮共13个目标文件精确暂存,已有文档段落保持未暂存。
+1
View File
@@ -128,6 +128,7 @@ export type UserPayload = {
}; };
export type PendingAuditCounts = { export type PendingAuditCounts = {
signatureImports?: number;
enterpriseCertifications: number; enterpriseCertifications: number;
smsAudits: number; smsAudits: number;
templates: number; templates: number;
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { CheckCircle2, Info, Pencil, Plus, RadioTower, Trash2 } from 'lucide-react'; import { CheckCircle2, Info, Pencil, Plus, RadioTower, Trash2 } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { adminApi, type AdminChannel, type ChannelGroup } from '@/api/adminApi'; import { adminApi, type AdminChannel, type ChannelGroup } from '@/api/adminApi';
import { formatRateAmount, MONEY_UNITS_PER_YUAN } from '@/utils/currency';
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui'; import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
type Carrier = 'mobile' | 'unicom' | 'telecom'; type Carrier = 'mobile' | 'unicom' | 'telecom';
@@ -227,6 +228,15 @@ export function AdminChannelGroupFormPage() {
width: '280px', width: '280px',
render: (route) => channelById.get(route.channelId)?.name ?? '未命名通道', render: (route) => channelById.get(route.channelId)?.name ?? '未命名通道',
}, },
{
key: 'cost',
title: '成本价格(元/条)',
width: '180px',
render: (route) => {
const price = channelById.get(route.channelId)?.unitPrice;
return price == null ? '—' : formatRateAmount(price / MONEY_UNITS_PER_YUAN);
},
},
{ {
key: 'status', key: 'status',
title: '通道状态', title: '通道状态',
@@ -253,6 +263,15 @@ export function AdminChannelGroupFormPage() {
width: '280px', width: '280px',
render: (route) => channelById.get(route.channelId)?.name ?? '未命名通道', render: (route) => channelById.get(route.channelId)?.name ?? '未命名通道',
}, },
{
key: 'cost',
title: '成本价格(元/条)',
width: '180px',
render: (route) => {
const price = channelById.get(route.channelId)?.unitPrice;
return price == null ? '—' : formatRateAmount(price / MONEY_UNITS_PER_YUAN);
},
},
{ {
key: 'status', key: 'status',
title: '通道状态', title: '通道状态',
@@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Link } from 'react-router-dom';
import { expect, it, vi } from 'vitest';
import { AdminSignatureAuditPage } from './AdminSignatureAuditPage';
vi.mock('@/api/adminApi', () => ({ adminApi: { listEnterpriseSignatures: vi.fn().mockResolvedValue([]) } }));
vi.mock('./ReportImportAuditPanel', () => ({ ReportImportAuditPanel: () => <div></div> }));
it('opens import reviews when the notification changes query parameters on the current audit page', async () => {
const user = userEvent.setup();
render(
<MemoryRouter initialEntries={['/admin/signatures']}>
<Link to="/admin/signatures?tab=import"></Link>
<AdminSignatureAuditPage />
</MemoryRouter>,
);
expect(screen.queryByText('导入审核面板内容')).not.toBeInTheDocument();
await user.click(screen.getByRole('link', { name: '签名导入待审入口' }));
expect(await screen.findByText('导入审核面板内容')).toBeVisible();
});
+1 -2
View File
@@ -161,7 +161,7 @@ function SignatureDetail({ item, onClose }: { item: ClientSmsSignature; onClose:
export function AdminSignatureAuditPage() { export function AdminSignatureAuditPage() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const requestedImportBatchId = searchParams.get('batchId') ?? undefined; const requestedImportBatchId = searchParams.get('batchId') ?? undefined;
const [activeTab, setActiveTab] = useState(searchParams.get('tab') === 'import' ? 'import' : 'single'); const activeTab = searchParams.get('tab') === 'import' ? 'import' : 'single';
const [items, setItems] = useState<ClientSmsSignature[]>([]); const [items, setItems] = useState<ClientSmsSignature[]>([]);
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const [status, setStatus] = useState('pending'); const [status, setStatus] = useState('pending');
@@ -269,7 +269,6 @@ export function AdminSignatureAuditPage() {
{error ? <p className="form-error">{error}</p> : null} {error ? <p className="form-error">{error}</p> : null}
<Tabs <Tabs
onChange={(value) => { onChange={(value) => {
setActiveTab(value);
setSearchParams(value === 'import' ? { tab: 'import' } : {}); setSearchParams(value === 'import' ? { tab: 'import' } : {});
}} }}
value={activeTab} value={activeTab}
@@ -104,6 +104,7 @@ export function ReportImportAuditPanel({
setError(''); setError('');
try { try {
await adminApi.reviewReportImportItems(detail.id, { decision, itemIds, reason: reason.trim() || undefined }); await adminApi.reviewReportImportItems(detail.id, { decision, itemIds, reason: reason.trim() || undefined });
window.dispatchEvent(new Event('cmpp-audit-count-refresh'));
setSelected(new Set()); setSelected(new Set());
setReason(''); setReason('');
setRejectOpen(false); setRejectOpen(false);
@@ -118,6 +118,8 @@ describe('ReportMaterialImportModal mapping profile action', () => {
}); });
const user = userEvent.setup(); const user = userEvent.setup();
const onCompleted = vi.fn(); const onCompleted = vi.fn();
const refresh = vi.fn();
window.addEventListener('cmpp-audit-count-refresh', refresh);
render(<ReportMaterialImportModal onClose={vi.fn()} onCompleted={onCompleted} />); render(<ReportMaterialImportModal onClose={vi.fn()} onCompleted={onCompleted} />);
await waitFor(() => expect(adminApi.listTenantOptions).toHaveBeenCalledTimes(1)); await waitFor(() => expect(adminApi.listTenantOptions).toHaveBeenCalledTimes(1));
await user.click(screen.getByText('所属企业').closest('label')!.querySelector('button')!); await user.click(screen.getByText('所属企业').closest('label')!.querySelector('button')!);
@@ -146,5 +148,7 @@ describe('ReportMaterialImportModal mapping profile action', () => {
), ),
); );
expect(onCompleted).toHaveBeenCalledWith(expect.objectContaining({ id: 'analysis-1', status: 'pending_review' })); expect(onCompleted).toHaveBeenCalledWith(expect.objectContaining({ id: 'analysis-1', status: 'pending_review' }));
expect(refresh).toHaveBeenCalledTimes(1);
window.removeEventListener('cmpp-audit-count-refresh', refresh);
}, 10_000); }, 10_000);
}); });
@@ -270,6 +270,7 @@ export function ReportMaterialImportModal({
} }
: undefined, : undefined,
}); });
window.dispatchEvent(new Event('cmpp-audit-count-refresh'));
onCompleted(result); onCompleted(result);
onClose(); onClose();
} catch (failure) { } catch (failure) {
+7 -2
View File
@@ -30,12 +30,12 @@ import {
Users, Users,
UserX, UserX,
} from 'lucide-react'; } from 'lucide-react';
import { adminApi } from '@/api/adminApi'; import { adminApi, type PendingAuditCounts } from '@/api/adminApi';
import { getLastUserActivityAt, readSession, type LoginSession } from '@/api/session'; import { getLastUserActivityAt, readSession, type LoginSession } from '@/api/session';
import { AppShell } from '@/layouts/AppShell'; import { AppShell } from '@/layouts/AppShell';
import { PortalSessionBoundary } from '@/layouts/PortalSessionBoundary'; import { PortalSessionBoundary } from '@/layouts/PortalSessionBoundary';
const EMPTY_PENDING_AUDITS = { const EMPTY_PENDING_AUDITS: Omit<PendingAuditCounts, 'total'> = {
enterpriseCertifications: 0, enterpriseCertifications: 0,
smsAudits: 0, smsAudits: 0,
templates: 0, templates: 0,
@@ -153,6 +153,11 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' }, { label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
{ label: '模板待审', count: pendingAudits.templates, to: '/admin/templates' }, { label: '模板待审', count: pendingAudits.templates, to: '/admin/templates' },
{ label: '签名待审', count: pendingAudits.signatures, to: '/admin/signatures' }, { label: '签名待审', count: pendingAudits.signatures, to: '/admin/signatures' },
{
label: '签名导入待审',
count: pendingAudits.signatureImports ?? 0,
to: '/admin/signatures?tab=import',
},
{ label: '引流信息待审', count: pendingAudits.drainageInfos, to: '/admin/drainage-audits' }, { label: '引流信息待审', count: pendingAudits.drainageInfos, to: '/admin/drainage-audits' },
]} ]}
navSections={[ navSections={[