From 6b5223daecdd79913ee4b3dd25a29bbb2f8bf726 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Wed, 19 Aug 2026 13:39:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E5=9B=BE=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E8=A1=8C=E5=A4=9A=E5=88=97=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=E6=8E=92=E5=B8=83=20+=20=E7=BB=9F=E4=B8=80'=E5=A4=9A?= =?UTF-8?q?=E5=9B=BE=E5=90=88=E5=B9=B6'=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合并模式新增'多行多列'排布(radio),可选每行列数(gridCols), 子图按最大单元格 contain 居中排列,列数变化实时重排 - 后端 /api/combine 支持 direction=grid + cols 参数 - 页面所有'双图合并'文案统一改为'多图合并' --- API.md | 5 +-- README.md | 2 +- app.js | 80 ++++++++++++++++++++++++++++++--------------- index.html | 16 ++++++--- server.js | 95 +++++++++++++++++++++++++++++++++++------------------- 5 files changed, 130 insertions(+), 68 deletions(-) diff --git a/API.md b/API.md index e72dc86..986c7f7 100644 --- a/API.md +++ b/API.md @@ -159,7 +159,7 @@ curl "http://192.168.0.101:16016/api/chart?data=%E4%BA%A7%E5%93%81,Q1,Q2%0A%E6%8 ## 3. POST /api/combine(多图合并) -将**多张图表**合并到一张图片中(默认 2 张,可多张),支持**横排(左右并排)** 或 **竖排(上下堆叠)** 两种方向,返回 PNG 图片。 +将**多张图表**合并到一张图片中(默认 2 张,可多张),支持**横排(单行)**、**竖排(单列)**、**多行多列网格**三种排布,返回 PNG 图片。 ### 请求 @@ -173,7 +173,8 @@ Content-Type: application/json | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `charts` | array | ✅ | 图表配置数组(N 张),每项同 `/api/chart` 参数:data/chartType/title/theme 等 | -| `direction` | string | 否 | `horizontal` 横排(默认)/ `vertical` 竖排 | +| `direction` | string | 否 | `horizontal` 横排(默认)/ `vertical` 竖排 / `grid` 多行多列 | +| `cols` | number | 否 | 多行多列时的每行列数(默认 2,仅 `grid` 生效) | | `gap` | number | 否 | 图间距(默认 24px) | | `pixelRatio` | number | 否 | 像素倍率,默认 2(越清晰文件越大) | | `background` | string | 否 | 背景色,默认 `#ffffff` | diff --git a/README.md b/README.md index 2169f74..7c6c502 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ ### 🖼️ 多图合并 - 将多张图表合并到一张图片中,适合对比/汇总场景,**默认2张,可一键不断添加**(支持删除) -- 支持**横排(左右并排)** 和 **竖排(上下堆叠)**,一键切换实时预览 +- 支持**横排(单行)**、**竖排(单列)**、**多行多列网格**(可设每行列数),一键切换实时预览 - 每张图可独立配置:数据、标题、图表类型、主题、图例/网格/标签/堆叠 - 支持导出 PNG diff --git a/app.js b/app.js index 74204e4..ebe4717 100644 --- a/app.js +++ b/app.js @@ -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; diff --git a/index.html b/index.html index 995245f..68de493 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@
- +
@@ -272,18 +272,24 @@ C, 20, 30, 40 - +