Refactor financial chart implementation

This commit is contained in:
R524809
2026-08-28 16:32:10 +08:00
parent 5ecc554aaa
commit 7aab3d7134
73 changed files with 2340 additions and 594 deletions
+45 -25
View File
@@ -22,21 +22,31 @@ function validateManifest(value) {
throw new Error('manifest 缺少 companies');
}
const chartIds = new Set();
value.companies.forEach((company) => {
if (!company.id || !Array.isArray(company.tabs)) throw new Error('manifest 中存在无效公司配置');
if (!company.id || !Array.isArray(company.tabs) || !company.chartCatalog) {
throw new Error('manifest 中存在无效公司配置');
}
Object.entries(company.chartCatalog).forEach(([chartId, chart]) => {
if (!chartId || !getChartBuilder(chart.key)) {
throw new Error(`图表 ${chartId || '(空)'} 引用了未注册 key:${chart.key}`);
}
if (Boolean(chart.csv) === Boolean(chart.views)) {
throw new Error(`图表 ${chartId} 必须声明 csv 或 views 之一`);
}
if (chart.views && (!chart.defaultView || !chart.views[chart.defaultView])) {
throw new Error(`图表 ${chartId} 的 defaultView 无效`);
}
});
const tabIds = new Set();
company.tabs.forEach((tab) => {
if (!tab.id || tabIds.has(tab.id)) throw new Error(`公司 ${company.id} 的 tab id 重复或为空`);
if (!Array.isArray(tab.chartRefs)) throw new Error(`公司 ${company.id} 的 ${tab.id} 缺少 chartRefs`);
tabIds.add(tab.id);
(tab.charts || []).forEach((chart) => {
if (!chart.id || chartIds.has(chart.id)) throw new Error(`图表 id 重复或为空:${chart.id || '(空)'}`);
if (!getChartBuilder(chart.key)) throw new Error(`图表 ${chart.id} 引用了未注册 key:${chart.key}`);
if (Boolean(chart.csv) === Boolean(chart.views)) throw new Error(`图表 ${chart.id} 必须声明 csv 或 views 之一`);
if (chart.views && (!chart.defaultView || !chart.views[chart.defaultView])) {
throw new Error(`图表 ${chart.id} 的 defaultView 无效`);
}
chartIds.add(chart.id);
const refs = new Set();
tab.chartRefs.forEach((chartId) => {
if (!company.chartCatalog[chartId]) throw new Error(`栏目 ${tab.id} 引用了不存在的图表:${chartId}`);
if (refs.has(chartId)) throw new Error(`栏目 ${tab.id} 重复引用图表:${chartId}`);
refs.add(chartId);
});
});
});
@@ -50,6 +60,15 @@ function getTab(company, tabId) {
return company.tabs.find((tab) => tab.id === tabId) || company.tabs[0];
}
function chartsForTab(company, tab) {
return tab.chartRefs.map((chartId) => ({
...company.chartCatalog[chartId],
id: chartId,
instanceId: `${company.id}--${tab.id}--${chartId}`,
viewKey: `${company.id}--${chartId}`,
}));
}
function parseHash() {
const [path = '', query = ''] = window.location.hash.replace(/^#\/?/, '').split('?');
const [companyId, tabId, chartId] = path.split('/').filter(Boolean).map(decodeURIComponent);
@@ -60,11 +79,11 @@ function parseHash() {
function normalizeState(nextState) {
const company = getCompany(nextState.companyId);
const tab = getTab(company, nextState.tabId);
const chart = tab.charts.find((item) => item.id === nextState.chartId);
const chart = chartsForTab(company, tab).find((item) => item.id === nextState.chartId);
const view = chart?.views && chart.views[nextState.view]
? nextState.view
: chart?.defaultView;
if (chart?.views && view) activeViews.set(chart.id, view);
if (chart?.views && view) activeViews.set(chart.viewKey, view);
return { companyId: company.id, tabId: tab.id, chartId: chart?.id, view };
}
@@ -83,12 +102,12 @@ function disposeAll() {
function chartSource(chart) {
if (!chart.views) return chart.csv;
const view = activeViews.get(chart.id) || chart.defaultView;
const view = activeViews.get(chart.viewKey) || chart.defaultView;
return chart.views[view];
}
async function renderChart(chart, version) {
const el = document.getElementById(`chart-${chart.id}`);
const el = document.getElementById(`chart-${chart.instanceId}`);
if (!el) return;
const source = chartSource(chart);
@@ -97,11 +116,11 @@ async function renderChart(chart, version) {
if (version !== renderVersion || !document.body.contains(el)) return;
const instance = echarts.init(el, null, { renderer: 'canvas' });
instances.set(chart.id, instance);
instances.set(chart.instanceId, instance);
const { width, height } = el.getBoundingClientRect();
instance.setOption(getChartBuilder(chart.key)(rows, { width, height }), true);
} catch (error) {
if (version === renderVersion) showChartError(chart.id, error.message);
if (version === renderVersion) showChartError(chart.instanceId, error.message);
console.error(`渲染图表失败:${chart.id}`, error);
}
}
@@ -125,16 +144,17 @@ function updateHash(nextState) {
window.location.hash = hash;
}
function selectAnchor(chartId) {
state = { ...state, chartId, view: activeViews.get(chartId) };
function selectAnchor(chart) {
state = { ...state, chartId: chart.id, view: activeViews.get(chart.viewKey) };
history.replaceState(null, '', serializeHash(state));
document.getElementById(`card-${chartId}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
document.getElementById(`card-${chart.instanceId}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function renderCurrent() {
state = normalizeState(state);
const company = getCompany(state.companyId);
const tab = getTab(company, state.tabId);
const charts = chartsForTab(company, tab);
const version = ++renderVersion;
disposeAll();
@@ -144,15 +164,15 @@ function renderCurrent() {
renderTabs(shell.tabs, company.tabs, tab.id, (tabId) => {
updateHash({ companyId: company.id, tabId });
});
renderCards(shell.cards, tab.charts || [], activeViews, {
renderCards(shell.cards, charts, activeViews, {
onExport: exportPNG,
onSelectView: (chartId, view) => {
activeViews.set(chartId, view);
updateHash({ companyId: company.id, tabId: tab.id, chartId, view });
onSelectView: (chart, view) => {
activeViews.set(chart.viewKey, view);
updateHash({ companyId: company.id, tabId: tab.id, chartId: chart.id, view });
},
});
renderAnchors(shell.chartNav, tab.charts || [], selectAnchor);
Promise.all((tab.charts || []).map((chart) => renderChart(chart, version)));
renderAnchors(shell.chartNav, charts, selectAnchor);
Promise.all(charts.map((chart) => renderChart(chart, version)));
}
async function start() {