Files
smart-fin-chart/js/ui.js
T
2026-09-09 15:31:05 +08:00

224 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
};
}