---
name: phone-adapt
description: 参照 documents/5.5寸手持端设计文件/ 的 HTML 设计稿，把 AirLogistics 指定页面适配成 5.5 寸手机竖屏版本——浏览器渲染设计稿逐态截图分析、复用 Phone* 公共组件写手机布局、复用现有 ViewModel 与接口、保证 Pad 端零改动，最后走完整登录流程实机联调并与设计稿逐项核对 UI。当用户说"适配手机端/做手机版/手持端适配"、点名某个设计稿入口（如"02 出库交接"、"做一下出港计重"）、要求"按设计稿还原页面"、或提到 5.5 寸/手机版/双形态时，都要使用本 skill，即使没明说"适配"二字。
---

# 5.5 寸手持端页面适配

把一个已有的平板页面，按设计稿适配出手机竖屏版本。核心约束是**只重写布局，业务逻辑与接口全部复用**，
Pad 端行为必须零变化。

先读 `CLAUDE.md` 的「5.5 寸手持端（手机版）双形态适配规范」章节——那里是权威规范，本 skill 是执行流程。

## 前置确认

需要用户给出**目标页面**（设计稿目录名或业务名，如「02 出库交接」/「出港计重」）。
其余自己查：`documents/5.5寸手持端设计文件/00_全部入口总览.html` 列出全部 11 个入口，
每个子目录下可能有多个 HTML（主页 + 详情 + 子页面），都要处理。

对应的平板页面靠业务名在 `module_gjc/gjj/gnc/gnj` 里搜 Activity。**注意认准首页菜单实际跳转的那个
Activity**，同名旧版文件在这个仓库里很常见。

## 阶段 1：渲染设计稿，看图不看代码

设计稿是带 Tailwind + JS 的交互原型，一个 HTML 里常塞了列表页、Tab、详情页、筛选弹层，靠 JS 切换。

**只读 HTML 源码会出错**，必须渲染成图：

```bash
S=.claude/skills/phone-adapt/scripts
# 1) 先看有哪些状态可切
python3 $S/design_shots.py --html "documents/5.5寸手持端设计文件/02_出库交接/出库交接.html" --list

# 2) 按 --list 结果逐态渲染（照抄 onclick 里的真实参数最省事）
python3 $S/design_shots.py --html "…/出库交接.html" --out /tmp/design \
  --state "01_列表:" \
  --state "02_已交接Tab:switchTab('shipped');" \
  --state "03_详情:showDetail({id:'PMC90901CZ',status:'已交接',rackNo:'076'});" \
  --state "04_筛选弹层:toggleFilterDrawer(true);"
```

然后用 Read 工具**逐张看图**，抽取：配色、字号、圆角、间距、图标、控件顺序、空态、
各状态下哪些元素显示/隐藏（例如「已交接」Tab 下底部操作条整条消失）。

`--list` 输出里如果出现 `window.location.href='xxx.html'`，说明是多文件原型，那些文件同样要渲染。

**设计稿的 JS 可能有 bug，别照抄字段映射。** 出库交接的详情页就把「航班日期」绑成了重量、
「备注」绑成了航班号（HTML 里 id 重复）。判断依据是**静态 label 文案**：label 写「航班日期」
就绑 `fdate`，写「板型」就绑 `boardType`。label 是设计意图，JS 只是 demo 接线。

标签文案同理：卡片上写死的「IMP代码」是占位符，实际要绑 `bean.dgrCode` 的值。

## 阶段 2：对齐接口能力，不要臆想

把设计稿的每个筛选项/Tab/字段，对照平板 ViewModel 已有的字段：

- **已有字段** → 直接复用（如出港移库的 Tab 直接绑既有 `moveState`）
- **平板没有的新维度**（新 Tab 状态、新下拉、新排序）→ **先查 api-doc MCP 拿权威参数名**，
  流程见 `references/api-doc.md`（连接方式 + 5 个工具 + 逐接口核对清单）

**最高决策规则（用户 2026-07-29 拍板，此后不再逐次询问）：业务改动完全以设计稿为准，不做妥协。**
接口不支持设计稿的某个维度时：UI 照设计稿完整实现（绑预留字段/占位，传参后端静默忽略无副作用），
缺口直接登记 `documents/后端接口对接问题清单.xlsx` 提修改需求，后端补齐后前端自动生效。
仅当设计稿本身歧义（label 无法推断字段语义）才用 `AskUserQuestion` 问用户。

