function element(tag, className, text) { const el = document.createElement(tag); if (className) el.className = className; if (text !== undefined) el.textContent = text; return el; } function button(label, { active = false, onClick, className = '' } = {}) { const el = element('button', className, label); el.type = 'button'; el.dataset.active = String(active); if (onClick) el.addEventListener('click', onClick); return el; } export function createAppShell(root, { onReload }) { root.replaceChildren(); const reloadButton = button('↻', { onClick: onReload, className: 'reload-button' }); reloadButton.title = '重新加载数据'; reloadButton.setAttribute('aria-label', '重新加载数据'); const layout = element('div', 'app-layout'); const companyNav = element('aside', 'sidebar company-sidebar'); const content = element('main', 'content'); const tabs = element('nav', 'tabs'); const cards = element('div', 'cards'); content.append(tabs, cards); const chartNav = element('aside', 'sidebar chart-sidebar'); layout.append(companyNav, content, chartNav); const cartBar = element('div', 'cart-bar'); cartBar.hidden = true; root.append(layout, reloadButton, cartBar); return { companyNav, tabs, cards, chartNav, cartBar }; } export function renderCompanies(container, companies, activeCompanyId, onSelect) { container.replaceChildren(element('h2', 'sidebar-title', '公司')); const list = element('div', 'company-list'); companies.forEach((company) => { list.append(button(company.name, { active: company.id === activeCompanyId, onClick: () => onSelect(company.id), className: 'company-button', })); }); container.append(list); } export function renderTabs(container, tabs, activeTabId, onSelect) { container.replaceChildren(); tabs.forEach((tab) => { container.append(button(tab.name, { active: tab.id === activeTabId, onClick: () => onSelect(tab.id), className: 'tab-button', })); }); } export function renderCards(container, charts, activeViews, { onExport, isCarted, onToggleCart, onSelectView }) { container.replaceChildren(); if (!charts.length) { container.append(element('p', 'empty-state', '该分类暂时没有图表。')); return; } charts.forEach((chart) => { const card = element('section', 'card'); card.id = `card-${chart.instanceId}`; const bar = element('div', 'card-bar'); const heading = element('div', 'card-heading'); heading.append(element('h2', 'card-title', chart.title)); const viewNames = Object.keys(chart.views || {}); const activeView = activeViews.get(chart.viewKey) || chart.defaultView || viewNames[0]; const source = chart.views ? chart.views[activeView] : chart.csv; heading.append(element('span', 'src', source)); bar.append(heading); if (viewNames.length > 1) { const switcher = element('div', 'view-switcher'); const labels = { all: '全部', annual: '年度', halfyear: '半年', quarter: '季度' }; viewNames.forEach((view) => { const viewButton = button(labels[view] || view, { active: view === activeView, onClick: () => onSelectView(chart, view), className: 'view-button', }); viewButton.dataset.view = view; switcher.append(viewButton); }); bar.append(switcher); } const cartButton = button(isCarted?.(chart.instanceId) ? '已加入 ✓' : '➕ 加入合并', { onClick: () => onToggleCart?.(chart), className: 'cart-button', }); cartButton.dataset.cartId = chart.instanceId; cartButton.dataset.inCart = String(Boolean(isCarted?.(chart.instanceId))); bar.append(cartButton, button('导出 PNG', { onClick: () => onExport(chart.instanceId, chart.filename || chart.title), className: 'export-button', })); const chartEl = element('div', 'chart'); chartEl.id = `chart-${chart.instanceId}`; chartEl.style.height = `${chart.height || 680}px`; card.append(bar, chartEl); container.append(card); }); } export function renderAnchors(container, charts, onSelect) { container.replaceChildren(element('h2', 'sidebar-title', '当前图表')); const list = element('div', 'anchor-list'); charts.forEach((chart) => { container.append(button(chart.title, { onClick: () => onSelect(chart.id), className: 'anchor-button', })); }); if (!charts.length) container.append(element('p', 'sidebar-empty', '暂无图表')); } export function showChartError(chartId, message) { const chartEl = document.getElementById(`chart-${chartId}`); if (!chartEl) return; chartEl.replaceChildren(element('div', 'chart-error', message)); } const CART_MODES = [ ['pairs', '两两合并'], ['single', '合并为一张'], ]; export function renderCartBar(container, { items = [], mode, onModeChange, onMove, onRemove, onClear, onPreview }) { container.replaceChildren(); if (!items.length) { container.hidden = true; return; } container.hidden = false; container.append(element('span', 'cart-label', `合并队列 ${items.length}`)); const chips = element('div', 'cart-chips'); items.forEach((item, index) => { const chip = element('div', 'cart-chip'); chip.title = item.title; const up = button('↑', { onClick: () => onMove?.(item.instanceId, -1), className: 'cart-chip-button' }); up.disabled = index === 0; const down = button('↓', { onClick: () => onMove?.(item.instanceId, 1), className: 'cart-chip-button' }); down.disabled = index === items.length - 1; chip.append( element('span', 'cart-chip-name', item.title), up, down, button('✕', { onClick: () => onRemove?.(item.instanceId), className: 'cart-chip-button' }), ); chips.append(chip); }); container.append(chips); container.append(element('span', 'cart-divider')); const modes = element('div', 'cart-modes'); CART_MODES.forEach(([value, label]) => { modes.append(button(label, { active: mode === value, onClick: () => onModeChange?.(value), className: 'cart-mode-button', })); }); container.append(modes); container.append(button('预览导出', { onClick: () => onPreview?.(), className: 'cart-export-button' })); container.append(button('清空', { onClick: () => onClear?.(), className: 'cart-clear-button' })); } export function showMergePreview(entries, { notice = '', onDownload, onDownloadAll, onClose }) { const overlay = element('div', 'modal-overlay'); const modal = element('div', 'modal'); const head = element('div', 'modal-head'); head.append(element('h2', 'modal-title', `合并预览 · ${entries.length} 张`)); if (notice) head.append(element('span', 'modal-notice', notice)); head.append(button('✕', { onClick: onClose, className: 'modal-close' })); const body = element('div', 'modal-body'); entries.forEach((entry) => { const item = element('div', 'preview-item'); entry.canvas.className = 'preview-canvas'; item.append(entry.canvas); item.append(button(`⬇ 下载:${entry.filename}.png`, { onClick: () => onDownload?.(entry), className: 'export-button', })); body.append(item); }); const foot = element('div', 'modal-foot'); foot.append(button('全部下载', { onClick: () => onDownloadAll?.(), className: 'primary-button' })); foot.append(button('取消', { onClick: onClose, className: 'export-button' })); modal.append(head, body, foot); overlay.append(modal); overlay.addEventListener('click', (event) => { if (event.target === overlay) onClose?.(); }); document.addEventListener('keydown', onKeydown); document.body.append(overlay); function onKeydown(event) { if (event.key === 'Escape') onClose?.(); } return () => { document.removeEventListener('keydown', onKeydown); overlay.remove(); }; }