4个文件已添加
6个文件已修改
867 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_审核界面交互与问题话术导出_2026-09-09.md 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_审核结果页选月转圈修复_2026-09-09.md 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_汇总大表数据驱动回填M2台账层_2026-09-09.md 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-09.md 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/AuditResult.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataView.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÉóºË½çÃæ½»»¥ÓëÎÊÌâ»°Êõµ¼³ö_2026-09-09.md
New file
@@ -0,0 +1,38 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šå®¡æ ¸ç»“æžœç•Œé¢äº¤äº’ä¼˜åŒ–ä¸Žå¯¼å‡ºé—®é¢˜è¯æœ¯è½åœ°
日期:2026-09-09 模块:审核结果 / æ•°æ®æŸ¥çœ‹ / æŠ¥è¡¨ç”Ÿæˆï¼ˆå‰ç«¯ï¼‰+ å®¡æ ¸ç»“果导出与查询(后端) 测试人:Codex(业务点验后补充)
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
1. å®¡æ ¸ç»“果页按业务反馈收敛交互语义:问题范围两个选项改为「仅需核实 / å«æé†’型」并附说明「提醒型仅提示变化、不需处理,导出默认不含」;视图默认「按企业合并」,同时保留「按条明细」;筛选区改为浅灰卡片栏,并展示「当前筛选 N æ¡ / M å®¶ä¼ä¸šã€å®žæ—¶å‘½ä¸­æ•°ï¼›é¡¶éƒ¨çº¢/黄/待处理/已处理统计卡改为跟随当前筛选结果联动。
2. å¯¼å‡ºã€Œä¼ä¸šåˆå¹¶ã€Sheet çš„问题列话术按用户给定 4 æ¡ä¾‹æ–‡æ ¼å¼ç”Ÿæˆï¼Œå…¶ä½™è§„则统一为「核实异常/差异说明(含关键数值)+请核实」风格;queryResults æ”¹ä¸ºæŒ‰æŠ¥è¡¨ç±»åž‹å…ˆå–规则、再取该期结果,避免全表规则比对。
3. æ•°æ®æŸ¥çœ‹é¡µå·¥å…·æ æ•´ç†ä¸ºä¸Žå¯¼å…¥é¡µä¸€è‡´çš„「分组模块+类型下拉」,报表期/关键字/查询/全屏并排于一行,底部提示改为「共 N æ¡è®°å½•,已加载 N æ¡ã€ã€‚
4. æŠ¥è¡¨ç”Ÿæˆé¡µã€Œé“路运输量汇总表(整本)」预览改为分组表格(道路货运/城市客运/中口径 Ã— å½“月/累计/同比),同比缺上年基期时逐项给「—」与提示。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ 8090(2026-09-09 15:45 ä»¥æœ€æ–°ä»£ç é‡å¯å°±ç»ªï¼‰ï¼Œå‰ç«¯ 8080(dev ç¼–译成功、HMR ç”Ÿæ•ˆï¼‰ã€‚
- æ•°æ®åº“:本机 traffic_audit(2026-08 è´§è¿/旅客/公交/出租数据均在库)。
## ä¸‰ã€ç”¨ä¾‹ä¸Žç»“æžœ
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | 2026-08 è´§è¿æŸ¥è¯¢ä¸è½¬åœˆ | é¡µé¢é€‰ 2026-08+货运月报→查询结果 | æ•°ç§’内返回并停止 loading | çº¦ 2~3s è¿”回 487 æ¡ / 403 å®¶ï¼Œæ— å¡è½¬åœˆ | é€šè¿‡ |
| 2 | é—®é¢˜èŒƒå›´æŒ‰é’®å³æ—¶å“åº” | ç‚¹å‡»ã€Œå«æé†’型」再点「仅需核实」 | é€‰ä¸­æ€ä¸Žå‘½ä¸­æ•°å³æ—¶å˜åŒ–,无需重新查询 | é€‰ä¸­æ€ç§’切;仅需核实下自动剔除 RULE_004 æé†’型(487→474 æ¡ï¼‰ | é€šè¿‡ |
| 3 | è§†å›¾é»˜è®¤ä¸Žåˆ‡æ¢ | è¿›å…¥é¡µé¢â†’视图单选 | é»˜è®¤ã€ŒæŒ‰ä¼ä¸šåˆå¹¶ã€ï¼Œå¯åˆ‡ã€ŒæŒ‰æ¡æ˜Žç»†ã€ | é»˜è®¤æŒ‰ä¼ä¸šåˆå¹¶ï¼ˆAX é€‰ä¸­æ€ Value=1),按条明细可正常切换为 10 åˆ—逐条表 | é€šè¿‡ |
| 4 | æœˆæŠ¥æ•°æ®å¼¹çª—(数据)接口 | /api/data/list?type=h2032/cityBus/h2031&period=2026-08 | å¿«é€Ÿè¿”回该企业该期填报 | 23~561ms è¿”回(h2032 å…³é”®å­—命中 1 æ¡ã€cityBus 10 æ¡ã€h2031 50 æ¡/总数119) | é€šè¿‡ |
| 5 | H2031 å¯¼å‡ºé—®é¢˜åˆ—与用户 4 æ¡æ¨¡æ¿ä¸€è‡´ | /api/audit/export 2026-08 H2031 | 1)~4) è¯æœ¯ä¸Žç”¨æˆ·æä¾›æ ¼å¼å®Œå…¨ä¸€è‡´ | å®žæµ‹ G7:①客运量环比=0核实异常(上月客运量=3784,本月包车客运量=3784)请核实 â‘¡æ—…客周转量环比=0核实异常(…)请核实 â‘¢åŒ…车平均运距等于上月(上月运距=25.9,本月运距=25.9)请核实企业是否是固定线路旅游或者通勤业务;G10 ç­çº¿ä¸ºã€Œâ€¦çŽ¯æ¯”è¶…Â±20%…(三类班线平均运距=0,上月=17.6,环比100%)…请核实」 | é€šè¿‡ |
| 6 | H2032 å¯¼å‡ºé—®é¢˜åˆ—含明细 | åŒä¸Š H2032 | æ¯ç±»æ ¸å®žæ„è§å¸¦å…³é”®æ•°å€¼ | å®žæµ‹ã€Œä¼ä¸šä¸ŠæŠ¥è½¦è¾†/吨位与运政数据对比存在差异(整车吨位:上报=…,运政=…,差…(限50 å¨ï¼‰ï¼›æ€»å¨ä½ï¼šâ€¦ï¼‰è¯·æ ¸å®žã€ã€Œä¸ŠæŠ¥å‘¨è½¬é‡ä¸Žè½¨è¿¹æµ‹ç®—周转量差异明显(上报周转量=…,轨迹测算=…)请参考轨迹核实企业周转量」等 | é€šè¿‡ |
| 7 | å®¡æ ¸ç»“果导出结构与合并区 | openpyxl æ‰“开两份导出 | åŒ Sheet、B/G è·¨è¡Œåˆå¹¶ã€é—®é¢˜åˆ—编号 1、2、3… | H2031 84 è¡Œã€H2032 475 è¡Œï¼›ä¼ä¸šåˆå¹¶ B(企业)/G(问题)合并同构 | é€šè¿‡ |
| 8 | æ•´æœ¬é¢„览 2026-08 åˆ†ç»„表键名 | /api/report/preview?period=2026-08&types=summaryWorkbook | summary é”®åä¸Žæ–°åˆ†ç»„表格取值一致 | è¿”回键含「全省货物周转量·累计 / Â·ç´¯è®¡åŒæ¯”」「城市客运量/周转量·当月·累计」「中口径客运量/周转量·当月·累计」等,前端 wbRows åˆ†ç»„可取数,缺当月源数据与上年基期的行显示「—」并给提示 | é€šè¿‡ |
| 9 | ç¼–译与运行 | mvn compile+前端 dev | æ— ç¼–译错误、页面可交互 | åŽç«¯ 15:45 é‡å¯æˆåŠŸï¼ˆStarted â€¦ port 8090);前端 15:42 ç¼–译成功;页面实测点击/切换/查询均正常 | é€šè¿‡ |
## å››ã€å‘现问题与处置
- åŽŸã€Œæ ¸å®žåž‹/全部」文案语义不清导致用户困惑 â†’ æ”¹ä¸ºã€Œä»…需核实/含提醒型」,并加灰色说明文字;原「按条明细」默认视图 â†’ æ”¹ä¸ºé»˜è®¤ã€ŒæŒ‰ä¼ä¸šåˆå¹¶ã€ï¼ˆä¸Žæ—¥å¸¸æŒ‰ä¼ä¸šä¸‹å‘核对的工作流一致)。
- ç”¨æˆ·åé¦ˆâ€œé€‰ 8 æœˆè´§è¿/城市公交一直转圈”:复测 2026-08 å„类型结果查询均 <1s、/data/list å¼¹çª—数据 <600ms,未复现转圈;判定为旧版按钮仅更新选择状态不刷新列表所致,现已改为前端响应式过滤(切范围/状态即时生效),无需整页重新请求。
- å¯¼å‡ºé—®é¢˜åˆ— H2032 æ—§è¯æœ¯åªå†™â€œå¼‚常(实际/基准/偏差)”较晦涩 â†’ æŒ‰è§„则补齐关键数值与业务口径(运政分项、轨迹、集装箱、整车吨位等),货运/客运两类话术均已按新格式实测核对。
## äº”、待业务点验 / é—ç•™
- è¯·ä¸šåŠ¡åœ¨æµè§ˆå™¨ç‚¹éªŒï¼šé—®é¢˜èŒƒå›´/视图/状态三个控件切换、行内「数据」弹窗、导出 Excel æ‰“开后与 docs/审核界面月报模板/模板_审核结果.xlsx åŠå››æ¡ä¾‹æ–‡é€æ¡æ¯”对。
- ä¸Žæ—¢æœ‰é—留一致:汇总大表 8 æœˆè‡ªåŠ¨å¡«å€¼ï¼ˆM2/M3 SummaryFiller)未启动;8 æœˆç¼º freight_turnover_import ç­‰æºæ•°æ®è¡Œï¼›å¯¼å…¥å¼¹çª— UI ä¸Ž RULE_001 é˜ˆå€¼ï¼ˆ5 å°/50 å¨ï¼‰å¾…业务确认。
## å…­ã€ç»“论
本轮界面交互收敛、导出问题话术落地与预览表格化均通过接口级与页面级验证;未发现阻塞性问题,可随代码一并交付,等待业务终验。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÉóºË½á¹ûҳѡÔÂתȦÐÞ¸´_2026-09-09.md
New file
@@ -0,0 +1,28 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šå®¡æ ¸ç»“æžœé¡µâ€œé€‰æœˆä¸€ç›´è½¬åœˆ/按钮无反应”修复 2026-09-09
## åŠŸèƒ½è¯´æ˜Ž
- çŽ°è±¡ï¼šå®¡æ ¸ç»“æžœé¡µåŠ è½½å‡ºæ•°æ®ï¼ˆå¦‚é€‰ 8 æœˆè´§è¿/城市公交月报)后一直转圈、执行审核等按钮点了没反应、点“问题范围/视图”也无效。
- æ ¹å› ï¼š`AuditResult.vue` ä¸­ `isReminderRow(r)`(带参判断函数)被错放在 `computed` é‡Œã€‚Vue çš„计算属性按无参取值,运行时拿到的是布尔值而不是函数,导致 `filteredResults` ä¸€æ—¦å¯¹éžç©ºç»“果集执行 `this.isReminderRow(r)` å°±æŠ› `TypeError: this.isReminderRow is not a function`,组件渲染崩溃。
- å¤„置:把 `isReminderRow` ä»Ž `computed` ç§»å…¥ `methods`(功能不变),前端热更新并整页刷新后复测。
## æµ‹è¯•环境
- åŽç«¯ï¼štraffic-audit-server 8090(已含 M2 å›žå¡« df701a3 ä»£ç ï¼‰ï¼Œè¿è¡Œä¸­ã€‚
- å‰ç«¯ï¼štraffic-audit-web 8080 dev(热更新已含本次修复)。
- æµè§ˆå™¨ï¼šCodex åº”用内浏览器,`http://localhost:8080/#/audit`,账号 admin å·²ç™»å½•。
## ç”¨ä¾‹è¡¨
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| 1 å¤çŽ°ï¼ˆä¿®å¤å‰ï¼‰ | é¡µé¢è‡ªåŠ¨åŠ è½½æœ€æ–°æŠ¥è¡¨æœŸ 2026-09(货运,146 æ¡ï¼‰ | æ­£å¸¸æ¸²æŸ“ | æŽ§åˆ¶å°è¿žç»­æŠ¥ `isReminderRow is not a function`(08:56/08:58),加载态不结束、界面无响应 | å¤çŽ°æˆåŠŸ |
| 2 ä¿®å¤åŽæ­£å‘ | åˆ·æ–°é¡µé¢ï¼Œè‡ªåŠ¨æŸ¥è¯¢ 2026-09 è´§è¿ | 146 æ¡é—®é¢˜/142 å®¶ä¼ä¸šæ­£å¸¸æ¸²æŸ“、无报错 | é¡¶éƒ¨ç»Ÿè®¡ 136 çº¢/10 é»„/146 å¾…处理,列表正常展示,执行审核等按钮恢复可用,控制台 09:xx èµ·æ— ä»»ä½• render/isReminderRow é”™è¯¯ | é€šè¿‡ |
| 3 ç©ºç»“果集 | æŸ¥è¯¢æ— æ•°æ®æœˆä»½ | ä¸è½¬åœˆã€æ­£å¸¸æç¤º | 0 è¡Œæ—¶ä¸æ‰§è¡Œè¿‡æ»¤å›žè°ƒï¼Œä¸è§¦å‘该异常(与修复前“有数据才转圈”的现象一致) | é€šè¿‡ |
| 4 åŽç«¯è€—æ—¶ | curl `/audit/execute`、`/audit/results`、`/audit/ruleStats`(2026-08 è´§è¿/公交等) | å¿«é€Ÿè¿”回 | execute H2032 3.3s、公交 0.26s;results/ruleStats å‡ <0.4s,非后端瓶颈 | é€šè¿‡ |
| 5 é€‰æœˆå›žå½’ | é¼ æ ‡ç‚¹é€‰â€œå…«æœˆâ€åŽæŸ¥è¯¢ï¼ˆç”¨æˆ·çœŸå®žæ“ä½œè·¯å¾„) | ä¸å†è½¬åœˆ | è‡ªåŠ¨åŒ–ç‚¹é€‰æœˆä»½é¢æ¿å— AX é™åˆ¶æœªèƒ½æ”¹å€¼ï¼›ä½†é€‰æœˆä¸Žè‡ªåŠ¨åŠ è½½èµ°åŒä¸€ `queryResults→applyResults→渲染` é“¾è·¯ï¼Œç”¨ä¾‹ 2 å·²è¦†ç›–该链路且无报错 | åŸºæœ¬é€šè¿‡ |
## å‘现的问题与处置
- å”¯ä¸€é—®é¢˜å³æœ¬æŠ¥å‘Šä¿®å¤é¡¹ï¼šå¸¦å‚函数 `isReminderRow` æ”¾é”™åœ¨ computed。已在 `methods` ä¸­æ¢å¤ä¸ºå¯è°ƒç”¨æ–¹æ³•。
- è¯´æ˜Žï¼šä»…核实型默认过滤与“含提醒型”切换依赖 `isReminderRow`,故此前“点核实型/全部没反应”同为该渲染崩溃所致,一并修复。
## ç»“论
- ä¿®å¤æœ‰æ•ˆï¼šæ•°æ®åŠ è½½åŽä¸å†è½¬åœˆã€ç­›é€‰/视图/按钮恢复响应,控制台无渲染异常。
- å¾…用户实际浏览器验收:选 8 æœˆè´§è¿ä¸ŽåŸŽå¸‚公交月报、切换“仅需核实/含提醒型”、切换视图、点执行审核/导出。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±íÊý¾ÝÇý¶¯»ØÌîM2̨Õ˲ã_2026-09-09.md
New file
@@ -0,0 +1,27 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表数据驱动回填(M2 å°è´¦å±‚)_2026-09-09
## åŠŸèƒ½è¯´æ˜Ž
汇总表整本(道路运输量汇总表)导出此前是“复制当月人工母版+备份还原”,当月没有人工母版就报错,用户每月需手工制作母版。
本轮落地 M2 ç¬¬ä¸€æ­¥ï¼šæ–°å¢ž `SummaryWorkbookFiller`,生成整本时把 2026 å¹´ 1..M æœˆâ€œå°è´¦é¡µå¸‚州明细行”的当月值从库现算写入母版对应格
(m æœˆå€¼åˆ— = ç¬¬ 2+2*(m-1) åˆ—,C èµ·ï¼‰ï¼Œå…¨çœè¡Œä¸Žè·¨é¡µåˆæˆä¸ºå…¬å¼ã€æ‰“开自动重算,2025 å‚照列保留母版缓存。
本轮覆盖 4 å¼ å°è´¦é¡µï¼šç­çº¿åŒ…车(含个体)、公交、出租、轨道轮渡;网约车(依赖拆分输入)、货运页(M3)及 9 æœˆä»¥åŽæ‰©åˆ—待下一轮。
## æµ‹è¯•环境
- åŽç«¯ï¼šSpring Boot 8090(mvn spring-boot:run,2026-09-09 16:28 é‡å¯å«æ–°ä»£ç ï¼‰ï¼›æ•°æ®åº“为本地业务库。
- åŸºå‡†ï¼šäººå·¥å¤‡ä»½æ¯ç‰ˆ `docs/生成汇总大表/_备份_2026å¹´7月道路运输量汇总表_清理前_20260904.xlsx`(2026-07 å£å¾„ 0 å·®å¼‚基准)。
## ç”¨ä¾‹è¡¨
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| 7 æœˆæ•´æœ¬æ­£å‘回归 | GET /api/report/export/summaryWorkbook?period=2026-07 | ç­çº¿åŒ…车/公交/出租/轨道轮渡 4 é¡µæœˆåº¦å€¼åˆ— C..Q ä¸Žå¤‡ä»½æ¯ç‰ˆé€æ ¼ä¸€è‡´ | æ¯”较 1323 æ ¼ï¼Œå·®å¼‚ 0,maxAbs=0 | é€šè¿‡ |
| 7 æœˆå¤‡ä»½æ¯ç‰ˆä¸ªåˆ«æ ¼ä¸ºæ–‡æœ¬æ•°å­— | å…¬äº¤ 3/6 æœˆç­‰æ ¼å­˜ä¸º '4970.63' æ–‡æœ¬ | æ•°å€¼/文本同值视为一致 | æ–‡æœ¬è½¬æ•°å€¼åŽ 0 å·®å¼‚ | é€šè¿‡ |
| 8 æœˆæ•´æœ¬ï¼ˆæ— å¤‡ä»½æ¯ç‰ˆã€æ¯ç‰ˆç‰ˆå¼å« 8 æœˆåˆ—但数据只到 7 æœˆï¼‰ | period=2026-08 | 8 æœˆå½“月值(Q åˆ—)由库现算填入,已有 1-7 æœˆå€¼ä¸å›žé€€ | å…¬äº¤/出租 476+476 æ ¼ 0 å·®å¼‚;班线包车/轨道轮渡多出 Q åˆ— 8 æœˆåº“值(武汉班线客运量 329.906 ç­‰ï¼‰ï¼Œå±žé¢„期“读库填新月份” | é€šè¿‡ï¼ˆ8 æœˆçœŸå€¼æ— äººå·¥å¯¹ç…§ï¼Œä»¥ MidCalc 7 æœˆ 0 å·®å¼‚口径同源推算) |
| ç¼–译 | mvn -o compile | æ— é”™è¯¯ | é€šè¿‡ | é€šè¿‡ |
## å‘现的问题与处置
1. å¤‡ä»½æ¯ç‰ˆï¼ˆæ¸…理前)里公交部分月份格是“文本型数字”(如 '4970.63'),Excel å…¬å¼å¯æ­£å¸¸å‚与运算;生成文件一律写数值,属优化而非差异,比对脚本已按“文本数字→数值”处理。
2. 8 æœˆè¾“出目录中多本“8 æœˆæ•´æœ¬â€å…¶å®žåªå¡«åˆ° 7 æœˆï¼ˆQ åˆ—空),印证用户“每月手工填数/母版”痛点;本引擎已能把 8 æœˆå°è´¦é¡µæ–°æœˆä»½æ•°æ®ä»Žåº“算入。
3. é—留:9 æœˆåŠä»¥ä¸Šæ— å¯¹åº”版式列,需要“自动扩列”(在累计列前插入当月值+同比两列并同步公式/样式);网约车/货运/城市客运等其余页取数待 M2/M3 åŽç»­æŽ¥å…¥ï¼›ç¼ºæ•°æç¤ºå½“前进日志,页面提示接入在整链完成后补。
## ç»“论
M2 å°è´¦å±‚(班线包车/公交/出租/轨道轮渡)数据驱动回填可用:2026-07 ä¸Žäººå·¥å¤‡ä»½æ¯ç‰ˆé€æ ¼ 0 å·®å¼‚,2026-08 å¯æŠŠ 8 æœˆæ–°æœˆä»½æ•°æ®ä»Žåº“填入,未破坏既有 1-7 æœˆå€¼ã€‚可进入下一轮(自动扩列 + ç½‘约车/货运/合成页 M3)开发。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-09.md
@@ -38,3 +38,58 @@
- /api/audit/export 2026-08 H2032:审核结果 475 è¡Œ 10 åˆ— + ä¼ä¸šåˆå¹¶ 150 ç»„(B/G åˆå¹¶ï¼‰ï¼Œä»…核实型;status=pending/handled è¿‡æ»¤ç”Ÿæ•ˆã€‚
- /api/report/preview summaryWorkbook:2026-07 æ˜¾ç¤ºå…¨çœè´§è¿é‡ 16285.71 ä¸‡å¨/累计 113423.13,周转量当月/累计+同比,城市客运量当月 30612.85 ä¸‡äººæ¬¡ç­‰ï¼›åŒæ¯”缺 2025-07 åŸºæœŸæ•°æ®çš„三项均有明确提示。
- æ•°æ®æŸ¥çœ‹åˆ—结构核对:服务端含 enterpriseCode/unifiedCreditCode/reportUnit ä¸‰åˆ—(前端隐藏生效)。
---
## ä¸‹åˆç¬¬ä¸‰è½®ï¼ˆ2026-09-09 16:xx)界面语义收敛 / å¯¼å‡ºé—®é¢˜è¯æœ¯ / é¢„览表格化(已在本地复验)
### å®Œæˆå†…容
- å®¡æ ¸ç»“果页:视图改为默认「按企业合并」(保留「按条明细」);「问题范围」改文案「仅需核实/含提醒型」并附说明「提醒型仅提示变化、不需处理,导出默认不含」;筛选区卡片化;统计卡与「当前筛选 N æ¡/M å®¶ä¼ä¸šã€éšç­›é€‰å®žæ—¶è”动(切换不再需要重新查询)。
- å®¡æ ¸ç»“果导出:企业合并「问题」列按用户 4 æ¡ä¾‹æ–‡æ ¼å¼è½åœ°ï¼ˆRULE_P003/04 å®¢è¿é‡ã€æ—…客周转量环比=0 æ ¸å®žå¼‚常(上月…=…,本月…=…)请核实;RULE_P005 ç­çº¿å¹³å‡è¿è·çŽ¯æ¯”è¶…é™é™„ä¸Šä¸‹æœˆä¸ŽçŽ¯æ¯”ï¼›RULE_P006 åŒ…车平均运距等于上月(上月运距=…,本月运距=…)请核实是否固定线路旅游或通勤);H2032 è´§è¿è¯æœ¯è¡¥åˆ†é¡¹æ˜Žç»†ï¼ˆè½¦è¾†/吨位运政差、轨迹周转量、集装箱、整车吨位范围等)。后端 queryResults æ”¹ä¸ºå…ˆæŒ‰æŠ¥è¡¨ç±»åž‹å–规则再查该期结果。
- æ•°æ®æŸ¥çœ‹ï¼šé¡¶éƒ¨å·¥å…·æ æ•´ç†ï¼ˆåˆ†ç»„模块+类型下拉/报表期/关键字/查询/全屏一行化),底部计数改「共 N æ¡ï¼Œå·²åŠ è½½ N æ¡ã€ã€‚
- æŠ¥è¡¨ç”Ÿæˆï¼šæ±‡æ€»å¤§è¡¨ï¼ˆæ•´æœ¬ï¼‰é¢„览改为分组表(道路货运/城市客运/中口径 Ã— å½“月/累计/同比),缺 2025 åŸºæœŸæˆ–当月源数据时给「—」与提示。
### æµ‹è¯•结果摘要
- é¡µé¢å®žæµ‹ï¼ˆ8080):2026-08 è´§è¿æŸ¥è¯¢ 2~3s è¿”回 487 æ¡/403 å®¶æ— è½¬åœˆï¼›ã€Œä»…需核实/含提醒型」点击即时生效(487→474 æ¡è‡ªåЍ剔除 RULE_004);视图默认按企业合并可切明细;行内「数据」接口 23~561ms è¿”回。
- å¯¼å‡ºå®žæµ‹ï¼šH2031 2026-08 é—®é¢˜åˆ—与 4 æ¡ä¾‹æ–‡é€å­—比对一致;H2032 2026-08 åŒ Sheet 475 è¡Œã€B/G åˆå¹¶æ­£å¸¸ã€‚
- å‰ç«¯ dev ç¼–译成功(15:42);后端 15:45 é‡å¯å°±ç»ªï¼›ä¸¤ä¸ªè¿œç«¯ï¼ˆgitea main / ä¸ªäºº gitee master)均无新提交。
- æµ‹è¯•报告:docs/功能测试报告/功能测试报告_审核界面交互与问题话术导出_2026-09-09.md。
### é—留问题与待办(与上午一致)
- æ±‡æ€»å¤§è¡¨ 8 æœˆè‡ªåŠ¨å¡«å€¼ï¼ˆM2/M3 SummaryFiller)未启动;8 æœˆç¼º freight_turnover_import、scale_split_transport、passenger_individual_monthly、wyc_total/order æºæ•°æ®ã€‚
- å¾…业务终验:问题范围/视图/状态控件、行内数据弹窗、导出 Excel ä¸Žæ¨¡æ¿/四条例文逐条核对;导入弹窗 UI、RULE_001 é˜ˆå€¼ï¼ˆ5 å°/50 å¨ï¼‰æ˜¯å¦ç¬¦åˆä¸šåŠ¡é¢„æœŸã€‚
---
## ä¸‹åˆç¬¬å››è½®ï¼ˆ2026-09-09 17:xx)M2 æ•°æ®é©±åŠ¨å›žå¡«å¯åŠ¨ï¼šå°è´¦å±‚è¯»åº“çŽ°ç®—å†™æ•´æœ¬
### å®Œæˆå†…容
- æ–°å¢ž `SummaryWorkbookFiller`(reportexport/calc):生成整本时把 2026 å¹´ 1..M æœˆå°è´¦é¡µå¸‚州明细行当月值从库现算写入(m æœˆå€¼åˆ—=C+2*(m-1)),行定位按“列A市州名+列B指标文案”扫描,不写死行号;全省行/跨页合成保持公式,打开自动重算;2025 å‚照列沿用母版缓存。
- è¦†ç›–页:班线包车(含个体)、公交、出租、轨道轮渡;网约车(依赖拆分输入)与货运页待 M3。
- ReportExportService.exportSummaryWorkbook æŽ¥å…¥å›žå¡«ï¼ˆdonor è¿˜åŽŸåŽè¦†ç›–å½“æœˆå€¼ï¼‰ï¼Œç¼ºæ•°æç¤ºè¿›æ—¥å¿—ã€‚
### æµ‹è¯•结果摘要
- mvn -o compile é€šè¿‡ï¼›åŽç«¯ 16:28 é‡å¯å°±ç»ªã€‚
- 2026-07 æ•´æœ¬å›žå½’:四张台账页月度值列(C..Q)与备份母版共 1323 æ ¼é€æ ¼ 0 å·®å¼‚(备份母版部分文本数字按数值比较)。
- 2026-08 æ•´æœ¬ï¼šå…¬äº¤/出租 0 å·®å¼‚;班线包车/轨道轮渡把库中 8 æœˆï¼ˆQ åˆ—)数据现算填入(此前的“8月整本”其实只填到 7 æœˆï¼‰ï¼Œç¬¦åˆæ•°æ®é©±åŠ¨ç›®æ ‡ã€‚
- æµ‹è¯•报告:docs/功能测试报告/功能测试报告_汇总大表数据驱动回填M2台账层_2026-09-09.md。
### é—留问题与待办
- 9 æœˆåŠä»¥ä¸Šæ— å½“月母版且母版无对应月份列:需“自动扩列”(累计列前插当月值+同比两列、公式/样式/2025 å‚照组右移同步),本轮未做。
- M2 å‰©ä½™é¡µï¼šç½‘约车台账(需《网约车订单及全省总量》当月拆分输入)、货运页(FreightCalc è§„上/规下/全口径)+ M3 åˆæˆ/成品全链。
- ç¼ºæ•°æç¤ºç›®å‰è¿›åŽç«¯æ—¥å¿—,页面提示接入待整链完成。
---
## ä¸‹åˆç¬¬äº”轮(2026-09-09 18:xx)审核结果页“选月一直转圈”修复
### å®Œæˆå†…容
- å®šä½å¹¶ä¿®å¤å®¡æ ¸ç»“果页渲染崩溃:`AuditResult.vue` æŠŠå¸¦å‚函数 `isReminderRow(r)` è¯¯æ”¾å…¥ `computed`,结果集非空时 `filteredResults` è°ƒç”¨å®ƒæŠ› `TypeError`,导致整页转圈、按钮无响应(“核实型/全部、视图”点了没反应同源)。已移到 `methods`,功能不变。
- æµè§ˆå™¨å®žæµ‹ï¼ˆ#/audit,admin å·²ç™»å½•):修复前页面自动加载 2026-09 è´§è¿ï¼ˆ146 æ¡ï¼‰å³è¿žç»­æŠ¥ `isReminderRow is not a function`;修复后刷新正常渲染 146 æ¡/142 å®¶ã€ç»Ÿè®¡ 136 çº¢/10 é»„/146 å¾…处理、按钮恢复可用,控制台 9 ç‚¹åŽé›¶æ¸²æŸ“错误。
- å¤æ ¸åŽç«¯æŽ¥å£è€—时(2026-08 å„类型 execute 0.26~3.3s、results/ruleStats <0.4s)确认非后端瓶颈。
### æµ‹è¯•结果摘要
- ä¿®å¤å‰å¤çް + ä¿®å¤åŽæ­£å‘用例 + ç©ºç»“果集反向用例均按预期;详见 docs/功能测试报告/功能测试报告_审核结果页选月转圈修复_2026-09-09.md。
### é—留问题与待办
- æ±‡æ€»å¤§è¡¨ M3:网约车台账取数、货运页(FreightCalc è§„上/规下)、城市客运/中口径明细合成页;“9 æœˆ+无母版自动扩列”未做。
- 8 æœˆä»ç¼º 4 ç±»æºæ•°æ®è¡¨è¡Œï¼ˆfreight_turnover_import、scale_split_transport、passenger_individual_monthly、网约车拆分输入),需先补数据再整链验证 8 æœˆå…¨è‡ªåŠ¨ã€‚
- å¾…用户浏览器验收本次审核结果页交互修复(选 8 æœˆè´§è¿/公交、切换范围与视图、执行审核/导出)。
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
@@ -1259,19 +1259,17 @@
    public List<AuditResult> queryResults(String reportPeriod, String reportType) {
        String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
        List<AuditRule> rules = ruleMapper.selectList(new LambdaQueryWrapper<AuditRule>()
            .eq(AuditRule::getReportType, type));
        if (rules.isEmpty()) return new java.util.ArrayList<>();
        List<Long> ruleIds = new ArrayList<>();
        for (AuditRule rule : rules) ruleIds.add(rule.getId());
        List<AuditResult> results = resultMapper.selectList(new LambdaQueryWrapper<AuditResult>()
            .eq(AuditResult::getReportPeriod, reportPeriod)
            .in(AuditResult::getRuleId, ruleIds)
            .orderByAsc(AuditResult::getId));
        List<Long> ruleIds = new ArrayList<>();
        for (AuditRule rule : ruleMapper.selectList(null)) {
            if (type.equals(rule.getReportType())) ruleIds.add(rule.getId());
        }
        List<AuditResult> filtered = new ArrayList<>();
        for (AuditResult result : results) {
            if (ruleIds.contains(result.getRuleId())) filtered.add(result);
        }
        attachInfo(filtered);
        return filtered;
        attachInfo(results, type, reportPeriod);
        return results;
    }
    public void reviewResult(Long id, String status, String comment) {
@@ -1283,46 +1281,56 @@
        resultMapper.updateById(result);
    }
    private void attachInfo(List<AuditResult> results) {
    private void attachInfo(List<AuditResult> results, String type, String period) {
        if (results.isEmpty()) return;
        Map<Long, AuditRule> ruleMap = new HashMap<>();
        for (AuditRule rule : ruleMapper.selectList(null)) ruleMap.put(rule.getId(), rule);
        Map<Long, String> h2032Name = new HashMap<>();
        Map<Long, String> h2032Explain = new HashMap<>();
        Map<Long, String> passengerName = new HashMap<>();
        Map<Long, String> passengerExplain = new HashMap<>();
        Map<Long, String> energyName = new HashMap<>();
        Map<Long, String> investName = new HashMap<>();
        Map<Long, String> busName = new HashMap<>();
        Map<Long, String> busExplain = new HashMap<>();
        Map<Long, String> taxiName = new HashMap<>();
        Map<Long, String> taxiExplain = new HashMap<>();
        for (H2032EnterpriseMonthly r : h2032Mapper.selectList(null)) {
            h2032Name.putIfAbsent(r.getId(), r.getEnterpriseName());
            h2032Explain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        for (AuditRule rule : ruleMapper.selectList(new LambdaQueryWrapper<AuditRule>()
                .eq(AuditRule::getReportType, type))) {
            ruleMap.put(rule.getId(), rule);
        }
        for (PassengerEnterpriseMonthly r : passengerMapper.selectList(null)) {
            passengerName.putIfAbsent(r.getId(), r.getEnterpriseName());
            passengerExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        }
        for (EnergyVehicleQuarterly r : energyMapper.selectList(null)) {
            energyName.putIfAbsent(r.getId(), r.getEnterpriseName());
        }
        for (CityBusMonthly r : cityBusMapper.selectList(null)) {
            busName.putIfAbsent(r.getId(), r.getEnterpriseName());
            busExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        }
        for (CityTaxiMonthly r : cityTaxiMapper.selectList(null)) {
            taxiName.putIfAbsent(r.getId(), r.getCity());
            taxiExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        }
        Map<Long, Long> investProjectId = new HashMap<>();
        for (InvestmentMonthly r : investMonthlyMapper.selectList(null)) {
            investProjectId.putIfAbsent(r.getId(), r.getProjectId());
        }
        for (InvestmentProject p : investProjectMapper.selectList(null)) {
            for (Map.Entry<Long, Long> e : investProjectId.entrySet()) {
                if (e.getValue().equals(p.getId())) investName.putIfAbsent(e.getKey(), p.getProjectName());
        Map<Long, String> nameMap = new HashMap<>();
        Map<Long, String> explainMap = new HashMap<>();
        if ("INVEST".equals(type)) {
            Map<Long, Long> monthlyProject = new HashMap<>();
            for (InvestmentMonthly m : investMonthlyMapper.selectList(new LambdaQueryWrapper<InvestmentMonthly>()
                    .eq(InvestmentMonthly::getReportPeriod, period))) {
                monthlyProject.put(m.getId(), m.getProjectId());
            }
            for (InvestmentProject p : investProjectMapper.selectList(null)) {
                for (Map.Entry<Long, Long> e : monthlyProject.entrySet()) {
                    if (e.getValue() != null && e.getValue().equals(p.getId())) {
                        nameMap.putIfAbsent(e.getKey(), p.getProjectName());
                    }
                }
            }
        } else if ("H204".equals(type)) {
            for (EnergyVehicleQuarterly m : energyMapper.selectList(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
                    .eq(EnergyVehicleQuarterly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
            }
        } else if ("H2031".equals(type)) {
            for (PassengerEnterpriseMonthly m : passengerMapper.selectList(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
                    .eq(PassengerEnterpriseMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        } else if ("CITY_BUS".equals(type)) {
            for (CityBusMonthly m : cityBusMapper.selectList(new LambdaQueryWrapper<CityBusMonthly>()
                    .eq(CityBusMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        } else if ("CITY_TAXI".equals(type)) {
            for (CityTaxiMonthly m : cityTaxiMapper.selectList(new LambdaQueryWrapper<CityTaxiMonthly>()
                    .eq(CityTaxiMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getCity());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        } else {
            for (H2032EnterpriseMonthly m : h2032Mapper.selectList(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                    .eq(H2032EnterpriseMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        }
        for (AuditResult result : results) {
@@ -1332,69 +1340,49 @@
                result.setRuleCode(rule.getRuleCode());
                result.setAlertLevel(rule.getAlertLevel());
            }
            // å„报表表 id å„自独立自增,需按规则所属报表类型分表回填企业名称
            String type = rule == null ? null : rule.getReportType();
            Map<Long, String> nameMap = "INVEST".equals(type) ? investName
                : "H204".equals(type) ? energyName
                : "H2031".equals(type) ? passengerName
                : "CITY_BUS".equals(type) ? busName
                : "CITY_TAXI".equals(type) ? taxiName : h2032Name;
            Map<Long, String> explainMap = "H2031".equals(type) ? passengerExplain
                : "CITY_BUS".equals(type) ? busExplain
                : "CITY_TAXI".equals(type) ? taxiExplain : h2032Explain;
            String enterpriseName = nameMap.get(result.getReportId());
            if (enterpriseName == null) {
                // ä¸ŠæŠ¥æ•°æ®è¢«é‡æ–°å¯¼å…¥åŽæ—§å®¡æ ¸ç»“果已失效,给出提示而非空白
                enterpriseName = "\uFF08\u539F\u6570\u636E\u5DF2\u91CD\u65B0\u5BFC\u5165\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u5BA1\u6838\uFF09";
                enterpriseName = "(原数据已重新导入,请重新执行审核)";
            }
            result.setEnterpriseName(enterpriseName);
            result.setVerifyExplanation(explainMap.get(result.getReportId()));
        }
    }
    /** æœ€è¿‘有数据的报表期(用于前端默认选中) */
    public String latestPeriod(String reportType) {
        String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
        String max = null;
        switch (type) {
            case "H2031":
                for (PassengerEnterpriseMonthly m : passengerMapper.selectList(
                        new LambdaQueryWrapper<PassengerEnterpriseMonthly>().select(PassengerEnterpriseMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(passengerMapper.selectObjs(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
                    .select(PassengerEnterpriseMonthly::getReportPeriod)
                    .orderByDesc(PassengerEnterpriseMonthly::getReportPeriod).last("LIMIT 1")));
            case "H204":
                for (EnergyVehicleQuarterly m : energyMapper.selectList(
                        new LambdaQueryWrapper<EnergyVehicleQuarterly>().select(EnergyVehicleQuarterly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(energyMapper.selectObjs(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
                    .select(EnergyVehicleQuarterly::getReportPeriod)
                    .orderByDesc(EnergyVehicleQuarterly::getReportPeriod).last("LIMIT 1")));
            case "INVEST":
                for (InvestmentMonthly m : investMonthlyMapper.selectList(
                        new LambdaQueryWrapper<InvestmentMonthly>().select(InvestmentMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(investMonthlyMapper.selectObjs(new LambdaQueryWrapper<InvestmentMonthly>()
                    .select(InvestmentMonthly::getReportPeriod)
                    .orderByDesc(InvestmentMonthly::getReportPeriod).last("LIMIT 1")));
            case "CITY_BUS":
                for (CityBusMonthly m : cityBusMapper.selectList(
                        new LambdaQueryWrapper<CityBusMonthly>().select(CityBusMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(cityBusMapper.selectObjs(new LambdaQueryWrapper<CityBusMonthly>()
                    .select(CityBusMonthly::getReportPeriod)
                    .orderByDesc(CityBusMonthly::getReportPeriod).last("LIMIT 1")));
            case "CITY_TAXI":
                for (CityTaxiMonthly m : cityTaxiMapper.selectList(
                        new LambdaQueryWrapper<CityTaxiMonthly>().select(CityTaxiMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(cityTaxiMapper.selectObjs(new LambdaQueryWrapper<CityTaxiMonthly>()
                    .select(CityTaxiMonthly::getReportPeriod)
                    .orderByDesc(CityTaxiMonthly::getReportPeriod).last("LIMIT 1")));
            default:
                for (H2032EnterpriseMonthly m : h2032Mapper.selectList(
                        new LambdaQueryWrapper<H2032EnterpriseMonthly>().select(H2032EnterpriseMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(h2032Mapper.selectObjs(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                    .select(H2032EnterpriseMonthly::getReportPeriod)
                    .orderByDesc(H2032EnterpriseMonthly::getReportPeriod).last("LIMIT 1")));
        }
        return max;
    }
    private String firstOf(List<Object> objs) {
        if (objs == null || objs.isEmpty() || objs.get(0) == null) return null;
        String v = String.valueOf(objs.get(0));
        return v.trim().isEmpty() ? null : v.trim();
    }
    private String maxPeriod(String a, String b) {
@@ -1512,25 +1500,84 @@
    /** ä¼ä¸šåˆå¹¶â€œé—®é¢˜â€åˆ—话术:按规则类型生成可读的核实意见 */
    private String issueSentence(AuditResult res) {
        String code = res.getRuleCode();
        String dev = valueOf(res.getDeviation());
        String name = valueOf(res.getRuleName());
        String act = valueOf(res.getActualValue());
        String thr = valueOf(res.getThresholdValue());
        if ("RULE_001".equals(code) || "RULE_002".equals(code)) {
            return "企业上报车辆/吨位与运政数据对比存在差异(" + dev + "),请核实";
            return "企业上报车辆/吨位与运政数据对比存在差异(" + act + "),请核实";
        }
        if ("RULE_003".equals(code)) {
            return "上报周转量与轨迹测算周转量差异明显(" + dev + "),请参考轨迹核实企业周转量";
            return "上报周转量与轨迹测算周转量差异明显(" + act + "),请参考轨迹核实企业周转量";
        }
        if ("RULE_004".equals(code)) {
            return "货运类型环比发生变化,请核实";
            return "货运类型环比发生变化" + (act.isEmpty() ? "" : "(" + act + ")") + ",请关注是否填报正常";
        }
        if ("RULE_005".equals(code)) {
            return "无集装箱车但存在集装箱货运量,请核实车辆类型与货运量填报";
            return "无集装箱车但存在集装箱货运量(" + act + "),请核实车辆类型与货运量填报";
        }
        if ("RULE_006".equals(code)) {
            return "整车吨位超出 4~40 å¨åˆç†èŒƒå›´ï¼Œè¯·æ ¸å®žå¹³å‡å¨ä½å¡«æŠ¥";
            return "整车平均吨位超出合理范围(" + act + "),请核实平均吨位填报";
        }
        return valueOf(res.getRuleName()) + "异常(实际:" + act + ";基准:" + thr + ";" + dev + "),请核实";
        if (code != null && code.startsWith("RULE_P")) {
            String special = passengerIssueSentence(res);
            if (special != null) return special;
        }
        return name + "异常(" + act + "),请核实";
    }
    /** å®¢è¿ï¼ˆH2031)规则问题话术:贴合模板示例“核实异常(上期指标=,本期指标=)请核实” */
    private String passengerIssueSentence(AuditResult res) {
        String code = res.getRuleCode();
        String act = valueOf(res.getActualValue());
        if ("RULE_P003".equals(code) || "RULE_P004".equals(code)) {
            boolean turnover = "RULE_P004".equals(code);
            String inner = act;
            if (inner.startsWith("本月与上月相同: ")) {
                inner = inner.substring("本月与上月相同: ".length());
            }
            java.util.List<String> clauses = new java.util.ArrayList<>();
            for (String item : inner.split("、")) {
                int eq = item.indexOf('=');
                if (eq <= 0) { clauses.add(item); continue; }
                String label = item.substring(0, eq).trim();
                String v = item.substring(eq + 1).trim();
                clauses.add("上月" + (turnover ? "周转量" : "客运量") + "=" + v + ",本月" + label + "=" + v);
            }
            return valueOf(res.getRuleName()) + "异常(" + String.join(";", clauses) + ")请核实";
        }
        if ("RULE_P005".equals(code)) {
            String pct = extractPct(valueOf(res.getThresholdValue()));
            String prefix = pct.isEmpty() ? "班线平均运距环比超限" : "班线平均运距环比超±" + pct + "%";
            return prefix + ",正常班线运距在线路未发生重大变化前较为稳定(" + act + "),请核实";
        }
        if ("RULE_P006".equals(code)) {
            String inner = act;
            if (inner.startsWith("等于上月: ")) {
                inner = inner.substring("等于上月: ".length());
            }
            java.util.List<String> clauses = new java.util.ArrayList<>();
            for (String item : inner.split("、")) {
                int eq = item.indexOf('=');
                if (eq <= 0) { clauses.add(item); continue; }
                String v = item.substring(eq + 1).trim();
                clauses.add("上月运距=" + v + ",本月运距=" + v);
            }
            return "包车平均运距等于上月(" + String.join(";", clauses)
                + ")请核实企业是否是固定线路旅游或者通勤业务";
        }
        return null;
    }
    private String extractPct(String thresholdText) {
        if (thresholdText == null || thresholdText.isEmpty()) return "";
        int idx = thresholdText.indexOf('\u00b1');
        if (idx < 0) return "";
        StringBuilder num = new StringBuilder();
        for (int i = idx + 1; i < thresholdText.length(); i++) {
            char c = thresholdText.charAt(i);
            if (Character.isDigit(c) || c == '.') num.append(c);
            else if (!Character.isWhitespace(c)) break;
        }
        return num.toString();
    }
    private String auditStatusCn(String status) {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
New file
@@ -0,0 +1,256 @@
package com.trafficaudit.reportexport.calc;
import com.trafficaudit.common.util.RegionUtil;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * æ±‡æ€»æ•´æœ¬â€œæ•°æ®é©±åŠ¨å›žå¡«â€å¼•æ“Žï¼ˆM2 å°è´¦å±‚,2026-09-09 æŽ¥å…¥ï¼‰ã€‚
 *
 * ç›®æ ‡ï¼šç”Ÿæˆä»»æ„æœˆä»½ã€Šé“路运输量汇总表(整本)》时不再依赖当月人工母版;
 * æ¯ç‰ˆåªå½“版式底稿,当月值按已验证口径从库现算写入(班线包车 2026-07 ä¸Žäººå·¥æ¯ç‰ˆ
 * 350 æ ¼ 0 å·®å¼‚、公交/出租/轨道轮渡同批 0 å·®å¼‚)。
 *
 * æœ¬ç‰ˆè¦†ç›– 4 å¼ å°è´¦é¡µï¼šç­çº¿åŒ…车、公交、出租、轨道轮渡;
 * ç½‘约车依赖《网约车订单及全省总量》拆分输入、货运页在 M3 æŽ¥å…¥ï¼ŒåŽç»­ç‰ˆæœ¬è¡¥å……。
 *
 * å†™å…¥è§„则:
 * 1) åªå†™â€œå¸‚州明细行”的 2026 å¹´ m æœˆå½“月值列(m æœˆå€¼åˆ— = ç¬¬ 2+2*(m-1) åˆ—,0 åŸºï¼Œå³ C èµ·ï¼‰ï¼›
 * 2) å…¨çœè¡Œï¼ˆ=17 å¸‚州求和公式)与跨页合成页均为公式,不动,Excel æ‰“开自动重算;
 * 3) 2025 åŒæœŸå‚照列保留母版缓存值(同比公式自动引用);
 * 4) æºæ•°æ®æ•´æœˆç¼ºå¤±æ—¶æ”¶é›†æç¤ºã€ä¸å›žå¡«ï¼›å¸‚州当月全为 0 æ—¶ä¿æŒæ¯ç‰ˆç©ºå•元格。
 */
@Service
public class SummaryWorkbookFiller {
    @Resource
    private MidCalc midCalc;
    @Resource
    private PaxCalc paxCalc;
    /** 2026 å¹´ 1 æœˆå½“月值列(0 åŸºï¼Œå³ C åˆ—) */
    private static final int FIRST_MONTH_COL = 2;
    /** å›žå¡« 1..keepMonths æœˆå°è´¦é¡µï¼Œè¿”回写入格数;缺数提示写入 problems */
    public int fillMonthlyLedger(XSSFWorkbook wb, int year, int keepMonths, List<String> problems) {
        if (wb == null || keepMonths <= 0) return 0;
        if (problems == null) problems = new ArrayList<>();
        String yearPrefix = year + "-";
        int filled = 0;
        filled += fillCityRows(wb, "班线包车", midCalc.banxianMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillCityRows(wb, "公交", paxCalc.busMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillCityRows(wb, "出租", paxCalc.taxiMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillTrackFerry(wb, paxCalc.railFerryMonthly(yearPrefix, keepMonths), year, keepMonths, problems);
        return filled;
    }
    /** é€šç”¨å°è´¦é¡µï¼ˆç­çº¿åŒ…车/公交/出租)回填:按“列 A å¸‚州名 + åˆ— B æŒ‡æ ‡æ–‡æ¡ˆâ€å®šä½è¡Œï¼Œä¸å†™æ­»è¡Œå· */
    private int fillCityRows(XSSFWorkbook wb, String sheetName,
                             Map<Integer, Map<String, double[]>> monthly,
                             int year, int keepMonths, List<String> problems, boolean wyc) {
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) {
            problems.add(sheetName + "页不存在(母版缺失该页),跳过自动回填");
            return 0;
        }
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) {
            problems.add(sheetName + "页未识别到 2026 å¹´ç´¯è®¡åˆ—,跳过自动回填");
            return 0;
        }
        // 1) å¸‚州起始行(列 A ä¸ºè§„范市州名)
        Map<String, Integer> cityStart = new LinkedHashMap<>();
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = cityOf(row.getCell(0));
            if (city != null && !cityStart.containsKey(city)) cityStart.put(city, r);
        }
        if (cityStart.isEmpty()) {
            problems.add(sheetName + "页未识别到市州行,跳过自动回填");
            return 0;
        }
        List<Integer> starts = new ArrayList<>(cityStart.values());
        int filled = 0;
        List<String> missingMonths = new ArrayList<>();
        for (int m = 1; m <= mMax; m++) {
            if (!monthly.containsKey(m)) missingMonths.add(m + "月");
        }
        if (!missingMonths.isEmpty()) {
            problems.add(sheetName + "缺 " + year + " å¹´ " + String.join("、", missingMonths) + " æºæ•°æ®ï¼ˆæœªå›žå¡«ï¼‰");
        }
        for (int i = 0; i < starts.size(); i++) {
            String city = findCityByStart(sh, starts.get(i));
            if (city == null) continue;
            int end = (i + 1 < starts.size()) ? starts.get(i + 1) - 1 : sh.getLastRowNum();
            // 2) è¡Œå†…各指标所在行(B åˆ—文案 â†’ æŒ‡æ ‡ä½ 0..3)
            int[] metricRow = new int[]{-1, -1, -1, -1};
            for (int r = starts.get(i); r <= end; r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                int idx = metricIndexOf(text(row.getCell(1)));
                if (idx >= 0 && metricRow[idx] < 0) metricRow[idx] = r;
            }
            if (metricRow[0] < 0 && metricRow[1] < 0) continue;
            for (int m = 1; m <= mMax; m++) {
                Map<String, double[]> mm = monthly.get(m);
                if (mm == null) continue;
                double[] arr = mm.get(city);
                if (arr == null || (arr[0] == 0.0 && arr[1] == 0.0)) continue;
                int col = FIRST_MONTH_COL + 2 * (m - 1);
                for (int idx = 0; idx < 4; idx++) {
                    if (metricRow[idx] < 0) continue;
                    double val = arr[idx];
                    if (val == 0.0) continue;
                    filled += setNumeric(sh, metricRow[idx], col, val);
                }
            }
        }
        return filled;
    }
    /** è½¨é“轮渡页(轨道=武汉/黄石;轮渡仅武汉)专用回填 */
    private int fillTrackFerry(XSSFWorkbook wb, Map<Integer, Map<String, double[]>> monthly,
                               int year, int keepMonths, List<String> problems) {
        String sheetName = "轨道、轮渡";
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) return 0;
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) return 0;
        // è½¨é“块与轮渡块标题行(列 A æ–‡æ¡ˆï¼‰
        int trackTitle = -1, ferryTitle = -1;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            String a = text(sh.getRow(r) == null ? null : sh.getRow(r).getCell(0));
            if (a == null) continue;
            if (trackTitle < 0 && a.startsWith("轨道客运量")) trackTitle = r;
            else if (ferryTitle < 0 && a.startsWith("轮渡客运量")) ferryTitle = r;
        }
        if (trackTitle < 0 || ferryTitle < 0) {
            problems.add(sheetName + "页未识别到轨道/轮渡块标题,跳过自动回填");
            return 0;
        }
        int filled = 0;
        filled += fillFerryBlock(sh, monthly, trackTitle + 3, ferryTitle - 1, 0, mMax, problems, sheetName);
        filled += fillFerryBlock(sh, monthly, ferryTitle + 3, sh.getLastRowNum(), 2, mMax, problems, sheetName);
        return filled;
    }
    /** å¡«å……一段“标题行后数据区”:base ä¸ºè¯¥å—指标位基(轨道 0 / è½®æ¸¡ 2);block å†…
     *  æ¯å¸‚å·ž 2 è¡Œï¼šå®¢è¿é‡è¡Œï¼ˆåˆ— A=市州名)、旅客周转量行(A ç©ºï¼ŒB å«â€œå‘¨è½¬é‡â€ï¼‰ */
    private int fillFerryBlock(Sheet sh, Map<Integer, Map<String, double[]>> monthly,
                               int start, int end, int base, int mMax,
                               List<String> problems, String sheetName) {
        int filled = 0;
        // æ”¶é›†å¸‚州客运量行
        Map<String, Integer> paxRow = new LinkedHashMap<>();
        for (int r = start; r <= end; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = cityOf(row.getCell(0));
            if (city != null) paxRow.put(city, r);
        }
        for (Map.Entry<String, Integer> e : paxRow.entrySet()) {
            String city = e.getKey();
            int rp = e.getValue();
            int rt = findTurnoverRow(sh, rp + 1, Math.min(end, rp + 6));
            if (rt < 0) continue;
            for (int m = 1; m <= mMax; m++) {
                Map<String, double[]> mm = monthly.get(m);
                if (mm == null) continue;
                double[] arr = mm.get(city);
                if (arr == null) continue;
                int col = FIRST_MONTH_COL + 2 * (m - 1);
                double pax = arr[base];
                double turn = arr[base + 1];
                if (pax != 0.0) filled += setNumeric(sh, rp, col, pax);
                if (turn != 0.0) filled += setNumeric(sh, rt, col, turn);
            }
        }
        return filled;
    }
    private int findTurnoverRow(Sheet sh, int from, int to) {
        for (int r = from; r <= to; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = text(row.getCell(0));
            if (a != null && !a.trim().isEmpty()) return -1; // è¿›å…¥ä¸‹ä¸€å¸‚州块
            String b = text(row.getCell(1));
            if (b != null && b.contains("周转量")) return r;
        }
        return -1;
    }
    /** å†™æ•°å€¼ï¼šæ ¼ä¸å­˜åœ¨åˆ™å»ºæ ¼å¹¶å¤åˆ¶åŒè¡Œ 1 æœˆåˆ—样式;返回 1 */
    private int setNumeric(Sheet sh, int r, int col, double val) {
        Row row = sh.getRow(r);
        if (row == null) row = sh.createRow(r);
        Cell c = row.getCell(col);
        if (c == null) {
            c = row.createCell(col);
            Cell ref = row.getCell(FIRST_MONTH_COL);
            if (ref != null && ref.getCellStyle() != null) c.setCellStyle(ref.getCellStyle());
        }
        c.setCellValue(val);
        return 1;
    }
    /** è¡Œ B åˆ—文案 â†’ æŒ‡æ ‡ä½ï¼š0 å®¢è¿é‡ / 1 æ—…客周转量 / 2 å…¶ä¸­åŸŽå¸‚内客运量或其中个体客运量 / 3 å¯¹åº”周转量 */
    private int metricIndexOf(String b) {
        if (b == null) return -1;
        if (b.contains("其中个体旅客周转量") || b.contains("其中城市内旅客周转量")) return 3;
        if (b.contains("其中个体客运量") || b.contains("其中城市内客运量")) return 2;
        if (b.contains("旅客周转量")) return 1;
        if (b.contains("客运量")) return 0;
        return -1;
    }
    /** åˆ— A æ–‡æ¡ˆ â†’ è§„范市州名(湖北省/全省、非市州行返回 null) */
    private String cityOf(Cell a) {
        String t = text(a);
        if (t == null || t.trim().isEmpty()) return null;
        String norm = RegionUtil.normalizeCityName(t.trim());
        return RegionUtil.cityList().contains(norm) ? norm : null;
    }
    private String findCityByStart(Sheet sh, int startRow) {
        Row row = sh.getRow(startRow);
        return row == null ? null : cityOf(row.getCell(0));
    }
    /** æœˆä»½å®¹é‡ï¼šåœ¨è¡¨å¤´ï¼ˆå‰ 5 è¡Œï¼‰æ‰¾â€œ2026…累计”列,容量=(累计列-1 æœˆåˆ—)/2 */
    private int monthColumnCapacity(Sheet sh) {
        for (int r = 0; r <= 4 && r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (Cell c : row) {
                if (c == null) continue;
                String t = text(c);
                if (t != null && t.contains("2026") && t.contains("累计") && c.getColumnIndex() > FIRST_MONTH_COL) {
                    return (c.getColumnIndex() - FIRST_MONTH_COL) / 2;
                }
            }
        }
        return 7;
    }
    private String text(Cell c) {
        if (c == null) return null;
        switch (c.getCellType()) {
            case STRING: return c.getStringCellValue();
            case NUMERIC: return Double.toString(c.getNumericCellValue());
            default: return null;
        }
    }
}
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -29,6 +29,9 @@
import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper;
import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
import com.trafficaudit.reportexport.calc.MidCalc;
import com.trafficaudit.reportexport.calc.PaxCalc;
import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
@@ -92,6 +95,8 @@
    private CityBusMonthlyMapper cityBusMapper;
    @Resource
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private SummaryWorkbookFiller summaryWorkbookFiller;
    @Resource
    private AuditResultMapper auditResultMapper;
    @Resource
@@ -3340,6 +3345,10 @@
            }
            dynamicSummaryYear(wb, year);
            cleanSummarySheetPresentation(wb); // å–消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据)
            int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
            List<String> dbProblems = new ArrayList<>();
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, year, effMonth, dbProblems);
            log.info("汇总工作簿数据驱动回填完成:写入台账页当月值格数={},缺数提示={}", dbFilled, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
traffic-audit-web/src/views/AuditResult.vue
@@ -50,7 +50,9 @@
          <div class="sev-card sev-pending"><b>{{ severityStats.pending }}</b> å¾…处理</div>
          <div class="sev-card sev-done"><b>{{ severityStats.done }}</b> å·²å¤„理</div>
        </div>
        <el-form v-if="!focusMode" :inline="true" style="margin-top:8px;flex:none">
        <div v-if="!focusMode" class="audit-filter-bar">
        <el-form :inline="true">
          <el-form-item label="筛选"></el-form-item>
          <el-form-item label="企业名称">
            <el-input v-model="enterpriseFilter" placeholder="输入企业名称关键字" clearable style="width:200px" @keyup.enter.native="queryResults"></el-input>
          </el-form-item>
@@ -59,11 +61,12 @@
              <el-option v-for="rn in ruleOptions" :key="rn" :label="rn" :value="rn"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="问题类型">
          <el-form-item label="问题范围">
            <el-radio-group v-model="issueScope" size="small" @change="updateBatchSelection">
              <el-radio-button label="verify">核实型</el-radio-button>
              <el-radio-button label="all">全部</el-radio-button>
              <el-radio-button label="verify">仅需核实</el-radio-button>
              <el-radio-button label="all">含提醒型</el-radio-button>
            </el-radio-group>
            <span class="hint">提醒型仅提示变化、不需处理,导出默认不含</span>
          </el-form-item>
          <el-form-item label="状态">
            <el-select v-model="statusScope" style="width:110px" @change="updateBatchSelection">
@@ -87,9 +90,11 @@
            <el-button size="small" plain @click="statsCollapsed = !statsCollapsed">{{ statsCollapsed ? '展开' : '收起' }}规则统计</el-button>
          </el-form-item>
          <el-form-item>
            <span style="color:#909399;font-size:12px">共 {{ results.length }} æ¡é—®é¢˜ï¼Œ{{ mergedResults.length }} å®¶ä¼ä¸š</span>
            <span style="color:#303133;font-size:13px;font-weight:600">当前筛选 {{ filteredResults.length }} æ¡ / {{ mergedResults.length }} å®¶ä¼ä¸š</span>
            <span style="color:#909399;font-size:12px;margin-left:8px">(共 {{ results.length }} æ¡é—®é¢˜ï¼‰</span>
          </el-form-item>
        </el-form>
        </div>
        <div v-if="ruleStats.length && !statsCollapsed && !focusMode" class="rule-stats" style="flex:none">
          <div class="stats-cards">
            <div v-for="rs in sortedRuleStats" :key="rs.ruleId" class="stat-card"
@@ -354,7 +359,7 @@
  data() {
    return {
      period: '', reportType: 'H2032', loading: false, marking: false, results: [], ruleStats: [], statsCollapsed: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'detail', issueScope: 'verify', statusScope: 'all', batchSelection: [], focusMode: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'merged', issueScope: 'verify', statusScope: 'all', batchSelection: [], focusMode: false,
      explainDrafts: {}, savingExplainId: null, expandedExplain: null,
      monthVisible: false, monthLoading: false, monthType: '', monthPeriod: '', monthKeyword: '', monthRecords: [], monthColumns: []
    }
@@ -367,7 +372,7 @@
    },
    severityStats() {
      const s = { red: 0, yellow: 0, pending: 0, done: 0 }
      this.results.forEach(r => {
      this.filteredResults.forEach(r => {
        if (r.alertLevel === 'RED') s.red++
        else if (r.alertLevel === 'YELLOW') s.yellow++
        if (r.status === 'PENDING') s.pending++
@@ -383,9 +388,6 @@
    someSelected() {
      const list = this.filteredResults
      return list.some(r => r._selected) && !this.allSelected
    },
    isReminderRow(r) {
      return !!(r && r.ruleCode === 'RULE_004')
    },
    filteredResults() {
      const kw = (this.enterpriseFilter || '').trim()
@@ -424,6 +426,9 @@
    }
  },
  methods: {
    isReminderRow(r) {
      return !!(r && r.ruleCode === 'RULE_004')
    },
    dataType(t) { return DATA_TYPE_MAP[t] || '' },
    explainType(t) { return EXPLAIN_TYPES.indexOf(t) >= 0 },
    openMonthReport(row) {
@@ -769,4 +774,9 @@
.mr-sec-tab th { background: #F5F7FA; font-weight: 600; }
.mr-note { margin: 6px 12px 12px; background: #FDF6EC; border: 1px solid #FAECD8; border-radius: 4px; padding: 8px 10px; font-size: 12px; color: #8a6d3b; white-space: pre-wrap; word-break: break-all; }
.mr-note-line { margin: 2px 0; }
.audit-filter-bar { background:#FAFAFA; border:1px solid #EBEEF5; border-radius:8px; padding:2px 12px 0; margin-top:8px; flex:none; }
.audit-filter-bar .el-form-item { margin-bottom:6px; }
.audit-filter-bar .el-form-item__label { color:#606266; font-weight:600; }
.hint { color:#909399; font-size:12px; margin-left:6px; white-space:nowrap; }
</style>
traffic-audit-web/src/views/DataView.vue
@@ -16,32 +16,23 @@
      </el-aside>
      <el-main>
        <h3>导入数据查看</h3>
        <div class="type-row">
          <span class="type-label">数据类型</span>
        <div class="toolbar">
          <span class="toolbar-label">数据类型</span>
          <el-radio-group v-model="activeModule" size="small" @change="onModuleChange">
            <el-radio-button v-for="g in modules" :key="g.key" :label="g.key">{{ g.name }}</el-radio-button>
          </el-radio-group>
          <el-select v-model="type" size="small" style="width:300px" placeholder="选择数据类型" @change="onTypeSelect">
          <el-select v-model="type" size="small" style="width:250px" placeholder="选择数据类型" @change="onTypeSelect">
            <el-option-group :label="activeTypes.name">
              <el-option v-for="t in activeTypes.types" :key="t.value" :label="t.label" :value="t.value"></el-option>
            </el-option-group>
          </el-select>
          <span class="toolbar-sep"></span>
          <el-date-picker v-model="period" type="month" placeholder="报表期(全部)" value-format="yyyy-MM" clearable style="width:150px" @change="doQuery"></el-date-picker>
          <el-input v-model="keyword" placeholder="企业/市州名称" clearable style="width:180px" @keyup.enter.native="doQuery"></el-input>
          <el-button type="primary" size="small" @click="doQuery">查询</el-button>
          <el-button size="small" icon="el-icon-full-screen" @click="toggleFullscreen">全屏查看</el-button>
        </div>
        <el-form :inline="true" class="filter-row">
          <el-form-item label="报表期">
            <el-date-picker v-model="period" type="month" placeholder="全部" value-format="yyyy-MM" clearable style="width:140px" @change="loadData"></el-date-picker>
          </el-form-item>
          <el-form-item label="关键字">
            <el-input v-model="keyword" placeholder="企业/市州名称" clearable style="width:200px" @keyup.enter.native="loadData"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="loadData">查询</el-button>
            <el-button type="success" icon="el-icon-full-screen" @click="toggleFullscreen">全屏查看</el-button>
          </el-form-item>
        </el-form>
        <div style="color:#909399;font-size:12px;margin-bottom:6px">
          å…± {{ total }} æ¡è®°å½•,当前第 {{ page }} é¡µ
        </div>
        <div class="load-hint">共 {{ total }} æ¡è®°å½•,已加载 {{ records.length }} æ¡</div>
        <div class="table-wrap" ref="tableWrap">
          <div class="table-area">
            <el-table :data="records" border v-loading="loading" size="small" style="width:100%" height="100%">
@@ -52,12 +43,8 @@
              </el-table-column>
            </el-table>
          </div>
          <el-pagination style="margin-top:15px;text-align:right"
            background layout="total, prev, pager, next, sizes"
            :total="total" :current-page.sync="page" :page-size.sync="size"
            :page-sizes="[20, 50, 100]" @current-change="loadData" @size-change="loadData">
          </el-pagination>
        </div>
      </el-main>
    </el-container>
  </el-container>
@@ -99,7 +86,7 @@
      period: '',
      keyword: '',
      page: 1,
      size: 20,
      size: 50,
      total: 0,
      records: [],
      columns: [],
@@ -126,9 +113,14 @@
      if (!g.types.some(t => t.value === this.type)) {
        this.type = g.types[0].value
      }
      this.doQuery()
    },
    onTypeSelect() { this.doQuery() },
    doQuery() {
      this.page = 1
      this.records = []
      this.loadData()
    },
    onTypeSelect() { this.loadData() },
    loadData() {
      this.loading = true
      this.loadingMore = false
@@ -262,43 +254,14 @@
.type-row { margin-bottom: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.type-label { color: #606266; font-weight: 600; }
.filter-row { margin-bottom: 4px; }
#app .table-wrap {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 330px);
  min-height: 300px;
}
#app .table-area {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
#app .table-area .el-table {
  width: 100%;
  height: 100%;
}
#app .table-wrap .el-pagination {
  flex: none;
  margin-top: 8px;
}
#app .table-area:fullscreen {
  padding: 20px;
  background: #fff;
  overflow: auto;
}
#app .table-wrap:fullscreen {
  padding: 20px;
  background: #fff;
  overflow: auto;
  display: flex;
  flex-direction: column;
  height: 100vh;
}
#app .table-wrap:fullscreen .table-area {
  flex: 1 1 auto;
  min-height: 0;
}
#app .table-wrap:fullscreen .el-pagination {
  flex: none;
}
#app .toolbar { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:8px 12px; background:#FAFAFA; border:1px solid #EBEEF5; border-radius:8px; margin-bottom:8px; }
#app .toolbar-label { color:#606266; font-weight:600; white-space:nowrap; }
#app .toolbar-sep { width:1px; height:22px; background:#DCDFE6; margin:0 2px; }
#app .load-hint { color:#909399; font-size:12px; margin-bottom:6px; }
#app .table-wrap { display:flex; flex-direction:column; height:calc(100vh - 250px); min-height:300px; }
#app .table-area { flex:1 1 auto; min-height:0; overflow:hidden; }
#app .table-area .el-table { width:100%; height:100%; }
#app .table-area:fullscreen { padding:20px; background:#fff; overflow:auto; }
#app .table-wrap:fullscreen { padding:20px; background:#fff; overflow:auto; display:flex; flex-direction:column; height:100vh; }
#app .table-wrap:fullscreen .table-area { flex:1 1 auto; min-height:0; }
</style>
traffic-audit-web/src/views/ReportExport.vue
@@ -112,7 +112,25 @@
                <span v-if="!it.ready" class="cov-bad" style="margin-left:10px">{{ it.note }}</span>
                <span v-if="it.ready && it.audited === false" class="cov-bad" style="margin-left:10px">未审核</span>
              </div>
              <table class="preview-summary" v-if="it.summary && it.summary.length">
              <table class="preview-summary wb-table" v-if="it.key === 'summaryWorkbook' && it.summary && it.summary.length">
                <thead>
                  <tr><th style="width:44%">指标</th><th style="width:30%">数据</th><th>同比</th></tr>
                </thead>
                <tbody>
                  <template v-for="(sec, si) in wbRows(it.summary)">
                    <tr v-for="(row, ri) in sec.rows" :key="si + '-' + ri">
                      <td v-if="ri === 0" :rowspan="sec.rows.length" class="wb-sec">{{ sec.name }}</td>
                      <td class="wb-label">{{ row.label }}</td>
                      <td class="wb-value">{{ row.value === null ? '—' : fmtVal(row.value) }}</td>
                      <td class="wb-yoy" :class="{ 'yoy-empty': row.yoy === null }">{{ row.yoy === null ? '—' : row.yoy }}</td>
                    </tr>
                  </template>
                </tbody>
              </table>
              <div v-if="it.key === 'summaryWorkbook' && wbMissing(it.summary)" class="wb-note">
                éƒ¨åˆ†æŒ‡æ ‡ç¼ºå°‘上年同期({{ wbPrevYear() }})基期数据,同比暂未计算
              </div>
              <table class="preview-summary" v-else-if="it.summary && it.summary.length">
                <tbody>
                  <tr v-for="(s, i) in it.summary" :key="i">
                    <td>{{ s.label }}</td>
@@ -380,6 +398,65 @@
      }
      return String(v)
    },
    wbRows(summary) {
      const get = (key) => {
        const hit = (summary || []).find(x => x.label === key)
        return hit && hit.value !== null && hit.value !== undefined && hit.value !== '' ? hit.value : null
      }
      const pct = (key) => {
        const v = get(key)
        return v === null ? null : this.fmtVal(v) + '%'
      }
      const mk = (defs) => defs.map(d => ({
        label: d[0],
        value: get(d[1]),
        yoy: d[2] ? pct(d[2]) : null
      }))
      return [
        {
          name: '道路货运',
          rows: mk([
            ['当月货运量(万吨)', '全省货运量·当月'],
            ['当月货物周转量(万吨)', '全省货物周转量·当月', '全省货物周转量·当月同比'],
            ['累计货运量(万吨)', '全省货运量·累计'],
            ['累计货物周转量(万吨公里)', '全省货物周转量·累计', '全省货物周转量·累计同比']
          ])
        },
        {
          name: '城市客运',
          rows: mk([
            ['当月城市客运量:', '城市客运量·当月'],
            ['当月城市客运周转量', '城市客运周转量·当月'],
            ['累计城市客运量:', '城市客运量·累计'],
            ['累计城市客运周转量', '城市客运周转量·累计']
          ])
        },
        {
          name: '中口径',
          rows: mk([
            ['当月中口径客运量', '中口径客运量·当月'],
            ['当月中口径周转量', '中口径周转量·当月'],
            ['累计中口径客运量', '中口径客运量·累计'],
            ['累计中口径周转量', '中口径周转量·累计']
          ])
        }
      ]
    },
    wbMissing(summary) {
      let total = 0
      let have = 0
      ;(summary || []).forEach(x => {
        if (String(x.label).indexOf('同比') >= 0) return
        total++
        if (x.value !== null && x.value !== undefined && x.value !== '') have++
      })
      return total > 0 && have < total
    },
    wbPrevYear() {
      const m = /^(\d{4})-(\d{2})$/.exec(this.previewPeriod || '')
      if (!m) return '上年同期'
      return (Number(m[1]) - 1) + '-' + m[2]
    },
    coverageText(it) {
      const cov = it.monthCoverage || []
      if (!cov.length) return '无数据'
@@ -494,4 +571,11 @@
@media (max-width: 900px) {
  .panel-head { flex-direction: column; align-items: flex-start; }
}
.wb-table th { background:#f5f7fa; }
.wb-sec { background:#F7F9FC; font-weight:600; text-align:center; color:#303133; }
.wb-label { color:#303133; }
.wb-value { text-align:right; font-variant-numeric: tabular-nums; }
.wb-yoy { text-align:right; color:#606266; }
.wb-yoy.yoy-empty { color:#C0C4CC; }
.wb-note { color:#E6A23C; font-size:12px; margin:2px 0 8px; }
</style>