微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
243
scripts/ops/extract_h5_vertical_gaps.py
Normal file
243
scripts/ops/extract_h5_vertical_gaps.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
测量 board-finance H5 原型「经营一览」板块内所有关键元素的
|
||||
实际渲染位置(getBoundingClientRect),计算相邻元素间的真实垂直间距。
|
||||
|
||||
配置与 screenshot_h5_pages.py 一致:iPhone 15 Pro Max (430×932, DPR:3)
|
||||
"""
|
||||
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)
|
||||
|
||||
# 用 getBoundingClientRect 测量每个关键元素的精确位置
|
||||
MEASURE_JS = """
|
||||
() => {
|
||||
const section = document.getElementById('section-overview');
|
||||
if (!section) return { error: 'not found' };
|
||||
|
||||
const sRect = section.getBoundingClientRect();
|
||||
|
||||
function measure(el, label) {
|
||||
const r = el.getBoundingClientRect();
|
||||
const cs = getComputedStyle(el);
|
||||
return {
|
||||
label,
|
||||
top: Math.round(r.top * 100) / 100,
|
||||
bottom: Math.round(r.bottom * 100) / 100,
|
||||
height: Math.round(r.height * 100) / 100,
|
||||
left: Math.round(r.left * 100) / 100,
|
||||
right: Math.round(r.right * 100) / 100,
|
||||
width: Math.round(r.width * 100) / 100,
|
||||
// 相对于板块顶部的偏移
|
||||
relTop: Math.round((r.top - sRect.top) * 100) / 100,
|
||||
relBottom: Math.round((r.bottom - sRect.top) * 100) / 100,
|
||||
fontSize: cs.fontSize,
|
||||
lineHeight: cs.lineHeight,
|
||||
paddingTop: cs.paddingTop,
|
||||
paddingBottom: cs.paddingBottom,
|
||||
text: el.textContent?.trim()?.substring(0, 30) || '',
|
||||
};
|
||||
}
|
||||
|
||||
const items = [];
|
||||
|
||||
// 板块根
|
||||
items.push(measure(section, 'SECTION_ROOT'));
|
||||
|
||||
// 板块头
|
||||
const header = section.querySelector('.summary-header');
|
||||
if (header) {
|
||||
items.push(measure(header, 'HEADER_BAR'));
|
||||
const h3 = header.querySelector('h3');
|
||||
if (h3) items.push(measure(h3, 'HEADER_TITLE'));
|
||||
const p = header.querySelector('p');
|
||||
if (p) items.push(measure(p, 'HEADER_DESC'));
|
||||
const emoji = header.querySelector('span');
|
||||
if (emoji) items.push(measure(emoji, 'HEADER_EMOJI'));
|
||||
}
|
||||
|
||||
// 板块正文
|
||||
const content = section.querySelector('.summary-content');
|
||||
if (content) {
|
||||
items.push(measure(content, 'CONTENT_BODY'));
|
||||
}
|
||||
|
||||
// 收入概览子标题行
|
||||
const subRows = content?.querySelectorAll(':scope > div > .flex.items-center.gap-2');
|
||||
if (subRows) {
|
||||
subRows.forEach((el, i) => {
|
||||
items.push(measure(el, `SUB_LABEL_${i}`));
|
||||
el.querySelectorAll('span').forEach((s, j) => {
|
||||
items.push(measure(s, `SUB_LABEL_${i}_SPAN_${j}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 3列网格
|
||||
const grid3 = section.querySelector('.grid.grid-cols-3');
|
||||
if (grid3) {
|
||||
items.push(measure(grid3, 'GRID_3COL'));
|
||||
grid3.querySelectorAll(':scope > div').forEach((cell, i) => {
|
||||
items.push(measure(cell, `GRID3_CELL_${i}`));
|
||||
// 标签文字
|
||||
const labelP = cell.querySelector('p');
|
||||
if (labelP) items.push(measure(labelP, `GRID3_CELL_${i}_LABEL`));
|
||||
// 数值文字(第二个 p)
|
||||
const allP = cell.querySelectorAll('p');
|
||||
if (allP[1]) items.push(measure(allP[1], `GRID3_CELL_${i}_VALUE`));
|
||||
// 环比
|
||||
const compare = cell.querySelector('.compare-data');
|
||||
if (compare) items.push(measure(compare, `GRID3_CELL_${i}_COMPARE`));
|
||||
});
|
||||
}
|
||||
|
||||
// 确认收入行(bg-white/10 rounded-xl 内的第一个)
|
||||
const confirmedRow = content?.querySelector('.rounded-xl');
|
||||
if (confirmedRow) {
|
||||
items.push(measure(confirmedRow, 'CONFIRMED_ROW'));
|
||||
confirmedRow.querySelectorAll('p').forEach((p, i) => {
|
||||
items.push(measure(p, `CONFIRMED_P_${i}`));
|
||||
});
|
||||
}
|
||||
|
||||
// 分割线
|
||||
const divider = content?.querySelector('.border-t');
|
||||
if (divider) {
|
||||
items.push(measure(divider, 'DIVIDER'));
|
||||
}
|
||||
|
||||
// 现金流水子标题行
|
||||
// 它是 divider 后面的 div 内的 flex 行
|
||||
const cashSection = divider?.nextElementSibling;
|
||||
if (cashSection) {
|
||||
const cashSubLabel = cashSection.querySelector('.flex.items-center.gap-2');
|
||||
if (cashSubLabel) {
|
||||
items.push(measure(cashSubLabel, 'CASH_SUB_LABEL'));
|
||||
}
|
||||
}
|
||||
|
||||
// 2列网格
|
||||
const grid2 = section.querySelector('.grid.grid-cols-2');
|
||||
if (grid2) {
|
||||
items.push(measure(grid2, 'GRID_2COL'));
|
||||
grid2.querySelectorAll(':scope > div').forEach((cell, i) => {
|
||||
items.push(measure(cell, `GRID2_CELL_${i}`));
|
||||
const allP = cell.querySelectorAll('p');
|
||||
if (allP[0]) items.push(measure(allP[0], `GRID2_CELL_${i}_LABEL`));
|
||||
if (allP[1]) items.push(measure(allP[1], `GRID2_CELL_${i}_VALUE`));
|
||||
const compare = cell.querySelector('.compare-data');
|
||||
if (compare) items.push(measure(compare, `GRID2_CELL_${i}_COMPARE`));
|
||||
});
|
||||
}
|
||||
|
||||
// AI 洞察
|
||||
const aiSection = section.querySelector('.ai-insight-section');
|
||||
if (aiSection) {
|
||||
items.push(measure(aiSection, 'AI_SECTION'));
|
||||
const aiHeader = aiSection.querySelector('.ai-insight-header');
|
||||
if (aiHeader) items.push(measure(aiHeader, 'AI_HEADER'));
|
||||
const aiTitle = aiSection.querySelector('.ai-insight-title');
|
||||
if (aiTitle) items.push(measure(aiTitle, 'AI_TITLE'));
|
||||
aiSection.querySelectorAll('p').forEach((p, i) => {
|
||||
items.push(measure(p, `AI_LINE_${i}`));
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 70)
|
||||
print("H5 垂直间距精确测量 — getBoundingClientRect")
|
||||
print("iPhone 15 Pro Max (430×932, DPR:3)")
|
||||
print("=" * 70)
|
||||
|
||||
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 = await page.evaluate("() => window.devicePixelRatio")
|
||||
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)
|
||||
|
||||
items = await page.evaluate(MEASURE_JS)
|
||||
|
||||
# 保存原始数据
|
||||
out_path = OUT_DIR / "board-finance-vertical-gaps.json"
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(items, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 计算相邻元素间的真实垂直间距
|
||||
print(f"\n共测量 {len(items)} 个元素\n")
|
||||
|
||||
# 按 relTop 排序,打印每个元素的位置
|
||||
sorted_items = sorted(items, key=lambda x: x['relTop'])
|
||||
|
||||
print(f"{'元素':<30} {'relTop':>8} {'relBot':>8} {'height':>8} {'fontSize':>10} {'lineH':>10}")
|
||||
print("-" * 100)
|
||||
for it in sorted_items:
|
||||
print(f"{it['label']:<30} {it['relTop']:>8.1f} {it['relBottom']:>8.1f} {it['height']:>8.1f} {it['fontSize']:>10} {it['lineHeight']:>10}")
|
||||
|
||||
# 计算关键间距对
|
||||
print("\n" + "=" * 70)
|
||||
print("关键垂直间距(相邻元素 bottom→top 的实际 px 距离)")
|
||||
print("公式:gap_rpx = gap_px × 2 × 0.875")
|
||||
print("=" * 70)
|
||||
|
||||
gap_pairs = [
|
||||
("HEADER_BAR", "CONTENT_BODY", "头部栏底 → 正文顶"),
|
||||
("HEADER_TITLE", "HEADER_DESC", "标题底 → 副标题顶"),
|
||||
("SUB_LABEL_0", "GRID_3COL", "收入概览标签底 → 3列网格顶"),
|
||||
("GRID3_CELL_0_LABEL", "GRID3_CELL_0_VALUE", "标签底 → 数值顶(cell0)"),
|
||||
("GRID3_CELL_0_VALUE", "GRID3_CELL_0_COMPARE", "数值底 → 环比顶(cell0)"),
|
||||
("GRID_3COL", "CONFIRMED_ROW", "3列网格底 → 确认收入行顶"),
|
||||
("CONFIRMED_ROW", "DIVIDER", "确认收入行底 → 分割线顶"),
|
||||
("DIVIDER", "CASH_SUB_LABEL", "分割线底 → 现金流水标签顶"),
|
||||
("CASH_SUB_LABEL", "GRID_2COL", "现金流水标签底 → 2列网格顶"),
|
||||
("GRID2_CELL_0_LABEL", "GRID2_CELL_0_VALUE", "标签底 → 数值顶(2col cell0)"),
|
||||
("GRID2_CELL_0_VALUE", "GRID2_CELL_0_COMPARE", "数值底 → 环比顶(2col cell0)"),
|
||||
("GRID_2COL", "AI_SECTION", "2列网格底 → AI洞察顶"),
|
||||
("AI_HEADER", "AI_LINE_0", "AI标题底 → 第一行文字顶"),
|
||||
("AI_LINE_0", "AI_LINE_1", "AI第1行底 → 第2行顶"),
|
||||
]
|
||||
|
||||
item_map = {it['label']: it for it in items}
|
||||
for from_label, to_label, desc in gap_pairs:
|
||||
fr = item_map.get(from_label)
|
||||
to = item_map.get(to_label)
|
||||
if fr and to:
|
||||
gap_px = to['relTop'] - fr['relBottom']
|
||||
gap_rpx = round(gap_px * 2 * 0.875, 1)
|
||||
print(f" {desc}")
|
||||
print(f" {from_label}.bottom={fr['relBottom']:.1f} → {to_label}.top={to['relTop']:.1f}")
|
||||
print(f" 间距: {gap_px:.1f}px → {gap_rpx:.1f}rpx")
|
||||
print()
|
||||
else:
|
||||
missing = from_label if not fr else to_label
|
||||
print(f" {desc}: ⚠️ 未找到 {missing}")
|
||||
print()
|
||||
|
||||
await browser.close()
|
||||
|
||||
print(f"原始数据: {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user