""" 提取 board-finance H5 原型「经营一览」板块的精确 computed style 配置与 screenshot_h5_pages.py 一致:iPhone 15 Pro Max (430×932, DPR:3) 输出:每个关键元素的 padding/margin/gap/lineHeight/fontSize/height """ import asyncio import json from pathlib import Path from playwright.async_api import async_playwright BASE_URL = "http://127.0.0.1:5500/docs/h5_ui/pages" OUT_DIR = Path(__file__).resolve().parents[2] / "export" / "h5_style_extract" OUT_DIR.mkdir(parents=True, exist_ok=True) EXTRACT_JS = """ () => { const section = document.getElementById('section-overview'); if (!section) return { error: 'section-overview not found' }; const results = []; function extract(el, label) { const cs = getComputedStyle(el); return { label, tag: el.tagName, className: el.className?.substring?.(0, 80) || '', text: el.textContent?.trim()?.substring(0, 40) || '', paddingTop: cs.paddingTop, paddingBottom: cs.paddingBottom, paddingLeft: cs.paddingLeft, paddingRight: cs.paddingRight, marginTop: cs.marginTop, marginBottom: cs.marginBottom, marginLeft: cs.marginLeft, marginRight: cs.marginRight, gap: cs.gap, rowGap: cs.rowGap, columnGap: cs.columnGap, lineHeight: cs.lineHeight, fontSize: cs.fontSize, fontWeight: cs.fontWeight, height: cs.height, width: cs.width, borderTop: cs.borderTopWidth, borderBottom: cs.borderBottomWidth, display: cs.display, gridTemplateColumns: cs.gridTemplateColumns || '', }; } // 1. 板块根 results.push(extract(section, 'section-overview (root)')); // 2. 板块头 .summary-header const header = section.querySelector('.summary-header'); if (header) { results.push(extract(header, 'summary-header')); // 头部内子元素 header.querySelectorAll('h3, p, span').forEach((el, i) => { results.push(extract(el, `header-child-${i} (${el.tagName})`)); }); } // 3. 板块正文 .summary-content const content = section.querySelector('.summary-content'); if (content) { results.push(extract(content, 'summary-content')); } // 4. 收入概览子标题行 const subLabels = section.querySelectorAll('.summary-content > div > .flex.items-center.gap-2'); subLabels.forEach((el, i) => { results.push(extract(el, `sub-label-row-${i}`)); el.querySelectorAll('span').forEach((s, j) => { results.push(extract(s, `sub-label-row-${i}-span-${j}`)); }); }); // 5. 3列网格 const grid3 = section.querySelector('.grid.grid-cols-3'); if (grid3) { results.push(extract(grid3, 'grid-cols-3')); // 每个 cell grid3.querySelectorAll(':scope > div').forEach((cell, i) => { results.push(extract(cell, `grid3-cell-${i}`)); // cell 内的标签行和值 cell.querySelectorAll('p, span, div').forEach((el, j) => { results.push(extract(el, `grid3-cell-${i}-child-${j}`)); }); }); } // 6. 确认收入行 (bg-white/10 rounded-xl) const confirmedRow = section.querySelector('.bg-white\\\\/10'); // Tailwind 的 class 含 / 不好选,用更通用的方式 const allRoundedXl = section.querySelectorAll('.rounded-xl'); allRoundedXl.forEach((el, i) => { results.push(extract(el, `rounded-xl-${i}`)); el.querySelectorAll('p, span').forEach((c, j) => { results.push(extract(c, `rounded-xl-${i}-child-${j}`)); }); }); // 7. 分割线 const divider = section.querySelector('.border-t'); if (divider) { results.push(extract(divider, 'divider (border-t)')); } // 8. 2列网格 const grid2 = section.querySelector('.grid.grid-cols-2'); if (grid2) { results.push(extract(grid2, 'grid-cols-2')); grid2.querySelectorAll(':scope > div').forEach((cell, i) => { results.push(extract(cell, `grid2-cell-${i}`)); cell.querySelectorAll('p, span, div').forEach((el, j) => { results.push(extract(el, `grid2-cell-${i}-child-${j}`)); }); }); } // 9. AI 洞察 const aiSection = section.querySelector('.ai-insight-section'); if (aiSection) { results.push(extract(aiSection, 'ai-insight-section')); const aiHeader = aiSection.querySelector('.ai-insight-header'); if (aiHeader) { results.push(extract(aiHeader, 'ai-insight-header')); aiHeader.querySelectorAll('span, div').forEach((el, i) => { results.push(extract(el, `ai-header-child-${i}`)); }); } aiSection.querySelectorAll('p').forEach((el, i) => { results.push(extract(el, `ai-body-p-${i}`)); }); } return results; } """ async def main(): print("=" * 60) print("H5 样式提取 — iPhone 15 Pro Max (430×932, DPR:3)") print("=" * 60) async with async_playwright() as p: browser = await p.chromium.launch( headless=False, args=["--hide-scrollbars"], ) context = await browser.new_context( viewport={"width": 430, "height": 932}, device_scale_factor=3, ) page = await context.new_page() # 验证 DPR dpr = await page.evaluate("() => window.devicePixelRatio") print(f"devicePixelRatio = {dpr}") assert dpr == 3 # 导航 url = f"{BASE_URL}/board-finance.html" await page.goto(url, wait_until="load", timeout=15000) await page.wait_for_timeout(2500) # Tailwind CDN JIT 渲染 # 截图(对照用) screenshot_path = OUT_DIR / "board-finance-overview.png" # 先尝试截取板块区域 section = page.locator("#section-overview") await section.screenshot(path=str(screenshot_path)) print(f"截图: {screenshot_path}") # 提取样式 styles = await page.evaluate(EXTRACT_JS) # 保存 out_path = OUT_DIR / "board-finance-overview-styles.json" with open(out_path, "w", encoding="utf-8") as f: json.dump(styles, f, ensure_ascii=False, indent=2) print(f"样式数据: {out_path}") print(f"共提取 {len(styles)} 个元素") # 打印关键摘要 print("\n" + "=" * 60) print("关键间距摘要(px,需 ×2×0.875 转 rpx):") print("=" * 60) for item in styles: label = item['label'] # 只打印有意义的容器级元素 if any(k in label for k in ['header', 'content', 'grid', 'divider', 'confirmed', 'rounded', 'ai-insight', 'sub-label-row']): if 'child' not in label: pt = item['paddingTop'] pb = item['paddingBottom'] mt = item['marginTop'] mb = item['marginBottom'] gap = item['gap'] print(f" {label}:") print(f" padding: {pt} / {pb} (T/B) | margin: {mt} / {mb} (T/B) | gap: {gap}") await browser.close() print(f"\n✅ 完成,详细数据见 {out_path}") if __name__ == "__main__": asyncio.run(main())