整體架構流程
- 前端:ECharts 渲染圖表 →
getDataURL() 取得 Base64 PNG → 傳給後端。
- 後端:Base64 解碼存成圖片 → Apache POI 建立 Excel → 寫入標題、維度、數據、圖片 → 下載回前端。
前端核心程式碼(ExtJS + RequireJS)
1. 初始化 ECharts 模組路徑
initEChart: function() {
require.config({
paths: {
'echarts': 'js/com/bhtec/echart/echarts',
'echarts/chart/bar': 'js/com/bhtec/echart/echarts',
'echarts/chart/line': 'js/com/bhtec/echart/echarts',
'echarts/chart/pie': 'js/com/bhtec/echart/echarts'
}
});
}
2. 載入圖表並取得 Base64
loadChart: function(data, item) {
var that = this;
require(['echarts', 'echarts/chart/bar', 'echarts/chart/line', 'echarts/chart/pie'], function(ec) {
that.body.setHeight(800);
var myChart = ec.init(that.body.dom);
myChart.showLoading({text: "圖表資料載入中..."});
var option = {
tooltip: {trigger: 'axis', axisPointer: {type: 'shadow'}},
legend: {data: data.indis, x: 'left', y: 'top'},
toolbox: {
show: true, orient: 'vertical', x: 'right', y: 'center',
feature: {
mark: {show: true},
dataView: {show: true, readOnly: true},
magicType: {show: true, type: ['line', 'bar', 'stack', 'tiled']},
restore: {show: true},
saveAsImage: {show: true}
}
},
calculable: true,
animation: false,
xAxis: [{type: 'category', data: data.grp}],
yAxis: [{type: 'value', splitArea: {show: true}}],
series: data.bar.series
};
myChart.hideLoading();
myChart.setOption(option);
that.imgURL = myChart.getDataURL('png'); // 取得 Base64
});
}
3. 匯出動作:傳圖片 + 維度 + 數據
doExport: function() {
var url = this.chartPanel.getImageURL(); // Base64
var title = Ext.fly('indi-display-title-id').first().dom.innerHTML;
var left = Ext.getCmp("indi_pivotGrid_id").leftAxis.getTuples();
var top = Ext.getCmp("indi_pivotGrid_id").topAxis.getTuples();
// 1. 先把圖片 Base64 送後端存檔
Ext.Ajax.request({
url: 'indicator/exp2excl.mvc',
params: {imgURL: url, left: getAxisStr(left)}
});
// 2. 再開新視窗下載 Excel(含標題、維度、數據)
var data = Ext.getCmp("indi_pivotGrid_id").extractData();
var arr = [];
for (var i = 0; i < data.length; i++) arr.push(data[i]);
window.open('indicator/exportList2Excel.mvc?title=' + encodeURIComponent(encodeURIComponent(title))
+ '&left=' + encodeURIComponent(encodeURIComponent(getAxisStr(left)))
+ '&top=' + encodeURIComponent(encodeURIComponent(getAxisStr(top)))
+ '&data=' + arr.join(';'));
function getAxisStr(tuples) {
var a = [];
for (var i = 0; i < tuples.length; i++) a.push(IndiFn.getAxisStr(tuples[i]));
return a.join(',');
}
}
後端 Java 核心邏輯
1. Base64 解碼存圖
public void base64ToPic(String fileName, HttpServletRequest req) {
if (imgsURL == null) return;
try {
String[] parts = imgsURL.split(",");
String base64 = parts.length > 1 ? parts[1] : parts[0]; // 去掉 data:image/png;base64,
byte[] buffer = new BASE64Decoder().decodeBuffer(base64);
File outFile = new File(req.getRealPath("pic/" + fileName + ".jpg"));
try (OutputStream out = new FileOutputStream(outFile)) {
out.write(buffer);
}
} catch (Exception e) {
log.error("Base64 解碼失敗", e);
}
}
2. POI 寫入 Excel 並嵌入圖片
// 建立標題列
Row row = sheet.createRow(index + 3);
Cell cell = row.createCell(0);
cell.setCellType(CellType.BLANK);
cell.setCellValue(title);
// 圖片起始列
row = sheet.createRow(index + 6);
Cell imgCell = row.createCell(0);
imgCell.setCellType(CellType.BLANK);
// 讀取圖片轉 ByteArrayOutputStream
ByteArrayOutputStream bos = new ByteArrayOutputStream();
BufferedImage img = ImageIO.read(new File(req.getRealPath("pic/" + fileName + ".jpg")));
ImageIO.write(img, "PNG", bos);
// 寫入 Excel 圖片
Drawing patriarch = sheet.createDrawingPatriarch();
ClientAnchor anchor = new HSSFClientAnchor(5, 5, 5, 5, (short) 1, index + 6, (short) 6, 45);
patriarch.createPicture(anchor, workbook.addPicture(bos.toByteArray(), Workbook.PICTURE_TYPE_PNG));
// 輸出
try (OutputStream out = response.getOutputStream()) {
workbook.write(out);
}
關鍵技術點總結
| 環節 | 關鍵 API | 注意事項 |
| 前端取圖 | chart.getDataURL('png') | 回傳 Base64 Data URI,需去前綴 |
| 傳輸 | Ext.Ajax.request + window.open | 大量數據建議 POST 或分塊 |
| 後端解碼 | BASE64Decoder (Java 8 用 Base64.getDecoder()) | 處理 Data URI 前綴 |
| Excel 寫入 | Apache POI HSSF/XSSF | HSSF 僅支援 .xls,大數據請用 SXSSF |
| 圖片嵌入 | Drawing.createPicture(anchor, pictureIdx) | 錨點座標單位為 1/1024 字元寬度 |
現代化建議
- 前端改用
echarts.min.js + xlsx-style 或 exceljs 純前端生成 Excel,省去後端往復。
- 後端若保留,建議升級 POI 到 5.x,使用
SXSSFWorkbook 流式寫入避免 OOM。
- Base64 解碼請用標準
java.util.Base64,移除舊 sun.misc.BASE64Decoder。