114 lines
5.0 KiB
JavaScript
114 lines
5.0 KiB
JavaScript
import * as echarts from 'echarts';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { parseCSV } from '../js/csv.js';
|
||
import { getChartBuilder } from '../js/charts/index.js';
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifest.json'), 'utf8'));
|
||
const STYLE_RULES = {
|
||
legendTop: 85,
|
||
gridTop: 145,
|
||
lineSeries: { symbolSize: 9, borderWidth: 2.5 },
|
||
};
|
||
|
||
function entriesFromManifest(value) {
|
||
const entries = [];
|
||
value.companies.forEach((company) => {
|
||
if (!company.chartCatalog || !Array.isArray(company.tabs)) {
|
||
throw new Error(`公司 ${company.id || '(空)'} 缺少 chartCatalog 或 tabs`);
|
||
}
|
||
const chartIds = Object.keys(company.chartCatalog);
|
||
company.tabs.forEach((tab) => {
|
||
if (!Array.isArray(tab.chartRefs)) throw new Error(`栏目 ${tab.id} 缺少 chartRefs`);
|
||
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);
|
||
});
|
||
});
|
||
chartIds.forEach((id) => {
|
||
const chart = company.chartCatalog[id];
|
||
if (!getChartBuilder(chart.key)) throw new Error(`manifest 图表 ${chart.id} 引用了未注册 key:${chart.key}`);
|
||
if (Boolean(chart.csv) === Boolean(chart.views)) {
|
||
throw new Error(`manifest 图表 ${chart.id} 必须声明 csv 或 views 之一`);
|
||
}
|
||
if (chart.views && (!chart.defaultView || !chart.views[chart.defaultView])) {
|
||
throw new Error(`manifest 图表 ${chart.id} 的 defaultView 无效`);
|
||
}
|
||
const sources = chart.views
|
||
? Object.entries(chart.views).map(([view, csv]) => ({ view, csv }))
|
||
: [{ view: null, csv: chart.csv }];
|
||
if (!sources.every(({ csv }) => typeof csv === 'string' && csv.length > 0)) {
|
||
throw new Error(`manifest 图表 ${chart.id} 缺少 CSV`);
|
||
}
|
||
sources.forEach(({ view, csv }) => entries.push({ ...chart, id, company, view, csv }));
|
||
});
|
||
});
|
||
return entries;
|
||
}
|
||
|
||
const all = entriesFromManifest(manifest);
|
||
const only = process.argv.slice(2);
|
||
const targets = only.length === 0
|
||
? all
|
||
: all.filter((entry) => only.includes(entry.id) || only.includes(entry.key));
|
||
const unknown = only.filter((target) => !all.some((entry) => entry.id === target || entry.key === target));
|
||
|
||
if (unknown.length) console.log(`⚠ 未找到图表:${unknown.join(', ')}`);
|
||
|
||
let ok = 0;
|
||
let fail = 0;
|
||
const styleWarnings = [];
|
||
|
||
for (const entry of targets) {
|
||
const label = entry.view ? `${entry.id}:${entry.view}` : entry.id;
|
||
try {
|
||
const csvPath = path.resolve(ROOT, entry.csv);
|
||
if (!csvPath.startsWith(`${ROOT}${path.sep}`)) throw new Error(`CSV 路径越过项目根目录:${entry.csv}`);
|
||
const rows = parseCSV(fs.readFileSync(csvPath, 'utf8'));
|
||
const chart = echarts.init(null, null, { renderer: 'svg', ssr: true, width: 1100, height: 720 });
|
||
const option = getChartBuilder(entry.key)(rows, { width: 1100, height: 720, view: entry.view || undefined });
|
||
chart.setOption(option);
|
||
const svg = chart.renderToSVGString();
|
||
chart.dispose();
|
||
|
||
const badAttr = /style="[^"]*"[^"=>\s][^>]*>/.test(svg.slice(0, 3000));
|
||
if (option.legend?.top !== undefined && option.legend.top !== STYLE_RULES.legendTop) {
|
||
styleWarnings.push(`${label}: legend.top=${option.legend.top} (期望 ${STYLE_RULES.legendTop})`);
|
||
}
|
||
if (option.grid?.top !== undefined && option.legend?.top !== undefined && option.grid.top !== STYLE_RULES.gridTop) {
|
||
styleWarnings.push(`${label}: grid.top=${option.grid.top} (期望 ${STYLE_RULES.gridTop})`);
|
||
}
|
||
const circles = [...svg.matchAll(/<path d="M1 0A1[^"]*" transform="matrix\(([\d.]+),0,0,[\d.]+,[^)]+\)" fill="#fff" stroke="[^"]*" stroke-width="([\d.]+)"/g)];
|
||
if (circles.length > 0) {
|
||
const scale = +circles[0][1];
|
||
const strokeWidth = +circles[0][2];
|
||
if (Math.abs(scale * 2 - STYLE_RULES.lineSeries.symbolSize) > 0.1) {
|
||
styleWarnings.push(`${label}: 折线 symbolSize=${(scale * 2).toFixed(1)} (期望 ${STYLE_RULES.lineSeries.symbolSize})`);
|
||
}
|
||
if (Math.abs(strokeWidth * scale - STYLE_RULES.lineSeries.borderWidth) > 0.1) {
|
||
styleWarnings.push(`${label}: 折线 borderWidth=${(strokeWidth * scale).toFixed(1)} (期望 ${STYLE_RULES.lineSeries.borderWidth})`);
|
||
}
|
||
}
|
||
|
||
console.log(`${badAttr ? '⚠' : '✓'} ${label.padEnd(30)} ${String(svg.length).padStart(6)}B`);
|
||
ok += 1;
|
||
} catch (error) {
|
||
console.log(`✗ ${label.padEnd(30)} ${error.message}`);
|
||
fail += 1;
|
||
}
|
||
}
|
||
|
||
console.log(`\n${ok} 张成功 / ${fail} 张失败`);
|
||
if (styleWarnings.length) {
|
||
console.log(`\n⚠ ${styleWarnings.length} 处样式偏离规范:`);
|
||
styleWarnings.forEach((warning) => console.log(` ${warning}`));
|
||
} else {
|
||
console.log('✓ 样式规范全部符合');
|
||
}
|
||
|
||
process.exit(fail > 0 || unknown.length > 0 ? 1 : 0);
|