diff --git a/api/src/common/report-status.spec.ts b/api/src/common/report-status.spec.ts index bb23804..c43b861 100644 --- a/api/src/common/report-status.spec.ts +++ b/api/src/common/report-status.spec.ts @@ -4,10 +4,12 @@ describe('summarizeReportStatuses', () => { it.each([ [[], { status: 'not_applicable', approved: 0, total: 0 }], [['approved', 'approved'], { status: 'approved', approved: 2, total: 2 }], + [['abandoned', 'abandoned'], { status: 'abandoned', approved: 0, total: 2 }], [['failed', 'rejected'], { status: 'failed', approved: 0, total: 2 }], [['approved', 'failed'], { status: 'partial_success', approved: 1, total: 2 }], [['failed', 'pending'], { status: 'reporting', approved: 0, total: 2 }], [['waiting_material', 'pending'], { status: 'waiting_material', approved: 0, total: 2 }], + [['abandoned', 'reporting'], { status: 'reporting', approved: 0, total: 2 }], ])('summarizes %j without allowing one failure to override other targets', (statuses, expected) => { expect(summarizeReportStatuses(statuses)).toEqual(expected); }); diff --git a/api/src/common/report-status.ts b/api/src/common/report-status.ts index 0298f9f..ad7881d 100644 --- a/api/src/common/report-status.ts +++ b/api/src/common/report-status.ts @@ -11,8 +11,10 @@ export function summarizeReportStatuses(statuses: string[]): ReportStatusSummary const approved = statuses.filter((status) => status === 'approved').length; const failed = statuses.filter((status) => FAILED_REPORT_STATUSES.has(status)).length; + const abandoned = statuses.filter((status) => status === 'abandoned').length; if (approved === statuses.length) return { status: 'approved', approved, total: statuses.length }; + if (abandoned === statuses.length) return { status: 'abandoned', approved, total: statuses.length }; // Overall failure means every current target failed. A single failed channel must not // erase successful channels or targets that can still finish reporting. diff --git a/docs/contracts/admin-enterprise-signatures-r4.json b/docs/contracts/admin-enterprise-signatures-r4.json index fd1e951..1db2a8b 100644 --- a/docs/contracts/admin-enterprise-signatures-r4.json +++ b/docs/contracts/admin-enterprise-signatures-r4.json @@ -9,7 +9,7 @@ }, { "name": "CarrierReportTag", - "canonicalSha256": "2ee4038f46fdb4c8e9af1fdbc5f7959f38d2360e081c7dd3b44d465be3b6cfef" + "canonicalSha256": "6296b4332ef0669b17a3aa6703e7e407aa44008a9820d6fccff3313b238f1914" }, { "name": "signatureCardVisual", @@ -76,7 +76,7 @@ "canonicalSha256": "2f71bdc2f8044ca9b403affbb0cb3f31587e4c004a90e9e0836aa6143033b6da" } ], - "tableJsxSha256": "9d8b0f90e6a375fb3d019985d2b6d3409d7b41956e1893f0654c80499c1b64b4", + "tableJsxSha256": "3f321d3490479e62db729ebc49d4cc7eef46c24c9eaf5add7a0d046965de3109", "apiCalls": [ "listEnterpriseSignaturesPage", "listTenants", diff --git a/docs/system-functional-test-cases.md b/docs/system-functional-test-cases.md index 8be39cc..4ad8df3 100644 --- a/docs/system-functional-test-cases.md +++ b/docs/system-functional-test-cases.md @@ -4997,8 +4997,9 @@ npm run verify:phase8 | 用例ID | 场景 | 预期 | | --- | --- | --- | | TC-ENTERPRISE-SIGNATURE-DENSITY-001 | 首次打开运营端企业签名管理 | 不请求或展示顶部全部/待审核/报备中/异常统计;短信签名Tab不显示总数;列表使用紧凑表头与行布局 | -| TC-ENTERPRISE-SIGNATURE-DENSITY-002 | 查看签名及展开的引流信息 | 两级表头统一使用“审核状态”;移动/联通/电信仅显示已通过通道数/总通道数,不显示日期;引流信息列只显示条数,不显示异常数 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-002 | 查看签名及展开的引流信息 | 两级表头统一使用“审核状态”;签名及引流信息的移动/联通/电信列同时显示汇总状态及已通过通道数/总通道数,不显示日期;引流信息列只显示条数,不显示异常数 | | TC-ENTERPRISE-SIGNATURE-DENSITY-003 | 查看签名查询和签名列 | 查询标签及表头统一为“签名”,不显示签名用途;签名统一显示完整中文黑括号,例如“【聆界科技】”,已有括号不得重复包裹 | -| TC-ENTERPRISE-SIGNATURE-DENSITY-004 | 点击签名升序或降序按钮并翻页 | 请求携带signatureSort=asc/desc,后端先按签名及ID稳定排序再分页;切换排序回第一页,不只重排当前页,不产生重复请求 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-004 | 点击签名表头旁的向上或向下三角按钮并翻页 | 请求携带signatureSort=asc/desc,后端先按签名及ID稳定排序再分页;切换排序回第一页,不只重排当前页,不产生重复请求 | | TC-ENTERPRISE-SIGNATURE-DENSITY-005 | 查看签名及引流信息操作列 | 两级操作列都只展示“报备状态、编辑、删除”;不显示“报备详情”;三个入口继续调用原真实后端流程 | | TC-ENTERPRISE-SIGNATURE-DENSITY-006 | 1600×1000桌面视口查看并展开首条签名 | 表头与数据列对齐,操作按钮不换行,展开表格无裁切;页面无框架错误层,控制台无相关错误;窄视口由列表容器横向滚动,不挤压错列 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-007 | 某一运营商下所有当前目标通道的报备任务均为abandoned | 该签名及其引流信息在对应运营商列汇总为“放弃报备”,同时显示0/总通道数;仅部分通道放弃时不得误判为全部放弃 | diff --git a/docs/testing-progress.md b/docs/testing-progress.md index 028bf1e..c477e49 100644 --- a/docs/testing-progress.md +++ b/docs/testing-progress.md @@ -4249,3 +4249,12 @@ git diff --check - Browser插件本轮不可用,常规Chrome/Edge自动化进程因本机策略启动即退出;改用工作区自带Playwright Chromium复核本地构建。1600×1000页面身份、非空、无框架错误层、控制台、截图和交互通过:展开首条后可见3条引流信息;两级各3个指定操作、0个报备详情;点击降序实际请求`/api/admin/enterprise-signatures?signatureSort=desc&page=1&pageSize=10`。视觉夹具仅用于渲染验证,不冒充真实API验收。 - 本轮不推送、不部署,不访问或修改测试/预生产业务数据;未发送、补发、重投短信,未修改余额、通道或客户配置。真实登录环境API、浏览器控制台及页面效果仍须在未来获授权发布后复核。 - 提交只暂存本轮源码、测试、R4契约、测试用例及本节进度追加;原有4份修改文档、3份未跟踪文档、未跟踪`=`文件和`docs/testing-progress.md`中另一会话26行追加继续保留,不覆盖、不夹带。 + +## 2026-09-02 企业签名三网状态恢复与放弃报备汇总 + +- 重新核验 `main / 9e34757`、最近提交及交接进度后定位回归:高密度列表将三网单元格收敛为纯 `approved/total` 数字,导致签名和引流信息的状态语义不可见;公共报备汇总函数此前也没有“全部目标均为 abandoned”的独立结果。 +- 签名及展开的引流信息三网列现同时展示汇总状态和通过数/总通道数,覆盖全部通过、部分通过、报备中、资料待补充、报备失败、放弃报备和未报备;状态及数量继续来自真实列表API的汇总字段。只有同一运营商下全部当前目标通道均为 `abandoned` 才汇总为“放弃报备”,混合状态保持其可执行状态,不误判为全部放弃。 +- 签名表头的升序/降序图标由箭头改为上下实心三角,保留按钮无障碍名称、当前排序态及服务端稳定分页排序语义。同步更新R4契约和 `TC-ENTERPRISE-SIGNATURE-DENSITY-002/004/007`。 +- 定向公共汇总8项、企业签名组件2项通过;全量API 51套586项、前端10文件50项通过,前后端TypeScript、定向ESLint(仅保留既有Fast Refresh警告)、Vite构建、依赖安全、结构质量、R4契约、包体积及 `git diff --check` 通过。Vite仍仅有既有Chart分块超过500kB提示,入口gzip 107.39KiB符合250KiB预算。 +- 测试环境 `100.93.204.60` 当前经Tailscale显示离线、最后在线约12小时前,ping及SSH均不可达;因此尚不能重新建立和验证本次独立恢复资产,也未部署、重启服务或变更测试数据。待主机恢复连通后,必须先完成PostgreSQL custom dump、运行目录、环境/systemd/Nginx/原标记/SHA清单及三项可恢复性校验,才能发布。 +- 本轮未访问或修改预生产,未发送、补发或重投短信,未修改余额、通道或客户配置。原有4份修改文档、3份未跟踪文档、未跟踪`=`文件及本文件中其他会话追加保持不动。 diff --git a/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.test.tsx b/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.test.tsx index 13f075d..6f4416f 100644 --- a/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.test.tsx +++ b/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.test.tsx @@ -17,7 +17,7 @@ const signature = { carrierReportSummary: { mobile: { status: 'approved', approved: 3, total: 3 }, unicom: { status: 'reporting', approved: 2, total: 3 }, - telecom: { status: 'waiting_material', approved: 1, total: 3 }, + telecom: { status: 'abandoned', approved: 0, total: 3 }, }, drainageInfo: { links: [{ id: 'drainage-1', siteName: 'www.lisglo.com', url: 'www.lisglo.com', auditStatus: 'approved' }] }, drainageCarrierReportSummary: { @@ -42,15 +42,17 @@ function renderTable(overrides: Partial { - it('uses compact headers, bracketed signatures, channel counts and only the requested row actions', () => { + it('uses compact headers, bracketed signatures, carrier statuses with counts and only the requested row actions', () => { renderTable(); expect(screen.getByText('【聆界科技】')).toBeVisible(); expect(screen.getAllByText('审核状态')).toHaveLength(2); expect(screen.queryByText('签名名称')).not.toBeInTheDocument(); expect(screen.queryByText('签名审核')).not.toBeInTheDocument(); expect(screen.queryByText('报备详情')).not.toBeInTheDocument(); - expect(screen.getAllByLabelText('已报备通过3个,总通道数3个')).toHaveLength(2); - expect(screen.getByLabelText('已报备通过2个,总通道数3个')).toHaveTextContent('2/3'); + expect(screen.getAllByLabelText('全部通过,已报备通过3个,总通道数3个')).toHaveLength(2); + expect(screen.getByLabelText('部分通过,已报备通过2个,总通道数3个')).toHaveTextContent('部分通过2/3'); + expect(screen.getByLabelText('放弃报备,已报备通过0个,总通道数3个')).toHaveTextContent('放弃报备0/3'); + expect(screen.getByLabelText('资料待补充,已报备通过0个,总通道数3个')).toHaveTextContent('资料待补充0/3'); expect(screen.getAllByRole('button', { name: '报备状态' })).toHaveLength(2); expect(screen.getAllByRole('button', { name: '编辑' })).toHaveLength(2); expect(screen.getAllByRole('button', { name: '删除' })).toHaveLength(2); @@ -61,5 +63,8 @@ describe('EnterpriseSignaturesTable dense presentation', () => { renderTable({ expandedSignatureId: '', setSignatureSort }); await userEvent.click(screen.getByRole('button', { name: '签名降序' })); expect(setSignatureSort).toHaveBeenCalledWith('desc'); + expect(screen.getByRole('button', { name: '签名升序' }).querySelector('.lucide-triangle')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '签名降序' }).querySelector('.enterprise-signature-table__sort-triangle--down')).toBeInTheDocument(); + expect(document.querySelector('.lucide-arrow-up, .lucide-arrow-down')).not.toBeInTheDocument(); }); }); diff --git a/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.tsx b/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.tsx index 5720900..a41febc 100644 --- a/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.tsx +++ b/src/apps/admin/enterprise-signatures/EnterpriseSignaturesTable.tsx @@ -1,5 +1,5 @@ import type { Dispatch, SetStateAction } from 'react'; -import { ArrowDown, ArrowUp, ChevronDown, ChevronRight, Edit3, Plus } from 'lucide-react'; +import { ChevronDown, ChevronRight, Edit3, Plus, Triangle } from 'lucide-react'; import type { ClientSmsSignature } from '@/api/adminApi'; import { Button, DeleteRiskAction, Pagination } from '@/components/ui'; import { AuditStatusTag, CarrierReportCount, formatSignatureName, readDrainagePayload, signatureCardVisual } from './signature.helpers'; @@ -51,8 +51,8 @@ export function EnterpriseSignaturesTable({ 签名 - - + + 企业 diff --git a/src/apps/admin/enterprise-signatures/signature.helpers.tsx b/src/apps/admin/enterprise-signatures/signature.helpers.tsx index 75e3e88..54cca59 100644 --- a/src/apps/admin/enterprise-signatures/signature.helpers.tsx +++ b/src/apps/admin/enterprise-signatures/signature.helpers.tsx @@ -16,19 +16,13 @@ export function carrierLabel(carrier?: string | null) { } export function formatSignatureName(name: string) { - const value = name.trim().replace(/^[【\[]+|[】\]]+$/g, '').trim(); + const value = name.trim().replace(/^(?:【|\[)+|(?:】|\])+$/g, '').trim(); return value ? `【${value}】` : '—'; } export function CarrierReportTag({ summary }: { summary?: CarrierReportSummary }) { if (!summary || summary.status === 'not_applicable' || summary.total === 0) return 不适用; - let label = '未报备'; - let tone: 'success' | 'danger' | 'warning' | 'info' | 'neutral' = 'neutral'; - if (summary.status === 'approved') { label = '全部通过'; tone = 'success'; } - else if (summary.status === 'failed' || summary.status === 'rejected') { label = '报备失败'; tone = 'danger'; } - else if (summary.status === 'waiting_material') { label = '资料待补充'; tone = 'warning'; } - else if (summary.approved > 0) { label = '部分通过'; tone = 'info'; } - else if (summary.status === 'reporting' || summary.status === 'exporting') { label = '报备中'; tone = 'warning'; } + const { label, tone } = carrierReportMeta(summary); return {label}({summary.approved}/{summary.total}); } @@ -36,20 +30,31 @@ export function CarrierReportCount({ summary }: { summary?: CarrierReportSummary if (!summary || summary.status === 'not_applicable' || summary.total === 0) { return ; } - const tone = summary.approved === summary.total - ? 'success' - : ['failed', 'rejected'].includes(summary.status) ? 'danger' : 'warning'; + const { label, tone } = carrierReportMeta(summary); return ( - {summary.approved}/{summary.total} + {label} + {summary.approved}/{summary.total} ); } +function carrierReportMeta(summary: CarrierReportSummary) { + let label = '未报备'; + let tone: 'success' | 'danger' | 'warning' | 'info' | 'neutral' = 'neutral'; + if (summary.status === 'approved') { label = '全部通过'; tone = 'success'; } + else if (summary.status === 'abandoned') { label = '放弃报备'; tone = 'neutral'; } + else if (summary.status === 'failed' || summary.status === 'rejected') { label = '报备失败'; tone = 'danger'; } + else if (summary.status === 'waiting_material') { label = '资料待补充'; tone = 'warning'; } + else if (summary.approved > 0) { label = '部分通过'; tone = 'info'; } + else if (summary.status === 'reporting' || summary.status === 'exporting') { label = '报备中'; tone = 'warning'; } + return { label, tone }; +} + export function signatureCardVisual(auditStatus: string, summaries?: Record) { if (auditStatus === 'rejected') return { label: '签名审核已驳回', tone: 'red' as SignatureCardTone }; if (auditStatus === 'pending') return { label: '签名待审核', tone: 'amber' as SignatureCardTone }; diff --git a/src/styles/global.css b/src/styles/global.css index 4197ef3..057e3ae 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -2072,24 +2072,30 @@ .enterprise-signature-table__sortable { gap: 4px; } .enterprise-signature-table__sort-actions { gap: 2px; } .enterprise-signature-table__sort-actions .ui-button { min-height: 26px; min-width: 26px; padding: 4px; } +.enterprise-signature-table__sort-triangle--down { transform: rotate(180deg); } .enterprise-signature-table__signature { min-width: 0; } .enterprise-signature-table__signature strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .carrier-report-count { align-items: center; border-radius: var(--radius-sm); - display: inline-flex; + display: inline-grid; font-size: var(--font-size-xs); font-variant-numeric: tabular-nums; font-weight: var(--font-weight-semibold); + gap: 1px; justify-content: center; - min-width: 40px; - padding: 4px 7px; + justify-items: center; + line-height: 1.2; + min-width: 64px; + padding: 4px 6px; } +.carrier-report-count small { font-size: 11px; font-weight: var(--font-weight-medium); opacity: .82; } .carrier-report-count--success { background: var(--color-success-soft); color: var(--color-success); } .carrier-report-count--warning { background: var(--color-warning-soft); color: var(--color-warning); } .carrier-report-count--danger { background: var(--color-danger-soft); color: var(--color-danger); } +.carrier-report-count--info { background: var(--color-info-soft); color: var(--color-info); } .carrier-report-count--neutral { background: var(--color-surface-subtle); color: var(--color-text-subtle); } .signature-report-status {