fix: 8条整改落地——数据查看滚动加载/规则阈值联动通用化/旅客运政导入兼容.xls/审核月报弹窗删企业解释列/汇总大表导出无筛选自适应/报表Tab重组/筛选栏与月份年份顺序 + 测试报告与工作日结
1个文件已删除
8个文件已修改
2个文件已添加
539 ■■■■■ 已修改文件
docs/公路旅客/模板_公路旅客运政车辆数.xls 补丁 | 查看 | 原始文档 | blame | 历史
docs/公路旅客/模板_公路旅客运政车辆数.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_整改8项回归验证_2026-09-08.md 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-08.md 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/rulemanage/controller/RuleManageController.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/AuditResult.vue 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataView.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹«Â·ÂÿÍ/Ä£°å_¹«Â·ÂÿÍÔËÕþ³µÁ¾Êý.xls
Binary files differ
docs/¹«Â·ÂÿÍ/Ä£°å_¹«Â·ÂÿÍÔËÕþ³µÁ¾Êý.xlsx
Binary files differ
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_Õû¸Ä8Ïî»Ø¹éÑéÖ¤_2026-09-08.md
New file
@@ -0,0 +1,40 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_整改8项回归验证_2026-09-08
## åŠŸèƒ½è¯´æ˜Ž
针对用户 2026-09-08 æå‡ºçš„ 8 æ¡ç•Œé¢ä¸ŽåŠŸèƒ½æ•´æ”¹ï¼ˆâ€œä¸Šæ¬¡è¯´æ”¹çš„æ²¡æ”¹â€ï¼‰é€æ¡è½åœ°å¹¶å›žå½’ï¼š
1. æ•°æ®æŸ¥çœ‹é¡µæ»šåŠ¨åˆ°åº•è‡ªåŠ¨åŠ è½½æ›´å¤šæ•°æ®è¡Œï¼ˆä¸å†åªæ˜¾ç¤ºä¸€é¡µï¼‰ï¼›
2. å®¡æ ¸è§„则阈值改动后,规则名称/规则说明中的百分比随阈值联动(如 Â±10%→±20%→±30%→回退),并对全部规则生效、持久化;
3. å…¬è·¯æ—…客“运政车辆数”导入兼容新版运政库导出文件(.xls,表头 OWNERNAME/总车辆数/客位数,无企业代码列),同时兼容旧中文四列表头;
4. å®¡æ ¸ç»“果:删除“企业解释”列及解释输入框;点击“数据”弹出该企业该报表期的月报样式查看;
5. ç”Ÿæˆæ±‡æ€»å¤§è¡¨ï¼ˆæ•´æœ¬ï¼‰ï¼šå¯¼å‡ºæ—¶å–消全部页筛选(班线包车等页不再像缺数据)、还原隐藏行、按数据自动加宽列宽、数值保留两位小数显示(不丢失全精度)、公式保留且打开自动重算;
6. æŠ¥è¡¨ç”Ÿæˆé¡µï¼šåŸŽå¸‚客运报表标签下的 tag åˆ é™¤ã€åç§°ä¸Žå…¶å®ƒä¸€è‡´ï¼›â€œé“路运输量汇总表(整本)”移入首个独立 Tab“汇总大表”;
7. å®¡æ ¸ç»“果页筛选栏顺序:报表期在报表类型左边;
8. æŠ¥è¡¨ç”Ÿæˆæœˆä»½ä¸‹æ‹‰ï¼šé»˜è®¤æœ€å…ˆå±•示当前年份(2026 å¹´ 1–12 æœˆï¼‰ï¼Œå†å±•示 2025 å¹´ä¸Ž 2027 å¹´ã€‚
## æµ‹è¯•环境
- æœ¬æœºï¼šWindows,后端 Spring Boot :8090,前端 Vue2 :8080(页面经重新编译加载最新代码,后端已重启)。
- ç™»å½•:admin(app.captcha.required=false æµ‹è¯•模式)。
- æ•°æ®åº“:本机 traffic-audit å¼€å‘库(含 2026-08 è´§è¿æœˆæŠ¥ 13056 æ¡ç­‰æ—¢æœ‰æ•°æ®ï¼‰ã€‚
- æ—¥æœŸï¼š2026-09-08。
## ç”¨ä¾‹è¡¨ï¼ˆè¾“å…¥ / é¢„期 / å®žé™… / ç»“论)
| # | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|------|------|------|------|
| 1 | æ•°æ®æŸ¥çœ‹é¡µï¼ˆH203-2,共 13056 æ¡ï¼‰ï¼Œå°†è¡¨æ ¼ç«–向滚动到底 | è‡ªåŠ¨è¿½åŠ ä¸‹ä¸€é¡µï¼Œè¡Œæ•° 20→40,页签从“第 1 é¡µâ€å˜â€œç¬¬ 2 é¡µâ€ | åˆå§‹ 20 è¡Œï¼ˆå®¹å™¨é«˜ 299、内容高 796);滚动后 40 è¡Œï¼ˆå†…容高 1592,scrollTop 496.8),“共 13056 æ¡è®°å½•,当前第 2 é¡µâ€ | é€šè¿‡ |
| 2 | è§„则管理:将 P005 é˜ˆå€¼ 20→30→20 åå¤ä¿å­˜ | åç§°/说明中的 Â±N% éšåŠ¨ï¼ˆÂ±20%↔±30%),其它无关数字不动,重开页面仍一致 | P005 ä»¥åŠ B001(增速)、E005(±%)、RULE_003(高出%)、T001(差距%)均实测通过,显式(0.2→0.1)与比值(1.3→1.5)口径均正确并持久化 | é€šè¿‡ |
| 3 | å¯¼å…¥ 2026-08 å…¬è·¯æ—…客运政车辆数(新 .xls:OWNERNAME/总车辆数/客位数) | 750 å®¶ä¼ä¸šå…¨éƒ¨å…¥åº“、抽样数值与源文件一致;旧中文表头文件仍可导入 | æ–°æ¨¡æ¿æ”¾å…¥ docs/公路旅客/模板_公路旅客运政车辆数.xls å¯¼å…¥æˆåŠŸï¼›æŠ½æŸ¥ç§­å½’åŽ¿å¿—å°š 5/35、安陆市涢安 26/668 ä¸Žæºæ–‡ä»¶ä¸€è‡´ï¼›ä¼ä¸šä»£ç åˆ—为空属正常(新模板无此列) | é€šè¿‡ |
| 4 | å®¡æ ¸ç»“果页:①检查行内操作列;②点击“数据” | â‘  æ— â€œä¼ä¸šè§£é‡Šâ€åˆ—/输入框,只有 æ•°æ®/确认/忽略;② å¼¹å‡ºè¯¥ä¼ä¸šè¯¥æŠ¥è¡¨æœŸçš„æœˆæŠ¥æ ·å¼ï¼ˆæ ‡é¢˜=企业名+报表期 æœˆæŠ¥æ•°æ®ï¼›åˆ†åŒº=运力情况→运输生产情况→平均指标→填报人信息;底部关闭可用) | å®žæµ‹æ—  4 åˆ—“企业解释”残留;“数据”弹窗内容与分区顺序符合预期,关闭正常 | é€šè¿‡ |
| 5 | å¯¼å‡ºâ€œé“路运输量汇总表(整本)”2026-08 å¹¶è„šæœ¬æ ¡éªŒ 19 ä¸ª sheet | å…¨éƒ¨é¡µæ—  autoFilter、无隐藏行;数值单元格保留两位小数显示与全精度;系统求和/同比公式保留;打开即重算 | æ ¡éªŒæ–‡ä»¶ï¼šdocs/生成汇总大表/输出/生成_道路运输量汇总表_2026-08_清理验证.xlsx;19 é¡µ filter=None、hidden=[],C4/D4 ç­‰é•¿æ±‚和与同比公式保留 | é€šè¿‡ |
| 6 | æŠ¥è¡¨ç”Ÿæˆé¡µæ£€æŸ¥ Tab ä¸Žæ ‡ç­¾ | é¡ºåºï¼šæ±‡æ€»å¤§è¡¨â†’货运报表→旅客报表→城市客运报表→投资报表→能耗报表;城市客运无 tag;整本从城市客运移出 | æ±‡æ€»å¤§è¡¨ç‹¬ç«‹ä¸ºé¦–组;城市客运标签无“公交/出租/轨道轮渡/汇总”tag | é€šè¿‡ |
| 7 | å®¡æ ¸ç»“果页筛选栏 | æŠ¥è¡¨æœŸåœ¨æŠ¥è¡¨ç±»åž‹å·¦è¾¹ | ç­›é€‰æ é¡ºåºä¸ºâ€œæŠ¥è¡¨æœŸ â†’ æŠ¥è¡¨ç±»åž‹ â†’ æ‰§è¡Œå®¡æ ¸æŒ‰é’®â€ | é€šè¿‡ |
| 8 | æŠ¥è¡¨ç”Ÿæˆé¡µç‚¹å¼€æœˆä»½ä¸‹æ‹‰ | é»˜è®¤å±•开当前年份 2026 å¹´ 1–12 æœˆï¼Œå…¶æ¬¡ 2025 å¹´ï¼Œå†æ¬¡ 2027 å¹´ | å®žæµ‹ä¸‹æ‹‰é¡ºåºï¼š2026å¹´1月…2026å¹´12月、2025å¹´1月…2025å¹´12月、2027å¹´1月…2027å¹´12月 | é€šè¿‡ |
## å‘现的问题与处置
- åŽ†å²é—®é¢˜ï¼šè§„åˆ™åç§°/说明里除阈值外还混有与阈值无关的数字(如运距区间 [10,3000]),旧实现会误改。本轮改为“按旧阈值匹配对得上的百分比片段才替换”,并对读列表做幂等自愈,实测未再误伤无关数字。
- åŽ†å²é—®é¢˜ï¼šæ±‡æ€»å¤§è¡¨æ¯ç‰ˆæœ¬èº«å¸¦ç­›é€‰ä¸Žéšè—è¡Œï¼Œæ—§å¯¼å‡ºä¼šåŽŸæ ·å¸¦å‡ºï¼Œçœ‹èµ·æ¥åƒç¼ºæ•°æ®ã€‚æœ¬è½®å¯¼å‡ºå‰ç»Ÿä¸€æ¸…ç†å¹¶éªŒè¯ 19 é¡µå…¨å¹²å‡€ã€‚
- åŽ†å²é—®é¢˜ï¼šå…¬è·¯æ—…å®¢è¿æ”¿å¯¼å…¥æ­¤å‰åªè®¤â€œä¼ä¸šä»£ç /企业名称/车辆数/载客位数”中文四列,同事下载的新模板少两列导致导不进去。本轮按别名+表头大写兼容新旧两种结构。
- å¤‡æ³¨ï¼šdocs/审核界面月报模板/HTML_审核结果_Aug_2026.html ä¸ºæœ¬æ¬¡å®¡æ ¸ç»“果页与弹窗验证留档。
## ç»“论
8 æ¡æ•´æ”¹å…¨éƒ¨å®Œæˆå¹¶é€šè¿‡å›žå½’验证;相关改动见代码提交(DataImportService/ReportExportService/RuleManageController/AuditResult.vue/DataView.vue/ReportExport.vue/DataImport.vue)。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-08.md
@@ -74,3 +74,20 @@
- `mvn` ç¼–译通过(前一交接已完成);前端热编译 3 æ¬¡æˆåŠŸï¼ˆ15:19/15:24)。
### é—留与下一步
- 8 æœˆå½“月数据导入后 M2/M3 å¡«å€¼å¤éªŒï¼›ä¸´æ—¶è„šæœ¬ `_verify_*.py/.sql`、`_do_patch1.py` ä¸åˆ ä¸å…¥åº“。
---
## è¡¥å……(2026-09-08 å‚æ™šï¼ŒåŒä¸€ä¼šè¯ç»­ï¼‰ï¼šç”¨æˆ· 8 æ¡æ•´æ”¹å…¨éƒ¨è½åœ°å¹¶å›žå½’
### ä»Šæ—¥æ–°å¢ž
1. **数据查看滚动加载(真机复测)**:表格固定高度、滚动容器绑定到 el-table å†…部 body-wrapper;实测 20→40 è¡Œè¿½åŠ ã€é¡µç­¾ç¬¬1页→第2页(13056 æ¡æ€»æ•°ä¸å˜ï¼‰ã€‚
2. **规则阈值联动普及到全部规则**:`RuleManageController.syncThresholdText` æ”¹ä¸ºæŒ‰â€œæ—§é˜ˆå€¼å¯¹åº”片段”替换(支持 Â±N%、>N%、0.5→50%、1.3→高出30% ç­‰å£å¾„),读列表幂等自愈;实测 P005/B001/E005/RULE_003/T001 æ˜¾å¼ä¸Žæ¯”值口径均正确并持久化,说明中与阈值无关的数字不再被误改。
3. **公路旅客运政车辆数导入兼容新模板**:同事下载的运政库导出 .xls(表头 OWNERNAME/总车辆数/客位数、无企业代码列)与旧中文四列均可导入;`docs/公路旅客/模板_公路旅客运政车辆数.xlsx` æ¢ä¸º `.xls`;2026-08 å…± 750 å®¶å…¥åº“、抽样与源文件一致。
4. **审核结果“数据”改月报样式弹窗 + åˆ é™¤ä¼ä¸šè§£é‡Šåˆ—**:行内仅剩 æ•°æ®/确认/忽略;弹窗按 è¿åŠ›æƒ…å†µâ†’è¿è¾“ç”Ÿäº§æƒ…å†µâ†’å¹³å‡æŒ‡æ ‡â†’å¡«æŠ¥äººä¿¡æ¯ åˆ†åŒºï¼Œæ ‡é¢˜=企业名+报表期。
5. **汇总大表导出清理**:`cleanSummarySheetPresentation` å–消各页筛选并还原隐藏行(班线包车等页不再像缺数据),保留两位小数显示/全精度、公式与打开自动重算,按内容自动加宽列宽(只加宽不缩窄、跳过合并标题);19 é¡µå¯¼å‡ºè„šæœ¬æ ¡éªŒé€šè¿‡ã€‚
6. **报表生成 Tab é‡ç»„**:“汇总大表(整本)”独立为首个 Tab;城市客运报表去掉 tag、命名与其它一致。
7. **审核页筛选栏顺序**:报表期移到报表类型左边。
8. **月份下拉年份顺序**:默认当前年 2026 åœ¨æœ€å‰ï¼Œå…¶åŽ 2025、2027。
### æµ‹è¯•结果摘要
- è¯¦è§ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_整改8项回归验证_2026-09-08.md》,8/8 é€šè¿‡ï¼›å‰ç«¯å·²é‡æ–°ç¼–译、后端已重启加载新代码。
### é—留与下一步
- 8 æœˆå½“月数据 M2/M3 å¡«å€¼å¤éªŒï¼›9 æœˆèµ·çš„逐月导入→扩列→审核→生成完整链待业务数据到位后继续。
- æœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼ˆ_*.ps1/_*.sql/*.bak/docs.zip/生成产物xlsx ç­‰ï¼‰æœªå…¥åº“,待整理清理。
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -558,24 +558,30 @@
        }
    }
    /** å…¬è·¯æ—…客运政车辆数(独立模板:企业代码/企业名称/车辆数/载客位数) */
    /** å…¬è·¯æ—…客运政车辆数(兼容两种表头:① ä¼ä¸šä»£ç /企业名称/车辆数/载客位数;② è¿æ”¿åº“导出 OWNERNAME/总车辆数/客位数,无企业代码列) */
    public ImportResult importPassengerAuth(MultipartFile file, String period) throws Exception {
        List<PassengerAuthVehicle> authList = new ArrayList<>();
        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
            Sheet sheet = wb.getSheetAt(0);
            Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
            requireHeaderCols(sheet, "旅客运政车辆数", "企业名称", "车辆数", "载客位数");
            Map<String, Integer> colMap = buildHeaderMapCompat(sheet.getRow(0));
            Integer nameCol = firstColIdx(colMap, "企业名称", "OWNERNAME");
            Integer vehicleCol = firstColIdx(colMap, "车辆数", "总车辆数");
            Integer seatCol = firstColIdx(colMap, "载客位数", "客位数");
            Integer codeCol = firstColIdx(colMap, "企业代码");
            if (nameCol == null || vehicleCol == null || seatCol == null) {
                throw new RuntimeException("所选数据类型【旅客运政车辆数】与文件内容不符:表头需含【企业名称/车辆数/载客位数】或【OWNERNAME/总车辆数/客位数】,请确认是否选错了数据类型或使用了旧模板");
            }
            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                Row row = sheet.getRow(r);
                if (row == null) continue;
                String name = gs(row, colMap, "企业名称");
                String name = getString(row, nameCol);
                if (name == null || name.trim().isEmpty()) continue;
                PassengerAuthVehicle auth = new PassengerAuthVehicle();
                auth.setReportPeriod(period);
                auth.setEnterpriseCode(gs(row, colMap, "企业代码"));
                auth.setEnterpriseCode(codeCol == null ? null : getString(row, codeCol));
                auth.setEnterpriseName(name.trim());
                auth.setVehicleCount(gi(row, colMap, "车辆数"));
                auth.setSeatCount(gi(row, colMap, "载客位数"));
                auth.setVehicleCount(getInt(row, vehicleCol));
                auth.setSeatCount(getInt(row, seatCol));
                authList.add(auth);
            }
        } catch (Exception e) {
@@ -1396,6 +1402,33 @@
        return colMap;
    }
    /** å…¼å®¹è¡¨å¤´æ˜ å°„:去引号/去空白;纯英文表头统一大写(如 'OWNERNAME' â†’ OWNERNAME),便于新旧模板混用 */
    private Map<String, Integer> buildHeaderMapCompat(Row header) {
        Map<String, Integer> colMap = new HashMap<>();
        if (header == null) return colMap;
        for (Cell cell : header) {
            if (cell == null) continue;
            String h = FORMATTER.formatCellValue(cell).replace("'", "").replace("\"", "").trim();
            if (h.isEmpty()) continue;
            boolean ascii = true;
            for (int i = 0; i < h.length(); i++) {
                if (h.charAt(i) > 127) { ascii = false; break; }
            }
            if (ascii) h = h.toUpperCase();
            if (!colMap.containsKey(h)) colMap.put(h, cell.getColumnIndex());
        }
        return colMap;
    }
    /** æŒ‰åˆ«åé¡ºåºå–第一个存在的列号,全部缺失返回 null */
    private Integer firstColIdx(Map<String, Integer> colMap, String... aliases) {
        for (String a : aliases) {
            Integer idx = colMap.get(a);
            if (idx != null) return idx;
        }
        return null;
    }
    private int colIdx(Map<String, Integer> colMap, String header) {
        Integer idx = colMap.get(header);
        return idx == null ? -1 : idx;
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3232,7 +3232,9 @@
                log.info("汇总工作簿裁剪至 {} æœˆå®Œæˆï¼Œæ¸…空/改写单元格数={}", toMonth, trimmed);
            }
            dynamicSummaryYear(wb, year);
            applyTwoDecimalFormat(wb); // æ±‡æ€»å¤§è¡¨ä¿ç•™äººå·¥æ¯ç‰ˆåˆ—宽/版式:不做 autoFit(长公式会把列宽顶爆)
            cleanSummarySheetPresentation(wb); // å–消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据)
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            autoFitContentColumns(wb); // æŒ‰æ•°æ®è‡ªåŠ¨åŠ å®½åˆ—å®½ï¼ˆåªåŠ å®½ä¸ç¼©çª„ï¼Œè·³è¿‡åˆå¹¶æ ‡é¢˜æ ¼ï¼Œé˜²é•¿æ–‡æœ¬æŠŠå•åˆ—æ’‘çˆ†ï¼‰
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
            return toBytes(wb);
