diff --git a/.gitignore b/.gitignore
index 4e5ea97..be385e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules/
logs/
+data/
*.png
*.svg
diff --git a/app.js b/app.js
index b63fb2a..ec0f098 100644
--- a/app.js
+++ b/app.js
@@ -67,6 +67,13 @@ document.addEventListener('DOMContentLoaded', () => {
// 初始化导出设置
renderHistory();
refreshExportHint();
+
+ // 收藏:URL 带 id 时,在新标签页加载该收藏配置进行再次制作
+ const urlParams = new URLSearchParams(location.search);
+ const favId = urlParams.get('id');
+ if (favId) {
+ loadFavoriteForEdit(favId);
+ }
});
// ===== 数据解析 =====
@@ -1510,3 +1517,272 @@ function generateCombine() {
function exportCombine() {
exportCurrent('combine');
}
+
+// ===== 收藏功能 =====
+function collectChartConfig() {
+ return {
+ mode: 'chart',
+ data: document.getElementById('dataInput').value,
+ chartType: document.getElementById('chartType').value,
+ title: document.getElementById('chartTitle').value,
+ theme: document.getElementById('themeStyle').value,
+ showLegend: document.getElementById('showLegend').checked,
+ showGrid: document.getElementById('showGrid').checked,
+ showLabel: document.getElementById('showLabel').checked,
+ stackMode: document.getElementById('stackMode').checked,
+ smoothLine: document.getElementById('smoothLine').checked,
+ dualAxis: document.getElementById('dualAxis').checked,
+ leftAxisName: document.getElementById('leftAxisName').value,
+ rightAxisName: document.getElementById('rightAxisName').value,
+ enableSplit: document.getElementById('enableSplit').checked,
+ splitIndex: parseInt(document.getElementById('splitIndex').value) || 3,
+ leftLabel: document.getElementById('leftLabel').value,
+ rightLabel: document.getElementById('rightLabel').value,
+ splitStyle: document.getElementById('splitStyle').value,
+ seriesColors: [...seriesColors],
+ seriesOrder: [...seriesOrder],
+ seriesAxis: [...seriesAxis],
+ seriesTypeOverrides: window.seriesTypeOverrides || {}
+ };
+}
+
+function collectTableConfig() {
+ return {
+ mode: 'table',
+ data: document.getElementById('dataInput').value,
+ title: document.getElementById('tableTitle').value,
+ theme: document.getElementById('tableTheme').value,
+ fontSize: parseInt(document.getElementById('tableFontSize').value) || 14,
+ stripeRows: document.getElementById('stripeRows').checked,
+ borderStyle: document.getElementById('borderStyle').value
+ };
+}
+
+function collectCombineConfig() {
+ return {
+ mode: 'combine',
+ charts: combineCharts.map(c => ({ ...c })),
+ direction: (document.querySelector('input[name="combineDirection"]:checked') || {}).value || 'horizontal',
+ gridCols: parseInt(document.getElementById('gridCols').value) || 2
+ };
+}
+
+function collectConfig() {
+ if (currentMode === 'chart') return collectChartConfig();
+ if (currentMode === 'table') return collectTableConfig();
+ return collectCombineConfig();
+}
+
+function favoriteTitle(cfg) {
+ if (cfg.mode === 'chart') return cfg.title || '未命名图表';
+ if (cfg.mode === 'table') return cfg.title || '未命名表格';
+ return '多图合并';
+}
+
+// 点击「⭐ 收藏」:把当前图/表按原始大小保存到收藏区
+function hasFavoriteContent(cfg) {
+ if (cfg.mode === 'combine') {
+ return Array.isArray(cfg.charts) && cfg.charts.some(c => c.data && c.data.trim());
+ }
+ return !!(cfg.data && cfg.data.trim());
+}
+
+function saveFavorite() {
+ let cfg;
+ try { cfg = collectConfig(); } catch (e) { alert(e.message); return; }
+ if (!hasFavoriteContent(cfg)) { alert('没有可收藏的内容,请先生成图或表'); return; }
+
+ const btn = document.getElementById('btnFavorite');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ 收藏中...'; }
+
+ getSourceCanvas()
+ .then(canvas => {
+ const image = canvas.toDataURL('image/png');
+ const title = favoriteTitle(cfg);
+ return fetch('/api/favorites', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ mode: cfg.mode, title, config: cfg, image })
+ });
+ })
+ .then(res => res.json().then(data => ({ ok: res.ok, data })))
+ .then(({ ok, data }) => {
+ if (!ok) throw new Error((data && data.error) || '收藏失败');
+ showToast('⭐ 已收藏:' + (data.title || ''));
+ refreshFavorites();
+ })
+ .catch(err => alert(err.message))
+ .finally(() => {
+ if (btn) { btn.disabled = false; btn.textContent = '⭐ 收藏'; }
+ });
+}
+
+// ===== 收藏区(弹窗) =====
+function openFavorites() {
+ const modal = document.getElementById('favoritesModal');
+ if (modal) modal.style.display = 'flex';
+ refreshFavorites();
+}
+
+function closeFavorites() {
+ const modal = document.getElementById('favoritesModal');
+ if (modal) modal.style.display = 'none';
+}
+
+function refreshFavorites() {
+ fetch('/api/favorites')
+ .then(res => res.json())
+ .then(data => renderFavorites((data && data.favorites) || []))
+ .catch(err => {
+ const list = document.getElementById('favoritesList');
+ if (list) list.innerHTML = `
加载失败:${escHtml(err.message)}
`;
+ });
+}
+
+function renderFavorites(favs) {
+ const list = document.getElementById('favoritesList');
+ const count = document.getElementById('favCount');
+ if (count) count.textContent = String(favs.length);
+ if (!list) return;
+
+ if (!favs.length) {
+ list.innerHTML = `🕊️ 暂无收藏,点击「⭐ 收藏」保存当前图表或表格
`;
+ return;
+ }
+
+ const MODE_LABEL = { chart: '📈 图表', table: '📋 表格', combine: '🖼️ 多图' };
+ list.innerHTML = favs.map(f => `
+
+
+

+
${MODE_LABEL[f.mode] || escHtml(f.mode)}
+
+
+
${escHtml(f.title)}
+
${f.width || '?'} × ${f.height || '?'}px · ${formatFavTime(f.createdAt)}
+
+
+
+
+
+
+
+ `).join('');
+}
+
+function formatFavTime(iso) {
+ if (!iso) return '';
+ const d = new Date(iso);
+ const pad = n => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
+}
+
+// 编辑:新标签页打开主页面并加载该收藏配置
+function editFavorite(id) {
+ window.open('/?id=' + id, '_blank');
+}
+
+function downloadFavorite(id) {
+ const a = document.createElement('a');
+ a.href = '/api/favorites/' + id + '/image?download=1';
+ a.download = 'favorite_' + id + '.png';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+}
+
+function deleteFavorite(id) {
+ if (!confirm('确定删除这条收藏吗?')) return;
+ fetch('/api/favorites/' + id, { method: 'DELETE' })
+ .then(res => res.json())
+ .then(data => {
+ if (data && data.ok) { refreshFavorites(); showToast('🗑️ 已删除'); }
+ else alert((data && data.error) || '删除失败');
+ })
+ .catch(err => alert(err.message));
+}
+
+// 加载收藏配置(新标签页再次制作)
+function loadFavoriteForEdit(id) {
+ fetch('/api/favorites/' + id)
+ .then(res => res.json())
+ .then(data => {
+ if (!data || !data.ok || !data.favorite) throw new Error('收藏不存在或已删除');
+ applyFavoriteConfig(data.favorite);
+ showToast('✏️ 已载入收藏配置,可继续编辑制作');
+ })
+ .catch(err => showToast('❌ ' + err.message));
+}
+
+function applyFavoriteConfig(fav) {
+ const cfg = fav.config || {};
+ const mode = fav.mode || cfg.mode || 'chart';
+
+ switchMode(mode);
+
+ if (mode === 'chart') {
+ document.getElementById('dataInput').value = cfg.data || '';
+ document.getElementById('chartType').value = cfg.chartType || 'bar';
+ document.getElementById('chartTitle').value = cfg.title || '';
+ document.getElementById('themeStyle').value = cfg.theme || 'default';
+ document.getElementById('showLegend').checked = cfg.showLegend !== false;
+ document.getElementById('showGrid').checked = cfg.showGrid !== false;
+ document.getElementById('showLabel').checked = !!cfg.showLabel;
+ document.getElementById('stackMode').checked = !!cfg.stackMode;
+ document.getElementById('smoothLine').checked = cfg.smoothLine !== false;
+ document.getElementById('dualAxis').checked = !!cfg.dualAxis;
+ document.getElementById('leftAxisName').value = cfg.leftAxisName || '';
+ document.getElementById('rightAxisName').value = cfg.rightAxisName || '';
+ document.getElementById('enableSplit').checked = !!cfg.enableSplit;
+ document.getElementById('splitIndex').value = cfg.splitIndex || 3;
+ document.getElementById('leftLabel').value = cfg.leftLabel || '';
+ document.getElementById('rightLabel').value = cfg.rightLabel || '';
+ document.getElementById('splitStyle').value = cfg.splitStyle || 'solid';
+
+ generateChart();
+
+ // 覆盖系列顺序/颜色/轴/独立类型
+ if (Array.isArray(cfg.seriesOrder)) seriesOrder = [...cfg.seriesOrder];
+ if (Array.isArray(cfg.seriesColors)) seriesColors = [...cfg.seriesColors];
+ if (Array.isArray(cfg.seriesAxis)) seriesAxis = [...cfg.seriesAxis];
+ window.seriesTypeOverrides = cfg.seriesTypeOverrides || {};
+ renderSeriesConfig();
+ updateChart();
+ onDualAxisChange();
+ } else if (mode === 'table') {
+ document.getElementById('dataInput').value = cfg.data || '';
+ document.getElementById('tableTitle').value = cfg.title || '';
+ document.getElementById('tableTheme').value = cfg.theme || 'default';
+ document.getElementById('tableFontSize').value = cfg.fontSize || 14;
+ updateFontSize();
+ document.getElementById('stripeRows').checked = cfg.stripeRows !== false;
+ document.getElementById('borderStyle').value = cfg.borderStyle || 'all';
+ generateTable();
+ } else if (mode === 'combine') {
+ if (Array.isArray(cfg.charts) && cfg.charts.length) {
+ combineCharts = cfg.charts.map(c => ({ ...defaultCombineChart(), ...c }));
+ }
+ const dir = document.querySelector(`input[name="combineDirection"][value="${cfg.direction || 'horizontal'}"]`);
+ if (dir) dir.checked = true;
+ document.getElementById('gridCols').value = cfg.gridCols || 2;
+ renderCombineCharts();
+ generateCombine();
+ onCombineGridToggle();
+ }
+}
+
+// ===== Toast =====
+let toastTimer = null;
+function showToast(msg) {
+ let t = document.getElementById('appToast');
+ if (!t) {
+ t = document.createElement('div');
+ t.id = 'appToast';
+ document.body.appendChild(t);
+ }
+ t.textContent = msg;
+ t.classList.add('show');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(() => t.classList.remove('show'), 2200);
+}
diff --git a/index.html b/index.html
index 1ce9d96..01e1411 100644
--- a/index.html
+++ b/index.html
@@ -13,6 +13,9 @@
@@ -230,6 +233,7 @@ C, 20, 30, 40
▸
+
@@ -420,5 +424,17 @@ C, 20, 30, 40
+
+
+
+
+
+
⭐ 收藏区
+
+
+
共 0 条 · 点击图片看原图 · 「编辑」在新标签页再次制作
+
+
+