| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- (async()=>{
- const R={score:{},issues:[],facts:{}};
- const px=v=>parseFloat(v)||0;
- const all=[...document.querySelectorAll('body *')].filter(e=>e.offsetParent!==null||e.tagName==='TH');
- // —— 1 排版: 字号阶梯收敛
- const sizes={}; all.forEach(e=>{const s=getComputedStyle(e).fontSize; sizes[s]=(sizes[s]||0)+1});
- const sizeList=Object.entries(sizes).sort((a,b)=>b[1]-a[1]);
- R.facts.fontSizes=sizeList.map(([k,v])=>k+'×'+v);
- const nSizes=sizeList.filter(([k,v])=>v>=3).length;
- R.score.type_scale = nSizes<=7?100:nSizes<=9?80:50;
- if(nSizes>7) R.issues.push(`字号阶梯 ${nSizes} 档 (>7): ${sizeList.filter(x=>x[1]>=3).map(x=>x[0]).join(',')}`);
- // 字族一致
- const fams=new Set(all.map(e=>getComputedStyle(e).fontFamily.split(',')[0].replace(/"/g,'')));
- R.facts.families=[...fams];
- R.score.type_family = fams.size<=3?100:70;
- if(fams.size>3) R.issues.push('字族 '+fams.size+' 种: '+[...fams].join('|'));
- // 数字等宽覆盖 (.num/.kpi b 必须 tabular)
- const numEls=[...document.querySelectorAll('.num,.kpi b,.cnt')];
- const badNum=numEls.filter(e=>!/tabular-nums/.test(getComputedStyle(e).fontVariantNumeric));
- R.score.type_tabular = numEls.length? Math.round((1-badNum.length/numEls.length)*100):100;
- if(badNum.length) R.issues.push(`数字未等宽 ${badNum.length}/${numEls.length}`);
- // 行长 (正文 ch)
- // 行长: CJK 按 1em/字, 拉丁按 0.5em/字 → 目标 ≤ 46 CJK 字 (中文正文舒适区 35~46)
- const proses=[...document.querySelectorAll('p.small,.fig-note,.det p,.empty')].slice(0,40);
- const longs=proses.filter(e=>{const fs=px(getComputedStyle(e).fontSize); const t=e.textContent;
- const cjk=(t.match(/[\u4e00-\u9fff]/g)||[]).length, frac=t.length?cjk/t.length:0;
- const em=fs*(0.5+0.5*frac); // 中文占比越高, 每字越宽
- return e.getBoundingClientRect().width/em>46+ (1-frac)*44});
- R.score.line_length = proses.length? Math.round((1-longs.length/proses.length)*100):100;
- if(longs.length) R.issues.push(`行长超舒适区的段落 ${longs.length}/${proses.length}`);
- // —— 2 间距: 4px 栅格
- const spaced=all.slice(0,400);
- const offGrid=[];
- spaced.forEach(e=>{const s=getComputedStyle(e);
- ['marginTop','marginBottom','paddingTop','paddingBottom','paddingLeft','gap'].forEach(k=>{
- const v=px(s[k]); if(v>0 && Math.abs(v-Math.round(v))<0.01 && v%1===0 && (v%2!==0)) offGrid.push(e.className+'.'+k+'='+v)})});
- R.facts.offGrid=offGrid.slice(0,8);
- R.score.spacing_grid = offGrid.length<=3?100:offGrid.length<=10?80:60;
- if(offGrid.length>3) R.issues.push(`非偶数间距 ${offGrid.length} 处 (示例 ${offGrid.slice(0,3).join(', ')})`);
- // —— 3 版面: 无横向溢出 + 点击目标
- R.facts.bodyScroll=[document.documentElement.scrollWidth, window.innerWidth];
- R.score.layout_nohscroll = document.documentElement.scrollWidth<=window.innerWidth+1?100:0;
- if(document.documentElement.scrollWidth>window.innerWidth+1) R.issues.push('页面横向溢出 '+document.documentElement.scrollWidth+'>'+window.innerWidth);
- const targets=[...document.querySelectorAll('button,select,a.tl')];
- const small=targets.filter(e=>{const r=e.getBoundingClientRect(); return r.height>0&&r.height<24});
- R.score.layout_targets = targets.length? Math.round((1-small.length/targets.length)*100):100;
- if(small.length) R.issues.push(`点击目标<24px高 ${small.length}/${targets.length}`);
- // —— 4 对比度 (WCAG AA 4.5:1 正文 / 3:1 大字)
- // 颜色解析: 支持 rgb()/rgba()/color(srgb r g b / a), 0..1 与 0..255 自动判别; 半透明按父底合成
- function parse(c){
- if(!c) return null;
- const nums=(c.match(/-?[\d.]+(e-?\d+)?/g)||[]).map(Number);
- if(nums.length<3) return null;
- let [r,g,b]=nums; const a=nums.length>3?nums[3]:1;
- const isUnit=/color\(/.test(c)||(r<=1&&g<=1&&b<=1&&/\./.test(c));
- if(isUnit){r*=255;g*=255;b*=255}
- return {r,g,b,a};
- }
- function over(fg,bg){ // alpha 合成
- const a=fg.a; return {r:fg.r*a+bg.r*(1-a), g:fg.g*a+bg.g*(1-a), b:fg.b*a+bg.b*(1-a), a:1};
- }
- function lumc(o){const f=v=>{v/=255; return v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4)};
- return .2126*f(o.r)+.7152*f(o.g)+.0722*f(o.b)}
- function bg(e){ // 从元素向上合成到不透明底
- let n=e, stack=[];
- while(n&&n!==document.documentElement){
- const c=parse(getComputedStyle(n).backgroundColor);
- if(c&&c.a>0){ stack.push(c); if(c.a>=1) break }
- n=n.parentElement;
- }
- let base=parse(getComputedStyle(document.body).backgroundColor)||{r:255,g:255,b:255,a:1};
- if(base.a<1) base={r:255,g:255,b:255,a:1};
- for(let i=stack.length-1;i>=0;i--) base=over(stack[i], base);
- return base;
- }
- const texts=[...document.querySelectorAll('p,td,th,span,b,h1,h3,button,a')].filter(e=>e.textContent.trim()&&e.children.length===0).slice(0,220);
- const lowC=[];
- texts.forEach(e=>{const s=getComputedStyle(e); const fg=parse(s.color); if(!fg) return;
- const b=bg(e); const fgc=fg.a<1?over(fg,b):fg;
- const L1=lumc(fgc)+.05, L2=lumc(b)+.05;
- const ratio=Math.max(L1,L2)/Math.min(L1,L2); const fs=px(s.fontSize); const need=(fs>=18||(fs>=14&&+s.fontWeight>=600))?3:4.5;
- if(ratio<need) lowC.push(`${e.className||e.tagName} ${fs}px ${ratio.toFixed(2)}:1<${need}`)});
- R.facts.lowContrast=lowC.slice(0,10);
- R.score.contrast = texts.length? Math.round((1-lowC.length/texts.length)*100):100;
- if(lowC.length) R.issues.push(`对比度不足 ${lowC.length}/${texts.length}: ${lowC.slice(0,3).join(' | ')}`);
- // —— 5 图表
- const svgs=[...document.querySelectorAll('svg')];
- let chartIssues=[];
- svgs.forEach((sv,i)=>{
- const card=sv.closest('.card')||sv.closest('.fig'); const title=(card?.querySelector('h3')||card?.querySelector('h4'))?.textContent||'?';
- const texts=[...sv.querySelectorAll('text')];
- const tiny=texts.filter(t=>px(getComputedStyle(t).fontSize)<9);
- if(tiny.length) chartIssues.push(`${title.slice(0,16)}: ${tiny.length}个刻度字号<9px`);
- // 图例重叠
- const legs=texts.filter(t=>/^—/.test(t.textContent));
- for(let a=0;a<legs.length;a++)for(let b=a+1;b<legs.length;b++){
- const r1=legs[a].getBoundingClientRect(),r2=legs[b].getBoundingClientRect();
- if(r1.right>r2.left+1&&r2.right>r1.left+1&&Math.abs(r1.top-r2.top)<4) chartIssues.push(`${title.slice(0,16)}: 图例重叠`)}
- // 单位
- const hasAxisLabel=[...sv.querySelectorAll('text')].some(x=>/[a-zA-Z%℃°]|m\/s|kW|bar|rpm|条|次|日|月/.test(x.textContent)&&x.textContent.length>1);
- if(!/\(|(/.test(title) && !card?.querySelector('.fig-note') && !hasAxisLabel) chartIssues.push(`${title.slice(0,16)}: 无单位无图注无轴标`);
- });
- R.facts.svgCount=svgs.length; R.facts.chartIssues=chartIssues.slice(0,10);
- R.score.charts = svgs.length? Math.max(0,100-chartIssues.length*12):100;
- if(chartIssues.length) R.issues.push(`图表问题 ${chartIssues.length}: ${chartIssues.slice(0,3).join(' | ')}`);
- // —— 总分
- const vals=Object.values(R.score); R.total=Math.round(vals.reduce((a,b)=>a+b,0)/vals.length);
- return JSON.stringify(R);
- })()
|