diff --git a/.claude/skills/phone-adapt/references/pad-safety.md b/.claude/skills/phone-adapt/references/pad-safety.md index 5c30f26..69b8c26 100644 --- a/.claude/skills/phone-adapt/references/pad-safety.md +++ b/.claude/skills/phone-adapt/references/pad-safety.md @@ -44,6 +44,26 @@ com/lukouguoji/gjc/databinding/ItemXxxBinding.java `activity_xxx_phone.xml` 会生成 `ActivityXxxPhoneBinding`,而字段声明的类型是 `ActivityXxxBinding`。泛型擦除让**编译期查不出来**,一进页面就 `ClassCastException`。 +### 弹框(BaseDialogModel)也要双变体,最容易漏 + +页面布局适配完,长按/批量操作弹出的 DialogModel 仍会以 Pad 居中样式出现在手机上 +(出港运抵的「状态重置」就漏过)。做法与页面完全同构: + +1. dialog 布局 `git mv` 到 `layout-sw600dp/`,`layout/` 下写同名手机版(设计稿通常是底部弹层: + `bg_phone_panel_top` + 居中标题 + `ic_phone_close` + 全宽 `bg_phone_btn_primary` 确定键) +2. 弹出位置在构造参数按形态切换(XPopup 位置与布局是两回事,都要改): + +```kotlin +class XxxDialogModel(...) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) +``` + +3. 两变体 `` 一致(imports 不要求一致) + +参考:`IntExpArriveResetDialogModel` + `dialog_int_exp_arrive_reset.xml` 双变体。 +**适配收尾时逐个 grep 该页 ViewModel 里 `DialogModel(` 的调用点**,每个弹框都过一遍。 + ## 业务层零破坏的三个模式 ### 新增查询维度:默认空值 + 仅非空进请求 diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ea40e..ae283ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ - 新增手机版资源:`bg_phone_tag_indigo`/`bg_phone_tag_amber`(申报状态三色标签)、 `bg_phone_fab_purple`/`bg_phone_fab_amber`/`bg_phone_fab_green`(长按浮层圆形操作按钮) - 后端缺口 2 项登记问题清单 #19~#20(分单补充信息保存接口不存在、Tab 无申报状态查询参数) +- 「出港运抵」对照设计稿 review 修正:状态重置弹框手机端改为设计稿底部弹层 + (**弹框适配首个先例**:`dialog_int_exp_arrive_reset.xml` 同名布局双变体,Pad 版移至 + `layout-sw600dp/`,`IntExpArriveResetDialogModel` 构造参数按 `DeviceUtil.isPhone()` 切 + `DIALOG_TYPE_BOTTOM`/`CENTER`);主列表补空态(图标 + 文案按 Tab 切换「暂无待/已运抵的运单」); + 回执页补空态兜底「暂无回执内容」(单分单入口常无回执,避免纯白屏);分单明细长按浮层按设计稿 + 补「回执」按钮(单分单跳回执页);分单页底部统计加分母「已选 N / M 项」;Tab 角标初始显示 0 ### 修复 (Fixed) - **RecyclerView + Tab 过滤新坑**:给 item 根节点加 `visibility="@{...}"` 切 Tab 会在列表里 @@ -33,12 +39,18 @@ 修饰符,Java 序列化(Intent 传递机制)会跳过其恢复,反序列化后为 null,切换选中状态时 NullPointerException 崩溃。修复:接收方 `initOnCreated` 对列表 `.map { it.copy() }` 重新 触发类体初始化补上(已实机崩溃复现并验证修复) +- `PhoneStatusTab`/`PhoneFilterPanel` 补上遗漏的 `dispatchSaveInstanceState`/ + `dispatchRestoreInstanceState` 屏蔽覆写(其余 5 个手机版复合组件均已重写,这两个是历史遗漏; + 同页多实例时子控件状态可能按 id 跨实例串档) ### 验证 (Verified) - 出港运抵双端实机验证通过:手机端 Tab 切换(角标/卡片双向正确)/全选/单选/长按菜单/查看分单 /分单明细全选与长按重置/补充信息(真实数据回填+下拉字典+提交请求体完整+后端404如实呈现)/ 回执页 全链路 crash=0、方向无闪屏;平板端布局与交互(4 搜索条+分单内嵌展开+底部 5 元素操作栏) 与改造前逐项一致 +- 出港运抵 review 修正双端实机验证通过:手机端重置底部弹层(长按→重置→下拉展开选项)/主列表空态 + /回执页空态/分单浮层重置+回执双按钮/「已选 0 / 2 项」分母 全部与设计稿一致且 crash=0; + 平板端回归:列表、批量「状态重置」弹框仍为居中 Pad 样式,行为与改造前一致 - 「出港计重」5.5 寸手机端适配(首个全链路 4+1 页模块,设计稿 03_出港计重): 待计重列表(三格统计 总票数/待计重/计重中 + 卡片状态标签 未计重/计重中 + 卡片内单票「提前运抵」+ FAB 进计重记录 + 筛选弹层 通道号/承运人/运单类型/代理人)、开始计重(头部预配统计卡 + PhoneFormRow 表单, diff --git a/CLAUDE.md b/CLAUDE.md index 347b5f6..d8503b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -975,7 +975,12 @@ Manifest 写死 `userLandscape` 时,即使 `BaseActivity` 在 `super.onCreate` 3. **Kotlin 侧不动**:`layoutId()`、`itemLayoutId`、ViewHolder 全部保持原样 4. **纯 UI 交互**(Tab 切换、弹层显隐)写在 Activity,不污染 ViewModel;状态过滤优先复用 ViewModel 既有字段 5. 手机布局顶部 `` 不变(变体自动切换) -6. **回到手机端首页加菜单入口**(最易遗忘,见下节)——不加的话页面在手机上根本进不去 +6. **弹框(BaseDialogModel)同样要适配**:dialog 布局照样走同名双变体(Pad 版移 `layout-sw600dp/`、 + 手机版在 `layout/` 写设计稿的底部弹层样式),弹出位置在构造参数按形态切换: + `BaseDialogModel(if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER)` + (参考 `IntExpArriveResetDialogModel` + `dialog_int_exp_arrive_reset.xml` 双变体)。 + 页面适配完要**逐个排查该页所有 DialogModel**,漏掉的会在手机上弹出 Pad 居中样式 +7. **回到手机端首页加菜单入口**(最易遗忘,见下节)——不加的话页面在手机上根本进不去 ### 手机端首页菜单入口(适配完必须加,否则没入口) @@ -1121,7 +1126,9 @@ override fun dispatchRestoreInstanceState(container: SparseArray) = visibility 绑定(实机截图验证过,见「常见编译错误速查」);② 通过 `Intent.putExtra(Serializable)` 传递的 `GjcHaWb`/`GjcMaWb`,其 `checked`/`showMore`/`actionMenuVisible` 字段标了 `@Transient`(= JVM `transient`,Java 序列化会跳过),反序列化后为 null,直接 - `.checked.set(...)` 会崩溃——接收方 `initOnCreated` 必须对列表 `.map { it.copy() }` 补上 + `.checked.set(...)` 会崩溃——接收方 `initOnCreated` 必须对列表 `.map { it.copy() }` 补上。 + 2026-07-30 review 修正:状态重置弹框改手机底部弹层(弹框双变体首例,见改造步骤第 6 条)、 + 主列表/回执页补空态、分单浮层补「回执」、分单统计加分母、Tab 角标初始 0 - ⏳ **待办**: - `PhoneSearchBar` 接入 AutoQuery 联想(`AutoQueryManager` 目前只对接了 Pad 系组件) - 出库交接 Tab 角标恒 0:`IntExpOutHandover/pageQueryTotal` 出参恒 0(后端缺陷,问题清单 #6), diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt index ba4c69e..f4c2b17 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt @@ -1,7 +1,9 @@ package com.lukouguoji.module_base.ui.weight.phone import android.content.Context +import android.os.Parcelable import android.util.AttributeSet +import android.util.SparseArray import android.view.View import android.widget.FrameLayout import android.widget.ImageView @@ -42,6 +44,18 @@ class PhoneFilterPanel @JvmOverloads constructor( defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { + /** + * 同一页面可能出现多个含内部 id 的手机版复合组件,系统按 id 保存/恢复视图状态会跨实例串档。 + * 这里只保存/恢复自身状态、不下发给子控件;筛选值由 ViewModel + DataBinding 持有,无需视图级恢复。 + */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } + val llPanel: LinearLayout val llContent: LinearLayout val tvTitle: TextView diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt index b16b0b3..28dd2d0 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt @@ -1,7 +1,9 @@ package com.lukouguoji.module_base.ui.weight.phone import android.content.Context +import android.os.Parcelable import android.util.AttributeSet +import android.util.SparseArray import android.view.Gravity import android.view.View import android.widget.LinearLayout @@ -37,6 +39,18 @@ class PhoneStatusTab @JvmOverloads constructor( defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { + /** + * 同一页面可能出现多个含内部 id 的手机版复合组件,系统按 id 保存/恢复视图状态会跨实例串档。 + * 这里只保存/恢复自身状态、不下发给子控件;选中值由 ViewModel + DataBinding 持有,无需视图级恢复。 + */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } + /** 双向绑定回调 */ var onChangeListener: InverseBindingListener? = null diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveSubOrderActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveSubOrderActivity.kt index 3e2259d..f67af53 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveSubOrderActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveSubOrderActivity.kt @@ -67,6 +67,12 @@ class IntExpArriveSubOrderActivity : viewModel.resetSingleClick(bean) } + /** 长按菜单「回执」(单条):复用回执只读页,仅传当前分单 */ + fun openReceipt(bean: GjcHaWb) { + bean.actionMenuVisible.set(false) + IntExpArriveReceiptActivity.start(this, viewModel.wbNo, arrayListOf(bean)) + } + fun toggleFilterPanel() { filterPanelVisible.set(!filterPanelVisible.get()) } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt index b18ae7c..3076ebf 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt @@ -6,14 +6,20 @@ import com.lukouguoji.gjc.R import com.lukouguoji.gjc.databinding.DialogIntExpArriveResetBinding import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际出港运抵 - 状态重置对话框 + * + * 布局按资源限定符自动切换:手机 layout/(底部弹层)、平板 layout-sw600dp/(居中弹框), + * 弹出位置随形态选择 BOTTOM/CENTER。 */ class IntExpArriveResetDialogModel( private val callback: (IntExpArriveResetDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 重置状态列表 val resetStatusList = MutableLiveData>() diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt index 4693b2f..4f52294 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt @@ -65,7 +65,11 @@ class IntExpArriveViewModel : BasePageViewModel() { ) /** 两个 Tab 的角标计数(当前已加载数据的客户端统计,非全量,见接口问题清单) */ - val arriveTabCounts = MutableLiveData(listOf("", "")) + val arriveTabCounts = MutableLiveData(listOf("0", "0")) + + /** 手机版空态(当前 Tab 过滤后无数据时显示,对应设计稿 empty-pending) */ + val emptyVisiblePhone = MutableLiveData(false) + val emptyTextPhone = MutableLiveData("暂无待运抵的运单") /** 手机版全选状态(仅统计待运抵 Tab 内的项,与 Pad 端 [isAllChecked] 统计口径不同) */ val isAllCheckedPhone = MutableLiveData(false) @@ -116,6 +120,8 @@ class IntExpArriveViewModel : BasePageViewModel() { val isPendingTab = currentTab.value == "pending" val filtered = allLoadedList.filter { it.isPending == isPendingTab } pageModel.rv?.commonAdapter()?.refresh(filtered) + emptyVisiblePhone.value = filtered.isEmpty() + emptyTextPhone.value = if (isPendingTab) "暂无待运抵的运单" else "暂无已运抵的运单" updateCheckAllStatusPhone() } diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_reset.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_reset.xml new file mode 100644 index 0000000..de6675c --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_reset.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml b/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml index e6523c7..c59a272 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml @@ -89,19 +89,51 @@ android:layout_height="0dp" android:layout_weight="1"> - + android:layout_height="match_parent"> + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_arrive_receipt.xml b/module_gjc/src/main/res/layout/activity_int_exp_arrive_receipt.xml index f9d7624..772a63e 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_arrive_receipt.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_arrive_receipt.xml @@ -11,6 +11,8 @@ + + @@ -24,20 +26,51 @@ - + android:layout_weight="1"> + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_arrive_sub_order.xml b/module_gjc/src/main/res/layout/activity_int_exp_arrive_sub_order.xml index 4e4b534..76c3559 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_arrive_sub_order.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_arrive_sub_order.xml @@ -82,7 +82,7 @@ android:layout_height="wrap_content" actionText='@{"申报"}' allChecked="@{viewModel.isAllChecked}" - countText='@{"已选 " + viewModel.selectedCount + " 项"}' + countText='@{"已选 " + viewModel.selectedCount + " / " + viewModel.displayList.size() + " 项"}' setOnActionClick="@{(v)-> viewModel.declareClick()}" setOnAllCheckClick="@{(v)-> activity.checkAllClick()}" /> diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml b/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml index de6675c..53d390d 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml @@ -1,8 +1,16 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - - - - + + android:layout_marginBottom="24dp"> - - + android:gravity="center" + android:text="重置状态" + android:textColor="@color/phone_text_strong" + android:textSize="18sp" + android:textStyle="bold" /> - + - + - - - - - - + + hint='@{"请选择状态"}' + list="@{model.resetStatusList}" + title='@{"状态"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={model.selectedResetStatus}" /> - - - - - + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_arrive_sub_order.xml b/module_gjc/src/main/res/layout/item_int_exp_arrive_sub_order.xml index db52c92..e421cdd 100644 --- a/module_gjc/src/main/res/layout/item_int_exp_arrive_sub_order.xml +++ b/module_gjc/src/main/res/layout/item_int_exp_arrive_sub_order.xml @@ -2,7 +2,7 @@ @@ -102,7 +102,7 @@ - + - + android:orientation="horizontal"> + + + + + +