feat: 多图合并支持多行多列网格排布 + 统一'多图合并'文案

- 合并模式新增'多行多列'排布(radio),可选每行列数(gridCols),
  子图按最大单元格 contain 居中排列,列数变化实时重排
- 后端 /api/combine 支持 direction=grid + cols 参数
- 页面所有'双图合并'文案统一改为'多图合并'
This commit is contained in:
2026-08-19 13:39:14 +08:00
parent e56c6cb79f
commit 6b5223daec
5 changed files with 130 additions and 68 deletions
+54 -26
View File
@@ -1016,7 +1016,7 @@ function quickSwitchTheme(theme) {
document.getElementById('tableTheme').value = theme;
updateTable();
}
// 图合并模式:仅切换按钮高亮,不联动(两图各自独立主题)
// 图合并模式:仅切换按钮高亮,不联动(各图独立主题)
}
// ===== 同步风格按钮状态(下拉框变更时) =====
@@ -1039,7 +1039,7 @@ document.addEventListener('DOMContentLoaded', () => {
initThemeButtons();
});
// ===== 图合并 =====
// ===== 图合并 =====
let combinedCanvas = null;
// 构建单个子图的 echarts option(不依赖全局状态,双图模式专用)
@@ -1245,6 +1245,13 @@ function removeCombineChart(i) {
generateCombine();
}
// 多行多列切换:显示/隐藏列数输入
function onCombineGridToggle() {
const grid = document.querySelector('input[name="combineDirection"]:checked').value === 'grid';
document.getElementById('gridColsGroup').style.display = grid ? 'block' : 'none';
generateCombine();
}
// 生成合并图(多图)
function generateCombine() {
const validCharts = combineCharts.filter(c => c.data && c.data.trim());
@@ -1284,34 +1291,55 @@ function generateCombine() {
const m = measureCombineChart(c.data);
return renderOne(c, m.w, m.h);
})).then(canvases => {
let W, H, scaled;
if (direction === 'vertical') {
// 竖排:等宽
const tw = Math.max(...canvases.map(c => c.width));
scaled = canvases.map(c => ({ c, w: tw, h: Math.round(c.height * tw / c.width) }));
W = tw;
H = scaled.reduce((s, x) => s + x.h, 0) + gap * (scaled.length - 1);
} else {
// 横排:等高
const th = Math.max(...canvases.map(c => c.height));
scaled = canvases.map(c => ({ c, w: Math.round(c.width * th / c.height), h: th }));
W = scaled.reduce((s, x) => s + x.w, 0) + gap * (scaled.length - 1);
H = th;
}
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, W, H);
if (direction === 'vertical') {
let y = 0;
scaled.forEach(x => { ctx.drawImage(x.c, 0, y, x.w, x.h); y += x.h + gap; });
if (direction === 'grid') {
// 多行多列网格:所有子图统一 contain 到最大单元格,按列填充
const cols = Math.max(1, parseInt(document.getElementById('gridCols').value) || 2);
const rows = Math.ceil(canvases.length / cols);
const cellW = Math.max(...canvases.map(c => c.width));
const cellH = Math.max(...canvases.map(c => c.height));
canvas.width = cols * cellW + (cols - 1) * gap;
canvas.height = rows * cellH + (rows - 1) * gap;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
canvases.forEach((c, i) => {
const r = Math.floor(i / cols), col = i % cols;
const scale = Math.min(cellW / c.width, cellH / c.height);
const dw = Math.round(c.width * scale), dh = Math.round(c.height * scale);
const x = col * (cellW + gap) + Math.round((cellW - dw) / 2);
const y = r * (cellH + gap) + Math.round((cellH - dh) / 2);
ctx.drawImage(c, x, y, dw, dh);
});
} else {
let x = 0;
scaled.forEach(s => { ctx.drawImage(s.c, x, 0, s.w, s.h); x += s.w + gap; });
let W, H, scaled;
if (direction === 'vertical') {
// 竖排:等宽
const tw = Math.max(...canvases.map(c => c.width));
scaled = canvases.map(c => ({ c, w: tw, h: Math.round(c.height * tw / c.width) }));
W = tw;
H = scaled.reduce((s, x) => s + x.h, 0) + gap * (scaled.length - 1);
} else {
// 横排:等高
const th = Math.max(...canvases.map(c => c.height));
scaled = canvases.map(c => ({ c, w: Math.round(c.width * th / c.height), h: th }));
W = scaled.reduce((s, x) => s + x.w, 0) + gap * (scaled.length - 1);
H = th;
}
canvas.width = W;
canvas.height = H;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, W, H);
if (direction === 'vertical') {
let y = 0;
scaled.forEach(x => { ctx.drawImage(x.c, 0, y, x.w, x.h); y += x.h + gap; });
} else {
let x = 0;
scaled.forEach(s => { ctx.drawImage(s.c, x, 0, s.w, s.h); x += s.w + gap; });
}
}
combinedCanvas = canvas;