import * as echarts from 'echarts'; import fs from 'fs'; // 项目根目录(从 test/regress.mjs 向上一层,或直接指定绝对路径) const P = '/Users/joey/sites/vest-tools/echart-demo/'; const {parseCSV}=await import(P+'js/csv.js'); const all=[ ['inventory','inventoryChart','data/inventory.csv'], ['margin','marginChart','data/margin.csv'], ['twoline','twoLineChart','data/margin.csv'], ['growth','growthChart','data/growth.csv'], ['fx','fxChart','data/fx.csv'], ['ppmt_assets','assetsChart','data/ppmt_assets.csv'], ['ppmt_profit','profitChart','data/ppmt_profit.csv'], ['ppmt_ip','ipChart','data/ppmt_ip.csv'], ['ppmt_category','categoryChart','data/ppmt_category.csv'], ['ppmt_sankey','sankeyChart','data/ppmt_sankey_2026h1.csv'], ['ppmt_region_yoy','regionChannelYoyChart','data/ppmt_region_channel_yoy.csv'], ['ppmt_growth','ppmtGrowthChart','data/ppmt_growth.csv'], ['ppmt_margins','ppmtMarginsChart','data/ppmt_margins.csv'], ['ppmt_bridge','ppmtBridgeChart','data/ppmt_margin_bridge.csv'], ['ppmt_core_profit','ppmtCoreProfitChart','data/ppmt_core_profit.csv'], ['ppmt_core_margin','ppmtCoreMarginChart','data/ppmt_core_margin.csv'], ['ppmt_ip_share','ppmtIpShareChart','data/ppmt_ip_share_2026h1.csv'], ['ppmt_ip_share_history','ppmtIpShareHistoryChart','data/ppmt_ip_share_history.csv'], ['ppmt_ip_ex_monsters','ppmtIpExMonstersChart','data/ppmt_ip_ex_monsters.csv'], ]; // 可传图表名只跑指定的(改一张图不用全量回归): // node test/regress.mjs ppmt_ip_share ppmt_core_margin const only=process.argv.slice(2); const targets=only.length>0 ? all.filter(([f])=>only.includes(f)) : all; if(only.length>0){ const missing=only.filter(f=>!all.some(([name])=>name===f)); if(missing.length>0) console.log(`⚠ 未找到图表:${missing.join(', ')}`); } // 样式规范断言:从核心利润/利润率拆解图总结的标准 const STYLE_RULES={ legendTop:85, // 固定值:副标题到图例约25px gridTop:145, // 固定值:图例到绘图区约60px lineSeries:{ // 折线标准样式(空心圆点) symbolSize:9, borderWidth:2.5, fill:'#fff', // 白心 }, }; let ok=0,fail=0,styleWarnings=[]; for(const [f,fn,csv] of targets){ try{ const mod=await import(P+`js/charts/${f}.js`); const rows=parseCSV(fs.readFileSync(P+csv,'utf8')); const c=echarts.init(null,null,{renderer:'svg',ssr:true,width:1100,height:720}); const opt=mod[fn](rows,{width:1100,height:720}); c.setOption(opt); const svg=c.renderToSVGString(); // 1. SVG 格式检查(字体引号) const badAttr=/style="[^"]*"[^"=>\s][^>]*>/.test(svg.slice(0,3000)); // 2. 样式规范检查(跳过无图例的图表) if(opt.legend?.top!==undefined){ if(opt.legend.top!==STYLE_RULES.legendTop){ styleWarnings.push(`${f}: legend.top=${opt.legend.top} (期望${STYLE_RULES.legendTop})`); } } if(opt.grid?.top!==undefined && opt.legend?.top!==undefined){ if(opt.grid.top!==STYLE_RULES.gridTop){ styleWarnings.push(`${f}: grid.top=${opt.grid.top} (期望${STYLE_RULES.gridTop})`); } } // 3. 折线圆点样式检查(在 SVG 里验证 symbolSize 和 borderWidth) const circles=[...svg.matchAll(/0){ const scale=+circles[0][1], sw=+circles[0][2]; const symbolSize=scale*2, borderWidth=sw*scale; if(Math.abs(symbolSize-STYLE_RULES.lineSeries.symbolSize)>0.1){ styleWarnings.push(`${f}: 折线 symbolSize=${symbolSize.toFixed(1)} (期望${STYLE_RULES.lineSeries.symbolSize})`); } if(Math.abs(borderWidth-STYLE_RULES.lineSeries.borderWidth)>0.1){ styleWarnings.push(`${f}: 折线 borderWidth=${borderWidth.toFixed(1)} (期望${STYLE_RULES.lineSeries.borderWidth})`); } } console.log(`${badAttr?'⚠':'✓'} ${f.padEnd(17)} ${String(svg.length).padStart(6)}B`); ok++; c.dispose(); }catch(e){console.log(`✗ ${f.padEnd(17)} ${e.message}`);fail++;} } console.log(`\n${ok} 张成功 / ${fail} 张失败`); if(styleWarnings.length>0){ console.log(`\n⚠ ${styleWarnings.length} 处样式偏离规范:`); styleWarnings.forEach(w=>console.log(` ${w}`)); }else{ console.log('✓ 样式规范全部符合'); } process.exit(fail>0?1:0);