按要求删除导航栏「教程」及整个教程模块。
删除内容
- 导航项「教程」(/guide/videos) 及其 activeMatch
- 侧边栏「教程中心」条目
- pages/guide/videos.md(教程中心页,76 行)
- pages/projects/{iotlcm,xiu-dtu,xiu-admin}/guide/tutorial.md(三份教程视频页,共 251 行)
- data/projects.ts 里三个项目的「教程视频」子页面登记
同步清理的引用(避免 404)
- pages/projects/iotlcm/index.md:“下一步”表格里的教程视频行
- pages/projects/iotlcm/guide/quickstart.md:“下一步”里的教程链接
- 4 个审计脚本里的教程路径:
audit-band-offset.mjs / audit-layout.mjs / audit-sidebar-footer.mjs / render-check.mjs
- 注释里「教程等」的措辞
保留的内容(与本次无关,未动)
- DemoVideo.vue 及其 data/demo.ts 里的 demoVideos:
那是「系统演示」页里的 BV 号外链卡片,不是教程栏目
- compare.md 里「录屏教程」对比维度:属于竞品对比表的正常描述
验证
- 导航实测 8 项:首页/系统演示/产品功能/核心能力/界面预览/项目/官方文档/联系我们,无「教程」
- 侧边栏实测仅「门户部署」
- 干净副本构建成功,产物中无 tutorial/videos 文件
- 断链检查:失效项里无一条与教程相关
- check-registry 与 check-nav-brand 通过
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
/**
|
||||
* 「整宽色块 + 内容被挤到右边」全站扫描
|
||||
* 症状:某条通栏 band(背景铺满视口)内,首个内容块的左边界离视口左边 > 20% 视口宽。
|
||||
* 这正是用户截图里页脚那条「左边一大片空白、内容挤在右边」的形态。
|
||||
*/
|
||||
const BASE = 'http://127.0.0.1:18075'
|
||||
const PAGES = [
|
||||
'/', '/demo/', '/about/', '/projects/', '/projects/iotlcm/',
|
||||
'/projects/iotlcm/compare/', '/projects/iotlcm/guide/quickstart/',
|
||||
'/projects/iotlcm/guide/deploy/',
|
||||
'/projects/xiu-dtu/', '/projects/xiu-admin/', '/guide/portal-deploy/',
|
||||
]
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
|
||||
for (const w of [1920, 1440, 1280]) {
|
||||
const page = await browser.newPage({ viewport: { width: w, height: 950 } })
|
||||
for (const path of PAGES) {
|
||||
await page.goto(BASE + path, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForFunction(() => !!document.querySelector('footer.pf-foot'), null, { timeout: 60000 })
|
||||
await page.waitForTimeout(900)
|
||||
const bad = await page.evaluate(() => {
|
||||
const vp = document.documentElement.clientWidth
|
||||
const out = []
|
||||
const bands = document.querySelectorAll('section, footer, header, div.p-hero, div.p-section')
|
||||
bands.forEach((el) => {
|
||||
const cs = getComputedStyle(el)
|
||||
const r = el.getBoundingClientRect()
|
||||
const hasBg = cs.backgroundImage !== 'none' || (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)')
|
||||
if (!hasBg) return
|
||||
if (r.width < vp * 0.9) return
|
||||
// 找该 band 内第一个「有内容的块」
|
||||
const probe = el.querySelector('.p-wrap, .pf-foot-main, .container')
|
||||
if (!probe) return
|
||||
const pr = probe.getBoundingClientRect()
|
||||
const offset = pr.left - r.left
|
||||
if (offset > vp * 0.2) {
|
||||
out.push({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
cls: (el.className || '').toString().slice(0, 50),
|
||||
offset: Math.round(offset),
|
||||
pct: Math.round((offset / vp) * 100),
|
||||
})
|
||||
}
|
||||
})
|
||||
return out
|
||||
})
|
||||
if (bad.length) console.log(`${w}\t${path}\t${JSON.stringify(bad)}`)
|
||||
}
|
||||
await page.close()
|
||||
}
|
||||
await browser.close()
|
||||
console.log('scan done')
|
||||
@@ -0,0 +1,161 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
/**
|
||||
* 全站「页脚/侧栏」布局审计
|
||||
* 检查点:
|
||||
* A) 页脚内容左边界是否与无侧栏页面(首页)一致 —— 不一致说明被侧栏挤了
|
||||
* B) 页脚可视区域是否被 fixed 层(.VPSidebar 等)遮住(按面积估算)
|
||||
* C) 横向溢出
|
||||
*/
|
||||
const BASE = process.env.AUDIT_BASE || 'http://127.0.0.1:18075'
|
||||
const PAGES = [
|
||||
'/',
|
||||
'/demo/',
|
||||
'/about/',
|
||||
'/projects/',
|
||||
'/projects/iotlcm/',
|
||||
'/projects/iotlcm/compare/',
|
||||
'/projects/iotlcm/guide/quickstart/',
|
||||
'/projects/iotlcm/guide/deploy/',
|
||||
'/projects/xiu-dtu/',
|
||||
'/projects/xiu-admin/',
|
||||
'/guide/portal-deploy/',
|
||||
]
|
||||
const VPS = [1920, 1600, 1440, 1280, 1024, 820, 390]
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
|
||||
const rows = []
|
||||
|
||||
for (const w of VPS) {
|
||||
const page = await browser.newPage({ viewport: { width: w, height: 900 } })
|
||||
for (const path of PAGES) {
|
||||
await page.goto(BASE + path, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForFunction(() => !!document.querySelector('footer.pf-foot'), null, { timeout: 60000 })
|
||||
await page.waitForTimeout(900)
|
||||
const r = await page.evaluate(() => {
|
||||
const f = document.querySelector('footer.pf-foot')
|
||||
const fr = f.getBoundingClientRect()
|
||||
const wrap = f.querySelector('.p-wrap')
|
||||
const wr = wrap.getBoundingClientRect()
|
||||
const side = document.querySelector('.VPSidebar')
|
||||
let sideR = null
|
||||
if (side) {
|
||||
const s = side.getBoundingClientRect()
|
||||
const cs = getComputedStyle(side)
|
||||
if (s.width > 0 && cs.display !== 'none' && cs.visibility !== 'hidden')
|
||||
sideR = { left: s.left, right: s.right, width: s.width, z: cs.zIndex }
|
||||
}
|
||||
// 页脚与侧栏的横向重叠宽度(仅当侧栏层级高于页脚时才算遮挡)
|
||||
const overlap = sideR ? Math.max(0, Math.min(sideR.right, fr.right) - Math.max(sideR.left, fr.left)) : 0
|
||||
return {
|
||||
vp: document.documentElement.clientWidth,
|
||||
footW: Math.round(fr.width),
|
||||
wrapLeft: Math.round(wr.left),
|
||||
wrapW: Math.round(wr.width),
|
||||
cls: f.className,
|
||||
sideW: sideR ? Math.round(sideR.width) : 0,
|
||||
overlap: sideR && Number(sideR.z) > Number(getComputedStyle(f).zIndex) ? Math.round(overlap) : 0,
|
||||
footZ: getComputedStyle(f).zIndex,
|
||||
sideZ: sideR?.z ?? '-',
|
||||
hOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
}
|
||||
})
|
||||
rows.push({ w, path, ...r })
|
||||
}
|
||||
await page.close()
|
||||
}
|
||||
|
||||
// 基线:每个宽度下无侧栏页面的 wrapLeft
|
||||
const baseline = {}
|
||||
for (const r of rows) if (!r.cls.includes('has-sidebar')) baseline[r.w] ??= r.wrapLeft
|
||||
|
||||
let bad = 0
|
||||
console.log('w\tpath\twrapLeft\tbaseline\tsideW\toverlap\thOverflow')
|
||||
for (const r of rows) {
|
||||
const base = baseline[r.w]
|
||||
const issues = []
|
||||
if (base !== undefined && Math.abs(r.wrapLeft - base) > 8) issues.push('页脚内容左偏')
|
||||
if (r.overlap > 0) issues.push(`侧栏覆盖页脚 ${r.overlap}px`)
|
||||
if (r.hOverflow > 2) issues.push(`横向溢出 ${r.hOverflow}px`)
|
||||
if (issues.length) {
|
||||
bad++
|
||||
console.log(`${r.w}\t${r.path}\t${r.wrapLeft}\t${base}\t${r.sideW}\t${r.overlap}\t${r.hOverflow}\t<< ${issues.join(' / ')}`)
|
||||
}
|
||||
}
|
||||
console.log(`\n共 ${rows.length} 组检查,${bad} 组异常`)
|
||||
|
||||
await browser.close()
|
||||
@@ -0,0 +1,55 @@
|
||||
import { chromium } from 'playwright'
|
||||
import { createServer } from 'http'
|
||||
import { stat, readFile } from 'fs/promises'
|
||||
import { extname, join, normalize } from 'path'
|
||||
|
||||
const ROOT = process.argv[2]
|
||||
if (!ROOT) { console.error('用法: node render-check.mjs <distDir>'); process.exit(1) }
|
||||
|
||||
const MIME = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css', '.svg':'image/svg+xml', '.png':'image/png', '.jpg':'image/jpeg', '.json':'application/json', '.woff2':'font/woff2', '.woff':'font/woff', '.ico':'image/x-icon' }
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
let p = decodeURIComponent(req.url.split('?')[0])
|
||||
if (!p.startsWith('/')) p = '/' + p
|
||||
let fp = normalize(join(ROOT, p))
|
||||
try { await stat(fp) } catch {
|
||||
try { await stat(fp + '.html'); fp = fp + '.html' }
|
||||
catch { try { await stat(join(fp, 'index.html')); fp = join(fp, 'index.html') } catch { fp = join(ROOT, '404.html') } }
|
||||
}
|
||||
if ((await stat(fp)).isDirectory()) fp = join(fp, 'index.html')
|
||||
const buf = await readFile(fp)
|
||||
res.writeHead(200, { 'content-type': MIME[extname(fp)] || 'application/octet-stream' })
|
||||
res.end(buf)
|
||||
} catch { res.writeHead(404); res.end('nf') }
|
||||
})
|
||||
await new Promise(r => server.listen(0, r))
|
||||
const port = server.address().port
|
||||
|
||||
const browser = await chromium.launch({ channel: 'chrome', args: ['--no-sandbox'] })
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, javaScriptEnabled: false })
|
||||
const page = await ctx.newPage()
|
||||
|
||||
const pages = ['', 'demo', 'about', 'projects/', 'projects/iotlcm/', 'projects/xiu-dtu/', 'projects/xiu-admin/', 'guide/portal-deploy', 'projects/iotlcm/compare', 'projects/iotlcm/guide/deploy', 'projects/iotlcm/guide/quickstart']
|
||||
const results = []
|
||||
for (const p of pages) {
|
||||
const resp = await page.goto(`http://127.0.0.1:${port}/${p}`, { waitUntil: 'load' })
|
||||
const info = await page.evaluate(() => ({
|
||||
is404: !!document.querySelector('.NotFound'),
|
||||
h1: (document.querySelector('h1')?.textContent || '').trim().slice(0, 36),
|
||||
hasNav: !!document.querySelector('.VPNav'),
|
||||
hasFooter: !!document.querySelector('.pf-foot'),
|
||||
}))
|
||||
results.push({ p: p || '(home)', status: resp.status(), ...info })
|
||||
}
|
||||
console.log(`\n=== ${ROOT} 渲染验收 ===`)
|
||||
let ok = 0, fail = 0
|
||||
for (const r of results) {
|
||||
const bad = r.is404 || r.status >= 400 || !r.hasNav
|
||||
if (bad) fail++; else ok++
|
||||
console.log(`${bad ? '❌' : '✅'} ${String(r.status).padEnd(4)} ${r.p.padEnd(34)} ${r.is404 ? '[404页]' : ''} ${r.h1}`)
|
||||
}
|
||||
console.log(`\n通过 ${ok} / 失败 ${fail}`)
|
||||
await browser.close()
|
||||
server.close()
|
||||
process.exit(fail ? 1 : 0)
|
||||
Reference in New Issue
Block a user