(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{ 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;ar2.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); })()