diff --git a/api/src/sms-config/admin-sms-config.controller.ts b/api/src/sms-config/admin-sms-config.controller.ts index a26b28d..e99e902 100644 --- a/api/src/sms-config/admin-sms-config.controller.ts +++ b/api/src/sms-config/admin-sms-config.controller.ts @@ -72,8 +72,8 @@ export class AdminSmsConfigController { } @Get('enterprise-signatures') - listSignatures(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('status') status?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('signatureKeyword') signatureKeyword?: string, @Query('drainageKeyword') drainageKeyword?: string, @Query('submittedAtFrom') submittedAtFrom?: string, @Query('submittedAtTo') submittedAtTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { - const query = { tenantId, keyword, status, enterpriseKeyword, applicationKeyword, signatureKeyword, drainageKeyword, submittedAtFrom, submittedAtTo, page: Number(page), pageSize: Number(pageSize) }; + listSignatures(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('status') status?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('signatureKeyword') signatureKeyword?: string, @Query('drainageKeyword') drainageKeyword?: string, @Query('signatureSort') signatureSort?: 'asc' | 'desc', @Query('submittedAtFrom') submittedAtFrom?: string, @Query('submittedAtTo') submittedAtTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { + const query = { tenantId, keyword, status, enterpriseKeyword, applicationKeyword, signatureKeyword, drainageKeyword, signatureSort, submittedAtFrom, submittedAtTo, page: Number(page), pageSize: Number(pageSize) }; return page || pageSize ? this.smsConfig.listSignaturesPage(query) : this.smsConfig.listSignatures(query); } diff --git a/api/src/sms-config/signature.service.ts b/api/src/sms-config/signature.service.ts index c8a498d..46015fc 100644 --- a/api/src/sms-config/signature.service.ts +++ b/api/src/sms-config/signature.service.ts @@ -18,6 +18,7 @@ export class SmsSignatureService { constructor(private readonly prisma: PrismaService, private readonly reportValidation: SmsReportValidationService, private readonly audit: SmsAuditService) {} async listSignatures(queryOrTenantId?: string | SignatureListQuery) { const query = typeof queryOrTenantId === 'string' ? { tenantId: queryOrTenantId } : queryOrTenantId ?? {}; + const signatureSort = query.signatureSort === 'asc' || query.signatureSort === 'desc' ? query.signatureSort : undefined; const signatures = await this.prisma.smsSignature.findMany({ where: { tenantId: query.tenantId, @@ -50,7 +51,9 @@ export class SmsSignatureService { drainageItems: { where: { auditStatus: { not: 'deleted' } }, orderBy: { updatedAt: 'desc' } }, reportTasks: { include: { channel: true, drainageInfo: true } }, }, - orderBy: { createdAt: 'desc' }, + orderBy: signatureSort + ? [{ name: signatureSort }, { id: 'asc' }] + : { createdAt: 'desc' }, ...(query.page && query.pageSize ? { skip: (query.page - 1) * query.pageSize, take: query.pageSize, diff --git a/api/src/sms-config/sms-config.contracts.ts b/api/src/sms-config/sms-config.contracts.ts index 52c0677..7174686 100644 --- a/api/src/sms-config/sms-config.contracts.ts +++ b/api/src/sms-config/sms-config.contracts.ts @@ -143,6 +143,7 @@ export interface SignatureListQuery { applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string; + signatureSort?: 'asc' | 'desc'; submittedAtFrom?: string; submittedAtTo?: string; page?: number; diff --git a/api/src/sms-config/sms-config.service.spec.ts b/api/src/sms-config/sms-config.service.spec.ts index 6e8dd9e..3bebc84 100644 --- a/api/src/sms-config/sms-config.service.spec.ts +++ b/api/src/sms-config/sms-config.service.spec.ts @@ -781,7 +781,7 @@ describe('SmsConfigService', () => { const prisma = createPrismaMock(); const service = new SmsConfigService(prisma as never); - await expect(service.listSignatures({ enterpriseKeyword: '租户', applicationKeyword: '应用', signatureKeyword: '签名', drainageKeyword: '官网' })).resolves.toEqual([ + await expect(service.listSignatures({ enterpriseKeyword: '租户', applicationKeyword: '应用', signatureKeyword: '签名', drainageKeyword: '官网', signatureSort: 'asc' })).resolves.toEqual([ expect.objectContaining({ id: 'sig-1', tenant: expect.objectContaining({ name: '租户A' }), @@ -803,6 +803,7 @@ describe('SmsConfigService', () => { drainageItems: { where: { auditStatus: { not: 'deleted' } }, orderBy: { updatedAt: 'desc' } }, reportTasks: { include: { channel: true, drainageInfo: true } }, }, + orderBy: [{ name: 'asc' }, { id: 'asc' }], })); }); diff --git a/docs/contracts/admin-enterprise-signatures-r4.json b/docs/contracts/admin-enterprise-signatures-r4.json index ea90841..fd1e951 100644 --- a/docs/contracts/admin-enterprise-signatures-r4.json +++ b/docs/contracts/admin-enterprise-signatures-r4.json @@ -57,24 +57,16 @@ }, { "name": "SignatureFormModal", - "canonicalSha256": "664e1cd87309c64c03f9198543171bb519d448eec4a83cf890444fee16d6dd66" + "canonicalSha256": "1768695d72a0953d6d0108ee4e67c3a321e4869de37a817dabeee8c3b59859ca" }, { "name": "DrainageFormModal", - "canonicalSha256": "0e63725f10e8021e0a7beb5a71f52773ce271faea137735d3fc184f07a39c6d6" - }, - { - "name": "SignatureReportModal", - "canonicalSha256": "4cfec954f564c998af70da441971bede16915f4bb5ac9c10e45da15101957de5" + "canonicalSha256": "30daa4dfc5c8dc83f17ef901143f61490d13d081aa8b30e4677e12f814025942" }, { "name": "ChannelReportStatusModal", "canonicalSha256": "e0717a89302e477fd1e57f18e3f97b993ebe3c7c42ccbfcd2761110054f6abcb" }, - { - "name": "DrainageReportModal", - "canonicalSha256": "d2b41c26eb3992bb1ff3b50ee5ef6c06e7f84d89832e3ec7446b35178520431f" - }, { "name": "DrainageReportStatusModal", "canonicalSha256": "ef692f08d2140e82e40fc12f716b15a902e3c66948140e8512f5a317c629215c" @@ -84,7 +76,7 @@ "canonicalSha256": "2f71bdc2f8044ca9b403affbb0cb3f31587e4c004a90e9e0836aa6143033b6da" } ], - "tableJsxSha256": "5cae4926ab10a762b26d77d0a0633b85de68fa204b7d5732ba0942e7fc276234", + "tableJsxSha256": "9d8b0f90e6a375fb3d019985d2b6d3409d7b41956e1893f0654c80499c1b64b4", "apiCalls": [ "listEnterpriseSignaturesPage", "listTenants", @@ -100,7 +92,6 @@ "applications", "deleteTarget", "drainageModal", - "drainageReport", "drainageStatusTarget", "enterpriseKeyword", "appliedEnterpriseKeyword", @@ -111,9 +102,9 @@ "error", "expandedSignatureId", "signatureKeyword", + "signatureSort", "appliedSignatureKeyword", "signatureModal", - "signatureReport", "reportStatusTarget", "signatures", "total", diff --git a/docs/system-functional-test-cases.md b/docs/system-functional-test-cases.md index c2b0550..8be39cc 100644 --- a/docs/system-functional-test-cases.md +++ b/docs/system-functional-test-cases.md @@ -4991,3 +4991,14 @@ npm run verify:phase8 | TC-DISK-DEDUP-002 | 打乱采集顺序、根目录有绑定路径、主路径停止采集、趋势缺失或NaN | 文件系统ID保持稳定;根盘兼容指标及趋势正确;主路径按层级/长度/字典序确定;同一文件系统只有一条趋势,不补零,不因路径改变丢失历史 | | TC-DISK-DEDUP-003 | 不同instance/device/fstype的样本容量相同,或指标不可用 | 不同文件系统不误合并;缺失指标为null/空趋势,页面显示暂无数据而非旧卡片;不得仅凭容量相同合并 | | TC-DISK-DEDUP-004 | 核对API查询、基础规则和按当前有效阈值生成的托管规则 | 容量和inode均max by(instance,device,fstype),可用容量min聚合,不按mountpoint重复告警;阈值不变;promtool校验通过,未来发布后再核对实际加载规则、告警及真实页面;告警标签变化会影响指纹和for计时,不虚报已保留 | + +## TC-ENTERPRISE-SIGNATURE-DENSITY-20260902 企业签名高密度列表 + +| 用例ID | 场景 | 预期 | +| --- | --- | --- | +| TC-ENTERPRISE-SIGNATURE-DENSITY-001 | 首次打开运营端企业签名管理 | 不请求或展示顶部全部/待审核/报备中/异常统计;短信签名Tab不显示总数;列表使用紧凑表头与行布局 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-002 | 查看签名及展开的引流信息 | 两级表头统一使用“审核状态”;移动/联通/电信仅显示已通过通道数/总通道数,不显示日期;引流信息列只显示条数,不显示异常数 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-003 | 查看签名查询和签名列 | 查询标签及表头统一为“签名”,不显示签名用途;签名统一显示完整中文黑括号,例如“【聆界科技】”,已有括号不得重复包裹 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-004 | 点击签名升序或降序按钮并翻页 | 请求携带signatureSort=asc/desc,后端先按签名及ID稳定排序再分页;切换排序回第一页,不只重排当前页,不产生重复请求 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-005 | 查看签名及引流信息操作列 | 两级操作列都只展示“报备状态、编辑、删除”;不显示“报备详情”;三个入口继续调用原真实后端流程 | +| TC-ENTERPRISE-SIGNATURE-DENSITY-006 | 1600×1000桌面视口查看并展开首条签名 | 表头与数据列对齐,操作按钮不换行,展开表格无裁切;页面无框架错误层,控制台无相关错误;窄视口由列表容器横向滚动,不挤压错列 | diff --git a/docs/testing-progress.md b/docs/testing-progress.md index fd183fc..028bf1e 100644 --- a/docs/testing-progress.md +++ b/docs/testing-progress.md @@ -4238,3 +4238,14 @@ git diff --check - 本地全量API 51套584项、前端9文件48项通过;前后端TypeScript检查、Vite构建、依赖安全、部署契约、包体积门禁及`git diff --check`通过。Vite仍提示既有Chart分块超过500kB,但入口gzip约107.39KiB,符合250KiB预算。新增回归覆盖绑定路径、乱序、主路径缺失、不同主机/设备/类型、缺失样本及展开交互;更新`TC-PORTAL-008/009`,新增`TC-DISK-DEDUP-001..004`。 - 本轮仅本地单元/组件回归及真实采集查询核验,没有部署新API/页面;登录浏览器视觉、控制台、真实页面展开及告警加载验收留待下一次明确授权发布,不以测试替身或构建通过冒充真实页面验收。既存日报5秒事务超时不在本轮修复范围。 - 提交仅包含本轮监控源码、规则、测试用例和本节进度追加;其他会话的迁盘文档及进度追加不纳入。本轮没有发送、补发、重投短信,也没有修改余额、通道或客户配置;待下一版本指令后再决定推送和目标环境发布。 + +## 2026-09-02 运营端企业签名管理高密度改版(仅本地提交) + +- 按用户确认的第1稿落实现有企业签名管理,不新增顶部统计接口或Tab数量;保留既有4项查询及查询/重置语义。签名列表改为紧凑表头/行布局,签名统一以中文黑括号展示且不重复包裹,不展示用途;引流信息仅显示条数,不计算或显示异常数。 +- 签名及引流两级表头统一为“审核状态”。移动、联通、电信列从文字状态/日期组合收敛为真实`approved/total`通道数字,零适用通道显示“—”,完整含义保留在title及无障碍标签中;数据继续来自现有真实报备目标汇总,不使用静态业务数据替代产品实现。 +- 签名列新增升/降序双按钮,前端传递`signatureSort=asc|desc`,后端按`name`再按`id`稳定排序后分页;无排序参数的既有调用继续按创建时间倒序。切换排序回第一页,已在第一页时仅发送一次请求。签名与引流操作列均只保留“报备状态、编辑、删除”,移除两个报备详情入口及对应未使用展示组件。 +- 更新R4页面契约:移除已废弃的两个详情组件/状态,纳入排序状态和新表格JSX;同时将前次签名/引流字段变更后已过期但未同步的两个表单函数哈希对齐当前源码。门禁通过,仍确认8个真实API调用和24项页面状态。 +- 定向回归:企业签名组件2项、SMS配置服务68项通过;全量API 51套584项、前端10文件50项通过,前后端TypeScript、定向ESLint、Vite构建、依赖安全、部署契约、R4契约及包体积门禁通过。Vite仍仅有既有Chart分块超过500kB提示,入口gzip 107.41KiB符合250KiB预算。 +- 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行追加继续保留,不覆盖、不夹带。 diff --git a/src/api/admin/governance.api.ts b/src/api/admin/governance.api.ts index 846a669..5bd43fb 100644 --- a/src/api/admin/governance.api.ts +++ b/src/api/admin/governance.api.ts @@ -40,7 +40,7 @@ export const adminGovernanceApi = { request(`/admin/reviews/${type}/${id}/decision`, { method: 'POST', body: JSON.stringify(body) }), listEnterpriseSignatures: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string; submittedAtFrom?: string; submittedAtTo?: string } = {}) => request(withQuery('/admin/enterprise-signatures', query)), - listEnterpriseSignaturesPage: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string; page: number; pageSize: number }) => + listEnterpriseSignaturesPage: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string; signatureSort?: 'asc' | 'desc'; page: number; pageSize: number }) => request>(withQuery('/admin/enterprise-signatures', query)), listEnterpriseSignatureOptions: (query: { tenantId?: string } = {}) => request(withQuery('/admin/enterprise-signature-options', query)), diff --git a/src/apps/admin/AdminEnterpriseSignaturesPage.tsx b/src/apps/admin/AdminEnterpriseSignaturesPage.tsx index 1e7e6e8..f886fd0 100644 --- a/src/apps/admin/AdminEnterpriseSignaturesPage.tsx +++ b/src/apps/admin/AdminEnterpriseSignaturesPage.tsx @@ -6,7 +6,7 @@ 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, DrainageReportModal, DrainageReportStatusModal, SignatureReportModal } 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'; @@ -16,7 +16,6 @@ export function AdminEnterpriseSignaturesPage() { const [applications, setApplications] = useState([]); 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 [drainageReport, setDrainageReport] = 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(''); @@ -27,9 +26,9 @@ export function AdminEnterpriseSignaturesPage() { const [error, setError] = useState(''); const [expandedSignatureId, setExpandedSignatureId] = useState(''); const [signatureKeyword, setSignatureKeyword] = useState(''); + const [signatureSort, setSignatureSort] = useState<'asc' | 'desc'>('asc'); const [appliedSignatureKeyword, setAppliedSignatureKeyword] = useState(''); const [signatureModal, setSignatureModal] = useState(null); - const [signatureReport, setSignatureReport] = useState(null); const [reportStatusTarget, setReportStatusTarget] = useState(null); const [signatures, setSignatures] = useState([]); const [total, setTotal] = useState(0); @@ -40,10 +39,10 @@ export function AdminEnterpriseSignaturesPage() { const pageSize = 10; - async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, signatureKeyword: appliedSignatureKeyword, drainageKeyword: appliedDrainageKeyword }, targetPage = page) { + 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, page: targetPage, pageSize }), + adminApi.listEnterpriseSignaturesPage({ ...filters, signatureSort: targetSort, page: targetPage, pageSize }), adminApi.listTenants(), adminApi.listEnterpriseApplicationOptions(), ]); @@ -130,13 +129,17 @@ export function AdminEnterpriseSignaturesPage() { loadData={loadData} setDeleteTarget={setDeleteTarget} setDrainageModal={setDrainageModal} - setDrainageReport={setDrainageReport} setDrainageStatusTarget={setDrainageStatusTarget} setExpandedSignatureId={setExpandedSignatureId} setPage={setPage} + setSignatureSort={(nextSort) => { + setSignatureSort(nextSort); + if (page === 1) void loadData(undefined, 1, nextSort); + else setPage(1); + }} setReportStatusTarget={setReportStatusTarget} setSignatureModal={setSignatureModal} - setSignatureReport={setSignatureReport} + signatureSort={signatureSort} total={total} totalPages={totalPages} visibleSignatures={visibleSignatures} @@ -159,7 +162,7 @@ export function AdminEnterpriseSignaturesPage() {
setEnterpriseKeyword(event.target.value)} placeholder="请输入企业名称" prefix={} value={enterpriseKeyword} /> setApplicationKeyword(event.target.value)} placeholder="请输入企业应用名称" prefix={} value={applicationKeyword} /> - setSignatureKeyword(event.target.value)} placeholder="请输入签名名称或用途" prefix={} value={signatureKeyword} /> + setSignatureKeyword(event.target.value)} placeholder="请输入签名" prefix={} value={signatureKeyword} /> setDrainageKeyword(event.target.value)} placeholder="请输入引流信息、URL 或备注" prefix={} value={drainageKeyword} />
+ + + + 企业 + 应用 + 审核状态 + 移动 + 联通 + 电信 + 引流信息 + 操作 +
{visibleSignatures.map((signature) => { const payload = readDrainagePayload(signature); const visibleDrainageLinks = appliedDrainageKeyword @@ -59,16 +77,15 @@ export function EnterpriseSignaturesTable({ -
签名名称{signature.name}
-
企业{signature.tenant?.name ?? signature.tenantId}
-
应用{signature.application?.name ?? '-'}
-
签名审核
-
-
-
-
引流信息{payload.links.length} 条
+
{formatSignatureName(signature.name)}
+
{signature.tenant?.name ?? signature.tenantId}
+
{signature.application?.name ?? '-'}
+
+
+
+
+
{payload.links.length} 条
- void loadData()} portal="admin" targetId={signature.id} targetType="signature" /> @@ -82,9 +99,9 @@ export function EnterpriseSignaturesTable({
引流url或号码 审核状态 - - - + 移动 + 联通 + 电信 操作
{visibleDrainageLinks.map((item) => { @@ -93,11 +110,10 @@ export function EnterpriseSignaturesTable({
{item.url} - - - + + + - diff --git a/src/apps/admin/enterprise-signatures/SignatureReportModals.tsx b/src/apps/admin/enterprise-signatures/SignatureReportModals.tsx index f40748a..775e630 100644 --- a/src/apps/admin/enterprise-signatures/SignatureReportModals.tsx +++ b/src/apps/admin/enterprise-signatures/SignatureReportModals.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Info } from 'lucide-react'; import { adminApi, type ClientSmsSignature } from '@/api/adminApi'; import { Button, CarrierTag, Modal, Select, Textarea } from '@/components/ui'; -import { AuditStatusTag, carrierLabel, CarrierReportTag, formatDate } from './signature.helpers'; +import { carrierLabel, CarrierReportTag } from './signature.helpers'; import type { DrainageInfo } from './signature.types'; const reportStatusOptions = [ @@ -11,28 +11,6 @@ const reportStatusOptions = [ { label: '报备失败', value: 'failed' }, { label: '放弃报备', value: 'abandoned' }, ]; -export function SignatureReportModal({ item, onClose }: { item: ClientSmsSignature; onClose: () => void }) { - return ( - 关闭} onClose={onClose} open title="签名报备详情"> -
-
-
企业名称{item.tenant?.name ?? item.tenantId}
-
应用名称{item.application?.name ?? '-'}
-
签名名称{item.name}
-
签名审核
-
更新时间{formatDate(item.updatedAt)}
-
-
- - - -
-
{(item.reportTargets ?? []).map((target) =>
{target.channel.name}
)}
-
-
- ); -} - export function ChannelReportStatusModal({ item, onClose, onSaved }: { item: ClientSmsSignature; onClose: () => void; onSaved: () => void }) { const targets = item.reportTargets ?? []; const carriers = ['mobile', 'unicom', 'telecom'] as const; @@ -56,24 +34,6 @@ export function ChannelReportStatusModal({ item, onClose, onSaved }: { item: Cli ; } -export function DrainageReportModal({ item, onClose, signature }: { item: DrainageInfo; onClose: () => void; signature: ClientSmsSignature }) { - const summary = signature.drainageCarrierReportSummary?.[item.id]; - const targets = signature.drainageReportTargets?.[item.id] ?? []; - return ( - 关闭} onClose={onClose} open size="xl" title="引流信息报备详情"> -
-
引流url或号码{item.url}
-
引流url或号码{item.url}
-
-
-
-
备注{item.remark || '-'}
-
-
{targets.map((target) =>
{target.channel.name}({carrierLabel(target.channel.carrier)})
)}
-
- ); -} - export function DrainageReportStatusModal({ item, onClose, onSaved, signature }: { item: DrainageInfo; onClose: () => void; onSaved: () => void; signature: ClientSmsSignature }) { const targets = signature.drainageReportTargets?.[item.id] ?? []; const [statuses, setStatuses] = useState>(() => Object.fromEntries(targets.map((target) => [target.channelId, target.status]))); diff --git a/src/apps/admin/enterprise-signatures/signature.helpers.tsx b/src/apps/admin/enterprise-signatures/signature.helpers.tsx index 92ccf28..75e3e88 100644 --- a/src/apps/admin/enterprise-signatures/signature.helpers.tsx +++ b/src/apps/admin/enterprise-signatures/signature.helpers.tsx @@ -15,6 +15,11 @@ export function carrierLabel(carrier?: string | null) { return carrierLabels[carrier] ?? carrier; } +export function formatSignatureName(name: string) { + 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 = '未报备'; @@ -27,6 +32,24 @@ export function CarrierReportTag({ summary }: { summary?: CarrierReportSummary } return {label}({summary.approved}/{summary.total}); } +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'; + return ( + + {summary.approved}/{summary.total} + + ); +} + 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 8c8f16a..4197ef3 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -2007,18 +2007,18 @@ .admin-enterprise-signature-list .signature-summary { gap: var(--space-2); - grid-template-columns: 22px minmax(104px, 0.85fr) minmax(132px, 1fr) minmax(104px, 0.8fr) repeat(5, minmax(76px, 0.55fr)) 184px; - min-width: 0; + grid-template-columns: 22px minmax(132px, 0.9fr) minmax(168px, 1.1fr) minmax(120px, 0.8fr) minmax(82px, 0.55fr) repeat(3, minmax(64px, 0.45fr)) minmax(68px, 0.45fr) 224px; + min-height: 64px; + min-width: 1160px; padding-inline: var(--space-3); } .admin-enterprise-signature-list .signature-actions { - display: grid; + display: flex; gap: 6px; - grid-template-columns: repeat(2, minmax(0, 1fr)); justify-self: end; - min-width: 184px; - width: 184px; + min-width: 224px; + width: 224px; } .admin-enterprise-signature-list .signature-actions .ui-button { @@ -2033,6 +2033,65 @@ font-weight: 400; } +.admin-enterprise-signature-list { + gap: 0; + overflow-x: auto; +} + +.admin-enterprise-signature-list .signature-card { + border-radius: 0; + border-top: 0; +} + +.admin-enterprise-signature-list .signature-card:last-of-type { + border-radius: 0 0 var(--radius-md) var(--radius-md); +} + +.enterprise-signature-table__head { + align-items: center; + background: var(--color-surface-subtle); + border: 1px solid var(--color-border); + border-radius: var(--radius-md) var(--radius-md) 0 0; + color: var(--color-text-muted); + display: grid; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + gap: var(--space-2); + grid-template-columns: 22px minmax(132px, 0.9fr) minmax(168px, 1.1fr) minmax(120px, 0.8fr) minmax(82px, 0.55fr) repeat(3, minmax(64px, 0.45fr)) minmax(68px, 0.45fr) 224px; + min-height: 46px; + min-width: 1160px; + padding-inline: var(--space-3); +} + +.enterprise-signature-table__sortable, +.enterprise-signature-table__sort-actions { + align-items: center; + display: flex; +} + +.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__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; + font-size: var(--font-size-xs); + font-variant-numeric: tabular-nums; + font-weight: var(--font-weight-semibold); + justify-content: center; + min-width: 40px; + padding: 4px 7px; +} + +.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--neutral { background: var(--color-surface-subtle); color: var(--color-text-subtle); } + .signature-report-status { display: grid; gap: var(--space-4); @@ -2233,13 +2292,6 @@ grid-row: 1 / span 2; } - .admin-enterprise-signature-list .signature-summary { - grid-template-columns: 22px repeat(7, minmax(76px, 1fr)) 184px; - } - - .admin-enterprise-signature-list .signature-summary > div:nth-of-type(3) { - display: none; - } } @media (max-width: 860px) { @@ -2254,20 +2306,6 @@ grid-row: auto; } - .admin-enterprise-signature-list .signature-summary { - grid-template-columns: 22px repeat(2, minmax(0, 1fr)); - } - - .admin-enterprise-signature-list .signature-summary > div, - .admin-enterprise-signature-list .signature-summary > div:nth-of-type(3) { - display: grid; - } - - .admin-enterprise-signature-list .signature-actions { - grid-column: 2 / -1; - justify-self: stretch; - width: 100%; - } } .system-log-filters__actions {