微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
429
scripts/ops/screenshot_h5_pages.py
Normal file
429
scripts/ops/screenshot_h5_pages.py
Normal file
@@ -0,0 +1,429 @@
|
||||
"""
|
||||
H5 原型页面批量截图脚本
|
||||
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())
|
||||
Reference in New Issue
Block a user