这一步不能猜，也**不要拿「实测不生效」当结论**。出库交接第一版猜了 `hoState`，实测两个 Tab
返回同一批数据，一度误判为「后端不支持」；后查 api-doc 才发现正确参数名是 `handoverState`——
后端对不认识的参数是**静默忽略**，不报错，实测只能证明「这个名字不对」，证不出「后端没这能力」。

「参数是否真的生效」的 A/B 验证优先走 Proxyman MCP（`create_compose_http_from_flow` 复制
真实请求改参重发，token 自动带上），分工与流程见 `references/proxyman.md`：
**api-doc 管契约（该有什么），Proxyman 管事实（实际发/回了什么）**。

核对完把结论落两处（模板与格式见 `references/api-doc.md`）：

- 后端真实缺口（缺入参/缺出参/数据不回填）→ 记入 `documents/后端接口对接问题清单.xlsx`，
  这是与后端对接的正式文档，按「模块 → 页面 → 接口定义 → 接口地址」索引，**每个页面适配完都要维护**
- 参数名/字段绑定错误这类前端问题 → 直接改代码

阶段 5 仍要用真实请求体验证新参数确实生效；没生效就在清单文档与交付说明里如实标注，
不要让它看起来是通的。

### 开工前对齐方案

分析完先把结论摊给用户，避免闷头做错方向。一次说清，别挤牙膏：

```
📱 页面适配方案：<页面名>

设计稿：documents/5.5寸手持端设计文件/<目录>/（N 个 HTML：主页 / 详情 / …）
平板页面：XxxActivity + XxxViewModel（module_xxx）

手机版结构：
  搜索行：<字段>（含扫码/筛选按钮）
  状态 Tab：<Tab1> / <Tab2>  → 绑定 <字段名>
  列表卡片：<字段…>，两种形态（<形态A> / <形态B>）
  底部操作条：全选 + 已选统计 + [<按钮>]
  筛选弹层：<字段1> / <字段2> / <字段3>
  二级页：<详情页名>（新增 Activity / 复用已有）

复用组件：PhoneSearchBar、PhoneStatusTab、PhoneStatBox…（无需新增 / 需新增 XXX）

接口：列表 <接口> 复用；新增参数 <名>（待你确认）；新增字典 <DictUtils 方法>
Pad 端影响：无（新字段默认空、额外请求由手机开关控制）

设计稿与平板的差异：<如手机版无「配运」按钮>

确认后开始编码。
```

## 阶段 3：写布局，复用公共组件

`module_base/.../ui/weight/phone/` 已有 7 个组件，**优先复用，不要重写 XML**。
组件清单、属性表与用法见 `references/components.md`（写布局前读一遍）。

改造步骤：

1. 平板布局 `git mv` 到 `res/layout-sw600dp/`（item 布局同理）
2. 在 `res/layout/` 下建**同名**文件写手机布局
3. Kotlin 侧 `layoutId()` / `itemLayoutId` **一律不动**，不做设备分支

三条硬约束（违反会编译失败或运行时崩溃，细节见 `references/pad-safety.md`）：

| 约束 | 说明 |
|------|------|
| 两变体 `<variable>` 完全一致 | 手机变体新增 `activity` 变量时，平板变体也要补上（写注释说明平板不用它） |
| 共用 id 双方都要有 | ViewHolder/Activity 引用的 id（`srl`/`rv`/`iv_icon` 等）两边都保留 |
| 单侧独有 id 自动 `@Nullable` | 如手机独有的 `ll_detail`，ViewHolder 里必须判空调用 |

**一套 item 布局覆盖多种卡片形态**，不要为「待交接/已交接」建两个布局。按 bean 状态切
visibility 即可（Bean 上加个只读计算属性如 `isHandovered` 让 XML 更干净；计算属性不参与 Gson
序列化，安全）。

## 阶段 4：接业务，且不碰 Pad

手机端新增的查询维度用这个模式，能做到平板端请求次数与行为零变化：

```kotlin
// ViewModel：新字段默认空值，getData() 里仅非空才进请求
val handoverState = MutableLiveData("")   // 平板布局无对应控件 → 恒为空 → 不进请求
val filterParams = mapOf(
    /* 原有字段… */
    "handoverState" to handoverState.value?.ifEmpty { null },   // 参数名以 api-doc 为准
)

// 手机专属的额外请求（Tab 角标计数、新下拉字典）用内部开关控制
private var phoneExtrasEnabled = false
fun initPhoneExtras() { phoneExtrasEnabled = true; handoverState.value = "0"; /* 加载字典 */ }
```

```kotlin
// Activity：只有手机形态才开启，平板不调用
if (DeviceUtil.isPhone()) viewModel.initPhoneExtras()
```

