微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View 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())