feat: 增加了word文档导出工具方法
This commit is contained in:
parent
750445cbdd
commit
1c80c33bd6
@ -38,6 +38,7 @@
|
||||
<jsoup.version>1.11.3</jsoup.version>
|
||||
<jasypt.version>1.16</jasypt.version>
|
||||
<ip2region.version>1.7.2</ip2region.version>
|
||||
<jfreechart.version>1.5.4</jfreechart.version>
|
||||
<easy.captcha.version>1.6.2</easy.captcha.version>
|
||||
<useragentutils.version>1.21</useragentutils.version>
|
||||
<gson.version>2.10.1</gson.version>
|
||||
@ -256,6 +257,13 @@
|
||||
<version>${poi.ooxml.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JFreeChart 图表渲染(Word 图表导出) -->
|
||||
<dependency>
|
||||
<groupId>org.jfree</groupId>
|
||||
<artifactId>jfreechart</artifactId>
|
||||
<version>${jfreechart.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- fastjson -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package com.yfd.platform.qgc_export.controller;
|
||||
|
||||
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||
import com.yfd.platform.qgc_export.service.IWordChartExportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Word 图表导出控制器
|
||||
* <p>
|
||||
* 支持饼图、柱状图、折线图、折柱混合图、堆叠柱状图、堆叠柱状图归一化(百分比)等图形导出到 Word。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/qgcExport/word")
|
||||
@Tag(name = "Word 图表导出")
|
||||
@Validated
|
||||
public class WordChartExportController {
|
||||
|
||||
@Resource
|
||||
private IWordChartExportService wordChartExportService;
|
||||
|
||||
/**
|
||||
* 导出 Word 文档(含多种图表)
|
||||
*
|
||||
* @param request 文档标题 + 图表列表
|
||||
* @param response HttpServletResponse
|
||||
*/
|
||||
@PostMapping("/export")
|
||||
@Operation(summary = "导出 Word 文档(饼图/柱状图/折线图/折柱混合/堆叠/归一化堆叠)")
|
||||
public void exportWord(@RequestBody WordChartExportRequest request, HttpServletResponse response) {
|
||||
wordChartExportService.exportWord(request, response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.yfd.platform.qgc_export.domain.word;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Word 导出支持的图表类型
|
||||
*/
|
||||
@Getter
|
||||
public enum ChartType {
|
||||
|
||||
/** 饼图 */
|
||||
PIE("饼图"),
|
||||
/** 柱状图 */
|
||||
BAR("柱状图"),
|
||||
/** 折线图 */
|
||||
LINE("折线图"),
|
||||
/** 折柱混合图 */
|
||||
BAR_LINE("折柱混合图"),
|
||||
/** 堆叠柱状图 */
|
||||
STACKED_BAR("堆叠柱状图"),
|
||||
/** 堆叠柱状图(归一化/百分比) */
|
||||
PERCENT_STACKED_BAR("堆叠柱状图归一化");
|
||||
|
||||
private final String desc;
|
||||
|
||||
ChartType(String desc) {
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略大小写解析图表类型
|
||||
*/
|
||||
public static ChartType from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalized = value.trim().replace("-", "_").toUpperCase();
|
||||
for (ChartType type : values()) {
|
||||
if (type.name().equals(normalized)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
// 兼容别名
|
||||
return switch (normalized) {
|
||||
case "BARLINE", "BAR_LINE", "LINECOLUMN", "MIXED" -> BAR_LINE;
|
||||
case "STACKED", "STACKEDBAR" -> STACKED_BAR;
|
||||
case "PERCENT_STACKED", "PERCENTSTACKEDBAR", "NORMALIZED" -> PERCENT_STACKED_BAR;
|
||||
case "COLUMN" -> BAR;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.yfd.platform.qgc_export.domain.word;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Word 图表导出请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "Word 图表导出请求")
|
||||
public class WordChartExportRequest {
|
||||
|
||||
@Schema(description = "文档标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "文档副标题/说明(可选)")
|
||||
private String subtitle;
|
||||
|
||||
@Schema(description = "图表列表,按顺序依次插入文档")
|
||||
private List<WordChartVo> charts = new ArrayList<>();
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.yfd.platform.qgc_export.domain.word;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Word 文档中的单个图表描述
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "Word 图表")
|
||||
public class WordChartVo {
|
||||
|
||||
@Schema(description = "图表类型:pie/bar/line/barLine/stackedBar/percentStackedBar")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "图表标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "X 轴名称")
|
||||
private String xAxisName;
|
||||
|
||||
@Schema(description = "Y 轴(左轴)名称")
|
||||
private String yAxisName;
|
||||
|
||||
@Schema(description = "Y 轴(右轴)名称,折柱混合图用")
|
||||
private String yAxisNameRight;
|
||||
|
||||
@Schema(description = "X 轴分类(如月份、站点名等)")
|
||||
private List<String> categories = new ArrayList<>();
|
||||
|
||||
@Schema(description = "系列数据列表")
|
||||
private List<WordSeriesVo> series = new ArrayList<>();
|
||||
|
||||
@Schema(description = "是否显示数值标签")
|
||||
private boolean showValues = true;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.yfd.platform.qgc_export.domain.word;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图表中的单个系列(折柱混合图中可指定主轴/副轴)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "图表系列")
|
||||
public class WordSeriesVo {
|
||||
|
||||
@Schema(description = "系列名称(图例)")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "系列数值,与 categories 一一对应")
|
||||
private List<Double> values = new ArrayList<>();
|
||||
|
||||
@Schema(description = "Y 轴索引:0=主轴(左), 1=副轴(右),默认 0。折柱混合图用")
|
||||
private Integer yAxisIndex = 0;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.yfd.platform.qgc_export.service;
|
||||
|
||||
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Word 图表导出服务
|
||||
*/
|
||||
public interface IWordChartExportService {
|
||||
|
||||
/**
|
||||
* 根据图表配置生成 Word 文档并写入响应
|
||||
*
|
||||
* @param request 文档标题 + 图表列表
|
||||
* @param response HttpServletResponse
|
||||
*/
|
||||
void exportWord(WordChartExportRequest request, HttpServletResponse response);
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.yfd.platform.qgc_export.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||
import com.yfd.platform.qgc_export.service.IWordChartExportService;
|
||||
import com.yfd.platform.qgc_export.util.WordChartExportUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Word 图表导出服务实现
|
||||
*/
|
||||
@Service
|
||||
public class WordChartExportServiceImpl implements IWordChartExportService {
|
||||
|
||||
@Override
|
||||
public void exportWord(WordChartExportRequest request, HttpServletResponse response) {
|
||||
byte[] data;
|
||||
try {
|
||||
data = WordChartExportUtil.generateWord(request);
|
||||
} catch (IOException e) {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
String fileName = resolveFileName(request);
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentLength(data.length);
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
|
||||
|
||||
try (OutputStream os = response.getOutputStream()) {
|
||||
os.write(data);
|
||||
os.flush();
|
||||
} catch (IOException e) {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveFileName(WordChartExportRequest request) {
|
||||
if (request != null && StrUtil.isNotBlank(request.getTitle())) {
|
||||
return request.getTitle() + ".docx";
|
||||
}
|
||||
return "图表报告.docx";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,308 @@
|
||||
package com.yfd.platform.qgc_export.util;
|
||||
|
||||
import com.yfd.platform.qgc_export.domain.word.ChartType;
|
||||
import com.yfd.platform.qgc_export.domain.word.WordChartVo;
|
||||
import com.yfd.platform.qgc_export.domain.word.WordSeriesVo;
|
||||
import org.jfree.chart.ChartFactory;
|
||||
import org.jfree.chart.JFreeChart;
|
||||
import org.jfree.chart.StandardChartTheme;
|
||||
import org.jfree.chart.axis.CategoryAxis;
|
||||
import org.jfree.chart.axis.NumberAxis;
|
||||
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
|
||||
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
|
||||
import org.jfree.chart.plot.CategoryPlot;
|
||||
import org.jfree.chart.plot.DatasetRenderingOrder;
|
||||
import org.jfree.chart.plot.PiePlot;
|
||||
import org.jfree.chart.plot.PlotOrientation;
|
||||
import org.jfree.chart.renderer.category.BarRenderer;
|
||||
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
|
||||
import org.jfree.chart.renderer.category.StackedBarRenderer;
|
||||
import org.jfree.chart.ui.RectangleInsets;
|
||||
import org.jfree.data.category.DefaultCategoryDataset;
|
||||
import org.jfree.data.general.DefaultPieDataset;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图表渲染工具:将结构化图表数据渲染为 BufferedImage,供 Word 导出插入。
|
||||
* <p>
|
||||
* 支持:饼图、柱状图、折线图、折柱混合图、堆叠柱状图、堆叠柱状图归一化(百分比)。
|
||||
*/
|
||||
public final class ChartRenderUtil {
|
||||
|
||||
/** 默认渲染宽度 */
|
||||
private static final int DEFAULT_WIDTH = 900;
|
||||
/** 默认渲染高度 */
|
||||
private static final int DEFAULT_HEIGHT = 520;
|
||||
/** 中文字体 */
|
||||
private static final String FONT_NAME = "SimSun";
|
||||
|
||||
static {
|
||||
// 统一主题,避免中文乱码,去除阴影
|
||||
StandardChartTheme theme = (StandardChartTheme) StandardChartTheme.createJFreeTheme();
|
||||
Font extraLarge = new Font(FONT_NAME, Font.BOLD, 20);
|
||||
Font large = new Font(FONT_NAME, Font.BOLD, 14);
|
||||
Font regular = new Font(FONT_NAME, Font.PLAIN, 12);
|
||||
Font small = new Font(FONT_NAME, Font.PLAIN, 10);
|
||||
theme.setExtraLargeFont(extraLarge);
|
||||
theme.setLargeFont(large);
|
||||
theme.setRegularFont(regular);
|
||||
theme.setSmallFont(small);
|
||||
theme.setShadowVisible(false);
|
||||
ChartFactory.setChartTheme(theme);
|
||||
}
|
||||
|
||||
private ChartRenderUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染图表为图片
|
||||
*/
|
||||
public static BufferedImage render(WordChartVo chart) {
|
||||
ChartType chartType = ChartType.from(chart.getType());
|
||||
if (chartType == null) {
|
||||
throw new IllegalArgumentException("不支持的图表类型: " + chart.getType());
|
||||
}
|
||||
JFreeChart jfreeChart = switch (chartType) {
|
||||
case PIE -> buildPieChart(chart);
|
||||
case BAR -> buildBarChart(chart);
|
||||
case LINE -> buildLineChart(chart);
|
||||
case BAR_LINE -> buildBarLineChart(chart);
|
||||
case STACKED_BAR -> buildStackedBarChart(chart, false);
|
||||
case PERCENT_STACKED_BAR -> buildStackedBarChart(chart, true);
|
||||
};
|
||||
if (jfreeChart == null) {
|
||||
return null;
|
||||
}
|
||||
jfreeChart.setBackgroundPaint(Color.WHITE);
|
||||
return jfreeChart.createBufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
}
|
||||
|
||||
private static JFreeChart buildPieChart(WordChartVo chart) {
|
||||
DefaultPieDataset<String> dataset = new DefaultPieDataset<>();
|
||||
List<String> categories = chart.getCategories();
|
||||
WordSeriesVo series = chart.getSeries().isEmpty() ? null : chart.getSeries().get(0);
|
||||
if (categories == null || categories.isEmpty() || series == null || series.getValues() == null) {
|
||||
return null;
|
||||
}
|
||||
List<Double> values = series.getValues();
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
Double value = i < values.size() ? values.get(i) : 0.0;
|
||||
dataset.setValue(categories.get(i), value == null ? 0.0 : value);
|
||||
}
|
||||
|
||||
JFreeChart jfreeChart = ChartFactory.createPieChart(
|
||||
chart.getTitle(), dataset, true, true, false);
|
||||
PiePlot<String> plot = (PiePlot<String>) jfreeChart.getPlot();
|
||||
plot.setLabelGenerator(new StandardPieSectionLabelGenerator(
|
||||
"{0}: {1} ({2})", new DecimalFormat("0.##"), new DecimalFormat("0.0%")));
|
||||
plot.setLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||
plot.setLegendLabelGenerator(new StandardPieSectionLabelGenerator("{0}"));
|
||||
plot.setBackgroundPaint(Color.WHITE);
|
||||
plot.setOutlineVisible(false);
|
||||
plot.setLabelBackgroundPaint(new Color(255, 255, 255, 200));
|
||||
plot.setLabelOutlinePaint(null);
|
||||
plot.setLabelShadowPaint(null);
|
||||
return jfreeChart;
|
||||
}
|
||||
|
||||
private static JFreeChart buildBarChart(WordChartVo chart) {
|
||||
DefaultCategoryDataset dataset = buildDataset(chart);
|
||||
JFreeChart jfreeChart = ChartFactory.createBarChart(
|
||||
chart.getTitle(),
|
||||
chart.getXAxisName(),
|
||||
chart.getYAxisName(),
|
||||
dataset,
|
||||
PlotOrientation.VERTICAL,
|
||||
true, true, false);
|
||||
CategoryPlot plot = jfreeChart.getCategoryPlot();
|
||||
stylePlot(plot);
|
||||
BarRenderer renderer = (BarRenderer) plot.getRenderer();
|
||||
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||
renderer.setDefaultItemLabelGenerator(
|
||||
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||
renderer.setItemMargin(0.1);
|
||||
return jfreeChart;
|
||||
}
|
||||
|
||||
private static JFreeChart buildLineChart(WordChartVo chart) {
|
||||
DefaultCategoryDataset dataset = buildDataset(chart);
|
||||
JFreeChart jfreeChart = ChartFactory.createLineChart(
|
||||
chart.getTitle(),
|
||||
chart.getXAxisName(),
|
||||
chart.getYAxisName(),
|
||||
dataset,
|
||||
PlotOrientation.VERTICAL,
|
||||
true, true, false);
|
||||
CategoryPlot plot = jfreeChart.getCategoryPlot();
|
||||
stylePlot(plot);
|
||||
LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
|
||||
renderer.setDefaultShapesVisible(true);
|
||||
renderer.setDefaultShapesFilled(true);
|
||||
renderer.setDefaultStroke(new BasicStroke(2.0f));
|
||||
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||
renderer.setDefaultItemLabelGenerator(
|
||||
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||
return jfreeChart;
|
||||
}
|
||||
|
||||
private static JFreeChart buildStackedBarChart(WordChartVo chart, boolean percent) {
|
||||
DefaultCategoryDataset dataset = buildDataset(chart);
|
||||
if (percent) {
|
||||
dataset = normalizePercent(dataset);
|
||||
}
|
||||
JFreeChart jfreeChart = ChartFactory.createStackedBarChart(
|
||||
chart.getTitle(),
|
||||
chart.getXAxisName(),
|
||||
percent ? (chart.getYAxisName() == null ? "占比(%)" : chart.getYAxisName()) : chart.getYAxisName(),
|
||||
dataset,
|
||||
PlotOrientation.VERTICAL,
|
||||
true, true, false);
|
||||
CategoryPlot plot = jfreeChart.getCategoryPlot();
|
||||
stylePlot(plot);
|
||||
StackedBarRenderer renderer = (StackedBarRenderer) plot.getRenderer();
|
||||
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||
if (percent) {
|
||||
renderer.setDefaultItemLabelGenerator(
|
||||
new StandardCategoryItemLabelGenerator("{2}%", new DecimalFormat("0.##")));
|
||||
NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
|
||||
rangeAxis.setRange(0.0, 100.0);
|
||||
rangeAxis.setNumberFormatOverride(new DecimalFormat("0'%'"));
|
||||
} else {
|
||||
renderer.setDefaultItemLabelGenerator(
|
||||
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||
}
|
||||
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||
return jfreeChart;
|
||||
}
|
||||
|
||||
private static JFreeChart buildBarLineChart(WordChartVo chart) {
|
||||
DefaultCategoryDataset barDataset = new DefaultCategoryDataset();
|
||||
DefaultCategoryDataset lineDataset = new DefaultCategoryDataset();
|
||||
boolean hasBar = false;
|
||||
boolean hasLine = false;
|
||||
for (WordSeriesVo series : chart.getSeries()) {
|
||||
if (series == null || series.getName() == null || series.getValues() == null) {
|
||||
continue;
|
||||
}
|
||||
boolean onRight = series.getYAxisIndex() != null && series.getYAxisIndex() == 1;
|
||||
DefaultCategoryDataset target = onRight ? lineDataset : barDataset;
|
||||
if (onRight) {
|
||||
hasLine = true;
|
||||
} else {
|
||||
hasBar = true;
|
||||
}
|
||||
for (int i = 0; i < chart.getCategories().size(); i++) {
|
||||
Double value = i < series.getValues().size() ? series.getValues().get(i) : null;
|
||||
target.addValue(value == null ? 0.0 : value, series.getName(), chart.getCategories().get(i));
|
||||
}
|
||||
}
|
||||
|
||||
// 若全部被归到一个轴,至少保证主轴有数据
|
||||
if (!hasBar && hasLine) {
|
||||
return buildLineChart(chart);
|
||||
}
|
||||
|
||||
CategoryAxis domainAxis = new CategoryAxis(chart.getXAxisName());
|
||||
domainAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||
NumberAxis leftAxis = new NumberAxis(chart.getYAxisName());
|
||||
leftAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||
NumberAxis rightAxis = new NumberAxis(chart.getYAxisNameRight());
|
||||
rightAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||
|
||||
BarRenderer barRenderer = new BarRenderer();
|
||||
barRenderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||
barRenderer.setDefaultItemLabelGenerator(
|
||||
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||
barRenderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||
|
||||
LineAndShapeRenderer lineRenderer = new LineAndShapeRenderer();
|
||||
lineRenderer.setDefaultShapesVisible(true);
|
||||
lineRenderer.setDefaultShapesFilled(true);
|
||||
lineRenderer.setDefaultStroke(new BasicStroke(2.0f));
|
||||
lineRenderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||
lineRenderer.setDefaultItemLabelGenerator(
|
||||
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||
lineRenderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||
|
||||
CategoryPlot plot = new CategoryPlot(barDataset, domainAxis, leftAxis, barRenderer);
|
||||
plot.setDataset(1, lineDataset);
|
||||
plot.setRenderer(1, lineRenderer);
|
||||
plot.setRangeAxis(1, rightAxis);
|
||||
plot.mapDatasetToRangeAxis(0, 0);
|
||||
plot.mapDatasetToRangeAxis(1, 1);
|
||||
plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
|
||||
plot.setBackgroundPaint(Color.WHITE);
|
||||
plot.setDomainGridlinesVisible(true);
|
||||
plot.setRangeGridlinesVisible(true);
|
||||
plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
|
||||
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
|
||||
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
|
||||
|
||||
return new JFreeChart(chart.getTitle(), JFreeChart.DEFAULT_TITLE_FONT, plot, true);
|
||||
}
|
||||
|
||||
private static DefaultCategoryDataset buildDataset(WordChartVo chart) {
|
||||
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
|
||||
List<String> categories = chart.getCategories();
|
||||
if (categories == null || categories.isEmpty()) {
|
||||
return dataset;
|
||||
}
|
||||
for (WordSeriesVo series : chart.getSeries()) {
|
||||
if (series == null || series.getName() == null || series.getValues() == null) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
Double value = i < series.getValues().size() ? series.getValues().get(i) : null;
|
||||
dataset.addValue(value == null ? 0.0 : value, series.getName(), categories.get(i));
|
||||
}
|
||||
}
|
||||
return dataset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据按列(分类)归一化为百分比,用于归一化堆叠柱状图
|
||||
*/
|
||||
private static DefaultCategoryDataset normalizePercent(DefaultCategoryDataset source) {
|
||||
DefaultCategoryDataset result = new DefaultCategoryDataset();
|
||||
int rowCount = source.getRowCount();
|
||||
int columnCount = source.getColumnCount();
|
||||
if (rowCount == 0 || columnCount == 0) {
|
||||
return result;
|
||||
}
|
||||
for (int col = 0; col < columnCount; col++) {
|
||||
double sum = 0.0;
|
||||
for (int row = 0; row < rowCount; row++) {
|
||||
Number value = source.getValue(row, col);
|
||||
sum += value == null ? 0.0 : value.doubleValue();
|
||||
}
|
||||
for (int row = 0; row < rowCount; row++) {
|
||||
Number value = source.getValue(row, col);
|
||||
double v = value == null ? 0.0 : value.doubleValue();
|
||||
double percent = sum == 0.0 ? 0.0 : v / sum * 100.0;
|
||||
result.addValue(percent, source.getRowKey(row), source.getColumnKey(col));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void stylePlot(CategoryPlot plot) {
|
||||
plot.setBackgroundPaint(Color.WHITE);
|
||||
plot.setDomainGridlinesVisible(true);
|
||||
plot.setRangeGridlinesVisible(true);
|
||||
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
|
||||
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
|
||||
plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
|
||||
CategoryAxis domainAxis = plot.getDomainAxis();
|
||||
domainAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||
NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
|
||||
rangeAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package com.yfd.platform.qgc_export.util;
|
||||
|
||||
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||
import com.yfd.platform.qgc_export.domain.word.WordChartVo;
|
||||
import org.apache.poi.util.Units;
|
||||
import org.apache.poi.xwpf.usermodel.Document;
|
||||
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Word 图表导出工具:将多个图表渲染为图片并按顺序写入 .docx 文档。
|
||||
*/
|
||||
public final class WordChartExportUtil {
|
||||
|
||||
/** 插入图片的目标宽度(单位:磅,1 磅 = 1/72 英寸) */
|
||||
private static final double IMAGE_WIDTH_PT = 460.0;
|
||||
|
||||
private WordChartExportUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Word 文档字节数组
|
||||
*
|
||||
* @param request 文档标题 + 图表列表
|
||||
* @return .docx 字节数组
|
||||
*/
|
||||
public static byte[] generateWord(WordChartExportRequest request) throws IOException {
|
||||
List<WordChartVo> charts = request == null ? List.of() : request.getCharts();
|
||||
try (XWPFDocument document = new XWPFDocument();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
|
||||
appendTitle(document, request);
|
||||
for (WordChartVo chart : charts) {
|
||||
appendChart(document, chart);
|
||||
}
|
||||
document.write(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendTitle(XWPFDocument document, WordChartExportRequest request) {
|
||||
String title = request == null ? null : request.getTitle();
|
||||
if (title != null && !title.isBlank()) {
|
||||
XWPFParagraph paragraph = document.createParagraph();
|
||||
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setText(title);
|
||||
run.setBold(true);
|
||||
run.setFontSize(22);
|
||||
run.setFontFamily("SimSun");
|
||||
}
|
||||
String subtitle = request == null ? null : request.getSubtitle();
|
||||
if (subtitle != null && !subtitle.isBlank()) {
|
||||
XWPFParagraph paragraph = document.createParagraph();
|
||||
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setText(subtitle);
|
||||
run.setFontSize(12);
|
||||
run.setColor("808080");
|
||||
run.setFontFamily("SimSun");
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendChart(XWPFDocument document, WordChartVo chart) throws IOException {
|
||||
BufferedImage image = ChartRenderUtil.render(chart);
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
byte[] png = toPngBytes(image);
|
||||
double imageHeightPt = IMAGE_WIDTH_PT * image.getHeight() / image.getWidth();
|
||||
|
||||
XWPFParagraph paragraph = document.createParagraph();
|
||||
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||
XWPFRun run = paragraph.createRun();
|
||||
try (ByteArrayInputStream in = new ByteArrayInputStream(png)) {
|
||||
try {
|
||||
run.addPicture(in, Document.PICTURE_TYPE_PNG, "chart.png",
|
||||
Units.toEMU(IMAGE_WIDTH_PT), Units.toEMU(imageHeightPt));
|
||||
} catch (org.apache.poi.openxml4j.exceptions.InvalidFormatException e) {
|
||||
throw new IOException("插入图片失败", e);
|
||||
}
|
||||
}
|
||||
// 每个图表后空一行,避免图片粘连
|
||||
XWPFParagraph spacer = document.createParagraph();
|
||||
spacer.setSpacingAfter(120);
|
||||
}
|
||||
|
||||
private static byte[] toPngBytes(BufferedImage image) throws IOException {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(image, "png", out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user