feat(报表): 新增客运站/物流站场投资明细表导出 + 中口径同比口径修复 + 隐藏网约车订单入口

报表:新增两张投资明细表导出
- /export/investPassengerDetail:模板「模板_X月客运站投资明细表.xlsx」(明细+汇总),
只覆盖 H 自开始建设累计 / J 自年初累计 / K 本月完成 / M 形象进度;
I 本年计划投资、L 建设阶段、G 计划总投资不动母版;库内为空保留母版原值。
- /export/investLogisticsDetail:模板「模板_X月物流站场投资明细.xls」,
覆盖项目行 H/J/K/M 后按表内数值重算渠道行/市州行/合计行,
并刷新「分市州规划内外项目完成情况」「市州汇总」(渠道小计保留程序算出的数)。
- 前端「投资报表」组新增两项,fileNameOf() 把「X月」按报表期替换为具体月份。

中口径:排名/明细同比基期不再按「值是否为 0」判定
- 各维度登记「有源行」(markMid),midMonthHas()/midCumHitMonths() 判断基期;
去年缺任一月则同比留空,修正公交行累计同比 −28577% → +13.15%。
- loadMidClassMap() 用 passengerVolume - passengerCity 兜底跨县公交口径,负值归零。
- 解析异常由 catch-continue 改为记入 midIssues + log.warn(getLastMidIssues() 可查)。
- WycSplitCalc:某月 pin 缺失/≤0 时回落 wyc_total_monthly,source 标 db+wyc_total。

界面:导入界面隐藏「网约车订单及全省总量」(hidden: true,入口/概览/清空三处统一走 typeList())。

母版:docs/城市客运/模板_网约车订单及全省总量.xlsx、docs/生成汇总大表/生成_道路运输量汇总表.xlsx 为用户手工调整。

详见 docs/功能测试报告/功能测试报告_投资明细表与导入取列定位修复_2026-09-19.md
7个文件已修改
565 ■■■■■ 已修改文件
docs/城市客运/模板_网约车订单及全省总量.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
docs/生成汇总大表/生成_道路运输量汇总表.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 494 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/³ÇÊпÍÔË/Ä£°å_ÍøÔ¼³µ¶©µ¥¼°È«Ê¡×ÜÁ¿.xlsx
Binary files differ
docs/Éú³É»ã×Ü´ó±í/Éú³É_µÀ·ÔËÊäÁ¿»ã×ܱí.xlsx
Binary files differ
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java
@@ -4,8 +4,10 @@
import com.trafficaudit.common.util.RegionUtil;
import com.trafficaudit.dataimport.entity.CityTaxiMonthly;
import com.trafficaudit.dataimport.entity.WycOrderMonthly;
import com.trafficaudit.dataimport.entity.WycTotalMonthly;
import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper;
import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
import com.trafficaudit.dataimport.mapper.WycTotalMonthlyMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter;
@@ -80,6 +82,8 @@
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private WycOrderMonthlyMapper wycOrderMapper;
    @Resource
    private WycTotalMonthlyMapper wycTotalMapper;
    @Value("${city-passenger.template-dir:docs/城市客运}")
    private String templateDir;
@@ -254,14 +258,30 @@
        if (sum <= 0) return null;
        double orderSum = sum;
        if (row.getOrderSum() != null && row.getOrderSum() > 0) orderSum = row.getOrderSum();
        double pinK = row.getPinK() == null ? 0.0 : row.getPinK();
        double pinCK = row.getPinCK() == null ? 0.0 : row.getPinCK();
        double pinZ = row.getPinZ() == null ? 0.0 : row.getPinZ();
        double pinCZ = row.getPinCZ() == null ? 0.0 : row.getPinCZ();
        boolean pinFix = false;
        // pin å…œåº•:订单表 pin åˆ—为空/为 0 æ—¶å›žè½ wyc_total_monthly(pin ä¸“表)
        if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0) {
            WycTotalMonthly tot = wycTotalMapper.selectOne(new LambdaQueryWrapper<WycTotalMonthly>()
                    .eq(WycTotalMonthly::getReportPeriod, period).last("LIMIT 1"));
            if (tot != null) {
                if (pinK <= 0 && tot.getPinK() != null && tot.getPinK() > 0) { pinK = tot.getPinK(); pinFix = true; }
                if (pinCK <= 0 && tot.getPinCK() != null && tot.getPinCK() > 0) { pinCK = tot.getPinCK(); pinFix = true; }
                if (pinZ <= 0 && tot.getPinZ() != null && tot.getPinZ() > 0) { pinZ = tot.getPinZ(); pinFix = true; }
                if (pinCZ <= 0 && tot.getPinCZ() != null && tot.getPinCZ() > 0) { pinCZ = tot.getPinCZ(); pinFix = true; }
            }
        }
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("pinK", row.getPinK() == null ? 0.0 : row.getPinK());
        map.put("pinCK", row.getPinCK() == null ? 0.0 : row.getPinCK());
        map.put("pinZ", row.getPinZ() == null ? 0.0 : row.getPinZ());
        map.put("pinCZ", row.getPinCZ() == null ? 0.0 : row.getPinCZ());
        map.put("pinK", pinK);
        map.put("pinCK", pinCK);
        map.put("pinZ", pinZ);
        map.put("pinCZ", pinCZ);
        map.put("orders", orders);
        map.put("orderSum", orderSum);
        map.put("source", "db");
        map.put("source", pinFix ? "db+wyc_total" : "db");
        return map;
    }
    private File findInput() throws Exception {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
@@ -155,6 +155,22 @@
        return buildResponse(reportService.investCounty(period, mode), "生成_经济强县交通物流基础设施投资统计报表.xls");
    }
    /** ç”Ÿæˆ_X月客运站投资明细表.xlsx(投资模块明细表1:客运站场) */
    @GetMapping("/export/investPassengerDetail")
    public ResponseEntity<byte[]> exportInvestPassengerDetail(@RequestParam("period") String period,
                                                              @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_X月客运站投资明细表.xlsx", period);
        return buildResponse(reportService.exportInvestPassengerDetail(period, mode), "生成_客运站投资明细表.xlsx");
    }
    /** ç”Ÿæˆ_X月物流站场投资明细.xls(投资模块明细表2:物流园区) */
    @GetMapping("/export/investLogisticsDetail")
    public ResponseEntity<byte[]> exportInvestLogisticsDetail(@RequestParam("period") String period,
                                                              @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_X月物流站场投资明细.xls", period);
        return buildResponse(reportService.exportInvestLogisticsDetail(period, mode), "生成_物流站场投资明细.xls");
    }
    /** ç”Ÿæˆ_轨道轮渡客运量分市州明细.xlsx(城市客运,轨道/轮渡取自公交源文件) */
    @GetMapping("/export/cityRailFerryDetail")
    public ResponseEntity<byte[]> exportCityRailFerryDetail(@RequestParam("period") String period,
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -1189,18 +1189,9 @@
    /** ä¸­å£å¾„月同比:k=-1 è¡¨ç¤ºæ€»è¡Œï¼ˆç­çº¿+公交+出租城乡+网约车城乡 å››é¡¹ä¹‹å’Œï¼‰ï¼›æ— åŽ»å¹´æ•°æ®ç•™ç©º */
    private void fillMidYoyCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month,
                                String area, int k, boolean volume) {
        double v = 0.0, lv = 0.0;
        if (k == -1) {
            for (int kk = 1; kk <= 4; kk++) {
                v += midClassVal(mid, year, month, area, kk, volume);
                lv += midClassVal(mid, year - 1, month, area, kk, volume);
            }
        } else {
            v = midClassVal(mid, year, month, area, k, volume);
            lv = midClassVal(mid, year - 1, month, area, k, volume);
        }
        Double y = midMonthYoy(mid, year, month, area, k, volume);
        c.setBlank();
        if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4));
        if (y != null) c.setCellValue(y);
    }
    /** ä¸­å£å¾„累计同比列(1..months æœˆç´¯è®¡ï¼›å››ç»´é½å…¨ï¼‰ */
