// 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()