diff --git a/CLAUDE.md b/CLAUDE.md index b260621..52cdf8c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -594,6 +594,708 @@ companion object { } ``` +### 列表查询+多选+批量处理页面开发 (完整指南) + +这是项目中最常见的业务场景之一:列表查询、多选Item、批量操作。本指南基于`IntExpOutHandoverActivity`(国际出港-出库交接)的实际案例总结。 + +#### 典型场景特征 + +- ✅ 顶部多条件搜索区域 +- ✅ 列表支持多选(图片切换表示选择状态) +- ✅ 底部全选按钮+统计信息+批量操作按钮 +- ✅ 分页加载数据 + +#### 开发步骤总览 (8步) + +1. 修改/创建Bean (添加ObservableBoolean选择状态) +2. 定义API接口 (列表查询+统计查询+批量操作) +3. 创建ViewHolder (处理选择图标点击) +4. 创建ViewModel (继承BasePageViewModel) +5. 创建Activity布局 (搜索区+列表+底部栏) +6. 创建列表项布局 (使用completeSpace对齐) +7. 创建Activity (绑定数据+观察全选状态) +8. 在AndroidManifest.xml中注册 + +--- + +#### 步骤1: 修改/创建Bean + +**关键点**: 使用`ObservableBoolean`支持实时UI更新 + +```kotlin +import androidx.databinding.ObservableBoolean + +class GjcUldUseBean { + // ... 业务字段 ... + + // ========== UI扩展字段 ========== + val checked: ObservableBoolean = ObservableBoolean(false) // 选中状态 + + // 兼容现有API的isSelected属性 + var isSelected: Boolean + get() = checked.get() + set(value) = checked.set(value) +} +``` + +**为什么用ObservableBoolean而不是Boolean?** +- DataBinding会自动观察ObservableBoolean的变化 +- 调用`checked.set(true)`会立即触发UI刷新 +- 普通Boolean需要手动调用`notifyDataSetChanged()` + +--- + +#### 步骤2: 定义API接口 + +```kotlin +// Api.kt +/** + * 列表查询(分页) + */ +@POST("IntExpOutHandover/pageQuery") +suspend fun getIntExpOutHandoverList(@Body data: RequestBody): BaseListBean + +/** + * 统计查询(合计信息) + */ +@POST("IntExpOutHandover/pageQueryTotal") +suspend fun getIntExpOutHandoverTotal(@Body data: RequestBody): BaseResultBean + +/** + * 批量操作(交接完成) + */ +@POST("IntExpOutHandover/handover") +suspend fun completeHandover(@Body data: RequestBody): BaseResultBean +``` + +--- + +#### 步骤3: 创建ViewHolder + +**关键点**: 添加选择图标点击事件 + +```kotlin +class IntExpOutHandoverViewHolder(view: View) : + BaseViewHolder(view) { + + override fun onBind(item: Any?, position: Int) { + val bean = getItemBean(item) ?: return + binding.bean = bean + binding.position = position + binding.executePendingBindings() + + // 添加图标点击事件 - 切换选择状态 + binding.ivIcon.setOnClickListener { + // 反转checked状态 + bean.checked.set(!bean.checked.get()) + + // 立即更新UI (图片自动切换) + binding.executePendingBindings() + } + } +} +``` + +--- + +#### 步骤4: 创建ViewModel + +**关键点**: 继承`BasePageViewModel`,实现全选逻辑 + +```kotlin +class IntExpOutHandoverViewModel : BasePageViewModel() { + + // ========== 搜索条件 ========== + val flightDate = MutableLiveData("") + val flightNo = MutableLiveData("") + val fdest = MutableLiveData("") + val uldNo = MutableLiveData("") + + // ========== 统计信息 ========== + val totalCount = MutableLiveData("0") + val totalPc = MutableLiveData("0") + val totalWeight = MutableLiveData("0") + + // ========== 全选状态 ========== + val isAllChecked = MutableLiveData(false) + + init { + // 监听全选状态,自动更新所有列表项 + isAllChecked.observeForever { checked -> + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return@observeForever + list.forEach { it.checked.set(checked) } + pageModel.rv?.commonAdapter()?.notifyDataSetChanged() + } + } + + // ========== 适配器配置 ========== + val itemViewHolder = IntExpOutHandoverViewHolder::class.java + val itemLayoutId = R.layout.item_int_exp_out_handover + + /** + * 搜索按钮点击 + */ + fun searchClick() { + refresh() + } + + /** + * 全选按钮点击 (切换全选状态) + */ + fun checkAllClick() { + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + + // 切换全选状态 + val shouldCheckAll = !isAllChecked.value!! + list.forEach { it.checked.set(shouldCheckAll) } + isAllChecked.value = shouldCheckAll + + pageModel.rv?.commonAdapter()?.notifyDataSetChanged() + } + + /** + * 扫码ULD + */ + fun scanUld() { + ScanModel.startScan(getTopActivity(), Constant.RequestCode.ULD) + } + + /** + * 完成交接 (批量操作) + */ + fun completeHandover() { + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + val selectedItems = list.filter { it.isSelected } + + if (selectedItems.isEmpty()) { + showToast("请选择要交接的ULD") + return + } + + val requestData = selectedItems.toRequestBody() + + launchLoadingCollect({ NetApply.api.completeHandover(requestData) }) { + onSuccess = { + showToast("交接完成") + viewModelScope.launch { + FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh") + } + refresh() + } + } + } + + /** + * 获取数据 (重写BasePageViewModel) + */ + override fun getData() { + // 构建搜索条件 + val filterParams = mapOf( + "fdate" to flightDate.value?.ifEmpty { null }, + "fno" to flightNo.value?.ifEmpty { null }, + "fdest" to fdest.value?.ifEmpty { null }, + "uld" to uldNo.value?.ifEmpty { null } + ) + + // 列表参数 (含分页) + val listParams = (filterParams + mapOf( + "pageNum" to pageModel.page, + "pageSize" to pageModel.limit + )).toRequestBody() + + // 统计参数 (无分页) + val totalParams = filterParams.toRequestBody() + + // 获取列表 (带Loading) + launchLoadingCollect({ NetApply.api.getIntExpOutHandoverList(listParams) }) { + onSuccess = { pageModel.handleListBean(it) } + } + + // 获取统计信息 (后台请求,不阻塞列表) + launchCollect({ NetApply.api.getIntExpOutHandoverTotal(totalParams) }) { + onSuccess = { result -> + val data = result.data + totalCount.value = (data?.wbNumber ?: 0).toString() + totalPc.value = (data?.totalPc ?: 0).toString() + totalWeight.value = (data?.totalWeight ?: 0.0).toString() + } + } + } +} +``` + +--- + +#### 步骤5: 创建Activity布局 + +**关键要素**: 搜索区 + 列表 + 底部栏 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +#### 步骤6: 创建列表项布局 + +**关键点**: 使用`completeSpace`属性实现Key左对齐 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**权重分配原则**: +- 较长字段(如"ULD编号"、"交接时间")使用较大权重(1.5) +- 较短字段(如"总重"、"航班号")使用较小权重(1.0) +- `completeSpace`根据文字字数设置(3-5个字符宽度) + +--- + +#### 步骤7: 创建Activity + +```kotlin +@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_OUT_HANDOVER) +class IntExpOutHandoverActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_int_exp_out_handover + override fun viewModelClass() = IntExpOutHandoverViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("出库交接") + binding.viewModel = viewModel + + // 观察全选状态,更新图标透明度 + viewModel.isAllChecked.observe(this) { isAllChecked -> + binding.checkIcon.alpha = if (isAllChecked) 1.0f else 0.5f + } + + // 绑定分页 + viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) + + // 设置item点击监听 + binding.rv.addOnItemClickListener(viewModel) + + // 监听刷新事件 + FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { + viewModel.refresh() + } + + // 初始加载数据 + viewModel.refresh() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == Constant.RequestCode.ULD && resultCode == Activity.RESULT_OK) { + viewModel.uldNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT) + viewModel.searchClick() + } + } +} +``` + +--- + +#### 步骤8: 注册Activity + +```xml + + +``` + +--- + +#### 关键技术点总结 + +##### 1. ObservableBoolean vs Boolean + +| 特性 | ObservableBoolean | Boolean | +|------|------------------|---------| +| DataBinding支持 | ✅ 自动观察 | ❌ 不支持 | +| UI实时更新 | ✅ 调用set()自动刷新 | ❌ 需手动notifyDataSetChanged() | +| 代码简洁性 | ✅ 更简洁 | ❌ 需额外代码 | + +##### 2. 全选交互逻辑 + +``` +用户点击全选按钮 + ↓ +checkAllClick() 被调用 + ↓ +遍历列表,调用 bean.checked.set(shouldCheckAll) + ↓ +ObservableBoolean触发DataBinding更新 + ↓ +列表项图片自动切换 (img_plane ↔ img_plane_s) +``` + +##### 3. 图片资源切换 + +```xml + +loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}" +``` + +**图片资源**: +- `img_plane.png` - 未选中状态 +- `img_plane_s.png` - 已选中状态 (通常是高亮/彩色版本) +- `img_check_all.png` - 全选图标 + +##### 4. completeSpace属性原理 + +`completeSpace`是自定义BindingAdapter,用于实现Key左对齐: + +```kotlin +// TextViewAdapter.kt +@BindingAdapter("completeSpace") +fun completeSpace(tv: TextView, count: Int) { + // 1. 根据count个"一"字宽度设置TextView宽度 + val s = StringBuilder() + (1..count).forEach { _ -> s.append("一") } + val measureText = tv.paint.measureText(s.toString()) + ViewUtils.setWidth(tv, measureText.roundToInt()) + + // 2. 自动填充全角空格使文本均匀分布 + // 确保"航班日期:"与"航班号:"的冒号位置对齐 +} +``` + +**使用示例**: +- `completeSpace="@{5}"` - 5个"一"字宽度 (适合"ULD编号:"、"航班日期:") +- `completeSpace="@{4}"` - 4个"一"字宽度 (适合"航班号:"、"交接人:") +- `completeSpace="@{3}"` - 3个"一"字宽度 (适合"总重:"、"货重:") + +##### 5. 分页处理 + +BasePageViewModel自动处理分页逻辑: +- `pageModel.page` - 当前页码 +- `pageModel.limit` - 每页条数 +- `pageModel.handleListBean(it)` - 自动处理列表数据和分页状态 + +##### 6. 批量操作最佳实践 + +```kotlin +fun completeHandover() { + // 1. 获取列表 + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + + // 2. 过滤选中项 + val selectedItems = list.filter { it.isSelected } + + // 3. 验证 + if (selectedItems.isEmpty()) { + showToast("请选择要交接的ULD") + return + } + + // 4. 转换为RequestBody + val requestData = selectedItems.toRequestBody() + + // 5. 发起请求 + launchLoadingCollect({ NetApply.api.completeHandover(requestData) }) { + onSuccess = { + showToast("交接完成") + // 6. 发送刷新事件 + viewModelScope.launch { + FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh") + } + // 7. 刷新当前页 + refresh() + } + } +} +``` + +--- + +#### 常见错误与解决方案 + +##### 错误1: 点击图标不切换 + +```kotlin +// ❌ 错误: 使用普通Boolean +var isSelected: Boolean = false + +// ✅ 正确: 使用ObservableBoolean +val checked: ObservableBoolean = ObservableBoolean(false) +``` + +##### 错误2: 全选不生效 + +```kotlin +// ❌ 错误: 直接修改isSelected +list.forEach { it.isSelected = checked } + +// ✅ 正确: 调用ObservableBoolean的set方法 +list.forEach { it.checked.set(checked) } +``` + +##### 错误3: 布局不对齐 + +```xml + + + + + + + + +``` + +##### 错误4: 忘记观察全选状态 + +```kotlin +// ❌ 错误: 没有观察isAllChecked + +// ✅ 正确: 在Activity中观察全选状态 +viewModel.isAllChecked.observe(this) { isAllChecked -> + binding.checkIcon.alpha = if (isAllChecked) 1.0f else 0.5f +} +``` + +--- + +#### 参考示例 + +**完整实现参考**: +- Activity: `module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverActivity.kt` +- ViewModel: `module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverViewModel.kt` +- ViewHolder: `module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpOutHandoverViewHolder.kt` +- Activity布局: `module_gjc/src/main/res/layout/activity_int_exp_out_handover.xml` +- Item布局: `module_gjc/src/main/res/layout/item_int_exp_out_handover.xml` +- Bean: `module_base/src/main/java/com/lukouguoji/module_base/bean/GjcUldUseBean.kt` + +**其他类似实现**: +- `GjcAssembleAllocateActivity` - 国际出港组装分配 + +--- + ### 常见业务场景 #### 扫码