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); root.append(layout, reloadButton); return { companyNav, tabs, cards, chartNav }; } 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, 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 = { annual: '年度', halfyear: '半年', quarter: '季度' }; viewNames.forEach((view) => { switcher.append(button(labels[view] || view, { active: view === activeView, onClick: () => onSelectView(chart, view), className: 'view-button', })); }); bar.append(switcher); } bar.append(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)); }