fix: 增加导出excel工具类
This commit is contained in:
parent
a69695bafc
commit
8e5a76e644
82
backend/src/main/java/com/yfd/platform/annotation/Excel.java
Normal file
82
backend/src/main/java/com/yfd/platform/annotation/Excel.java
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package com.yfd.platform.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用 Excel 导出列注解(参考若依 RuoYi 的 @Excel 设计)
|
||||||
|
* <p>标注在实体字段上,配合 {@code ExcelUtil<T>} 即可实现该字段的导出。
|
||||||
|
* 支持的转换能力:字典翻译(dictType)、静态值转换(readConverterExp)、日期格式化。</p>
|
||||||
|
* <pre>
|
||||||
|
* @Excel(name = "性别", dictType = "sys_user_sex", sort = 3)
|
||||||
|
* private Integer gender;
|
||||||
|
*
|
||||||
|
* @Excel(name = "状态", readConverterExp = "0=停用,1=正常", sort = 5)
|
||||||
|
* private Integer status;
|
||||||
|
*
|
||||||
|
* @Excel(name = "创建时间", dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 6)
|
||||||
|
* private LocalDateTime createTime;
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
public @interface Excel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出列名(Excel 表头)
|
||||||
|
*/
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列排序(数值越小越靠前;未标注 sort 的字段按声明顺序排在其后)
|
||||||
|
*/
|
||||||
|
int sort() default Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型:如 "sys_user_sex",导出时自动将字典值翻译为字典标签
|
||||||
|
* (复用项目 DictCache 缓存,无需额外查询)
|
||||||
|
*/
|
||||||
|
String dictType() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态转换表达式:如 "0=正常,1=停用"(逗号分隔,等号前为值、后为标签)
|
||||||
|
* 优先级高于 {@link #dictType()}
|
||||||
|
*/
|
||||||
|
String readConverterExp() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期格式(支持 java.util.Date、LocalDate、LocalDateTime)
|
||||||
|
*/
|
||||||
|
String dateFormat() default "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列宽(字符数),0 表示根据表头与内容自动计算
|
||||||
|
*/
|
||||||
|
int width() default 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否导出该列
|
||||||
|
*/
|
||||||
|
boolean isExport() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对齐方式
|
||||||
|
*/
|
||||||
|
Align align() default Align.AUTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单元格对齐
|
||||||
|
*/
|
||||||
|
enum Align {
|
||||||
|
/** 自动(文本左对齐、数值右对齐) */
|
||||||
|
AUTO,
|
||||||
|
/** 左对齐 */
|
||||||
|
LEFT,
|
||||||
|
/** 居中 */
|
||||||
|
CENTER,
|
||||||
|
/** 右对齐 */
|
||||||
|
RIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,11 +7,15 @@ import com.yfd.platform.qgc_base.domain.SdWtBH;
|
|||||||
import com.yfd.platform.qgc_base.domain.SdEngInfoBHOperateRequest;
|
import com.yfd.platform.qgc_base.domain.SdEngInfoBHOperateRequest;
|
||||||
import com.yfd.platform.qgc_base.service.ISdWtBHService;
|
import com.yfd.platform.qgc_base.service.ISdWtBHService;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.yfd.platform.utils.ExcelUtil;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/base/wt")
|
@RequestMapping("/base/wt")
|
||||||
@Tag(name = "水温站管理")
|
@Tag(name = "水温站管理")
|
||||||
@ -51,4 +55,11 @@ public class SdWtBHController {
|
|||||||
boolean result = service.delete(request == null ? null : request.getIds(), request == null ? null : request.getSource());
|
boolean result = service.delete(request == null ? null : request.getIds(), request == null ? null : request.getSource());
|
||||||
return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败");
|
return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/export")
|
||||||
|
@Operation(summary = "水温站导出")
|
||||||
|
public void export(@RequestBody DataSourceRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
Page<SdWtBH> page = service.queryPageList(request);
|
||||||
|
new ExcelUtil<>(SdWtBH.class).exportExcel(response, page.getRecords(), "水温站数据", "水温站导出");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.yfd.platform.annotation.Excel;
|
||||||
import com.yfd.platform.annotation.FieldChinese;
|
import com.yfd.platform.annotation.FieldChinese;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@ -23,15 +24,18 @@ public class SdWtBH implements Serializable {
|
|||||||
/** 水温站站码 */
|
/** 水温站站码 */
|
||||||
@TableId(type = IdType.INPUT)
|
@TableId(type = IdType.INPUT)
|
||||||
@FieldChinese("水温站站码")
|
@FieldChinese("水温站站码")
|
||||||
|
@Excel(name = "水温站站码")
|
||||||
private String stcd;
|
private String stcd;
|
||||||
|
|
||||||
/** 数据时间 */
|
/** 数据时间 */
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
@FieldChinese("数据时间")
|
@FieldChinese("数据时间")
|
||||||
|
@Excel(name="数据时间")
|
||||||
private Date tm;
|
private Date tm;
|
||||||
|
|
||||||
/** 水温站站名 */
|
/** 水温站站名 */
|
||||||
@FieldChinese("水温站站名")
|
@FieldChinese("水温站站名")
|
||||||
|
@Excel(name="水温站站名")
|
||||||
private String stnm;
|
private String stnm;
|
||||||
|
|
||||||
/** 站类 */
|
/** 站类 */
|
||||||
@ -39,38 +43,47 @@ public class SdWtBH implements Serializable {
|
|||||||
private String sttp;
|
private String sttp;
|
||||||
|
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
|
@Excel(name="站类")
|
||||||
private String sttpName;
|
private String sttpName;
|
||||||
|
|
||||||
/** 经度 */
|
/** 经度 */
|
||||||
@FieldChinese("经度")
|
@FieldChinese("经度")
|
||||||
|
@Excel(name="经度")
|
||||||
private BigDecimal lgtd;
|
private BigDecimal lgtd;
|
||||||
|
|
||||||
/** 纬度 */
|
/** 纬度 */
|
||||||
@FieldChinese("纬度")
|
@FieldChinese("纬度")
|
||||||
|
@Excel(name="纬度")
|
||||||
private BigDecimal lttd;
|
private BigDecimal lttd;
|
||||||
|
|
||||||
/** 高程 */
|
/** 高程 */
|
||||||
@FieldChinese("高程")
|
@FieldChinese("高程")
|
||||||
|
@Excel(name = "高程")
|
||||||
private BigDecimal elev;
|
private BigDecimal elev;
|
||||||
|
|
||||||
/** 站址 */
|
/** 站址 */
|
||||||
@FieldChinese("站址")
|
@FieldChinese("站址")
|
||||||
|
@Excel(name="站址")
|
||||||
private String stlc;
|
private String stlc;
|
||||||
|
|
||||||
/** 测站来源 */
|
/** 测站来源 */
|
||||||
@FieldChinese("测站来源")
|
@FieldChinese("测站来源")
|
||||||
|
@Excel(name="测站来源")
|
||||||
private String stsr;
|
private String stsr;
|
||||||
|
|
||||||
/** 水温监测断面设备类型:1=浮动式 2=固定式 */
|
/** 水温监测断面设备类型:1=浮动式 2=固定式 */
|
||||||
@FieldChinese("水温监测断面设备类型")
|
@FieldChinese("水温监测断面设备类型")
|
||||||
|
@Excel(name="水温监测断面设备类型")
|
||||||
private Integer wtDeviceType;
|
private Integer wtDeviceType;
|
||||||
|
|
||||||
/** 水温监测断面类型 */
|
/** 水温监测断面类型 */
|
||||||
@FieldChinese("水温监测断面类型")
|
@FieldChinese("水温监测断面类型")
|
||||||
|
@Excel(name="水温监测断面类型")
|
||||||
private String wtType;
|
private String wtType;
|
||||||
|
|
||||||
/** 电站水温结构类型 */
|
/** 电站水温结构类型 */
|
||||||
@FieldChinese("电站水温结构类型")
|
@FieldChinese("电站水温结构类型")
|
||||||
|
@Excel(name="电站水温结构类型")
|
||||||
private String wts;
|
private String wts;
|
||||||
|
|
||||||
/** 固定垂向水温类型:1=相对位置 2=高程 */
|
/** 固定垂向水温类型:1=相对位置 2=高程 */
|
||||||
|
|||||||
@ -2620,7 +2620,7 @@ public class WarnDataServiceImpl implements WarnDataService {
|
|||||||
}
|
}
|
||||||
String stcd = ao.getDvcd().trim();
|
String stcd = ao.getDvcd().trim();
|
||||||
String tm = DateUtil.formatDateTime(ao.getAitime());
|
String tm = DateUtil.formatDateTime(ao.getAitime());
|
||||||
String remarkJson = JSONUtil.toJsonStr(ao.getAidata());
|
// String remarkJson = JSONUtil.toJsonStr(ao.getAidata());
|
||||||
switch (ao.getType()) {
|
switch (ao.getType()) {
|
||||||
case "0000":
|
case "0000":
|
||||||
// ai心跳数据
|
// ai心跳数据
|
||||||
@ -2634,12 +2634,12 @@ public class WarnDataServiceImpl implements WarnDataService {
|
|||||||
aicomRows.add(new Object[]{
|
aicomRows.add(new Object[]{
|
||||||
stcd, tm, "AI_5001",
|
stcd, tm, "AI_5001",
|
||||||
ao.getAidata().getArea().toPlainString(),
|
ao.getAidata().getArea().toPlainString(),
|
||||||
ao.getFiles(), remarkJson
|
ao.getFiles()
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "5011":
|
case "5011":
|
||||||
// 漂浮物清理
|
// 漂浮物清理
|
||||||
aicomRows.add(new Object[]{stcd, tm, "AI_5011", null, ao.getFiles(), remarkJson});
|
aicomRows.add(new Object[]{stcd, tm, "AI_5011", null, ao.getFiles()});
|
||||||
break;
|
break;
|
||||||
case "5021":
|
case "5021":
|
||||||
// 视频流量
|
// 视频流量
|
||||||
@ -2647,7 +2647,7 @@ public class WarnDataServiceImpl implements WarnDataService {
|
|||||||
stcd, tm, "AI_5014",
|
stcd, tm, "AI_5014",
|
||||||
ao.getAidata() != null && ao.getAidata().getFlow() != null
|
ao.getAidata() != null && ao.getAidata().getFlow() != null
|
||||||
? ao.getAidata().getFlow().toPlainString() : null,
|
? ao.getAidata().getFlow().toPlainString() : null,
|
||||||
ao.getFiles(), remarkJson
|
ao.getFiles()
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "AI_RIV":
|
case "AI_RIV":
|
||||||
@ -2656,7 +2656,7 @@ public class WarnDataServiceImpl implements WarnDataService {
|
|||||||
aicomRows.add(new Object[]{
|
aicomRows.add(new Object[]{
|
||||||
stcd, tm, ao.getType(),
|
stcd, tm, ao.getType(),
|
||||||
ao.getAidata() != null ? ao.getAidata().getValue() : null,
|
ao.getAidata() != null ? ao.getAidata().getValue() : null,
|
||||||
ao.getFiles(), remarkJson
|
ao.getFiles()
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -2678,7 +2678,7 @@ public class WarnDataServiceImpl implements WarnDataService {
|
|||||||
sql.append(" UNION ALL ");
|
sql.append(" UNION ALL ");
|
||||||
}
|
}
|
||||||
sql.append("SELECT ? AS STCD, TO_DATE(?, 'YYYY-MM-DD HH24:MI:SS') AS TM, ")
|
sql.append("SELECT ? AS STCD, TO_DATE(?, 'YYYY-MM-DD HH24:MI:SS') AS TM, ")
|
||||||
.append("? AS TYPE, ? AS AI_VAL, ? AS FID, ? AS REMARK FROM DUAL");
|
.append("? AS TYPE, ? AS AI_VAL, ? AS FID AS REMARK FROM DUAL");
|
||||||
}
|
}
|
||||||
sql.append(") source ON (target.STCD = source.STCD AND target.TM = source.TM AND target.TYPE = source.TYPE) ")
|
sql.append(") source ON (target.STCD = source.STCD AND target.TM = source.TM AND target.TYPE = source.TYPE) ")
|
||||||
.append("WHEN NOT MATCHED THEN INSERT (STCD, TM, TYPE, AI_VAL, FID, REMARK, RECORD_TIME, IS_DELETED) ")
|
.append("WHEN NOT MATCHED THEN INSERT (STCD, TM, TYPE, AI_VAL, FID, REMARK, RECORD_TIME, IS_DELETED) ")
|
||||||
|
|||||||
288
backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java
Normal file
288
backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
package com.yfd.platform.utils;
|
||||||
|
|
||||||
|
import com.yfd.platform.annotation.Excel;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
|
import org.apache.poi.ss.usermodel.Font;
|
||||||
|
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||||
|
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFColor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用 Excel 导出工具(参考若依 RuoYi 的 ExcelUtil 设计)
|
||||||
|
* <p>
|
||||||
|
* 使用方法:
|
||||||
|
* <ol>
|
||||||
|
* <li>在实体字段上标注 {@link Excel} 注解,声明列名、排序、字典翻译、日期格式等;</li>
|
||||||
|
* <li>Controller 中按需查询数据后调用:<pre>
|
||||||
|
* List<SysUser> list = sysUserService.list(wrapper);
|
||||||
|
* new ExcelUtil<>(SysUser.class).exportExcel(response, list, "用户数据", "用户数据");
|
||||||
|
* </pre></li>
|
||||||
|
* </ol>
|
||||||
|
* 特性:流式写出(SXSSF,适合大数据量)、表头冻结与自动筛选、列宽自适应、
|
||||||
|
* 字典翻译(复用 DictCache)、静态值转换(readConverterExp)、日期格式化、Long 转字符串避免精度丢失。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class ExcelUtil<T> {
|
||||||
|
|
||||||
|
/** 表头背景色(中蓝色 RGB) */
|
||||||
|
private static final byte[] HEADER_BG = {(byte) 0x4A, (byte) 0x86, (byte) 0xC8};
|
||||||
|
/** 列宽上限(字符数),防止超长内容撑爆表格 */
|
||||||
|
private static final int MAX_COLUMN_WIDTH = 50;
|
||||||
|
|
||||||
|
private final Class<T> clazz;
|
||||||
|
/** 参与导出的字段列表(已按 @Excel.sort 升序排列,仅保留 isExport=true) */
|
||||||
|
private final List<Field> exportFields;
|
||||||
|
|
||||||
|
public ExcelUtil(Class<T> clazz) {
|
||||||
|
this.clazz = clazz;
|
||||||
|
this.exportFields = Arrays.stream(clazz.getDeclaredFields())
|
||||||
|
.filter(f -> f.isAnnotationPresent(Excel.class))
|
||||||
|
.filter(f -> f.getAnnotation(Excel.class).isExport())
|
||||||
|
.sorted(Comparator.comparingInt(f -> f.getAnnotation(Excel.class).sort()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (exportFields.isEmpty()) {
|
||||||
|
log.warn("ExcelUtil: 类 {} 未配置任何 @Excel 导出字段", clazz.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 对外 API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 Excel 到 HTTP 响应(自动设置 Content-Type 与文件名编码)
|
||||||
|
*
|
||||||
|
* @param response HTTP 响应
|
||||||
|
* @param list 待导出数据
|
||||||
|
* @param sheetName 工作表名称
|
||||||
|
* @param fileName 下载文件名(不含扩展名)
|
||||||
|
*/
|
||||||
|
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String fileName) throws IOException {
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("\\+", "%20");
|
||||||
|
response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + encoded + ".xlsx");
|
||||||
|
exportExcel(response.getOutputStream(), list, sheetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 Excel 到输出流(xlsx 格式)
|
||||||
|
*
|
||||||
|
* @param out 输出流
|
||||||
|
* @param list 待导出数据
|
||||||
|
* @param sheetName 工作表名称
|
||||||
|
*/
|
||||||
|
public void exportExcel(OutputStream out, List<T> list, String sheetName) throws IOException {
|
||||||
|
// SXSSF 流式写出:内存中只保留 100 行,适合大批量导出
|
||||||
|
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
|
||||||
|
writeSheet(workbook, sheetName, list);
|
||||||
|
workbook.write(out);
|
||||||
|
workbook.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 内部实现 ====================
|
||||||
|
|
||||||
|
private void writeSheet(SXSSFWorkbook workbook, String sheetName, List<T> list) {
|
||||||
|
String safeName = sanitizeSheetName(sheetName);
|
||||||
|
Sheet sheet = workbook.createSheet(safeName);
|
||||||
|
int columnCount = exportFields.size();
|
||||||
|
|
||||||
|
CellStyle headerStyle = buildHeaderStyle(workbook);
|
||||||
|
CellStyle dataLeft = buildDataStyle(workbook, HorizontalAlignment.LEFT);
|
||||||
|
CellStyle dataCenter = buildDataStyle(workbook, HorizontalAlignment.CENTER);
|
||||||
|
CellStyle dataRight = buildDataStyle(workbook, HorizontalAlignment.RIGHT);
|
||||||
|
|
||||||
|
int[] colMaxLen = new int[columnCount];
|
||||||
|
|
||||||
|
// 1. 表头行
|
||||||
|
Row headerRow = sheet.createRow(0);
|
||||||
|
for (int i = 0; i < columnCount; i++) {
|
||||||
|
Excel excel = exportFields.get(i).getAnnotation(Excel.class);
|
||||||
|
Cell cell = headerRow.createCell(i);
|
||||||
|
cell.setCellValue(excel.name());
|
||||||
|
cell.setCellStyle(headerStyle);
|
||||||
|
colMaxLen[i] = displayLength(excel.name());
|
||||||
|
}
|
||||||
|
// 冻结表头,滚动时保持可见
|
||||||
|
sheet.createFreezePane(0, 1);
|
||||||
|
|
||||||
|
// 2. 数据行
|
||||||
|
int dataRows = list == null ? 0 : list.size();
|
||||||
|
for (int r = 0; r < dataRows; r++) {
|
||||||
|
Row row = sheet.createRow(r + 1);
|
||||||
|
T entity = list.get(r);
|
||||||
|
for (int i = 0; i < columnCount; i++) {
|
||||||
|
Field field = exportFields.get(i);
|
||||||
|
String text = convertToString(field, entity);
|
||||||
|
Cell cell = row.createCell(i);
|
||||||
|
cell.setCellValue(text);
|
||||||
|
cell.setCellStyle(chooseDataStyle(field, dataLeft, dataCenter, dataRight));
|
||||||
|
int len = displayLength(text);
|
||||||
|
if (len > colMaxLen[i]) {
|
||||||
|
colMaxLen[i] = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 列宽:注解指定优先,否则按内容自适应
|
||||||
|
for (int i = 0; i < columnCount; i++) {
|
||||||
|
Excel excel = exportFields.get(i).getAnnotation(Excel.class);
|
||||||
|
int width = excel.width() > 0 ? excel.width() : Math.min(colMaxLen[i] + 2, MAX_COLUMN_WIDTH);
|
||||||
|
sheet.setColumnWidth(i, width * 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 自动筛选(表头 + 数据区)
|
||||||
|
if (columnCount > 0) {
|
||||||
|
sheet.setAutoFilter(new CellRangeAddress(0, Math.max(dataRows, 0), 0, columnCount - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将实体字段值转换为 Excel 单元格字符串
|
||||||
|
*/
|
||||||
|
private String convertToString(Field field, Object entity) {
|
||||||
|
Excel excel = field.getAnnotation(Excel.class);
|
||||||
|
Object value = getFieldValue(field, entity);
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String strValue = String.valueOf(value);
|
||||||
|
|
||||||
|
// 1. 静态转换表达式优先:如 "0=停用,1=正常"
|
||||||
|
if (!excel.readConverterExp().isEmpty()) {
|
||||||
|
for (String item : excel.readConverterExp().split(",")) {
|
||||||
|
String[] kv = item.split("=");
|
||||||
|
if (kv.length == 2 && kv[0].trim().equals(strValue)) {
|
||||||
|
return kv[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 字典翻译:如 dictType="sys_user_sex" → 字典标签
|
||||||
|
// if (!excel.dictType().isEmpty()) {
|
||||||
|
// try {
|
||||||
|
// String label = SpringUtils.getBean(DictCache.class).getLabel(excel.dictType(), value);
|
||||||
|
// if (label != null) {
|
||||||
|
// return label;
|
||||||
|
// }
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// log.warn("Excel 字典翻译失败: dictType={}, value={}, err={}", excel.dictType(), value, e.getMessage());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 3. 日期格式化
|
||||||
|
if (value instanceof LocalDateTime localDateTime) {
|
||||||
|
return localDateTime.format(DateTimeFormatter.ofPattern(excel.dateFormat()));
|
||||||
|
}
|
||||||
|
if (value instanceof LocalDate localDate) {
|
||||||
|
return localDate.format(DateTimeFormatter.ofPattern(excel.dateFormat()));
|
||||||
|
}
|
||||||
|
if (value instanceof Date date) {
|
||||||
|
return new SimpleDateFormat(excel.dateFormat()).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 默认(Long 直接转字符串,避免 Excel 科学计数法与前端精度丢失)
|
||||||
|
return strValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getFieldValue(Field field, Object entity) {
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(entity);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
log.warn("Excel 读取字段值失败: field={}, err={}", field.getName(), e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据注解对齐方式选择数据单元格样式
|
||||||
|
*/
|
||||||
|
private CellStyle chooseDataStyle(Field field, CellStyle left, CellStyle center, CellStyle right) {
|
||||||
|
Excel.Align align = field.getAnnotation(Excel.class).align();
|
||||||
|
return switch (align) {
|
||||||
|
case CENTER -> center;
|
||||||
|
case RIGHT -> right;
|
||||||
|
case AUTO, LEFT -> left;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private CellStyle buildHeaderStyle(SXSSFWorkbook workbook) {
|
||||||
|
// SXSSF 底层为 XSSFWorkbook,样式均为 XSSFCellStyle,可直接设置自定义 RGB 颜色
|
||||||
|
XSSFCellStyle style = (XSSFCellStyle) workbook.createCellStyle();
|
||||||
|
style.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
style.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
setBorders(style);
|
||||||
|
style.setFillForegroundColor(new XSSFColor(HEADER_BG, null));
|
||||||
|
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
Font font = workbook.createFont();
|
||||||
|
font.setBold(true);
|
||||||
|
font.setColor(IndexedColors.WHITE.getIndex());
|
||||||
|
font.setFontHeightInPoints((short) 11);
|
||||||
|
style.setFont(font);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CellStyle buildDataStyle(SXSSFWorkbook workbook, HorizontalAlignment alignment) {
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
style.setAlignment(alignment);
|
||||||
|
style.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
setBorders(style);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBorders(CellStyle style) {
|
||||||
|
style.setBorderTop(BorderStyle.THIN);
|
||||||
|
style.setBorderBottom(BorderStyle.THIN);
|
||||||
|
style.setBorderLeft(BorderStyle.THIN);
|
||||||
|
style.setBorderRight(BorderStyle.THIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算字符串显示宽度(中文字符按 2 个字符宽度计)
|
||||||
|
*/
|
||||||
|
private int displayLength(String s) {
|
||||||
|
if (s == null || s.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int len = 0;
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
len += s.charAt(i) > 0xFF ? 2 : 1;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤 Excel 工作表名非法字符([]:*?/\ 等)
|
||||||
|
*/
|
||||||
|
private String sanitizeSheetName(String name) {
|
||||||
|
String safe = name == null || name.isBlank() ? "Sheet1" : name.replaceAll("[\\[\\]:*?/\\\\]", "_");
|
||||||
|
return safe.length() > 31 ? safe.substring(0, 31) : safe;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user