Files
Neo-ZQYY/scripts/ops/screenshot_h5_pages.py
2026-03-15 10:15:02 +08:00

437 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
[已废弃 — 本 spec 不使用]
H5 原型页面批量截图脚本全页面截图430×932 视口)。
已被 anchor_compare.py 的逐段截图方案430×752 视口)取代。
保留原因:仍可用于快速获取全页面参考截图,但不作为像素对比的输入源。
替代工具scripts/ops/anchor_compare.py extract-h5 <page>
原始参数iPhone 15 Pro Max: 430×932, DPR:3 → 输出 1290×N 像素 PNG
"""
import asyncio
import sys
from pathlib import Path
from playwright.async_api import async_playwright, Page
BASE_URL = "http://127.0.0.1:5500/docs/h5_ui/pages"
OUT_DIR = Path(__file__).resolve().parents[2] / "docs" / "h5_ui" / "screenshots"
# 确保输出目录存在
OUT_DIR.mkdir(parents=True, exist_ok=True)
# 21 个默认态页面
DEFAULT_PAGES = [
"login", "apply", "reviewing", "no-permission",
"task-list", "task-detail", "task-detail-priority",
"task-detail-relationship", "task-detail-callback",
"performance", "performance-records",
"board-finance", "board-customer", "board-coach",
"coach-detail", "customer-detail", "customer-service-records",
"chat", "chat-history", "notes", "my-profile",
]
HIDE_SCROLLBAR_JS = """
() => {
document.documentElement.style.overflow = 'auto';
document.documentElement.style.scrollbarWidth = 'none';
const s = document.createElement('style');
s.textContent = '::-webkit-scrollbar { display: none !important; }';
document.head.appendChild(s);
}
"""
async def setup_page(page: Page, url: str) -> None:
"""导航到页面,等待 Tailwind CDN 渲染,隐藏滚动条"""
# Go Live 的 WebSocket 长连接会阻止 networkidle改用 load
await page.goto(url, wait_until="load", timeout=15000)
await page.wait_for_timeout(2500) # Tailwind CDN JIT 渲染
await page.evaluate(HIDE_SCROLLBAR_JS)
await page.wait_for_timeout(300)
async def take_screenshot(page: Page, name: str) -> dict:
"""截取全页截图并返回文件信息"""
out_path = OUT_DIR / f"{name}.png"
await page.screenshot(path=str(out_path), full_page=True)
size = out_path.stat().st_size
return {"name": name, "size": size, "path": str(out_path)}
async def get_page_functions(page: Page) -> list[str]:
"""获取页面内联 script 中定义的函数名"""
return await page.evaluate("""
() => {
const scripts = document.querySelectorAll('script:not([src])');
const fns = [];
for (const s of scripts) {
const matches = s.textContent.matchAll(/function\\s+(\\w+)/g);
for (const m of matches) fns.push(m[1]);
}
return fns;
}
""")
async def screenshot_default_pages(page: Page) -> list[dict]:
"""截取所有默认态页面"""
results = []
for name in DEFAULT_PAGES:
url = f"{BASE_URL}/{name}.html"
await setup_page(page, url)
info = await take_screenshot(page, name)
print(f"{name} ({info['size']:,} bytes)")
results.append(info)
return results
async def screenshot_interactive_pages(page: Page) -> list[dict]:
"""截取所有交互态页面 — 全部用 DOM class 操作,不依赖闭包内函数"""
results = []
# --- task-list: context menu ---
await setup_page(page, f"{BASE_URL}/task-list.html")
# showContextMenu 在 IIFE 闭包内,改用 DOM class 操作
await page.evaluate("""
() => {
const overlay = document.getElementById('contextOverlay');
const menu = document.getElementById('contextMenu');
if (overlay) overlay.classList.add('active');
if (menu) {
menu.style.left = '120px';
menu.style.top = '300px';
menu.classList.add('active');
}
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "task-list--context-menu")
print(f" ✅ task-list--context-menu ({info['size']:,} bytes)")
results.append(info)
# --- task-list: note modal ---
await setup_page(page, f"{BASE_URL}/task-list.html")
# remarkModal 通过 .active class 显示
await page.evaluate("""
() => {
const modal = document.getElementById('remarkModal');
if (modal) modal.classList.add('active');
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "task-list--note-modal")
print(f" ✅ task-list--note-modal ({info['size']:,} bytes)")
results.append(info)
# --- task-detail: note modal ---
await setup_page(page, f"{BASE_URL}/task-detail.html")
await page.evaluate("""
() => {
const modal = document.getElementById('noteModal');
if (modal) {
modal.classList.add('active');
modal.classList.remove('hidden');
modal.style.display = '';
}
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "task-detail--note-modal")
print(f" ✅ task-detail--note-modal ({info['size']:,} bytes)")
results.append(info)
# --- task-detail: abandon modal ---
await setup_page(page, f"{BASE_URL}/task-detail.html")
await page.evaluate("""
() => {
const modal = document.getElementById('abandonModal');
if (modal) {
modal.classList.add('active');
modal.classList.remove('hidden');
modal.style.display = '';
}
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "task-detail--abandon-modal")
print(f" ✅ task-detail--abandon-modal ({info['size']:,} bytes)")
results.append(info)
# --- coach-detail: note modal ---
await setup_page(page, f"{BASE_URL}/coach-detail.html")
await page.evaluate("""
() => {
const popup = document.getElementById('notesPopup');
if (popup) {
popup.classList.add('active', 'show');
popup.classList.remove('hidden');
popup.style.display = '';
}
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "coach-detail--note-modal")
print(f" ✅ coach-detail--note-modal ({info['size']:,} bytes)")
results.append(info)
return results
async def screenshot_board_finance_interactive(page: Page) -> list[dict]:
"""board-finance 交互态截图 — 用 DOM class 操作"""
results = []
# filter dropdown (时间筛选)
await setup_page(page, f"{BASE_URL}/board-finance.html")
await page.evaluate("""
() => {
const overlay = document.getElementById('filterOverlay');
const dropdown = document.getElementById('timeDropdown');
const bar = document.getElementById('filterBar');
if (overlay) overlay.classList.add('show');
if (dropdown) {
// 定位到筛选栏下方
if (bar) {
const rect = bar.getBoundingClientRect();
dropdown.style.top = rect.bottom + 'px';
}
dropdown.classList.add('show');
}
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "board-finance--filter-dropdown")
print(f" ✅ board-finance--filter-dropdown ({info['size']:,} bytes)")
results.append(info)
# tip modal
await setup_page(page, f"{BASE_URL}/board-finance.html")
await page.evaluate("""
() => {
const toast = document.getElementById('tipToast');
const overlay = document.getElementById('tipOverlay');
const titleEl = document.getElementById('tipTitle');
const contentEl = document.getElementById('tipContent');
if (titleEl) titleEl.textContent = '发生额/正价';
if (contentEl) contentEl.innerHTML = '<strong>发生额</strong>:所有订单的原价总和(不扣优惠)\\n用于衡量门店整体业务规模';
if (overlay) overlay.classList.add('show');
if (toast) toast.classList.add('show');
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "board-finance--tip-modal")
print(f" ✅ board-finance--tip-modal ({info['size']:,} bytes)")
results.append(info)
# toc panel
await setup_page(page, f"{BASE_URL}/board-finance.html")
await page.evaluate("""
() => {
const dropdown = document.getElementById('tocDropdown');
const overlay = document.getElementById('tocOverlay');
if (dropdown) dropdown.classList.add('show');
if (overlay) overlay.classList.add('show');
}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, "board-finance--toc-panel")
print(f" ✅ board-finance--toc-panel ({info['size']:,} bytes)")
results.append(info)
return results
async def screenshot_board_coach_interactive(page: Page) -> list[dict]:
"""board-coach 交互态截图 — 用 DOM class 操作切换维度容器"""
results = []
# 4 个排序维度切换:直接操作 dim-container 的 active class
sort_dims = [
("perf-high", "dim-perf", "定档业绩最高"),
("salary-high", "dim-salary", "工资最高"),
("storage-value", "dim-sv", "客源储值最高"),
("task-complete", "dim-task", "任务完成最多"),
]
for suffix, dim_id, label_text in sort_dims:
await setup_page(page, f"{BASE_URL}/board-coach.html")
await page.evaluate(f"""
() => {{
// 切换维度容器
document.querySelectorAll('.dim-container').forEach(el => el.classList.remove('active'));
const target = document.getElementById('{dim_id}');
if (target) target.classList.add('active');
// 更新排序标签
const label = document.getElementById('sortLabel');
if (label) label.textContent = '{label_text}';
}}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, f"board-coach--{suffix}")
print(f" ✅ board-coach--{suffix} ({info['size']:,} bytes)")
results.append(info)
# 3 个筛选下拉:直接操作 dropdown 的 show class
filter_types = [
("sort-dropdown", "sortDropdown"),
("skill-dropdown", "skillDropdown"),
("time-dropdown", "timeDropdown"),
]
for suffix, dropdown_id in filter_types:
await setup_page(page, f"{BASE_URL}/board-coach.html")
await page.evaluate(f"""
() => {{
const overlay = document.getElementById('filterOverlay');
const dropdown = document.getElementById('{dropdown_id}');
const bar = document.getElementById('filterBar');
if (overlay) overlay.classList.add('show');
if (dropdown) {{
// 定位到筛选栏下方
if (bar) {{
const rect = bar.getBoundingClientRect();
dropdown.style.top = rect.bottom + 'px';
}}
dropdown.classList.add('show');
}}
}}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, f"board-coach--{suffix}")
print(f" ✅ board-coach--{suffix} ({info['size']:,} bytes)")
results.append(info)
return results
async def screenshot_board_customer_interactive(page: Page) -> list[dict]:
"""board-customer 交互态截图 — 用 DOM class 操作切换维度容器"""
results = []
# 8 个维度切换:直接操作 dim-container 的 active class
customer_dims = [
("recall", "dim-recall", "最应召回"),
("potential", "dim-potential", "最大消费潜力"),
("balance", "dim-balance", "最高余额"),
("recharge", "dim-recharge", "最近充值"),
("recent", "dim-recent", "最近到店"),
("spend60", "dim-spend60", "最高消费 近60天"),
("freq60", "dim-freq60", "最频繁 近60天"),
("loyal", "dim-loyal", "最专一 近60天"),
]
for suffix, dim_id, label_text in customer_dims:
await setup_page(page, f"{BASE_URL}/board-customer.html")
await page.evaluate(f"""
() => {{
// 切换维度容器
document.querySelectorAll('.dim-container').forEach(el => el.classList.remove('active'));
const target = document.getElementById('{dim_id}');
if (target) target.classList.add('active');
// 更新类型标签
const label = document.getElementById('typeLabel');
if (label) label.textContent = '{label_text}';
}}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, f"board-customer--{suffix}")
print(f" ✅ board-customer--{suffix} ({info['size']:,} bytes)")
results.append(info)
# 2 个筛选下拉:直接操作 dropdown 的 show class
filter_types = [
("type-dropdown", "typeDropdown"),
("project-dropdown", "projectDropdown"),
]
for suffix, dropdown_id in filter_types:
await setup_page(page, f"{BASE_URL}/board-customer.html")
await page.evaluate(f"""
() => {{
const overlay = document.getElementById('filterOverlay');
const dropdown = document.getElementById('{dropdown_id}');
const bar = document.getElementById('filterBar');
if (overlay) overlay.classList.add('show');
if (dropdown) {{
if (bar) {{
const rect = bar.getBoundingClientRect();
dropdown.style.top = rect.bottom + 'px';
}}
dropdown.classList.add('show');
}}
}}
""")
await page.wait_for_timeout(500)
info = await take_screenshot(page, f"board-customer--{suffix}")
print(f" ✅ board-customer--{suffix} ({info['size']:,} bytes)")
results.append(info)
return results
async def main():
print("=" * 60)
print("H5 截图 — iPhone 15 Pro Max (430×932, DPR:3)")
print(f"输出目录: {OUT_DIR}")
print("=" * 60)
async with async_playwright() as p:
# --hide-scrollbars 消除桌面模式下滚动条占宽,保证输出 1290px (430×3)
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"\ndevicePixelRatio = {dpr}")
assert dpr == 3, f"DPR 应为 3实际为 {dpr}"
# 1. 默认态截图
print("\n📸 默认态截图 (21 页):")
default_results = await screenshot_default_pages(page)
# 2. 交互态截图
print("\n📸 交互态截图:")
interactive_results = await screenshot_interactive_pages(page)
# 3. board-finance 交互态
print("\n📸 board-finance 交互态:")
finance_results = await screenshot_board_finance_interactive(page)
# 4. board-coach 交互态
print("\n📸 board-coach 交互态:")
coach_results = await screenshot_board_coach_interactive(page)
# 5. board-customer 交互态
print("\n📸 board-customer 交互态:")
customer_results = await screenshot_board_customer_interactive(page)
all_results = (
default_results + interactive_results +
finance_results + coach_results + customer_results
)
await browser.close()
# 验证第一张截图的尺寸(读 PNG IHDR chunk无需 Pillow
import struct
with open(all_results[0]["path"], "rb") as f:
f.read(16) # 跳过 PNG 签名(8) + IHDR chunk length(4) + type(4)
w, h = struct.unpack(">II", f.read(8))
print(f"\n🔍 验证: {all_results[0]['name']}.png → {w}×{h} px")
assert w == 1290, f"宽度应为 1290 (430×3),实际为 {w}"
print(f"\n✅ 全部完成: {len(all_results)} 张截图")
print(f" 输出目录: {OUT_DIR}")
if __name__ == "__main__":
asyncio.run(main())