表格分类统计及排序

各工点提交的电动风阀及防火阀需要进行分类统计汇总。编写了一个通用工具。

其中导出列索引为需要导出的列。

排序列索引为需要进行排序的列,其权重为最前的权重最高。

汇总列为需要累加的列。

java 复制代码
package ghs.easyexcel.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import ghs.easyexcel.config.ExcelSummaryConfig;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.*;

/**
 * Excel通用汇总工具类(核心逻辑)
 * 功能:分类小结、排序列全局统计、手动表头、数据排序
 */
public class GenericExcelSummaryUtil {
    private static final Logger log = LoggerFactory.getLogger(GenericExcelSummaryUtil.class);

    // 原始数据(行号→列索引→值)
    private final Map<Integer, Map<Integer, Object>> rawDataMap = new LinkedHashMap<>();
    // 分类数据(分类值→数据行列表)
    private final Map<Object, List<Map<Integer, Object>>> categoryDataMap = new LinkedHashMap<>();
    // 全局总计(汇总列索引→值)
    private final Map<Integer, Double> globalSummaryMap = new HashMap<>();
    // 源文件第二行表头(列索引→表头名)
    private Map<Integer, String> sourceHeaderMap = new HashMap<>();
    // 排序列全局统计(排序列值→汇总列索引→值)
    private final Map<Object, Map<Integer, Double>> sortColumnGlobalSummaryMap = new HashMap<>();
    // 配置类
    private final ExcelSummaryConfig config;
    // 缓存:导出列List(避免重复转换)
    private List<Integer> exportColumnList;
    // 缓存:汇总列Set(快速判断)
    private Set<Integer> summaryColumnSet;

    // 构造方法:初始化配置和缓存
    public GenericExcelSummaryUtil(ExcelSummaryConfig config) {
        this.config = config;
        this.exportColumnList = config.getExportColumnIndexList();
        this.summaryColumnSet = new HashSet<>(config.getSummaryColumnIndexList());
        // 初始化全局总计为0
        for (Integer colIdx : summaryColumnSet) {
            globalSummaryMap.put(colIdx, 0.0);
        }
        log.info("📌 工具类初始化完成!导出列:{},汇总列:{}", exportColumnList, summaryColumnSet);
    }

    /**
     * 主流程:执行汇总(验证→读数据→分组→生成数据→写Excel)
     */
    public void executeSummary() throws Exception {
        validateConfigAndFile();
        readRawExcelData();
        groupByCategoryAndCalcStat();
        List<Map<Integer, Object>> summaryData = generateSummaryData();
        writeSummaryExcel(summaryData);
        log.info("🎉 汇总完成!源文件:{},目标文件:{}", config.getSourceFilePath(), config.getTargetFilePath());
    }

    // ------------------------------ 步骤1:验证配置和文件 ------------------------------
    private void validateConfigAndFile() {
        // 验证文件
        File sourceFile = new File(config.getSourceFilePath());
        if (!sourceFile.exists()) {
            throw new RuntimeException("源文件不存在:" + config.getSourceFilePath());
        }
        if (!sourceFile.getName().endsWith(".xlsx") && !sourceFile.getName().endsWith(".xls")) {
            throw new RuntimeException("源文件格式错误(仅支持.xlsx/.xls):" + config.getSourceFilePath());
        }
        // 验证配置
        if (exportColumnList.isEmpty()) {
            throw new RuntimeException("导出列索引不能为空!");
        }
        if (summaryColumnSet.isEmpty()) {
            throw new RuntimeException("汇总列索引不能为空!");
        }
        if (config.getSortColumnIndex() < 0) {
            throw new RuntimeException("排序列索引不能为负数!");
        }
        // 检查所有排序列索引
        for (Integer sc : config.getSortColumnIndexList()) {
            if (sc < 0) {
                throw new RuntimeException("排序列索引不能为负数:" + sc);
            }
        }
        log.info("✅ 配置和文件验证通过");
    }

    // ------------------------------ 步骤2:读取源Excel数据(含表头) ------------------------------
    private void readRawExcelData() {
        EasyExcel.read(config.getSourceFilePath(), new AnalysisEventListener<Map<Integer, Object>>() {
            private int rowNum = 0; // 源文件原始行号(从1开始)

            @Override
            public void invoke(Map<Integer, Object> rowData, AnalysisContext context) {
                rowNum++;
                // 捕获源文件第二行作为表头(清理特殊字符)
                if (rowNum == 2) {
                    sourceHeaderMap = new HashMap<>();
                    for (Map.Entry<Integer, Object> entry : rowData.entrySet()) {
                        int colIdx = entry.getKey();
                        String header = entry.getValue() == null ? "" : entry.getValue().toString().trim()
                                .replaceAll("[\r\n\t\f]", "") // 清理换行/制表符/换页符
                                .replaceAll("\\s+", " ");    // 多个空格转单个
                        sourceHeaderMap.put(colIdx, header);
                    }
                    log.info("📝 捕获源文件第二行表头:{}", sourceHeaderMap);
                    return;
                }

                // 跳过配置的行数(如前2行)
                if (rowNum <= config.getSkipRows()) {
                    log.debug("🔄 跳过第{}行(配置跳过前{}行)", rowNum, config.getSkipRows());
                    return;
                }

                // 过滤导出列数据
                Map<Integer, Object> filteredRow = new LinkedHashMap<>();
                for (Integer colIdx : exportColumnList) {
                    Object value = rowData.get(colIdx);
                    filteredRow.put(colIdx, value == null ? "" : value.toString().trim());
                }

                // 跳过空行
                boolean isEmptyRow = filteredRow.values().stream()
                        .map(Object::toString)
                        .allMatch(StringUtils::isEmpty);
                if (isEmptyRow) {
                    log.debug("🔄 跳过第{}行(空行)", rowNum);
                    return;
                }

                rawDataMap.put(rowNum, filteredRow);
                log.debug("📥 读取第{}行数据:{}", rowNum, filteredRow);
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext context) {
                if (sourceHeaderMap.isEmpty()) {
                    log.warn("⚠️  未捕获到源文件第二行表头,将使用默认列名(列_索引)");
                }
                log.info("📊 源Excel读取完成!有效数据行数:{}", rawDataMap.size());
            }
        }).headRowNumber(0).sheet().doRead(); // headRowNumber(0):按索引读所有行
    }

    // ------------------------------ 步骤3:按所有非汇总导出列联合分组 + 计算统计值 ------------------------------
    private void groupByCategoryAndCalcStat() {
        int sortColIdx = config.getSortColumnIndex();
        Map<Object, Map<Integer, Double>> sortStatMap = new HashMap<>();

        // 确定分组列:所有非汇总的导出列
        List<Integer> groupKeyColumns = new ArrayList<>();
        for (Integer colIdx : exportColumnList) {
            if (!summaryColumnSet.contains(colIdx)) {
                groupKeyColumns.add(colIdx);
            }
        }
        log.info("📌 分组列(非汇总导出列):{}", groupKeyColumns);

        // 遍历原始数据,分组并累加统计
        for (Map<Integer, Object> rowData : rawDataMap.values()) {
            // 1. 构建分组键(所有非汇总导出列的值拼接)
            StringBuilder keyBuilder = new StringBuilder();
            for (int i = 0; i < groupKeyColumns.size(); i++) {
                Object val = rowData.get(groupKeyColumns.get(i));
                String strVal = val == null ? "" : val.toString().trim();
                if (i > 0) keyBuilder.append("|||");
                keyBuilder.append(strVal);
            }
            String groupKey = keyBuilder.toString();
            if (groupKey.replaceAll("\\|\\|\\|", "").trim().isEmpty()) {
                log.warn("⚠️  分组键全为空,跳过行:{}", rowData);
                continue;
            }

            // 2. 按联合分组键分组
            categoryDataMap.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(rowData);

            // 3. 处理排序列(如耐温要求)
            final String sortVal = rowData.get(sortColIdx) == null ? "" : rowData.get(sortColIdx).toString().trim();
            final String sortValFinal = StringUtils.isEmpty(sortVal) ? "未分类" : sortVal;

            // 4. 累加全局总计和排序列统计
            for (Integer sumColIdx : summaryColumnSet) {
                double value = parseDouble(rowData.get(sumColIdx), 0.0);
                // 全局总计
                globalSummaryMap.put(sumColIdx, globalSummaryMap.get(sumColIdx) + value);
                // 排序列统计
                sortStatMap.computeIfAbsent(sortValFinal, k -> new HashMap<>());
                Map<Integer, Double> colStat = sortStatMap.get(sortValFinal);
                colStat.put(sumColIdx, colStat.getOrDefault(sumColIdx, 0.0) + value);
            }
        }

        // 保存排序列统计结果
        sortColumnGlobalSummaryMap.clear();
        sortColumnGlobalSummaryMap.putAll(sortStatMap);

        // 分类内按排序列排序(支持多列)
        List<Integer> sortColIdxList = config.getSortColumnIndexList();
        categoryDataMap.forEach((catVal, rowList) -> rowList.sort((r1, r2) -> compareBySortColumns(r1, r2, sortColIdxList)));

        log.info("📊 分组和统计完成!分组数:{},排序列统计类型数:{}",
                categoryDataMap.size(), sortColumnGlobalSummaryMap.size());
    }