@@ -4800,6 +4802,26 @@
    }
    /** æ±‡æ€»è¡¨å¯¼å‡ºåŽæŒ‰å†…容自动加宽列宽(只加宽不缩窄;跳过合并单元格标题,防长标题把单列撑爆) */
    /** æ±‡æ€»å¤§è¡¨è¾“出前清理母版残留下来的筛选与行隐藏:筛选箭头/隐藏行会让整页看起来“缺数据” */
    private void cleanSummarySheetPresentation(org.apache.poi.ss.usermodel.Workbook wb) {
        if (wb == null) return;
        for (int s = 0; s < wb.getNumberOfSheets(); s++) {
            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
            if (sh == null) continue;
            if (sh instanceof org.apache.poi.xssf.usermodel.XSSFSheet) {
                org.apache.poi.xssf.usermodel.XSSFSheet xs = (org.apache.poi.xssf.usermodel.XSSFSheet) sh;
                if (xs.getCTWorksheet().isSetAutoFilter()) xs.getCTWorksheet().unsetAutoFilter();
                for (org.apache.poi.ss.usermodel.Row row : xs) {
                    if (row == null) continue;
                    org.apache.poi.xssf.usermodel.XSSFRow xr = (org.apache.poi.xssf.usermodel.XSSFRow) row;
                    if (xr.getCTRow().isSetHidden()) xr.getCTRow().unsetHidden();
                }
            } else {
                sh.setAutoFilter(null);
            }
        }
    }
    private void autoFitContentColumns(org.apache.poi.ss.usermodel.Workbook wb) {
        if (wb == null) return;
        org.apache.poi.ss.usermodel.DataFormatter dfmt = new org.apache.poi.ss.usermodel.DataFormatter();
traffic-audit-server/src/main/java/com/trafficaudit/rulemanage/controller/RuleManageController.java
@@ -55,29 +55,94 @@
        if (body.getThreshold() != null) rule.setThreshold(body.getThreshold());
        if (body.getAlertLevel() != null) rule.setAlertLevel(body.getAlertLevel());
        if (body.getDescription() != null) rule.setDescription(body.getDescription());
        // é˜ˆå€¼å˜åŒ–时同步规则名称与说明(名称/说明里的 Â±N% æŒ‰æ–°é˜ˆå€¼æ”¹å†™ï¼‰ï¼Œå¹¶éšä¿å­˜æŒä¹…化
        // é˜ˆå€¼å˜åŒ–时同步规则名称与说明(名称/说明里与旧阈值对应的 N% ç‰‡æ®µæŒ‰æ–°é˜ˆå€¼æ”¹å†™ï¼‰ï¼Œå¹¶éšä¿å­˜æŒä¹…化
        if (body.getThreshold() != null && !body.getThreshold().equals(oldThreshold)) {
            syncThresholdText(rule);
            syncThresholdText(rule, oldThreshold);
        }
        rule.setUpdatedAt(LocalDateTime.now());
        ruleMapper.updateById(rule);
        return Result.ok("已保存");
    }
    /** Â±N% å­—样匹配(名称/说明中随阈值动态变化的片段) */
    private static final Pattern PCT_TOKEN = Pattern.compile("±\\s*\\d+(?:\\.\\d+)?\\s*%");
    /** % å‰çš„æ•°å€¼ï¼ˆåç§°/说明中随阈值动态变化的片段;% å‰å¯èƒ½æ˜¯ Â±ã€>、超过、高出 ç­‰æè¿°è¯ï¼Œä»…替换数字本身) */
    private static final Pattern PCT_NUM = Pattern.compile("\\d+(?:\\.\\d+)?(?=%)");
    /** æŠŠæ–‡æœ¬ä¸­çš„ Â±N% æ›¿æ¢ä¸º Â±threshold%(含已带 Â± çš„前缀,避免双 Â±ï¼‰ */
    private String decorateThresholdText(String text, String threshold) {
        if (text == null || text.trim().isEmpty() || threshold == null || threshold.trim().isEmpty()) return text;
        String token = "±" + threshold.trim() + "%";
        return PCT_TOKEN.matcher(text).replaceAll(Matcher.quoteReplacement(token));
    /** è§£æžé˜ˆå€¼å­—符串中第一个数值;无法解析返回 null */
    private Double parseThresholdNum(String threshold) {
        if (threshold == null || threshold.trim().isEmpty()) return null;
        java.util.regex.Matcher m = Pattern.compile("\\d+(?:\\.\\d+)?").matcher(threshold.trim());
        if (!m.find()) return null;
        try {
            return Double.parseDouble(m.group());
        } catch (Exception e) {
            return null;
        }
    }
    /** åç§°/说明随阈值同步(读列表时也动态校正,DB ä¸­æ—§æ–‡æœ¬ä¸é˜»å¡žæ˜¾ç¤ºï¼‰ */
    private void syncThresholdText(AuditRule rule) {
    /** æ•°å€¼å±•示:整数去掉小数位,其余保留原精度 */
    private String fmtNum(double v) {
        long l = Math.round(v);
        if (Math.abs(v - l) < 1e-9) return Long.toString(l);
        return new java.math.BigDecimal(String.valueOf(v)).stripTrailingZeros().toPlainString();
    }
    /** ç”±é˜ˆå€¼æ•°å€¼æŽ¨å¯¼â€œè¯´æ˜Žæ–‡å­—里写的是百分之几”的候选口径:
     *  â‘  é˜ˆå€¼æœ¬èº«å°±æ˜¯ç™¾åˆ†æ•°ï¼ˆå¦‚ 20 â†’ Â±20%);
     *  â‘¡ é˜ˆå€¼æ˜¯å°æ•°æ¯”例(如 0.02 â†’ 2%、0.5 â†’ 50%);此时 <1 æ‰å¯ç”¨ï¼›
     *  â‘¢ é˜ˆå€¼æ˜¯æ¯”值(如 1.3 â†’ é«˜å‡º30%);此时 >1 æ‰å¯ç”¨ã€‚ */
    private double[] thresholdPercentCandidates(double v) {
        double[] cands;
        if (v > 0 && v < 1) {
            cands = new double[]{v, v * 100};
        } else if (v > 1) {
            cands = new double[]{v, (v - 1) * 100};
        } else {
            cands = new double[]{v};
        }
        return cands;
    }
    /** æŒ‰æ—§é˜ˆå€¼åŒ¹é…æ–‡æœ¬ä¸­å·²æœ‰çš„百分比片段,仅替换“与旧阈值对得上”的数字为新阈值对应数字;对不上的数字(如说明里与阈值无关的数值)保持不动 */
    private String decorateThresholdText(String text, String oldThreshold, String newThreshold) {
        if (text == null || text.trim().isEmpty()) return text;
        Double oldNum = parseThresholdNum(oldThreshold);
        Double newNum = parseThresholdNum(newThreshold);
        if (oldNum == null || newNum == null) return text;
        double[] oldCands = thresholdPercentCandidates(oldNum);
        double[] newCands = thresholdPercentCandidates(newNum);
        if (oldCands.length != newCands.length) return text;
        java.util.regex.Matcher m = PCT_NUM.matcher(text);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            double token;
            try {
                token = Double.parseDouble(m.group());
            } catch (Exception e) {
                continue;
            }
            String repl = null;
            for (int i = 0; i < oldCands.length; i++) {
                if (Math.abs(oldCands[i] - token) < 1e-9) {
                    repl = fmtNum(newCands[i]);
                    break;
                }
            }
            if (repl == null) continue;
            m.appendReplacement(sb, Matcher.quoteReplacement(repl));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /** åç§°/说明随阈值同步(update æ—¶æŒ‰ æ—§â†’æ–° æ”¹å†™å¹¶æŒä¹…化) */
    private void syncThresholdText(AuditRule rule, String oldThreshold) {
        if (rule == null || rule.getThreshold() == null) return;
        rule.setRuleName(decorateThresholdText(rule.getRuleName(), rule.getThreshold()));
        rule.setDescription(decorateThresholdText(rule.getDescription(), rule.getThreshold()));
        rule.setRuleName(decorateThresholdText(rule.getRuleName(), oldThreshold, rule.getThreshold()));
        rule.setDescription(decorateThresholdText(rule.getDescription(), oldThreshold, rule.getThreshold()));
    }
    /** è¯»åˆ—表时自愈:仅当文本片段与当前阈值对应时才改写(幂等,不误伤与阈值无关的数字) */
    private void syncThresholdText(AuditRule rule) {
        syncThresholdText(rule, rule == null ? null : rule.getThreshold());
    }
}
traffic-audit-web/src/views/AuditResult.vue
@@ -17,6 +17,9 @@
      <el-main :class="{ 'main-focus': focusMode }" style="display:flex;flex-direction:column;min-height:0">
        <h3 v-show="!focusMode" style="flex:none;margin:0 0 10px">审核结果</h3>
        <el-form :inline="true" style="flex:none">
          <el-form-item label="报表期">
            <el-date-picker v-model="period" type="month" placeholder="选择月份" value-format="yyyy-MM" @change="onPeriodChange"></el-date-picker>
          </el-form-item>
          <el-form-item label="报表类型">
            <el-radio-group v-model="reportType" @change="onTypeChange">
              <el-radio label="H2032">货运月报</el-radio>
@@ -30,9 +33,7 @@
            
            </el-radio-group>
          </el-form-item>
          <el-form-item label="报表期">
            <el-date-picker v-model="period" type="month" placeholder="选择月份" value-format="yyyy-MM" @change="onPeriodChange"></el-date-picker>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="executeAudit" :loading="loading">执行审核</el-button>
            <el-button type="success" plain @click="markAudited" :loading="marking">标记已审核</el-button>
@@ -116,12 +117,7 @@
                <i :class="statusIcon(row.status)"></i> {{ statusText(row.status) }}
              </el-tag>
              <span class="spacer"></span>
              <el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="goData(row)">数据</el-link>
              <el-tag v-if="explainType(reportType)" size="mini" :type="row.verifyExplanation ? 'success' : 'warning'"
                       style="cursor:pointer" :title="expandedExplain === row.reportId ? '收起解释' : '展开填写/查看解释'"
                       @click="toggleExplain(row.reportId)">
                {{ row.verifyExplanation ? '解释:已填写' : '解释:未填写' }}
              </el-tag>
              <el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="openMonthReport(row)">数据</el-link>
              <el-button v-if="row.status === 'PENDING'" size="mini" type="success" icon="el-icon-check" round @click="review(row, 'CONFIRMED')">确认</el-button>
              <el-button v-if="row.status === 'PENDING'" size="mini" type="info" icon="el-icon-close" round plain @click="review(row, 'IGNORED')">忽略</el-button>
              <span v-else class="done-text">已处理</span>
@@ -135,21 +131,6 @@
                <div class="detail-line"><span class="dt-label">实际</span><span class="dt-val">{{ row.actualValue }}</span></div>
                <div class="detail-line"><span class="dt-label">基准</span><span class="dt-val">{{ row.thresholdValue }}</span></div>
                <div class="detail-line"><span class="dt-label">差异</span><span class="dt-val dt-warn">{{ row.deviation }}</span></div>
              </div>
            </div>
            <div v-if="explainType(reportType) && expandedExplain === row.reportId" class="explain-side" style="margin:0 12px 10px">
              <div class="explain-head">
                <span class="explain-title">企业解释</span>
                <el-tag size="mini" :type="row.verifyExplanation ? 'success' : 'warning'">{{ row.verifyExplanation ? '已填写' : '未填写' }}</el-tag>
                <span style="flex:1"></span>
                <el-button size="mini" text @click="expandedExplain = null">收起</el-button>
              </div>
              <span v-if="row.verifyExplanation" class="explain-cur">{{ row.verifyExplanation }}</span>
              <el-input v-model="explainDrafts[row.reportId]" type="textarea" :rows="2" resize="none"
                        placeholder="填写/修改该企业的解释说明…"></el-input>
              <div class="explain-actions">
                <el-button size="mini" type="primary" plain :loading="savingExplainId === row.reportId" @click="saveExplain(row)">保存解释</el-button>
                <el-button v-if="(explainDrafts[row.reportId] || '') !== (row.verifyExplanation || '')" size="mini" text @click="resetDraft(row.reportId)">重置</el-button>
              </div>
            </div>
          </div>
@@ -175,7 +156,7 @@
                  <b>{{ item.ruleName }}</b>
                  <el-tag :type="item.alertLevel === 'RED' ? 'danger' : 'warning'" size="mini">{{ item.alertLevel }}</el-tag>
                  <el-tag :type="statusType(item.status)" size="mini">{{ statusText(item.status) }}</el-tag>
                  <el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="goData(item)">数据</el-link>
                  <el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="openMonthReport(item)">数据</el-link>
                  <span style="flex:1"></span>
                  <el-button v-if="item.status === 'PENDING'" size="mini" type="success" @click="review(item, 'CONFIRMED')">确认</el-button>
                  <el-button v-if="item.status === 'PENDING'" size="mini" type="info" @click="review(item, 'IGNORED')">忽略</el-button>
@@ -188,32 +169,6 @@
              </div>
            </template>
          </el-table-column>
          <el-table-column label="企业解释" width="340">
            <template slot-scope="scope">
              <div v-if="explainType(reportType) && scope.row.items[0]" class="explain-cell">
                <div v-if="expandedExplain !== scope.row.items[0].reportId" class="explain-compact">
                  <el-tag size="mini" :type="scope.row.items[0].verifyExplanation ? 'success' : 'warning'">{{ scope.row.items[0].verifyExplanation ? '已填写' : '未填写' }}</el-tag>
                  <div v-if="scope.row.items[0].verifyExplanation" class="explain-preview">{{ scope.row.items[0].verifyExplanation }}</div>
                  <el-button size="mini" text type="primary" @click="expandedExplain = scope.row.items[0].reportId">填写/修改</el-button>
                </div>
                <div v-else class="explain-side">
                  <div class="explain-head">
                    <span class="explain-title">企业解释</span>
                    <span style="flex:1"></span>
                    <el-button size="mini" text @click="expandedExplain = null">收起</el-button>
                  </div>
                  <span v-if="scope.row.items[0].verifyExplanation" class="explain-cur">{{ scope.row.items[0].verifyExplanation }}</span>
                  <el-input v-model="explainDrafts[scope.row.items[0].reportId]" type="textarea" :rows="2" resize="none"
                            placeholder="填写/修改该企业的解释说明…"></el-input>
                  <div class="explain-actions">
                    <el-button size="mini" type="primary" plain :loading="savingExplainId === scope.row.items[0].reportId" @click="saveExplain(scope.row.items[0])">保存解释</el-button>
                    <el-button v-if="(explainDrafts[scope.row.items[0].reportId] || '') !== (scope.row.items[0].verifyExplanation || '')" size="mini" text @click="resetDraft(scope.row.items[0].reportId)">重置</el-button>
                  </div>
                </div>
              </div>
              <span v-else style="color:#C0C4CC;font-size:12px">—</span>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="130">
            <template slot-scope="scope">
              <el-button v-if="hasPending(scope.row)" size="mini" type="success" plain @click="reviewAll(scope.row, 'CONFIRMED')">全部确认</el-button>
@@ -224,6 +179,50 @@
        </div>
      </el-main>
    </el-container>
        <el-dialog title="数据查看(月报样式)" :visible.sync="monthVisible" width="880px" top="4vh" custom-class="mr-dialog">
          <div v-if="monthLoading" style="padding:40px 0;text-align:center;color:#909399">加载中…</div>
          <div v-else-if="!monthRecords.length" style="padding:30px 0;text-align:center;color:#909399">
            æœªæŸ¥è¯¢åˆ°è¯¥ä¼ä¸šã€è¯¥æŠ¥è¡¨æœŸçš„填报数据(可能还未导入)
          </div>
          <div v-else class="mr-body">
            <div v-for="(rec, ri) in monthRecords" :key="ri" class="mr-card">
              <div class="mr-title">{{ monthTitle(rec) }}</div>
              <table class="mr-meta">
                <tbody>
                  <tr v-for="(m, mi) in monthMeta(rec)" :key="mi">
                    <td class="mr-meta-k">{{ m.label }}</td>
                    <td class="mr-meta-v">{{ fmtVal(m.value) }}</td>
                  </tr>
                </tbody>
              </table>
              <div v-for="sec in monthSections(rec)" :key="sec.title" class="mr-sec">
                <div class="mr-sec-title">{{ sec.title }}</div>
                <table class="mr-sec-tab" border="1">
                  <thead>
                    <tr>
                      <th style="width:34%">指标名称</th>
                      <th v-for="(h, hi) in sec.head" :key="hi" style="width:16.5%">{{ h }}</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr v-for="(rw, rwi) in sec.rows" :key="rwi">
                      <td>{{ rw.label }}</td>
                      <td v-for="(c, ci) in rw.cells" :key="ci">{{ fmtVal(c.v) }}</td>
                    </tr>
                  </tbody>
                </table>
              </div>
              <div v-if="rec.verifyExplanation || rec.reportNotes" class="mr-note">
                <div v-if="rec.verifyExplanation" class="mr-note-line"><b>核实解释:</b>{{ rec.verifyExplanation }}</div>
                <div v-if="rec.reportNotes" class="mr-note-line"><b>备注:</b>{{ rec.reportNotes }}</div>
              </div>
            </div>
          </div>
          <span slot="footer">
            <el-button size="small" @click="monthVisible = false">关闭</el-button>
          </span>
        </el-dialog>
  </el-container>
</template>
<script>
@@ -231,6 +230,104 @@
import api from '@/api'
const DATA_TYPE_MAP = { H2032: 'h2032', H2031: 'h2031', CITY_BUS: 'cityBus', CITY_TAXI: 'cityTaxi' }
const EXPLAIN_TYPES = ['H2032', 'H2031', 'CITY_BUS', 'CITY_TAXI']
const MONTH_CFG = {
  h2032: {
    meta: [['报表期', 'reportPeriod'], ['企业名称', 'enterpriseName'], ['统一社会信用代码', 'unifiedCreditCode'], ['填报单位', 'reportUnit']],
    sections: [
      { title: '一、运力情况', head: ['车辆数(辆)', '标记吨位数(吨)'], rows: [
        ['总计', ['vehicleTotal', 'tonsTotal']],
        ['其中:牵引车', ['vehicleTractor', null]],
        ['挂车', ['vehicleTrailer', 'tonsTrailer']],
        ['其中:集装箱车', ['vehicleContainer', 'tonsContainer']],
        ['整车', ['vehicleWhole', 'tonsWhole']]
      ]},
      { title: '二、运输生产情况', head: ['货运量(吨)', '货物周转量(吨公里)'], rows: [
        ['运输量', ['freightTotal', 'turnoverTotal']],
        ['其中:集装箱', ['freightContainer', null]],
        ['其中:煤炭及制品', ['freightCoal', null]],
        ['石油、天然气及制品', ['freightOilGas', null]],
        ['其中:原油', ['freightCrudeOil', null]],
        ['金属矿石', ['freightMetalOre', null]],
        ['其中:铁矿石', ['freightIronOre', null]],
        ['矿建性建筑材料', ['freightBuilding', null]],
        ['粮食', ['freightGrain', null]]
      ]},
      { title: '平均指标', head: [], rows: [
        ['货车平均吨位', ['avgTonnage', null]],
        ['货车平均运距', ['avgDistance', null]]
      ]},
      { title: '填报人信息', head: [], rows: [
        ['单位负责人', ['unitLeader', null]], ['统计负责人', ['statsLeader', null]],
        ['填表人', ['contactPerson', null]], ['联系电话', ['contactPhone', null]], ['报出日期', ['reportDate', null]]
      ]}
    ]
  },
  h2031: {
    meta: [['报表期', 'reportPeriod'], ['企业名称', 'enterpriseName']],
    sections: [
      { title: '一、运力情况', head: ['车辆数(辆)', '核定载客位(客位)'], rows: [
        ['总计', ['vehicleTotal', 'seatTotal']],
        ['客运班车(含定线旅游客运客车)', ['vehicleSchedule', 'seatSchedule']],
        ['客运包车(含非定线旅游客运客车)', ['vehicleCharter', 'seatCharter']]
      ]},
      { title: '二、运输生产情况', head: ['客运量(人次)', '旅客周转量(人公里)'], rows: [
        ['总计', ['passengerTotal', 'turnoverTotal']],
        ['一类客运班线(含定线旅游)', ['passengerClass1', 'turnoverClass1']],
        ['二类客运班线(含定线旅游)', ['passengerClass2', 'turnoverClass2']],
        ['三类客运班线(含定线旅游)', ['passengerClass3', 'turnoverClass3']],
        ['四类客运班线(含定线旅游)', ['passengerClass4', 'turnoverClass4']],
        ['客运包车(含非定线旅游)', ['passengerCharter', 'turnoverCharter']]
      ]},
      { title: '客运平均运距', head: [], rows: [
        ['客运平均运距', ['avgDistanceTotal', null]],
        ['一类客运班线平均运距', ['avgDistanceClass1', null]],
        ['二类客运班线平均运距', ['avgDistanceClass2', null]],
        ['三类客运班线平均运距', ['avgDistanceClass3', null]],
        ['四类客运班线平均运距', ['avgDistanceClass4', null]],
        ['客运包车平均运距', ['avgDistanceCharter', null]]
      ]}
    ]
  },
  cityBus: {
    meta: [['报表期', 'reportPeriod'], ['市州', 'city'], ['企业名称', 'enterpriseName']],
    sections: [
      { title: '一、公共汽电车运营', head: [], rows: [
        ['运营车数(辆)', ['opVehicles', null]], ['运营站点总个数(个)', ['stopTotal', null]],
        ['其中:城市运营站点', ['stopCity', null]], ['客运量(万人次)', ['passengerVolume', null]],
        ['其中:城市客运量', ['passengerCity', null]], ['旅客周转量(万人公里)', ['turnover', null]],
        ['其中:城市旅客周转量', ['turnoverCity', null]]
      ]},
      { title: '二、派生指标与增速', head: [], rows: [
        ['平均运距(公里)', ['avgDistance', null]], ['城市内平均运距', ['avgDistanceCity', null]],
        ['城际城乡客运量', ['passengerChengxiang', null]], ['城际城乡周转量', ['turnoverChengxiang', null]],
        ['城际城乡平均运距', ['avgDistanceChengxiang', null]], ['客运量环比', ['ratePassengerMom', null]],
        ['城市内客运量环比', ['ratePassengerCityMom', null]], ['城际城乡客运量环比', ['rateChengxiangMom', null]],
        ['客运量同比', ['ratePassengerYoy', null]], ['城市内客运量占比', ['passengerCityRatio', null]],
        ['城际城乡客运量占比', ['chengxiangRatio', null]], ['填表人', ['contactPerson', null]],
        ['报出日期', ['reportDate', null]]
      ]}
    ]
  },
  cityTaxi: {
    meta: [['报表期', 'reportPeriod'], ['市州', 'city'], ['企业名称', 'enterpriseName']],
    sections: [
      { title: '一、巡游出租运营服务', head: [], rows: [
        ['载客车次总数(万车次)', ['tripTotal', null]], ['其中:城市载客车次', ['tripCity', null]],
        ['客运量(万人次)', ['passengerVolume', null]], ['其中:城市客运量', ['passengerCity', null]],
        ['旅客周转量(万人公里)', ['turnover', null]], ['其中:城市旅客周转量', ['turnoverCity', null]],
        ['运营车辆数(辆)', ['opVehicles', null]]
      ]},
      { title: '二、派生指标与增速', head: [], rows: [
        ['平均运距(公里)', ['avgDistance', null]], ['城市内平均运距', ['avgDistanceCity', null]],
        ['单车次载客人数', ['passengersPerTrip', null]], ['载客车次环比', ['rateTripMom', null]],
        ['载客车次同比', ['rateTripYoy', null]], ['客运量环比', ['ratePassengerMom', null]],
        ['客运量同比', ['ratePassengerYoy', null]], ['城市内客运量占比', ['passengerCityRatio', null]],
        ['城际城乡平均运距', ['avgDistanceChengxiang', null]]
      ]}
    ]
  }
}
export default {
  components: { AppHeader },
  name: 'AuditResult',
@@ -238,7 +335,8 @@
    return {
      period: '', reportType: 'H2032', loading: false, marking: false, results: [], ruleStats: [], statsCollapsed: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'merged', batchSelection: [], focusMode: false,
      explainDrafts: {}, savingExplainId: null, expandedExplain: null
      explainDrafts: {}, savingExplainId: null, expandedExplain: null,
      monthVisible: false, monthLoading: false, monthType: '', monthPeriod: '', monthKeyword: '', monthRecords: [], monthColumns: []
    }
  },
  computed: {
@@ -301,10 +399,56 @@
  methods: {
    dataType(t) { return DATA_TYPE_MAP[t] || '' },
    explainType(t) { return EXPLAIN_TYPES.indexOf(t) >= 0 },
    goData(row) {
    openMonthReport(row) {
      const type = DATA_TYPE_MAP[this.reportType]
      if (!type) return
      this.$router.push({ path: '/data', query: { type, period: row.reportPeriod, keyword: row.enterpriseName || row.city || '' } })
      this.monthType = type
      this.monthPeriod = row.reportPeriod
      this.monthKeyword = row.enterpriseName || row.city || ''
      this.monthRecords = []
      this.monthColumns = []
      this.monthVisible = true
      this.monthLoading = true
      api.get('/data/list', {
        params: {
          type, period: row.reportPeriod,
          keyword: row.enterpriseName || row.city || '',
          page: 1, size: 50
        }, timeout: 60000
      }).then(res => {
        const d = res.data || {}
        this.monthRecords = d.records || []
        this.monthColumns = d.columns || []
        if (!this.monthRecords.length) this.$message.warning('未查询到该企业该报表期的填报数据(可能尚未导入)')
      }).catch(e => {
        if (e && e._toast) return
        this.$message.error('月报数据查询失败')
      }).finally(() => { this.monthLoading = false })
    },
    monthTitle(rec) {
      const cfg = MONTH_CFG[this.monthType] || null
      if (!cfg) return (this.monthKeyword || '') + '(' + this.monthPeriod + ')'
      const name = rec.enterpriseName || rec.city || this.monthKeyword || ''
      return name + ' ' + (rec.reportPeriod || this.monthPeriod || '') + ' æœˆæŠ¥æ•°æ®'
    },
    monthMeta(rec) {
      const cfg = MONTH_CFG[this.monthType] || null
      if (!cfg) return []
      return cfg.meta.map(m => ({ label: m[0], value: rec[m[1]] })).filter(m => m.value !== undefined)
    },
    monthSections(rec) {
      const cfg = MONTH_CFG[this.monthType]
      if (!cfg) return []
      return cfg.sections.map(sec => ({
        title: sec.title, head: sec.head || [],
        rows: sec.rows.map(r => ({ label: r[0], cells: (r[1] || []).map(prop => ({ prop, v: prop ? rec[prop] : null })) }))
          .filter(rw => rw.cells.some(c => c.v !== null && c.v !== undefined && c.v !== ''))
      })).filter(sec => sec.rows.length)
    },
    fmtVal(v) {
      if (v === null || v === undefined || v === '') return '—'
      if (typeof v === 'number') return String(v)
      return String(v)
    },
    goExplain(row) {
      this.$router.push({ path: '/explain', query: { reportType: this.reportType, keyword: row.enterpriseName || row.city || '', period: row.reportPeriod } })
@@ -581,4 +725,20 @@
.stat-num { color: #909399; }
.stat-num b { font-weight: 700; }
.hit-num { color: #E6A23C; }
.mr-dialog .el-dialog__body { padding: 12px 18px; }
.mr-body { max-height: 66vh; overflow-y: auto; }
.mr-card { border: 1px solid #DCDFE6; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
.mr-title { background: #409EFF; color: #fff; font-weight: 600; padding: 8px 12px; font-size: 15px; }
.mr-meta { width: 100%; border-collapse: collapse; background: #F7F9FC; }
.mr-meta td { border: 1px solid #EBEEF5; padding: 6px 10px; font-size: 13px; }
.mr-meta-k { width: 120px; color: #909399; background: #F0F4FA; }
.mr-meta-v { color: #303133; font-weight: 600; }
.mr-sec { padding: 10px 12px 4px; }
.mr-sec-title { font-weight: 600; color: #303133; margin: 4px 0 8px; }
.mr-sec-tab { width: 100%; border-collapse: collapse; font-size: 13px; }
.mr-sec-tab th, .mr-sec-tab td { border: 1px solid #DCDFE6; padding: 5px 8px; text-align: left; color: #303133; }
.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; }
</style>
traffic-audit-web/src/views/DataImport.vue
@@ -251,7 +251,7 @@
        scaleSplit: '模板_规上规下拆分运输量.xlsx',
        freightTurnover: '模板_货运量周转量.xlsx',
        passengerMonthly: '公路旅客运输月度生产情况.xlsx(含运政车辆数sheet)',
        passengerAuth: '模板_公路旅客运政车辆数.xlsx(列:企业代码/企业名称/车辆数/载客位数)',
        passengerAuth: '模板_公路旅客运政车辆数.xls(支持 ä¼ä¸šä»£ç /企业名称/车辆数/载客位数 ä¸Ž è¿æ”¿åº“导出 OWNERNAME/总车辆数/客位数)',
        passengerIndividual: '个体客运量汇总.xlsx(列:所属地区/企业代码/企业名称/个体客运量/个体旅客周转量)',
        energyMonthly: '道路货运车辆能源消耗情况(2026年).xlsx(能耗明细+运政车辆信息两sheet)',
        energyAuth: '模板_能耗车辆运政信息.xlsx(列:车牌号/车辆类型/燃料类型/标记吨位/准牵引质量)',
@@ -276,7 +276,7 @@
        scaleSplit: '货运/模板_规上规下拆分运输量.xlsx',
        freightTurnover: '货运/模板_货运量周转量.xlsx',
        passengerMonthly: '公路旅客/模板_交企统H203-1表_公路旅客运输月度生产情况.xlsx',
        passengerAuth: '公路旅客/模板_公路旅客运政车辆数.xlsx',
        passengerAuth: '公路旅客/模板_公路旅客运政车辆数.xls',
        passengerIndividual: '公路旅客/个体客运量汇总.xlsx',
        energyMonthly: '能耗/模板_道路货运车辆能源消耗情况(2026).xlsx',
        energyAuth: '能耗/模板_能耗车辆运政信息.xlsx',
traffic-audit-web/src/views/DataView.vue
@@ -111,7 +111,19 @@
        this.$message.error('数据查询失败')
      }).finally(() => {
        this.loading = false
        this.$nextTick(() => this.attachBodyScroll())
      })
    },
    // el-table å›ºå®šé«˜åº¦åŽï¼ŒçœŸæ­£çš„竖向滚动容器是内部的 body-wrapper(页面本身不滚动)
    attachBodyScroll() {
      const wrap = this.$refs.tableWrap
      if (!wrap) return
      const bw = wrap.querySelector('.el-table__body-wrapper')
      if (bw && bw._vwScrollHandler !== this.onScroll) {
        if (bw._vwScrollHandler) bw.removeEventListener('scroll', bw._vwScrollHandler)
        bw.addEventListener('scroll', this.onScroll, { passive: true })
        bw._vwScrollHandler = this.onScroll
      }
    },
    // æ»šåŠ¨åˆ°åº•éƒ¨è‡ªåŠ¨è¿½åŠ ä¸‹ä¸€é¡µï¼ˆä¿ç•™æ‰‹åŠ¨åˆ†é¡µè·³é¡µèƒ½åŠ›ï¼‰
    loadMore() {
@@ -143,6 +155,7 @@
        this.$message.error('加载更多失败')
      }).finally(() => {
        this.loadingMore = false
        this.$nextTick(() => this.attachBodyScroll())
      })
    },
    onScroll(e) {
@@ -189,17 +202,39 @@
      // capture=true:el-table å†…部 body-wrapper çš„æ»šåŠ¨åœ¨æ•èŽ·é˜¶æ®µä¹Ÿèƒ½æ”¶åˆ°
      this.$refs.tableWrap.addEventListener('scroll', this.onScroll, { passive: true, capture: true })
    }
    this.$nextTick(() => this.attachBodyScroll())
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.onScroll)
    if (this.$refs.tableWrap) {
      this.$refs.tableWrap.removeEventListener('scroll', this.onScroll, { capture: true })
      const bw = this.$refs.tableWrap.querySelector('.el-table__body-wrapper')
      if (bw && bw._vwScrollHandler) bw.removeEventListener('scroll', bw._vwScrollHandler)
    }
  }
}
</script>
<style scoped>
.el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
#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;
@@ -211,6 +246,7 @@
  overflow: auto;
  display: flex;
  flex-direction: column;
  height: 100vh;
}
#app .table-wrap:fullscreen .table-area {
  flex: 1 1 auto;
traffic-audit-web/src/views/ReportExport.vue
@@ -171,6 +171,11 @@
      noteDraft: '',
      modules: [
        {
          key: 'summary', title: '汇总大表', items: [
            { key: 'summaryWorkbook', label: '道路运输量汇总表(整本)', fileName: '生成_道路运输量汇总表.xlsx', status: null, statusNote: '' }
          ], checked: []
        },
        {
          key: 'freight', title: '货运报表', items: [
            { key: 'cityDetail', label: '货运量分市州明细', fileName: '生成_货运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'freightRank', label: '货运量排名', fileName: '生成_货运量排名.xlsx', status: null, statusNote: '' },
@@ -185,15 +190,14 @@
            { key: 'passengerMidAnalysis', label: '中口径分析', fileName: '生成_公路旅客中口径分析.xlsx', status: null, statusNote: '' }
          ], checked: []
        },        {
          key: 'cityPassenger', title: '城市客运报表', tag: '公交/出租/轨道轮渡/汇总', tagType: 'success', items: [
          key: 'cityPassenger', title: '城市客运报表', items: [
            { key: 'cityBusDetail', label: '城市公交客运量分市州明细', fileName: '生成_城市公交客运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'cityTaxiDetail', label: '巡游出租客运量分市州明细', fileName: '生成_巡游出租客运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'cityRailFerryDetail', label: '轨道轮渡客运量分市州明细', fileName: '生成_轨道轮渡客运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'cityPassengerCitySum', label: '城市客运客运量分市州累计', fileName: '生成_城市客运客运量分市州累计.xlsx', status: null, statusNote: '' },
            { key: 'cityPassengerDetailSum', label: '城市客运各市州明细表', fileName: '生成_城市客运各市州明细表.xlsx', status: null, statusNote: '' },
            { key: 'cityPassengerSummary', label: '城市客运汇总', fileName: '生成_城市客运汇总.xlsx', status: null, statusNote: '' },
            { key: 'wycSplit', label: '网约车客运量拆分表', fileName: '生成_网约车客运量拆分表.xlsx', status: null, statusNote: '' },
            { key: 'summaryWorkbook', label: '道路运输量汇总表(整本)', fileName: '生成_道路运输量汇总表.xlsx', status: null, statusNote: '' }
            { key: 'wycSplit', label: '网约车客运量拆分表', fileName: '生成_网约车客运量拆分表.xlsx', status: null, statusNote: '' }
          ], checked: []
        },
        {
@@ -238,7 +242,7 @@
    // æŠ¥è¡¨æœŸä¸‹æ‹‰åˆ†ç»„:年报按年份;月报/季报按年分组列 1-12 æœˆï¼ˆèƒ½è€—只列季度末月)
    periodGroups() {
      const year = new Date().getFullYear()
      const years = [year - 1, year, year + 1]
      const years = [year, year - 1, year + 1]
      if (this.cycle === 'year') {
        return [{ label: '选择年份', options: years.map(y => ({ label: y + 'å¹´', value: String(y) })) }]
      }