- 手机版列表(三色状态标签卡片 + 筛选弹层 + 浮动新增 + 全选/批量删除)、 一套 activity_uld_edit 手机布局按 pageType 覆盖 ULD 新增/修改/详情三态 (详情右上铅笔进修改、状态底部选择面板、所在港默认 HFE、手机端必填校验); 平板布局迁至 layout-sw600dp,Kotlin 双端共用,平板端实机回归零变化 - 新增公共组件 PhoneFormRow(行式表单,INPUT/SELECT/DATE/TEXT 四形态, SELECT 内置底部选择面板);PhoneBottomBar 增加 actionDanger 危险按钮样式; 新增 FAB/danger按钮/浅红标签/垃圾桶/铅笔等资源 - ULDBean 补解析接口既有出参 ifNo/efNo/checkInDate/checkOutDate 并预留 source; 批量删除以队列串行调用单条 deleteUld 实现 - 手机首页「国际」Tab 接入 ULD管理入口(ARouter + ComprehensiveUld 权限), 登录→菜单→页面全链路实机验证通过,方向无闪屏,crash=0 - 后端缺口 5 项(状态三态、来源字段、筛选入参、批量删除接口、字段语义) 登记《后端接口对接问题清单.xlsx》#7~#11;skill 沉淀本次踩坑 (FAB elevation 盖弹层、Spinner 异步回调竞态、登录哈希回填) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.7 KiB
手机版公共组件(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 → 橙色高亮标签) |
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}" 切浅红删除按钮。
⚠️ 两个已踩坑:
- 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:
<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 的组件都要写:
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) =
dispatchFreezeSelfOnly(container)
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) =
dispatchThawSelfOnly(container)
取值本来就由 ViewModel + DataBinding 持有,不需要视图级恢复,屏蔽掉没有副作用。
配套的第二道防线:多形态组件里,文本回写要限定形态,别让隐藏控件影响取值:
et.doOnTextChanged { text, _, _, _ ->
if (type == PhoneDataLayoutType.INPUT) value = text.toString()
}
遇到"值被神秘清空"怎么定位
在 setter 里打一条带堆栈的日志,一次就能看到真凶:
android.util.Log.d("DBG", "value '$field' -> '$v'", Throwable("trace"))
出库交接那次的栈是 TextView.onRestoreInstanceState → EditText.setText → doOnTextChanged,
指向非常明确。排完记得删日志。
新增组件的写法约定
照现有 7 个组件的模式来,保持一致:
- 继承
LinearLayout/FrameLayout,init里inflate(context, R.layout.layout_phone_xxx, this) - 内部布局用
<merge tools:parentTag="android.widget.LinearLayout">,避免多一层嵌套 - 属性做成 Kotlin
var+ setter 里立即生效;双向绑定值用onChangeListener: InverseBindingListener? - BindingAdapter 统一加到
PhoneWidgetKtx.kt,函数名带组件名前缀避免顶层函数重名 - 有互斥形态时,
type要先于value生效(把 type 放进同一个多属性 adapter 的第一个?.let),否则 value 会写进错误的子控件 - 加上上面那两个
dispatch*SelfOnly覆写 - 完成后同步更新
CLAUDE.md的组件表和记忆文件phone-5.5inch-adaptation.md
资源
配色一律用 phone_*,背景用 bg_phone_*,图标用 ic_phone_*,禁止写死色值。
已有的(不够再加,加完更新 CLAUDE.md):
- 色:
phone_primaryphone_page_bgphone_bgphone_stat_bgphone_text_title/body/hint/label/strong/weak_btnphone_dividerphone_badge_bgphone_greenphone_check_borderphone_tag_gray_*phone_tag_green_*phone_tag_red_*phone_orange_bg/text - 背景:
bg_phone_cardbg_phone_searchbg_phone_inputbg_phone_stat_boxbg_phone_panel_topbg_phone_icon_roundbg_phone_btn_primarybg_phone_btn_outlinebg_phone_badgebg_phone_tag_green/gray/red/orangebg_phone_card_header_blue/green - 图标:
ic_phone_airplaneic_phone_locationic_phone_personic_phone_check_circleic_phone_chevron_right/downic_phone_calendaric_phone_closeic_phone_check_round_checked/unchecked(圆形复选框) - 复用 Pad 的矢量图:
img_searchimg_scanimg_filter(可app:tint染色)
设计稿里的 MDI 图标若缺失,直接写 24×24 的 vector(用 MDI 官方 path),比找位图靠谱且能染色。