    // ------------------------------ 步骤4:生成汇总数据(每组合并为一行,汇总列相加) ------------------------------
    private List<Map<Integer, Object>> generateSummaryData() {
        List<Map<Integer, Object>> summaryList = new ArrayList<>();
        int serialNum = 0;

        // 每个分组合并为一行:非汇总列保持原值,汇总列求和
        for (Map.Entry<Object, List<Map<Integer, Object>>> entry : categoryDataMap.entrySet()) {
            List<Map<Integer, Object>> rowList = entry.getValue();
            serialNum++;

            Map<Integer, Object> mergedRow = new LinkedHashMap<>();
            mergedRow.put(-1, serialNum); // 序号

            for (Integer colIdx : exportColumnList) {
                if (summaryColumnSet.contains(colIdx)) {
                    // 汇总列:求和
                    double total = rowList.stream().mapToDouble(r -> parseDouble(r.get(colIdx), 0.0)).sum();
                    mergedRow.put(colIdx, config.formatNumber(total));
                } else {
                    // 非汇总列:取第一行的值(同组值相同)
                    mergedRow.put(colIdx, rowList.get(0).get(colIdx));
                }
            }
            mergedRow.put(-2, "-"); // 备注列
            summaryList.add(mergedRow);
        }

        // 按排序列索引多列排序(优先级按输入顺序)
        List<Integer> sortColIdxList = config.getSortColumnIndexList();
        summaryList.sort((r1, r2) -> compareBySortColumns(r1, r2, sortColIdxList));

        // 排序后重新编序号
        for (int i = 0; i < summaryList.size(); i++) {
            summaryList.get(i).put(-1, i + 1);
        }

        // 最后一行:全局小结,所有汇总列的数值加一起
        Map<Integer, Object> totalRow = new LinkedHashMap<>();
        totalRow.put(-1, "");
        for (Integer colIdx : exportColumnList) {
            if (summaryColumnSet.contains(colIdx)) {
                double grandTotal = summaryList.stream()
                        .mapToDouble(r -> parseDouble(r.get(colIdx), 0.0))
                        .sum();
                totalRow.put(colIdx, config.formatNumber(grandTotal));
            } else {
                totalRow.put(colIdx, "");
            }
        }
        totalRow.put(-2, config.getGlobalTotalRemark());
        summaryList.add(totalRow);

        log.info("📋 汇总数据生成完成!合并后行数:{}(共{}个分组,已按排序列{}排序,含全局小结行)",
                summaryList.size(), categoryDataMap.size(), sortColIdxList);
        return summaryList;
    }

    // ------------------------------ 步骤5:写入汇总Excel(手动表头) ------------------------------
 // 步骤5:写入汇总Excel(手动表头) - 修复后
 // 步骤5:写入汇总Excel(手动表头) - 最终修复版
 // 步骤5:写入汇总Excel(兜底方案:POI手动写入表头)
    private void writeSummaryExcel(List<Map<Integer, Object>> summaryData) {
        // 1. 准备表头内容 (EasyExcel 格式:List<List<String>>)
        List<List<String>> head = new ArrayList<>();
        
        // 序号列表头
        List<String> head0 = new ArrayList<>();
        head0.add(config.getSerialNumberColumnName());
        head.add(head0);
        
        // 导出列对应的表头
        for (Integer colIdx : exportColumnList) {
            List<String> h = new ArrayList<>();
            h.add(sourceHeaderMap.getOrDefault(colIdx, "列_" + colIdx));
            head.add(h);
        }
        
        // 备注列表头
        List<String> headLast = new ArrayList<>();
        headLast.add("备注");
        head.add(headLast);

        // 2. 准备目标文件
        File targetFile = new File(config.getTargetFilePath());
        if (!targetFile.getParentFile().exists()) {
            targetFile.getParentFile().mkdirs();
        }

        // 3. 使用 EasyExcel 统一写入表头和数据
        EasyExcel.write(targetFile)
                .head(head) // 设置表头结构
                .registerWriteHandler(getContentCellStyleStrategy()) // 设置单元格样式
                .registerWriteHandler(new CellWriteHandler() {
                    @Override
                    public void afterCellDispose(CellWriteHandlerContext context) {
                        Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
                        Cell cell = context.getCell();
                        Row row = context.getRow();
                        
                        // 1. 处理中西文双字体逻辑
                        CellStyle cellStyle = cell.getCellStyle();
                        Font font = workbook.getFontAt(cellStyle.getFontIndexAsInt());
                        
                        if (font instanceof XSSFFont) {
                            XSSFFont xssfFont = (XSSFFont) font;
                            CTFont ctFont = xssfFont.getCTFont();
                            
                            if (ctFont.sizeOfNameArray() == 0) {
                                ctFont.addNewName().setVal("Times New Roman");
                            } else {
                                ctFont.getNameArray(0).setVal("Times New Roman");
                            }
                            
                            if (ctFont.sizeOfCharsetArray() == 0) {
                                ctFont.addNewCharset().setVal(134);
                            } else {
                                ctFont.getCharsetArray(0).setVal(134);
                            }
                            
                            if (ctFont.sizeOfFamilyArray() == 0) {
                                ctFont.addNewFamily().setVal(2);
                            } else {
                                ctFont.getFamilyArray(0).setVal(2);
                            }
                        }

                        // 2. 特殊逻辑:如果是空行(用于分隔的空行),移除边框
                        if (!context.getHead()) {
                            Cell snCell = row.getCell(0);
                            int remarkColIdx = exportColumnList.size() + 1;
                            Cell remarkCell = row.getCell(remarkColIdx);
                            
                            boolean isSeparatorRow = isCellEmpty(snCell) && isCellEmpty(remarkCell);
                            
                            if (isSeparatorRow) {
                                // 创建一个无边框的样式(建议在实际项目中缓存此样式以提高性能)
                                CellStyle noBorderStyle = workbook.createCellStyle();
                                noBorderStyle.cloneStyleFrom(cellStyle);
                                noBorderStyle.setBorderLeft(BorderStyle.NONE);
                                noBorderStyle.setBorderRight(BorderStyle.NONE);
                                noBorderStyle.setBorderTop(BorderStyle.NONE);
                                noBorderStyle.setBorderBottom(BorderStyle.NONE);
                                cell.setCellStyle(noBorderStyle);
                            }
                        }
                    }
                    
                    private boolean isCellEmpty(Cell cell) {
                        if (cell == null) return true;
                        if (cell.getCellType() == CellType.BLANK) return true;
                        if (cell.getCellType() == CellType.STRING) return cell.getStringCellValue().trim().isEmpty();
                        return false;
                    }
                })
                .registerWriteHandler(new RowWriteHandler() {
                    @Override
                    public void afterRowDispose(RowWriteHandlerContext context) {
                        // 仅处理数据行(非表头)
                        if (!context.getHead()) {
                            Row row = context.getRow();
                            // 获取备注列(最后一列)的值。索引:1(序号) + exportColumnList.size()
                            int remarkColIdx = exportColumnList.size() + 1;
                            Cell remarkCell = row.getCell(remarkColIdx);
                            
                            if (remarkCell != null && remarkCell.getCellType() == CellType.STRING 
                                    && config.getCategorySummaryRemark().equals(remarkCell.getStringCellValue())) {
                                
                                // 找到最后一个非汇总列在导出列表中的位置
                                int lastNonSummaryPosInExport = -1;
                                for (int i = exportColumnList.size() - 1; i >= 0; i--) {
                                    if (!summaryColumnSet.contains(exportColumnList.get(i))) {
                                        lastNonSummaryPosInExport = i;
                                        break;
                                    }
                                }
                                
                                if (lastNonSummaryPosInExport >= 0) {
                                    // 合并范围:从第一个导出列到最后一个非汇总列
                                    // Excel列坐标 = 导出列表位置 + 1(+1是因为第0列是序号)
                                    int startCol = 1; // 第一个导出列
                                    int endCol = lastNonSummaryPosInExport + 1; // 最后一个非汇总列
                                    
                                    if (endCol > startCol) {
                                        Sheet sheet = context.getWriteSheetHolder().getSheet();
                                        // 执行合并:CellRangeAddress(行号, 行号, 起始列, 结束列)
                                        sheet.addMergedRegion(new CellRangeAddress(row.getRowNum(), row.getRowNum(), startCol, endCol));
                                        
                                        // 合并后将最后一个单元格居中显示(小结文字在最后一个单元格)
                                        Cell lastCell = row.getCell(endCol);
                                        if (lastCell != null) {
                                            CellStyle style = sheet.getWorkbook().createCellStyle();
                                            style.cloneStyleFrom(lastCell.getCellStyle());
                                            style.setAlignment(HorizontalAlignment.CENTER);
                                            style.setVerticalAlignment(VerticalAlignment.CENTER);
                                            lastCell.setCellStyle(style);
                                        }
                                    }
                                }
                            }
                        }
                    }
                })
                .sheet("Excel汇总表")
                .doWrite(() -> {
                    List<List<Object>> excelData = new ArrayList<>();
                    for (Map<Integer, Object> row : summaryData) {
                        List<Object> excelRow = new ArrayList<>();
                        // 序号
                        excelRow.add(row.get(-1));
                        // 各导出列数据
                        for (Integer colIdx : exportColumnList) {
                            excelRow.add(row.get(colIdx));
                        }
                        // 备注
                        excelRow.add(row.get(-2));
                        excelData.add(excelRow);
                    }
                    return excelData;
                });

        log.info("✅ Excel写入完成!目标文件:{}", targetFile.getAbsolutePath());
    }