设备判断只用于**数据默认值与额外请求**，**绝不用于选布局**（选布局靠资源限定符）。

纯 UI 交互（弹层显隐、Tab 切换）写在 Activity，用 `ObservableBoolean` 暴露给 XML，不下沉到 ViewModel。

设计稿若少了平板上的某个按钮（如出库交接手机版只有「交接」没有「配运」），按设计稿做，
但要在交付说明里点出来这个功能在手机端不可达，让用户决定。

新增手机专属页面（详情页等）记得：`app/src/main/AndroidManifest.xml` 注册 +
`app/src/debug/AndroidManifest.xml` 追加 `exported="true"` 便于直启调试。

注册时 `screenOrientation` **必须写 `unspecified`**：

```xml
<activity android:name="…"
    android:configChanges="orientation|keyboardHidden"
    android:exported="false"
    android:screenOrientation="unspecified" />
```

- 写死 `userLandscape` → 手机端「先横屏、约 1 秒后转竖屏」闪一下（方向在建窗口时就定了，代码再纠正已晚）
- 用 `@integer/xxx` 资源引用 → **更糟**：系统解析 Manifest 不套用设备限定符，平板会取到默认（竖屏）值，
  先竖后横，且 AutoSize 随之选错基准导致 UI 放大约 1.6 倍

`unspecified` 让系统按设备当前物理方向建窗口，两端起始方向本就正确，
`BaseActivity` 的形态锁定不会产生二次纠正。

### ⚠️ 别忘了加手机端首页入口

手机端首页是 `module_p` 的 `PDAEnterActivity`，「国际」Tab 的菜单在
`module_p/.../ui/enter/gj/GjViewModel.kt`（国内在 `gn/GnViewModel.kt`）。
**菜单是手写清单，不是接口下发**；权限串只做过滤（`authList.contains(bean.key)`）。

适配完页面不回来加一行，手机上就**根本没有入口**——出库交接第一版就漏了这步，
只能靠 adb 直启才能进去。加法：

```kotlin
ActionBean(
    "出港移库",
    R.drawable.gjc_yi_ku_icon,                   // 图标复用 Pad 同款，复制 PNG 到 module_p/res/drawable-xxhdpi/
    Constant.AuthName.GjcYiKuListActivity,       // 权限串与 Pad 端菜单一致
    ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE   // 填了 route 就走通用跳转，无需 when 分支
),
```

权限串和路由**都以 Pad 端 `app/.../HomeFragment.kt` 里该权限对应的 `ARouter.build(...)` 为准**
（同一业务常有新旧两个 Activity，Pad 菜单实际跳的才是要适配的那个）。
`module_p` 只依赖 `module_base`，看不到业务模块，所以跨模块必须走 ARouter、图标必须复制一份。

菜单没出现时先分清是「没权限」还是「没加清单」：

```bash
adb -s <serial> shell "run-as com.lukouguoji.aerologic \
  cat /data/data/com.lukouguoji.aerologic/shared_prefs/data.xml" | tr ',' '\n' | grep -oE "App[A-Za-z]+" | sort -u
```

## 阶段 5：端到端实机验证

先构建，再双端跑。命令与排障细节见 `references/verification.md`。

```bash
./gradlew assembleDebug 2>&1 | grep -E "^(BUILD|FAILURE)|error:"
```

构建过后核对 DataBinding 变体是否如预期（单侧 id 应为 `@Nullable`）：

```bash
grep -B3 "llDetail" module_gjc/build/generated/data_binding_base_class_source_out/debug/out/\
com/lukouguoji/gjc/databinding/ItemXxxBinding.java
```

**手机端**（真实数据链路，从登录开始）：

```bash
P=.claude/skills/phone-adapt/scripts/ui_probe.sh
adb -s <phone> install -r -d app/build/outputs/apk/debug/app-debug.apk
# 走登录 → 底部「国际」Tab → 点新加的菜单项进入（顺便验证入口加对了）
# 同名文字多处时 tap-text 会提示命中数，用第 4 个参数指定序号（如底部 Tab 的「国际」）
# 若账号确实没有该权限，改用冷启动直启（见 verification.md）
$P <phone> form                 # 应打印 PHONE(sw=411dp…)
$P <phone> wait-text "待交接"    # 点击前先等页面就绪：加载弹窗会吞掉点击
$P <phone> tap-text "已交接"     # 按文字点击，比手算坐标可靠
$P <phone> texts                # 用文字变化断言点击确实生效（如底部条应消失）
$P <phone> shot /tmp/p1.png
$P <phone> crash                # 必须为 0
```

