merge: 手机适配分支 feature/phone-ui-adaptation 合入 main,平板与 5.5 寸手机统一在主干开发
合并原则:业务层以 main 平板为准,手机端只做 UI 适配。 - 冲突布局:平板改动三方合并进 layout-sw600dp/,手机布局保留并补齐主干新增业务 - Bean/ViewModel 冲突取平板业务逻辑(计重托盘联动、开始计重运抵重量自动计算、 出港查询 dateState、仓库入/出库件数重量、清仓备注等),手机仅保留展示字段 - 主干新功能同步到手机:运抵海关放行/分单号筛选/回执走 getResponse、删除申报; 出库交接装机状态与允许装机/配运;出港移库详情与编辑共用 IntExpMoveEditActivity; 出入港仓库清仓备注、出库弹框件数重量、进港仓库 pcLabel;ULD 所在港/位置 - 新增手机变体:出库/入库/清仓/配运/删除申报弹框、移库详情编辑页、海关回执页; PhoneBottomBar 增次操作按钮 - 手机对齐平板:去掉 likeNo 与平板没有的筛选入参、恢复平板默认查询日期, 补齐平板表单/筛选项与操作入口;移除手机专属 IntExpMoveDetailActivity - CLAUDE.md 记录单分支约定与「业务以平板为准」规则,CHANGELOG 补合并记录 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
432
CLAUDE.md
432
CLAUDE.md
@@ -846,6 +846,396 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
|
||||
|
||||
---
|
||||
|
||||
## 5.5 寸手持端(手机版)双形态适配规范
|
||||
|
||||
项目需同时支持 **10 寸平板横屏** 与 **5.5 寸手机竖屏**。核心原则:**业务逻辑层完全复用,只重写布局**。
|
||||
|
||||
> **分支约定(2026-10-11 起)**:手机适配分支 `feature/phone-ui-adaptation` 已合并入 `main`,此后平板与手机**只在 `main` 上同时开发**,不再区分分支。
|
||||
> **业务以平板为准**:主流程交互、表单/筛选项、接口入参一律与平板一致,手机端只做 UI 适配——平板底部批量按钮在手机上可改为卡片级操作或 `PhoneBottomBar` 次按钮(`subAction1Text/subAction2Text`),
|
||||
> 但不得新增平板没有的查询入参(如 `likeNo`)、不得改写平板的默认查询值(如默认当天日期)、不得少平板的表单项。
|
||||
> 平板新增/修改功能时,必须同步检查对应手机布局与该页全部弹框的手机变体。
|
||||
|
||||
### 核心机制:布局按资源限定符自动切换
|
||||
|
||||
```
|
||||
res/layout-sw600dp/activity_xxx.xml ← 平板(最小宽度 >= 600dp)
|
||||
res/layout/activity_xxx.xml ← 手机(兜底,5.5 寸约 360~411dp)
|
||||
```
|
||||
|
||||
两个变体**同名**,DataBinding 会生成同一个 Binding 基类 + 两个变体实现:
|
||||
|
||||
```
|
||||
ActivityXxxBinding.java 公共基类(Activity 中引用的就是它)
|
||||
ActivityXxxBindingImpl.java 手机变体
|
||||
ActivityXxxBindingSw600dpImpl.java 平板变体
|
||||
```
|
||||
|
||||
因此 **Activity / ViewModel / ViewHolder 的 `layoutId()`、`itemLayoutId` 全部保持原样,无需任何设备判断**。
|
||||
|
||||
### ⛔ 严禁:用「不同文件名 + 代码判断」切换布局
|
||||
|
||||
```kotlin
|
||||
// ❌ 错误:会生成两个不同的 Binding 类
|
||||
override fun layoutId() =
|
||||
if (DeviceUtil.isPhone()) R.layout.activity_xxx_phone else R.layout.activity_xxx
|
||||
```
|
||||
|
||||
`activity_xxx_phone.xml` 生成的是 `ActivityXxxPhoneBinding`,而 `binding` 字段声明的类型是 `ActivityXxxBinding`。
|
||||
泛型擦除导致**编译期不报错**,但一进入该页面就 `ClassCastException` 崩溃。**必须用同名文件 + 资源限定符。**
|
||||
|
||||
### 布局变体的强制约束
|
||||
|
||||
| 约束 | 说明 |
|
||||
|------|------|
|
||||
| **DataBinding 变量必须完全一致** | 两个变体 `<data>` 中的 `<variable>` 名称与类型必须相同,否则编译失败 |
|
||||
| **共用控件 id 必须双方都有** | Activity / ViewHolder 中引用的 id(如 `srl`/`rv`/`checkIcon`/`iv_icon`)两个变体都要保留 |
|
||||
| **单侧独有的 id 自动变 `@Nullable`** | 只在一个变体存在的 id,Binding 基类中会是可空字段,Kotlin 侧强制 null 检查(编译器自动保障,不用担心漏改) |
|
||||
|
||||
### 已改造的基础设施
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `module_base/.../util/DeviceUtil.kt` | 设备形态判定。按 `smallestScreenWidthDp >= 600` 区分平板/手机;读 `Resources.getSystem()` 以绕开 AutoSize 对 density 的改写,保证判定稳定 |
|
||||
| `module_base/BaseActivity.kt` | `applyDeviceOrientation()` 按形态锁方向(手机竖屏 / 平板横屏)。Manifest 统一写 `unspecified` 配合它使用。子类覆写 `lockOrientationByDevice()` 返回 false 可跳过 |
|
||||
| `module_base/base/BaseBindingActivity.kt` | 注释固化变体约定(`layoutId()` 不做设备分支) |
|
||||
| `module_base/MyApplication.kt` | **AutoSize 增加手机竖屏基准 390×844**(见下节) |
|
||||
| `module_base/res/values/colors.xml` | 手机端配色 `phone_*` 系列 |
|
||||
| `module_base/res/drawable/bg_phone_*.xml` | 手机端卡片、搜索框、状态标签、主按钮背景 |
|
||||
| `module_base/res/layout/title_tool_bar.xml` | 手机版公共标题栏(平板版已移至 `layout-sw600dp/`) |
|
||||
|
||||
`DeviceUtil` 用法:
|
||||
|
||||
```kotlin
|
||||
DeviceUtil.isPhone() // 是否手机
|
||||
DeviceUtil.isTablet() // 是否平板
|
||||
DeviceUtil.describe() // "PHONE(sw=411dp, force=AUTO)",日志排查用
|
||||
DeviceUtil.forceMode = DeviceUtil.ForceMode.PHONE // 调试:在平板上强制走手机布局
|
||||
```
|
||||
|
||||
> `DeviceUtil.layout(padId, phoneId)` 仅用于**极少数**确实需要代码级判断的场景(如非 DataBinding 的布局)。
|
||||
> **常规页面一律用资源限定符,不要调用它。**
|
||||
|
||||
### ⚠️ 屏幕方向:Manifest 一律写 `unspecified`,由 `BaseActivity` 按形态锁定
|
||||
|
||||
**症状(改造前)**:手机上打开页面(开屏页最明显)先显示横屏,约 1 秒后才转竖屏,伴随 Activity 重建。
|
||||
|
||||
**原因**:`android:screenOrientation` 由系统在**创建 Activity 窗口时**生效,此时业务代码还没执行。
|
||||
Manifest 写死 `userLandscape` 时,即使 `BaseActivity` 在 `super.onCreate` 之前
|
||||
`setRequestedOrientation(PORTRAIT)`,也只能在横屏窗口已建好之后再纠正 → 必然闪一下。
|
||||
|
||||
**做法**:Manifest 统一写 `unspecified`,让系统按设备**当前物理方向**建窗口
|
||||
(平板横屏摆放 → 横屏起,手机 → 竖屏起,两端起始方向本就正确),
|
||||
`BaseActivity.applyDeviceOrientation()` 再按形态锁定,不产生二次纠正:
|
||||
|
||||
```xml
|
||||
<activity android:name="…"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:screenOrientation="unspecified" />
|
||||
```
|
||||
|
||||
全项目 192 处已统一(`module_p` 等 24 处本就是 `portrait` 的 PDA 页面保持不变)。
|
||||
|
||||
#### ⛔ 别用 `@integer/screen_orientation` 这类资源引用(已验证行不通)
|
||||
|
||||
一度改成 `android:screenOrientation="@integer/screen_orientation"` + `values/`=portrait、
|
||||
`values-sw600dp/`=userLandscape,**看似优雅,实测是错的**:
|
||||
|
||||
系统解析 Manifest 属性时**用的是默认配置,不套用设备限定符**,所以 `values-sw600dp/` 永远不会被选中,
|
||||
所有 Activity 实际都拿到了 `values/` 的竖屏值。后果是平板端:
|
||||
|
||||
1. 先竖屏、随后被 `BaseActivity` 纠正成横屏 —— 反而把闪屏问题搬到了平板上
|
||||
2. **AutoSize 在竖屏配置下选了平板竖屏基准 720dp**(density=1280/720≈1.78,正常应为 1280/1152≈1.11)
|
||||
→ 交互几次后整个 UI 放大约 1.6 倍
|
||||
|
||||
> 验证方法(可复现):把 `values/` 与 `values-sw600dp/` 的值**互换**,并临时让
|
||||
> `lockOrientationByDevice()` 返回 false(排除运行时纠正),装到平板上看首屏方向。
|
||||
> 结果是平板取了 `values/` 的值 → 证明限定符被忽略。
|
||||
|
||||
### ⚠️ AutoSize 设计基准必须区分手机(易漏且后果严重)
|
||||
|
||||
项目使用 AndroidAutoSize,`MyApplication.initAutoSizeConfig()` 按方向+设备切换设计尺寸:
|
||||
|
||||
| 场景 | 设计基准 |
|
||||
|------|----------|
|
||||
| 横屏(平板) | 1152 × 720 dp |
|
||||
| 竖屏 + 手机 | **390 × 844 dp**(对应 5.5 寸设计稿画布) |
|
||||
| 竖屏 + 平板 | 720 × 1280 dp |
|
||||
| PictureSelector | 360 × 480 dp |
|
||||
|
||||
**若手机沿用平板竖屏的 720dp 基准,手机上所有 dp 会被压缩约一半,布局全部错位。**
|
||||
|
||||
### 手机版 UI 规范(依据 `documents/5.5寸手持端设计文件/` 设计稿)
|
||||
|
||||
| 部位 | 规范 |
|
||||
|------|------|
|
||||
| 画布基准 | 390 × 844 dp |
|
||||
| 顶栏 | 高 48dp,`@color/phone_primary`(#2563EB),左侧「‹ 返回」,标题居中 18sp 加粗 |
|
||||
| 页面背景 | `@color/phone_bg` (#F3F4F6) |
|
||||
| 列表卡片 | `@drawable/bg_phone_card`,圆角 12dp,`marginHorizontal=16dp`,`marginTop=12dp`,`padding=14dp` |
|
||||
| 卡片头行 | 圆形勾选框 20dp(`radiobtn_checked_style`/`radiobtn_unchecked_style`)+ 运单号 15sp 加粗 + 右侧状态标签 |
|
||||
| 卡片字段 | 两列网格,13sp,`@color/phone_text_body`,行距 8dp |
|
||||
| 状态标签 | `bg_phone_tag_green`(已完成)/ `bg_phone_tag_gray`(未完成),11sp |
|
||||
| 搜索行 | 搜索框 40dp 高 + `bg_phone_search`,内含放大镜 + 输入 + 扫码;右侧筛选按钮 40dp |
|
||||
| 状态 Tab | 高 42dp,选中文字 `phone_primary` + 底部 28×3dp 指示条 |
|
||||
| 底部操作条 | 白底 + `elevation=8dp`,全选 + 统计(13sp/11sp 两行)+ 主按钮 `bg_phone_btn_primary` |
|
||||
| 筛选弹层 | `FrameLayout` 遮罩 `#80000000` + 底部白色面板,重置 / 确定各占一半 |
|
||||
|
||||
### 手机版页面改造步骤
|
||||
|
||||
1. **移动平板布局**:`git mv res/layout/activity_xxx.xml res/layout-sw600dp/activity_xxx.xml`(item 布局同理)
|
||||
2. **新建手机布局**:在 `res/layout/` 下创建**同名**文件,保持相同 `<variable>` 与共用控件 id
|
||||
3. **Kotlin 侧不动**:`layoutId()`、`itemLayoutId`、ViewHolder 全部保持原样
|
||||
4. **纯 UI 交互**(Tab 切换、弹层显隐)写在 Activity,不污染 ViewModel;状态过滤优先复用 ViewModel 既有字段
|
||||
5. 手机布局顶部 `<include layout="@layout/title_tool_bar" />` 不变(变体自动切换)
|
||||
6. **弹框(BaseDialogModel)同样要适配**:dialog 布局照样走同名双变体(Pad 版移 `layout-sw600dp/`、
|
||||
手机版在 `layout/` 写设计稿的底部弹层样式),弹出位置在构造参数按形态切换:
|
||||
`BaseDialogModel<XxxBinding>(if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER)`
|
||||
(参考 `IntExpArriveResetDialogModel` + `dialog_int_exp_arrive_reset.xml` 双变体)。
|
||||
页面适配完要**逐个排查该页所有 DialogModel**,漏掉的会在手机上弹出 Pad 居中样式
|
||||
7. **回到手机端首页加菜单入口**(最易遗忘,见下节)——不加的话页面在手机上根本进不去
|
||||
|
||||
### 手机端首页菜单入口(适配完必须加,否则没入口)
|
||||
|
||||
手机端首页是 `module_p` 的 `PDAEnterActivity`(手持机首页),底部四个 Tab 对应:
|
||||
|
||||
| Tab | ViewModel | 位置 |
|
||||
|-----|-----------|------|
|
||||
| 国内 | `GnViewModel` | `module_p/.../ui/enter/gn/` |
|
||||
| 国际 | `GjViewModel` | `module_p/.../ui/enter/gj/` |
|
||||
|
||||
**菜单项是手写清单,不是接口下发**。权限机制只做过滤:登录返回的权限串存在
|
||||
`SharedPreferenceUtil.getString(Constant.Share.authList)`(一个逗号分隔的长字符串),
|
||||
菜单项按 `authList.contains(bean.key)` 决定显不显示。
|
||||
|
||||
所以「适配完页面但手机首页看不到入口」的原因通常**不是没权限,而是清单里没加**。
|
||||
排查顺序:先确认权限串里有对应 key,再去看清单。
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
加一项就是加一行 —— `ActionBean` 的第 4 个参数填 ARouter 路由即可,无需写 when 分支
|
||||
(`module_p` 只依赖 `module_base`,看不到 `module_gjc` 等业务模块,跨模块只能走路由):
|
||||
|
||||
```kotlin
|
||||
ActionBean(
|
||||
"出港移库",
|
||||
R.drawable.gjc_yi_ku_icon, // 图标复用 Pad 端同款
|
||||
Constant.AuthName.GjcYiKuListActivity, // 权限串,与 Pad 端菜单保持一致
|
||||
ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE // 有 route 就走通用跳转
|
||||
),
|
||||
```
|
||||
|
||||
两点注意:
|
||||
|
||||
- **权限串与路由都要和 Pad 端菜单对齐**:以 `app/.../HomeFragment.kt` 里该权限对应的
|
||||
`ARouter.build(...)` 为准。同一业务常有新旧两个 Activity(如移库的 `GjcYiKuListActivity`
|
||||
与 `IntExpMoveActivity`),Pad 菜单实际跳的才是要适配的那个
|
||||
- **图标**:`module_p` 看不到 `module_gjc` 的资源,把 Pad 图标 PNG 复制一份到
|
||||
`module_p/src/main/res/drawable-xxhdpi/`(该模块既有 4 个图标就是这么放的,同名不同密度桶可共存)
|
||||
|
||||
**示例:状态 Tab 零 ViewModel 改动**(`IntExpMoveActivity` 已实现)
|
||||
|
||||
```kotlin
|
||||
// Activity 中新增纯 UI 方法,写入 ViewModel 既有的 moveState 字段("" 全部 / "0" 未移库 / "1" 已移库)
|
||||
fun switchTab(state: String) {
|
||||
if (viewModel.moveState.value == state) return
|
||||
viewModel.moveState.value = state
|
||||
viewModel.searchClick() // 复用原有查询链路
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- XML 中判等要把字面量放前面,避免 LiveData 为 null 时崩溃 -->
|
||||
android:textColor="@{`0`.equals(viewModel.moveState) ? @color/phone_primary : @color/phone_text_body}"
|
||||
```
|
||||
|
||||
### 手机版公共组件库(`module_base/.../ui/weight/phone/`)
|
||||
|
||||
7 个组件覆盖设计稿中反复出现的部件,**新页面优先复用,不要重写 XML**。属性均为无命名空间写法,
|
||||
DataBinding 适配器统一放在 `PhoneWidgetKtx.kt`(同名属性按控件类型解析,与平板端 Pad* 组件互不冲突)。
|
||||
|
||||
| 组件 | 用途 | 关键属性 |
|
||||
|------|------|----------|
|
||||
| `PhoneSearchBar` | 顶部搜索行:搜索框 + 扫码 + 圆形筛选按钮 | `hint`、`value`(双向)、`showScan`、`showFilter`、`upperCase`、`setOnScanClickListener`、`setOnFilterClickListener`、`setRefreshCallBack` |
|
||||
| `PhoneStatusTab` | 状态 Tab 条(文字 + 角标,选中态下划线) | `tabs`(List\<KeyValue\>)、`counts`(List\<String\>)、`value`(双向)、`setOnTabChanged` |
|
||||
| `PhoneDataLayout` | 表单/筛选项,`PhoneDataLayoutType` = `INPUT`/`DATE`/`SPINNER` | `title`、`hint`、`type`、`value`(双向)、`list`、`required`、`enable`、`setRefreshCallBack` |
|
||||
| `PhoneFilterPanel` | 底部筛选弹层(遮罩 + 面板 + 重置/确认) | `title`、`panelVisible`、`setOnResetClick`、`setOnConfirmClick`、`setOnDismissClick` |
|
||||
| `PhoneBottomBar` | 底部操作条:全选 + 已选统计 + 主按钮 | `allChecked`、`countText`、`actionText`、`setOnAllCheckClick`、`setOnActionClick` |
|
||||
| `PhoneStatBox` | 卡片内 2~4 列统计框(未传 label 的列自动隐藏) | `label1..4`、`value1..4` |
|
||||
| `PhoneKvItem` | 详情页字段项(灰标签 + 数值),`tag=true` 转高亮标签(占位符 "-"/"--" 不套底) | `title`、`value`、`tag`、`tagColor`("orange" 默认/"green") |
|
||||
| `PhoneFormRow` | 行式表单项(左标签+右值+分隔线),`PhoneFormRowType` = `INPUT`/`SELECT`(内置底部选择面板)/`DATE`/`TEXT`(只读右对齐,空值 "--") | `title`、`hint`、`type`、`value`(双向)、`list`、`required`、`enable`、`numeric`、`setRefreshCallBack` |
|
||||
|
||||
`PhoneBottomBar` 追加 `actionDanger` 属性:true 时主按钮转浅红底红字 + 垃圾桶图标(ULD 管理批量删除)。
|
||||
新增资源:`bg_phone_btn_danger`、`bg_phone_fab`(圆形浮动 + 按钮)、`bg_phone_tag_red_light`(浅红标签)、
|
||||
`ic_phone_trash`、`ic_phone_edit`、色值 `phone_danger_bg/text`。
|
||||
出港计重追加:`bg_phone_btn_weak`(浅蓝弱按钮,卡片内提前运抵)、`bg_phone_card_top_accent`
|
||||
(卡片顶部主色描边条)、`ic_phone_clipboard`(FAB 剪贴板)、`ic_phone_weight`(砝码图标,可 tint)。
|
||||
出港查询追加:`ic_phone_package`(件数)、`ic_phone_tag`(特码)、`bg_phone_dot_blue/gray`(状态圆点);
|
||||
`PhoneFilterPanel` 内容插槽已内置 ScrollView(筛选项超屏时收缩滚动,底部按钮始终可见——
|
||||
9 个筛选项曾把「重置/确认」挤出屏幕)。
|
||||
出港仓库追加:`bg_phone_btn_disabled`(灰底禁用按钮,与 `bg_phone_btn_weak` 同 10dp 圆角,
|
||||
卡片操作按钮禁用态配 `phone_tag_gray_text` 文字)。
|
||||
进港移库追加:`bg_phone_btn_green`(绿色主按钮,拍照上传「确认提交」)、`bg_phone_card_header_orange`
|
||||
(橙色详情卡表头,异常照片)、`bg_phone_photo_add`(虚线加号槽位)、色值 `phone_amber`。
|
||||
|
||||
⚠️ **弹层与浮动按钮的层级**:`PhoneFilterPanel` 默认无 elevation,页面里有带 elevation 的浮动按钮(FAB)时
|
||||
会穿透到弹层之上——给弹层实例加 `android:elevation="8dp"`(大于 FAB 的 6dp)。
|
||||
|
||||
**`PhoneFilterPanel` 自带内容插槽**:把 `PhoneDataLayout` 直接写在其标签内即可,组件会自动移入面板中部
|
||||
并施加 20dp 项间距,无需关心内部层级:
|
||||
|
||||
```xml
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
panelVisible="@{activity.filterPanelVisible}"
|
||||
setOnResetClick="@{(v)-> activity.resetFilter()}"
|
||||
setOnConfirmClick="@{(v)-> activity.confirmFilter()}"
|
||||
setOnDismissClick="@{(v)-> activity.toggleFilterPanel()}">
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
title='@{"航班日期"}' hint='@{"请选择航班日期"}'
|
||||
type="@{PhoneDataLayoutType.DATE}" value="@={viewModel.flightDate}" />
|
||||
|
||||
</com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel>
|
||||
```
|
||||
|
||||
#### ⚠️ 复合自定义 View 必须屏蔽子控件状态恢复(已踩坑)
|
||||
|
||||
同一页面放多个 `PhoneDataLayout`/`PhoneStatBox` 时,它们的内部子控件(`et`/`tv`/`spinner`…)**共用同一套 id**。
|
||||
系统按 id 保存/恢复视图状态会跨实例串档 —— 实测表现为:DATE 项的默认日期被邻近输入框恢复的空文本清空,
|
||||
并通过双向绑定把 ViewModel 字段也写成了空值(筛选条件静默丢失)。
|
||||
|
||||
因此每个含内部 id 的手机版组件都重写了:
|
||||
|
||||
```kotlin
|
||||
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) = dispatchFreezeSelfOnly(container)
|
||||
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) = dispatchThawSelfOnly(container)
|
||||
```
|
||||
|
||||
**新增同类组件时必须照做**(取值由 ViewModel + DataBinding 持有,本就不需要视图级恢复)。
|
||||
|
||||
### 已完成 / 待办
|
||||
|
||||
- ✅ **已完成**:适配框架 + 手机端公共资源 + 7 个手机版公共组件
|
||||
- ✅ **已完成页面**:
|
||||
- 「国际出港移库」`IntExpMoveActivity`(框架参考页);详情/编辑与平板共用 `IntExpMoveEditActivity`
|
||||
(手机布局为设计稿「运单详情」样式,卡片「查看详情」/「编辑」分别对应平板点击卡片/侧滑编辑)
|
||||
- 「国际出港出库交接」`IntExpOutHandoverActivity` + 「板箱详情」`IntExpOutHandoverDetailActivity`
|
||||
(公共组件首个完整落地页,双端实机验证通过)
|
||||
- 「ULD管理」`UldListActivity` + `UldEditActivity`(app 模块首个适配页;一套编辑布局覆盖
|
||||
新增/修改/详情三态;批量删除为前端循环调用单条接口;状态三态/来源等 5 项后端缺口见问题清单 #7~#11)
|
||||
- 「出港计重」全链路 4+1 页:`GjcWeighingListActivity`(待计重列表,三格统计 + 卡片单票提前运抵 +
|
||||
FAB 进记录)、`GjcWeighingStartActivity`(开始计重,托盘数量/自重按设计稿手输)、
|
||||
`GjcWeighingRecordListActivity`(计重记录)、`GjcWeighingRecordDetailsActivity`(计重明细,
|
||||
isEditMode 复用为手机全屏「修改记录」覆层)+ 手机专属新页 `GjcWeighingWbDetailActivity`(运单详情)。
|
||||
合并搜索框只分流到平板同名入参:含字母→fno、其余→wbNo;
|
||||
checkIn 过滤等 7 项后端缺口见问题清单 #12~#18
|
||||
- 「出港运抵」主列表 + 3 个手机专属新页:`IntExpArriveActivity`(待运抵/已运抵 Tab,客户端按
|
||||
`declareStatus` 是否为空拆分数据集,卡片长按浮层:待运抵→补充、已运抵→重置+回执)、
|
||||
`IntExpArriveSubOrderActivity`(分单明细,数据直接复用主列表已加载的 `haWbList`,零新增请求)、
|
||||
`IntExpArriveReceiptActivity`(分单回执只读页,复用 `haWbList[].response`;主单「回执」与平板侧滑同走
|
||||
`getResponse` → 共用 `CustomsResponseActivity`)、
|
||||
`IntExpArriveSupplementActivity`(补充信息,参照 Pad 端国际进港「收发货人信息」批量应用模式,
|
||||
后端接口不存在,实机 404 已确认,见问题清单 #19)。
|
||||
Tab 过滤两个坑:① **RecyclerView 里 `View.GONE` 不会让 item 塌陷为 0 高度**(不同于普通
|
||||
ViewGroup),必须在数据层整体 `refresh()` 过滤后的子集给 adapter,不能给 item 根节点加
|
||||
visibility 绑定(实机截图验证过,见「常见编译错误速查」);② 通过 `Intent.putExtra(Serializable)`
|
||||
传递的 `GjcHaWb`/`GjcMaWb`,其 `checked`/`showMore`/`actionMenuVisible` 字段标了
|
||||
`@Transient`(= JVM `transient`,Java 序列化会跳过),反序列化后为 null,直接
|
||||
`.checked.set(...)` 会崩溃——接收方 `initOnCreated` 必须对列表 `.map { it.copy() }` 补上。
|
||||
2026-07-30 review 修正:状态重置弹框改手机底部弹层(弹框双变体首例,见改造步骤第 6 条)、
|
||||
主列表/回执页补空态、分单浮层补「回执」、分单统计加分母、Tab 角标初始 0
|
||||
- 「提取出库」`IntImpPickUpDLVActivity`(module_gjj,类型2 多选批量页 + 手机专属详情新页
|
||||
`IntImpPickUpDetailActivity`(Intent 传 Serializable bean 零请求)。待出库/已出库 Tab 绑
|
||||
**文档定义入参 `outState`**(0/1,抓包证实携带,效果待内网 #29)+ 角标双 pageQueryTotal;
|
||||
双形态卡片按 `isPickedUp`(dlvTime 非空)切换;手机进入时清空平板默认「当天缴费日期」查全量;
|
||||
缺口 #28:无 fdate 入参、出参无车牌(提货车辆)/目的港/业务类型。
|
||||
⚠️ 踩坑:`checkAllClick` 全选后底部「已选 N 项」不更新——批量勾选不走 FlowBus 单卡事件,
|
||||
必须在 checkAllClick 里同步 selectedCountText)
|
||||
- 「进港查询」`IntImpQueryActivity`(module_gjj;06 出港查询的进港镜像:三格统计
|
||||
总票数/已入库/已出库为已加载数据客户端计数(`pageQueryTotal` 无分项出参,缺口 #30),
|
||||
卡片状态前端推断:`dlvTime` 非空=已出库(绿)/`inDate` 非空=已入库(橙)/兜底未入库(灰);
|
||||
筛选弹层 9 项**入参全部经 api-doc 核对真实存在**(分页参数名 `page`/`limit`,品名参数名
|
||||
`goods`);详情页与运单追踪复用 08/06 已适配页零新增;平板侧滑筛选面板代码已判空守卫。
|
||||
⚠️ 踩坑:代理人绑定不能写 `bean.agentName ?? bean.agentCode`——转换器把 JSON null 归一成
|
||||
空串,`??` 只判 null 不判空串,须用 Bean 计算属性 `agentDisplay`(06 出港查询同步修正))
|
||||
- 「进港移库」`GnjMoveStashListActivity`(app 模块,**2026-08-03 归属更正后重做**:设计稿 11
|
||||
实际归属**国内进港移库 DomImpMove**(后端 8 接口齐全、Pad 首页「国内进港→进港移库」既有页面),
|
||||
最初误判为国际进港并按 IntExpMove 约定预接了不存在的 IntImpMove 模块(原缺口 #31),该套
|
||||
module_gjj 前端实现已整体删除。现行实现:主列表双形态(Pad 布局移 layout-sw600dp 零改动),
|
||||
待移库/已移库 Tab 走 **`search`/`searchMoved` 双端点**(`searchMoved` 出参无移库标识,前端按
|
||||
端点给 bean 打 `isMoved`),Tab 角标 = 两端点各调一次 limit=1 取分页 total(`initPhoneExtras`
|
||||
开关,仅手机发起);手机专属新页 `GnjMoveStashDetailActivity`(Intent 传 bean 零请求,三卡片:
|
||||
基本信息/移库信息(仅已移库)/异常照片)+ `GnjMoveStashPhotoActivity`(进入先调 `detail` 回填
|
||||
已有照片——`searchMoved` 出参无 pic 三字段 #34,提交走 **`modify` 最小体**
|
||||
`{mawbId,remark,picNumber,pic,originalPic}`,与平板移交编辑页同链路);批量移库复用平板
|
||||
`moveStashClick`(`move`:fid+ids)。手机首页入口在**国内 Tab**(`GnViewModel`,权限串
|
||||
`GnjYiKu=AppDomImpMove`,需补通用 route 跳转逻辑,图标从 module_gnj 复制);
|
||||
GnjMoveStashListActivity 补 @Route `/app/GnjMoveStashListActivity`。
|
||||
2026-08-03 内网真实数据双端验证通过;筛选 A/B:`wbNo`/`awbType` 生效,
|
||||
`fdate`/`fno`/`spCode` 被后端静默忽略(#32)、`opId`/`opDate` 恒 null(#33))
|
||||
- 「进港仓库」`IntImpStorageUseActivity`(module_gjj,07 出港仓库的进港镜像;**首页菜单实际跳转页,
|
||||
旧版 `GjjWareHouseActivity` 路由已注释弃用**。差异:卡片/筛选用始发站(fdep 入参)替代目的站;
|
||||
搜索框「运单号/航班号」分流(含字母→fno、其余→wbNo,抓包证实);筛选 5 项无入参(缺口 #25,
|
||||
比出港多 2 项);连带首次适配进港详情 `IntImpQueryDetailsActivity` + 3 Fragment(报文区 3 项:
|
||||
原始舱单/理货报告/海关放行,件重区直取 inPc/inWeight 无需求和,detail keys 以平板绑定为准 +
|
||||
dep/dest1/dest/unNumber/opDate 按出港推断待实测 #27);修改库位手机弹层「原库位」只列未出库
|
||||
库位(沿用平板校验);**前端接口前缀是 `IntImpStorage` 不是 `IntImpStorageUse`**,api-doc 按
|
||||
后者搜不到)
|
||||
- 「出港仓库」`IntExpStorageUseActivity`(嵌套多选列表页型的首个适配:平板「勾选主/子列表 +
|
||||
底部批量操作」在手机上改为**卡片级四按钮**(清仓/修改库位/出库/入库,已清仓置灰),语义等价
|
||||
「勾选该主单+全部库位」直接复用既有 performXxx 链路,ViewHolder 对双侧独有 id 全判空;
|
||||
三格统计 总票数/未清仓/已清仓(后两格 = 同条件 + `clearNormal=0/1` 各调一次 pageQueryTotal,
|
||||
过滤是否生效待内网 A/B,缺口 #24);筛选弹层 8 项中运单类型/业务类型/品名(中) 为后端未定义
|
||||
入参(缺口 #23,预留传参);三个操作弹框双变体化 + 手机版修改库位弹层加「原库位」单选;
|
||||
出库确认 AlertDialog → ConfirmDialogModel;详情与运单追踪完全复用出港查询已适配页,零新增页面)
|
||||
- 「出港查询」`GjcQueryActivity`(列表:三格统计 总票数/已入库/已离港 + 卡片状态标签,
|
||||
状态为前端推断:`fclose` 非空=已离港/`opDate` 非空=已入库;筛选弹层 9 项,日期起止与代理
|
||||
从平板搜索行并入,特码改下拉 `getSpecialCodeList(flag=1, ieFlag="")`;分项统计为已加载
|
||||
数据客户端计数,`isFclose` 实测被后端忽略,缺口 #21)+ `GjcQueryDetailsActivity`(3 Tab
|
||||
复用 ViewPager2+Fragment,手机变体 PhoneKvItem 网格;入库件数/重量= warehouseList 求和;
|
||||
锁定状态/计费重量后端无字段,缺口 #22)+ 「运单追踪」`LogDetailActivity`(app 模块公共页
|
||||
双变体首例:手机竖排时间线由 Activity `buildVerticalSteps` 动态构建,平板横向步骤条不动;
|
||||
手机标题按形态切「运单追踪」)。平板侧滑筛选面板代码已判空守卫(手机变体无 filter_panel
|
||||
include,Binding 字段为 @Nullable)
|
||||
- ✅ **设计稿 11 个入口(01~11)已于 2026-07-31 全部适配完成**
|
||||
- ⏳ **待办**:
|
||||
- `PhoneSearchBar` 接入 AutoQuery 联想(`AutoQueryManager` 目前只对接了 Pad 系组件)
|
||||
- 联调新发现的后端缺口 #35~#41(ULD uld 搜索 500、IntImpStorage 列表过滤全失效、已移库无 mawbId
|
||||
无法补传照片等)待后端处理,详见问题清单
|
||||
- ✅ **2026-10-09 后端反馈联调完成**:问题清单 #1~#34 逐项内网直调 + 模拟器端到端验证,结论写入清单 P 列。
|
||||
**后端口径:手机筛选一律与平板端查询参数保持一致**(不为手机新增入参);分项统计统一取
|
||||
`pageQueryTotal` 出参(`ManifestTotalDto` 已补齐分项字段),不要再按状态多调 total。
|
||||
最新字段语义见 `.claude/skills/phone-adapt/references/api-doc.md` 末节
|
||||
- 📋 **接口对接**:适配中发现的后端接口缺口统一记录在 `documents/后端接口对接问题清单.xlsx`
|
||||
(按模块/页面/接口定义/接口地址索引,与后端对接的正式文档,每页适配完必须维护);
|
||||
接口入参/出参一律以 api-doc MCP 为准核对,流程见
|
||||
`.claude/skills/phone-adapt/references/api-doc.md`——**后端对不认识的参数静默忽略**,
|
||||
猜参数名会得出「后端不支持」的误判(出库交接 `hoState`→`handoverState` 已踩过)
|
||||
|
||||
### 调试便利:直接拉起页面
|
||||
|
||||
`app/src/debug/AndroidManifest.xml`(**仅 Debug 生效,不进 Release 包**)通过 `tools:replace` 将适配调试页临时置为 `exported="true"`,免去每次登录再逐级点入:
|
||||
|
||||
```bash
|
||||
adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.page.move.IntExpMoveActivity
|
||||
adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.activity.IntExpOutHandoverActivity
|
||||
```
|
||||
|
||||
新增调试页时在该文件追加对应 `<activity>` 即可。
|
||||
|
||||
> 模拟器通常连不上内网服务(提示「接口访问失败」),列表为空。若需验证卡片/详情的还原度,
|
||||
> 可在 Activity 里临时注入几条样例 Bean(`binding.rv.commonAdapter()?.refresh(list)`)截图比对,**验证后务必删除**。
|
||||
|
||||
**参考文件**:`module_base/.../util/DeviceUtil.kt`、`module_gjc/res/layout/activity_int_exp_move.xml`(手机)、`module_gjc/res/layout-sw600dp/activity_int_exp_move.xml`(平板)
|
||||
|
||||
---
|
||||
|
||||
## Import 路径速查
|
||||
|
||||
### 基类与常用类
|
||||
@@ -877,6 +1267,7 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
|
||||
| 类 | 正确路径 |
|
||||
|----|----------|
|
||||
| `DictUtils` | `com.lukouguoji.module_base.util.DictUtils` |
|
||||
| `DeviceUtil` | `com.lukouguoji.module_base.util.DeviceUtil` |
|
||||
| `MediaUtil` | `com.lukouguoji.module_base.util.MediaUtil` |
|
||||
| `UploadUtil` | `com.lukouguoji.module_base.util.UploadUtil` |
|
||||
| `KeyValue` | `dev.utils.app.info.KeyValue` |
|
||||
@@ -903,6 +1294,18 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
|
||||
| 资源引用不存在 | drawable/color/string 缺失 | 先检查资源是否存在,不存在则创建或用已有资源 |
|
||||
| `View.VISIBLE` 报错 | 未导入 | XML `<data>` 中加 `<import type="android.view.View" />` |
|
||||
| `textStyle` DataBinding 报错 | 不支持 | 用固定值 `android:textStyle="bold"` |
|
||||
| 手机版页面一进入就 `ClassCastException` | 用了「不同文件名 + 代码判断」切换布局,生成了两个 Binding 类 | 改用同名布局 + 资源限定符(`layout/` 与 `layout-sw600dp/`),见「5.5 寸手持端双形态适配规范」 |
|
||||
| 布局变体编译报 `variable not found` | 两个变体的 `<data>` 中 `<variable>` 不一致 | 手机与平板变体的变量名/类型必须完全相同 |
|
||||
| 手机上元素小一半 / 布局错位 | AutoSize 仍按平板 720dp 基准换算 | 确认 `MyApplication.initAutoSizeConfig()` 中手机竖屏走 390×844 分支 |
|
||||
| 手机版筛选项默认值(如日期)自动被清空 | 多个复合自定义 View 内部子控件 id 相同,系统按 id 恢复状态时跨实例串档 | 组件重写 `dispatchSaveInstanceState`/`dispatchRestoreInstanceState` 为 `dispatchFreezeSelfOnly`/`dispatchThawSelfOnly`(见「手机版公共组件库」) |
|
||||
| 手机上页面先横屏再转竖屏(闪一下) | Manifest 写死 `screenOrientation="userLandscape"`,系统建窗口时已按横屏,代码纠正为时已晚 | 改成 `android:screenOrientation="unspecified"`,由 `BaseActivity` 按形态锁定(见「屏幕方向」小节) |
|
||||
| 平板先竖屏再转横屏 + 交互后 UI 放大约 1.6 倍 | Manifest 用了 `@integer/screen_orientation` 资源引用;系统解析 Manifest 不套用限定符,全部取到竖屏值,AutoSize 随之选了平板竖屏 720dp 基准 | Manifest 改回 `unspecified`(见「屏幕方向」小节的 ⛔ 说明) |
|
||||
| Tab/分类切换后列表顶部留一大块空白,卡片数量却不对 | 给 item 根节点加了 `visibility="@{tab条件 ? VISIBLE : GONE}"`——RecyclerView/LinearLayoutManager 不会像普通 ViewGroup 那样让 GONE 的子项自动塌陷为 0 高度,仍按测量高度参与滚动区域计算 | 不要用 item 级 visibility 做过滤;在 ViewModel 里自行维护完整列表,Tab 切换时把过滤后的子集整体 `commonAdapter()?.refresh(filteredList)` 给 adapter |
|
||||
| 通过 `Intent.putExtra(Serializable)` 传递的 Bean,切换其 `checked`/`showMore` 等状态时崩溃 `NullPointerException: ... on a null object reference` | 这些字段标了 `@Transient`(= JVM `transient`,本意是避免 Gson 网络请求序列化 `ObservableBoolean`),但 Java 对象序列化(Intent 传递走的机制)会跳过 `transient` 字段的恢复,反序列化后是 null | 接收方 `initOnCreated` 里对列表做 `.map { it.copy() }`——`.copy()` 会重新走一遍类体初始化逻辑,用全新 `ObservableBoolean(false)` 补上 |
|
||||
| 布局构建报 `Error: The string "--" is not permitted within comments` | XML 注释中出现连续双横线(如写"兜底 \"--\""),XML 规范禁止 | 注释里改用文字描述(如「兜底双横线」),代码绑定表达式中的 `"--"` 不受影响 |
|
||||
| 多选页手机版点「全选」后底部「已选 N 项」不更新(单卡勾选正常) | 批量勾选在 `checkAllClick` 里直接 set,不走 FlowBus 的单卡 EVENT_CHECK_CHANGED 事件,`selectedCountText` 只在事件回调里更新 | `checkAllClick` 末尾自行同步 `selectedCountText`(平板布局不绑定该字段,无影响) |
|
||||
| 手机版数字输入框(`PhoneFormRow` 双向绑 Double 的 `xxxStr`)输入 "18" 变成 "81" | getter 由 Double 反推字符串("1" → "1.0"),与输入框文本不等触发 `setText`,光标回到开头 | Bean 里保留输入原文(`@Transient` 私有 String),getter 优先返回原文(参考 `GjcCheckInRecord.trayTotalWeightStr`) |
|
||||
| XML 里 `bean.a ?? bean.b` 兜底不生效,字段明明有值却显示占位符 | 网络层转换器把 JSON null 归一成空串 `""`,DataBinding 的 `??` 只判 null 不判空串,永远取到左侧的空串 | 在 Bean 上加计算属性做 `isNullOrEmpty` 双兜底(如 `agentDisplay`),XML 绑计算属性(进港查询/出港查询代理人已踩过) |
|
||||
|
||||
---
|
||||
|
||||
@@ -968,10 +1371,16 @@ private fun loadDictLists() {
|
||||
```xml
|
||||
<activity android:name="com.lukouguoji.gjc.activity.XxxActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false" android:screenOrientation="userLandscape" />
|
||||
android:exported="false"
|
||||
android:screenOrientation="unspecified" />
|
||||
```
|
||||
> `screenOrientation` **必须写 `unspecified`**,不要写死 `userLandscape`、也不要用资源引用。
|
||||
> 写死横屏会让手机端「先横屏、约 1 秒后转竖屏」闪一下;资源引用则会让平板端出问题
|
||||
> (详见下方「屏幕方向」小节)。
|
||||
|
||||
3. 在 `ARouterConstants` 注册路由(如需)
|
||||
4. 标题栏统一用 `<include layout="@layout/title_tool_bar" />`,Activity 中 `setBackArrow("标题")`
|
||||
5. **双形态布局**:需同时支持手机的页面,平板布局放 `res/layout-sw600dp/`、手机布局放 `res/layout/`,两者**同名**且变量与共用 id 一致。详见「5.5 寸手持端双形态适配规范」
|
||||
|
||||
### 常见业务操作
|
||||
|
||||
@@ -1119,6 +1528,9 @@ Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail)
|
||||
- `ic_new_expand`(全部展开/收起)36dp + padding 4dp
|
||||
- **图标之间间距统一 `marginStart=16dp`**(禁止使用 8dp,会导致视觉过近)
|
||||
- 常用资源: `bg_white_radius_8`、`colorPrimary`、`text_normal`、`text_gray`、`color_bottom_layout`
|
||||
- **双形态适配:逻辑与 UI 严格分离** — 手机版只重写布局,业务逻辑一律复用现有 ViewModel / ViewHolder;纯 UI 交互(Tab 切换、弹层显隐)写在 Activity,不下沉到 ViewModel
|
||||
- **禁止用不同文件名切换布局** — 手机/平板布局必须同名 + 资源限定符,否则 `ClassCastException`(详见「5.5 寸手持端双形态适配规范」)
|
||||
- 手机版资源统一使用 `phone_*` 颜色与 `bg_phone_*` drawable,禁止直接写死色值
|
||||
|
||||
### 环境配置
|
||||
|
||||
@@ -1132,4 +1544,20 @@ Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail)
|
||||
2. 资源引用错误 → 检查 drawable/color/string 是否存在
|
||||
3. DataBinding 错误 → 检查 import、枚举值、View 类导入
|
||||
4. suspend function 错误 → 在 `viewModelScope.launch` 中调用
|
||||
5. 仍有问题 → `./gradlew clean` 后重新构建
|
||||
5. 双形态布局问题 → 查「常见编译错误速查」中 3 条布局变体相关条目;可用 `DeviceUtil.describe()` 打日志确认当前形态
|
||||
6. 仍有问题 → `./gradlew clean` 后重新构建
|
||||
|
||||
### 双端验证命令
|
||||
|
||||
```bash
|
||||
~/Library/Android/sdk/emulator/emulator -list-avds # 可用:Aerologic_Phone_5.5_API31(手机,首选)、Aerologic_Tablet(平板)、Medium_Phone_API_36.1
|
||||
adb -s <设备> shell wm size && adb -s <设备> shell wm density # 确认屏幕参数
|
||||
adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果 PHONE(sw=384dp) / TABLET(sw=800dp)
|
||||
adb -s <设备> exec-out screencap -p > /tmp/shot.png # 截图比对
|
||||
```
|
||||
|
||||
> 手机端验证**首选 `Aerologic_Phone_5.5_API31`**:按真实交付手持机 1:1 定制——720×1440、5.51 寸(300dpi,sw=384dp)、
|
||||
> Android 12 / API 31(与 targetSdk 一致)、arm64-v8a,与实机的差异最小。`Medium_Phone_API_36.1`(1080×2400、API 36)
|
||||
> 仅作兼容性参考,分辨率与系统版本都和交付机不符。
|
||||
|
||||
> 改动 `BaseActivity` / `BaseBindingActivity` / 公共组件后,**必须在平板上回归**,确认现有平板版行为不变。
|
||||
|
||||
Reference in New Issue
Block a user