import { chromium } from 'playwright' /** * 全站布局审计 * * 查两类问题(都是「页脚被裁 / 页面被挤」的典型症状): * 1) 横向溢出:文档元素 scrollWidth 超过 clientWidth,页面出现左右滚动条 * 2) 页脚错位:footer 的左边界没有对齐内容区,或宽度小于视口(被侧边栏挤掉一块) * 3) 页脚被遮挡:footer 宽度正常但被侧栏覆盖 */ const BASE = 'http://127.0.0.1:8160/www' const PAGES = [ '/', '/demo.html', '/about.html', '/projects/', '/projects/iotlcm/', '/projects/xiu-dtu/', '/projects/xiu-admin/', '/projects/iotlcm/compare.html', '/projects/iotlcm/guide/quickstart.html', '/projects/iotlcm/guide/deploy.html', '/guide/portal-deploy.html', '/public/screenshots/readme.html', ] const VIEWPORTS = [ { w: 1440, h: 900, name: 'desktop' }, { w: 1024, h: 800, name: 'laptop' }, { w: 820, h: 900, name: 'tablet' }, { w: 390, h: 844, name: 'mobile' }, ] const browser = await chromium.launch({ executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', }) const problems = [] for (const vp of VIEWPORTS) { const page = await browser.newPage({ viewport: { width: vp.w, height: vp.h } }) for (const path of PAGES) { try { await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 30000 }) await page.waitForTimeout(250) const r = await page.evaluate(() => { const out = {} // 1) 横向溢出 out.docScrollWidth = document.documentElement.scrollWidth out.docClientWidth = document.documentElement.clientWidth out.hOverflow = out.docScrollWidth - out.docClientWidth // 找出真正超宽的元素(排除 fixed 定位的装饰层) const wide = [] document.querySelectorAll('body *').forEach((el) => { const cs = getComputedStyle(el) if (cs.position === 'fixed' || cs.position === 'sticky') return if (cs.overflowX !== 'visible') return const rect = el.getBoundingClientRect() if (rect.width === 0) return if (rect.right > document.documentElement.clientWidth + 1) { wide.push({ tag: el.tagName.toLowerCase(), cls: (el.className || '').toString().slice(0, 60), right: Math.round(rect.right), width: Math.round(rect.width), }) } }) out.wideEls = wide.slice(0, 5) out.wideCount = wide.length // 2) 页脚位置 const footer = document.querySelector('footer.pf-foot') if (footer) { const fr = footer.getBoundingClientRect() const wrap = footer.querySelector('.p-wrap') const wr = wrap ? wrap.getBoundingClientRect() : null out.footer = { left: Math.round(fr.left), width: Math.round(fr.width), vpWidth: document.documentElement.clientWidth, wrapLeft: wr ? Math.round(wr.left) : null, wrapWidth: wr ? Math.round(wr.width) : null, // 页脚是否铺满视口宽度(允许 1px 误差) spansVp: Math.abs(fr.width - document.documentElement.clientWidth) <= 1, // 页脚是否从视口左边开始 startsAtZero: Math.abs(fr.left) <= 1, } } else { out.footer = null } // 3) 侧边栏是否覆盖页脚(取侧栏底部与页脚顶部比较) const aside = document.querySelector('.VPSidebar') if (aside) { const ar = aside.getBoundingClientRect() const cs = getComputedStyle(aside) out.sidebar = { left: Math.round(ar.left), right: Math.round(ar.right), top: Math.round(ar.top), bottom: Math.round(ar.bottom), width: Math.round(ar.width), position: cs.position, visible: cs.display !== 'none' && ar.width > 0, } } else { out.sidebar = null } return out }) const issues = [] if (r.hOverflow > 2) { issues.push(`横向溢出 ${r.hOverflow}px`) } if (r.footer) { if (!r.footer.spansVp) issues.push(`页脚宽度 ${r.footer.width} ≠ 视口 ${r.footer.vpWidth}`) if (!r.footer.startsAtZero) issues.push(`页脚左偏 ${r.footer.left}px`) // 页脚内容区如果没从 0 附近开始,说明被侧栏挤了 if (r.sidebar?.visible && r.footer.wrapLeft !== null) { if (Math.abs(r.footer.wrapLeft - r.footer.left) > 40) { issues.push(`页脚内容被挤:wrap.left=${r.footer.wrapLeft}, footer.left=${r.footer.left}`) } } } else { issues.push('页脚缺失') } if (issues.length) { problems.push({ vp: vp.name, path, issues, detail: r }) } } catch (e) { problems.push({ vp: vp.name, path, issues: [`访问失败: ${e.message.slice(0, 80)}`] }) } } await page.close() } await browser.close() if (!problems.length) { console.log('✅ 未发现布局问题') } else { console.log(`❌ 发现 ${problems.length} 处问题\n`) for (const p of problems) { console.log(`[${p.vp}] ${p.path}`) for (const i of p.issues) console.log(' - ' + i) if (p.detail?.wideEls?.length) { console.log(' 超宽元素: ' + JSON.stringify(p.detail.wideEls)) } console.log() } }