Files
aerologic-app/.claude/skills/phone-adapt/scripts/design_shots.py
YANG JIANKUAN db062c8ee3 feat: 完成国际出港移库/出库交接页面5.5寸手机端适配
- 落地手机端双形态适配框架:同名布局+资源限定符(layout/手机、
  layout-sw600dp/平板)自动切换,DeviceUtil 设备形态判定,7个
  手机版公共组件(PhoneSearchBar/StatusTab/DataLayout/FilterPanel/
  BottomBar/StatBox/KvItem)
- 完成「国际出港移库」「国际出港出库交接」两页手机端适配,新增
  各自详情页(IntExpMoveDetailActivity/IntExpOutHandoverDetailActivity)
- 手机端首页菜单接入"出港移库""出库交接"入口
- 修复手机端进入页面先横屏后转竖屏的闪屏问题:Manifest 全项目
  192 处改为 screenOrientation="unspecified",由 BaseActivity
  按设备形态运行时锁定方向
- 修复该方案的中间版本在平板端引入的回归(先竖后横 + UI放大1.6倍)
- AutoSize 补充手机竖屏 390×844 设计基准
- 修复 CHANGELOG.md 因脚本异常导致的内容重复损坏(膨胀至12万行),
  恢复正常结构并补充本次变更记录

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 14:18:29 +08:00

138 lines
5.2 KiB
Python
Executable File
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.

#!/usr/bin/env python3
"""
把 5.5 寸手持端设计稿HTML 原型)渲染成截图,供逐像素比对。
设计稿是带 Tailwind + JS 的交互原型列表页、Tab、详情页、筛选弹层往往挤在同一个
HTML 里,靠 JS 函数切换。只读源码容易漏掉视觉细节(间距、圆角、真实配色、
以及 JS 里 id 映射写错导致的字段错位),所以务必渲染成图来看。
用法:
# 1) 先列出 HTML 里可用的状态切换函数与元素 id据此决定要截哪些状态
python3 design_shots.py --html 出库交接.html --list
# 2) 渲染:默认状态 + 注入 JS 触发的各个状态
python3 design_shots.py --html 出库交接.html --out /tmp/shots \
--state "01_列表:" \
--state "02_已交接Tab:switchTab('shipped');" \
--state "03_详情:showDetail({id:'PMC1',status:'已交接'});" \
--state "04_筛选弹层:toggleFilterDrawer(true);"
每个 --state 形如 "名称:JS"JS 可为空表示页面初始态。
输出 <out>/<名称>.png同时保留注入后的 <名称>.html 便于复查。
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import urllib.parse
CHROME_CANDIDATES = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
shutil.which("google-chrome") or "",
shutil.which("chromium") or "",
shutil.which("chromium-browser") or "",
]
def find_chrome() -> str:
for path in CHROME_CANDIDATES:
if path and os.path.exists(path):
return path
sys.exit(
"找不到 Chrome/Chromium。请安装或用 --chrome 指定可执行文件路径。"
)
def list_hooks(html: str) -> None:
"""列出可用于构造状态的 JS 函数和元素 id。"""
funcs = sorted(set(re.findall(r"function\s+([A-Za-z_$][\w$]*)\s*\(", html)))
onclicks = sorted(set(re.findall(r'onclick="([^"]+)"', html)))
ids = sorted(set(re.findall(r'id="([^"]+)"', html)))
print("== JS 函数(可直接注入调用)==")
for f in funcs:
print(f" {f}()")
print("\n== onclick 表达式(含真实参数,照抄最省事)==")
for o in onclicks[:40]:
print(f" {o}")
if len(onclicks) > 40:
print(f" ... 其余 {len(onclicks) - 40} 条略")
print("\n== 元素 id判断有哪些页面/弹层)==")
print(" " + ", ".join(ids))
def render(chrome: str, html: str, name: str, js: str, out_dir: str,
width: int, height: int, scale: float, budget: int) -> str:
# 等 load 之后再触发,确保 CDN 的 Tailwind/Iconify 已生效
inject = (
"<script>window.addEventListener('load',function(){"
f"setTimeout(function(){{{js}}},300);}});</script>"
)
page_path = os.path.join(out_dir, f"{name}.html")
with open(page_path, "w", encoding="utf-8") as fp:
fp.write(html.replace("</body>", inject + "</body>"))
png_path = os.path.join(out_dir, f"{name}.png")
subprocess.run(
[
chrome,
"--headless=new",
"--disable-gpu",
"--hide-scrollbars",
f"--force-device-scale-factor={scale}",
f"--window-size={width},{height}",
f"--virtual-time-budget={budget}",
f"--screenshot={png_path}",
"file://" + urllib.parse.quote(os.path.abspath(page_path)),
],
capture_output=True,
)
return png_path
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--html", required=True, help="设计稿 HTML 路径")
ap.add_argument("--out", default="/tmp/design_shots", help="输出目录")
ap.add_argument("--state", action="append", default=[],
help='形如 "名称:JS"可重复JS 为空表示初始态')
ap.add_argument("--list", action="store_true",
help="只列出可用的 JS 函数 / onclick / id不截图")
ap.add_argument("--chrome", default="", help="Chrome 可执行文件路径")
# 原型容器一般是 375×812 的手机壳,留出边距后 460×900 刚好完整收进去
ap.add_argument("--width", type=int, default=460)
ap.add_argument("--height", type=int, default=900)
ap.add_argument("--scale", type=float, default=2.0, help="缩放倍数2 便于看清细节")
ap.add_argument("--budget", type=int, default=9000,
help="虚拟时间预算(ms),需覆盖 CDN 脚本加载")
args = ap.parse_args()
with open(args.html, encoding="utf-8") as fp:
html = fp.read()
if args.list:
list_hooks(html)
return
chrome = args.chrome or find_chrome()
os.makedirs(args.out, exist_ok=True)
states = args.state or ["01_default:"]
for spec in states:
name, _, js = spec.partition(":")
png = render(chrome, html, name.strip(), js, args.out,
args.width, args.height, args.scale, args.budget)
ok = os.path.exists(png) and os.path.getsize(png) > 0
size = os.path.getsize(png) if ok else 0
print(f"{'' if ok else ''} {name.strip():<24} {png} ({size} bytes)")
print(f"\n用 Read 工具逐张查看 {args.out}/*.png再开始写布局。")
if __name__ == "__main__":
main()