    // ------------------------------ 辅助方法 ------------------------------
    /**
     * 多列排序比较:按排序列优先级依次比较
     * 先比较第一个排序列,若相同则比较第二个,以此类推
     */
    private int compareBySortColumns(Map<Integer, Object> r1, Map<Integer, Object> r2, List<Integer> sortColIdxList) {
        for (Integer sortColIdx : sortColIdxList) {
            String s1 = config.getSortValue(r1.get(sortColIdx));
            String s2 = config.getSortValue(r2.get(sortColIdx));
            int cmp = s1.compareTo(s2);
            if (cmp != 0) {
                return cmp;
            }
        }
        return 0;
    }

    /**
     * 安全转换double(处理空值/非数字)
     */
    private double parseDouble(Object value, double defaultValue) {
        if (value == null || StringUtils.isEmpty(value.toString())) {
            return defaultValue;
        }
        try {
            return Double.parseDouble(value.toString().replaceAll("[^0-9.-]", ""));
        } catch (NumberFormatException e) {
            log.warn("⚠️  值转换为double失败,使用默认值{}:{}", defaultValue, value);
            return defaultValue;
        }
    }

    /**
     * 创建空行(用于分隔)
     */
    private Map<Integer, Object> createEmptyRow() {
        Map<Integer, Object> emptyRow = new LinkedHashMap<>();
        emptyRow.put(-1, "");
        for (Integer colIdx : exportColumnList) {
            emptyRow.put(colIdx, "");
        }
        emptyRow.put(-2, "-");
        return emptyRow;
    }

    /**
     * 获取排序列的列名(用于统计标题)
     */
    private String getSortColumnName() {
        return sourceHeaderMap.getOrDefault(config.getSortColumnIndex(), "排序列");
    }

    /**
     * 获取汇总列的前一列在导出列表中的位置对应的源列索引(用于显示统计文本)
     */
    private int getSummaryPrevColIdx() {
        // 找到导出列表中第一个汇总列的位置,返回它前一个位置的源列索引
        int firstSummaryPos = -1;
        for (int i = 0; i < exportColumnList.size(); i++) {
            if (summaryColumnSet.contains(exportColumnList.get(i))) {
                firstSummaryPos = i;
                break;
            }
        }
        if (firstSummaryPos > 0) {
            return exportColumnList.get(firstSummaryPos - 1);
        }
        return config.getSortColumnIndex();
    }

    /**
     * 获取内容单元格样式(居中 + 宋体/Times New Roman 10号)
     */
    private HorizontalCellStyleStrategy getContentCellStyleStrategy() {
        // 内容样式
        WriteCellStyle contentStyle = new WriteCellStyle();
        contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        contentStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        
        // 设置内容边框
        contentStyle.setBorderLeft(BorderStyle.THIN);
        contentStyle.setBorderRight(BorderStyle.THIN);
        contentStyle.setBorderTop(BorderStyle.THIN);
        contentStyle.setBorderBottom(BorderStyle.THIN);
        
        // 主字体设为 Times New Roman (用于英文/数字)
        WriteFont contentFont = new WriteFont();
        contentFont.setFontName("Times New Roman");
        contentFont.setFontHeightInPoints((short) 10);
        contentStyle.setWriteFont(contentFont);
        
        // 表头样式
        WriteCellStyle headStyle = new WriteCellStyle();
        headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        headStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        
        // 设置表头边框
        headStyle.setBorderLeft(BorderStyle.THIN);
        headStyle.setBorderRight(BorderStyle.THIN);
        headStyle.setBorderTop(BorderStyle.THIN);
        headStyle.setBorderBottom(BorderStyle.THIN);
        
        // 表头主字体设为 Times New Roman
        WriteFont headFont = new WriteFont();
        headFont.setFontName("Times New Roman");
        headFont.setFontHeightInPoints((short) 10);
        headFont.setBold(true);
        headStyle.setWriteFont(headFont);
        
        return new HorizontalCellStyleStrategy(headStyle, contentStyle);
    }
    
 // 在GenericExcelSummaryUtil中定义回调接口
    public interface ProgressCallback {
        void onProgress(int progress, String msg);
    }
    
}
java 复制代码
package ghs.easyexcel.swing;

// import ch.qos.logback.classic.LoggerContext; // 暂时注释,不使用 Logback 的 LoggerContext

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.formdev.flatlaf.*;
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
import com.formdev.flatlaf.themes.FlatMacLightLaf;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import ghs.easyexcel.config.ExcelSummaryConfig;
import ghs.easyexcel.util.GenericExcelSummaryUtil;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * Excel汇总工具Swing界面(乱码修复版)
 * 解决日志窗口乱码问题:统一UTF-8编码 + 适配中文字体 + 字节流转字符流
 */
public class ExcelSummarySwingApp extends JFrame {
    // ========== 常量配置 ==========
    private static final String LOG_SEP = "----------------------------------------\n";
    private static final String DEFAULT_SOURCE_FILE = "data" + File.separator + "电动风阀导入示例.xlsx";
    private static final int PROGRESS_MAX = 100;
    // 统一使用微软雅黑字体
    private static final String CHINESE_FONT = "微软雅黑";
    private final org.slf4j.Logger log = LoggerFactory.getLogger(ExcelSummarySwingApp.class);
    // 日志队列用于异步更新UI
    private final BlockingQueue<String> logQueue = new LinkedBlockingQueue<>();
    private final Font UI_FONT = new Font("微软雅黑", Font.PLAIN, 14);
    private final ExcelSummarySwingApp instance; // 保存实例引用以供日志回调
    // ========== GUI Designer 绑定的组件变量 ==========
    private JPanel mainPanel;              // 主面板(GUI Designer根组件)
    private final Color categoryColumnColor = new Color(255, 200, 200); // 分类列 - 浅红色
    private final Color exportColumnsColor = new Color(200, 255, 200);  // 导出列 - 浅绿色
    private final Color sortColumnColor = new Color(200, 200, 255);     // 排序列 - 浅蓝色
    private final Color summaryColumnsColor = new Color(255, 255, 200); // 汇总列 - 浅黄色
    private final Set<Integer> categoryColumns = new HashSet<>();       // 分类列集合
    private final Set<Integer> exportColumns = new HashSet<>();         // 导出列集合
    private final Set<Integer> sortColumns = new HashSet<>();           // 排序列集合
    private final Set<Integer> summaryColumns = new HashSet<>();        // 汇总列集合
    private final boolean logUpdaterRunning = true;
    // 设备类型快速选择按钮
    private JButton btn_deviceDamper;
    private JButton btn_deviceFireDamper;
    // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables  @formatter:off
    private JPanel pnlTop;
    private JPanel pnlFile;
    private JTextField t_sourceFile;
    private JPanel pnlFileButtons;
    private JButton btn_selectFile;
    private JButton btn_previewHeader;
    private JPanel pnlHeaderPreview;
    private JScrollPane spHeaderPreview;
    private JTable jTableHeaderPreview;
    private JPanel pnlParams;
    private JTextField t_skipRows;
    private JTextField t_categoryColumn;
    private JTextField t_exportColumns;
    private JTextField t_sortColumn;
    private JTextField t_summaryColumns;
    private JTextField t_decimalPlaces;
    private JPanel pnlBottom;
    private JPanel pnlButtonProgress;
    private JPanel pnlButton;
    private JButton btn_execute;
    private JButton btn_clear;
    private JPanel pnlProgress;
    private JProgressBar progressBar;
    private JPanel pnlLog;
    private JScrollPane spLog;
    private JTextArea t_log;