@@ -1221,19 +1212,13 @@
            if (blockOffset < 0 || blockOffset % 5 == 0) continue;
            int k = blockOffset % 5;
            String area = areas.get(blockOffset / 5);
            double v = 0.0, lv = 0.0;
            for (int m = 1; m <= months; m++) {
                {
                    v += midClassVal(mid, year, m, area, k, volume);
                    lv += midClassVal(mid, year - 1, m, area, k, volume);
                }
            }
            Double y = midCumYoy(mid, year, area, months, k, volume);
            Row row = sheet.getRow(r0);
            if (row == null) continue;
            Cell c = row.getCell(colIdx);
            if (c == null) c = row.createCell(colIdx);
            c.setBlank();
            if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4));
            if (y != null) c.setCellValue(y);
        }
    }
@@ -2326,6 +2311,56 @@
        return volume ? arr[k][0] : arr[k][1];
    }
    /** B3:登记「该期该市州该维度有源行」。判据是源表有没有这一行,而不是算出来的值是否非 0
     *  â€”—因为「值为 0」可能是真实业务(如武汉市巡游出租全部为城市内,城际城乡恒为 0)。 */
    private void markMid(Map<Integer, Map<String, boolean[]>> pres, int key, String city, int k) {
        pres.computeIfAbsent(key, kk -> new HashMap<>())
            .computeIfAbsent(city, kk -> new boolean[5])[k] = true;
    }
    /**
     * B3:该月该维度是否「有源」(同比完备性校验用)。
     * k &lt;= 0 è¡¨ç¤ºã€Œæ€»é‡è¡Œã€ï¼Œè¦æ±‚四维(班线/公交/出租/网约车)都有源才算齐全。
     */
    private boolean midMonthHas(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) {
        Map<String, boolean[]> pm = midPresence.get().get(year * 100 + month);
        if (pm == null) return false;
        boolean[] b = pm.get(area);
        if (b == null) return false;
        if (k <= 0) {
            for (int kk = 1; kk <= 4; kk++) {
                if (!b[kk]) return false;
            }
            return true;
        }
        return k < b.length && b[k];
    }
    /** B3:去年 1..monthCount æœˆçš„命中月份数(用于判定同比基数是否齐全) */
    private int midCumHitMonths(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) {
        int n = 0;
        for (int m = 1; m <= monthCount; m++) {
            if (midMonthHas(mid, year, m, area, k, volume)) n++;
        }
        return n;
    }
    /** B3:当月同比——去年同月基数缺失则留空(缺数据不再当 0 è®¡ç®—) */
    private Double midMonthYoy(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) {
        if (!midMonthHas(mid, year - 1, month, area, k, volume)) return null;
        double v = midClassVal(mid, year, month, area, k, volume);
        if (v == 0.0) return null;
        return yoyOf(v, midClassVal(mid, year - 1, month, area, k, volume));
    }
    /** B3:累计同比——去年 1..monthCount å„月齐全才计算,缺任何一月一律留空 */
    private Double midCumYoy(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) {
        if (midCumHitMonths(mid, year - 1, area, monthCount, k, volume) < monthCount) return null;
        double v = midCumClass(mid, year, area, monthCount, k, volume);
        if (v == 0.0) return null;
        return yoyOf(v, midCumClass(mid, year - 1, area, monthCount, k, volume));
    }
    /** å…¬å¼å•元格替换为缓存数值(避免跨簿引用断链;非数值缓存置空) */
    private void keepCached(Cell c) {
        try {
@@ -2346,7 +2381,20 @@
     * ä¸­å£å¾„分类月度数据:年*100+月 -> (市州/全省 -> double[5][2])
     * ç»´åº¦ 0=总量 1=公路班线(h2031+个体) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), å‘¨è½¬é‡(万人公里)]
     */
    /** B3:最近一次 loadMidClassMap çš„「维度是否有源行」登记表(key -> city -> boolean[5]),用于同比完备性校验 */
    private final ThreadLocal<Map<Integer, Map<String, boolean[]>>> midPresence =
            ThreadLocal.withInitial(HashMap::new);
    /** B4/B5:最近一次 loadMidClassMap çš„æ•°æ®é—®é¢˜æ¸…单(线程内,随导出体检输出/写生成件) */
    private final ThreadLocal<java.util.List<String>> midIssues = ThreadLocal.withInitial(java.util.ArrayList::new);
    /** B4/B5:取最近一次中口径数据加载的问题清单(只读副本) */
    public java.util.List<String> getLastMidIssues() { return new java.util.ArrayList<>(midIssues.get()); }
    private Map<Integer, Map<String, double[][]>> loadMidClassMap() {
        midIssues.get().clear();   // B5:本次加载的问题清单
        Map<Integer, Map<String, boolean[]>> pres = new HashMap<>();
        midPresence.set(pres);     // B3:本次加载的维度有源登记
        Map<Integer, Map<String, double[][]>> mid = new HashMap<>();
        for (PassengerEnterpriseMonthly e : passengerMapper.selectList(null)) {
            if (e.getReportPeriod() == null) continue;
@@ -2368,6 +2416,8 @@
                + nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter())) / 10000.0;
            addMidClass(mid, key, city, 1, pass, turn);
            addMidClass(mid, key, "湖北省", 1, pass, turn);
            markMid(pres, key, city, 1);
            markMid(pres, key, "湖北省", 1);
        }
        for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { // ä¸ªä½“并入公路班线/总量(09-03 å£å¾„)
            if (e.getReportPeriod() == null) continue;
@@ -2387,6 +2437,8 @@
            double turn = nz(e.getTurnover()) / 10000.0;
            addMidClass(mid, key, city, 1, pass, turn);
            addMidClass(mid, key, "湖北省", 1, pass, turn);
            markMid(pres, key, city, 1);
            markMid(pres, key, "湖北省", 1);
        }
        for (CityBusMonthly b : cityBusMapper.selectList(null)) {
            if (b.getReportPeriod() == null || b.getCity() == null) continue;
@@ -2402,8 +2454,17 @@
            String city = RegionUtil.normalizeCityName(b.getCity());
            if (city == null) continue;
            int key = y * 100 + m;
            addMidClass(mid, key, city, 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
            addMidClass(mid, key, "湖北省", 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
            // B1 å…œåº•:存储的「城际城乡」缺失(<=0)时用「总客运量 âˆ’ åŸŽå¸‚内」现算,避免 2025 å…¨å¹´å…¬äº¤ç»´åº¦è¢«å½“成 0
            double cxPass = nz(b.getPassengerChengxiang());
            if (cxPass <= 0) cxPass = nz(b.getPassengerVolume()) - nz(b.getPassengerCity());
            double cxTurn = nz(b.getTurnoverChengxiang());
            if (cxTurn <= 0) cxTurn = nz(b.getTurnover()) - nz(b.getTurnoverCity());
            if (cxPass < 0) cxPass = 0.0;
            if (cxTurn < 0) cxTurn = 0.0;
            addMidClass(mid, key, city, 2, cxPass, cxTurn);
            addMidClass(mid, key, "湖北省", 2, cxPass, cxTurn);
            markMid(pres, key, city, 2);
            markMid(pres, key, "湖北省", 2);
        }
        // ç»´åº¦3:城际城乡巡游出租 = å°è´¦é¡µã€Œå‡ºç§Ÿ!总量 âˆ’ å‡ºç§Ÿ!城市内」(源 city_taxi_monthly)
        for (CityTaxiMonthly t : cityTaxiMapper.selectList(null)) {
@@ -2416,6 +2477,8 @@
            double turn = nz(t.getTurnover()) - nz(t.getTurnoverCity());
            addMidClass(mid, key, city, 3, pass, turn);
            addMidClass(mid, key, "湖北省", 3, pass, turn);
            markMid(pres, key, city, 3);
            markMid(pres, key, "湖北省", 3);
        }
        // ç»´åº¦4:城际城乡网约车 = ç½‘约车拆分结果(与台账「网约车」页同源同法)
        for (WycOrderMonthly o : wycOrderMapper.selectList(null)) {
@@ -2427,12 +2490,18 @@
            try {
                wr = wycSplitCalc.calc(per);
            } catch (Exception e) {
                continue; // è¯¥æœŸç¼ºè®¢å•/pin æˆ–出租源数据,跳过(不影响其它期)
                // B5:不再静默跳过——记录「期 + åŽŸå› ã€å¹¶å†™æ—¥å¿—ï¼Œéšå¯¼å‡ºä½“æ£€ä¸€å¹¶è¾“å‡º
                String msg = per + " ç½‘约车城际城乡维度缺失:" + e.getMessage();
                midIssues.get().add(msg);
                log.warn("loadMidClassMap {}", msg);
                continue;
            }
            for (Map.Entry<String, WycSplitCalc.WycMetrics> en : wr.getByCity().entrySet()) {
                WycSplitCalc.WycMetrics wm = en.getValue();
                addMidClass(mid, key, en.getKey(), 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
                addMidClass(mid, key, "湖北省", 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
                markMid(pres, key, en.getKey(), 4);
                markMid(pres, key, "湖北省", 4);
            }
        }
        return mid;
@@ -2586,15 +2655,11 @@
                title.getCell(10).setCellValue(currentYear + "å¹´" + maxMonth + "月全省分市州完成道路客运生产情况");
            }
            // å…¨çœè¡Œ r3:B3/F3 ä¸º SUM å…¬å¼ä¿ç•™ï¼›D3/H3 åŒæ¯”填值(0-based åˆ— 3/7)
            setValOrBlank(sheet, 2, 3, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, true),
                midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, true)));
            setValOrBlank(sheet, 2, 7, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, false),
                midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, false)));
            setValOrBlank(sheet, 2, 3, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, true));
            setValOrBlank(sheet, 2, 7, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, false));
            // å³å—全省行 N3/R3:当月同比(L3/P3 ä¸º SUM å…¬å¼ä¿ç•™ï¼‰
            setValOrBlank(sheet, 2, 13, yoyOf(midClassVal(mid, currentYear, maxMonth, "湖北省", 0, true),
                midClassVal(mid, currentYear - 1, maxMonth, "湖北省", 0, true)));
            setValOrBlank(sheet, 2, 17, yoyOf(midClassVal(mid, currentYear, maxMonth, "湖北省", 0, false),
                midClassVal(mid, currentYear - 1, maxMonth, "湖北省", 0, false)));
            setValOrBlank(sheet, 2, 13, midMonthYoy(mid, currentYear, maxMonth, "湖北省", 0, true));
            setValOrBlank(sheet, 2, 17, midMonthYoy(mid, currentYear, maxMonth, "湖北省", 0, false));
            // å¸‚州行 r4-20:B/F ç´¯è®¡å€¼ã€D/H åŒæ¯”ï¼›C/E/G/I æŽ’名公式保留
            List<String> cities = RegionUtil.CITY_LIST;
            for (int i = 0; i < cities.size(); i++) {
@@ -2605,18 +2670,18 @@
                double lastPass = midCumClass(mid, currentYear - 1, city, maxMonth, 0, true);
                double lastTurn = midCumClass(mid, currentYear - 1, city, maxMonth, 0, false);
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
                setValOrBlank(sheet, r0, 3, yoyOf(pass, lastPass));
                setValOrBlank(sheet, r0, 3, midCumYoy(mid, currentYear, city, maxMonth, 0, true));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 7, yoyOf(turn, lastTurn));
                setValOrBlank(sheet, r0, 7, midCumYoy(mid, currentYear, city, maxMonth, 0, false));
                // å³å—当月:L/P å€¼ã€N/R å½“月同比(M/O/Q/S æŽ’名公式保留)
                double mPass = midClassVal(mid, currentYear, maxMonth, city, 0, true);
                double mTurn = midClassVal(mid, currentYear, maxMonth, city, 0, false);
                double mLastPass = midClassVal(mid, currentYear - 1, maxMonth, city, 0, true);
                double mLastTurn = midClassVal(mid, currentYear - 1, maxMonth, city, 0, false);
                setValOrBlank(sheet, r0, 11, mPass == 0 ? null : round(mPass, 4));
                setValOrBlank(sheet, r0, 13, mPass == 0 ? null : yoyOf(mPass, mLastPass));
                setValOrBlank(sheet, r0, 13, mPass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, true));
                setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : round(mTurn, 4));
                setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : yoyOf(mTurn, mLastTurn));
                setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, false));
            }
            recalc(wb);
            return toBytes(wb);