核对请求/响应体优先用 Proxyman MCP（`filter_flows` + `get_flow_detail`，比 `req` 的
logcat 抓包完整可靠；模拟器代理一行命令接入，见 `references/proxyman.md`）；
Proxyman 不可用时兜底 `$P <phone> req pageQuery`。

逐项走查：列表/卡片两种形态、详情页、筛选弹层（重置+确认）、Tab 切换、单选与全选联动、
下拉真实取数、扫码入口。每步 `crash` 都要为 0。

点完**一定要断言效果**（`texts` 或截图），不要假设点中了——加载弹窗盖屏时 `input tap` 会被静默吞掉。

手机端还要确认方向没闪屏（冷启动采样 `mRotation` 应恒为 0），命令见 `references/verification.md`。

**Pad 端回归**（必做）：同样安装到平板模拟器打开同一页面截图，与改造前逐项对比——
搜索条数量、默认值、按钮、底部统计都应一致。改过 `module_base` 公共组件时这步尤其不能省。

## 阶段 6：与设计稿二次核对

把阶段 1 的设计稿截图和阶段 5 的实机截图放在一起，用 Read 工具对照看，逐项核对：

配色 / 字号 / 字重 / 圆角 / 内外间距 / 图标形状与大小 / 控件顺序 / 选中态 / 状态标签配色 /
**空值兜底**（真实数据空字段很常见——出库交接就出现过「库位」为空时橙色高亮标签只剩一个孤零零的
小色块，需要空值时不套标签底）。

服务端当天没数据时（列表空）先放宽条件重查；仍为空、或要覆盖真实数据凑不出的形态
（空字段、超长文本、罕见状态标签），**首选 Proxyman Map Local 造 mock**——零代码改动、
零重构建，见 `references/proxyman.md` 场景 3（2026-07-31 出港查询用它一次验完
已离港/已入库/未入库三种卡片形态）。验证完 `delete_rule` + 清模拟器代理。

Proxyman 不可用时才退回老办法：Activity 里临时注入样例 Bean 截图比对：

```kotlin
// import com.lukouguoji.module_base.ktx.commonAdapter
binding.rv.postDelayed({
    binding.rv.commonAdapter()?.refresh(listOf(/* 覆盖每种卡片形态各一条 */))
}, 2500)   // 等首次请求结束，否则会被真实结果覆盖
```

**核对完必须删掉**，删完重新构建并 grep 确认无残留。

## 收尾：文档与交付说明

代码之外还要落四处文档，否则下一个人会重复踩坑：

- 新增了公共组件/资源 → 更新 `CLAUDE.md`「手机版公共组件库」表 + `references/components.md`
- 踩到新坑（崩溃、静默失效、环境问题）→ 写进 `CLAUDE.md`「常见编译错误速查」和本 skill 的
  对应 reference，把**定位手法**也写上，不只写结论
- 接口有后端缺口（阶段 2 / 阶段 5 发现的）→ 维护 `documents/后端接口对接问题清单.xlsx`；
  已确认的字段语义追加到 `references/api-doc.md` 的「已核对过的语义」，别让下个页面再查一遍
- `CHANGELOG.md` 追加条目（新增 / 改进 / 修复 / 验证 四段），记忆文件
  `phone-5.5inch-adaptation.md` 同步

向用户汇报时明确写出这四块：

1. 新增/修改的文件清单（布局、组件、Activity/ViewModel/Bean）
2. Pad 端零破坏的具体做法 + 回归结论
3. 复用与新增的公共组件
4. **待确认事项**：后端未生效的参数、自己推断的业务规则（如 IMP 代码红/绿的判定）、
   设计稿与平板功能的差异。如实说，别含糊。

## 参考文件

- `references/api-doc.md` — api-doc MCP 连接与逐接口核对流程、后端缺口记录规范、已核对字段语义（阶段 2 前读）
- `references/proxyman.md` — Proxyman MCP 抓包/直调 A/B/Map Local mock 的接入与四个场景（阶段 2、5、6 用；与 api-doc 的分工：契约 vs 事实）
- `references/components.md` — 7 个手机版公共组件的属性表、用法与封装新组件的规矩（写布局前读）
- `references/pad-safety.md` — 布局变体约束、Pad 零破坏模式、易崩点（写代码前读）
- `references/verification.md` — 双端 adb 命令、登录与直启、已知环境坑（验证前读）
- `scripts/design_shots.py` — 设计稿逐态渲染截图
- `scripts/ui_probe.sh` — 实机取文字/按文字点击/等待/截图/抓请求体/查崩溃
