feat: 增加宁德时代的图表

This commit is contained in:
R524809
2026-09-24 17:14:03 +08:00
parent 3a3d19d5dd
commit f646ee6718
15 changed files with 1459 additions and 240 deletions
+73 -2
View File
@@ -46,16 +46,23 @@ export function createAppShell(root, { onReload }) {
const layout = element('div', 'app-layout');
const companyNav = element('aside', 'sidebar company-sidebar');
const content = element('main', 'content');
// tab 栏与参数行共处一个 sticky 容器:参数行是独立的一行(不是 tab 栏的一部分),
// 但要和 tab 栏一起钉在顶部。若让两行各自 sticky,参数行就得设「tab 栏高度」的 top
// 偏移,而 tab 栏会换行、窄屏还会横向滚动,高度并不固定;绑在一个容器里天然不会错位。
const tabHeader = element('div', 'tab-header');
const tabs = element('nav', 'tabs');
const params = element('div', 'tab-controls');
params.hidden = true;
tabHeader.append(tabs, params);
const cards = element('div', 'cards');
content.append(tabs, cards);
content.append(tabHeader, 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 };
return { companyNav, tabs, params, cards, chartNav, cartBar };
}
export function renderCompanies(container, companies, activeCompanyId, onSelect) {
@@ -82,6 +89,70 @@ export function renderTabs(container, tabs, activeTabId, onSelect) {
});
}
/**
* tab 级数值参数控件(目前只有「现价」)—— 由 manifest 的 tab.controls 声明。
*
* 位置:页面里独立的一行,钉在 tab 栏正下方(见 createAppShell 的 .tab-header)。
* 不塞进 tab 栏:它是改数据口径的输入,混在导航按钮里容易被当成 tab 误点。
*
* 回调约定:onChange(id, rawValue) 返回 false 表示这次输入不合法(越界/非数字),
* 输入框标红示意,图表保持原样 —— 校验规则属于控制层,壳层不重复实现。
*/
export function renderTabControls(container, controls, values, onChange) {
// 容器是常驻的,每次整屏重渲染都要清空;否则 resize(200ms 防抖)会叠出多行控件
container.replaceChildren();
container.hidden = !controls?.length;
if (!controls?.length) return;
controls.forEach((control) => {
const field = element('label', 'param-field');
if (control.hint) field.title = control.hint;
field.append(element('span', 'param-label', control.label));
const input = document.createElement('input');
input.className = 'param-input';
input.type = 'number';
input.inputMode = 'decimal';
input.step = String(control.step ?? 1);
input.min = String(control.min);
input.max = String(control.max);
input.value = String(values?.[control.id] ?? control.value);
input.setAttribute('aria-label', `${control.label}${control.unit ?? ''}`);
field.append(input, element('span', 'param-unit', control.unit ?? ''));
// 合法区间常驻显示:输入越界时它变红,用户不必猜上下限
field.append(element('span', 'param-range', `${control.min}–${control.max}`));
const reset = button('重置', {
className: 'param-reset',
onClick: () => {
input.value = String(control.value);
input.dataset.invalid = 'false';
// 值已回到默认,按钮自己进入禁用态(不等下一次 input 事件,否则点完还亮着)
reset.disabled = true;
onChange?.(control.id, input.value, { immediate: true });
input.focus();
},
});
reset.title = `回到默认值 ${control.value}`;
const apply = (immediate = false) => {
const ok = onChange?.(control.id, input.value, { immediate }) !== false;
input.dataset.invalid = String(!ok);
reset.disabled = input.value === String(control.value);
};
input.addEventListener('input', () => apply());
// 回车立即生效,不等防抖(防抖只针对连续输入)
input.addEventListener('keydown', (event) => {
if (event.key !== 'Enter') return;
event.preventDefault();
apply(true);
});
reset.disabled = input.value === String(control.value);
container.append(field, reset);
});
}
export function renderCards(container, charts, activeViews, { onExport, isCarted, onToggleCart, onSelectView }) {
container.replaceChildren();
if (!charts.length) {