@@ -2678,9 +2743,9 @@
                double lastPass = midCumClass(mid, currentYear - 1, "湖北省", maxMonth, dims[i], true);
                double lastTurn = midCumClass(mid, currentYear - 1, "湖北省", maxMonth, dims[i], false);
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
                setValOrBlank(sheet, r0, 2, yoyOf(pass, lastPass));
                setValOrBlank(sheet, r0, 2, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], true));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 6, yoyOf(turn, lastTurn));
                setValOrBlank(sheet, r0, 6, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], false));
            }
            // ä¸‹åŠå—(当月):标题行9、数据行11-15(0-based 8、10-14)
            Row bottomTitle = sheet.getRow(8);
@@ -2694,9 +2759,9 @@
                double lastPass = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], true);
                double lastTurn = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], false);
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
                setValOrBlank(sheet, r0, 2, pass == 0 ? null : yoyOf(pass, lastPass));
                setValOrBlank(sheet, r0, 2, pass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "湖北省", dims[i], true));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 6, turn == 0 ? null : yoyOf(turn, lastTurn));
                setValOrBlank(sheet, r0, 6, turn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "湖北省", dims[i], false));
            }
            // ä¸‹åŠå—占比公式由样例指向上半块(B$3/F$3),改写为本块总行(B$11/F$11),行12-15
            for (int i = 1; i < names.length; i++) {
@@ -4823,6 +4888,8 @@
            case "investFiveYearLogistics": return investFiveYearLogistics(period, mode);
            case "investBillion": return investBillion(period, mode);
            case "investCounty": return investCounty(period, mode);
            case "investPassengerDetail": return exportInvestPassengerDetail(period, mode);
            case "investLogisticsDetail": return exportInvestLogisticsDetail(period, mode);
            case "cityBusDetail": return exportCityBusDetail(period, mode);
            case "cityTaxiDetail": return exportCityTaxiDetail(period, mode);
            case "cityRailFerryDetail": return exportCityRailFerryDetail(period, mode);
@@ -4848,6 +4915,8 @@
        REPORT_FILE_NAMES.put("investFiveYearLogistics", "生成_十五五规划物流项目进展情况.xls");
        REPORT_FILE_NAMES.put("investBillion", "生成_湖北省(客货站场)亿元投资项目.xls");
        REPORT_FILE_NAMES.put("investCounty", "生成_经济强县交通物流基础设施投资统计报表.xls");
        REPORT_FILE_NAMES.put("investPassengerDetail", "生成_客运站投资明细表.xlsx");
        REPORT_FILE_NAMES.put("investLogisticsDetail", "生成_物流站场投资明细.xls");
        REPORT_FILE_NAMES.put("cityBusDetail", "生成_城市公交客运量分市州明细.xlsx");
        REPORT_FILE_NAMES.put("cityTaxiDetail", "生成_巡游出租客运量分市州明细.xlsx");
        REPORT_FILE_NAMES.put("cityRailFerryDetail", "生成_轨道轮渡客运量分市州明细.xlsx");
@@ -5399,6 +5468,355 @@
    }
    // ==================== æŠ•资模块:客运站投资明细表 / ç‰©æµç«™åœºæŠ•资明细(模板底稿原位替换) ====================
    /** ç‰©æµç«™åœºæ˜Žç»†è¡¨æ¨¡æ¿ä¸­çš„市州小节短名 -> åº“内规范全名 */
    private static final Map<String, String> LOGI_CITY_LABELS = new LinkedHashMap<>();
    static {
        LOGI_CITY_LABELS.put("武汉", "武汉市");
        LOGI_CITY_LABELS.put("黄石", "黄石市");
        LOGI_CITY_LABELS.put("十堰", "十堰市");
        LOGI_CITY_LABELS.put("宜昌", "宜昌市");
        LOGI_CITY_LABELS.put("襄阳", "襄阳市");
        LOGI_CITY_LABELS.put("鄂州", "鄂州市");
        LOGI_CITY_LABELS.put("荆门", "荆门市");
        LOGI_CITY_LABELS.put("孝感", "孝感市");
        LOGI_CITY_LABELS.put("荆州", "荆州市");
        LOGI_CITY_LABELS.put("黄冈", "黄冈市");
        LOGI_CITY_LABELS.put("咸宁", "咸宁市");
        LOGI_CITY_LABELS.put("随州", "随州市");
        LOGI_CITY_LABELS.put("恩施", "恩施州");
        LOGI_CITY_LABELS.put("仙桃", "仙桃市");
        LOGI_CITY_LABELS.put("潜江", "潜江市");
        LOGI_CITY_LABELS.put("天门", "天门市");
        LOGI_CITY_LABELS.put("林区", "神农架林区");
    }
    /** è¯¥æœŸå„项目月度数据:projectId -> InvestmentMonthly */
    private Map<Long, InvestmentMonthly> investMonthlyOf(String period) {
        Map<Long, InvestmentMonthly> map = new HashMap<>();
        for (InvestmentMonthly m : investMonthlyMapper.selectList(
                new LambdaQueryWrapper<InvestmentMonthly>().eq(InvestmentMonthly::getReportPeriod, period))) {
            map.put(m.getProjectId(), m);
        }
        return map;
    }
    /** å†™å•元格数值(保留样式/合并):null æ¸…空 */
    private void putNum(Row row, int idx, Double v) {
        if (row == null) return;
        Cell c = row.getCell(idx);
        if (c == null) c = row.createCell(idx);
        c.setBlank();
        if (v != null) c.setCellValue(v);
    }
    /** å†™å•元格文本(保留样式/合并):空串清空 */
    private void putText(Row row, int idx, String v) {
        if (row == null) return;
        Cell c = row.getCell(idx);
        if (c == null) c = row.createCell(idx);
        c.setBlank();
        if (v != null && !v.trim().isEmpty()) c.setCellValue(v.trim());
    }
    /** è¯»å•元格数值(空/非数值/公式无缓存一律 0) */
    private double numOf(Cell c) {
        if (c == null) return 0.0;
        try {
            if (c.getCellType() == CellType.NUMERIC || c.getCellType() == CellType.FORMULA) {
                return c.getNumericCellValue();
            }
            String s = cellText(c);
            if (s == null) return 0.0;
            s = s.replace(",", "").trim();
            return s.isEmpty() ? 0.0 : Double.parseDouble(s);
        } catch (Exception e) {
            return 0.0;
        }
    }
    /** æ¨¡æ¿æ ‡é¢˜é‡Œçš„「(N月月报)」「(N月)」改为报表期月份 */
    private void fixInvestMonthTitle(Sheet sheet, int rowIdx, int colIdx, int month) {
        Row row = sheet.getRow(rowIdx);
        if (row == null) return;
        String t = cellText(row.getCell(colIdx));
        if (t == null) return;
        String nv = t.replaceAll("(\\d+月月报)", "(" + month + "月月报)")
                      .replaceAll("(\\d+月)", "(" + month + "月)");
        if (!nv.equals(t)) putText(row, colIdx, nv);
    }
    /** æ¨¡æ¿è¡Œ A åˆ—是否为「一二三四、」渠道分组标题 */
    private boolean isInvestChannelLabel(String a) {
        return a != null && a.matches("^[一二三四五六七八九十]+、.*");
    }
    /**
     * ç”Ÿæˆ_X月客运站投资明细表.xlsx(模板:docs/投资/模板/模板_X月客运站投资明细表.xlsx,sheet:明细 + æ±‡æ€»ï¼‰ã€‚
     * å£å¾„:模板即上月台账底稿;本月只覆盖「库内有该期数据」的项目行
     * ï¼ˆH自开始建设累计 / I本年计划 / J自年初累计 / K本月完成 / L建设阶段 / M形象进度),
     * åº“内无该期的项目行保留母版原值(等同人工「上月结转 + æœ¬æœˆæ›´æ–°ã€ï¼‰ï¼›
     * åŸŽå¸‚/渠道/合计行沿用模板 SUM å…¬å¼ï¼Œéšè¡¨å†…数值自动重算。
     */
    public byte[] exportInvestPassengerDetail(String period, String mode) throws Exception {
        int year = Integer.parseInt(period.split("-")[0]);
        int month = monthOf(period, mode);
        List<InvestmentProject> projects = investProjectMapper.selectList(
                new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "客运站场"));
        Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period);
        File tpl = resolveAnyTemplate(investTemplateDir, "模板_X月客运站投资明细表.xlsx");
        try (InputStream in = new FileInputStream(tpl);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
            Sheet sheet = wb.getSheet("明细");
            if (sheet == null) throw new RuntimeException("模板《模板_X月客运站投资明细表.xlsx》缺少「明细」工作表");
            fixInvestMonthTitle(sheet, 0, 1, month);
            String curCity = null;
            Set<Long> used = new HashSet<>();
            int filled = 0, kept = 0, dup = 0;
            for (int r = 8; r <= sheet.getLastRowNum(); r++) {
                Row row = sheet.getRow(r);
                if (row == null) continue;
                String a = nvl(cellText(row.getCell(0)));
                String b = nvl(cellText(row.getCell(1)));
                if (isCityLabel(a)) { curCity = templateCityFull(a); continue; }
                if (isInvestChannelLabel(a)) continue;
                if (a.contains("合计") || a.contains("总计")) continue;
                if (!a.matches("\\d+(\\.\\d+)?")) continue;
                if (b.isEmpty() || curCity == null) continue;
                InvestmentProject p = matchInvest(projects, b, curCity, "客运站场", used);
                InvestmentMonthly m = p == null ? null : mByProject.get(p.getId());
                if (p == null || m == null) {
                    kept++;
                    // æœ‰åº“内项目但当月无数据 â†’ è®°ä¸ºå¾…核对;无匹配项目 â†’ ç»“转上月
                    if (p != null) dup++;
                    continue;
                }
                used.add(p.getId());
                // å£å¾„(用户 2026-09-18 ç¡®è®¤ï¼‰ï¼šåªè¦†ç›–人工会随月报更新的四列——
                // H è‡ªå¼€å§‹å»ºè®¾ç´¯è®¡ / J è‡ªå¹´åˆç´¯è®¡ / K æœ¬æœˆå®Œæˆ / M å½¢è±¡è¿›åº¦ï¼›
                // I æœ¬å¹´è®¡åˆ’投资、L å»ºè®¾é˜¶æ®µã€G è®¡åˆ’总投资为省定/母版口径,一律不动;库内为空则保留母版原值
                if (m.getStartCum() != null) putNum(row, 7, m.getStartCum());
                if (m.getYearCum() != null) putNum(row, 9, m.getYearCum());
                if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone());
                if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc());
                filled++;
            }
            Sheet sumSheet = wb.getSheet("汇总");
            if (sumSheet != null) {
                Row r2 = sumSheet.getRow(1);
                if (r2 != null) {
                    Cell c = r2.getCell(0);
                    if (c == null) c = r2.createCell(0);
                    c.setBlank();
                    c.setCellValue(java.time.LocalDate.of(year, month, 1).atStartOfDay());
                }
            }
            try {
                wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
            } catch (Exception e) {
                log.warn("客运站投资明细表公式求值失败: {}", e.getMessage());
            }
            log.info("客运站投资明细表 {}:写入 {} è¡Œï¼Œç»“转母版原值 {} è¡Œï¼ˆå…¶ä¸­åº“内有项目但当月无数据 {} è¡Œï¼‰",
                    period, filled, kept, dup);
            return toBytes(wb);
        }
    }
    /**
     * ç”Ÿæˆ_X月物流站场投资明细.xls(模板:docs/投资/模板/模板_X月物流站场投资明细.xls)。
     * è¦†ç›–「 åˆ†é¡¹ç›®æŠ•资完成情况」项目行(口径同客运站明细表),并按表内数值重算
     * æ¸ é“行 / å¸‚州行 / åˆè®¡è¡Œï¼ŒåŒæ­¥åˆ·æ–°ã€Œåˆ†å¸‚州规划内外项目完成情况」「市州汇总」两张汇总页。
     */
    public byte[] exportInvestLogisticsDetail(String period, String mode) throws Exception {
        int year = Integer.parseInt(period.split("-")[0]);
        int month = monthOf(period, mode);
        List<InvestmentProject> projects = investProjectMapper.selectList(
                new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "物流园区"));
        Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period);
        HSSFWorkbook wb = loadInvestTemplate("模板_X月物流站场投资明细.xls");
        HSSFSheet sheet = wb.getSheet(" åˆ†é¡¹ç›®æŠ•资完成情况");
        if (sheet == null) throw new RuntimeException("模板《模板_X月物流站场投资明细.xls》缺少「 åˆ†é¡¹ç›®æŠ•资完成情况」工作表");
        fixInvestMonthTitle(sheet, 0, 0, month);
        Row head = sheet.getRow(3);
        if (head != null) {
            String t = cellText(head.getCell(0));
            if (t != null) {
                String nv = t.replaceAll("\\d{4}å¹´\\d+月", year + "å¹´" + month + "月");
                if (!nv.equals(t)) putText(head, 0, nv);
            }
        }
        // 1) ç»“构扫描:市州小节行
        List<Integer> cityRows = new ArrayList<>();
        for (int r = 7; r <= sheet.getLastRowNum(); r++) {
            HSSFRow row = sheet.getRow(r);
            if (row == null) continue;
            String a = nvl(cellText(row.getCell(0)));
            String b = nvl(cellText(row.getCell(1)));
            if (LOGI_CITY_LABELS.containsKey(a) && b.isEmpty()) cityRows.add(r);
        }
        Map<String, double[]> citySums = new LinkedHashMap<>();   // å…¨å -> G,H,I,J,K
        Map<String, double[]> cityStats = new LinkedHashMap<>();  // å…¨å -> {总数, ç´¯è®¡, è§„划内数, è§„划内累计, è§„划外数, è§„划外累计}
        Set<Long> used = new HashSet<>();
        int filled = 0, kept = 0;
        for (int i = 0; i < cityRows.size(); i++) {
            int cr = cityRows.get(i);
            String cityLabel = nvl(cellText(sheet.getRow(cr).getCell(0)));
            String city = LOGI_CITY_LABELS.getOrDefault(cityLabel, cityLabel);
            int start = cr + 1;
            int end = (i + 1 < cityRows.size() ? cityRows.get(i + 1) : sheet.getLastRowNum() + 1) - 1;
            List<Integer> chRows = new ArrayList<>();
            for (int r = start; r <= end; r++) {
                HSSFRow row = sheet.getRow(r);
                if (row == null) continue;
                if (isInvestChannelLabel(nvl(cellText(row.getCell(0))))) chRows.add(r);
            }
            // (1) å…ˆæŒ‰é¡¹ç›®å+市州 è¦†ç›–有库内当期数据的项目行
            for (int r = start; r <= end; r++) {
                HSSFRow row = sheet.getRow(r);
                if (row == null) continue;
                String a = nvl(cellText(row.getCell(0)));
                String b = nvl(cellText(row.getCell(1)));
                if (isInvestChannelLabel(a)) continue;
                if (!a.matches("\\d+(\\.\\d+)?")) continue;
                if (b.isEmpty()) continue;
                InvestmentProject p = matchInvest(projects, b, city, "物流园区", used);
                InvestmentMonthly m = p == null ? null : mByProject.get(p.getId());
                if (p == null || m == null) { kept++; continue; }
                used.add(p.getId());
                // å£å¾„同客运站明细表:只覆盖 H è‡ªå¼€å§‹å»ºè®¾ç´¯è®¡ / J è‡ªå¹´åˆç´¯è®¡ / K æœ¬æœˆå®Œæˆ / M å½¢è±¡è¿›åº¦
                if (m.getStartCum() != null) putNum(row, 7, m.getStartCum());
                if (m.getYearCum() != null) putNum(row, 9, m.getYearCum());
                if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone());
                if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc());
                filled++;
            }
            // (2) å†æŒ‰æ¸ é“块汇总,并写回渠道行 / å¸‚州行
            double[] citySum = new double[5];
            double[] stat = new double[6];
            for (int j = 0; j < chRows.size(); j++) {
                int r0 = chRows.get(j);
                int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1;
                boolean planned = !nvl(cellText(sheet.getRow(r0).getCell(0))).startsWith("四、");
                double[] s = new double[5];
                for (int r = r0 + 1; r <= r1; r++) {
                    HSSFRow row = sheet.getRow(r);
                    if (row == null) continue;
                    String a = nvl(cellText(row.getCell(0)));
                    String b = nvl(cellText(row.getCell(1)));
                    if (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue;
                    if (b.isEmpty()) continue;
                    for (int k = 0; k < 5; k++) s[k] += numOf(row.getCell(6 + k));
                    stat[0] += 1;
                    stat[1] += numOf(row.getCell(9));
                    if (planned) { stat[2] += 1; stat[3] += numOf(row.getCell(9)); }
                    else { stat[4] += 1; stat[5] += numOf(row.getCell(9)); }
                }
                HSSFRow crow = sheet.getRow(r0);
                for (int k = 0; k < 5; k++) putNum(crow, 6 + k, s[k] == 0 ? null : s[k]);
                for (int k = 0; k < 5; k++) citySum[k] += s[k];
            }
            // æ¸ é“块之外的游离项目行(模板未分组时)也计入市州合计
            for (int r = start; r <= end; r++) {
                HSSFRow row = sheet.getRow(r);
                if (row == null) continue;
                String a = nvl(cellText(row.getCell(0)));
                String b = nvl(cellText(row.getCell(1)));
                if (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue;
                if (b.isEmpty()) continue;
                boolean inChannel = false;
                for (int j = 0; j < chRows.size(); j++) {
                    int r0 = chRows.get(j);
                    int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1;
                    if (r > r0 && r <= r1) { inChannel = true; break; }
                }
                if (inChannel) continue;
                for (int k = 0; k < 5; k++) citySum[k] += numOf(row.getCell(6 + k));
                stat[0] += 1;
                stat[1] += numOf(row.getCell(9));
            }
            HSSFRow cityRow = sheet.getRow(cr);
            for (int k = 0; k < 5; k++) putNum(cityRow, 6 + k, citySum[k] == 0 ? null : citySum[k]);
            citySums.put(city, citySum);
            cityStats.put(city, stat);
        }
        // 2) åˆè®¡è¡Œï¼ˆç¬¬ 7 è¡Œï¼‰
        double[] total = new double[5];
        for (double[] s : citySums.values()) {
            for (int k = 0; k < 5; k++) total[k] += s[k];
        }
        for (int k = 0; k < 5; k++) putNum(sheet.getRow(6), 6 + k, total[k] == 0 ? null : total[k]);
        // 3) åˆ†å¸‚州规划内外项目完成情况
        HSSFSheet citySheet = wb.getSheet("分市州规划内外项目完成情况");
        if (citySheet != null) {
            Row t1 = citySheet.getRow(0);
            if (t1 != null) {
                String t = cellText(t1.getCell(0));
                if (t != null) {
                    String nv = t.replaceAll("\\d{4}å¹´", year + "å¹´");
                    if (!nv.equals(t)) putText(t1, 0, nv);
                }
            }
            double[] st = new double[6];
            for (int r = 3; r <= citySheet.getLastRowNum(); r++) {
                HSSFRow row = citySheet.getRow(r);
                if (row == null) continue;
                String a = nvl(cellText(row.getCell(0)));
                String b = nvl(cellText(row.getCell(1)));
                if (a.contains("合计") || b.contains("合计")) {
                    for (int k = 0; k < 6; k++) putNum(row, 2 + k, st[k] == 0 ? null : st[k]);
                    continue;
                }
                double[] s = cityStats.get(LOGI_CITY_LABELS.getOrDefault(b, b));
                if (s == null) continue;
                for (int k = 0; k < 6; k++) {
                    putNum(row, 2 + k, s[k] == 0 ? null : s[k]);
                    st[k] += s[k];
                }
            }
        }
        // 4) å¸‚州汇总(B å¹´åº¦è®¡åˆ’投资为官方口径,保持模板原值;C/D/E æŒ‰åˆ†é¡¹ç›®é¡µé‡ç®—)
        HSSFSheet sumSheet = wb.getSheet("市州汇总");
        if (sumSheet != null) {
            HSSFRow r2 = sumSheet.getRow(1);
            if (r2 != null) {
                putNum(r2, 0, (double) org.apache.poi.ss.usermodel.DateUtil.getExcelDate(
                        java.time.LocalDate.of(year, month, 1).atStartOfDay()));
            }
            double cT = 0.0, dT = 0.0;
            for (int r = 7; r <= 23; r++) {
                HSSFRow row = sumSheet.getRow(r);
                if (row == null) continue;
                String a = nvl(cellText(row.getCell(0)));
                double[] s = citySums.get(LOGI_CITY_LABELS.getOrDefault(a, a));
                if (s == null) continue;
                putNum(row, 2, s[3] == 0 ? null : s[3]);
                putNum(row, 3, s[4] == 0 ? null : s[4]);
                double b = numOf(row.getCell(1));
                putNum(row, 4, b > 0 ? round(s[3] / b * 100, 4) : null);
                cT += s[3];
                dT += s[4];
            }
            HSSFRow tot = sumSheet.getRow(6);
            if (tot != null) {
                putNum(tot, 2, cT == 0 ? null : cT);
                putNum(tot, 3, dT == 0 ? null : dT);
                double b = numOf(tot.getCell(1));
                putNum(tot, 4, b > 0 ? round(cT / b * 100, 4) : null);
            }
        }
        try {
            wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
        } catch (Exception e) {
            log.warn("物流站场投资明细公式求值失败: {}", e.getMessage());
        }
        log.info("物流站场投资明细 {}:写入 {} è¡Œï¼Œç»“转母版原值 {} è¡Œ", period, filled, kept);
        return toBytesHssf(wb);
    }
    private static class InvestAgg {
        double total, startCum, yearPlan, yearCum;
traffic-audit-web/src/views/DataImport.vue
@@ -37,7 +37,7 @@
                                <div class="module-sel-row">
                  <el-select v-model="moduleSel[g.key]" placeholder="选择数据类型" style="width:260px"
                             @change="onModuleChange(g.key)" clearable>
                    <el-option v-for="t in g.types" :key="t.value" :label="t.label" :value="t.value"></el-option>
                    <el-option v-for="t in typeList(g)" :key="t.value" :label="t.label" :value="t.value"></el-option>
                  </el-select>
                  <div v-if="importType" class="tpl-box">
                    <span class="tpl-name">模板:{{ templateName }}</span>
@@ -114,7 +114,7 @@
                <div v-else class="overview-grid">
                  <div class="ov-row" v-for="g in visibleGroups" :key="g.key">
                    <div class="ov-group">{{ g.name }}</div>
                    <div class="ov-item" v-for="t in g.types" :key="t.value" :class="overviewState(t.value).cls">
                    <div class="ov-item" v-for="t in typeList(g)" :key="t.value" :class="overviewState(t.value).cls">
                      <div class="ov-name">{{ t.label }}</div>
                      <div class="ov-status">{{ overviewState(t.value).text }}</div>
                    </div>
@@ -134,7 +134,7 @@
            <div v-if="clearScope === 'types'" class="clear-types">
              <el-select v-model="clearTypes" multiple filterable collapse-tags placeholder="选择要清空的数据类型" style="width:100%">
                <el-option-group v-for="g in clearableGroups()" :key="g.key" :label="g.name">
                  <el-option v-for="t in g.types" :key="t.value" :value="t.value"
                  <el-option v-for="t in typeList(g)" :key="t.value" :value="t.value"
                             :label="t.label + '(' + overviewState(t.value).text + ')'"></el-option>
                </el-option-group>
              </el-select>
@@ -226,7 +226,7 @@
            { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' },
            { value: 'wycTotal', label: '网约车总数' },
            { value: 'wycOrder', label: '网约车订单及全省总量' },
            { value: 'wycOrder', label: '网约车订单及全省总量', hidden: true },
            { value: 'wycOrderReport', label: '网约车订单报告' }
          ]
        },
