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:
167
.claude/skills/phone-adapt/references/pad-safety.md
Normal file
167
.claude/skills/phone-adapt/references/pad-safety.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Pad 端零破坏 & 易崩点
|
||||
|
||||
平板端是在跑的生产功能,适配手机不能改变它的任何行为。以下是必须遵守的约束和踩过的坑。
|
||||
|
||||
## 布局变体的三条硬约束
|
||||
|
||||
同名布局 + 资源限定符(`layout/` 手机 / `layout-sw600dp/` 平板)会让 DataBinding 生成
|
||||
**一个 Binding 基类 + 两个变体实现**,因此 Kotlin 侧零改动。代价是两个变体必须兼容:
|
||||
|
||||
### 1. `<variable>` 必须完全一致
|
||||
|
||||
手机变体需要 `activity` 变量(暴露纯 UI 方法给 XML)时,**平板变体也要补上**,否则编译报
|
||||
`variable not found`。平板不用它,加个注释说明即可:
|
||||
|
||||
```xml
|
||||
<!-- 平板端不使用,仅为与手机变体保持变量一致 -->
|
||||
<variable name="activity" type="com.lukouguoji.gjc.activity.XxxActivity" />
|
||||
```
|
||||
|
||||
### 2. 共用 id 双方都要保留
|
||||
|
||||
Activity / ViewHolder 引用到的 id(`srl`、`rv`、`iv_icon`、`checkIcon`…)两个变体都要有。
|
||||
可以换位置换样式,但 id 得在。
|
||||
|
||||
### 3. 单侧独有的 id 会变 `@Nullable`
|
||||
|
||||
只在手机变体存在的 id(如「查看详情」的 `ll_detail`),Binding 基类里是可空字段,
|
||||
Kotlin 侧必须判空:
|
||||
|
||||
```kotlin
|
||||
// 平板变体没有这个控件,为 null 时直接跳过
|
||||
binding.llDetail?.setOnClickListener { XxxDetailActivity.start(itemView.context, bean) }
|
||||
```
|
||||
|
||||
编译器会强制你处理,不用担心漏掉。改完可以核对生成代码确认:
|
||||
|
||||
```bash
|
||||
grep -B3 "llDetail" module_gjc/build/generated/data_binding_base_class_source_out/debug/out/\
|
||||
com/lukouguoji/gjc/databinding/ItemXxxBinding.java
|
||||
```
|
||||
|
||||
### ⛔ 绝对不要用「不同文件名 + 代码判断」切布局
|
||||
|
||||
`activity_xxx_phone.xml` 会生成 `ActivityXxxPhoneBinding`,而字段声明的类型是
|
||||
`ActivityXxxBinding`。泛型擦除让**编译期查不出来**,一进页面就 `ClassCastException`。
|
||||
|
||||
## 业务层零破坏的三个模式
|
||||
|
||||
### 新增查询维度:默认空值 + 仅非空进请求
|
||||
|
||||
平板布局没有对应控件 → 字段恒为空 → 不进请求体 → 平板查询与改造前逐字节一致。
|
||||
|
||||
```kotlin
|
||||
val handoverState = MutableLiveData("") // 手机 Tab 绑这个,平板无控件
|
||||
val hoId = MutableLiveData("")
|
||||
|
||||
val filterParams = mapOf(
|
||||
"fdate" to flightDate.value?.ifEmpty { null },
|
||||
/* 原有字段… */
|
||||
"hoState" to handoverState.value?.ifEmpty { null }, // 空 → null → 不影响平板
|
||||
"hoId" to hoId.value?.ifEmpty { null },
|
||||
)
|
||||
```
|
||||
|
||||
### 手机专属的额外请求:ViewModel 内部开关
|
||||
|
||||
Tab 角标计数、新增下拉字典这类请求,平板不需要,多发就是改变了平板行为(请求次数)。
|
||||
用一个内部布尔控制,由 Activity 在手机形态下开启:
|
||||
|
||||
```kotlin
|
||||
// ViewModel
|
||||
private var phoneExtrasEnabled = false
|
||||
|
||||
fun initPhoneExtras() {
|
||||
phoneExtrasEnabled = true
|
||||
handoverState.value = "0" // 手机默认停在第一个 Tab
|
||||
DictUtils.getWHSUserList(addAll = false) { hoUserList.postValue(it) }
|
||||
}
|
||||
|
||||
override fun getData() {
|
||||
/* 原有列表 + 统计请求… */
|
||||
if (phoneExtrasEnabled) loadTabCounts(filterParams) // 平板不触发
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Activity
|
||||
if (DeviceUtil.isPhone()) viewModel.initPhoneExtras()
|
||||
```
|
||||
|
||||
`DeviceUtil.isPhone()` 只用来决定**数据默认值和额外请求**,**永远不用来选布局**。
|
||||
|
||||
### 纯 UI 状态放 Activity
|
||||
|
||||
弹层显隐、Tab 高亮这类与业务无关的状态,用 `ObservableBoolean` 放在 Activity 上暴露给 XML,
|
||||
不要污染 ViewModel:
|
||||
|
||||
```kotlin
|
||||
val filterPanelVisible = ObservableBoolean(false)
|
||||
fun toggleFilterPanel() = filterPanelVisible.set(!filterPanelVisible.get())
|
||||
```
|
||||
|
||||
### 同名 id 在两个变体里必须是同一种控件
|
||||
|
||||
平板变体的 `btnMove` 是 `TextView`,手机版底部换成了 `PhoneBottomBar`——**不要为了复用
|
||||
Activity 里的 `binding.btnMove` 而把这个 id 安到 PhoneBottomBar 上**。同名不同类型会让
|
||||
DataBinding 把字段类型退化成公共父类,编译期未必报错,运行时行为难料。
|
||||
|
||||
正确做法:手机端的按钮点击走组件自身的 `setOnActionClick`,`btnMove` 就只留在平板变体里,
|
||||
Activity 侧判空调用:
|
||||
|
||||
```kotlin
|
||||
binding.btnMove?.setOnClickListener { showMoveConfirmDialog() }
|
||||
```
|
||||
|
||||
被 XML 调用的 Activity 方法记得改成 public。
|
||||
|
||||
### 调字典要对齐既有页面的参数口径
|
||||
|
||||
`DictUtils` 的方法参数常有"看起来合理但后端不认"的取值。出港移库的特码下拉一开始传
|
||||
`ieFlag="E"`(出港),接口返回空数组;而同模块既有页面(`GjcWeighingStartViewModel` 等)
|
||||
一律传 `ieFlag=""`,能正常返回。
|
||||
|
||||
所以调字典前先 grep 一下同模块其他页面怎么调的,照抄它们的参数;改完必须在实机上
|
||||
**把下拉点开确认真的有数据**,空列表在 UI 上看不出报错。
|
||||
|
||||
### 共享方法别为手机改语义
|
||||
|
||||
`checkAllClick()` 这类平板也在用的方法,不要为了手机端的语义去改它。
|
||||
例:手机「待交接」Tab 下理论上只有未交接记录(服务端按状态过滤),所以全选照原样全量勾选就是对的;
|
||||
若为此加上「跳过已交接」的过滤,反而改变了平板端(平板列表混排两种状态)的行为。
|
||||
|
||||
## 屏幕方向:Manifest 必须写资源引用
|
||||
|
||||
新增 Activity 注册时:
|
||||
|
||||
```xml
|
||||
<activity android:name="…"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:screenOrientation="@integer/screen_orientation" />
|
||||
```
|
||||
|
||||
**不要写死 `userLandscape`。** `screenOrientation` 由系统在创建 Activity 窗口时生效,
|
||||
那时业务代码还没跑;写死横屏后即使 `BaseActivity` 在 `super.onCreate` 之前改成竖屏,
|
||||
用户也会看到「先横屏、约 1 秒后转竖屏」的闪屏和重建。
|
||||
|
||||
资源取值:`module_base/res/values/integers.xml` = 1(portrait),
|
||||
`values-sw600dp/integers.xml` = 11(userLandscape)。全项目已统一替换。
|
||||
|
||||
## 改 module_base 公共代码时
|
||||
|
||||
`module_base` 被所有业务模块依赖,改动会影响全部页面。原则:
|
||||
|
||||
- **只做增量**:加新组件、新颜色、新 drawable,别改既有组件的行为
|
||||
- 确实要改公共基类(`BaseActivity` / `BaseBindingActivity` / Pad 系组件)时,
|
||||
**必须在平板上回归**至少 2~3 个不同类型的既有页面
|
||||
- Bean 上加只读计算属性是安全的(Gson 只序列化字段,不动 getter);
|
||||
加**字段**要谨慎,会进请求体
|
||||
|
||||
## 已知的既有问题(不是你改坏的)
|
||||
|
||||
用 `am start` 直启页面时,若**先经过首页**再启目标页,会崩在
|
||||
`LoadingModel.showLoading` → XPopup `popupInfo is null`。在**未改动**的页面上同样复现,
|
||||
属于既有缺陷。**冷启动直启不受影响**(`force-stop` 后直接 `am start`)。
|
||||
|
||||
遇到崩溃先做这个对照实验再下结论:拿一个你没碰过的页面用同样方式打开,看是否同样崩。
|
||||
Reference in New Issue
Block a user