    public ExcelSummarySwingApp() {
        // 0. 保存实例引用
        initComponents();
        instance = this;
        // 1. 初始化界面(GUI Designer生成的UI)
        initUI();
        // 2. 初始化表头预览表格
        initHeaderPreviewTable();
        // 3. 初始化菜单栏
        initMenuBar();
        // 4. 绑定事件
        bindEvents();
        // 5. 重定向日志(UTF-8编码)
        redirectSystemOutToTextArea();
        // 6. 启动日志更新线程
        startLogUpdaterThread();
        // 7. 配置自定义日志Appender
        setupCustomLogAppender();
    }

    /**
     * 程序入口
     */
    public static void main(String[] args) {
        // 1. 初始化风格(放在最前,确保 UI 组件创建前生效)
        initLookAndFeelStatic();

        try {
            // 2. 强制设置 UTF-8 环境属性
            System.setProperty("file.encoding", "UTF-8");
            System.setProperty("sun.jnu.encoding", "UTF-8");
            System.setProperty("user.language", "zh");
            System.setProperty("user.region", "CN");
        } catch (SecurityException e) {
            // 忽略权限限制
        }

        SwingUtilities.invokeLater(() -> {
            ExcelSummarySwingApp app = new ExcelSummarySwingApp();
            app.setVisible(true);
            app.logToUI("💻 Excel汇总工具(大字深色增强版)启动完成!\n");
            app.logToUI("📢 当前系统编码已强制设为: UTF-8\n");
        });
    }