@@ -584,6 +584,10 @@
    clearableGroups() {
      return this.groups.filter(g => !g.hidden)
    },
    // å•个数据类型也可隐藏(hidden: true):入口、概览、清空下拉统一过滤
    typeList(g) {
      return (g && g.types ? g.types : []).filter(t => !t.hidden)
    },
    confirmClear() {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      if (this.clearing) return
traffic-audit-web/src/views/ReportExport.vue
@@ -225,7 +225,9 @@
            { key: 'investPlan', label: '全省预安排计划进度汇总', fileName: '生成_全省预安排计划进度情况汇总.xls', status: null, statusNote: '' },
            { key: 'investFiveYearLogistics', label: '十五五规划物流项目进展', fileName: '生成_十五五规划物流项目进展情况.xls', status: null, statusNote: '' },
            { key: 'investBillion', label: '亿元投资项目', fileName: '生成_湖北省(客货站场)亿元投资项目.xls', status: null, statusNote: '' },
            { key: 'investCounty', label: '经济强县项目投资统计', fileName: '生成_经济强县交通物流基础设施投资统计报表.xls', status: null, statusNote: '' }
            { key: 'investCounty', label: '经济强县项目投资统计', fileName: '生成_经济强县交通物流基础设施投资统计报表.xls', status: null, statusNote: '' },
            { key: 'investPassengerDetail', label: '客运站投资明细表', fileName: '生成_X月客运站投资明细表.xlsx', status: null, statusNote: '' },
            { key: 'investLogisticsDetail', label: '物流站场投资明细', fileName: '生成_X月物流站场投资明细.xls', status: null, statusNote: '' }
          ], checked: []
        },
        {
@@ -476,7 +478,7 @@
      const doExport = () => {
        if (items.length === 1) {
          api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 })
            .then(res => { this.downloadBlob(res, items[0].fileName); this.$message.success('报表导出成功'); this.loading = '' })
            .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); this.$message.success('报表导出成功'); this.loading = '' })
            .catch(err => this.failWith(err))
          return
        }
@@ -547,6 +549,13 @@
      }
      return text.length > 200 ? text.slice(0, 200) + '…' : text
    },
    // æ–‡ä»¶åå ä½ç¬¦ X月 -> å½“前报表期月份(如「生成_X月客运站投资明细表.xlsx」->「生成_8月客运站投资明细表.xlsx」)
    fileNameOf(item) {
      const name = (item && item.fileName) || ''
      const mm = parseInt((this.period || '').split('-')[1], 10)
      if (!mm) return name
      return name.replace(/X月/g, mm + '月')
    },
    downloadBlob(res, fileName) {
      const isZip = /\.zip$/.test(fileName)
      const isXls = /\.xls$/.test(fileName)