fix: 4项整改——审核结果按模板双Sheet导出(仅核实型+状态筛选)/明细表格化、数据查看分组选择与隐藏三列、报表页隐藏货运旅客城市客运小表、整本预览展示货运城市客运中口径当月累计同比 + 测试报告与工作日结
7个文件已修改
1个文件已添加
493 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_审核结果模板展示与导出及报表页整改_2026-09-09.md 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-09.md 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/controller/AuditController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/AuditResult.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataView.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÉóºË½á¹ûÄ£°åչʾÓëµ¼³ö¼°±¨±íÒ³Õû¸Ä_2026-09-09.md
New file
@@ -0,0 +1,41 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šå®¡æ ¸ç»“æžœæ¨¡æ¿åŒ–å±•ç¤º/导出与数据查看、报表生成页整改
日期:2026-09-09 模块:审核结果 / æ•°æ®æŸ¥çœ‹ / æŠ¥è¡¨ç”Ÿæˆã€€æµ‹è¯•人:Codex(业务侧点验后补充)
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
1. å®¡æ ¸ç»“果页明细视图按用户新模板(docs/审核界面月报模板/模板_审核结果_.xlsx)做成 10 åˆ—平铺表格;默认只看「核实型」问题(RULE_004 çŽ¯æ¯”æé†’é»˜è®¤æŽ’é™¤ï¼‰ï¼Œå¯åˆ‡ã€Œå…¨éƒ¨ã€ï¼›çŠ¶æ€å¯æŒ‰ å…¨éƒ¨/待处理/已处理 è¿‡æ»¤ï¼Œå¯¼å‡ºä¸Žç•Œé¢ç­›é€‰ä¸€è‡´ã€‚
2. å¯¼å‡º Excel æŒ‰æ¨¡æ¿åŒ Sheet:`审核结果`(逐条)+`企业合并`(按企业汇总 + è‡ªåŠ¨ç¼–å·é—®é¢˜åˆ—ï¼Œä¼ä¸šä¸Žé—®é¢˜åˆ—è·¨è¡Œåˆå¹¶ï¼‰ã€‚
3. æ•°æ®æŸ¥çœ‹ï¼šé¡¶éƒ¨æ¨¡å—+数据类型下拉与导入页一致(文案已对齐导入页),报表期/关键字查询移到第二行;企业代码/统一社会信用代码/填报单位 3 åˆ—隐藏。
4. æŠ¥è¡¨ç”Ÿæˆï¼šè´§è¿æŠ¥è¡¨/旅客报表/城市客运报表三组模块隐藏,仅保留 æ±‡æ€»å¤§è¡¨/投资报表/能耗报表;道路运输量汇总表(整本)预览展示 è´§è¿é‡/货运周转量、城市客运量/周转量、中口径客运量/周转量 çš„ å½“月/累计/同比。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ 8090(mvn ç¼–译通过、已重启就绪),前端 8080(dev ç¼–译成功、热更新产物抽查通过)。
- æ•°æ®åº“:本机 traffic_audit(2026-08 è´§è¿ H2032 å·²å®¡æ ¸æ•°æ®åœ¨åº“)。
## ä¸‰ã€ç”¨ä¾‹ä¸Žç»“æžœ
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | å®¡æ ¸å¯¼å‡ºåŒ Sheet | /api/audit/export?period=2026-08&reportType=H2032&onlyVerify=true&status=all | å®¡æ ¸ç»“æžœ10列+企业合并7列、问题列自动编号、企业/问题合并 | 475 è¡ŒÃ—10 åˆ—;企业合并 150 ç»„合并区(B/G åˆ—);问题形如“1、上报周转量与轨迹测算…请参考轨迹核实” | é€šè¿‡ |
| 2 | å¯¼å‡ºæŽ’除提醒型 | onlyVerify=true | RULE_004 æé†’型不出现 | æœªå‡ºçް RULE_004 | é€šè¿‡ |
| 3 | å¯¼å‡ºçŠ¶æ€è¿‡æ»¤ | status=pending / handled | ä»…待处理 / ä»…已处理 | pending=475 è¡Œï¼›handled æ— æ•°æ®ï¼ˆå½“前期无已处理行,表头保留) | é€šè¿‡ |
| 4 | åˆå¹¶å•元格对齐模板 | openpyxl æ£€æŸ¥ merged | ä¼ä¸š(B)与问题(G)跨行合并 | ä¸Žæ¨¡æ¿ B2:B4/G2:G4 åŒæž„ | é€šè¿‡ |
| 5 | æ•´æœ¬é¢„览 7 æœˆ | preview?period=2026-07&types=summaryWorkbook | è´§è¿/城市客运/中口径当月+累计+同比行 | è´§è¿é‡å½“月 16285.71 ä¸‡å¨ã€ç´¯è®¡ 113423.13;周转量当月/累计/同比;城市客运量当月 30612.85 ä¸‡äººæ¬¡ï¼›ä¸­å£å¾„当月/累计齐全 | é€šè¿‡ |
| 6 | æ•´æœ¬é¢„览缺 2025 åŸºæœŸæç¤º | åŒ 5 | åŒæ¯”缺数据显示明确提示而非静默 | è´§è¿é‡/城市客运/中口径 3 é¡¹å‡æç¤ºâ€œ2025-07 åŒæœŸæ•°æ®æœªå¯¼å…¥åº“,同比暂不显示” | é€šè¿‡ |
| 7 | æ•´æœ¬é¢„览 8 æœˆç¼ºå½“月源数据 | preview?period=2026-08 | ç¼ºå½“月数据时提示 | æç¤ºç¼º 2026-08 è´§è¿é‡å‘¨è½¬é‡/规上规下当月,累计取最近导入月 | é€šè¿‡ |
| 8 | æ•°æ®æŸ¥çœ‹åˆ—隐藏 | /api/data/list?type=h2032 åˆ—集合 | å‰ç«¯éšè— 3 åˆ— | æœåŠ¡ç«¯å« enterpriseCode/unifiedCreditCode/reportUnit(前端已按 prop+label è¿‡æ»¤ï¼‰ | é€šè¿‡ï¼ˆé¡µé¢å±•示待人工点验) |
| 9 | æŠ¥è¡¨é¡µæ¨¡å—可见性 | å‰ç«¯äº§ç‰©æŠ½æŸ¥ | è´§è¿/旅客/城市客运隐藏 | 3 ç»„ hidden:true,页签仅 æ±‡æ€»å¤§è¡¨/投资/能耗 | é€šè¿‡ |
| 10 | ç¼–译/运行 | mvn compile + é‡å¯ | æ— ç¼–译错误 | EXIT=0,服务就绪 | é€šè¿‡ |
## å››ã€å‘现问题与处置
- å¯¼å‡ºã€Œä¼ä¸šåˆå¹¶ã€åŽŸå®žçŽ°åˆå¹¶æŠ¥è¡¨æœŸåˆ—è€Œéžä¼ä¸šåˆ—ï¼Œä¸Žæ¨¡æ¿ä¸ç¬¦ â†’ å·²ä¿®æ­£ä¸º B(企业)+G(问题)合并。
- å¯¼å‡ºçŠ¶æ€æœªéšç•Œé¢ç­›é€‰ â†’ åŽç«¯å‚æ•°ç”± handledOnly æ”¹ä¸º status=all/pending/handled,前端同步传参。
- é¢„览同比因库内无 2025-07 åŸºæœŸæ•°æ®è€Œç¼ºè¡Œï¼ˆè´§è¿é‡/城市客运/中口径)→ å¢žåŠ é€é¡¹æç¤ºæ–‡æ¡ˆï¼Œæ˜Žç¡®â€œå¾… 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°â€ï¼›æ•´æœ¬å®žé™…ç”Ÿæˆä»ä»¥æ¯ç‰ˆ 2025 å¹´åˆ—公式为准。
- æ•°æ®æŸ¥çœ‹ç±»åž‹æ–‡æ¡ˆä¸Žå¯¼å…¥é¡µä¸ä¸€è‡´ï¼ˆH203-1旅客月报 vs å…¬è·¯æ—…客运输月度生产情况等)→ å·²å¯¹é½å¯¼å…¥é¡µæ–‡æ¡ˆï¼ˆvalue æ²¿ç”¨æ•°æ®æŸ¥çœ‹åŽç«¯æ”¯æŒ key)。
## äº”、待业务点验 / é—ç•™
- ä¸‰ä¸ªé¡µé¢ï¼ˆå®¡æ ¸ç»“果、数据查看、报表生成)建议在浏览器逐页点一遍:筛选联动、表格列展示、导出文件打开核对与模板逐列比对。
- æ•°æ®æŸ¥çœ‹å½“前仅开放后端已支持的类型(货运 H2032/运政/轨迹/规上规下/货运量周转量、公路旅客 H2031/旅客运政、公交/出租/出租运政);个体客运量周转量、网约车总数/订单、投资、能耗等导入类型的数据查看待后续确认是否开放。
- æ•´æœ¬é¢„览的同比行依赖 2025 å¹´åŸºæœŸæ•°æ®å…¥åº“;此前约定的“同比以母版缓存优先(M2 SummaryFiller)”改造仍未启动。
## å…­ã€ç»“论
本次 4 é¡¹ç•Œé¢/导出整改已落地并通过接口级与产物级验证;页面视觉与交互建议业务在浏览器终验后即可作为今日交付的一部分。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-09.md
@@ -22,3 +22,19 @@
## æ˜Žæ—¥è®¡åˆ’
- å¯åЍ M2 SummaryFiller(先 7 æœˆå›žå½’ diff=0,再 8 æœˆå®žå¼¹ï¼‰ï¼Œè¡¥é½ 8 æœˆè´§è¿é‡å‘¨è½¬é‡/规上规下/个体客运等缺数后整链验证。
---
## ä¸Šåˆç¬¬äºŒè½®ï¼ˆ4 é¡¹æ•´æ”¹ï¼‰2026-09-09 11:xx
### å®Œæˆå†…容
- å®¡æ ¸ç»“果页:明细视图改为与用户模板同款的 10 åˆ—表格(报表期/企业/规则编码/规则/等级/实际/阈值/偏差/状态/操作),卡片式移除;新增「问题类型(核实型默认/全部)」与「状态(全部/待处理/已处理)」筛选;导出把两项筛选带给后端。
- å®¡æ ¸å¯¼å‡ºï¼ˆæ¨¡æ¿ docs/审核界面月报模板/模板_审核结果_.xlsx åŒ Sheet):Sheet1 å®¡æ ¸ç»“果(仅核实型,排除 RULE_004 æé†’型);Sheet2 ä¼ä¸šåˆå¹¶æŒ‰ä¼ä¸šæ±‡æ€»ï¼Œé—®é¢˜åˆ—自动生成 1、2、3 ç¼–号话术,企业/问题两列按模板合并单元格(此前错合并报表期列已修正)。
- æ•°æ®æŸ¥çœ‹ï¼šæ•°æ®ç±»åž‹é€‰æ‹©æ”¹ä¸ºã€Œåˆ†ç»„模块+类型下拉」与导入页同款布局与文案;报表期、关键字查询放到下方筛选行;企业代码/统一社会信用代码/填报单位三列隐藏(按 prop ä¸Ž label åŒé‡è¿‡æ»¤ï¼‰ã€‚
- æŠ¥è¡¨ç”Ÿæˆï¼šè´§è¿/旅客/城市客运三个小表模块整组隐藏,页签仅剩 æ±‡æ€»å¤§è¡¨/投资报表/能耗报表;道路运输量汇总表(整本)预览新增关键指标:货运量、货运周转量、城市客运量/周转量、中口径客运量/周转量的当月/1..M累计/同比(数据齐即显示,缺 2025 å¹´åŸºæœŸæ•°æ®æ—¶é€é¡¹ç»™æç¤ºï¼‰ã€‚
### æµ‹è¯•结果摘要
- mvn ç¼–译通过;后端重启就绪;前端 dev ç¼–译成功(chunk æŠ½æŸ¥ç¡®è®¤æ–°å‚数已进产物)。
- /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 ä¸‰åˆ—(前端隐藏生效)。
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/controller/AuditController.java
@@ -56,8 +56,10 @@
    @GetMapping("/export")
    public ResponseEntity<byte[]> exportResults(@RequestParam("period") String period,
                                                @RequestParam(value = "reportType", defaultValue = "H2032") String reportType) throws Exception {
        byte[] bytes = auditService.exportResults(period, reportType);
                                                @RequestParam(value = "reportType", defaultValue = "H2032") String reportType,
                                                @RequestParam(value = "onlyVerify", defaultValue = "true") boolean onlyVerify,
                                                @RequestParam(value = "status", defaultValue = "all") String status) throws Exception {
        byte[] bytes = auditService.exportResults(period, reportType, onlyVerify, status);
        String name = java.net.URLEncoder.encode("审核结果_" + period + ".xlsx", "UTF-8");
        return ResponseEntity.ok()
            .header("Content-Disposition", "attachment; filename*=UTF-8''" + name)
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
@@ -1403,24 +1403,29 @@
        return a;
    }
    /** å®¡æ ¸ç»“果导出 Excel(含企业名/规则名/状态/审核意见) */
    /** å®¡æ ¸ç»“果导出 Excel(两 Sheet,按业务模板):Sheet1 å®¡æ ¸ç»“果(逐条明细);Sheet2 ä¼ä¸šåˆå¹¶ï¼ˆæŒ‰ä¼ä¸šæ±‡æ€» + é—®é¢˜åˆ—) */
    public byte[] exportResults(String reportPeriod, String reportType) throws Exception {
        return exportResults(reportPeriod, reportType, true, "all");
    }
    /** onlyVerify=true æ—¶å‰”除“提醒型”规则(如 RULE_004 çŽ¯æ¯”æé†’ï¼‰ï¼Œåªä¿ç•™æ ¸å®žåž‹é—®é¢˜ï¼›status=all/pending/handled ä¸Žç•Œé¢â€œçŠ¶æ€â€ç­›é€‰ä¸€è‡´ */
    public byte[] exportResults(String reportPeriod, String reportType, boolean onlyVerify, String status) throws Exception {
        List<AuditResult> results = queryResults(reportPeriod, reportType);
        java.util.List<AuditResult> filtered = new java.util.ArrayList<>();
        String st = status == null ? "all" : status.trim();
        for (AuditResult res : results) {
            if (onlyVerify && isReminderRule(res.getRuleCode())) continue;
            if ("pending".equals(st) && !"PENDING".equals(res.getStatus())) continue;
            if ("handled".equals(st) && (res.getStatus() == null || "PENDING".equals(res.getStatus()))) continue;
            filtered.add(res);
        }
        try (XSSFWorkbook wb = new XSSFWorkbook()) {
            // ---------- Sheet1 å®¡æ ¸ç»“果(逐条,列与业务模板一致) ----------
            Sheet sheet = wb.createSheet("审核结果");
            String[] headers = {"报表期", "企业/市州", "规则编码", "审核规则", "等级", "实际值", "阈值/基准", "偏差说明", "状态", "审核意见"};
            Row head = sheet.createRow(0);
            for (int i = 0; i < headers.length; i++) {
                Cell cell = head.createCell(i);
                cell.setCellValue(headers[i]);
                org.apache.poi.ss.usermodel.CellStyle style = wb.createCellStyle();
                org.apache.poi.ss.usermodel.Font font = wb.createFont();
                font.setBold(true);
                style.setFont(font);
                cell.setCellStyle(style);
            }
            writeAuditHeader(sheet, headers);
            int r = 1;
            for (AuditResult res : results) {
            for (AuditResult res : filtered) {
                Row row = sheet.createRow(r++);
                row.createCell(0).setCellValue(valueOf(res.getReportPeriod()));
                row.createCell(1).setCellValue(valueOf(res.getEnterpriseName()));
@@ -1437,12 +1442,97 @@
            for (int i = 0; i < widths.length; i++) {
                sheet.setColumnWidth(i, widths[i] * 256);
            }
            // ---------- Sheet2 ä¼ä¸šåˆå¹¶ï¼ˆåŒä¸€ä¼ä¸šå¤šæ¡è§„则并排列出 + é—®é¢˜æ±‡æ€»åˆ—) ----------
            Sheet ms = wb.createSheet("企业合并");
            String[] h2 = {"报表期", "企业/市州", "审核规则", "实际值", "阈值/基准", "偏差说明", "问题"};
            writeAuditHeader(ms, h2);
            java.util.LinkedHashMap<String, java.util.List<AuditResult>> groups = new java.util.LinkedHashMap<>();
            for (AuditResult res : filtered) {
                String key = valueOf(res.getEnterpriseName());
                if (key.isEmpty()) key = valueOf(res.getReportPeriod()) + "#" + valueOf(res.getActualValue());
                groups.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(res);
            }
            int rowCursor = 1;
            for (java.util.Map.Entry<String, java.util.List<AuditResult>> e : groups.entrySet()) {
                java.util.List<AuditResult> items = e.getValue();
                int groupStart = rowCursor;
                StringBuilder problems = new StringBuilder();
                int seq = 0;
                for (AuditResult it : items) {
                    Row row = ms.createRow(rowCursor++);
                    if (seq == 0) {
                        row.createCell(0).setCellValue(valueOf(it.getReportPeriod()));
                        row.createCell(1).setCellValue(valueOf(it.getEnterpriseName()).isEmpty() ? e.getKey() : valueOf(it.getEnterpriseName()));
                    }
                    row.createCell(2).setCellValue(valueOf(it.getRuleName()));
                    row.createCell(3).setCellValue(valueOf(it.getActualValue()));
                    row.createCell(4).setCellValue(valueOf(it.getThresholdValue()));
                    row.createCell(5).setCellValue(valueOf(it.getDeviation()));
                    seq++;
                    if (problems.length() > 0) problems.append("\n");
                    problems.append(seq).append("、").append(issueSentence(it));
                }
                if (rowCursor - groupStart > 1) {
                    ms.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(groupStart, rowCursor - 1, 1, 1));
                    ms.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(groupStart, rowCursor - 1, 6, 6));
                }
                Row first = ms.getRow(groupStart);
                if (first.getCell(6) == null) first.createCell(6);
                first.getCell(6).setCellValue(problems.toString());
            }
            int[] widths2 = {12, 30, 22, 34, 22, 34, 60};
            for (int i = 0; i < widths2.length; i++) {
                ms.setColumnWidth(i, widths2[i] * 256);
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            wb.write(out);
            return out.toByteArray();
        }
    }
    private void writeAuditHeader(Sheet sheet, String[] headers) {
        Row head = sheet.createRow(0);
        for (int i = 0; i < headers.length; i++) {
            Cell cell = head.createCell(i);
            cell.setCellValue(headers[i]);
            org.apache.poi.ss.usermodel.CellStyle style = sheet.getWorkbook().createCellStyle();
            org.apache.poi.ss.usermodel.Font font = sheet.getWorkbook().createFont();
            font.setBold(true);
            style.setFont(font);
            cell.setCellStyle(style);
        }
    }
    /** æé†’型规则:仅提示变化、不需人工核实(如 RULE_004 è´§è¿ç±»åž‹çŽ¯æ¯”æé†’ï¼‰ */
    private boolean isReminderRule(String ruleCode) {
        return "RULE_004".equals(ruleCode);
    }
    /** ä¼ä¸šåˆå¹¶â€œé—®é¢˜â€åˆ—话术:按规则类型生成可读的核实意见 */
    private String issueSentence(AuditResult res) {
        String code = res.getRuleCode();
        String dev = valueOf(res.getDeviation());
        String act = valueOf(res.getActualValue());
        String thr = valueOf(res.getThresholdValue());
        if ("RULE_001".equals(code) || "RULE_002".equals(code)) {
            return "企业上报车辆/吨位与运政数据对比存在差异(" + dev + "),请核实";
        }
        if ("RULE_003".equals(code)) {
            return "上报周转量与轨迹测算周转量差异明显(" + dev + "),请参考轨迹核实企业周转量";
        }
        if ("RULE_004".equals(code)) {
            return "货运类型环比发生变化,请核实";
        }
        if ("RULE_005".equals(code)) {
            return "无集装箱车但存在集装箱货运量,请核实车辆类型与货运量填报";
        }
        if ("RULE_006".equals(code)) {
            return "整车吨位超出 4~40 å¨åˆç†èŒƒå›´ï¼Œè¯·æ ¸å®žå¹³å‡å¨ä½å¡«æŠ¥";
        }
        return valueOf(res.getRuleName()) + "异常(实际:" + act + ";基准:" + thr + ";" + dev + "),请核实";
    }
    private String auditStatusCn(String status) {
        if ("CONFIRMED".equals(status)) return "已确认";
        if ("IGNORED".equals(status)) return "已忽略";
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -334,12 +334,7 @@
                    if (resolveSummaryMother(period, mode) == null) {
                        problems.add("docs/生成汇总大表 ä¸‹ç¼ºå°‘可复制的母版汇总表");
                    } else {
                        Map<Integer, Map<String, double[]>> busM = loadCityBusByMonth(yearPrefix, month);
                        Map<Integer, Map<String, double[]>> taxiM = loadCityTaxiByMonth(yearPrefix, month);
                        Map<String, double[]> bc = busM.get(month);
                        Map<String, double[]> tc = taxiM.get(month);
                        if (bc != null && bc.get("全省") != null) addSum(summary, "城市公交客运量(全省当月)", bc.get("全省")[0], "万人");
                        if (tc != null && tc.get("全省") != null) addSum(summary, "巡游出租客运量(全省当月)", tc.get("全省")[0], "万人");
                        summaryWorkbookPreview(period, yearNum, month, summary, problems);
                    }
                } catch (Exception ex) {
                    problems.add(ex.getMessage());
@@ -443,6 +438,118 @@
        return sb.toString();
    }
    /** æ±‡æ€»æ•´æœ¬å…³é”®æŒ‡æ ‡é¢„览:货运量/周转量、城市客运、中口径的 å½“月、1..M ç´¯è®¡ã€å½“月同比、累计同比(同比取库内去年同期,缺则显示—) */
    private void summaryWorkbookPreview(String period, int year, int month,
                                        List<Map<String, Object>> summary, List<String> problems) {
        String yearPrefix = year + "-";
        // ---- è´§è¿ï¼ˆé‡=模板_货运量周转量全省行;周转量=规上规下拆分表全省行) ----
        FreightTurnoverImport ft = loadFreightTurnover(period).get("湖北省");
        Double fm = ft == null ? null : freightMonth(ft, month);
        Double fcum = ft == null ? null : freightCum(ft, month);
        Double fy = ft == null ? null : freightYoy(ft, month);
        Double fcy = ft == null ? null : freightCumYoy(ft, month);
        addKpi(summary, "全省货运量", fm, fcum, fy, fcy, "万吨");
        if (fm != null && fy == null) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " åŒæœŸè´§è¿é‡æ•°æ®æœªå¯¼å…¥åº“,货运量当月/累计同比暂不显示,待 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°");
        }
        ScaleSplitTransport tm = loadProvinceMonthMap(period).get(month);
        ScaleSplitTransport tc = getProvinceCumulative(period);
        Double tvM = tm == null ? null : tm.getTotalTurnover();
        Double tvC = tc == null ? null : tc.getTotalTurnover();
        Double tvMY = tm == null ? null : getYoyMetric(tm, "turnover", "total");
        Double tvCY = tc == null ? null : getYoyMetric(tc, "turnover", "total");
        addKpi(summary, "全省货物周转量", tvM, tvC, tvMY, tvCY, "万吨公里");
        if (fm == null) problems.add("模板_货运量周转量缺 " + period + " å½“月数据,货运量仅能提供截至最近导入月的累计");
        if (tvM == null) problems.add("规上规下拆分缺 " + period + " å½“月数据,周转量/规上规下当月缺,累计为截至最近导入月");
        // ---- åŸŽå¸‚客运(公交+出租+轨道+轮渡 å…¨çœå°è®¡ï¼›æŒ‡æ ‡ä½ 0/2/4/6=客运,1/3/5/7=周转) ----
        double[] cumM = cityCumProv(yearPrefix, month);
        double[] cumPrev = cityCumProv(yearPrefix, month - 1);
        double[] monthVec = diff8(cumM, cumPrev);
        double cityPaxM = sumIdx(monthVec, new int[]{0, 2, 4, 6});
        double cityTurnM = sumIdx(monthVec, new int[]{1, 3, 5, 7});
        double cityPaxC = sumIdx(cumM, new int[]{0, 2, 4, 6});
        double cityTurnC = sumIdx(cumM, new int[]{1, 3, 5, 7});
        double[] lastCumM = cityCumProv((year - 1) + "-", month);
        double[] lastCumPrev = cityCumProv((year - 1) + "-", month - 1);
        double[] lastVec = diff8(lastCumM, lastCumPrev);
        double lastPaxM = sumIdx(lastVec, new int[]{0, 2, 4, 6});
        double lastPaxC = sumIdx(lastCumM, new int[]{0, 2, 4, 6});
        double lastTurnM = sumIdx(lastVec, new int[]{1, 3, 5, 7});
        double lastTurnC = sumIdx(lastCumM, new int[]{1, 3, 5, 7});
        addKpiRaw(summary, "城市客运量", cityPaxM, cityPaxC, lastPaxM, lastPaxC, "万人次");
        addKpiRaw(summary, "城市客运周转量", cityTurnM, cityTurnC, lastTurnM, lastTurnC, "万人公里");
        // ---- ä¸­å£å¾„(公路旅客企业 H203-1 å…¨çœï¼Œå•位换算 /10000) ----
        Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap();
        double paxM = 0.0, paxC = 0.0, turnM = 0.0, turnC = 0.0;
        double lastPaxM2 = 0.0, lastPaxC2 = 0.0, lastTurnM2 = 0.0, lastTurnC2 = 0.0;
        for (int m = 1; m <= month; m++) {
            PassengerAgg agg = aggOf(data, year, m, "湖北省");
            if (agg != null) {
                double pax = agg.passengerTotal / 10000.0;
                double turn = agg.turnoverTotal / 10000.0;
                if (m == month) { paxM = pax; turnM = turn; }
                paxC += pax;
                turnC += turn;
            }
            PassengerAgg last = aggOf(data, year - 1, m, "湖北省");
            if (last != null) {
                double lp = last.passengerTotal / 10000.0;
                double lt = last.turnoverTotal / 10000.0;
                if (m == month) { lastPaxM2 = lp; lastTurnM2 = lt; }
                lastPaxC2 += lp;
                lastTurnC2 += lt;
            }
        }
        addKpiRaw(summary, "中口径客运量", paxM, paxC, lastPaxM2, lastPaxC2, "万人次");
        addKpiRaw(summary, "中口径周转量", turnM, turnC, lastTurnM2, lastTurnC2, "万人公里");
        if (paxM <= 0 && paxC <= 0 && cityPaxC <= 0 && fm == null) {
            problems.add("货运量周转量/规上规下/公路旅客/城市客运当月数据均缺失");
        }
        if (cityPaxC > 0 && lastPaxC <= 0) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " åŒæœŸåŸŽå¸‚客运数据未导入库,城市客运同比暂不显示,待 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°");
        }
        if (paxC > 0 && lastPaxC2 <= 0) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " åŒæœŸå…¬è·¯æ—…客数据未导入库,中口径同比暂不显示,待 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°");
        }
    }
    /** åŸŽå¸‚客运全省累计向量(8 æŒ‡æ ‡ï¼šå…¬äº¤å®¢è¿/周转、出租客运/周转、轨道客运/周转、轮渡客运/周转) */
    private double[] cityCumProv(String yearPrefix, int month) {
        if (month <= 0) return new double[8];
        return loadCityPassengerCumulative(yearPrefix, month).getOrDefault("全省", new double[8]);
    }
    private double[] diff8(double[] a, double[] b) {
        double[] out = new double[8];
        for (int i = 0; i < 8; i++) out[i] = a[i] - b[i];
        return out;
    }
    private double sumIdx(double[] arr, int[] idx) {
        double sum = 0.0;
        for (int i : idx) sum += (i < arr.length ? arr[i] : 0.0);
        return sum;
    }
    /** ç™¾åˆ†æ¯”同比(值已为比率 0.xx):输出四行 å½“月/累计/当月同比/累计同比 */
    private void addKpi(List<Map<String, Object>> summary, String name, Double m, Double cum, Double yoyM, Double yoyC, String unit) {
        if (m != null) addSum(summary, name + "·当月", round(m, 2), unit);
        if (cum != null) addSum(summary, name + "·累计", round(cum, 2), unit);
        if (yoyM != null) addSum(summary, name + "·当月同比", round(yoyM * 100.0, 1), "%");
        if (yoyC != null) addSum(summary, name + "·累计同比", round(yoyC * 100.0, 1), "%");
    }
    /** åŽŸå§‹æ•°å€¼ç‰ˆï¼ˆæœˆ/累计为 0 è§†ä¸ºæ— æ•°æ®ä¸å‡ºè¡Œï¼‰ */
    private void addKpiRaw(List<Map<String, Object>> summary, String name, double m, double cum, double lastM, double lastC, String unit) {
        if (m > 0) addSum(summary, name + "·当月", round(m, 2), unit);
        if (cum > 0) addSum(summary, name + "·累计", round(cum, 2), unit);
        if (m > 0 && lastM > 0) addSum(summary, name + "·当月同比", round((m - lastM) / lastM * 100.0, 1), "%");
        if (cum > 0 && lastC > 0) addSum(summary, name + "·累计同比", round((cum - lastC) / lastC * 100.0, 1), "%");
    }
    private void addSum(List<Map<String, Object>> summary, String label, Double value, String unit) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("label", label);