    /**
     * 静态风格初始化:设置 FlatLaf 和全局大字体
     */
    private static void initLookAndFeelStatic() {
        try {
            // 开启抗锯齿和窗口装饰
            System.setProperty("flatlaf.useWindowDecorations", "true");
            // 强制 UI 缩放 (1.25倍)
            System.setProperty("flatlaf.uiScale", "1.25");

            // 设置默认字体 (14号),使用英文名称提高兼容性
            Font globalFont = new Font("Microsoft YaHei UI", Font.PLAIN, 14);
            UIManager.put("defaultFont", globalFont);

            // 应用深色主题
         //   FlatDarkLaf.setup();

            // set look and feel
            try {
                UIManager.setLookAndFeel(GlazzedTheme.class.getName());
            } catch (Exception e) {
                // fallback
                GlazzedTheme.setup();
                GlazzedTheme.updateUI();
                throw new RuntimeException(e);
            }


            // 覆盖所有组件字体(确保 L&F setup 后不被重置)
            String[] components = {
                    "Label.font", "Button.font", "Table.font", "TableHeader.font",
                    "TextField.font", "TextArea.font", "TitledBorder.font",
                    "ComboBox.font", "List.font", "CheckBox.font", "RadioButton.font",
                    "ProgressBar.font", "TabbedPane.font", "Menu.font", "MenuItem.font",
                    "ToolTip.font", "OptionPane.messageFont", "OptionPane.buttonFont",
                    "TitledBorder.font", "Panel.font", "ScrollPane.font"
            };
            for (String key : components) {
                UIManager.put(key, globalFont);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化菜单栏,添加风格切换功能
     */
    private void initMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        JMenu viewMenu = new JMenu("视图(V)");
        viewMenu.setMnemonic('V');

        JMenu themeMenu = new JMenu("界面风格");

        // 定义主题选项
        addThemeMenuItem(themeMenu, "IntelliJ (亮色)", FlatIntelliJLaf.class.getName());
        addThemeMenuItem(themeMenu, "Darcula (深色)", FlatDarculaLaf.class.getName());
        addThemeMenuItem(themeMenu, "Flat Light", FlatLightLaf.class.getName());
        addThemeMenuItem(themeMenu, "Flat Dark", FlatDarkLaf.class.getName());
        addThemeMenuItem(themeMenu, "macOS Light", FlatMacLightLaf.class.getName());
        addThemeMenuItem(themeMenu, "macOS Dark", FlatMacDarkLaf.class.getName());
        addThemeMenuItem(themeMenu, "GlazzedTheme", GlazzedTheme.class.getName());

        viewMenu.add(themeMenu);
        menuBar.add(viewMenu);
        this.setJMenuBar(menuBar);
    }

    /**
     * 添加主题菜单项
     */
    private void addThemeMenuItem(JMenu menu, String name, String lafClassName) {
        JMenuItem menuItem = new JMenuItem(name);
        menuItem.addActionListener(e -> {
            try {
                UIManager.setLookAndFeel(lafClassName);
                FlatLaf.updateUI(); // 通知所有窗口更新 UI
                log.info("🎨 已切换界面风格为:{}", name);
            } catch (Exception ex) {
                log.error("❌ 切换界面风格失败:{}", ex.getMessage());
            }
        });
        menu.add(menuItem);
    }

    /**
     * 初始化界面(使用GUI Designer生成的mainPanel)
     */
    private void initUI() {
        // 如果 mainPanel 为 null,说明 GUI Designer 的自动初始化(字节码增强)未生效
        // 常见原因:在非 IntelliJ 环境下运行,或者 Maven/Gradle 未配置相关插件
//        if (mainPanel == null) {
//            log.error("❌ mainPanel 为 null!GUI Designer 自动初始化失败。请检查 IDE 设置或手动初始化组件。");
//            // 紧急回退:创建一个基础面板防止崩溃,但 UI 可能会缺失内容
//            mainPanel = new JPanel(new BorderLayout());
//            t_log = new JTextArea();
//            // ... 如果需要手动完全初始化,代码量会非常大。建议检查 IDE 设置。
//        }

        this.setTitle("东莞2号线三期电动风阀防火阀汇总工具---读入的文件只能有一个表头!!!中间不要插入表头!!!");
        this.setSize(1000, 750); // 调大初始窗口大小
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setLocationRelativeTo(null);

        // 使用GUI Designer生成的mainPanel
        this.setContentPane(mainPanel);

        // 设置日志区域属性
        if (t_log != null) {
            t_log.getDocument().putProperty("charset", StandardCharsets.UTF_8.name());
            t_log.setBackground(Color.BLACK);
            t_log.setForeground(new Color(0, 255, 0)); // 亮绿色
            t_log.setCaretColor(Color.GREEN);

            // 确保使用支持中文的字体,解决乱码。使用 Microsoft YaHei UI 提高稳定性
            Font logFont = new Font("Microsoft YaHei UI", Font.PLAIN, 14);
            t_log.setFont(logFont);
            t_log.setLineWrap(true);
            t_log.setWrapStyleWord(true);
        }
    }

    /**
     * 初始化表头预览表格
     */
    private void initHeaderPreviewTable() {
        // 防御性检查:如果 jTableHeaderPreview 为 null,说明 GUI Designer 初始化未生效
        if (jTableHeaderPreview == null) {
            log.error("❌ jTableHeaderPreview 为 null!跳过预览表格初始化。请按前述步骤调整 IDE 设置。");
            return;
        }

        // 创建表头预览表格模型
        String[] columnNames = {"0A", "1B", "2C", "3D", "4E", "5F", "6G", "7H", "8I", "9J", "10K", "11L", "12M", "13N"};
        DefaultTableModel tableModel = new DefaultTableModel(columnNames, 1);
        jTableHeaderPreview.setModel(tableModel);

        // 设置自定义渲染器
        ColoredCellRenderer renderer = new ColoredCellRenderer();
        for (int i = 0; i < jTableHeaderPreview.getColumnCount(); i++) {
            jTableHeaderPreview.getColumnModel().getColumn(i).setCellRenderer(renderer);
        }
    }

    /**
     * 绑定按钮事件(GUI Designer组件)
     */
    private void bindEvents() {
        // 防御性检查:确保按钮组件已由 GUI Designer 初始化
        if (btn_previewHeader == null || btn_selectFile == null || btn_execute == null || btn_clear == null) {
            log.error("❌ 按钮组件未初始化(null)!无法绑定事件。请检查 GUI Designer 设置并重新构建项目。");
            return;
        }

        // 预览表头按钮事件
        btn_previewHeader.addActionListener(e -> previewHeader());
        // 选择文件按钮
        btn_selectFile.addActionListener(e -> {
            JFileChooser fileChooser = new JFileChooser();
            // 默认打开程序所在目录
            fileChooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
            File defaultFile = new File(DEFAULT_SOURCE_FILE).getAbsoluteFile();
            if (defaultFile.exists()) {
                fileChooser.setSelectedFile(defaultFile);
            }
            FileNameExtensionFilter filter = new FileNameExtensionFilter(
                    "Excel文件 (*.xlsx, *.xls)", "xlsx", "xls");
            fileChooser.setFileFilter(filter);
            if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
                File selectedFile = fileChooser.getSelectedFile();
                t_sourceFile.setText(selectedFile.getAbsolutePath());
                log.info("📂 已选择源文件:{}", selectedFile.getAbsolutePath());
            }
        });

        // 执行汇总按钮
        btn_execute.addActionListener(e -> {
            progressBar.setValue(0);
            progressBar.setString("准备执行...");
            btn_execute.setEnabled(false);
            btn_selectFile.setEnabled(false);
            t_log.setText("");
            log.info(LOG_SEP + "🚀 开始执行Excel汇总..." + LOG_SEP);

            new Thread(() -> {
                try {
                    updateProgress(10, "校验输入参数");
                    ExcelSummaryConfig config = parseParams();
                    if (config == null) {
                        updateProgress(0, "参数校验失败");
                        return;
                    }

                    updateProgress(20, "初始化汇总工具");
                    GenericExcelSummaryUtil summaryUtil = new GenericExcelSummaryUtil(config);

                    updateProgress(30, "读取源Excel数据");
                    Thread.sleep(500);
                    updateProgress(60, "数据读取完成,开始汇总计算");

                    summaryUtil.executeSummary();
                    updateProgress(90, "汇总计算完成,写入Excel");

                    updateProgress(100, "汇总完成");

                    SwingUtilities.invokeLater(() -> {
                        UIManager.put("OptionPane.messageFont", UI_FONT); // 设置弹窗字体
                        UIManager.put("OptionPane.buttonFont", UI_FONT); // 设置弹窗按钮字体
                        int result = JOptionPane.showConfirmDialog(this,
                                "汇总完成!\n输出文件:" + config.getTargetFilePath() + "\n是否打开输出文件?",
                                "成功", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE);
                        if (result == JOptionPane.YES_OPTION) {
                            try {
                                File outputFile = new File(config.getTargetFilePath());
                                if (outputFile.exists()) {
                                    Desktop.getDesktop().open(outputFile);
                                } else {
                                    log.error("❌ 输出文件不存在:{}", config.getTargetFilePath());
                                }
                            } catch (Exception ex) {
                                log.error("❌ 打开输出文件失败:{}", ex.getMessage(), ex);
                                JOptionPane.showMessageDialog(this, "打开输出文件失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
                            }
                        }
                        log.info(LOG_SEP + "🎉 汇总执行完成!" + LOG_SEP);
                    });
                } catch (Exception ex) {
                    updateProgress(0, "执行失败");
                    String errorMsg = "汇总失败:" + ex.getMessage();
                    log.error("❌ " + errorMsg, ex);
                    SwingUtilities.invokeLater(() -> {
                        UIManager.put("OptionPane.messageFont", UI_FONT); // 设置弹窗字体
                        UIManager.put("OptionPane.buttonFont", UI_FONT); // 设置弹窗按钮字体
                        JOptionPane.showMessageDialog(this, errorMsg, "错误", JOptionPane.ERROR_MESSAGE);
                    });
                } finally {
                    SwingUtilities.invokeLater(() -> {
                        btn_execute.setEnabled(true);
                        btn_selectFile.setEnabled(true);
                    });
                }
            }).start();
        });

        // 清空按钮
        btn_clear.addActionListener(e -> {
            t_sourceFile.setText(new File(DEFAULT_SOURCE_FILE).getAbsolutePath());
            t_skipRows.setText("2");
            t_exportColumns.setText("1,5,8,9,10,6,13");
            t_summaryColumns.setText("6,13");
            t_categoryColumn.setText("1");
            t_sortColumn.setText("9,8,5");
            t_decimalPlaces.setText("2");
            progressBar.setValue(0);
            progressBar.setString("未执行");
            t_log.setText("");
            // 清空后也更新高亮列
            updateHighlightedColumns();
            log.info("🗑️  已清空所有参数和日志,恢复默认配置");
        });

        // 设备类型快速选择按钮
        btn_deviceDamper.addActionListener(e -> {
            t_exportColumns.setText("1,5,8,9,10,6,13");
            t_sortColumn.setText("9,8,5");
            t_summaryColumns.setText("6,13");
            t_categoryColumn.setText("1");
            t_skipRows.setText("2");
            t_decimalPlaces.setText("2");
            updateHighlightedColumns();
            log.info("📌 已选择设备类型:电动风阀,参数已自动填充");
        });
        btn_deviceFireDamper.addActionListener(e -> {
            t_exportColumns.setText("1,2,5,8,6,11");
            t_sortColumn.setText("2,5");
            t_summaryColumns.setText("6,11");
            t_categoryColumn.setText("1");
            t_skipRows.setText("2");
            t_decimalPlaces.setText("2");
            updateHighlightedColumns();
            log.info("📌 已选择设备类型:防火阀,参数已自动填充");
        });

        // 为参数输入框添加监听器,当参数改变时更新高亮列
        addParameterChangeListener(t_categoryColumn);
        addParameterChangeListener(t_exportColumns);
        addParameterChangeListener(t_sortColumn);
        addParameterChangeListener(t_summaryColumns);
    }

    /**
     * 为参数输入框添加监听器,当参数改变时更新高亮列
     */
    private void addParameterChangeListener(JTextField textField) {
        textField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
            public void insertUpdate(javax.swing.event.DocumentEvent e) {
                updateHighlightedColumns();
            }

            public void removeUpdate(javax.swing.event.DocumentEvent e) {
                updateHighlightedColumns();
            }

            public void changedUpdate(javax.swing.event.DocumentEvent e) {
                updateHighlightedColumns();
            }
        });
    }

    /**
     * 线程安全更新进度条
     */
    private void updateProgress(int progress, String msg) {
        SwingUtilities.invokeLater(() -> {
            progressBar.setValue(progress);
            progressBar.setString(msg + " (" + progress + "%)");
        });
        log.info("📊 进度{}%:{}", progress, msg);
    }

