feat: 完成出港运抵页面5.5寸手机端适配
- 主列表新增待运抵/已运抵 Tab(客户端按申报状态拆分)+ 卡片长按浮层(补充/重置/回执) - 新增分单明细页:复用主列表已加载数据,零新增请求 - 新增回执内容只读页 - 新增补充信息页:参照 Pad 端收发货人信息批量应用模式(后端接口暂缺,已登记问题清单) - 手机首页「国际」Tab 增加「出港运抵」入口 - 修复两个平台坑:RecyclerView 内 GONE 不塌陷高度;Serializable Intent 传递 遇 @Transient 字段反序列化为 null 导致的崩溃 - 登记后端接口缺口 2 项(#19~#20)至问题清单 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,13 @@ curl -s -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
|
||||
0 正常/1 故障(三态需求=缺口 #7);无「来源」字段(#8)、无 `checkInDate`/`ifNo` 筛选入参(#9)、
|
||||
无批量删除(#10,`deleteUld` 是 `@Query` 单条);出参 `ifNo`/`efNo`/`checkInDate`/`checkOutDate`
|
||||
文档无描述,按命名推断进/出港航班号与出入库时间(#11 待确认)
|
||||
- `IntExpArrive`(出港运抵,2026-07-30 内网实测):`declareStatus`(申报状态)只在出参,
|
||||
`pageQuery`/`pageQueryTotal` 入参均无此字段(缺口 #20,待运抵/已运抵 Tab 靠客户端拆分);
|
||||
`declareStatus`/`GjcHaWb.arrivalStatus` 三色编码:`01`=正常(绿)、`W`=靛蓝态、其余非空=琥珀异常态;
|
||||
`haWbList[].response`/`arrivalOpDate` 是回执文本与对应时间,实测确认真实存在非空数据;
|
||||
分单补充收发货人信息**后端完全没有对应接口**(`search_endpoints` 对 `IntExpArrive`/`HaWb`
|
||||
关键字均搜不到分单更新接口,只有国际进港的 `IntImpAirManifest/complete`),
|
||||
实机调用拟定路径 `IntExpArrive/haWb/supplement` 确认 404(缺口 #19)
|
||||
- `IntExpCheckIn`(出港计重,2026-07-29 内网实测):`likeNo` **并非任意模糊**——按 8 位 `no`
|
||||
匹配(11 位 wbNo 或部分号均 0 条),11 位全号必须走 `wbNo`;`checkIn`/`checkInList`
|
||||
文档有定义但 `pageQuery`/`pageQueryTotal` **均静默忽略**(缺口 #12,待计重/计重中计数依赖);
|
||||
|
||||
@@ -130,7 +130,7 @@ binding.btnMove?.setOnClickListener { showMoveConfirmDialog() }
|
||||
例:手机「待交接」Tab 下理论上只有未交接记录(服务端按状态过滤),所以全选照原样全量勾选就是对的;
|
||||
若为此加上「跳过已交接」的过滤,反而改变了平板端(平板列表混排两种状态)的行为。
|
||||
|
||||
## 屏幕方向:Manifest 必须写资源引用
|
||||
## 屏幕方向:Manifest 一律写 `unspecified`
|
||||
|
||||
新增 Activity 注册时:
|
||||
|
||||
@@ -138,15 +138,20 @@ binding.btnMove?.setOnClickListener { showMoveConfirmDialog() }
|
||||
<activity android:name="…"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:screenOrientation="@integer/screen_orientation" />
|
||||
android:screenOrientation="unspecified" />
|
||||
```
|
||||
|
||||
**不要写死 `userLandscape`。** `screenOrientation` 由系统在创建 Activity 窗口时生效,
|
||||
那时业务代码还没跑;写死横屏后即使 `BaseActivity` 在 `super.onCreate` 之前改成竖屏,
|
||||
用户也会看到「先横屏、约 1 秒后转竖屏」的闪屏和重建。
|
||||
**两种错误方案都实测踩过,别再试:**
|
||||
- 写死 `userLandscape` → 手机「先横屏、约 1 秒后转竖屏」(方向在建窗口时就定了,
|
||||
`BaseActivity` 在 `super.onCreate` 之前纠正也来不及)
|
||||
- 用 `@integer/screen_orientation` 资源引用(曾经以为是优雅方案)→ **更糟**:系统解析
|
||||
Manifest 属性不套用设备限定符,所有 Activity 实际都拿到 `values/` 默认值(竖屏),
|
||||
平板因此先竖后横,且 AutoSize 在竖屏配置下选中平板竖屏 720dp 基准
|
||||
(正常应为 1152×720 横屏基准),交互几次后整个 UI 放大约 1.6 倍
|
||||
|
||||
资源取值:`module_base/res/values/integers.xml` = 1(portrait),
|
||||
`values-sw600dp/integers.xml` = 11(userLandscape)。全项目已统一替换。
|
||||
`unspecified` 让系统按设备当前物理方向建窗口(平板横屏摆放→横屏起,手机→竖屏起,
|
||||
两端起始方向本就正确),`BaseActivity.applyDeviceOrientation()` 再按形态锁定,
|
||||
不产生二次纠正。全项目 192 处已统一为 `unspecified`。
|
||||
|
||||
## 改 module_base 公共代码时
|
||||
|
||||
@@ -158,6 +163,81 @@ binding.btnMove?.setOnClickListener { showMoveConfirmDialog() }
|
||||
- Bean 上加只读计算属性是安全的(Gson 只序列化字段,不动 getter);
|
||||
加**字段**要谨慎,会进请求体
|
||||
|
||||
## RecyclerView 里不要用 item 级 visibility 做 Tab/分类过滤
|
||||
|
||||
普通 ViewGroup(LinearLayout 等)里 `View.GONE` 的子 View 会自动塌陷为 0 高度、不占布局空间——
|
||||
但 **RecyclerView + LinearLayoutManager 不是这样**:它按子项的测量高度参与滚动区域计算,
|
||||
不会因为子项是 GONE 就跳过。给 item 根节点加 `visibility="@{tab条件 ? VISIBLE : GONE}"`
|
||||
切 Tab,会在列表里留下一段空白(隐藏项的高度仍被保留),角标数字和实际卡片数量对不上。
|
||||
|
||||
出港运抵的「待运抵/已运抵」Tab 就踩了这个坑(实机截图验证:切到「已运抵」后顶部有一大块
|
||||
空白,隐藏的待运抵卡片高度并未塌陷)。正确做法是在数据层过滤,而不是视图层:
|
||||
|
||||
```kotlin
|
||||
// ViewModel:自己维护完整列表,不依赖 adapter.items 的历史状态
|
||||
// (见下一节:adapter.items 在 loadMore 场景可能已经是上次过滤后的子集,不能当作真源)
|
||||
private var allLoadedList: List<GjcMaWb> = emptyList()
|
||||
|
||||
override fun getData() {
|
||||
launchLoadingCollect({ ... }) {
|
||||
onSuccess = { result ->
|
||||
val newPageItems = result.list ?: emptyList()
|
||||
allLoadedList = if (pageModel.page <= 1) newPageItems else allLoadedList + newPageItems
|
||||
|
||||
pageModel.handleListBean(result) {
|
||||
// 无论 handleListBean 内部把 adapter.items 搞成什么样,这里用 allLoadedList
|
||||
// 强制覆盖为「当前 Tab 的正确子集」,兜底纠正
|
||||
val filtered = allLoadedList.filter { it.isPending == (currentTab.value == "pending") }
|
||||
pageModel.rv?.commonAdapter()?.refresh(filtered)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onTabChangedPhone() {
|
||||
val filtered = allLoadedList.filter { it.isPending == (currentTab.value == "pending") }
|
||||
pageModel.rv?.commonAdapter()?.refresh(filtered) // 只是 items.clear()+addAll()+notifyDataSetChanged,无副作用
|
||||
}
|
||||
```
|
||||
|
||||
`CommonAdapter.refresh()` 只是替换内部列表并 `notifyDataSetChanged()`,不影响
|
||||
SmartRefreshLayout 的分页/完成状态,可以放心在 `handleListBean` 回调里再调用一次。
|
||||
|
||||
## Intent 传递 Serializable Bean:`@Transient` 字段反序列化后是 null
|
||||
|
||||
`GjcMaWb`/`GjcHaWb` 的 `checked`/`showMore`/`actionMenuVisible` 三个 UI 状态字段都标了
|
||||
`@Transient`(本意是避免 Gson 把 `ObservableBoolean` 的内部结构序列化进网络请求体)。
|
||||
但 Kotlin 的 `@Transient` 注解生成的就是 JVM 的 `transient` 修饰符,这个修饰符对 **Java
|
||||
对象序列化**(`Intent.putExtra(String, Serializable)` 走的正是这条路径)有实际含义:
|
||||
反序列化时会跳过 `transient` 字段的恢复,对象引用类型的字段结果就是 **null**。
|
||||
|
||||
新增手机专属页面时,如果要把 `List<GjcHaWb>` 通过 `ArrayList<GjcHaWb>` 塞进 `Intent`
|
||||
传给下一个页面,接收方拿到的对象「看起来」字段都对(wbNo、pc、weight 这些正常字段没问题,
|
||||
因为它们不是 `@Transient`),但只要调用 `bean.checked.set(...)` 就会立刻崩溃:
|
||||
|
||||
```
|
||||
NullPointerException: Attempt to invoke virtual method
|
||||
'void androidx.databinding.ObservableBoolean.set(boolean)' on a null object reference
|
||||
```
|
||||
|
||||
出港运抵的「分单明细」页第一版全选按钮就是这样崩的。**修复只需一行**,在接收方
|
||||
`initOnCreated` 里对拿到的列表整体 `.copy()` 一遍——`.copy()` 会重新触发一次真正的对象构造,
|
||||
类体里 `val checked = ObservableBoolean(false)` 这类初始化逻辑跟着重新跑一遍,
|
||||
把 null 补成全新实例:
|
||||
|
||||
```kotlin
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun initOnCreated(intent: Intent) {
|
||||
val list = intent.getSerializableExtra(Constant.Key.DATA) as? ArrayList<GjcHaWb>
|
||||
// .copy() 补上被 Java 序列化跳过的 @Transient 字段
|
||||
fullList = (list ?: emptyList()).map { it.copy() }
|
||||
}
|
||||
```
|
||||
|
||||
这个坑只在「Intent 传递 Serializable Bean + 接收方要切换 checked/showMore 等状态」时才会
|
||||
触发;纯只读展示(如回执页)不调用这些字段的 setter,不会崩,但补一道 `.copy()` 无害,
|
||||
建议只要通过 Intent 传了 haWbList/maWbList 就统一加上,别等下次踩到才想起来。
|
||||
|
||||
## 已知的既有问题(不是你改坏的)
|
||||
|
||||
用 `am start` 直启页面时,若**先经过首页**再启目标页,会崩在
|
||||
|
||||
Reference in New Issue
Block a user