feat: update claude md file

This commit is contained in:
2025-12-04 17:35:03 +08:00
parent 7f7ffcccac
commit d67c5edb19

702
CLAUDE.md
View File

@@ -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<GjcUldUseBean>
/**
* 统计查询(合计信息)
*/
@POST("IntExpOutHandover/pageQueryTotal")
suspend fun getIntExpOutHandoverTotal(@Body data: RequestBody): BaseResultBean<ManifestTotalDto>
/**
* 批量操作(交接完成)
*/
@POST("IntExpOutHandover/handover")
suspend fun completeHandover(@Body data: RequestBody): BaseResultBean<Boolean>
```
---
#### 步骤3: 创建ViewHolder
**关键点**: 添加选择图标点击事件
```kotlin
class IntExpOutHandoverViewHolder(view: View) :
BaseViewHolder<GjcUldUseBean, ItemIntExpOutHandoverBinding>(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<GjcUldUseBean> ?: 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<GjcUldUseBean> ?: 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<GjcUldUseBean> ?: 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<String>(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
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 标题栏 -->
<include layout="@layout/title_tool_bar" />
<!-- 搜索区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 航班日期 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_weight="1"
type="@{SearchLayoutType.DATE}"
value="@={viewModel.flightDate}" />
<!-- 航班号 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_weight="1"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.flightNo}" />
<!-- ULD编号 (带扫码) -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_weight="1"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.uldNo}"
icon="@{@drawable/scan_code}"
setOnIconClickListener="@{(v)-> viewModel.scanUld()}" />
<!-- 搜索按钮 -->
<ImageView
style="@style/iv_search_action"
android:onClick="@{()-> viewModel.searchClick()}"
android:src="@drawable/img_search" />
</LinearLayout>
<!-- 列表 -->
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<!-- 底部栏: 全选 + 统计 + 操作按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/color_bottom_layout"
android:gravity="center_vertical">
<!-- 全选按钮 (图标+文字) -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:onClick="@{()-> viewModel.checkAllClick()}">
<ImageView
android:id="@+id/checkIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/img_check_all" />
<TextView
android:text="全选"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<!-- 统计信息 -->
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:text='@{"合计:"+viewModel.totalCount+"票"}'
android:textColor="@color/white" />
<TextView
android:text='@{"总件数:"+viewModel.totalPc}'
android:textColor="@color/white" />
<TextView
android:text='@{"总重量:"+viewModel.totalWeight}'
android:textColor="@color/white" />
</LinearLayout>
<!-- 批量操作按钮 -->
<TextView
style="@style/tv_bottom_btn"
android:onClick="@{()-> viewModel.completeHandover()}"
android:text="交接完成" />
</LinearLayout>
</LinearLayout>
```
---
#### 步骤6: 创建列表项布局
**关键点**: 使用`completeSpace`属性实现Key左对齐
```xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_8"
android:orientation="horizontal">
<!-- 选中图标 (根据checked状态切换图片) -->
<ImageView
android:id="@+id/iv_icon"
android:layout_width="40dp"
android:layout_height="40dp"
loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}"
android:src="@drawable/img_plane" />
<!-- 数据展示区域 -->
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:orientation="vertical">
<!-- 第一行数据 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- ULD编号 (weight=1.5) -->
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1.5"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
completeSpace="@{5}"
android:text="ULD编号"
android:textColor="@color/text_normal" />
<TextView
android:text="@{bean.uld}"
android:textColor="@color/colorPrimary"
android:textStyle="bold" />
</LinearLayout>
<!-- 架子车号 (weight=1) -->
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
completeSpace="@{5}"
android:text="架子车号:"
android:textColor="@color/text_normal" />
<TextView
android:text="@{String.valueOf(bean.carId)}"
android:textColor="@color/text_normal" />
</LinearLayout>
<!-- 总重 (weight=1) -->
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
completeSpace="@{3}"
android:text="总重:"
android:textColor="@color/text_normal" />
<TextView
android:text="@{String.valueOf((int)bean.totalWeight)}"
android:textColor="@color/text_normal" />
</LinearLayout>
<!-- 装机重量 (weight=1) -->
<!-- 货重 (weight=1.5) -->
<!-- ... 其他字段 ... -->
</LinearLayout>
<!-- 第二行数据 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<!-- 航班日期 (weight=1.5) -->
<!-- 航班号 (weight=1) -->
<!-- 目的港 (weight=1) -->
<!-- 交接人 (weight=1) -->
<!-- 交接时间 (weight=1.5) -->
<!-- ... -->
</LinearLayout>
</LinearLayout>
</LinearLayout>
```
**权重分配原则**:
- 较长字段(如"ULD编号"、"交接时间")使用较大权重(1.5)
- 较短字段(如"总重"、"航班号")使用较小权重(1.0)
- `completeSpace`根据文字字数设置(3-5个字符宽度)
---
#### 步骤7: 创建Activity
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_OUT_HANDOVER)
class IntExpOutHandoverActivity :
BaseBindingActivity<ActivityIntExpOutHandoverBinding, IntExpOutHandoverViewModel>() {
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<String>(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
<!-- app/src/main/AndroidManifest.xml -->
<activity
android:name="com.lukouguoji.gjc.activity.IntExpOutHandoverActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="userLandscape" />
```
---
#### 关键技术点总结
##### 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 是自定义BindingAdapter -->
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<GjcUldUseBean> ?: 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<String>(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
<!-- ❌ 错误: 直接拼接key和value -->
<TextView android:text='@{"航班日期 " + bean.fdate}' />
<!-- ✅ 正确: 使用completeSpace属性 -->
<LinearLayout>
<TextView completeSpace="@{5}" android:text="航班日期:" />
<TextView android:text="@{bean.fdate}" />
</LinearLayout>
```
##### 错误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` - 国际出港组装分配
---
### 常见业务场景
#### 扫码