feat: 新增收藏功能 - 图/表/合并图一键收藏,按原始大小保存到收藏区,可查看原图/下载/删除,点击编辑在新标签页加载配置再次制作

This commit is contained in:
2026-08-19 17:55:18 +08:00
parent 7a03ecc063
commit 606610f8d3
5 changed files with 704 additions and 2 deletions
+276
View File
@@ -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 = `<div class="fav-empty">加载失败:${escHtml(err.message)}</div>`;
});
}
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 = `<div class="fav-empty">🕊️ 暂无收藏,点击「⭐ 收藏」保存当前图表或表格</div>`;
return;
}
const MODE_LABEL = { chart: '📈 图表', table: '📋 表格', combine: '🖼️ 多图' };
list.innerHTML = favs.map(f => `
<div class="fav-card">
<div class="fav-card-img">
<img src="/api/favorites/${f.id}/image" alt="${escHtml(f.title)}" loading="lazy"
onclick="window.open('/api/favorites/${f.id}/image','_blank')" title="点击查看原图">
<span class="fav-badge">${MODE_LABEL[f.mode] || escHtml(f.mode)}</span>
</div>
<div class="fav-card-body">
<div class="fav-card-title" title="${escHtml(f.title)}">${escHtml(f.title)}</div>
<div class="fav-card-meta">${f.width || '?'} × ${f.height || '?'}px · ${formatFavTime(f.createdAt)}</div>
<div class="fav-card-actions">
<button class="btn btn-primary btn-sm" onclick="editFavorite('${f.id}')" title="在新标签页再次制作">✏️ 编辑</button>
<button class="btn btn-secondary btn-sm" onclick="downloadFavorite('${f.id}')" title="下载原图">⬇️ 下载</button>
<button class="btn btn-remove btn-sm" onclick="deleteFavorite('${f.id}')" title="删除">🗑️</button>
</div>
</div>
</div>
`).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);
}