- 列表/详情/运单追踪三页同名布局双变体(平板移 layout-sw600dp/,Kotlin 零设备分支) - 列表:搜索行 + 三格统计(总票数/已入库/已离港,已加载数据客户端计数)+ 状态标签卡片 (已离港/已入库/未入库,前端按 fclose/opDate 推断)+ 9 项底部筛选弹层(特码改下拉) - 详情:3 Tab 复用 ViewPager2+Fragment,PhoneKvItem 网格;入库件数/重量按 warehouseList 求和 - 运单追踪:公共页 LogDetailActivity 双变体首例,手机竖排时间线动态构建,平板横向步骤条不动 - 修复 PhoneFilterPanel 筛选项超屏时底部按钮被挤出屏幕(内容插槽加 ScrollView) - PhoneKvItem 新增 tagColor 绿色高亮(海关放行),占位符不套标签底 - 手机首页国际 Tab 加出港查询入口;后端缺口 #21/#22 登记问题清单 - 双端实机验证通过(内网真实数据 + Proxyman Map Local 补验未入库兜底态) - skill 新增 references/proxyman.md:抓包/直调 A/B/Mock 与 api-doc 配合规范 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
8.5 KiB
Markdown
141 lines
8.5 KiB
Markdown
# 手机版公共组件(`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 → 高亮标签)、`tagColor`("orange" 默认 / "green" 放行正常态);占位符 "-"/"--" 不套标签底 |
|
||
| `PhoneFormRow` | 行式表单项(左标签 minWidth 110dp + 右值 + 1px 分隔线),覆盖新增/修改/详情三态表单 | `title`、`hint`、`type`(`PhoneFormRowType.INPUT`/`SELECT`/`DATE`/`TEXT`)、`value`(双向)、`list`(SELECT 选项)、`required`、`enable`、`numeric`、`setRefreshCallBack` |
|
||
|
||
`PhoneFormRow` 要点:SELECT 内置 XPopup 底部选择面板(点击整行弹出,无需页面代码);TEXT 为只读右对齐、
|
||
空值自动占位 "--"(详情态);type 用三元按 `pageType` 切换即可一套布局覆盖三态页面(参考
|
||
`app/res/layout/activity_uld_edit.xml`)。`PhoneBottomBar` 的 `actionDanger="@{true}"` 切浅红删除按钮。
|
||
|
||
`PhoneFilterPanel` 内容插槽已内置 ScrollView(2026-07-31 加):筛选项少时行为不变;
|
||
筛选项多到超屏(出港查询 9 项)时内容区收缩滚动,底部「重置/确认」按钮始终可见——
|
||
此前无滚动时按钮会被挤出屏幕外且毫无报错,只表现为"点不到确认"。
|
||
|
||
⚠️ 两个已踩坑:
|
||
- **FAB 盖住弹层**:浮动按钮带 elevation 时会浮在 `PhoneFilterPanel` 之上,弹层实例要加
|
||
`android:elevation="8dp"`(> FAB 的 6dp)
|
||
- **Spinner 选择回调是异步的**:`onItemSelected` 由系统 post,自动化测试里 tap 选项后立即 tap「确认」
|
||
可能抢在回值之前发出请求(表现为 UI 已显示选中但请求参数为 null)——脚本里选完要 `sleep 1` 以上;
|
||
真人操作间隔足够,不受影响
|
||
|
||
`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_btn_weak`(浅蓝弱按钮)
|
||
`bg_phone_badge` `bg_phone_tag_green/gray/red/orange` `bg_phone_card_header_blue/green`
|
||
`bg_phone_card_top_accent`(卡片顶部主色描边条,出港计重头部卡片)
|
||
- 图标:`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`(圆形复选框)
|
||
`ic_phone_clipboard`(剪贴板,白色 FAB 用)`ic_phone_weight`(砝码,可 tint)
|
||
`ic_phone_package`(件数)`ic_phone_tag`(特码/标签)
|
||
- 圆点:`bg_phone_dot_green/blue/gray`(状态指示点;蓝=出港查询卡片头,灰=时间线未达节点)
|
||
- 复用 Pad 的矢量图:`img_search` `img_scan` `img_filter`(可 `app:tint` 染色)
|
||
|
||
设计稿里的 MDI 图标若缺失,直接写 24×24 的 vector(用 MDI 官方 path),比找位图靠谱且能染色。
|