213 lines
7.4 KiB
Python
213 lines
7.4 KiB
Python
"""
|
||
提取 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())
|