""" 固定步长滚动截图辅助工具(v3 — 纯参数计算 + 文件管理)。 本脚本不执行截图或对比,仅提供: 1. scrollTop 序列计算(给定 scrollHeight → 步数和序列) 2. 文件命名和目录结构管理 3. 差异率汇总报告生成 实际截图和对比通过 MCP 工具在对话中执行: - H5 截图:Playwright MCP(browser_navigate → browser_evaluate → browser_take_screenshot) - MP 截图:微信 MCP(navigate_to → evaluate_script → screenshot) - 像素对比:image_compare MCP(compare_images) 用法: python scripts/ops/anchor_compare.py calc # 计算 scrollTop 序列 python scripts/ops/anchor_compare.py status [] # 查看截图状态 python scripts/ops/anchor_compare.py report # 生成/更新差异率报告 python scripts/ops/anchor_compare.py list # 列出所有页面及状态 截图参数(双端统一): viewport 430×752, DPR=1.5, 输出 645×1128 步长 600px, maxScroll ≤ 10 视为单屏 """ import io import json import math import sys from pathlib import Path def _ensure_utf8_stdio(): """Windows 终端 GBK 编码兼容:强制 stdout/stderr 为 UTF-8。""" if sys.platform == "win32": sys.stdout = io.TextIOWrapper( sys.stdout.buffer, encoding="utf-8", errors="replace" ) sys.stderr = io.TextIOWrapper( sys.stderr.buffer, encoding="utf-8", errors="replace" ) # ─── 路径常量 ─── ROOT = Path(__file__).resolve().parents[2] COMPARE_DIR = ROOT / "docs" / "h5_ui" / "compare" H5_PAGES_DIR = ROOT / "docs" / "h5_ui" / "pages" # ─── 截图参数 ─── VIEWPORT_W = 430 VIEWPORT_H = 752 DPR = 1.5 TARGET_W = 645 # 430 × 1.5 TARGET_H = 1128 # 752 × 1.5 STEP_PX = 600 # 固定步长(逻辑像素) SINGLE_SCREEN_THRESHOLD = 10 # maxScroll ≤ 此值视为单屏 H5_BASE_URL = "http://127.0.0.1:5500/docs/h5_ui/pages" # ═══════════════════════════════════════════════════════════ # 页面清单(来自 design.md §5,2026-03-10 Playwright 实测) # ═══════════════════════════════════════════════════════════ PAGE_DATA: dict[str, dict] = { "board-finance": {"scrollHeight": 5600, "maxScroll": 4848, "steps": 10, "dims": 1}, "board-coach": {"scrollHeight": 754, "maxScroll": 2, "steps": 1, "dims": 4}, "board-customer": {"scrollHeight": 752, "maxScroll": 0, "steps": 1, "dims": 8}, "task-detail": {"scrollHeight": 2995, "maxScroll": 2243, "steps": 5, "dims": 1}, "task-detail-callback": {"scrollHeight": 2397, "maxScroll": 1645, "steps": 4, "dims": 1}, "task-detail-priority": {"scrollHeight": 2389, "maxScroll": 1637, "steps": 4, "dims": 1}, "task-detail-relationship": {"scrollHeight": 2275, "maxScroll": 1523, "steps": 4, "dims": 1}, "coach-detail": {"scrollHeight": 2918, "maxScroll": 2166, "steps": 5, "dims": 1}, "customer-detail": {"scrollHeight": 3070, "maxScroll": 2318, "steps": 5, "dims": 1}, "performance": {"scrollHeight": 7705, "maxScroll": 6953, "steps": 13, "dims": 1}, "task-list": {"scrollHeight": 1428, "maxScroll": 676, "steps": 3, "dims": 1}, "my-profile": {"scrollHeight": 752, "maxScroll": 0, "steps": 1, "dims": 1}, "customer-service-records": {"scrollHeight": 961, "maxScroll": 209, "steps": 2, "dims": 1}, "performance-records": {"scrollHeight": 2677, "maxScroll": 1925, "steps": 5, "dims": 1}, "chat": {"scrollHeight": 1061, "maxScroll": 309, "steps": 2, "dims": 1}, "chat-history": {"scrollHeight": 752, "maxScroll": 0, "steps": 1, "dims": 1}, "notes": {"scrollHeight": 1709, "maxScroll": 957, "steps": 3, "dims": 1}, } # board-coach 排序维度 BOARD_COACH_DIMS = ["perf", "salary", "sv", "task"] # board-customer 客户维度 BOARD_CUSTOMER_DIMS = ["recall", "potential", "balance", "recharge", "recent", "spend60", "freq60", "loyal"] # ═══════════════════════════════════════════════════════════ # 核心计算 # ═══════════════════════════════════════════════════════════ def compute_scroll_sequence(scroll_height: int, viewport_height: int = VIEWPORT_H) -> list[int]: """根据 scrollHeight 和 viewportHeight 计算固定步长的 scrollTop 序列。 规则(design.md §2.2): maxScroll = scrollHeight - viewportHeight maxScroll ≤ 10 → 单屏 [0] N = floor(maxScroll / 600) + 1 序列:0, 600, 1200, ... 最后一步 clamp 到 maxScroll """ max_scroll = scroll_height - viewport_height if max_scroll <= SINGLE_SCREEN_THRESHOLD: return [0] sequence = [] for i in range(math.floor(max_scroll / STEP_PX) + 1): target = i * STEP_PX if target >= max_scroll: target = max_scroll sequence.append(target) if sequence[-1] != max_scroll: sequence.append(max_scroll) return sequence def page_output_dir(page_name: str, dimension: str | None = None) -> Path: """返回页面的截图输出目录。多维度页面按维度分子目录。""" base = COMPARE_DIR / page_name if dimension: return base / dimension return base def h5_url(page_name: str) -> str: """返回 H5 页面的 Live Server URL。""" return f"{H5_BASE_URL}/{page_name}.html" # ═══════════════════════════════════════════════════════════ # CLI 命令 # ═══════════════════════════════════════════════════════════ def cmd_calc(scroll_height: int): """计算 scrollTop 序列""" seq = compute_scroll_sequence(scroll_height) max_scroll = scroll_height - VIEWPORT_H print(f"scrollHeight: {scroll_height}px") print(f"viewportHeight: {VIEWPORT_H}px") print(f"maxScroll: {max_scroll}px") print(f"步数: {len(seq)}") print(f"序列: {seq}") def cmd_status(page_name: str | None = None): """查看截图状态""" pages = [page_name] if page_name else list(PAGE_DATA.keys()) print(f"\n{'页面':<30} {'步数':>4} {'维度':>4} {'H5':>4} {'MP':>4} {'diff':>4}") print("-" * 60) for name in pages: data = PAGE_DATA.get(name) if not data: print(f" 未知页面: {name}") continue dims = data["dims"] steps = data["steps"] if dims > 1: # 多维度页面:检查每个维度子目录 dim_list = BOARD_COACH_DIMS if name == "board-coach" else BOARD_CUSTOMER_DIMS for dim in dim_list: out_dir = page_output_dir(name, dim) h5_count = len(list(out_dir.glob("h5--step-*.png"))) if out_dir.exists() else 0 mp_count = len(list(out_dir.glob("mp--step-*.png"))) if out_dir.exists() else 0 diff_count = len(list(out_dir.glob("diff--step-*.png"))) if out_dir.exists() else 0 print(f" {name}/{dim:<22} {steps:>4} {1:>4} {h5_count:>4} {mp_count:>4} {diff_count:>4}") else: out_dir = page_output_dir(name) h5_count = len(list(out_dir.glob("h5--step-*.png"))) if out_dir.exists() else 0 mp_count = len(list(out_dir.glob("mp--step-*.png"))) if out_dir.exists() else 0 diff_count = len(list(out_dir.glob("diff--step-*.png"))) if out_dir.exists() else 0 print(f" {name:<30} {steps:>4} {dims:>4} {h5_count:>4} {mp_count:>4} {diff_count:>4}") def cmd_list(): """列出所有页面及参数""" total_units = 0 print(f"\n{'#':>2} {'页面':<30} {'scrollH':>7} {'maxScr':>7} {'步数':>4} {'维度':>4} {'单元':>4}") print("-" * 70) for i, (name, data) in enumerate(PAGE_DATA.items(), 1): units = data["steps"] * data["dims"] total_units += units seq = compute_scroll_sequence(data["scrollHeight"]) print(f"{i:>2} {name:<30} {data['scrollHeight']:>7} {data['maxScroll']:>7} " f"{data['steps']:>4} {data['dims']:>4} {units:>4}") print("-" * 70) print(f" {'合计':<30} {'':>7} {'':>7} {'':>4} {'':>4} {total_units:>4}") def cmd_report(page_name: str): """读取已有 diff 图的差异率数据,生成/更新 report.md""" out_dir = page_output_dir(page_name) report_path = out_dir / "report.md" if not out_dir.exists(): print(f"目录不存在: {out_dir.relative_to(ROOT)}") return # 查找 diff 图 diff_files = sorted(out_dir.glob("diff--step-*.png")) if not diff_files: print(f"未找到 diff 图: {out_dir.relative_to(ROOT)}/diff--step-*.png") return print(f"\n差异率报告: {page_name}") print(f"diff 图数量: {len(diff_files)}") print(f"报告路径: {report_path.relative_to(ROOT)}") print(f"\n注意:差异率数据需要从 image_compare MCP 的输出中手动填入") def print_usage(): print(""" 用法: python scripts/ops/anchor_compare.py [args] 命令: calc 计算 scrollTop 序列 status [] 查看截图状态(不指定页面则显示全部) report 生成差异率报告框架 list 列出所有页面及参数 截图和对比通过 MCP 工具在对话中执行: H5 截图 → Playwright MCP MP 截图 → 微信开发者工具 MCP 像素对比 → image_compare MCP 示例: python scripts/ops/anchor_compare.py calc 5600 python scripts/ops/anchor_compare.py status board-finance python scripts/ops/anchor_compare.py list """) def main(): _ensure_utf8_stdio() if len(sys.argv) < 2: print_usage() sys.exit(1) command = sys.argv[1] if command in ("--help", "-h"): print_usage() return if command == "list": cmd_list() elif command == "calc": if len(sys.argv) < 3: print("缺少 scrollHeight 参数") sys.exit(1) cmd_calc(int(sys.argv[2])) elif command == "status": page = sys.argv[2] if len(sys.argv) >= 3 else None cmd_status(page) elif command == "report": if len(sys.argv) < 3: print("缺少 page 参数") sys.exit(1) cmd_report(sys.argv[2]) else: print(f"未知命令: {command}") print_usage() sys.exit(1) if __name__ == "__main__": main()