feat: 新增收藏功能 - 图/表/合并图一键收藏,按原始大小保存到收藏区,可查看原图/下载/删除,点击编辑在新标签页加载配置再次制作
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user