    /**
     * 解析参数
     */
    private ExcelSummaryConfig parseParams() {
        try {
            String sourcePath = t_sourceFile.getText().trim();
            if (sourcePath.isEmpty()) {
                JOptionPane.showMessageDialog(this, "请选择源Excel文件!", "错误", JOptionPane.ERROR_MESSAGE);
                return null;
            }
            File sourceFile = new File(sourcePath);
            if (!sourceFile.exists()) {
                JOptionPane.showMessageDialog(this, "源文件不存在:" + sourcePath, "错误", JOptionPane.ERROR_MESSAGE);
                return null;
            }

            String targetPath = generateTargetFilePath(sourcePath);

            int skipRows = Integer.parseInt(t_skipRows.getText().trim());
            int categoryCol = Integer.parseInt(t_categoryColumn.getText().trim());
            int[] sortCols = parseIntArray(t_sortColumn.getText().trim());
            int decimalPlaces = Integer.parseInt(t_decimalPlaces.getText().trim());

            int[] exportCols = parseIntArray(t_exportColumns.getText().trim());
            int[] summaryCols = parseIntArray(t_summaryColumns.getText().trim());

            if (skipRows < 0) throw new RuntimeException("跳过行数不能为负数!");
            if (categoryCol < 0) throw new RuntimeException("分类列索引不能为负数!");
            for (int sc : sortCols) { if (sc < 0) throw new RuntimeException("排序列索引不能为负数!"); }
            if (decimalPlaces < 0 || decimalPlaces > 10) throw new RuntimeException("小数位数需在0-10之间!");
            if (exportCols.length == 0) throw new RuntimeException("导出列索引不能为空!");
            if (summaryCols.length == 0) throw new RuntimeException("汇总列索引不能为空!");

            ExcelSummaryConfig config = new ExcelSummaryConfig();
            config.setSourceFilePath(sourcePath);
            config.setTargetFilePath(targetPath);
            config.setSkipRows(skipRows);
            config.setExportColumnIndexes(exportCols);
            config.setSummaryColumnIndexes(summaryCols);
            config.setCategoryColumnIndex(categoryCol);
            config.setSortColumnIndexes(sortCols);
            config.setDecimalPlaces(decimalPlaces);

            log.info("📋 参数解析完成:");
            log.info("  源文件:{}", sourcePath);
            log.info("  输出文件:{}", targetPath);
            log.info("  跳过行数:{}", skipRows);
            log.info("  导出列:{}", Arrays.toString(exportCols));
            log.info("  汇总列:{}", Arrays.toString(summaryCols));
            log.info("  分类列:{},排序列:{},小数位数:{}", categoryCol, Arrays.toString(sortCols), decimalPlaces);

            return config;
        } catch (NumberFormatException ex) {
            JOptionPane.showMessageDialog(this, "参数格式错误:请输入整数!\n" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            log.error("❌ 参数格式错误", ex);
            return null;
        } catch (RuntimeException ex) {
            JOptionPane.showMessageDialog(this, ex.getMessage(), "参数错误", JOptionPane.ERROR_MESSAGE);
            log.error("❌ 参数校验失败", ex);
            return null;
        }
    }

    /**
     * 生成输出文件路径
     */
    private String generateTargetFilePath(String sourcePath) {
        int dotIndex = sourcePath.lastIndexOf(".");
        return sourcePath.substring(0, dotIndex) + "-out" + sourcePath.substring(dotIndex);
    }

    /**
     * 逗号分隔字符串转int数组
     */
    private int[] parseIntArray(String str) {
        if (str.isEmpty()) return new int[0];
        String[] strArr = str.replaceAll("\\s+", "").split(",");
        int[] intArr = new int[strArr.length];
        for (int i = 0; i < strArr.length; i++) {
            intArr[i] = Integer.parseInt(strArr[i]);
        }
        return intArr;
    }

    /**
     * 预览表头功能:读取源文件表头并在日志区域显示
     */
    private void previewHeader() {
        String filePath = t_sourceFile.getText().trim();
        if (filePath.isEmpty()) {
            JOptionPane.showMessageDialog(this, "请先选择源Excel文件!", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }

        File file = new File(filePath);
        if (!file.exists()) {
            JOptionPane.showMessageDialog(this, "源文件不存在:" + filePath, "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }

        try {
            // 临时禁用日志输出,避免调试信息显示在UI上
            java.util.logging.Logger excelLogger = java.util.logging.Logger.getLogger("com.alibaba.excel");
            java.util.logging.Level originalLevel = excelLogger.getLevel();
            excelLogger.setLevel(java.util.logging.Level.WARNING);

            try {
                // 使用AnalysisEventListener来处理读取的数据
                List<List<String>> allRows = new ArrayList<>();
                EasyExcel.read(file.getAbsolutePath(), new AnalysisEventListener<Map<Integer, Object>>() {
                    @Override
                    public void invoke(Map<Integer, Object> data, AnalysisContext context) {
                        // 将Map转换为List
                        List<String> row = new ArrayList<>();
                        if (!data.isEmpty()) {
                            // 按顺序从0开始添加,直到遇到空的键
                            int maxIndex = data.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1);
                            for (int i = 0; i <= maxIndex; i++) {
                                Object cellValue = data.get(i);
                                row.add(cellValue != null ? cellValue.toString() : "");
                            }
                        }
                        allRows.add(row);

                        // 只读取前几行
                        if (context.readRowHolder() != null && context.readRowHolder().getRowIndex() > 0) { // 只读取第0行(首行)
                            }
                    }

                    @Override
                    public void doAfterAllAnalysed(AnalysisContext context) {
                        // 读取完成后的操作
                    }
                }).sheet().doRead();

                if (allRows.isEmpty()) {
                    logToUI("⚠️ 无法读取到表头数据,请检查文件格式\n");
                    return;
                }

                // 使用第一行作为表头
                List<String> headers = allRows.get(0);

                // 更新JTable中的表头信息
                updateHeaderPreviewTable(headers);
            } finally {
                // 恢复日志级别
                excelLogger.setLevel(originalLevel);
            }

        } catch (Exception e) {
            log.error("❌ 预览表头失败:{}", e.getMessage(), e);
            logToUI("❌ 预览表头失败:" + e.getMessage() + "\n");
        }
    }

    /**
     * 以表格格式显示表头信息(按行显示)
     */
    private void displayHeadersInTableFormat(List<String> headers) {
        if (headers.isEmpty()) return;

        // 构建表格(按行显示)
        StringBuilder table = new StringBuilder();
        table.append("\n📋 文件表头预览:\n\n");

        // 按行显示每个列的信息
        for (int col = 0; col < headers.size(); col++) {
            String cellValue = headers.get(col);
            String cellStr = cellValue != null ? cellValue : "";
            table.append(String.format("第%d列:%s\n", col, cellStr));
        }

        table.append("\n");

        logToUI(table.toString());
    }

    /**
     * 更新表头预览表格
     */
    private void updateHeaderPreviewTable(List<String> headers) {
        if (headers == null || headers.isEmpty()) {
            logToUI("⚠️ 无法读取到表头数据,请检查文件格式\n");
            return;
        }

        // 创建表格模型并更新数据
        DefaultTableModel model = (DefaultTableModel) jTableHeaderPreview.getModel();

        // 清空现有数据
        for (int col = 0; col < model.getColumnCount(); col++) {
            model.setValueAt("", 0, col);
        }

        // 设置新的表头数据
        for (int i = 0; i < Math.min(headers.size(), model.getColumnCount()); i++) {
            model.setValueAt(headers.get(i), 0, i);
        }

        // 更新高亮列
        updateHighlightedColumns();

        logToUI("✅ 表头预览已更新\n");
    }

    /**
     * 更新高亮列
     */
    private void updateHighlightedColumns() {
        // 清空所有高亮集合
        categoryColumns.clear();
        exportColumns.clear();
        sortColumns.clear();
        summaryColumns.clear();

        try {
            // 获取分类列索引
            int categoryCol = Integer.parseInt(t_categoryColumn.getText().trim());
            if (categoryCol >= 0) {
                categoryColumns.add(categoryCol);
            }

            // 获取导出列索引
            int[] exportCols = parseIntArray(t_exportColumns.getText().trim());
            for (int col : exportCols) {
                if (col >= 0) {
                    exportColumns.add(col);
                }
            }

            // 获取排序列索引(支持多列)
            int[] sortColsArr = parseIntArray(t_sortColumn.getText().trim());
            for (int sc : sortColsArr) {
                if (sc >= 0) {
                    sortColumns.add(sc);
                }
            }

            // 获取汇总列索引
            int[] summaryCols = parseIntArray(t_summaryColumns.getText().trim());
            for (int col : summaryCols) {
                if (col >= 0) {
                    summaryColumns.add(col);
                }
            }

            // 重新绘制表格以更新颜色
            jTableHeaderPreview.repaint();
        } catch (NumberFormatException e) {
            log.error("参数格式错误,无法更新高亮列:{}", e.getMessage());
        }
    }

    /**
     * 计算字符串的显示宽度(中文字符按2个单位计算,英文字符按1个单位计算)
     */
    private int getStringDisplayWidth(String str) {
        if (str == null) return 0;

        int width = 0;
        for (char c : str.toCharArray()) {
            if (isChineseCharacter(c)) {
                width += 2; // 中文字符宽度为2
            } else {
                width += 1; // 英文字符宽度为1
            }
        }
        return width;
    }

    /**
     * 判断字符是否为中文字符
     */
    private boolean isChineseCharacter(char c) {
        return c >= 0x4E00 && c <= 0x9FFF; // 基本中文字符范围
    }

    /**
     * 右对齐填充字符串(考虑中文字符宽度)
     */
    private String padStringRight(String str, int totalDisplayWidth) {
        if (str == null) str = "";

        int currentWidth = getStringDisplayWidth(str);
        int padding = totalDisplayWidth - currentWidth;

        if (padding <= 0) return str;

        StringBuilder sb = new StringBuilder(str);
        for (int i = 0; i < padding; i++) {
            sb.append(' ');
        }
        return sb.toString();
    }

    /**
     * 左对齐填充字符串(考虑中文字符宽度)
     */
    private String padStringLeft(String str, int totalDisplayWidth) {
        if (str == null) str = "";

        int currentWidth = getStringDisplayWidth(str);
        int padding = totalDisplayWidth - currentWidth;

        if (padding <= 0) return str;

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < padding; i++) {
            sb.append(' ');
        }
        sb.append(str);
        return sb.toString();
    }

    /**
     * 配置自定义日志Appender,将SLF4J日志输出到UI
     */
    private void setupCustomLogAppender() {
        try {
            // 尝试配置 Logback Appender
            Object iLoggerFactory = LoggerFactory.getILoggerFactory();
            // 使用字符串比较避免编译时依赖
            if (iLoggerFactory.getClass().getName().equals("ch.qos.logback.classic.LoggerContext")) {
                // 使用反射调用方法
                Object rootLogger = iLoggerFactory.getClass().getMethod("getLogger", String.class).invoke(iLoggerFactory, "ROOT");

                // 移除原有的控制台 Appender,防止日志重复且确保级别翻译生效
                try {
                    rootLogger.getClass().getMethod("detachAppender", String.class).invoke(rootLogger, "console");
                    rootLogger.getClass().getMethod("detachAppender", String.class).invoke(rootLogger, "CONSOLE");
                } catch (Exception ex) {
                    // 忽略移除失败
                }

                // 创建并配置自定义Appender
                CustomLogAppender customAppender = new CustomLogAppender();
                customAppender.setContext((ch.qos.logback.core.Context) iLoggerFactory);
                customAppender.setName("CustomUIAppender");
                customAppender.setLogQueue(logQueue);
                customAppender.setApp(this);
                customAppender.start();

                // 添加到根日志记录器
                rootLogger.getClass().getMethod("addAppender", Class.forName("ch.qos.logback.core.Appender")).invoke(rootLogger, customAppender);

                logToUI("✅ 自定义日志Appender配置完成\n");
            } else {
                logToUI("⚠️ 当前日志框架不是 Logback,使用 System.out 重定向\n");
            }
        } catch (Exception e) {
            logToUI("⚠️ 配置自定义日志Appender失败:" + e.getMessage() + "\n");
        }
    }

    /**
     * 启动日志更新线程,处理来自SLF4J的日志消息
     */
    private void startLogUpdaterThread() {
        Thread logUpdater = new Thread(() -> {
            while (logUpdaterRunning) {
                try {
                    String logMessage = logQueue.take(); // 阻塞等待日志消息
                    SwingUtilities.invokeLater(() -> {
                        t_log.append(logMessage);
                        t_log.setCaretPosition(t_log.getText().length());
                    });
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        logUpdater.setDaemon(true); // 设置为守护线程
        logUpdater.start();
    }

    /**
     * 自定义日志记录方法,将日志消息添加到队列
     */
    private void logToUI(String message) {
        try {
            logQueue.put(message + "\n");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    /**
     * 核心修复:重定向日志(强制使用 UTF-8 编码,彻底解决导出后的乱码)
     */
    private void redirectSystemOutToTextArea() {
        try {
            // 统一使用 UTF-8 编码进行内部管道传输
            // 这样无论系统默认编码是 GBK 还是其他,我们的内部重定向始终保持一致
            String encoding = StandardCharsets.UTF_8.name();

            PipedOutputStream pos = new PipedOutputStream();
            PipedInputStream pis = new PipedInputStream(pos);

            // 启动读取线程
            Thread readerThread = new Thread(() -> {
                // 使用 UTF-8 解析管道流
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(pis, encoding))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        logToUI(line);
                    }
                } catch (IOException e) {
                    // 流正常关闭
                }
            }, "日志流读取线程");
            readerThread.setDaemon(true);
            readerThread.start();

            // 强制设置系统输出编码为 UTF-8
            PrintStream ps = new PrintStream(pos, true, encoding);
            System.setOut(ps);
            System.setErr(ps);

            log.info("✅ 系统日志重定向成功 (已强制使用 UTF-8 编码)");
        } catch (IOException e) {
            log.error("❌ 设置系统输出重定向失败", e);
        }
    }

    private void initComponents() {

        mainPanel = new JPanel(new BorderLayout(10, 10));
        // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents  @formatter:off
        pnlTop = new JPanel();
        pnlFile = new JPanel();
        t_sourceFile = new JTextField();
        pnlFileButtons = new JPanel();
        btn_selectFile = new JButton();
        btn_previewHeader = new JButton();
        pnlHeaderPreview = new JPanel();
        spHeaderPreview = new JScrollPane();
        jTableHeaderPreview = new JTable();
        pnlParams = new JPanel();
        var label1 = new JLabel();
        t_skipRows = new JTextField();
        var label2 = new JLabel();
        t_categoryColumn = new JTextField();
        var label3 = new JLabel();
        t_exportColumns = new JTextField();
        var label4 = new JLabel();
        t_sortColumn = new JTextField();
        var label5 = new JLabel();
        t_summaryColumns = new JTextField();
        var label6 = new JLabel();
        t_decimalPlaces = new JTextField();
        var label7 = new JLabel();
        var label8 = new JLabel();
        var label9 = new JLabel();
        var label10 = new JLabel();
        pnlBottom = new JPanel();
        pnlButtonProgress = new JPanel();
        pnlButton = new JPanel();
        btn_execute = new JButton();
        btn_clear = new JButton();
        pnlProgress = new JPanel();
        progressBar = new JProgressBar();
        pnlLog = new JPanel();
        spLog = new JScrollPane();
        t_log = new JTextArea();

        //======== this ========
        setLayout(new BorderLayout(10, 10));

        //======== pnlTop ========
        {
            pnlTop.setLayout(new BorderLayout(10, 10));

            //======== pnlFile ========
            {
                pnlFile.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6587\u4ef6\u9009\u62e9"));
                pnlFile.setLayout(new BorderLayout(5, 5));

                //---- t_sourceFile ----
                t_sourceFile.setEditable(false);
                t_sourceFile.setText(new File(DEFAULT_SOURCE_FILE).getAbsolutePath());
                pnlFile.add(t_sourceFile, BorderLayout.CENTER);

                //======== pnlFileButtons ========
                {
                    pnlFileButtons.setLayout(new FlowLayout(FlowLayout.LEFT, 5, 0));

                    //---- btn_selectFile ----
                    btn_selectFile.setText("\u9009\u62e9\u6e90\u6587\u4ef6");
                    pnlFileButtons.add(btn_selectFile);

                    //---- btn_previewHeader ----
                    btn_previewHeader.setText("\u9884\u89c8\u8868\u5934");
                    pnlFileButtons.add(btn_previewHeader);

                    // 设备类型快速选择按钮组
                    pnlFileButtons.add(new javax.swing.JToolBar.Separator());
                    btn_deviceDamper = new JButton("电动风阀");
                    btn_deviceDamper.setForeground(new Color(0, 100, 0));
                    pnlFileButtons.add(btn_deviceDamper);

                    btn_deviceFireDamper = new JButton("防火阀");
                    btn_deviceFireDamper.setForeground(new Color(180, 0, 0));
                    pnlFileButtons.add(btn_deviceFireDamper);
                }
                pnlFile.add(pnlFileButtons, BorderLayout.EAST);
            }
            pnlTop.add(pnlFile, BorderLayout.NORTH);

            //======== pnlHeaderPreview ========
            {
                pnlHeaderPreview.setMaximumSize(new Dimension(-1, 100));
                pnlHeaderPreview.setMinimumSize(new Dimension(850, 80));
                pnlHeaderPreview.setPreferredSize(new Dimension(850, 90));
                pnlHeaderPreview.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u8868\u5934\u9884\u89c8"));
                pnlHeaderPreview.setLayout(new BorderLayout());

                //======== spHeaderPreview ========
                {
                    spHeaderPreview.setPreferredSize(new Dimension(850, 80));

                    //---- jTableHeaderPreview ----
                    jTableHeaderPreview.setEnabled(false);
                    jTableHeaderPreview.setRowHeight(30);
                    spHeaderPreview.setViewportView(jTableHeaderPreview);
                }
                pnlHeaderPreview.add(spHeaderPreview, BorderLayout.CENTER);
            }
            pnlTop.add(pnlHeaderPreview, BorderLayout.CENTER);

            //======== pnlParams ========
            {
                pnlParams.setMinimumSize(new Dimension(850, 140));
                pnlParams.setPreferredSize(new Dimension(850, 140));
                pnlParams.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6c47\u603b\u53c2\u6570"));
                pnlParams.setLayout(new GridLayoutManager(4, 4, new Insets(0, 0, 0, 0), 5, 5));

                //---- label1 ----
                label1.setText("\u8df3\u8fc7\u884c\u6570\uff08\u59822\uff09\uff1a");
                pnlParams.add(label1, new GridConstraints(0, 0, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- t_skipRows ----
                t_skipRows.setText("2");
                pnlParams.add(t_skipRows, new GridConstraints(0, 1, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
                    GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label2 ----
                label2.setText("\u5206\u7c7b\u5217\u7d22\u5f15\uff1a");
                pnlParams.add(label2, new GridConstraints(0, 2, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- t_categoryColumn ----
                t_categoryColumn.setText("1");
                pnlParams.add(t_categoryColumn, new GridConstraints(0, 3, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
                    GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label3 ----
                label3.setText("\u5bfc\u51fa\u5217\u7d22\u5f15\uff08\u9017\u53f7\u5206\u9694\uff09\uff1a");
                pnlParams.add(label3, new GridConstraints(1, 0, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- t_exportColumns ----
                t_exportColumns.setText("1,5,8,9,10,6,13");
                pnlParams.add(t_exportColumns, new GridConstraints(1, 1, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
                    GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label4 ----
                label4.setText("排序列索引(多列逗号分隔):");
                pnlParams.add(label4, new GridConstraints(1, 2, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- t_sortColumn ----
                t_sortColumn.setText("9");
                pnlParams.add(t_sortColumn, new GridConstraints(1, 3, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
                    GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label5 ----
                label5.setText("\u6c47\u603b\u5217\u7d22\u5f15\uff08\u9017\u53f7\u5206\u9694\uff09\uff1a");
                pnlParams.add(label5, new GridConstraints(2, 0, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- t_summaryColumns ----
                t_summaryColumns.setText("6,13");
                pnlParams.add(t_summaryColumns, new GridConstraints(2, 1, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
                    GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label6 ----
                label6.setText("\u5c0f\u6570\u4f4d\u6570\uff1a");
                pnlParams.add(label6, new GridConstraints(2, 2, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- t_decimalPlaces ----
                t_decimalPlaces.setText("2");
                pnlParams.add(t_decimalPlaces, new GridConstraints(2, 3, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
                    GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label7 ----
                label7.setText("");
                pnlParams.add(label7, new GridConstraints(3, 0, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label8 ----
                label8.setText("");
                pnlParams.add(label8, new GridConstraints(3, 1, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label9 ----
                label9.setText("");
                pnlParams.add(label9, new GridConstraints(3, 2, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));

                //---- label10 ----
                label10.setText("");
                pnlParams.add(label10, new GridConstraints(3, 3, 1, 1,
                    GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
                    GridConstraints.SIZEPOLICY_FIXED,
                    GridConstraints.SIZEPOLICY_FIXED,
                    null, null, null));
            }
            pnlTop.add(pnlParams, BorderLayout.SOUTH);
        }
     //   add(pnlTop, BorderLayout.NORTH);
        mainPanel.add(pnlTop, BorderLayout.NORTH);
        //======== pnlBottom ========
        {
            pnlBottom.setLayout(new BorderLayout(5, 5));

            //======== pnlButtonProgress ========
            {
                pnlButtonProgress.setLayout(new BorderLayout(10, 5));

                //======== pnlButton ========
                {
                    pnlButton.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 5));

                    //---- btn_execute ----
                    btn_execute.setText("\u6267\u884c\u6c47\u603b");
                    pnlButton.add(btn_execute);

                    //---- btn_clear ----
                    btn_clear.setText("\u6e05\u7a7a\u53c2\u6570");
                    pnlButton.add(btn_clear);
                }
                pnlButtonProgress.add(pnlButton, BorderLayout.WEST);

                //======== pnlProgress ========
                {
                    pnlProgress.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6267\u884c\u8fdb\u5ea6"));
                    pnlProgress.setLayout(new BorderLayout(5, 5));

                    //---- progressBar ----
                    progressBar.setMaximum(100);
                    progressBar.setMinimum(0);
                    progressBar.setString("\u672a\u6267\u884c");
                    progressBar.setStringPainted(true);
                    progressBar.setValue(0);
                    pnlProgress.add(progressBar, BorderLayout.CENTER);
                }
                pnlButtonProgress.add(pnlProgress, BorderLayout.CENTER);
            }
            pnlBottom.add(pnlButtonProgress, BorderLayout.NORTH);

            //======== pnlLog ========
            {
                pnlLog.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6267\u884c\u65e5\u5fd7"));
                pnlLog.setLayout(new BorderLayout(5, 5));

                //======== spLog ========
                {

                    //---- t_log ----
                    t_log.setBackground(Color.black);
                    t_log.setCaretColor(Color.green);
                    t_log.setEditable(false);
                    t_log.setFont(new Font("Yu Gothic UI", t_log.getFont().getStyle(), t_log.getFont().getSize()));
                    t_log.setForeground(Color.green);
                    spLog.setViewportView(t_log);
                }
                pnlLog.add(spLog, BorderLayout.CENTER);
            }
            pnlBottom.add(pnlLog, BorderLayout.CENTER);
        }
        //add(pnlBottom, BorderLayout.CENTER);

        mainPanel.add(pnlBottom, BorderLayout.CENTER);
        this.setContentPane(mainPanel);
        // JFormDesigner - End of component initialization  //GEN-END:initComponents  @formatter:on
    }

    /**
     * 自定义表格单元格渲染器,用于显示不同颜色的列
     */
    class ColoredCellRenderer implements TableCellRenderer {
        private final DefaultTableCellRenderer defaultRenderer = new DefaultTableCellRenderer();

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                                                       boolean hasFocus, int row, int column) {
            Component component = defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            // 如果选中,则使用选中状态的颜色
            if (isSelected) {
                component.setBackground(table.getSelectionBackground());
                component.setForeground(table.getSelectionForeground());
                return component;
            }

            // 根据列的用途设置不同背景色
            if (categoryColumns.contains(column)) {
                component.setBackground(categoryColumnColor);
            } else if (exportColumns.contains(column)) {
                component.setBackground(exportColumnsColor);
            } else if (sortColumns.contains(column)) {
                component.setBackground(sortColumnColor);
            } else if (summaryColumns.contains(column)) {
                component.setBackground(summaryColumnsColor);
            } else {
                component.setBackground(table.getBackground());
            }

            component.setForeground(table.getForeground());

            return component;
        }
    }
    // JFormDesigner - End of variables declaration  //GEN-END:variables  @formatter:on
}
相关推荐
万亿少女的梦1682 小时前
基于Spring Boot与MySQL的乡镇群众服务系统设计与实现
java·spring boot·mysql·权限管理·前后端分离
thefool1122662 小时前
Java 面向对象
java
2501_948106912 小时前
计算机毕业设计之jsp-智慧旅游分享平台
java·开发语言·spark·汽车·课程设计·旅游
阿里云云原生2 小时前
Agent 不再是“玩具”!AgentScope Java 2.0 GA 发布:构建企业级分布式智能体底座
java·开发语言·分布式·agentscope
4154112 小时前
MyBatis-Plus + PostGIS 实战(四):GeoJSON 序列化与前端地图对接,全局 WKT + 局部 GeoJSON,两种格式优雅共存
java·mybatis·postgis·geojson
峥嵘life2 小时前
Andorid 打开热点,关闭有线网,ifconfig卡死分析解决
java·服务器·网络
HillVue3 小时前
Token 叙事退潮,DAA 重新定义 AI 价值标准
java·人工智能·eclipse
行者全栈架构师3 小时前
【码动四季】Spring Boot 可观测性体系:Micrometer + OpenTelemetry + Grafana 全链路搭建
java·算法·架构
维C°3 小时前
PowerBI数据准备—合并文件夹中同表头Excel或csv
excel·powerbi