feat: 增加图片合并功能
This commit is contained in:
+121
@@ -1,17 +1,25 @@
|
||||
import { loadCSV } from './csv.js';
|
||||
import { getChartBuilder } from './charts/index.js';
|
||||
import { downloadCanvas, mergeImages } from './merge.js';
|
||||
import {
|
||||
createAppShell,
|
||||
renderAnchors,
|
||||
renderCards,
|
||||
renderCartBar,
|
||||
renderCompanies,
|
||||
renderTabs,
|
||||
showChartError,
|
||||
showMergePreview,
|
||||
} from './ui.js';
|
||||
|
||||
const instances = new Map();
|
||||
const activeViews = new Map();
|
||||
const chartRenderVersions = new Map();
|
||||
// 合并队列:instanceId → { instanceId, title, filename, dataUrl }
|
||||
// dataUrl 是加购(及后续每次重渲染)时的快照,切换 tab 后实例被 dispose 也能照常导出
|
||||
const cart = new Map();
|
||||
let cartMode = 'pairs';
|
||||
let closePreview;
|
||||
let manifest;
|
||||
let shell;
|
||||
let state;
|
||||
@@ -123,6 +131,7 @@ async function renderChart(chart, version, chartVersion) {
|
||||
instances.set(chart.instanceId, instance);
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
instance.setOption(getChartBuilder(chart.key)(rows, { width, height, view }), true);
|
||||
captureSnapshot(chart);
|
||||
} catch (error) {
|
||||
if (version === renderVersion) showChartError(chart.instanceId, error.message);
|
||||
console.error(`渲染图表失败:${chart.id}`, error);
|
||||
@@ -157,6 +166,116 @@ function exportPNG(chartId, filename) {
|
||||
anchor.click();
|
||||
}
|
||||
|
||||
/** 图表在合并队列中时,每次重渲染(切换视图、重载数据、resize)都刷新快照,保证所见即所得 */
|
||||
function captureSnapshot(chart) {
|
||||
if (!cart.has(chart.instanceId)) return;
|
||||
const instance = instances.get(chart.instanceId);
|
||||
if (!instance) return;
|
||||
cart.get(chart.instanceId).dataUrl = instance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' });
|
||||
}
|
||||
|
||||
function toggleCart(chart) {
|
||||
if (cart.delete(chart.instanceId)) {
|
||||
updateCartButton(chart.instanceId);
|
||||
} else {
|
||||
const instance = instances.get(chart.instanceId);
|
||||
if (!instance) return;
|
||||
cart.set(chart.instanceId, {
|
||||
instanceId: chart.instanceId,
|
||||
title: chart.title,
|
||||
filename: chart.filename || chart.title,
|
||||
dataUrl: instance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' }),
|
||||
});
|
||||
updateCartButton(chart.instanceId);
|
||||
}
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function updateCartButton(instanceId) {
|
||||
const btn = document.querySelector(`[data-cart-id="${instanceId}"]`);
|
||||
if (!btn) return;
|
||||
const inCart = cart.has(instanceId);
|
||||
btn.dataset.inCart = String(inCart);
|
||||
btn.textContent = inCart ? '已加入 ✓' : '➕ 加入合并';
|
||||
}
|
||||
|
||||
/** Map 保持插入序,删除后按新顺序重建即完成上移/下移 */
|
||||
function moveCartItem(instanceId, delta) {
|
||||
const entries = [...cart.entries()];
|
||||
const from = entries.findIndex(([id]) => id === instanceId);
|
||||
const to = from + delta;
|
||||
if (from < 0 || to < 0 || to >= entries.length) return;
|
||||
[entries[from], entries[to]] = [entries[to], entries[from]];
|
||||
cart.clear();
|
||||
entries.forEach(([id, item]) => cart.set(id, item));
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function clipName(name, max = 24) {
|
||||
return name.length > max ? `${name.slice(0, max)}…` : name;
|
||||
}
|
||||
|
||||
/** 按当前模式把队列合成为一组 { canvas, filename, partial? }:pairs 两两合并,single 全部拼一张 */
|
||||
async function buildCompositions() {
|
||||
const items = [...cart.values()];
|
||||
if (!items.length) return [];
|
||||
if (cartMode === 'single') {
|
||||
return [{
|
||||
canvas: await mergeImages(items.map((item) => item.dataUrl)),
|
||||
filename: `${clipName(items[0].filename)}等${items.length}张合并`,
|
||||
}];
|
||||
}
|
||||
const pairs = [];
|
||||
for (let i = 0; i < items.length; i += 2) {
|
||||
const pair = items.slice(i, i + 2);
|
||||
pairs.push({
|
||||
canvas: await mergeImages(pair.map((item) => item.dataUrl)),
|
||||
filename: pair.map((item) => clipName(item.filename)).join('+'),
|
||||
partial: pair.length === 1,
|
||||
});
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
async function openMergePreview() {
|
||||
const entries = await buildCompositions();
|
||||
if (!entries.length) return;
|
||||
const notice = entries.some((entry) => entry.partial) ? '队列为奇数张,最后一张单独导出' : '';
|
||||
const download = (entry) => downloadCanvas(entry.canvas, entry.filename);
|
||||
closePreview = showMergePreview(entries, {
|
||||
notice,
|
||||
onDownload: download,
|
||||
onDownloadAll: () => entries.forEach((entry, index) => {
|
||||
setTimeout(() => download(entry), index * 350);
|
||||
}),
|
||||
onClose: () => closePreview?.(),
|
||||
});
|
||||
}
|
||||
|
||||
function renderCart() {
|
||||
renderCartBar(shell.cartBar, {
|
||||
items: [...cart.values()],
|
||||
mode: cartMode,
|
||||
onModeChange: (mode) => {
|
||||
cartMode = mode;
|
||||
renderCart();
|
||||
},
|
||||
onMove: moveCartItem,
|
||||
onRemove: (instanceId) => {
|
||||
cart.delete(instanceId);
|
||||
updateCartButton(instanceId);
|
||||
renderCart();
|
||||
},
|
||||
onClear: () => {
|
||||
const ids = [...cart.keys()];
|
||||
cart.clear();
|
||||
ids.forEach(updateCartButton);
|
||||
renderCart();
|
||||
},
|
||||
onPreview: openMergePreview,
|
||||
});
|
||||
}
|
||||
|
||||
function updateHash(nextState) {
|
||||
const hash = serializeHash(nextState);
|
||||
if (window.location.hash === hash) {
|
||||
@@ -189,6 +308,8 @@ function renderCurrent() {
|
||||
});
|
||||
renderCards(shell.cards, charts, activeViews, {
|
||||
onExport: exportPNG,
|
||||
isCarted: (instanceId) => cart.has(instanceId),
|
||||
onToggleCart: toggleCart,
|
||||
onSelectView: (chart, view) => {
|
||||
if (activeViews.get(chart.viewKey) === view) return;
|
||||
activeViews.set(chart.viewKey, view);
|
||||
|
||||
Reference in New Issue
Block a user