Kaynağa Gözat

动态导出功能

xiongwanxiong 5 gün önce
ebeveyn
işleme
5eff9a2cf2

+ 9 - 3
wxjy-wxjy-service/src/main/java/cn/gov/customs/wxjy/analyze/controller/GoodsEntryController.java

@@ -1,5 +1,6 @@
 package cn.gov.customs.wxjy.analyze.controller;
 
+import java.io.IOException;
 import java.util.List;
 import javax.servlet.http.HttpServletResponse;
 
@@ -8,6 +9,7 @@ import cn.gov.customs.cacp.sdks.core.user.annotation.LogonUser;
 import cn.gov.customs.cacp.sdks.core.user.pojo.CacpLogonUser;
 import cn.gov.customs.wxjy.analyze.pojo.EntryInfo;
 import cn.gov.customs.wxjy.analyze.pojo.EntryList;
+import cn.gov.customs.wxjy.common.utils.poi.DynamicExcelUtil;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 import cn.gov.customs.wxjy.common.core.controller.BaseController;
@@ -63,8 +65,10 @@ public class GoodsEntryController extends BaseController {
      * 导出危险品报关单头列表
      */
     @PostMapping("/goods-export")
-    public void export(@LogonUser CacpLogonUser user, HttpServletResponse response, @RequestBody EntryQuery query) {
+    public void export(@LogonUser CacpLogonUser user, HttpServletResponse response, @RequestBody EntryQuery query) throws IOException {
         String customsCode = user.getCustomsCode();
+        // 获取前端传递的动态列配置
+        String exportHeadList = query.getExportHeadList();
         //总关查所有,判断是否为总关用户
         if(StringUtils.isEmpty(query.getCustomsCode())){
             if(!"4700".equals(customsCode)){
@@ -72,7 +76,9 @@ public class GoodsEntryController extends BaseController {
             }
         }
         List<Entry> list = this.service.exportList(query);
-        ExcelUtil<Entry> util = new ExcelUtil<Entry>(Entry.class);
-        util.exportExcel(response, list, "危险品报关单数据");
+//        ExcelUtil<Entry> util = new ExcelUtil<Entry>(Entry.class);
+        // 使用动态Excel工具类
+        DynamicExcelUtil<Entry> util = new DynamicExcelUtil<>(Entry.class);
+        util.exportExcel(response, list, "危险品报关单数据","",exportHeadList);
     }
 }

+ 15 - 0
wxjy-wxjy-service/src/main/java/cn/gov/customs/wxjy/analyze/pojo/Entry.java

@@ -21,38 +21,50 @@ public class Entry implements Serializable {
   private String id;
 
   // 报关单号,18位,4位关区代码+4位年份+入境(1)/出境(0)+9位序列号
+  @Excel(name = "报关单号",sort=1)
   private String entryId;
   /** 维护状态 */
+  @Excel(name = "维护状态",sort=2)
   private String mainStatus;
 
   /** 通关模式 */
+  @Excel(name = "通关模式",sort=3)
   private String passMode;
 
   // 运输方式 2:水运 3:铁路 4:公路 5:空运 6:邮件 9:其它
+  @Excel(name = "运输方式",sort=4)
   private String trafMode;
 
   // 出入境标志 I:入境 E:出境
+  @Excel(name = "出入境标志",dictType = "ie_flag")
   private String ieFlag;
 
   // 进出境口岸:4位
+  @Excel(name = "进出境口岸")
   private String iePort;
 
   // 申报口岸:业务现场关区代码:4位
+  @Excel(name = "申报口岸")
   private String declPort;
 
   // 所属海关:4位
+  @Excel(name = "所属海关")
   private String customsCode;
 
   // 货物运抵(进出口)时间
+  @Excel(name = "货物运抵(进出口)时间")
   private LocalDateTime ieDate;
 
   // 申报时间
+  @Excel(name = "申报时间")
   private LocalDateTime declDate;
 
   /** 是否提前申报标记 */
+  @Excel(name = "申报标记")
   private String declAdvanceFlag;
 
   /** 自动受理时间 */
+  @Excel(name = "自动受理时间")
   private LocalDateTime acceptDate;
 
   // 自动受理时间:对应ENTRY_WORKFLOW表STEP_ID='10000000'节点
@@ -78,9 +90,11 @@ public class Entry implements Serializable {
   private String frnConsignName;
 
   // 监管方式
+  @Excel(name = "监管方式")
   private String tradeMode;
 
   // 贸易国别代码
+  @Excel(name = "贸易国别")
   private String tradeCountry;
 
   //货运量毛重
@@ -100,6 +114,7 @@ public class Entry implements Serializable {
 
   // 报关模式(部分订单既是两步申报又是提前申报,做多两种模式所以该字段采用逗号存储,方便组合查询):
   // 1:一般申报 2:两步申报3:提前申报
+  @Excel(name = "报关模式")
   private String declMode;
 
   // 报关单状态 1:未放行 2:已放行未结关3:已结关

+ 3 - 0
wxjy-wxjy-service/src/main/java/cn/gov/customs/wxjy/analyze/pojo/EntryQuery.java

@@ -4,6 +4,7 @@ import lombok.Data;
 import java.io.Serializable;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import java.util.Date;
+import java.util.List;
 
 /**
  * 危险品报关单头对象 WXJY_ENTRY_HEAD
@@ -42,6 +43,8 @@ public class EntryQuery implements Serializable {
 
     private String goodsType;
 
+    private String exportHeadList;
+
     private String beginReleaseDate;
     private String endReleaseDate;
     private int pageIndex;

+ 867 - 0
wxjy-wxjy-service/src/main/java/cn/gov/customs/wxjy/common/utils/poi/DynamicExcelUtil.java

@@ -0,0 +1,867 @@
+package cn.gov.customs.wxjy.common.utils.poi;
+
+import cn.gov.customs.wxjy.common.annotation.Excel;
+import cn.gov.customs.wxjy.common.core.text.Convert;
+import cn.gov.customs.wxjy.common.exception.UtilException;
+import cn.gov.customs.wxjy.common.utils.DateUtils;
+import cn.gov.customs.wxjy.common.utils.DictUtils;
+import cn.gov.customs.wxjy.common.utils.StringUtils;
+import cn.gov.customs.wxjy.common.utils.reflect.ReflectUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 动态列Excel导出工具类(优化版)
+ * 支持动态列导出、注解配置、样式处理、数据转换、字典转换等功能
+ *
+ * @param <T> 数据模型类型
+ */
+public class DynamicExcelUtil<T> {
+    private static final Logger log = LoggerFactory.getLogger(DynamicExcelUtil.class);
+
+    // 公式特殊字符,用于防止CSV注入
+    private static final String[] FORMULA_STR = { "=", "-", "+", "@" };
+    private static final String FORMULA_REGEX_STR = "=|-|\\+|@";
+
+    private Class<T> clazz;
+    private List<T> dataList;
+    private Set<String> exportFields;
+    private Map<String, Excel> excelAnnotationMap;
+    private Map<String, CellStyle> styles;
+    private Workbook workbook;
+    private Sheet sheet;
+    private List<Object[]> sortedFieldList; // 排序后的字段列表 [Field, Excel注解]
+    private String title;
+    private String sheetName;
+    private boolean includeTitle = false;
+    private int currentRowNum = 0;
+    private Map<Integer, Double> statistics = new HashMap<>();
+
+    // 字典缓存,避免重复查询
+    private Map<String, String> dictCache = new HashMap<>();
+
+    /**
+     * 构造器
+     *
+     * @param clazz 数据模型类
+     */
+    public DynamicExcelUtil(Class<T> clazz) {
+        this.clazz = clazz;
+        this.exportFields = new LinkedHashSet<>(); // 保持顺序
+        this.excelAnnotationMap = new HashMap<>();
+        this.sortedFieldList = new ArrayList<>();
+        initExcelAnnotations();
+    }
+
+    /**
+     * 初始化Excel注解映射
+     */
+    private void initExcelAnnotations() {
+        // 获取所有字段(包括父类)
+        List<Field> allFields = new ArrayList<>();
+        Class<?> currentClass = clazz;
+        while (currentClass != null && currentClass != Object.class) {
+            allFields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
+            currentClass = currentClass.getSuperclass();
+        }
+
+        // 收集Excel注解
+        for (Field field : allFields) {
+            if (field.isAnnotationPresent(Excel.class)) {
+                Excel excel = field.getAnnotation(Excel.class);
+                excelAnnotationMap.put(field.getName(), excel);
+            }
+        }
+    }
+
+    /**
+     * 设置需要导出的字段(字符串数组)
+     *
+     * @param fieldNames 字段名数组
+     * @return 当前实例
+     */
+    public DynamicExcelUtil<T> setExportFields(String... fieldNames) {
+        if (fieldNames != null) {
+            exportFields.addAll(Arrays.asList(fieldNames));
+        }
+        return this;
+    }
+
+    /**
+     * 设置需要导出的字段(集合)
+     *
+     * @param fieldNames 字段名集合
+     * @return 当前实例
+     */
+    public DynamicExcelUtil<T> setExportFields(Collection<String> fieldNames) {
+        if (fieldNames != null) {
+            exportFields.addAll(fieldNames);
+        }
+        return this;
+    }
+
+    /**
+     * 设置需要导出的字段(逗号分隔的字符串)
+     *
+     * @param fieldNamesStr 逗号分隔的字段名字符串
+     * @return 当前实例
+     */
+    public DynamicExcelUtil<T> setExportFieldsByString(String fieldNamesStr) {
+        if (StringUtils.isNotEmpty(fieldNamesStr)) {
+            String[] fieldArray = fieldNamesStr.split(",");
+            for (String field : fieldArray) {
+                String trimmedField = field.trim();
+                if (StringUtils.isNotEmpty(trimmedField)) {
+                    exportFields.add(trimmedField);
+                }
+            }
+        }
+        return this;
+    }
+
+    /**
+     * 设置数据列表
+     *
+     * @param dataList 数据列表
+     * @return 当前实例
+     */
+    public DynamicExcelUtil<T> setData(List<T> dataList) {
+        this.dataList = dataList;
+        return this;
+    }
+
+    /**
+     * 设置标题
+     *
+     * @param title 标题
+     * @return 当前实例
+     */
+    public DynamicExcelUtil<T> setTitle(String title) {
+        this.title = title;
+        this.includeTitle = StringUtils.isNotEmpty(title);
+        return this;
+    }
+
+    /**
+     * 设置工作表名称
+     *
+     * @param sheetName 工作表名称
+     * @return 当前实例
+     */
+    public DynamicExcelUtil<T> setSheetName(String sheetName) {
+        this.sheetName = sheetName;
+        return this;
+    }
+
+    /**
+     * 获取所有可导出的字段名称(按注解sort排序)
+     *
+     * @return 字段名称列表
+     */
+    public List<String> getAllExportableFields() {
+        List<String> fields = new ArrayList<>(excelAnnotationMap.keySet());
+        // 按注解的sort属性排序
+        fields.sort(Comparator.comparingInt(fieldName -> {
+            Excel excel = excelAnnotationMap.get(fieldName);
+            return excel != null ? excel.sort() : 0;
+        }));
+        return fields;
+    }
+
+    /**
+     * 准备导出数据
+     */
+    private void prepareExport() {
+        // 如果没有指定导出字段,导出所有有注解的字段(按sort排序)
+        if (exportFields.isEmpty()) {
+            // 获取所有字段并按sort排序
+            List<Map.Entry<String, Excel>> sortedEntries = excelAnnotationMap.entrySet()
+                    .stream()
+                    .sorted(Comparator.comparingInt(entry -> entry.getValue().sort()))
+                    .collect(Collectors.toList());
+
+            for (Map.Entry<String, Excel> entry : sortedEntries) {
+                exportFields.add(entry.getKey());
+            }
+        }
+
+        // 过滤掉不存在的字段
+        exportFields.removeIf(fieldName -> !excelAnnotationMap.containsKey(fieldName));
+
+        // 获取字段对象和注解
+        sortedFieldList.clear();
+        for (String fieldName : exportFields) {
+            try {
+                Field field = getField(clazz, fieldName);
+                if (field != null) {
+                    Excel excel = excelAnnotationMap.get(fieldName);
+                    sortedFieldList.add(new Object[]{field, excel});
+                }
+            } catch (NoSuchFieldException e) {
+                log.warn("字段 {} 在类 {} 中不存在", fieldName, clazz.getName());
+            }
+        }
+
+        // 如果没有指定顺序,按注解的sort排序
+        if (sortedFieldList.size() > 1) {
+            sortedFieldList.sort(Comparator.comparingInt(o -> ((Excel) o[1]).sort()));
+        }
+    }
+
+    /**
+     * 递归获取字段(包括父类)
+     */
+    private Field getField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
+        try {
+            return clazz.getDeclaredField(fieldName);
+        } catch (NoSuchFieldException e) {
+            Class<?> superClass = clazz.getSuperclass();
+            if (superClass != null && superClass != Object.class) {
+                return getField(superClass, fieldName);
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * 创建工作簿和样式
+     */
+    private void createWorkbookAndStyles() {
+        // 创建SXSSFWorkbook,支持大数据量
+        this.workbook = new SXSSFWorkbook(500);
+        this.sheet = workbook.createSheet(StringUtils.isNotEmpty(sheetName) ? sheetName : "Sheet1");
+
+        // 创建样式
+        createStyles();
+    }
+
+    /**
+     * 创建样式
+     */
+    private void createStyles() {
+        this.styles = new HashMap<>();
+
+        // 标题样式
+        CellStyle titleStyle = workbook.createCellStyle();
+        titleStyle.setAlignment(HorizontalAlignment.CENTER);
+        titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+        Font titleFont = workbook.createFont();
+        titleFont.setFontName("宋体");
+        titleFont.setFontHeightInPoints((short) 16);
+        titleFont.setBold(true);
+        titleStyle.setFont(titleFont);
+        styles.put("title", titleStyle);
+
+        // 表头样式 - 默认样式
+        CellStyle headerStyle = workbook.createCellStyle();
+        headerStyle.setAlignment(HorizontalAlignment.CENTER);
+        headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+        headerStyle.setBorderTop(BorderStyle.THIN);
+        headerStyle.setBorderBottom(BorderStyle.THIN);
+        headerStyle.setBorderLeft(BorderStyle.THIN);
+        headerStyle.setBorderRight(BorderStyle.THIN);
+        headerStyle.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        headerStyle.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        headerStyle.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        headerStyle.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+        Font headerFont = workbook.createFont();
+        headerFont.setFontName("宋体");
+        headerFont.setFontHeightInPoints((short) 11);
+        headerFont.setBold(true);
+        headerStyle.setFont(headerFont);
+        styles.put("header", headerStyle);
+
+        // 数据样式 - 默认样式
+        CellStyle dataStyle = workbook.createCellStyle();
+        dataStyle.setAlignment(HorizontalAlignment.LEFT);
+        dataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+        dataStyle.setBorderTop(BorderStyle.THIN);
+        dataStyle.setBorderBottom(BorderStyle.THIN);
+        dataStyle.setBorderLeft(BorderStyle.THIN);
+        dataStyle.setBorderRight(BorderStyle.THIN);
+        dataStyle.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        dataStyle.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        dataStyle.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        dataStyle.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        Font dataFont = workbook.createFont();
+        dataFont.setFontName("宋体");
+        dataFont.setFontHeightInPoints((short) 10);
+        dataStyle.setFont(dataFont);
+        styles.put("data", dataStyle);
+
+        // 创建数字样式
+        createNumberStyles();
+
+        // 创建日期样式
+        createDateStyles();
+
+        // 创建文本样式(防止科学计数法)
+        CellStyle textStyle = workbook.createCellStyle();
+        textStyle.cloneStyleFrom(dataStyle);
+        DataFormat textFormat = workbook.createDataFormat();
+        textStyle.setDataFormat(textFormat.getFormat("@"));
+        styles.put("text", textStyle);
+    }
+
+    /**
+     * 创建数字样式
+     */
+    private void createNumberStyles() {
+        // 普通数字样式
+        CellStyle numberStyle = workbook.createCellStyle();
+        numberStyle.cloneStyleFrom(styles.get("data"));
+        numberStyle.setAlignment(HorizontalAlignment.RIGHT);
+        DataFormat numberFormat = workbook.createDataFormat();
+        numberStyle.setDataFormat(numberFormat.getFormat("#,##0"));
+        styles.put("number", numberStyle);
+
+        // 两位小数样式
+        CellStyle decimalStyle = workbook.createCellStyle();
+        decimalStyle.cloneStyleFrom(styles.get("data"));
+        decimalStyle.setAlignment(HorizontalAlignment.RIGHT);
+        DataFormat decimalFormat = workbook.createDataFormat();
+        decimalStyle.setDataFormat(decimalFormat.getFormat("#,##0.00"));
+        styles.put("decimal", decimalStyle);
+    }
+
+    /**
+     * 创建日期样式
+     */
+    private void createDateStyles() {
+        // 日期样式
+        CellStyle dateStyle = workbook.createCellStyle();
+        dateStyle.cloneStyleFrom(styles.get("data"));
+        dateStyle.setAlignment(HorizontalAlignment.CENTER);
+        DataFormat dateFormat = workbook.createDataFormat();
+        dateStyle.setDataFormat(dateFormat.getFormat("yyyy-mm-dd"));
+        styles.put("date", dateStyle);
+
+        // 日期时间样式
+        CellStyle datetimeStyle = workbook.createCellStyle();
+        datetimeStyle.cloneStyleFrom(styles.get("data"));
+        datetimeStyle.setAlignment(HorizontalAlignment.CENTER);
+        DataFormat datetimeFormat = workbook.createDataFormat();
+        datetimeStyle.setDataFormat(datetimeFormat.getFormat("yyyy-mm-dd hh:mm:ss"));
+        styles.put("datetime", datetimeStyle);
+    }
+
+    /**
+     * 创建标题行
+     */
+    private void createTitleRow() {
+        if (includeTitle && StringUtils.isNotEmpty(title)) {
+            Row titleRow = sheet.createRow(currentRowNum++);
+            titleRow.setHeightInPoints(30);
+
+            // 创建标题单元格
+            Cell titleCell = titleRow.createCell(0);
+            titleCell.setCellValue(title);
+            titleCell.setCellStyle(styles.get("title"));
+
+            // 合并单元格
+            int lastCol = Math.max(0, sortedFieldList.size() - 1);
+            if (lastCol > 0) {
+                sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), 0, lastCol));
+            }
+        }
+    }
+
+    /**
+     * 创建表头行
+     */
+    private void createHeaderRow() {
+        Row headerRow = sheet.createRow(currentRowNum++);
+        headerRow.setHeightInPoints(25);
+
+        int colIndex = 0;
+        for (Object[] fieldInfo : sortedFieldList) {
+            Excel excel = (Excel) fieldInfo[1];
+            Cell cell = headerRow.createCell(colIndex);
+            cell.setCellValue(excel.name());
+            cell.setCellStyle(styles.get("header"));
+
+            // 设置列宽,width()返回double,需要转换为int
+            setColumnWidth(colIndex, excel);
+
+            colIndex++;
+        }
+    }
+
+    /**
+     * 设置列宽
+     */
+    private void setColumnWidth(int colIndex, Excel excel) {
+        double widthValue = excel.width();
+        int width;
+
+        if (widthValue > 0) {
+            // width()返回double,转换为int,公式为(width + 0.72) * 256
+            width = (int) ((widthValue + 0.72) * 256);
+        } else {
+            // 根据列名长度自动设置宽度
+            int nameLength = excel.name().getBytes().length;
+            width = Math.max(nameLength * 256 + 512, 2000);
+        }
+
+        // 设置最大最小限制
+        width = Math.min(width, 255 * 256); // 最大255个字符
+        width = Math.max(width, 1500); // 最小约6个字符
+
+        sheet.setColumnWidth(colIndex, width);
+    }
+
+    /**
+     * 填充数据行
+     */
+    private void fillDataRows() {
+        if (dataList == null || dataList.isEmpty()) {
+            return;
+        }
+
+        for (T item : dataList) {
+            Row dataRow = sheet.createRow(currentRowNum++);
+            dataRow.setHeightInPoints(20);
+
+            int colIndex = 0;
+            for (Object[] fieldInfo : sortedFieldList) {
+                Field field = (Field) fieldInfo[0];
+                Excel excel = (Excel) fieldInfo[1];
+
+                Cell cell = dataRow.createCell(colIndex);
+
+                try {
+                    field.setAccessible(true);
+                    Object value = field.get(item);
+
+                    // 应用单元格样式
+                    applyCellStyle(cell, excel);
+
+                    // 设置单元格值
+                    setCellValue(cell, value, excel);
+
+                    // 统计
+                    addStatistics(colIndex, value, excel);
+
+                } catch (Exception e) {
+                    log.error("设置单元格值失败,字段:{}", field.getName(), e);
+                    cell.setCellValue("");
+                    cell.setCellStyle(styles.get("data"));
+                }
+
+                colIndex++;
+            }
+        }
+    }
+
+    /**
+     * 应用单元格样式
+     */
+    private void applyCellStyle(Cell cell, Excel excel) {
+        CellStyle style = getCellStyle(excel);
+        cell.setCellStyle(style);
+    }
+
+    /**
+     * 根据注解获取单元格样式
+     */
+    private CellStyle getCellStyle(Excel excel) {
+        String styleKey;
+
+        // 根据单元格类型确定样式
+        switch (excel.cellType()) {
+            case NUMERIC:
+                // 根据scale判断小数位数
+                if (excel.scale() > 0) {
+                    styleKey = "decimal";
+                } else {
+                    styleKey = "number";
+                }
+                break;
+            case TEXT:
+                styleKey = "text";
+                break;
+            // 在若依框架的Excel.ColumnType中,没有DATE和DATETIME枚举值
+            // 日期格式化是通过dateFormat属性实现的
+            default:
+                styleKey = "data";
+        }
+
+        CellStyle baseStyle = styles.get(styleKey);
+
+        // 克隆样式以避免修改全局样式
+        CellStyle cellStyle = workbook.createCellStyle();
+        cellStyle.cloneStyleFrom(baseStyle);
+
+        // 设置水平对齐方式
+        switch (excel.align()) {
+            case LEFT:
+                cellStyle.setAlignment(HorizontalAlignment.LEFT);
+                break;
+            case CENTER:
+                cellStyle.setAlignment(HorizontalAlignment.CENTER);
+                break;
+            case RIGHT:
+                cellStyle.setAlignment(HorizontalAlignment.RIGHT);
+                break;
+        }
+
+        return cellStyle;
+    }
+
+    /**
+     * 设置单元格值
+     */
+    private void setCellValue(Cell cell, Object value, Excel excel) {
+        if (value == null) {
+            cell.setCellValue(excel.defaultValue());
+            return;
+        }
+
+        // 首先转换单元格值
+        String cellValue = convertCellValue(value, excel);
+
+        // 防止CSV注入
+        if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) {
+            cellValue = "\t" + cellValue;
+        }
+
+        // 根据单元格类型设置值
+        switch (excel.cellType()) {
+            case NUMERIC:
+                try {
+                    if (value instanceof Number) {
+                        double numValue = ((Number) value).doubleValue();
+                        // 如果有scale设置,进行格式化
+                        if (excel.scale() >= 0) {
+                            BigDecimal bd = BigDecimal.valueOf(numValue);
+                            bd = bd.setScale(excel.scale(), excel.roundingMode());
+                            cell.setCellValue(bd.doubleValue());
+                        } else {
+                            cell.setCellValue(numValue);
+                        }
+                    } else {
+                        cell.setCellValue(Double.parseDouble(cellValue));
+                    }
+                } catch (NumberFormatException e) {
+                    cell.setCellValue(cellValue);
+                }
+                break;
+            default:
+                // 对于日期类型,如果设置了dateFormat,这里cellValue已经是格式化后的字符串
+                cell.setCellValue(StringUtils.isNull(cellValue) ? excel.defaultValue() : cellValue);
+                break;
+        }
+    }
+
+    /**
+     * 转换单元格值
+     */
+    private String convertCellValue(Object value, Excel excel) {
+        if (value == null) {
+            return excel.defaultValue();
+        }
+
+        String strValue = value.toString();
+
+        // 日期格式化 - 如果有dateFormat属性,优先使用
+        if (StringUtils.isNotEmpty(excel.dateFormat())) {
+            return parseDateToStr(excel.dateFormat(), value);
+        }
+
+        // 字典转换
+        if (StringUtils.isNotEmpty(excel.dictType())) {
+            String cacheKey = excel.dictType() + "_" + strValue;
+            if (!dictCache.containsKey(cacheKey)) {
+                String dictLabel = DictUtils.getDictLabel(excel.dictType(), strValue, excel.separator());
+                dictCache.put(cacheKey, dictLabel != null ? dictLabel : strValue);
+            }
+            return dictCache.get(cacheKey);
+        }
+
+        // 读取转换器(例如:0=男,1=女)
+        if (StringUtils.isNotEmpty(excel.readConverterExp())) {
+            return convertByExp(strValue, excel.readConverterExp(), excel.separator());
+        }
+
+        return strValue;
+    }
+
+    /**
+     * 添加统计信息
+     */
+    private void addStatistics(int columnIndex, Object value, Excel excel) {
+        if (excel.isStatistics() && value != null) {
+            try {
+                double numValue = 0;
+                if (value instanceof Number) {
+                    numValue = ((Number) value).doubleValue();
+                } else {
+                    numValue = Double.parseDouble(value.toString());
+                }
+                statistics.merge(columnIndex, numValue, Double::sum);
+            } catch (NumberFormatException e) {
+                // 忽略非数字值
+            }
+        }
+    }
+
+    /**
+     * 创建统计行
+     */
+    private void createStatisticsRow() {
+        if (!statistics.isEmpty()) {
+            Row statRow = sheet.createRow(currentRowNum++);
+            Cell totalCell = statRow.createCell(0);
+            totalCell.setCellValue("合计");
+            totalCell.setCellStyle(styles.get("header"));
+
+            for (Map.Entry<Integer, Double> entry : statistics.entrySet()) {
+                Cell cell = statRow.createCell(entry.getKey());
+                cell.setCellValue(entry.getValue());
+
+                // 应用数字样式
+                CellStyle numberStyle = workbook.createCellStyle();
+                numberStyle.cloneStyleFrom(styles.get("decimal"));
+                cell.setCellStyle(numberStyle);
+            }
+        }
+    }
+
+    /**
+     * 日期格式化
+     */
+    private String parseDateToStr(String dateFormat, Object value) {
+        if (value == null) {
+            return "";
+        }
+        if (value instanceof Date) {
+            return DateUtils.parseDateToStr(dateFormat, (Date) value);
+        }
+        return value.toString();
+    }
+
+    /**
+     * 解析导出值 0=男,1=女,2=未知
+     */
+    private String convertByExp(String propertyValue, String converterExp, String separator) {
+        if (StringUtils.isEmpty(propertyValue) || StringUtils.isEmpty(converterExp)) {
+            return propertyValue;
+        }
+
+        StringBuilder result = new StringBuilder();
+        String[] convertSource = converterExp.split(",");
+
+        // 处理多个值的情况(用分隔符分隔)
+        String[] propertyValues = propertyValue.split(separator);
+
+        for (String propVal : propertyValues) {
+            boolean found = false;
+            for (String item : convertSource) {
+                String[] itemArray = item.split("=");
+                if (itemArray.length == 2 && propVal.trim().equals(itemArray[0].trim())) {
+                    if (result.length() > 0) {
+                        result.append(separator);
+                    }
+                    result.append(itemArray[1].trim());
+                    found = true;
+                    break;
+                }
+            }
+            // 如果没有找到匹配,使用原值
+            if (!found) {
+                if (result.length() > 0) {
+                    result.append(separator);
+                }
+                result.append(propVal.trim());
+            }
+        }
+
+        return result.toString();
+    }
+
+    /**
+     * 导出Excel到HttpServletResponse
+     *
+     * @param response HttpServletResponse
+     * @param fileName 文件名(不需要后缀)
+     * @throws IOException 异常
+     */
+    public void exportToResponse(HttpServletResponse response, String fileName) throws IOException {
+        // 准备数据
+        prepareExport();
+
+        // 创建工作簿和样式
+        createWorkbookAndStyles();
+
+        // 创建标题行
+        createTitleRow();
+
+        // 创建表头行
+        createHeaderRow();
+
+        // 填充数据
+        fillDataRows();
+
+        // 创建统计行
+        createStatisticsRow();
+
+        // 设置响应头
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("UTF-8");
+
+        // 对文件名进行URL编码
+        String encodedFileName = URLEncoder.encode(
+                        fileName + ".xlsx", StandardCharsets.UTF_8.toString())
+                .replaceAll("\\+", "%20");
+        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
+
+        // 写入响应流
+        workbook.write(response.getOutputStream());
+
+        // 清理临时文件(SXSSFWorkbook会生成临时文件)
+        if (workbook instanceof SXSSFWorkbook) {
+            ((SXSSFWorkbook) workbook).dispose();
+        }
+        workbook.close();
+    }
+
+    /**
+     * 导出Excel(简化方法)
+     *
+     * @param response HttpServletResponse
+     * @param dataList 数据列表
+     * @param sheetName 工作表名
+     * @throws IOException 异常
+     */
+    public void exportExcel(HttpServletResponse response, List<T> dataList, String sheetName) throws IOException {
+        setData(dataList)
+                .setSheetName(sheetName)
+                .exportToResponse(response, sheetName);
+    }
+
+    /**
+     * 导出Excel(完整方法)
+     *
+     * @param response HttpServletResponse
+     * @param dataList 数据列表
+     * @param sheetName 工作表名
+     * @param title 标题
+     * @param exportFields 导出的字段(逗号分隔的字符串)
+     * @throws IOException 异常
+     */
+    public void exportExcel(HttpServletResponse response, List<T> dataList,
+                            String sheetName, String title, String exportFields) throws IOException {
+        setData(dataList)
+                .setSheetName(sheetName)
+                .setTitle(title)
+                .setExportFieldsByString(exportFields)
+                .exportToResponse(response, sheetName);
+    }
+
+    /**
+     * 导出Excel(完整方法,数组参数)
+     *
+     * @param response HttpServletResponse
+     * @param dataList 数据列表
+     * @param sheetName 工作表名
+     * @param title 标题
+     * @param exportFields 导出的字段数组
+     * @throws IOException 异常
+     */
+    public void exportExcel(HttpServletResponse response, List<T> dataList,
+                            String sheetName, String title, String... exportFields) throws IOException {
+        setData(dataList)
+                .setSheetName(sheetName)
+                .setTitle(title)
+                .setExportFields(exportFields)
+                .exportToResponse(response, sheetName);
+    }
+
+    /**
+     * 导出到文件
+     *
+     * @param filePath 文件路径
+     * @throws IOException 异常
+     */
+    public void exportToFile(String filePath) throws IOException {
+        prepareExport();
+        createWorkbookAndStyles();
+        createTitleRow();
+        createHeaderRow();
+        fillDataRows();
+        createStatisticsRow();
+
+        try (OutputStream fos = new java.io.FileOutputStream(filePath)) {
+            workbook.write(fos);
+        }
+
+        if (workbook instanceof SXSSFWorkbook) {
+            ((SXSSFWorkbook) workbook).dispose();
+        }
+        workbook.close();
+    }
+
+    /**
+     * 导出到字节数组
+     *
+     * @return 字节数组
+     * @throws IOException 异常
+     */
+    public byte[] exportToBytes() throws IOException {
+        prepareExport();
+        createWorkbookAndStyles();
+        createTitleRow();
+        createHeaderRow();
+        fillDataRows();
+        createStatisticsRow();
+
+        try (java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream()) {
+            workbook.write(baos);
+            return baos.toByteArray();
+        } finally {
+            if (workbook instanceof SXSSFWorkbook) {
+                ((SXSSFWorkbook) workbook).dispose();
+            }
+            workbook.close();
+        }
+    }
+
+    /**
+     * 清空缓存和状态
+     */
+    public void clear() {
+        this.exportFields.clear();
+        this.dictCache.clear();
+        this.statistics.clear();
+        this.sortedFieldList.clear();
+        this.title = null;
+        this.sheetName = null;
+        this.includeTitle = false;
+        this.currentRowNum = 0;
+    }
+
+    /**
+     * 获取导出字段列表(用于调试)
+     *
+     * @return 导出字段列表
+     */
+    public List<String> getExportFieldList() {
+        return new ArrayList<>(exportFields);
+    }
+}

+ 502 - 0
wxjy-wxjy-web/src/components/DynamicColumnSelector/DynamicColumnDialog.vue

@@ -0,0 +1,502 @@
+<template>
+  <cacp-dialog
+    v-model="dialogVisible"
+    :title="title"
+    :width="width"
+    :resizable="false"
+    @closed="onDialogClosed"
+  >
+    <div class="dynamic-column-dialog">
+      <!-- 搜索框 -->
+      <div class="column-search" v-if="showSearch">
+        <el-input
+          v-model="searchText"
+          placeholder="搜索列名"
+          clearable
+          :prefix-icon="Search"
+          @keyup.enter="handleSearch"
+        />
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="column-operations">
+        <el-button type="text" @click="selectAll" :disabled="!columns.length">全选</el-button>
+        <el-button type="text" @click="clearAll" :disabled="!tempSelectedKeys.length">清空</el-button>
+        <el-button type="text" @click="resetToDefault">恢复默认</el-button>
+        <el-button type="text" @click="toggleSelectAll" v-if="showToggleAll">
+          {{ isAllSelected ? '取消全选' : '全选当前' }}
+        </el-button>
+      </div>
+
+      <!-- 列选择区域 -->
+      <div class="column-select-area">
+        <el-checkbox-group v-model="tempSelectedKeysModel">
+          <div
+            v-for="group in filteredColumnGroups"
+            :key="group.key"
+            class="column-group"
+          >
+            <div class="group-header" v-if="group.title && group.columns.length > 0">
+              <el-checkbox
+                :indeterminate="group.indeterminate"
+                :model-value="group.allChecked"
+                @change="toggleGroup(group.key, $event)"
+              >
+                <span class="group-title">{{ group.title }}</span>
+<!--                <span class="group-count">({{ group.selectedCount }}/{{ group.columns.length }})</span>-->
+              </el-checkbox>
+            </div>
+            <div class="group-items" v-if="group.columns.length > 0">
+              <el-checkbox
+                v-for="column in group.columns"
+                :key="column.key"
+                :label="column.key"
+                :title="column.tooltip || column.label"
+                :disabled="column.disabled"
+              >
+                <span class="column-label">{{ column.label }}</span>
+<!--                <span v-if="column.width" class="column-width">({{ column.width }})</span>-->
+                <el-tag v-if="column.defaultVisible" size="small" type="success" class="default-tag">
+                  默认
+                </el-tag>
+              </el-checkbox>
+            </div>
+          </div>
+        </el-checkbox-group>
+
+        <!-- 无数据提示 -->
+        <div v-if="!hasFilteredColumns" class="no-data">
+          <el-empty description="未找到匹配的列" :image-size="80" />
+        </div>
+      </div>
+
+      <!-- 底部统计信息 -->
+      <div class="column-statistics">
+        已选择 {{ tempSelectedKeys.length }}/{{ columns.length }} 列
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleCancel">取消</el-button>
+        <el-button type="primary" @click="handleConfirm" :disabled="!tempSelectedKeys.length">
+          确定
+        </el-button>
+      </span>
+    </template>
+  </cacp-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch, nextTick } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import { type CheckboxValueType, ElMessage } from 'element-plus'
+
+export interface ColumnConfig {
+  key: string
+  label: string
+  width?: string | number
+  tooltip?: string
+  groupKey?: string
+  groupTitle?: string
+  defaultVisible?: boolean
+  property?: string
+  sortable?: boolean
+  isDict?: boolean
+  dictKey?: string
+  isDate?: boolean
+  formatter?: (value: any) => string
+  showOverflowTooltip?: boolean
+  disabled?: boolean
+}
+
+export interface ColumnGroup {
+  key: string
+  title?: string
+  columns: ColumnConfig[]
+  indeterminate: boolean
+  allChecked: boolean
+  selectedCount: number
+}
+
+interface Props {
+  modelValue: boolean
+  title?: string
+  width?: string | number
+  columns: ColumnConfig[]
+  selectedKeys: string[]
+  showSearch?: boolean
+  showToggleAll?: boolean
+  autoSave?: boolean
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'update:selectedKeys', keys: string[]): void
+  (e: 'confirm', keys: string[]): void
+  (e: 'cancel'): void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  title: '选择显示的列',
+  width: '600px',
+  showSearch: true,
+  showToggleAll: false,
+  autoSave: false
+})
+
+const emit = defineEmits<Emits>()
+
+// 搜索文本
+const searchText = ref('')
+
+// 临时选中的列键值
+const tempSelectedKeys = ref<string[]>([])
+
+// 使用 computed 包装用于 v-model
+const tempSelectedKeysModel = computed({
+  get: () => tempSelectedKeys.value,
+  set: (value: string[]) => {
+    tempSelectedKeys.value = value
+  }
+})
+
+// 对话框显示状态
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 计算是否全选
+const isAllSelected = computed(() =>
+  tempSelectedKeys.value.length === props.columns.length && props.columns.length > 0
+)
+
+// 是否有过滤后的列
+const hasFilteredColumns = computed(() => {
+  return filteredColumnGroups.value.some(group => group.columns.length > 0)
+})
+
+// 分组后的列配置
+const columnGroups = computed(() => {
+  const groups = new Map<string, ColumnGroup>()
+
+  // 初始化分组
+  props.columns.forEach(column => {
+    const groupKey = column.groupKey || 'default'
+    if (!groups.has(groupKey)) {
+      groups.set(groupKey, {
+        key: groupKey,
+        title: column.groupTitle || '',
+        columns: [],
+        indeterminate: false,
+        allChecked: false,
+        selectedCount: 0
+      })
+    }
+    groups.get(groupKey)!.columns.push(column)
+  })
+
+  // 计算每个分组的选中状态
+  groups.forEach(group => {
+    const selectedCount = group.columns.filter(col =>
+      tempSelectedKeys.value.includes(col.key)
+    ).length
+
+    group.selectedCount = selectedCount
+    group.allChecked = selectedCount === group.columns.length
+    group.indeterminate = selectedCount > 0 && selectedCount < group.columns.length
+  })
+
+  return Array.from(groups.values())
+})
+
+// 过滤后的列分组(根据搜索文本)
+const filteredColumnGroups = computed(() => {
+  if (!searchText.value.trim()) return columnGroups.value
+
+  const searchLower = searchText.value.toLowerCase().trim()
+
+  return columnGroups.value.map(group => ({
+    ...group,
+    columns: group.columns.filter(column =>
+      column.label.toLowerCase().includes(searchLower) ||
+      column.key.toLowerCase().includes(searchLower) ||
+      (column.tooltip && column.tooltip.toLowerCase().includes(searchLower))
+    )
+  })).filter(group => group.columns.length > 0)
+})
+
+// 初始化临时选中的列
+watch(
+  () => props.selectedKeys,
+  (newVal) => {
+    tempSelectedKeys.value = [...newVal]
+  },
+  { immediate: true }
+)
+
+// 监视对话框打开,重置搜索
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal) {
+      nextTick(() => {
+        searchText.value = ''
+      })
+    }
+  }
+)
+
+// 全选
+const selectAll = () => {
+  tempSelectedKeys.value = props.columns.map(col => col.key)
+}
+
+// 清空
+const clearAll = () => {
+  tempSelectedKeys.value = []
+}
+
+// 恢复默认
+const resetToDefault = () => {
+  const defaultKeys = props.columns
+    .filter(col => col.defaultVisible)
+    .map(col => col.key)
+  tempSelectedKeys.value = defaultKeys
+  if (props.autoSave) {
+    emit('update:selectedKeys', defaultKeys)
+  }
+}
+
+// 切换全选/取消全选
+const toggleSelectAll = () => {
+  if (isAllSelected.value) {
+    clearAll()
+  } else {
+    selectAll()
+  }
+}
+
+// 切换分组选中状态
+const toggleGroup = (groupKey: string, checked: CheckboxValueType) => {
+  const group = columnGroups.value.find(g => g.key === groupKey)
+  if (!group) return
+
+  const groupColumnKeys = group.columns.map(col => col.key)
+
+  // 将 CheckboxValueType 转换为 boolean
+  const isChecked = !!checked
+
+  if (isChecked) {
+    // 添加分组中所有列
+    const newKeys = [...tempSelectedKeys.value]
+    groupColumnKeys.forEach(key => {
+      if (!newKeys.includes(key)) {
+        newKeys.push(key)
+      }
+    })
+    tempSelectedKeys.value = newKeys
+  } else {
+    // 移除分组中所有列
+    tempSelectedKeys.value = tempSelectedKeys.value.filter(
+      key => !groupColumnKeys.includes(key)
+    )
+  }
+}
+
+// 处理搜索
+const handleSearch = () => {
+  // 搜索逻辑已经在 filteredColumnGroups 中实现
+}
+
+// 对话框关闭
+const onDialogClosed = () => {
+  searchText.value = ''
+}
+
+// 取消
+const handleCancel = () => {
+  // 恢复原始选中状态
+  tempSelectedKeys.value = [...props.selectedKeys]
+  emit('cancel')
+  dialogVisible.value = false
+}
+
+// 确认
+const handleConfirm = () => {
+  if (tempSelectedKeys.value.length === 0) {
+    ElMessage.warning('请至少选择一列')
+    return
+  }
+
+  emit('update:selectedKeys', [...tempSelectedKeys.value])
+  emit('confirm', [...tempSelectedKeys.value])
+  dialogVisible.value = false
+}
+
+// 键盘快捷键
+const handleKeydown = (e: KeyboardEvent) => {
+  if (e.key === 'Escape') {
+    handleCancel()
+  } else if (e.key === 'Enter' && e.ctrlKey) {
+    handleConfirm()
+  }
+}
+
+// 添加键盘事件监听
+if (typeof window !== 'undefined') {
+  window.addEventListener('keydown', handleKeydown)
+}
+</script>
+
+<style scoped lang="scss">
+.dynamic-column-dialog {
+  display: flex;
+  flex-direction: column;
+  //height: 100%;
+  max-height: 60vh;
+  overflow-y: auto;
+  padding-right: 10px;
+  .column-search {
+    margin-bottom: 16px;
+
+    :deep(.el-input__wrapper) {
+      border-radius: 6px;
+    }
+  }
+
+  .column-operations {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    margin-bottom: 16px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid var(--el-border-color-light);
+
+    .el-button {
+      padding: 0;
+      height: auto;
+      font-size: 14px;
+
+      &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+    }
+  }
+
+  .column-select-area {
+    flex: 1;
+    overflow-y: auto;
+    padding-right: 4px;
+
+    .column-group {
+      margin-bottom: 20px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .group-header {
+        margin-bottom: 12px;
+        padding: 8px 0;
+        background-color: var(--el-fill-color-light);
+        border-radius: 6px;
+
+        .el-checkbox {
+          width: 100%;
+          padding: 0 12px;
+
+          :deep(.el-checkbox__label) {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            width: 100%;
+          }
+
+          .group-title {
+            font-weight: 600;
+            color: var(--el-text-color-primary);
+          }
+
+          .group-count {
+            color: var(--el-text-color-secondary);
+            font-size: 12px;
+            font-weight: normal;
+          }
+        }
+      }
+
+      .group-items {
+        display: grid;
+        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+        gap: 12px;
+        padding: 0 4px;
+
+        .el-checkbox {
+          margin: 0;
+          padding: 8px 12px;
+          border: 1px solid var(--el-border-color-light);
+          border-radius: 6px;
+          transition: all 0.3s;
+
+          &:hover {
+            border-color: var(--el-color-primary);
+            background-color: var(--el-color-primary-light-9);
+          }
+
+          &:deep(.el-checkbox__label) {
+            display: flex;
+            align-items: center;
+            flex: 1;
+            min-width: 0;
+          }
+
+          .column-label {
+            flex: 1;
+            margin-right: 8px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+
+          .column-width {
+            color: var(--el-text-color-secondary);
+            font-size: 12px;
+            flex-shrink: 0;
+          }
+
+          .default-tag {
+            margin-left: 8px;
+            flex-shrink: 0;
+          }
+        }
+      }
+    }
+
+    .no-data {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 200px;
+    }
+  }
+
+  .column-statistics {
+    margin-top: 16px;
+    padding-top: 12px;
+    border-top: 1px solid var(--el-border-color-light);
+    font-size: 14px;
+    color: var(--el-text-color-secondary);
+    text-align: center;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>

+ 125 - 0
wxjy-wxjy-web/src/hooks/useDynamicColumns.ts

@@ -0,0 +1,125 @@
+import { ref, computed } from 'vue'
+import type { Ref, ComputedRef } from 'vue'  // 正确导入类型
+import { ElMessage } from 'element-plus'
+import type { ColumnConfig } from '@/components/DynamicColumnSelector/DynamicColumnDialog.vue'
+
+export interface UseDynamicColumnsOptions {
+  storageKey?: string
+  defaultVisibleKeys?: string[]
+  columns: ColumnConfig[]
+}
+
+export interface UseDynamicColumnsReturn {
+  selectedKeys: Ref<string[]>
+  visibleColumns: ComputedRef<ColumnConfig[]>
+  selectedCount: ComputedRef<number>
+  updateSelectedKeys: (keys: string[]) => void
+  resetToDefault: () => void
+  getTableColumnProps: (column: ColumnConfig) => Record<string, any>
+  getExportColumns: () => string[]
+}
+
+export function useDynamicColumns(options: UseDynamicColumnsOptions): UseDynamicColumnsReturn {
+  const {
+    storageKey,
+    defaultVisibleKeys = [],
+    columns
+  } = options
+
+  // 选中的列键值
+  const selectedKeys = ref<string[]>([])
+
+  // 计算属性:可见列
+  const visibleColumns = computed<ColumnConfig[]>(() => {
+    return columns.filter(column => selectedKeys.value.includes(column.key))
+  })
+
+  // 计算属性:选中的列数量
+  const selectedCount = computed(() => selectedKeys.value.length)
+
+  // 初始化选中的列
+  const initSelectedKeys = () => {
+    let keys: string[]
+
+    // 尝试从本地存储加载
+    if (storageKey) {
+      const savedKeys = localStorage.getItem(storageKey)
+      if (savedKeys) {
+        try {
+          keys = JSON.parse(savedKeys)
+        } catch (error) {
+          console.error('Failed to parse saved columns:', error)
+          keys = defaultVisibleKeys
+        }
+      } else {
+        keys = defaultVisibleKeys
+      }
+    } else {
+      keys = defaultVisibleKeys
+    }
+
+    // 过滤掉不存在的列
+    const validKeys = keys.filter(key =>
+      columns.some(col => col.key === key)
+    )
+
+    selectedKeys.value = validKeys.length > 0 ? validKeys : defaultVisibleKeys
+  }
+
+  // 保存到本地存储
+  const saveToStorage = () => {
+    if (storageKey) {
+      localStorage.setItem(storageKey, JSON.stringify(selectedKeys.value))
+    }
+  }
+
+  // 更新选中的列
+  const updateSelectedKeys = (keys: string[]) => {
+    selectedKeys.value = keys
+    saveToStorage()
+    ElMessage.success('列设置已保存')
+  }
+
+  // 重置为默认
+  const resetToDefault = () => {
+    selectedKeys.value = [...defaultVisibleKeys]
+    saveToStorage()
+    ElMessage.success('已恢复默认列设置')
+  }
+
+  // 获取表格列属性(用于el-table-column)
+  const getTableColumnProps = (column: ColumnConfig): Record<string, any> => {
+    const props: Record<string, any> = {
+      property: column.property || column.key,
+      label: column.label
+    }
+
+    if (column.width) props.width = column.width
+    // if (column.minWidth) props.minWidth = column.minWidth
+    if (column.sortable) props.sortable = column.sortable
+    if (column.showOverflowTooltip) props['show-overflow-tooltip'] = column.showOverflowTooltip
+
+    return props
+  }
+
+  // 导出时获取选中的列信息
+  const getExportColumns = (): string[] => {
+    return selectedKeys.value.map(key => {
+      const column = columns.find(col => col.key === key)
+      return column?.property || key
+    })
+  }
+
+  // 初始化
+  initSelectedKeys()
+
+  return {
+    selectedKeys,
+    visibleColumns,
+    selectedCount,
+    updateSelectedKeys,
+    resetToDefault,
+    getTableColumnProps,
+    getExportColumns
+  }
+}

+ 3 - 0
wxjy-wxjy-web/src/main.ts

@@ -18,6 +18,8 @@ import { BridgeCore } from '@cacp/bridge-core'
 import DictTag from '@/components/DictTag/dictTag.vue'
 import { getDict } from '@/utils/dict'
 
+import DynamicColumnDialog from '@/components/DynamicColumnSelector/DynamicColumnDialog.vue'
+
 // 初始化通信实例
 const bridge = new BridgeCore({
   allowedOrigins: [], //门户地址
@@ -47,6 +49,7 @@ app.use(router)
 app.use(plugins)
 app.use(directives)
 app.use(pinia)
+app.use(DynamicColumnDialog)
 app.component('DictTag', DictTag)
 // 绑定message监听,addEventListener中调用了store,所以需要放在app.use(pinia)之后调用。
 addEventListener(bridge)

+ 0 - 8
wxjy-wxjy-web/src/router/app-router.ts

@@ -44,14 +44,6 @@ const routers: Array<RouteRecordRaw> = [
     name: 'GenEdit',
     meta: { title: '编辑生成配置', activeMenu: '/gen/index' },
   },
-  {
-    path: '/analyze-entryHead',
-    name: 'EntryHead',
-    component: () => import('@/views/analyze/EntryHead.vue'),
-    meta: {
-      title: '表头',
-    }
-  },
   {
     path: '/analyze-newDeclaredGoods',
     name: 'NewDeclaredGoods',

+ 1 - 0
wxjy-wxjy-web/src/types/analyze/goodsEntry.ts

@@ -190,6 +190,7 @@ export interface EntryQuery1 {
   beginReleaseDate?: string
   endReleaseDate?: string
   goodsType?: string
+  exportHeadList?: string
 }
 
 

+ 735 - 155
wxjy-wxjy-web/src/views/analyze/GoodsEntry.vue

@@ -134,159 +134,39 @@
         @on-size-change="onSizeChange"
         :loading="loading"
     >
-      <el-table-column property="entryId" label="报关单号" sortable  width="160" />
-      <el-table-column property="passMode" label="通关模式" width="80">
-        <template #default="scope">
-          <dict-tag :options="dict.pass_mode" :dictValue="scope.row.passMode"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="trafMode" label="运输方式" width="80">
-        <template #default="scope">{{ formatTrafMode(scope.row.trafMode) }}</template>
-      </el-table-column>
-      <el-table-column property="ieFlag" label="出入境标志" width="90" >
-        <template #default="scope">
-          <dict-tag :options="dict.ie_flag" :dictValue="scope.row.ieFlag"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="iePort" label="进出境口岸" width="100" >
-        <template #default="scope">{{ formatIePort(scope.row.iePort) }}</template>
-      </el-table-column>
-      <el-table-column property="customsCode" label="主管海关" width="100">
-        <template #default="scope">
-          <dict-tag :options="dict.affiliation_customs_info" :dictValue="scope.row.customsCode"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="tradeMode" label="监管方式" width="80" >
-        <template #default="scope">{{ formatTradeMode(scope.row.tradeMode) }}</template>
-      </el-table-column>
-      <el-table-column property="declMode" label="报关模式" width="80" >
-        <template #default="scope">
-          <dict-tag :options="dict.decl_mode" :dictValue="scope.row.declMode"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="tradeCountry" label="贸易国别" width="80" >
-        <template #default="scope">{{ formatCountryIso(scope.row.tradeCountry) }}</template>
-      </el-table-column>
-      <el-table-column property="ieDate" label="进出口时间" sortable width="150">
-        <template #default="scope">{{ scope.row.ieDate ? dayjs(scope.row.ieDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="declDate" label="申报时间" sortable width="150">
-        <template #default="scope">{{ scope.row.declDate ? dayjs(scope.row.declDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="declAdvanceFlag" label="是否提前申报" width="110" >
-        <template #default="scope">
-          <dict-tag :options="dict.yes_no" :dictValue="scope.row.declAdvanceFlag"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="acceptDate" label="自动受理时间" width="150">
-        <template #default="scope">{{ scope.row.acceptDate ? dayjs(scope.row.acceptDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="exInPortDate" label="货物进港时间" width="150">
-        <template #default="scope">{{ scope.row.exInPortDate ? dayjs(scope.row.exInPortDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="consignCode" label="境内收发货人代码" width="130"/>
-      <el-table-column property="consignName" label="境内收发货人名称" :show-overflow-tooltip="true" min-width="200">
-      </el-table-column>
-      <el-table-column property="frnConsignCode" label="境外收发货人代码" width="130"/>
-      <el-table-column property="frnConsignName" label="境外收发货人名称(中文)" :show-overflow-tooltip="true" min-width="200">
-      </el-table-column>
-      <el-table-column property="grossWt" label="货运量毛重" width="90"/>
-      <el-table-column property="netWt" label="货运量净重" width="90"/>
-      <el-table-column property="rmbPrice" label="货运值人民币(总)" width="130"/>
-      <el-table-column property="usdPrice" label="货运值美元(总)" width="130"/>
-      <el-table-column property="orderReceiveDate" label="现场接单时间" width="150">
-        <template #default="scope">{{ scope.row.orderReceiveDate ? dayjs(scope.row.orderReceiveDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="orderReceiveCost" label="接单耗时(小时)" width="120"/>
-      <el-table-column property="certRlsDate" label="单证放行时间" width="150">
-        <template #default="scope">{{ scope.row.certRlsDate ? dayjs(scope.row.certRlsDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="preReleaseDate" label="担保放行时间" width="150">
-        <template #default="scope">{{ scope.row.preReleaseDate ? dayjs(scope.row.preReleaseDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="agentCode" label="申报单位代码" width="130"/>
-      <el-table-column property="agentName" label="申报单位名称" :show-overflow-tooltip="true" min-width="200">
-      </el-table-column>
-      <el-table-column property="noteS" label="备注" :show-overflow-tooltip="true" min-width="200"/>
-      <el-table-column property="ownerCode" label="生产销售单位代码" width="130"/>
-      <el-table-column property="ownerName" label="生产销售单位名称" :show-overflow-tooltip="true" min-width="200">
-      </el-table-column>
-      <el-table-column property="examDate" label="转关数据发送时间" width="150">
-        <template #default="scope">{{ scope.row.examDate ? dayjs(scope.row.examDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="checkDate" label="转关数据核销时间" width="150">
-        <template #default="scope">{{ scope.row.checkDate ? dayjs(scope.row.checkDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="releaseDate" label="结关时间" sortable width="150">
-        <template #default="scope">{{ scope.row.releaseDate ? dayjs(scope.row.releaseDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="cuCost" label="海关通关时间(小时)" width="140"/>
-      <el-table-column property="totalCost" label="整体通关时间(小时)" width="140"/>
-      <el-table-column property="beforeDeclCost" label="申报前准备时间(小时)" width="150"/>
-      <el-table-column property="profVerifyFlag" label="是否专业审单" width="110" >
-        <template #default="scope">
-          <dict-tag :options="dict.yes_no" :dictValue="scope.row.profVerifyFlag"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="newTwoStepFlag" label="是否新两步申报" width="120" >
-        <template #default="scope">
-          <dict-tag :options="dict.yes_no" :dictValue="scope.row.newTwoStepFlag"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="assessStartDate" label="现场验估时间" width="150">
-        <template #default="scope">{{ scope.row.assessStartDate ? dayjs(scope.row.assessStartDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="assessEndDate" label="验估处置完毕时间" width="150">
-        <template #default="scope">{{ scope.row.assessEndDate ? dayjs(scope.row.assessEndDate).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="checkFlag" label="是否查验" width="80" >
-        <template #default="scope">
-          <dict-tag :options="dict.yes_no" :dictValue="scope.row.checkFlag"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="checkCustomsCode" label="查验海关" width="100">
-        <template #default="scope">
-          <dict-tag :options="dict.affiliation_customs_info" :dictValue="scope.row.checkCustomsCode"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="manCreateTime" label="查验指令下达时间" width="150">
-        <template #default="scope">{{ scope.row.manCreateTime ? dayjs(scope.row.manCreateTime).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="manChkTimeStart" label="查验开始时间" sortable width="150">
-        <template #default="scope">{{ scope.row.manChkTimeStart ? dayjs(scope.row.manChkTimeStart).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="manChkTimeEnd" label="查验结束时间" width="150">
-        <template #default="scope">{{ scope.row.manChkTimeEnd ? dayjs(scope.row.manChkTimeEnd).format('YYYY-MM-DD HH:mm:ss'):'' }}</template>
-      </el-table-column>
-      <el-table-column property="manProcResult" label="处理结果" :show-overflow-tooltip="true" width="80">
-        <template #default="scope">
-          <dict-tag :options="dict.proc_result" :dictValue="scope.row.manProcResult"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="manProcIdea" label="处理意见" :show-overflow-tooltip="true" width="80">
-        <template #default="scope">
-          <dict-tag :options="dict.proc_idea" :dictValue="scope.row.manProcIdea"/>
-        </template>
-      </el-table-column>
-      <el-table-column property="gno" label="商品项号"  width="80" />
-      <el-table-column property="iqCode" label="检验检疫编码" width="120" />
-      <el-table-column property="codeTs" label="商品编码" width="100" />
-      <el-table-column property="gname" label="商品名称" :show-overflow-tooltip="true" min-width="120" />
-      <el-table-column property="gmodel" label="规格型号" :show-overflow-tooltip="true" min-width="120" />
-      <el-table-column property="qty1" label="第一(法定)数量" width="120" />
-      <el-table-column property="rmbPriceList" label="商品货运值人民币" width="130"/>
-      <el-table-column property="usdPriceList" label="商品货运值美元" width="130"/>
-      <el-table-column property="gCertFlag" label="每项商品需要监管证件" width="160"/>
-      <el-table-column property="ungid" label="UN编码" width="70"/>
-      <el-table-column property="ungClassify" label="危包类别" width="80"/>
-      <el-table-column property="ungGName" label="危险货物名称" :show-overflow-tooltip="true" min-width="120"/>
-      <el-table-column property="productCharCode" label="货物属性代码" width="110"/>
-      <el-table-column property="goodsType" label="危险品类型" width="130">
-        <template #default="scope">
-          <dict-tag :options="dict.goods_type" :whole-match="true" :dictValue="scope.row.goodsType"/>
-        </template>
-      </el-table-column>
+      <!-- 动态渲染所有列 -->
+      <template v-for="column in dynamicColumns.visibleColumns.value" :key="column.key">
+        <el-table-column v-bind="getColumnProps(column)">
+          <template #default="scope">
+            <template v-if="column.isDict">
+              <dict-tag
+                :options="getDictOptions(column.dictKey!)"
+                :dictValue="scope.row[column.property!]"
+                :whole-match="column.key === 'goodsType'"
+              />
+            </template>
+            <template v-else-if="column.isDate">
+              {{ scope.row[column.property!] ? dayjs(scope.row[column.property!]).format('YYYY-MM-DD HH:mm:ss'):'' }}
+            </template>
+            <template v-else-if="getFormatter(column.key)">
+              {{ getFormatter(column.key)!(scope.row[column.property!]) }}
+            </template>
+            <template v-else>
+              {{ scope.row[column.property!] }}
+            </template>
+          </template>
+        </el-table-column>
+      </template>
     </cacp-complex-table>
+
+    <!-- 动态列设置对话框 -->
+    <DynamicColumnDialog
+      v-model="showColumnDialog"
+      :width="'70%'"
+      :columns="allColumnConfigs"
+      :selected-keys="dynamicColumns.selectedKeys.value"
+      @update:selected-keys="dynamicColumns.updateSelectedKeys"
+    />
   </cacp-search-layout>
 </template>
 <script setup lang="ts">
@@ -311,6 +191,8 @@ import { permissionStatus } from '@/utils/globalPermission'
 import { useDictType } from '@/components/useDict'
 import { useCoreStore } from '@/stores'
 import DictTag from "@/components/DictTag/dictTag.vue";
+import DynamicColumnDialog, { type ColumnConfig } from '@/components/DynamicColumnSelector/DynamicColumnDialog.vue'
+import { useDynamicColumns } from '@/hooks/useDynamicColumns'
 const coreStore = useCoreStore()
 
 interface State {
@@ -331,7 +213,653 @@ const tradeMode = ref([]);
 const tradeCountry = ref([]);
 const countryIso = ref([]);
 
-const now = dayjs()
+const now = dayjs();
+
+// 定义所有列的配置
+const allColumnConfigs: ColumnConfig[] = [
+  {
+    key: 'entryId',
+    property: 'entryId',
+    label: '报关单号',
+    width: 160,
+    sortable: true,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true
+  },
+  {
+    key: 'passMode',
+    property: 'passMode',
+    label: '通关模式',
+    width: 80,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true,
+    isDict: true,
+    dictKey: 'pass_mode'
+  },
+  {
+    key: 'trafMode',
+    property: 'trafMode',
+    label: '运输方式',
+    width: 80,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true
+  },
+  {
+    key: 'ieFlag',
+    property: 'ieFlag',
+    label: '出入境标志',
+    width: 90,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true,
+    isDict: true,
+    dictKey: 'ie_flag'
+  },
+  {
+    key: 'iePort',
+    property: 'iePort',
+    label: '进出境口岸',
+    width: 100,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true
+  },
+  {
+    key: 'customsCode',
+    property: 'customsCode',
+    label: '主管海关',
+    width: 100,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true,
+    isDict: true,
+    dictKey: 'affiliation_customs_info'
+  },
+  {
+    key: 'tradeMode',
+    property: 'tradeMode',
+    label: '监管方式',
+    width: 80,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true
+  },
+  {
+    key: 'declMode',
+    property: 'declMode',
+    label: '报关模式',
+    width: 80,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true,
+    isDict: true,
+    dictKey: 'decl_mode'
+  },
+  {
+    key: 'tradeCountry',
+    property: 'tradeCountry',
+    label: '贸易国别',
+    width: 80,
+    groupKey: 'entry',
+    groupTitle: '报关单信息',
+    defaultVisible: true
+  },
+
+  // 时间信息组
+  {
+    key: 'ieDate',
+    property: 'ieDate',
+    label: '进出口时间',
+    width: 150,
+    sortable: true,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: true,
+    isDate: true
+  },
+  {
+    key: 'declDate',
+    property: 'declDate',
+    label: '申报时间',
+    width: 150,
+    sortable: true,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: true,
+    isDate: true
+  },
+  {
+    key: 'releaseDate',
+    property: 'releaseDate',
+    label: '结关时间',
+    width: 150,
+    sortable: true,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: true,
+    isDate: true
+  },
+  {
+    key: 'declAdvanceFlag',
+    property: 'declAdvanceFlag',
+    label: '是否提前申报',
+    width: 110,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: true,
+    isDict: true,
+    dictKey: 'yes_no'
+  },
+  {
+    key: 'acceptDate',
+    property: 'acceptDate',
+    label: '自动受理时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'exInPortDate',
+    property: 'exInPortDate',
+    label: '货物进港时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'orderReceiveDate',
+    property: 'orderReceiveDate',
+    label: '现场接单时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'certRlsDate',
+    property: 'certRlsDate',
+    label: '单证放行时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'preReleaseDate',
+    property: 'preReleaseDate',
+    label: '担保放行时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'examDate',
+    property: 'examDate',
+    label: '转关数据发送时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'checkDate',
+    property: 'checkDate',
+    label: '转关数据核销时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'assessStartDate',
+    property: 'assessStartDate',
+    label: '现场验估时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'assessEndDate',
+    property: 'assessEndDate',
+    label: '验估处置完毕时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'manCreateTime',
+    property: 'manCreateTime',
+    label: '查验指令下达时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'manChkTimeStart',
+    property: 'manChkTimeStart',
+    label: '查验开始时间',
+    width: 150,
+    sortable: true,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+  {
+    key: 'manChkTimeEnd',
+    property: 'manChkTimeEnd',
+    label: '查验结束时间',
+    width: 150,
+    groupKey: 'time',
+    groupTitle: '时间信息',
+    defaultVisible: false,
+    isDate: true
+  },
+
+  // 单位信息组
+  {
+    key: 'consignCode',
+    property: 'consignCode',
+    label: '境内收发货人代码',
+    width: 130,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: true
+  },
+  {
+    key: 'consignName',
+    property: 'consignName',
+    label: '境内收发货人名称',
+    showOverflowTooltip: true,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: true
+  },
+  {
+    key: 'frnConsignCode',
+    property: 'frnConsignCode',
+    label: '境外收发货人代码',
+    width: 130,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: true
+  },
+  {
+    key: 'frnConsignName',
+    property: 'frnConsignName',
+    label: '境外收发货人名称(中文)',
+    showOverflowTooltip: true,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: true
+  },
+  {
+    key: 'agentCode',
+    property: 'agentCode',
+    label: '申报单位代码',
+    width: 130,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: false
+  },
+  {
+    key: 'agentName',
+    property: 'agentName',
+    label: '申报单位名称',
+    showOverflowTooltip: true,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: false
+  },
+  {
+    key: 'ownerCode',
+    property: 'ownerCode',
+    label: '生产销售单位代码',
+    width: 130,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: false
+  },
+  {
+    key: 'ownerName',
+    property: 'ownerName',
+    label: '生产销售单位名称',
+    showOverflowTooltip: true,
+    groupKey: 'company',
+    groupTitle: '单位信息',
+    defaultVisible: false
+  },
+
+  // 货运信息组
+  {
+    key: 'grossWt',
+    property: 'grossWt',
+    label: '货运量毛重',
+    width: 90,
+    groupKey: 'cargo',
+    groupTitle: '货运信息',
+    defaultVisible: true
+  },
+  {
+    key: 'netWt',
+    property: 'netWt',
+    label: '货运量净重',
+    width: 90,
+    groupKey: 'cargo',
+    groupTitle: '货运信息',
+    defaultVisible: true
+  },
+  {
+    key: 'rmbPrice',
+    property: 'rmbPrice',
+    label: '货运值人民币(总)',
+    width: 130,
+    groupKey: 'cargo',
+    groupTitle: '货运信息',
+    defaultVisible: true
+  },
+  {
+    key: 'usdPrice',
+    property: 'usdPrice',
+    label: '货运值美元(总)',
+    width: 130,
+    groupKey: 'cargo',
+    groupTitle: '货运信息',
+    defaultVisible: true
+  },
+  {
+    key: 'grossWt',
+    property: 'grossWt',
+    label: '货运量毛重',
+    width: 90,
+    groupKey: 'cargo',
+    groupTitle: '货运信息',
+    defaultVisible: false
+  },
+
+  // 时效信息组
+  {
+    key: 'orderReceiveCost',
+    property: 'orderReceiveCost',
+    label: '接单耗时(小时)',
+    width: 120,
+    groupKey: 'duration',
+    groupTitle: '时效信息',
+    defaultVisible: true
+  },
+  {
+    key: 'cuCost',
+    property: 'cuCost',
+    label: '海关通关时间(小时)',
+    width: 140,
+    groupKey: 'duration',
+    groupTitle: '时效信息',
+    defaultVisible: true
+  },
+  {
+    key: 'totalCost',
+    property: 'totalCost',
+    label: '整体通关时间(小时)',
+    width: 140,
+    groupKey: 'duration',
+    groupTitle: '时效信息',
+    defaultVisible: true
+  },
+  {
+    key: 'beforeDeclCost',
+    property: 'beforeDeclCost',
+    label: '申报前准备时间(小时)',
+    width: 150,
+    groupKey: 'duration',
+    groupTitle: '时效信息',
+    defaultVisible: false
+  },
+
+  // 查验信息组
+  {
+    key: 'checkFlag',
+    property: 'checkFlag',
+    label: '是否查验',
+    width: 80,
+    groupKey: 'inspection',
+    groupTitle: '查验信息',
+    defaultVisible: true,
+    isDict: true,
+    dictKey: 'yes_no'
+  },
+  {
+    key: 'checkCustomsCode',
+    property: 'checkCustomsCode',
+    label: '查验海关',
+    width: 100,
+    groupKey: 'inspection',
+    groupTitle: '查验信息',
+    defaultVisible: false,
+    isDict: true,
+    dictKey: 'affiliation_customs_info'
+  },
+  {
+    key: 'manProcResult',
+    property: 'manProcResult',
+    label: '处理结果',
+    width: 80,
+    showOverflowTooltip: true,
+    groupKey: 'inspection',
+    groupTitle: '查验信息',
+    defaultVisible: false,
+    isDict: true,
+    dictKey: 'proc_result'
+  },
+  {
+    key: 'manProcIdea',
+    property: 'manProcIdea',
+    label: '处理意见',
+    width: 80,
+    showOverflowTooltip: true,
+    groupKey: 'inspection',
+    groupTitle: '查验信息',
+    defaultVisible: false,
+    isDict: true,
+    dictKey: 'proc_idea'
+  },
+  {
+    key: 'profVerifyFlag',
+    property: 'profVerifyFlag',
+    label: '是否专业审单',
+    width: 110,
+    groupKey: 'inspection',
+    groupTitle: '查验信息',
+    defaultVisible: false,
+    isDict: true,
+    dictKey: 'yes_no'
+  },
+  {
+    key: 'newTwoStepFlag',
+    property: 'newTwoStepFlag',
+    label: '是否新两步申报',
+    width: 120,
+    groupKey: 'inspection',
+    groupTitle: '查验信息',
+    defaultVisible: false,
+    isDict: true,
+    dictKey: 'yes_no'
+  },
+
+  // 商品信息组
+  {
+    key: 'gno',
+    property: 'gno',
+    label: '商品项号',
+    width: 80,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: true
+  },
+  {
+    key: 'iqCode',
+    property: 'iqCode',
+    label: '检验检疫编码',
+    width: 120,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'codeTs',
+    property: 'codeTs',
+    label: '商品编码',
+    width: 100,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'gname',
+    property: 'gname',
+    label: '商品名称',
+    showOverflowTooltip: true,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: true
+  },
+  {
+    key: 'gmodel',
+    property: 'gmodel',
+    label: '规格型号',
+    showOverflowTooltip: true,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: true
+  },
+  {
+    key: 'qty1',
+    property: 'qty1',
+    label: '第一(法定)数量',
+    width: 120,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'rmbPriceList',
+    property: 'rmbPriceList',
+    label: '商品货运值人民币',
+    width: 130,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'usdPriceList',
+    property: 'usdPriceList',
+    label: '商品货运值美元',
+    width: 130,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'gCertFlag',
+    property: 'gCertFlag',
+    label: '每项商品需要监管证件',
+    width: 160,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'ungid',
+    property: 'ungid',
+    label: 'UN编码',
+    width: 70,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'ungClassify',
+    property: 'ungClassify',
+    label: '危包类别',
+    width: 80,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'ungGName',
+    property: 'ungGName',
+    label: '危险货物名称',
+    showOverflowTooltip: true,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'productCharCode',
+    property: 'productCharCode',
+    label: '货物属性代码',
+    width: 110,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: false
+  },
+  {
+    key: 'goodsType',
+    property: 'goodsType',
+    label: '危险品类型',
+    width: 130,
+    groupKey: 'goods',
+    groupTitle: '商品信息',
+    defaultVisible: true,
+    isDict: true,
+    dictKey: 'goods_type'
+  },
+
+  // 其他信息组
+  {
+    key: 'noteS',
+    property: 'noteS',
+    label: '备注',
+    showOverflowTooltip: true,
+    groupKey: 'other',
+    groupTitle: '其他信息',
+    defaultVisible: false
+  }
+];
+
+// 使用动态列 Hook
+const dynamicColumns = useDynamicColumns({
+  storageKey: 'goods_entry_columns',
+  defaultVisibleKeys: allColumnConfigs
+    .filter(col => col.defaultVisible)
+    .map(col => col.key),
+  columns: allColumnConfigs
+})
+
+// 对话框显示状态
+const showColumnDialog = ref(false)
+
+
+
 
 const state = reactive<State>({
   queryData: {
@@ -370,9 +898,34 @@ const actions = <Array<TableAction>>[
     onclick: onExportData,
     limit: permissionStatus('none', 'GOODS_ENTRY_EXPORT_BT'),
     type: 'primary'
+  },
+  {
+    key: '3',
+    text: '动态列设置',
+    onclick: onColumnsSelector,
+    limit: 'none',
+    // limit: permissionStatus('none', 'GOODS_ENTRY_EXPORT_BT'),
+    type: 'primary'
   }
 ]
 
+const getColumnProps = (column: any) => {
+  // 类型断言为 ColumnConfig
+  const col = column as ColumnConfig
+
+  const props: Record<string, any> = {
+    property: col.property || col.key,
+    label: col.label
+  }
+
+  if (col.width) props.width = col.width
+  if (col.sortable) props.sortable = col.sortable
+  if (col.showOverflowTooltip) props['show-overflow-tooltip'] = col.showOverflowTooltip
+
+  return props
+}
+
+
 function onSearch() {
   onPageChange(1)
 }
@@ -382,6 +935,11 @@ function onReset() {
   onPageChange(1)
 }
 
+
+function onColumnsSelector() {
+  showColumnDialog.value = true;
+}
+
 function onView(row: Entry) {
   router.push({
     path: '/get-goods',
@@ -435,6 +993,24 @@ function formatIePort(code: string): string {
   }
   return ''
 }
+
+// 获取字典选项
+const getDictOptions = (dictKey: string) => {
+  return dict.value?.[dictKey] || []
+}
+
+// 根据列键获取格式化函数
+const getFormatter = (key: string): ((value: any) => string) | undefined => {
+  const formatters: Record<string, (value: any) => string> = {
+    'trafMode': formatTrafMode,
+    'tradeMode': formatTradeMode,
+    'tradeCountry': formatCountryIso,
+    'iePort': formatIePort
+  }
+
+  return formatters[key]
+}
+
 // 加载数据
 async function onLoadData() {
   const loginCustomsCode = coreStore.currentUser.customsCode;
@@ -460,7 +1036,8 @@ async function onLoadData() {
 
 // 加载数据
 async function onExportData() : Promise<void>{
-  const query = { ...state.queryData}
+  console.log(dynamicColumns.selectedKeys.value)
+  const query = { ...state.queryData,exportHeadList:dynamicColumns.selectedKeys.value.toString()}
   await exportList(query);
 }
 
@@ -501,7 +1078,10 @@ async function getCountryIso() {
 
 onBeforeMount(async () => {
   await Promise.all([ getCustoms(),getTrafMode(),getTradeMode(),getTradeCountry(),getCountryIso()])
-  onLoadData()
+  setTimeout(() => {
+    console.log('字典数据:', dict)
+    onLoadData()
+  }, 500)
 //  console.log(dict);
 })
 </script>