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>
This commit is contained in:
137
.claude/skills/phone-adapt/scripts/design_shots.py
Executable file
137
.claude/skills/phone-adapt/scripts/design_shots.py
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/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()
|
||||
119
.claude/skills/phone-adapt/scripts/ui_probe.sh
Executable file
119
.claude/skills/phone-adapt/scripts/ui_probe.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# 实机验证辅助:读取界面文字 / 按文字点击 / 截图 / 抓请求体。
|
||||
#
|
||||
# 为什么需要它:手机端验证要反复点 Tab、弹层按钮、卡片链接。靠肉眼估坐标点击
|
||||
# 极易点空(本次适配就因此白跑了几轮),而 uiautomator 能拿到元素真实 bounds。
|
||||
# tap-text 直接按文字定位并点中心,比手算坐标可靠得多。
|
||||
#
|
||||
# 用法:
|
||||
# ui_probe.sh <serial> texts 列出当前界面所有可见文字
|
||||
# ui_probe.sh <serial> bounds "已交接" 打印该文字元素的 bounds 与中心点
|
||||
# ui_probe.sh <serial> tap-text "确认" [序号] 按文字点击(取中心点);同名多处时默认第 1 处
|
||||
# ui_probe.sh <serial> wait-text "待交接" [秒] 等文字出现(默认 20 秒),点击前先等页面就绪
|
||||
# ui_probe.sh <serial> wait-gone "请稍候……" [秒] 等文字消失(如等加载弹窗关闭)
|
||||
# ui_probe.sh <serial> shot /tmp/a.png 截图到本地
|
||||
# ui_probe.sh <serial> req <关键字> 从 logcat 抓含关键字的请求/响应体
|
||||
# ui_probe.sh <serial> crash 统计并打印最近一次崩溃栈
|
||||
# ui_probe.sh <serial> form 打印形态识别结果 PHONE/TABLET
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SERIAL="${1:?用法: ui_probe.sh <serial> <命令> [参数]}"
|
||||
CMD="${2:?缺少命令}"
|
||||
ARG="${3:-}"
|
||||
PKG="com.lukouguoji.aerologic"
|
||||
A="adb -s $SERIAL"
|
||||
|
||||
dump() {
|
||||
$A shell uiautomator dump /sdcard/_probe.xml >/dev/null 2>&1
|
||||
# 注意:dump 出来的 XML 是一整行,必须按 '<' 拆成一节点一行才能按节点过滤。
|
||||
# (`tr '>' '>\n'` 是字符映射不是字符串替换,等于空操作,别踩这个坑。)
|
||||
$A shell cat /sdcard/_probe.xml | tr '<' '\n'
|
||||
}
|
||||
|
||||
# 打印所有 text 完全等于 $1 的节点的 bounds 与中心点
|
||||
centers_of() {
|
||||
dump | grep -F "text=\"$1\"" \
|
||||
| grep -oE 'bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' \
|
||||
| sed -E 's/bounds="\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\]"/\1 \2 \3 \4/' \
|
||||
| awk '{printf "bounds=[%d,%d][%d,%d] center=%d,%d\n", $1,$2,$3,$4, ($1+$3)/2, ($2+$4)/2}'
|
||||
}
|
||||
|
||||
case "$CMD" in
|
||||
texts)
|
||||
dump | tr '<' '\n' | grep -oE 'text="[^"]+"' | sed 's/text=//;s/"//g' | awk '!seen[$0]++'
|
||||
;;
|
||||
|
||||
bounds)
|
||||
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
|
||||
centers_of "$ARG"
|
||||
;;
|
||||
|
||||
tap-text)
|
||||
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
|
||||
idx="${4:-1}"
|
||||
all=$(centers_of "$ARG")
|
||||
[ -z "$all" ] && { echo "界面上找不到文字:$ARG"; exit 1; }
|
||||
total=$(echo "$all" | wc -l | tr -d ' ')
|
||||
# 同一文字可能在多处出现(如底部 Tab「国际」与列表里的「国际」),
|
||||
# 默认点第 1 个;命中多个时提示,必要时用第 4 个参数指定序号。
|
||||
if [ "$total" -gt 1 ]; then
|
||||
echo "注意:${ARG} 命中 $total 处,正在点第 $idx 处(可用第 4 个参数指定序号)"
|
||||
echo "$all" | nl -w2 -s' '
|
||||
fi
|
||||
line=$(echo "$all" | sed -n "${idx}p")
|
||||
[ -z "$line" ] && { echo "序号 $idx 超出范围(共 $total 处)"; exit 1; }
|
||||
cx=${line#*center=}; cx=${cx%%,*}
|
||||
cy=${line##*,}
|
||||
# 变量必须写成 ${ARG}:紧跟中文标点时 $ARG 会被当成变量名的一部分
|
||||
echo "tap ${ARG} @ $cx,$cy"
|
||||
$A shell input tap "$cx" "$cy"
|
||||
;;
|
||||
|
||||
# 等某段文字出现 / 消失。点击前先等页面就绪很重要:
|
||||
# 加载弹窗("请稍候……")会盖住整屏并吞掉 input tap,导致点击"静默失效"。
|
||||
wait-text)
|
||||
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
|
||||
for _ in $(seq 1 "${4:-20}"); do
|
||||
if dump | grep -qF "text=\"$ARG\""; then echo "出现: ${ARG}"; exit 0; fi
|
||||
sleep 1
|
||||
done
|
||||
echo "超时未出现: ${ARG}"; exit 1
|
||||
;;
|
||||
|
||||
wait-gone)
|
||||
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
|
||||
for _ in $(seq 1 "${4:-20}"); do
|
||||
if ! dump | grep -qF "text=\"$ARG\""; then echo "已消失: ${ARG}"; exit 0; fi
|
||||
sleep 1
|
||||
done
|
||||
echo "超时仍存在: ${ARG}"; exit 1
|
||||
;;
|
||||
|
||||
shot)
|
||||
out="${ARG:-/tmp/shot.png}"
|
||||
$A exec-out screencap -p > "$out"
|
||||
echo "$out ($(wc -c < "$out") bytes)"
|
||||
;;
|
||||
|
||||
req)
|
||||
# OkHttp 日志是多行 pretty JSON,用 -v raw 拿干净文本再按关键字取上下文
|
||||
$A logcat -d -v raw | grep -B24 -A24 -- "${ARG:-POST}" | grep -E '^\s*"|POST|<-- [0-9]{3}' | tail -60
|
||||
;;
|
||||
|
||||
crash)
|
||||
n=$($A logcat -d | grep -c "FATAL EXCEPTION" || true)
|
||||
echo "FATAL EXCEPTION 次数: $n"
|
||||
if [ "$n" -gt 0 ]; then
|
||||
$A logcat -d | grep -A25 "FATAL EXCEPTION" | tail -30
|
||||
fi
|
||||
;;
|
||||
|
||||
form)
|
||||
$A logcat -d -s BaseActivity:D | grep -oE "(PHONE|TABLET)\(sw=[0-9]+dp[^)]*\)" | tail -3
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "未知命令: $CMD"; exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user