Files
crawler-plugin/admin-frontend-vue/scripts/round2-audit.mjs
T

97 lines
4.6 KiB
JavaScript

// round2 DOM 审计:登录本地真实栈后,对 15 页提取结构化观感事实(颜色/卡壳/分页坐标/按钮形态/标题/列头/空态)。
// 用法:cd admin-frontend-vue && node scripts/round2-audit.mjs
import { chromium } from '@playwright/test'
const BASE = process.env.BASE_URL || 'http://localhost:5174/admin-vue'
const PAGES = [
['account-users', 'account/users'],
['account-menus', 'account/menus'],
['account-groups', 'account/groups'],
['asin-registry', 'asin-center/registry'],
['asin-invalid', 'asin-center/invalid'],
['asin-query', 'asin-center/query'],
['asin-categories', 'asin-center/categories'],
['asin-skip-price', 'asin-center/skip-price'],
['shop-keys', 'shop-center/keys'],
['shop-shops', 'shop-center/shops'],
['shop-data-tasks', 'shop-center/data-tasks'],
['records-history', 'records/history'],
['records-software-version', 'records/software-version'],
['records-digital-human-version', 'records/digital-human-version'],
['records-image-video-tasks', 'records/image-video-tasks'],
]
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'zh-CN' })
const page = await context.newPage()
async function ensureLoggedIn() {
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
await page.waitForTimeout(600)
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
await page.locator('#loginUsername').fill('admin')
await page.locator('#loginPassword').fill('admin123')
await page.locator('.btn-login').click()
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
await page.waitForTimeout(1200)
}
}
await ensureLoggedIn()
const out = {}
for (const [name, path] of PAGES) {
await page.goto(`${BASE}/${path}`, { waitUntil: 'domcontentloaded' })
await page.locator('.page-heading').first().waitFor({ state: 'visible', timeout: 12000 }).catch(() => {})
await page.waitForLoadState('networkidle').catch(() => {})
await page.waitForTimeout(500)
const fact = await page.evaluate(() => {
const cs = (el, prop) => (el ? getComputedStyle(el)[prop] : null)
const root = getComputedStyle(document.documentElement)
const q = (sel) => document.querySelectorAll(sel)
const text = (el) => (el ? (el.textContent || '').trim() : '')
const heading = document.querySelector('.page-heading h1, .page-heading h2, .page-heading .page-title')
const headingText = heading ? (heading.textContent || '').trim() : null
const pags = [...q('.el-pagination')].map((p) => {
const r = p.getBoundingClientRect()
return { y: Math.round(r.top), x: Math.round(r.left), w: Math.round(r.width) }
})
const btns = [...q('.el-button')].map((b) => {
const t = (b.textContent || '').trim().slice(0, 8)
return { t, link: b.classList.contains('is-link'), text: b.classList.contains('is-text'), primary: b.classList.contains('el-button--primary'), danger: b.classList.contains('el-button--danger') }
})
const ths = [...q('.el-table__header th')].map((x) => (x.textContent || '').trim())
const empty = q('.el-empty, .empty-tip, .empty').length
const emptyText = text(document.querySelector('.el-empty, .empty-tip')) || null
const indigoEls = [...q('body *')].filter((el) => {
const b = cs(el, 'backgroundColor')
const c = cs(el, 'color')
return /rgb\(99,\s*102,\s*241\)|rgb\(238,\s*240,\s*254\)|#6366f1|#eef0fe/i.test(`${b} ${c}`)
}).slice(0, 4).map((el) => ({ tag: el.tagName, cls: (el.className && String(el.className).slice(0, 60)) || '', bg: cs(el, 'backgroundColor'), color: cs(el, 'color') }))
const cards = q('.el-card').length
const toolbars = q('.table-toolbar, .list-toolbar').length
const pageHeadingDesc = text(document.querySelector('.page-heading p'))
return {
heading: headingText, desc: (pageHeadingDesc || '').slice(0, 80),
primary: root.getPropertyValue('--el-color-primary').trim(),
bg: root.getPropertyValue('--el-bg-color').trim() || cs(document.body, 'backgroundColor'),
paginations: pags, cards, toolbars,
ths: ths.slice(0, 14), empty, emptyText,
buttonShape: {
total: btns.length,
link: btns.filter((b) => b.link).length,
textPlain: btns.filter((b) => b.text && !b.link).length,
primarySolid: btns.filter((b) => b.primary && !b.text && !b.link).length,
danger: btns.filter((b) => b.danger).length,
sample: btns.slice(0, 12),
},
indigoEls,
}
})
out[name] = { path, ...fact }
}
console.log(JSON.stringify(out, null, 2))
await browser.close()