224 lines
7.7 KiB
JavaScript
224 lines
7.7 KiB
JavaScript
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();
|
||
};
|
||
}
|