traffic-audit-web/src/views/AuditResult.vue
@@ -59,6 +59,19 @@
              <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-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-group>
          </el-form-item>
          <el-form-item label="状态">
            <el-select v-model="statusScope" style="width:110px" @change="updateBatchSelection">
              <el-option label="全部" value="all"></el-option>
              <el-option label="待处理" value="pending"></el-option>
              <el-option label="已处理" value="handled"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="视图">
            <el-radio-group v-model="viewMode">
              <el-radio label="detail">按条明细</el-radio>
@@ -98,45 +111,52 @@
          å…± {{ results.length }} æ¡é—®é¢˜ï¼Œ{{ mergedResults.length }} å®¶ä¼ä¸š
        </div>
        <div class="table-area" style="flex:1;min-height:0">
        <!-- æ˜Žç»†è§†å›¾ï¼šä¸€æ¡é—®é¢˜ä¸€å¼ å¡ç‰‡ -->
        <!-- æ˜Žç»†è§†å›¾ï¼šä¸€æ¡é—®é¢˜ä¸€è¡Œï¼ˆä¸Žå¯¼å‡ºæ¨¡æ¿ Sheet“审核结果”同款) -->
        <div v-if="viewMode === 'detail'" class="detail-cards-wrap" v-loading="loading">
          <div class="detail-toolbar">
            <el-checkbox :value="allSelected" :indeterminate="someSelected" @change="toggleSelectAll">全选当前 {{ filteredResults.length }} æ¡</el-checkbox>
            <span class="toolbar-tip">勾选后可批量确认 / å¿½ç•¥</span>
          </div>
          <div v-for="row in filteredResults" :key="row.id" class="detail-card"
               :class="{ 'card-red': row.alertLevel === 'RED', 'card-yellow': row.alertLevel === 'YELLOW', 'card-done': row.status !== 'PENDING' }">
            <div class="card-head">
              <el-checkbox v-model="row._selected" @change="updateBatchSelection"></el-checkbox>
              <div class="card-ent">
                <div class="ent-name">{{ row.enterpriseName || row.city || '未知企业' }}</div>
                <div class="ent-sub">{{ row.reportPeriod }}</div>
              </div>
              <el-tag :type="row.alertLevel === 'RED' ? 'danger' : 'warning'" size="small" effect="dark">{{ row.alertLevel }}</el-tag>
              <el-tag :type="statusType(row.status)" size="small" effect="plain" class="status-tag">
                <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="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>
          <el-table :data="filteredResults" border stripe height="100%"
            :header-cell-style="{ background: '#F5F7FA', color: '#303133', fontWeight: 600 }">
            <el-table-column width="40">
              <template slot-scope="{ row }">
                <el-checkbox v-model="row._selected" @change="updateBatchSelection"></el-checkbox>
              </template>
            </el-table-column>
            <el-table-column prop="reportPeriod" label="报表期" width="90"></el-table-column>
            <el-table-column label="企业/市州" min-width="200" show-overflow-tooltip>
              <template slot-scope="{ row }">{{ row.enterpriseName || row.city || '未知企业' }}</template>
            </el-table-column>
            <el-table-column prop="ruleCode" label="规则编码" width="100"></el-table-column>
            <el-table-column prop="ruleName" label="审核规则" min-width="150" show-overflow-tooltip></el-table-column>
            <el-table-column label="等级" width="76">
              <template slot-scope="{ row }">
                <el-tag :type="row.alertLevel === 'RED' ? 'danger' : 'warning'" size="mini">{{ row.alertLevel }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="actualValue" label="实际值" min-width="230" show-overflow-tooltip></el-table-column>
            <el-table-column prop="thresholdValue" label="阈值/基准" min-width="140" show-overflow-tooltip></el-table-column>
            <el-table-column prop="deviation" label="偏差说明" min-width="170" show-overflow-tooltip></el-table-column>
            <el-table-column label="状态" width="92">
              <template slot-scope="{ row }">
                <el-tag :type="statusType(row.status)" size="mini">
                  <i :class="statusIcon(row.status)"></i> {{ statusText(row.status) }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="185" align="right">
              <template slot-scope="{ row }">
                <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>
              </template>
            </el-table-column>
            <div slot="empty">
              <el-empty description="当前筛选条件下没有问题数据"></el-empty>
            </div>
            <div class="card-body">
              <div class="rule-title">
                <b>{{ row.ruleName }}</b>
                <span class="rule-code">{{ row.ruleCode }}</span>
              </div>
              <div class="card-details">
                <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>
          <div v-if="!filteredResults.length" class="empty-tip">
            <el-empty description="当前筛选条件下没有问题数据"></el-empty>
          </div>
          </el-table>
        </div>
        <!-- åˆå¹¶è§†å›¾ -->
        <el-table v-else :data="mergedResults" border stripe v-loading="loading" height="100%"
@@ -334,7 +354,7 @@
  data() {
    return {
      period: '', reportType: 'H2032', loading: false, marking: false, results: [], ruleStats: [], statsCollapsed: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'merged', batchSelection: [], focusMode: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'detail', issueScope: 'verify', statusScope: 'all', batchSelection: [], focusMode: false,
      explainDrafts: {}, savingExplainId: null, expandedExplain: null,
      monthVisible: false, monthLoading: false, monthType: '', monthPeriod: '', monthKeyword: '', monthRecords: [], monthColumns: []
    }
@@ -364,13 +384,20 @@
      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()
      const rule = this.ruleFilter
      if (!kw && !rule) return this.results
      const scope = this.issueScope || 'verify'
      const st = this.statusScope || 'all'
      return this.results.filter(r => {
        if (kw && !(r.enterpriseName || '').includes(kw)) return false
        if (rule && r.ruleName !== rule) return false
        if (scope === 'verify' && this.isReminderRow(r)) return false
        if (st === 'pending' && r.status !== 'PENDING') return false
        if (st === 'handled' && (!r.status || r.status === 'PENDING')) return false
        return true
      })
    },
@@ -577,7 +604,8 @@
    },
    exportExcel() {
      if (!this.period) { this.$message.warning('请选择报表期'); return }
      window.open('/api/audit/export?period=' + this.period + '&reportType=' + this.reportType)
      const q = 'period=' + this.period + '&reportType=' + this.reportType + '&onlyVerify=' + (this.issueScope !== 'all') + '&status=' + (this.statusScope || 'all')
      window.open('/api/audit/export?' + q)
    },
    review(row, status) {
      if (status === 'IGNORED') {
traffic-audit-web/src/views/DataView.vue
@@ -16,21 +16,18 @@
      </el-aside>
      <el-main>
        <h3>导入数据查看</h3>
        <el-form :inline="true">
          <el-form-item label="数据类型">
            <el-radio-group v-model="type" @change="loadData">
              <el-radio-button label="h2032">H203-2月报</el-radio-button>
              <el-radio-button label="transportAuth">运政车辆</el-radio-button>
              <el-radio-button label="trackMileage">轨迹里程</el-radio-button>
              <el-radio-button label="scaleSplit">规上规下拆分</el-radio-button>
              <el-radio-button label="freightTurnover">货运量周转量</el-radio-button>
              <el-radio-button label="h2031">H203-1旅客月报</el-radio-button>
              <el-radio-button label="passengerAuth">旅客运政车辆</el-radio-button>
              <el-radio-button label="cityBus">城市公交月报</el-radio-button>
              <el-radio-button label="cityTaxi">巡游出租月报</el-radio-button>
              <el-radio-button label="cityTaxiAuth">出租车运政车辆</el-radio-button>
            </el-radio-group>
          </el-form-item>
        <div class="type-row">
          <span class="type-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-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>
        </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>
@@ -48,7 +45,7 @@
        <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%">
              <el-table-column v-for="c in columns" :key="c.prop" :prop="c.prop" :label="c.label" :min-width="c.width" show-overflow-tooltip>
              <el-table-column v-for="c in visibleColumns" :key="c.prop" :prop="c.prop" :label="c.label" :min-width="c.width" show-overflow-tooltip>
                <template slot-scope="scope">
                  <span>{{ formatCell(scope.row[c.prop]) }}</span>
                </template>
@@ -73,6 +70,31 @@
  name: 'DataView',
  data() {
    return {
      modules: [
        {
          key: 'freight', name: '道路货运', types: [
            { value: 'h2032', label: '道路货物运输月度生产情况' },
            { value: 'transportAuth', label: '运政车辆' },
            { value: 'trackMileage', label: '轨迹里程' },
            { value: 'scaleSplit', label: '规上规下拆分' },
            { value: 'freightTurnover', label: '货运量周转量' }
          ]
        },
        {
          key: 'passenger', name: '公路旅客', types: [
            { value: 'h2031', label: '公路旅客运输月度生产情况' },
            { value: 'passengerAuth', label: '运政车辆数' }
          ]
        },
        {
          key: 'city', name: '城市客运', types: [
            { value: 'cityBus', label: '城市公共交通月度运营情况' },
            { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' }
          ]
        }
      ],
      activeModule: 'freight',
      type: 'h2032',
      period: '',
      keyword: '',
@@ -85,7 +107,28 @@
      loadingMore: false
    }
  },
  computed: {
    activeTypes() {
      const g = this.modules.find(x => x.key === this.activeModule) || this.modules[0]
      return g
    },
    visibleColumns() {
      const hiddenProps = ['enterpriseCode', 'unifiedCreditCode', 'reportUnit']
      const hiddenLabels = ['企业代码', '统一社会信用代码', '填报单位']
      return (this.columns || []).filter(c =>
        hiddenProps.indexOf(c.prop) < 0 && hiddenLabels.indexOf(c.label) < 0)
    }
  },
  methods: {
    onModuleChange(key) {
      const g = this.modules.find(x => x.key === key)
      if (!g || !g.types.length) return
      if (!g.types.some(t => t.value === this.type)) {
        this.type = g.types[0].value
      }
      this.loadData()
    },
    onTypeSelect() { this.loadData() },
    loadData() {
      this.loading = true
      this.loadingMore = false
@@ -100,7 +143,7 @@
      }).then(res => {
        const d = res.data || {}
        this.records = d.records || []
        this.columns = d.columns || []
        this.columns = (d.columns || []).filter(c => ['enterpriseCode', 'unifiedCreditCode', 'reportUnit'].indexOf(c.prop) < 0 && ['企业代码', '统一社会信用代码', '填报单位'].indexOf(c.label) < 0)
        this.total = d.total || 0
        if (this.page > 1 && this.records.length === 0) {
          this.page = 1
@@ -149,7 +192,7 @@
        this.records = this.records.concat(addRows)
        this.page = nextPage
        if (d.total != null) this.total = d.total
        if (d.columns && d.columns.length) this.columns = d.columns
        if (d.columns && d.columns.length) this.columns = d.columns.filter(c => ['enterpriseCode', 'unifiedCreditCode', 'reportUnit'].indexOf(c.prop) < 0 && ['企业代码', '统一社会信用代码', '填报单位'].indexOf(c.label) < 0)
      }).catch(e => {
        if (e && e._toast) return
        this.$message.error('加载更多失败')
@@ -216,6 +259,9 @@
</script>
<style scoped>
.el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
.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;
traffic-audit-web/src/views/ReportExport.vue
@@ -27,7 +27,7 @@
          <!-- ç¬¬äºŒè¡Œï¼šæŠ¥è¡¨ç±»åž‹åˆ‡æ¢ï¼ˆè´§è¿ / æ—…客 / æŠ•资 / èƒ½è€— / åŸŽå¸‚客运);el-radio-group æ›¿ä»£ el-tabs,规避 el-tabs å¸ƒå±€æ— é™å¾ªçŽ¯ï¼ˆä¸Ž /import ä¿®å¤ä¸€è‡´ï¼‰ -->
          <el-radio-group v-model="activeKey" class="module-tabs">
            <el-radio-button v-for="m in modules" :key="m.key" :label="m.key">
            <el-radio-button v-for="m in visibleModules" :key="m.key" :label="m.key">
              {{ m.title }}<el-tag v-if="m.tag" size="mini" :type="m.tagType || 'info'" style="margin-left:6px">{{ m.tag }}</el-tag>
            </el-radio-button>
          </el-radio-group>
@@ -176,21 +176,21 @@
          ], checked: []
        },
        {
          key: 'freight', title: '货运报表', items: [
          key: 'freight', title: '货运报表', hidden: true, items: [
            { key: 'cityDetail', label: '货运量分市州明细', fileName: '生成_货运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'freightRank', label: '货运量排名', fileName: '生成_货运量排名.xlsx', status: null, statusNote: '' },
            { key: 'turnoverRank', label: '周转量排名', fileName: '生成_周转量排名.xlsx', status: null, statusNote: '' }
          ], checked: []
        },
        {
          key: 'passenger', title: '旅客报表', items: [
          key: 'passenger', title: '旅客报表', hidden: true, items: [
            { key: 'passengerCityDetail', label: '公路旅客分市州', fileName: '生成_公路旅客分市州.xlsx', status: null, statusNote: '' },
            { key: 'passengerMidDetail', label: '中口径明细', fileName: '生成_公路旅客中口径明细.xlsx', status: null, statusNote: '' },
            { key: 'passengerMidRank', label: '中口径排名', fileName: '生成_公路旅客中口径排名.xlsx', status: null, statusNote: '' },
            { key: 'passengerMidAnalysis', label: '中口径分析', fileName: '生成_公路旅客中口径分析.xlsx', status: null, statusNote: '' }
          ], checked: []
        },        {
          key: 'cityPassenger', title: '城市客运报表', items: [
          key: 'cityPassenger', title: '城市客运报表', hidden: true, 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: '' },
@@ -223,6 +223,9 @@
      const s = sessionStorage.getItem('modules')
      if (!s) return []
      return s.split(',').filter(Boolean)
    },
    visibleModules() {
      return this.modules.filter(m => !m.hidden)
    },
    curModule() {
      return this.modules.find(m => m.key === this.activeKey) || this.modules[0]
@@ -260,8 +263,8 @@
    }
  },
  created() {
    const mine = this.myModules.filter(k => this.modules.some(m => m.key === k))
    this.activeKey = (mine.length ? mine : this.modules.map(m => m.key))[0]
    const mine = this.myModules.filter(k => this.visibleModules.some(m => m.key === k))
    this.activeKey = (mine.length ? mine : this.visibleModules.map(m => m.key))[0]
    this.ensureCycle()
    this.refreshAll()
  },  methods: {