diff --git a/README.md b/README.md index 94fd6cb..19c3d9e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,11 @@ - 返回 PNG 图片,支持自定义分辨率和像素倍率 ### 📥 导出功能 -- Web UI 导出 PNG(2倍分辨率) +- 导出设置:分辨率预设(原始/1920×1080/1280×720/1024×768/800×600/自定义) +- 自定义分辨率手动输入,页面实时居中预览保存后的图 +- 分辨率历史记忆(最近5个,localStorage 持久化,一键复用) +- 文件名默认按日期时间自动命名(每次不同),支持手动命名 +- Web UI 导出 PNG / 图表模式支持 SVG - API 直接返回 PNG 图片流 ## 🚀 快速开始 diff --git a/app.js b/app.js index fe05cd3..24f41b5 100644 --- a/app.js +++ b/app.js @@ -60,13 +60,16 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('dataInput').value = sampleDataSets[0].data; generateChart(); - // 预填双图合并示例 + // 预填双图合并示例(不提前生成,切到双图模式时由 switchMode 触发) document.getElementById('combineData1').value = sampleDataSets[0].data; document.getElementById('combineData2').value = sampleDataSets[1].data; document.getElementById('combineTitle1').value = '季度销售对比'; document.getElementById('combineType2').value = 'line'; document.getElementById('combineTitle2').value = '年度增长趋势'; - generateCombine(); + + // 初始化导出设置 + renderHistory(); + refreshExportHint(); }); // ===== 数据解析 ===== @@ -135,6 +138,8 @@ function generateChart() { // 初始化图表 initChart(); updateChart(); + + refreshPreview(); } // ===== 初始化图表实例 ===== @@ -396,6 +401,8 @@ function updateChart() { // 绘制分割线(在 category 间隙中) drawSplitLine(); + + refreshPreview(); } // ===== 在柱子间隙中精确绘制分割线 ===== @@ -593,37 +600,206 @@ function clearData() { document.getElementById('seriesConfig').innerHTML = '
生成图表后可在此调整各系列的顺序和颜色
'; } -// ===== 导出图表 ===== -function exportChart(format) { +// ===== 导出图表(SVG 专用) ===== +function exportChartSvg() { if (!chartInstance) { alert('请先生成图表'); return; } + const svgChart = echarts.init(document.createElement('div'), null, { renderer: 'svg' }); + svgChart.setOption(chartInstance.getOption()); + const url = svgChart.getDataURL({ type: 'svg', pixelRatio: 2 }); + const a = document.createElement('a'); + a.href = url; + a.download = buildExportFilename('chart', 'svg'); + a.click(); + svgChart.dispose(); +} - if (format === 'png') { - const url = chartInstance.getDataURL({ - type: 'png', - pixelRatio: 2, - backgroundColor: document.getElementById('themeStyle').value === 'dark' ? '#1a1a2e' : '#fff' - }); - const a = document.createElement('a'); - a.href = url; - a.download = 'chart.png'; - a.click(); - } else if (format === 'svg') { - // 重新用 SVG 渲染 - const svgChart = echarts.init(document.createElement('div'), null, { renderer: 'svg' }); - svgChart.setOption(chartInstance.getOption()); - const url = svgChart.getDataURL({ - type: 'svg', - pixelRatio: 2 - }); - const a = document.createElement('a'); - a.href = url; - a.download = 'chart.svg'; - a.click(); - svgChart.dispose(); +// ===== 导出设置(分辨率/历史/命名) ===== +const EXPORT_PRESETS = { + 'original': { label: '原始大小', w: 0, h: 0 }, + '1920x1080': { label: '1920 × 1080', w: 1920, h: 1080 }, + '1280x720': { label: '1280 × 720', w: 1280, h: 720 }, + '1024x768': { label: '1024 × 768', w: 1024, h: 768 }, + '800x600': { label: '800 × 600', w: 800, h: 600 }, + 'custom': { label: '自定义', w: null, h: null } +}; +const HISTORY_KEY = 'dct_export_history'; +const HISTORY_MAX = 5; + +// 当前生效的分辨率 {w, h}(0 表示原始大小) +function getResolution() { + const preset = document.getElementById('exportPreset').value; + if (preset === 'original') return { w: 0, h: 0 }; + if (preset === 'custom') { + const w = parseInt(document.getElementById('customWidth').value) || 0; + const h = parseInt(document.getElementById('customHeight').value) || 0; + return { w, h }; } + const p = EXPORT_PRESETS[preset]; + return { w: p.w, h: p.h }; +} + +function onExportPresetChange() { + const preset = document.getElementById('exportPreset').value; + document.getElementById('customResGroup').style.display = preset === 'custom' ? 'block' : 'none'; + refreshExportHint(); + refreshPreview(); +} + +function onCustomResInput() { + refreshExportHint(); + refreshPreview(); + const { w, h } = getResolution(); + if (w > 0 && h > 0) saveHistory(w, h); +} + +// 历史分辨率管理(最多 5 个,存 localStorage) +function loadHistory() { + try { return JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; } catch (e) { return []; } +} + +function saveHistory(w, h) { + if (!w || !h) return; + let hist = loadHistory(); + const key = w + 'x' + h; + hist = hist.filter(x => x.key !== key); + hist.unshift({ key, w, h }); + hist = hist.slice(0, HISTORY_MAX); + localStorage.setItem(HISTORY_KEY, JSON.stringify(hist)); + renderHistory(); +} + +function renderHistory() { + const sel = document.getElementById('exportHistory'); + const hist = loadHistory(); + const cur = sel.value; + sel.innerHTML = hist.length + ? hist.map(h => ``).join('') + : ''; + if (cur && hist.some(h => h.key === cur)) sel.value = cur; +} + +function onHistorySelect() { + const v = document.getElementById('exportHistory').value; + if (!v) return; + const [w, h] = v.split('x').map(Number); + const preset = Object.entries(EXPORT_PRESETS).find(([k, p]) => p.w === w && p.h === h); + document.getElementById('exportPreset').value = preset ? preset[0] : 'custom'; + if (preset) { + document.getElementById('customResGroup').style.display = 'none'; + } else { + document.getElementById('customResGroup').style.display = 'block'; + document.getElementById('customWidth').value = w; + document.getElementById('customHeight').value = h; + } + refreshExportHint(); + refreshPreview(); +} + +// 文件名:手动填写优先,否则按日期时间自动命名(每次不同) +function buildExportFilename(prefix, ext) { + ext = ext || 'png'; + const manual = document.getElementById('exportFilename').value.trim(); + if (manual) { + return /\.[a-zA-Z0-9]+$/.test(manual) ? manual : manual + '.' + ext; + } + const d = new Date(); + const pad = n => String(n).padStart(2, '0'); + return `${prefix}_${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}.${ext}`; +} + +// 把源图 contain 居中缩放到目标分辨率画布(自动适配深色背景) +function applyResolution(srcCanvas, tw, th) { + if (!tw || !th) return srcCanvas; + let bg = '#ffffff'; + try { + const d = srcCanvas.getContext('2d').getImageData(0, 0, 1, 1).data; + if (d[3] > 0 && (d[0] + d[1] + d[2]) / 3 < 100) bg = '#1a1a2e'; + } catch (e) {} + const c = document.createElement('canvas'); + c.width = tw; c.height = th; + const ctx = c.getContext('2d'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, tw, th); + const scale = Math.min(tw / srcCanvas.width, th / srcCanvas.height); + const dw = Math.round(srcCanvas.width * scale); + const dh = Math.round(srcCanvas.height * scale); + ctx.drawImage(srcCanvas, Math.round((tw - dw) / 2), Math.round((th - dh) / 2), dw, dh); + return c; +} + +// 获取当前模式源 canvas(异步) +function getSourceCanvas() { + return new Promise((resolve, reject) => { + if (currentMode === 'chart') { + if (!chartInstance) return reject(new Error('请先生成图表')); + const cvs = chartInstance.getDom().querySelector('canvas'); + if (!cvs) return reject(new Error('图表未渲染')); + resolve(cvs); + } else if (currentMode === 'table') { + if (!tableImageBlob) return reject(new Error('请先生成表格')); + const url = URL.createObjectURL(tableImageBlob); + const img = new Image(); + img.onload = () => { + const c = document.createElement('canvas'); + c.width = img.width; c.height = img.height; + c.getContext('2d').drawImage(img, 0, 0); + URL.revokeObjectURL(url); + resolve(c); + }; + img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('表格图片加载失败')); }; + img.src = url; + } else { + if (!combinedCanvas) return reject(new Error('请先生成合并图')); + resolve(combinedCanvas); + } + }); +} + +// 生成最终导出 canvas(应用当前分辨率) +function getExportCanvas() { + return getSourceCanvas().then(src => { + const { w, h } = getResolution(); + return applyResolution(src, w, h); + }); +} + +// 统一下载图片 +function exportCurrent(prefix) { + getExportCanvas().then(canvas => { + const a = document.createElement('a'); + a.href = canvas.toDataURL('image/png'); + a.download = buildExportFilename(prefix); + a.click(); + const { w, h } = getResolution(); + if (w > 0 && h > 0) saveHistory(w, h); + }).catch(err => alert(err.message)); +} + +// 导出分辨率提示 +function refreshExportHint() { + const { w, h } = getResolution(); + const hint = document.getElementById('exportResHint'); + if (!hint) return; + hint.textContent = w > 0 && h > 0 ? `导出分辨率:${w} × ${h}(原图居中缩放)` : '导出分辨率:原始大小'; +} + +// 刷新导出预览:以原图为中心,居中展示保存后的图 +function refreshPreview() { + const box = document.getElementById('exportPreviewBox'); + const inner = document.getElementById('exportPreviewInner'); + const title = document.getElementById('exportPreviewTitle'); + if (!box || !inner) return; + getExportCanvas().then(canvas => { + const { w, h } = getResolution(); + const url = canvas.toDataURL('image/png'); + const sizeTag = w > 0 && h > 0 ? ` ${w} × ${h}` : ''; + title.innerHTML = `📐 导出预览${sizeTag}`; + inner.innerHTML = `❌
${err.message}
❌
${err.message}