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:
2026-07-29 14:18:29 +08:00
parent 9c4588e47e
commit db062c8ee3
134 changed files with 19602 additions and 2965 deletions

View File

@@ -0,0 +1,120 @@
# 手机版公共组件(`module_base/.../ui/weight/phone/`
7 个组件覆盖设计稿反复出现的部件。**先复用,实在没有再新增**——每个新组件都要维护、都要踩一遍
状态恢复的坑,能少一个是一个。
所有属性都是**无命名空间写法**(与平板端 `PadSearchLayout` 一致DataBinding 适配器集中在
`PhoneWidgetKtx.kt`。同名属性(`hint`/`value`/`title`/`list`)按控件类型解析,与 Pad 系组件不冲突。
## 组件与属性
| 组件 | 用途 | 属性 |
|------|------|------|
| `PhoneSearchBar` | 顶部搜索行:搜索框 + 扫码 + 圆形筛选按钮 | `hint``value`(双向)、`showScan``showFilter``upperCase``setOnScanClickListener``setOnFilterClickListener``setRefreshCallBack` |
| `PhoneStatusTab` | 状态 Tab 条(文字 + 角标 + 选中下划线) | `tabs`(List\<KeyValue\>)、`counts`(List\<String\>)、`value`(双向)、`setOnTabChanged` |
| `PhoneDataLayout` | 表单/筛选项,三形态 | `title``hint``type`(`PhoneDataLayoutType.INPUT`/`DATE`/`SPINNER`)、`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` | 详情页字段项(灰标签 + 数值) | `title``value``tag`(true → 橙色高亮标签) |
`KeyValue``dev.utils.app.info.KeyValue`,字段是 **`key`(显示文案)+ `value`(业务值)**
不是 `name``PhoneStatusTab``tabs` 就按 `KeyValue("待交接","0")` 传。
## PhoneFilterPanel 的内容插槽
筛选项直接写在标签内,组件 `onFinishInflate` 会自动把它们移进面板中部并施加 20dp 项间距,
使用方不用关心内部层级、也不用逐个写 `marginTop`
```xml
<com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel
android:layout_width="match_parent"
android:layout_height="match_parent"
title='@{"筛选条件"}'
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>
```
记得在 `<data>``<import type="com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayoutType" />`
## ⚠️ 新增复合组件必做:屏蔽子控件状态恢复
同一页面放多个同类组件时(比如筛选弹层里 3 个 `PhoneDataLayout`),它们的内部子控件
`et`/`tv`/`spinner`…)**共用同一套 id**。系统按 id 保存/恢复视图状态会跨实例串档。
实际后果出库交接踩过DATE 项的默认日期被邻近输入框恢复的空文本覆盖,
`EditText.setText("")` 触发 `doOnTextChanged`,再经双向绑定把 ViewModel 的航班日期也写成空——
**筛选条件静默丢失,界面上只表现为"日期没了",极难联想到状态恢复**
所以每个含内部 id 的组件都要写:
```kotlin
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) =
dispatchFreezeSelfOnly(container)
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) =
dispatchThawSelfOnly(container)
```
取值本来就由 ViewModel + DataBinding 持有,不需要视图级恢复,屏蔽掉没有副作用。
配套的第二道防线:多形态组件里,文本回写要限定形态,别让隐藏控件影响取值:
```kotlin
et.doOnTextChanged { text, _, _, _ ->
if (type == PhoneDataLayoutType.INPUT) value = text.toString()
}
```
### 遇到"值被神秘清空"怎么定位
在 setter 里打一条带堆栈的日志,一次就能看到真凶:
```kotlin
android.util.Log.d("DBG", "value '$field' -> '$v'", Throwable("trace"))
```
出库交接那次的栈是 `TextView.onRestoreInstanceState → EditText.setText → doOnTextChanged`
指向非常明确。**排完记得删日志。**
## 新增组件的写法约定
照现有 7 个组件的模式来,保持一致:
1. 继承 `LinearLayout`/`FrameLayout``init``inflate(context, R.layout.layout_phone_xxx, this)`
2. 内部布局用 `<merge tools:parentTag="android.widget.LinearLayout">`,避免多一层嵌套
3. 属性做成 Kotlin `var` + setter 里立即生效;双向绑定值用
`onChangeListener: InverseBindingListener?`
4. BindingAdapter 统一加到 `PhoneWidgetKtx.kt`,函数名带组件名前缀避免顶层函数重名
5. 有互斥形态时,**`type` 要先于 `value` 生效**(把 type 放进同一个多属性 adapter 的第一个
`?.let`),否则 value 会写进错误的子控件
6. 加上上面那两个 `dispatch*SelfOnly` 覆写
7. 完成后同步更新 `CLAUDE.md` 的组件表和记忆文件 `phone-5.5inch-adaptation.md`
## 资源
配色一律用 `phone_*`,背景用 `bg_phone_*`,图标用 `ic_phone_*`,禁止写死色值。
已有的(不够再加,加完更新 CLAUDE.md
- 色:`phone_primary` `phone_page_bg` `phone_bg` `phone_stat_bg` `phone_text_title/body/hint/label/strong/weak_btn`
`phone_divider` `phone_badge_bg` `phone_green` `phone_check_border` `phone_tag_gray_*` `phone_tag_green_*`
`phone_tag_red_*` `phone_orange_bg/text`
- 背景:`bg_phone_card` `bg_phone_search` `bg_phone_input` `bg_phone_stat_box` `bg_phone_panel_top`
`bg_phone_icon_round` `bg_phone_btn_primary` `bg_phone_btn_outline` `bg_phone_badge`
`bg_phone_tag_green/gray/red/orange` `bg_phone_card_header_blue/green`
- 图标:`ic_phone_airplane` `ic_phone_location` `ic_phone_person` `ic_phone_check_circle`
`ic_phone_chevron_right/down` `ic_phone_calendar` `ic_phone_close`
`ic_phone_check_round_checked/unchecked`(圆形复选框)
- 复用 Pad 的矢量图:`img_search` `img_scan` `img_filter`(可 `app:tint` 染色)
设计稿里的 MDI 图标若缺失,直接写 24×24 的 vector用 MDI 官方 path比找位图靠谱且能染色。