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

278 lines
11 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.
"""
固定步长滚动截图辅助工具v3 — 纯参数计算 + 文件管理)。
本脚本不执行截图或对比,仅提供:
1. scrollTop 序列计算(给定 scrollHeight → 步数和序列)
2. 文件命名和目录结构管理
3. 差异率汇总报告生成
实际截图和对比通过 MCP 工具在对话中执行:
- H5 截图Playwright MCPbrowser_navigate → browser_evaluate → browser_take_screenshot
- MP 截图:微信 MCPnavigate_to → evaluate_script → screenshot
- 像素对比image_compare MCPcompare_images
用法:
python scripts/ops/anchor_compare.py calc <scrollHeight> # 计算 scrollTop 序列
python scripts/ops/anchor_compare.py status [<page>] # 查看截图状态
python scripts/ops/anchor_compare.py report <page> # 生成/更新差异率报告
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 §52026-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 <command> [args]
命令:
calc <scrollHeight> 计算 scrollTop 序列
status [<page>] 查看截图状态(不指定页面则显示全部)
report <page> 生成差异率报告框架
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()