278 lines
11 KiB
Python
278 lines
11 KiB
Python
"""
|
||
固定步长滚动截图辅助工具(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 <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 §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 <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()
|