Files
aerologic-app/CLAUDE.md
YANG JIANKUAN c9625f6bfd fix: 国际进港舱单编辑页下拉框回填及移除404接口
- 使用 DictUtils checkedValue 机制回填编辑模式下拉框选中项
- 移除已404的 searchCargoType 接口调用
- PadDataLayoutNew 增加 updateSpinnerSilently 防止 adapter 重建覆盖值
- CLAUDE.md 补充编辑表单 SPINNER 回填规范

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 17:55:19 +08:00

983 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概况
**项目名称**: AirLogistics - 航空物流信息管理系统
**架构模式**: MVVM + 组件化 | **语言**: Kotlin 1.6.21 + Java | **版本**: 1.8.4 (versionCode 84)
**SDK**: minSdk 24 / targetSdk 30 / compileSdk 31 | **Gradle**: 7.3.3 | **JDK**: 1.8
### 核心架构
- **MVVM 基类**: `BaseActivity``BaseBindingActivity`DataBinding`BaseViewModel``BasePageViewModel`(分页列表)
- **适配器**: `CommonAdapter` + `BaseViewHolder` 统一列表封装
- **路由**: ARouter 1.5.2 | **事件**: FlowBusFlow+ EventBus 3.1.1
- **网络**: Retrofit 2.6.1 + OkHttp 3.12.12 + Coroutines
- `launchCollect`:无 Loading 后台请求
- `launchLoadingCollect`:带 Loading 请求
- `toRequestBody`Map/Bean 转 JSON
### 组件化模块
| 模块 | 说明 | 模块 | 说明 |
|------|------|------|------|
| `app/` | 应用壳层 | `module_base/` | 核心基础库 |
| `module_gnc/` | 国内出港 | `module_gnj/` | 国内进港 |
| `module_gjc/` | 国际出港 | `module_gjj/` | 国际进港 |
| `module_hangban/` | 航班管理 | `module_cargo/` | 货物追踪 |
| `module_mit/` | 监装监卸 | `module_p/` | PDA 功能 |
| `Printer/` | 蓝牙打印 | `MPChartLib/` | 图表库 |
### 关键目录结构
```
aerologic-app/
├── app/src/main/java/com/lukouguoji/aerologic/
│ ├── ui/viewModel/ # ViewModel
│ ├── ui/fragment/ # Fragment
│ └── page/ # 业务页面
├── module_base/src/main/java/com/lukouguoji/module_base/
│ ├── base/ # 基类 (BaseActivity, BaseViewModel, BaseViewHolder, BaseDialogModel)
│ ├── bean/ # 数据模型
│ ├── common/ # 常量 (Constant, DetailsPageType, ConstantEvent)
│ ├── http/net/ # 网络 (NetApply, Api)
│ ├── ktx/ # 扩展函数
│ ├── impl/ # FlowBus, observe
│ ├── interfaces/ # IOnItemClickListener
│ ├── router/ # ARouterConstants
│ └── ui/weight/ # UI 组件 (PadSearchLayout, PadDataLayout, PadDataLayoutNew)
├── module_gjc/src/main/ # 国际出港(典型参考模块)
└── 其他业务模块...
```
---
## 6 种典型页面类型
基于 `module_gjc`(国际出港)模块归纳,覆盖项目中所有常见页面模式。
### 类型 1列表查询页
**代表**: `GjcBoxWeighingActivity` / `GjcInspectionActivity`
**结构**: 搜索条件区 + SmartRefreshLayout 分页列表 + 底部统计/操作栏
**Activity 骨架**:
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_XXX)
class XxxActivity : BaseBindingActivity<ActivityXxxBinding, XxxViewModel>() {
override fun layoutId() = R.layout.activity_xxx
override fun viewModelClass() = XxxViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("页面标题")
binding.viewModel = viewModel
// 绑定分页
viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this)
// 监听刷新事件
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) { viewModel.refresh() }
viewModel.refresh()
}
}
```
**ViewModel 骨架**:
```kotlin
class XxxViewModel : BasePageViewModel() {
// 搜索条件
val flightDate = MutableLiveData(DateUtils.getCurrentTime().formatDate())
val flightNo = MutableLiveData("")
// 适配器配置
val itemViewHolder = XxxViewHolder::class.java
val itemLayoutId = R.layout.item_xxx
// 统计数据
val totalCount = MutableLiveData("0")
fun searchClick() { refresh() }
override fun getData() {
val params = mapOf(
"pageNum" to pageModel.page, "pageSize" to pageModel.limit,
"fdate" to flightDate.value?.ifEmpty { null },
"fno" to flightNo.value?.ifEmpty { null }
).toRequestBody()
launchLoadingCollect({ NetApply.api.getXxxList(params) }) {
onSuccess = { pageModel.handleListBean(it) }
}
// 统计(无 Loading不阻塞列表
launchCollect({ NetApply.api.getXxxTotal(totalParams) }) {
onSuccess = { totalCount.value = (it.data?.count ?: 0).toString() }
}
}
}
```
**布局结构**:
```xml
<LinearLayout orientation="vertical">
<include layout="@layout/title_tool_bar" />
<!-- 搜索区PadSearchLayout 横排 + 操作按钮(如有) -->
<LinearLayout orientation="horizontal">
<PadSearchLayout type="@{SearchLayoutType.DATE}" value="@={viewModel.flightDate}" />
<PadSearchLayout type="@{SearchLayoutType.INPUT}" value="@={viewModel.flightNo}" />
<ImageView style="@style/iv_search_action" android:onClick="@{()-> viewModel.searchClick()}" />
<!-- 如需新增/删除按钮,尺寸规范见「开发原则」工具栏图标尺寸规范 -->
</LinearLayout>
<!-- 分页列表 -->
<SmartRefreshLayout android:id="@+id/srl" layout_weight="1">
<RecyclerView android:id="@+id/rv"
itemLayoutId="@{viewModel.itemLayoutId}" viewHolder="@{viewModel.itemViewHolder}" />
</SmartRefreshLayout>
<!-- 底部统计栏 -->
<LinearLayout background="@color/color_bottom_layout" height="50dp">
<TextView text='@{"合计:" + viewModel.totalCount + "票"}' />
</LinearLayout>
</LinearLayout>
```
**参考文件**: `module_gjc/.../GjcBoxWeighingActivity.kt``GjcBoxWeighingViewModel.kt`
#### 列表项布局规范 (`item_xxx.xml`)
**整体结构**: 水平 LinearLayout → 左侧图标 + 中间内容区(多行 KV+ 右侧箭头
```xml
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginVertical="5dp"
android:background="@drawable/bg_item"
android:orientation="horizontal"
android:padding="10dp">
<!-- 左侧图标(普通列表用 img_plane多选列表用 img_plane/img_plane_s 切换) -->
<ImageView
android:id="@+id/iv_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/img_plane" />
<!-- 中间内容区 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:orientation="vertical">
<!-- 第一行 KV -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- KV 组件(见下方单个 KV 模板) -->
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 第二行 KVmarginTop=10dp -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<!-- KV 组件weight 和 completeSpace 必须与第一行对应位置相同) -->
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
<!-- 右侧箭头(固定写法) -->
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:src="@drawable/img_pda_right" />
</androidx.appcompat.widget.LinearLayoutCompat>
```
**单个 KV 组件模板**:
```xml
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:gravity="center_vertical">
<TextView
completeSpace="@{5}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签名:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.fieldName}" />
</androidx.appcompat.widget.LinearLayoutCompat>
```
**关键对齐规则**:
| 规则 | 说明 | 示例 |
|------|------|------|
| **列 weight 一致** | 第一行和第二行**相同位置**的 KV 组件必须使用相同的 `layout_weight` | 第一行第 1 列 `weight=1.0`,第二行第 1 列也必须 `weight=1.0` |
| **列 completeSpace 取最大值** | 同一列位置的 completeSpace 取两行中**较大**的那个值1 个汉字 = 1 宽度1 个标点 = 1 宽度) | 第一行"运单号:"=4第二行"特码:"=3 → 两行都用 `completeSpace=4` |
| **右侧箭头统一** | 固定使用 `@drawable/img_pda_right`,尺寸 `30x30dp``layout_gravity="center"``marginLeft="10dp"` | — |
**completeSpace 计算方法**: 统计 Label 字符数(含冒号),每个汉字算 1每个标点算 1。示例
- "运单号:" = 3字 + 1标点 = 4
- "运单类型:" = 4字 + 1标点 = 5
- "状态:" = 2字 + 1标点 = 3
- "始发站:" = 3字 + 1标点 = 4此处"始发站:"虽然有4个字符位但""也要占1个宽度
同列两行的 completeSpace 统一取 `max(row1, row2)`。例如第 1 列row1"运单号:"=4row2"特码:"=3 → 两行都用 4。
**常用字段 weight 参考表**(基于国际出港模块统计):
| 字段类型 | 典型 weight | 常见范围 | 典型 completeSpace |
|----------|------------|----------|-------------------|
| 运单号 | 1.0 | 0.9~1.2 | 4 |
| 件数 | 1.2 | 0.8~1.2 | 3~5 |
| 重量 | 0.8 | 0.7~1.0 | 3~5 |
| 状态 | 0.8 | 0.7~0.8 | 3~4 |
| 代理 | 0.8 | 0.7~0.8 | 3~4 |
| 特码 | 1.0 | 0.9~1.0 | 3~4 |
| 始发站/目的站 | 0.8 | 0.7~0.8 | 4 |
| 运单类型/业务类型 | 1.2 | 1.0~1.2 | 5 |
| 分单数 | 0.8 | 0.6~0.8 | 4 |
| 航班号/航班 | 1.0~1.2 | 1.0~1.2 | 4~5 |
| 时间类(入库/离港/过磅) | 1.0~1.2 | 1.0~1.2 | 5 |
> **原则**: 相同字段在不同页面应使用相近的 weight优先参照同模块已有布局。若新页面与已有页面**字段完全相同**,应直接复用其 weight 和 completeSpace 配置。
**典型 weight 分布示例**5列运单号/状态/代理/件数/重量 + 特码/始发站/目的站/运单类型/分单数):
```
位置: 第1列 第2列 第3列 第4列 第5列
weight: 1.0 0.8 0.8 1.2 0.8 ← 第一行
weight: 1.0 0.8 0.8 1.2 0.8 ← 第二行(必须相同)
cSpace: 4 4 4 5 4 ← 第一行
cSpace: 4 4 4 5 4 ← 第二行(必须相同,取 max
```
**参考文件**: `module_gjc/.../item_int_exp_tally.xml`(典型)、`item_gjc_query.xml``item_gjc_box_weighing.xml`
---
### 类型 2多选列表 + 批量操作页
**代表**: `IntExpOutHandoverActivity` / `GjcAssembleAllocateActivity`
**结构**: 类型 1 基础上 + 全选按钮 + ObservableBoolean 选中状态 + 批量操作
**与类型 1 的区别**:
1. **Bean 增加 ObservableBoolean**:
```kotlin
class XxxBean {
val checked: ObservableBoolean = ObservableBoolean(false)
var isSelected: Boolean
get() = checked.get()
set(value) = checked.set(value)
}
```
2. **ViewModel 增加全选逻辑**:
```kotlin
val isAllChecked = MutableLiveData(false)
init {
isAllChecked.observeForever { checked ->
val list = pageModel.rv?.commonAdapter()?.items as? List<XxxBean> ?: return@observeForever
list.forEach { it.checked.set(checked) }
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
}
fun checkAllClick() {
val list = pageModel.rv?.commonAdapter()?.items as? List<XxxBean> ?: return
val shouldCheckAll = !isAllChecked.value!!
list.forEach { it.checked.set(shouldCheckAll) }
isAllChecked.value = shouldCheckAll
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
fun batchAction() {
val selected = (pageModel.rv?.commonAdapter()?.items as? List<XxxBean>)
?.filter { it.isSelected } ?: return
if (selected.isEmpty()) { showToast("请选择数据"); return }
launchLoadingCollect({ NetApply.api.batchXxx(selected.toRequestBody()) }) {
onSuccess = { showToast("操作成功"); refresh() }
}
}
```
3. **Activity 观察全选图标**:
```kotlin
viewModel.isAllChecked.observe(this) { binding.checkIcon.alpha = if (it) 1.0f else 0.5f }
```
4. **Item 布局图片切换**:
```xml
<ImageView android:id="@+id/iv_icon"
loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}" />
```
5. **ViewHolder 中处理点击**:
```kotlin
binding.ivIcon.setOnClickListener {
bean.checked.set(!bean.checked.get())
binding.executePendingBindings()
}
```
**参考文件**: `module_gjc/.../IntExpOutHandoverActivity.kt``IntExpOutHandoverViewModel.kt`
---
### 类型 3嵌套多选列表页
**代表**: `IntExpStorageUseActivity`
**结构**: 主列表含子列表(展开/收起)+ 主子联动全选 + Dialog 操作
**与类型 2 的区别**:
1. **Activity 暴露给布局**(用于调用 Dialog 方法):
```kotlin
binding.activity = this // XML 中可调用 activity.showXxxDialog()
```
2. **联动全选(主+子列表)**:
```kotlin
isAllChecked.observeForever { checked ->
val list = pageModel.rv?.commonAdapter()?.items as? List<GjcMaWb> ?: return@observeForever
list.forEach {
it.checked.set(checked)
it.storageUseList?.forEach { sub -> sub.checked.set(checked) }
}
}
```
3. **全局展开/收起**:
```kotlin
val isAllExpanded = MutableLiveData(false)
fun toggleAllExpand() {
val shouldExpand = !isAllExpanded.value!!
isAllExpanded.value = shouldExpand
(pageModel.rv?.commonAdapter()?.items as? List<GjcMaWb>)?.forEach {
if (!it.storageUseList.isNullOrEmpty()) it.showMore.set(shouldExpand)
}
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
```
4. **子列表项 checkbox 样式**(必须使用 `_style` 系列,禁止使用 `_gray` 系列):
```xml
<!-- 子列表项 item_xxx_sub.xml 中的 iv_checkbox -->
<ImageView
android:id="@+id/iv_checkbox"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_weight="0.5"
loadImage="@{bean.checked.get() ? @drawable/radiobtn_checked_style : @drawable/radiobtn_unchecked_style}"
android:src="@drawable/radiobtn_unchecked_style" />
```
| 资源 | 含义 | 外观 |
|------|------|------|
| `radiobtn_checked_style` | 选中 | colorPrimary 蓝色实心圆 + 白色内环 |
| `radiobtn_unchecked_style` | 未选中 | 透明 + 黑色边框圆 |
| ~~`radiobtn_checked_gray`~~ | ❌ 禁用 | 灰色实心圆(错误样式) |
#### 含子列表的列表项 UI 规范(以 `item_int_exp_storage_use.xml` 为基准)
**主列表项卡片**
| 部位 | 属性 | 标准值 |
|------|------|--------|
| 外层容器 | marginHorizontal / marginTop | 10dp / 10dp |
| 卡片背景 | background | `@drawable/bg_white_radius_8` |
| 内容区 | padding | **10dp** |
| 选中图标 | 尺寸 / marginEnd | 40×40dp / 10dp |
| 选中图标 | 切换资源 | `img_plane_s`(选中)/ `img_plane`(未选中)|
| KV 文字 | textSize | **15sp**Key 和 Value 均需显式设置)|
| 首要字段值(运单号)| textColor | `@color/colorPrimary` |
| 其他字段 | textColor | 无需设置(继承默认 text_normal|
| 两行间距 | layout_marginTop | 10dp |
**展开/折叠按钮(`iv_show`**
| 属性 | 标准值 |
|------|--------|
| layout_height | **18dp** |
| padding | **4dp** |
| layout_marginBottom | 5dp**不设 marginTop**|
| src | `@mipmap/img_down` |
| 显示控制 | `visible="@{bean.subList != null && !bean.subList.empty}"` |
**子列表区域**
- 容器:`layout_marginTop="5dp"``background="#e3f6e0"`
- 表头行:`layout_marginVertical="10dp"``paddingHorizontal="10dp"`
- 表头文字:`textSize="14sp"``textColor="@color/text_normal"``textStyle="bold"``gravity="center"`
- 表头下方分隔线:`MaterialDivider` 高度 1px`background="@color/c999999"`
- 子列表项 padding`paddingHorizontal="10dp"``paddingVertical="8dp"`
- 子列表文字:`textSize="14sp"``textColor="@color/text_normal"``gravity="center"``layout_gravity="center_vertical"`
**子列表复选框(关键)**
```xml
<ImageView
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_weight="0.5"
loadImage="@{bean.checked.get() ? @drawable/radiobtn_checked_style : @drawable/radiobtn_unchecked_style}"
android:src="@drawable/radiobtn_unchecked_style" />
```
> `loadImage` 和 `android:src` **均须**使用 `_style` 系列,**禁止**使用 `_gray` 系列(参见上方复选框样式表)
**参考文件**: `module_gjc/.../IntExpStorageUseActivity.kt``IntExpStorageUseViewModel.kt`
---
### 类型 4Tab 详情页
**代表**: `GjcQueryDetailsActivity`
**结构**: 自定义 Tab 栏 + ViewPager2 + 多 Fragment
**Activity 骨架**:
```kotlin
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("查询详情")
binding.viewModel = viewModel
viewModel.initOnCreated(intent)
// ViewPager2 配置
binding.vp.adapter = CustomVP2Adapter(viewModel.fragmentList, supportFragmentManager, lifecycle)
binding.vp.isUserInputEnabled = false // 禁用滑动
binding.vp.offscreenPageLimit = 3
// Tab 切换
viewModel.currentTab.observe(this) { binding.vp.setCurrentItem(it, false) }
viewModel.loadDetails()
}
companion object {
@JvmStatic
fun start(context: Context, id: Long?) {
context.startActivity(Intent(context, XxxDetailsActivity::class.java)
.putExtra(Constant.Key.ID, id?.toString() ?: ""))
}
}
```
**ViewModel 骨架**:
```kotlin
class XxxDetailsViewModel : BaseViewModel() {
val currentTab = MutableLiveData(0)
val fragmentList by lazy {
listOf(
FragmentA.newInstance(this),
FragmentB.newInstance(this),
FragmentC.newInstance(this)
)
}
fun onTabClick(index: Int) { currentTab.value = index }
}
```
**Tab 布局模式**:
```xml
<!-- 自定义 Tab非 TabLayout -->
<LinearLayout height="40dp" orientation="horizontal">
<LinearLayout width="100dp" gravity="center" onClick="@{()->viewModel.onTabClick(0)}" orientation="vertical">
<TextView text="运单信息"
textColor="@{viewModel.currentTab == 0 ? @color/colorPrimary : @color/text_gray}" />
<View height="3dp"
background="@{viewModel.currentTab == 0 ? @color/colorPrimary : @color/transparent}"
visibility="@{viewModel.currentTab == 0 ? View.VISIBLE : View.INVISIBLE}" />
</LinearLayout>
<!-- 更多 Tab... -->
</LinearLayout>
<ViewPager2 android:id="@+id/vp" layout_weight="1" />
```
**参考文件**: `module_gjc/.../GjcQueryDetailsActivity.kt``GjcQueryDetailsViewModel.kt`
---
### 类型 5编辑表单页
**代表**: `GjcQueryEditActivity`
**结构**: ScrollView + PadDataLayoutNew 表单(只读+可编辑混合)+ 保存/取消
**Activity 骨架**:
```kotlin
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("运单修改")
binding.viewModel = viewModel
viewModel.initOnCreated(intent)
}
companion object {
@JvmStatic
fun start(context: Context, bean: XxxBean) {
context.startActivity(Intent(context, XxxEditActivity::class.java)
.putExtra(Constant.Key.DATA, Gson().toJson(bean)))
}
}
```
**ViewModel 骨架**:
```kotlin
class XxxEditViewModel : BaseViewModel() {
val dataBean = MutableLiveData(XxxBean())
val packageTypeList = MutableLiveData<List<KeyValue>>(emptyList())
fun initOnCreated(intent: Intent) {
val json = intent.getStringExtra(Constant.Key.DATA) ?: ""
if (json.isNotEmpty()) {
val bean = Gson().fromJson(json, XxxBean::class.java)
loadDropdownLists()
loadDetails(bean.id)
}
}
fun submit() {
val bean = dataBean.value ?: return
if (bean.wbNo.verifyNullOrEmpty("运单号不能为空")) return
launchLoadingCollect({ NetApply.api.updateXxx(bean.toRequestBody()) }) {
onSuccess = {
showToast("修改成功")
viewModelScope.launch { FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh") }
getTopActivity().finish()
}
}
}
fun cancel() { getTopActivity().finish() }
}
```
**表单布局模式**:
```xml
<ScrollView layout_weight="1" fillViewport="true">
<LinearLayout padding="15dp" orientation="vertical">
<LinearLayout background="@drawable/bg_white_radius_8" padding="15dp" orientation="vertical">
<!-- 三列表单行 -->
<LinearLayout orientation="horizontal">
<PadDataLayoutNew layout_weight="1" enable="@{false}" title='@{"运单号"}'
type="@{DataLayoutType.INPUT}" value='@{viewModel.dataBean.wbNo}' />
<PadDataLayoutNew layout_weight="1" enable="@{true}" title='@{"特码"}'
type="@{DataLayoutType.INPUT}" value='@={viewModel.dataBean.spCode}' />
<PadDataLayoutNew layout_weight="1" title='@{"包装类型"}'
type="@{DataLayoutType.SPINNER}" list="@{viewModel.packageTypeList}"
value='@={viewModel.dataBean.packageType}' />
</LinearLayout>
<!-- 备注(多行) -->
<PadDataLayoutNew inputHeight="@{80}" title='@{"备注"}'
type="@{DataLayoutType.INPUT}" value='@={viewModel.dataBean.remark}' />
</LinearLayout>
<!-- 底部按钮 -->
<LinearLayout gravity="center" marginTop="24dp">
<TextView style="@style/tv_bottom_btn" width="120dp" onClick="@{()-> viewModel.cancel()}" text="取消" />
<TextView style="@style/tv_bottom_btn" width="120dp" onClick="@{()-> viewModel.submit()}" text="保存" />
</LinearLayout>
</LinearLayout>
</ScrollView>
```
**参考文件**: `module_gjc/.../GjcQueryEditActivity.kt``GjcQueryEditViewModel.kt`
---
### 类型 6添加表单页含输入回调
**代表**: `GjcBoxWeighingAddActivity`
**结构**: 类型 5 基础上 + `setRefreshCallBack` 输入完成回调 + 实时计算 + 扫码自动填充
**与类型 5 的区别**:
1. **输入完成回调**(关键特性):
```xml
<!-- 使用方法引用,不能用 Lambda -->
<PadDataLayoutNew
setRefreshCallBack="@{viewModel::onCarIdInputComplete}"
title='@{"架子车号"}' type="@{DataLayoutType.INPUT}" value='@={viewModel.carId}' />
```
```kotlin
private var lastQueriedCarId = ""
fun onCarIdInputComplete() {
val id = carId.value
if (!id.isNullOrEmpty() && id != lastQueriedCarId) {
lastQueriedCarId = id
queryFlatcarInfo(id) // 输入完成后自动查询
}
}
```
2. **级联查询**(航班日期+航班号同时有值时查询):
```kotlin
fun onFlightNoInputComplete() { queryFlightIfReady() }
fun onFlightDateInputComplete() { lastQueriedFlight = ""; queryFlightIfReady() }
private fun queryFlightIfReady() {
val fdate = flightDate.value; val fno = flightNo.value
if (!fdate.isNullOrEmpty() && !fno.isNullOrEmpty()) {
val key = "$fdate-$fno"
if (key != lastQueriedFlight) { lastQueriedFlight = key; queryFlightInfo(fdate, fno) }
}
}
```
3. **实时计算**:
```kotlin
fun initOnCreated(activity: Activity) {
totalWeight.observe(activity as LifecycleOwner) {
val total = it?.toDoubleOrNull() ?: 0.0
val net = total - (dataBean.value?.carWeight ?: 0.0)
val cargo = net - (dataBean.value?.uldWeight ?: 0.0)
netWeight.value = if (net > 0) net.toString() else "0"
cargoWeight.value = if (cargo > 0) cargo.toString() else "0"
}
}
```
4. **输入限制**:
```kotlin
// Activity 中设置
binding.carIdInput.et.setUpperCaseAlphanumericFilter()
```
5. **重置功能**:
```kotlin
fun resetClick() {
dataBean.value = XxxBean()
carId.value = ""; uldNo.value = ""; flightNo.value = ""
lastQueriedCarId = ""; lastQueriedUld = ""; lastQueriedFlight = ""
}
```
**参考文件**: `module_gjc/.../GjcBoxWeighingAddActivity.kt``GjcBoxWeighingAddViewModel.kt`
---
## 自定义 Dialog 开发模式
基于 `BaseDialogModel`XPopup 封装),支持 5 种弹窗类型。
### 基础模板
```kotlin
class XxxDialogModel(
private val onConfirm: () -> Unit
) : BaseDialogModel<DialogXxxBinding>(DIALOG_TYPE_CENTER) { // CENTER/BOTTOM/DRAWER/FULL
override fun layoutId() = R.layout.dialog_xxx
override fun onDialogCreated(context: Context) {
binding.model = this
}
fun onConfirmClick() { onConfirm(); dismiss() }
}
// 使用
XxxDialogModel(onConfirm = { refresh() }).show()
```
### 弹窗类型
| 类型 | 常量 | 场景 | 示例 |
|------|------|------|------|
| 底部 | `DIALOG_TYPE_BOTTOM` | 操作选项 | 默认类型 |
| 中间 | `DIALOG_TYPE_CENTER` | 确认框、表单输入 | `ConfirmDialogModel` |
| 抽屉 | `DIALOG_TYPE_DRAWER` | 筛选条件 | `GjcQueryFilterDialogModel` |
| 全屏 | `DIALOG_TYPE_FULL` | 列表展示 | `NoticeMessageDialogModel` |
### 抽屉弹窗(筛选场景)
```kotlin
class XxxFilterDialogModel(
val filterField: MutableLiveData<String>,
private val onConfirm: () -> Unit
) : BaseDialogModel<DialogXxxFilterBinding>(DIALOG_TYPE_DRAWER) {
override fun onBuild(builder: XPopup.Builder) {
builder.popupPosition(PopupPosition.Right)
val width = DevUtils.getTopActivity().window.decorView.width / 3
builder.maxWidth(width).popupWidth(width)
}
override fun onDialogCreated(context: Context) {
binding.model = this
binding.lifecycleOwner = context as? LifecycleOwner
}
fun onResetClick() { filterField.value = "" }
fun onConfirmClick() { dismiss(); onConfirm() }
}
```
**参考文件**: `module_base/.../BaseDialogModel.kt``module_gjc/.../dialog/`
---
## 命名与文件组织
| 类型 | 命名 | 目录 |
|------|------|------|
| Activity | `XxxActivity` | `模块/page/``模块/activity/` |
| ViewModel | `XxxViewModel` | `模块/viewModel/` |
| ViewHolder | `XxxViewHolder` | `模块/holder/` |
| Adapter | `XxxAdapter` | `模块/adapter/` |
| Bean | `XxxBean` | `module_base/bean/` |
| Dialog | `XxxDialogModel` | `模块/dialog/` |
| 布局 | `activity_xxx.xml` / `item_xxx.xml` / `dialog_xxx.xml` | `res/layout/` |
---
## 构建命令
```bash
./gradlew clean # 清理
./gradlew assembleDebug # Debug APK
./gradlew assembleRelease # Release APK已签名
./gradlew installDebug # 安装到设备
adb devices -l # 查看设备
adb logcat | grep "com.lukouguoji.aerologic" # 日志
```
---
## DataBinding 关键规则
1. **lifecycleOwner 必须设置**`BaseBindingActivity` 已自动设置,手动使用时 `binding.lifecycleOwner = this`
2. **字符串拼接用反引号**: `@{` + `` ` `` + `姓名:` + `` ` `` + ` + viewModel.name}`
3. **LiveData 自动解包**: XML 中直接 `viewModel.dataBean.name`,不写 `.value`
4. **修改对象属性需重新赋值**: `dataBean.value = dataBean.value?.copy(name = "新值")`
5. **双向绑定用 `@={}`**: `value="@={viewModel.searchText}"`
6. **点击事件用 Lambda**: `onClick="@{() -> viewModel.submit()}"`
7. **setRefreshCallBack 用方法引用**: `setRefreshCallBack="@{viewModel::methodName}"`(不能用 Lambda
8. **使用 View.VISIBLE/GONE 必须导入**: `<import type="android.view.View" />`
9. **textStyle 不支持 DataBinding**: 只能用固定值如 `android:textStyle="bold"`
---
## UI 组件速查
### PadSearchLayout搜索区
| type | 用途 | 示例 |
|------|------|------|
| `SearchLayoutType.INPUT` | 文本输入 | `value="@={viewModel.flightNo}"` |
| `SearchLayoutType.DATE` | 日期选择 | `value="@={viewModel.flightDate}"` |
| `SearchLayoutType.SPINNER` | 下拉选择 | `list="@{viewModel.statusList}" value="@={viewModel.status}"` |
支持扫码图标: `icon="@{@mipmap/scan_code}" setOnIconClickListener="@{(v)-> viewModel.scan()}"`
### PadDataLayoutNew表单区
| type | 用途 | 关键属性 |
|------|------|----------|
| `DataLayoutType.INPUT` | 文本/多行输入 | `enable`, `required`, `maxLength`, `inputHeight`, `hint` |
| `DataLayoutType.SPINNER` | 下拉选择 | `list`, `hint` |
| `DataLayoutType.DATE` | 日期选择 | `hint` |
通用属性: `title='@{"标题"}'``titleLength="@{5}"``value='@={viewModel.field}'`
回调属性: `setRefreshCallBack="@{viewModel::onInputComplete}"`
### completeSpace 对齐
`completeSpace="@{5}"` 设置 Key 文本宽度(以"一"字宽度为单位),用于 Key-Value 布局对齐。
---
## Import 路径速查
### 基类与常用类
| 类 | 正确路径 |
|----|----------|
| `BaseActivity` | `com.lukouguoji.module_base.base.BaseActivity` |
| `BaseBindingActivity` | `com.lukouguoji.module_base.base.BaseBindingActivity` |
| `BaseViewModel` | `com.lukouguoji.module_base.base.BaseViewModel` |
| `BasePageViewModel` | `com.lukouguoji.module_base.base.BasePageViewModel` |
| `BaseViewHolder` | `com.lukouguoji.module_base.base.BaseViewHolder` |
| `BaseDialogModel` | `com.lukouguoji.module_base.base.BaseDialogModel` |
| `CustomVP2Adapter` | `com.lukouguoji.module_base.base.CustomVP2Adapter` |
| `Constant` | `com.lukouguoji.module_base.common.Constant` |
| `DetailsPageType` | `com.lukouguoji.module_base.common.DetailsPageType` |
| `ConstantEvent` | `com.lukouguoji.module_base.common.ConstantEvent` |
| `NetApply` | `com.lukouguoji.module_base.http.net.NetApply` |
| `FlowBus` | `com.lukouguoji.module_base.impl.FlowBus` |
| `observe`FlowBus 扩展) | `com.lukouguoji.module_base.impl.observe` |
| `IOnItemClickListener` | `com.lukouguoji.module_base.interfaces.IOnItemClickListener` |
| `ARouterConstants` | `com.lukouguoji.module_base.router.ARouterConstants` |
### 扩展函数(均在 `com.lukouguoji.module_base.ktx` 包下)
`launchCollect``launchLoadingCollect``showToast``toRequestBody``verifyNullOrEmpty``noNull``formatDate`
---
## 常见编译错误速查
| 错误 | 原因 | 修复 |
|------|------|------|
| `DetailsPageType` 找不到 | 包名错误 | `common.DetailsPageType`,非 `constant.` |
| `DataLayoutType.INTEGER` | 不存在 | 用 `DataLayoutType.INPUT` |
| `DetailsPageType.Edit` | 不存在 | 用 `DetailsPageType.Modify` |
| `IOnItemClickListener` 找不到 | 包名错误 | `interfaces.`,非 `impl.` |
| `FlowBus.observe` 无法调用 | 未导入扩展 | 单独导入 `com.lukouguoji.module_base.impl.observe` |
| `FlowBus.emit()` 报错 | 需在协程中 | `viewModelScope.launch { FlowBus.with<String>(...).emit(...) }` |
| 图片上传字段错误 | 无 `url` 字段 | 用 `result.data?.newName` |
| `pageType` 绑定失效 | 非 LiveData | 用 `MutableLiveData(DetailsPageType.Add)` |
| `RecyclerView items` 报错 | 不支持该属性 | Activity 中手动 `rv.commonAdapter()?.refresh(data)` |
| 资源引用不存在 | drawable/color/string 缺失 | 先检查资源是否存在,不存在则创建或用已有资源 |
| `View.VISIBLE` 报错 | 未导入 | XML `<data>` 中加 `<import type="android.view.View" />` |
| `textStyle` DataBinding 报错 | 不支持 | 用固定值 `android:textStyle="bold"` |
---
## 编辑表单下拉框SPINNER回填规范
编辑页面DetailsPageType.Modify下拉框需要根据已有数据自动选中对应项。**必须使用 `DictUtils``checkedValue` 参数**,禁止依赖组件自动匹配 value。
### 原理
`DictUtils``handleCallBack` 会将 `checkedValue` 匹配的 `KeyValue` 置于列表首位。`PadDataLayoutNew` 的 SPINNER 默认显示列表第 0 项,因此匹配项自动成为选中项,无需额外设置 selectedIndex。
### 标准做法(参考 `GjjManifestDetailsViewModel`、`GjjManifestAddViewModel`
1. **字典加载必须在编辑数据加载之后**(不能放在 `init` 中),确保 `checkedValue` 可用
2. **编辑模式传入 `checkedValue`**,新增模式传 `null`
3. **编辑模式不预置空 `KeyValue("", "")`**(否则空项会占据首位,覆盖 checkedValue 排序)
```kotlin
fun initOnCreated(intent: Intent) {
// 1. 先解析页面类型和编辑数据
if (pageType.value == DetailsPageType.Modify) {
loadManifestFromBean(bean) // 设置 agent.value、specialCode.value 等
}
// 2. 再加载字典列表(此时 checkedValue 已可用)
loadDictLists()
}
private fun loadDictLists() {
val isModify = pageType.value == DetailsPageType.Modify
DictUtils.getXxxList(
addAll = false,
checkedValue = if (isModify) field.value else null // 编辑模式传值,新增传 null
) {
xxxList.postValue(if (isModify) it else listOf(KeyValue("", "")) + it)
}
}
```
### checkedValue 取值规则
提交时用的哪个字段值,`checkedValue` 就传哪个。对照 `toKeyValue()``value` 字段确认匹配:
| DictUtils 方法 | KeyValue.value 来源 | checkedValue 示例 |
|---|---|---|
| 通用(`handleCallBack` | `DictBean.code` | `manifest.agentCode`(如 "SFINT" |
| `getShouYunPackageTypeList` | `PackageBean.name` | `manifest.packageType`(如 "木框" |
### 禁止做法
- ❌ 在 `init` 中加载字典(编辑数据尚未可用,无法传 `checkedValue`
- ❌ 依赖 `PadDataLayoutNew``value` 属性自动匹配列表Spinner adapter 重建时 `onItemSelected` 回调会覆盖已有值)
- ❌ 编辑模式下在列表前添加 `KeyValue("", "")`(会干扰 `checkedValue` 置顶排序)
---
## 开发检查清单
### 新页面开发必做
1. 创建 Bean如需→ API 接口 → ViewHolder列表页→ ViewModel → Activity → 布局
2. **在 `app/src/main/AndroidManifest.xml` 注册 Activity**(最易遗忘):
```xml
<activity android:name="com.lukouguoji.gjc.activity.XxxActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false" android:screenOrientation="userLandscape" />
```
3.`ARouterConstants` 注册路由(如需)
4. 标题栏统一用 `<include layout="@layout/title_tool_bar" />`Activity 中 `setBackArrow("标题")`
### 常见业务操作
**扫码**:
```kotlin
fun scanWaybill() { ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL) }
// Activity.onActivityResult 中: waybillNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT)
```
**图片上传**: `UploadUtil.upload(filePath)``result.data?.newName`(注意是 `newName``url`
**刷新事件**:
```kotlin
// 发送ViewModel 中,必须在协程中)
viewModelScope.launch { FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh") }
// 接收Activity 中,必须导入 observe
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) { viewModel.refresh() }
```
**静态启动方法**:
```kotlin
companion object {
@JvmStatic
fun start(context: Context, id: String) {
context.startActivity(Intent(context, XxxActivity::class.java).putExtra(Constant.Key.ID, id))
}
}
```
---
## 开发原则
- 资源引用必须存在 — 创建布局前确认 drawable/color/string 资源真实存在或主动创建
- 标题栏统一用 `title_tool_bar` — 禁止手动编写 Toolbar
- 优先使用 PadDataLayoutNew 和 PadSearchLayout 组件
- 在每个页面布局时,如有截图,务必尽可能还原图片上的页面设计,而不是推测假想。如有困难一律要询问,禁止自己想象
- 工具栏图标尺寸规范: `img_search` 36dp + padding 2dp`img_add` 40dp 无 padding使用 `drawable/img_add.xml` 矢量图,`drawable-xhdpi/img_add.png` 已废弃删除)
- 常用资源: `bg_white_radius_8``colorPrimary``text_normal``text_gray``color_bottom_layout`
### 环境配置
- **服务器**: `module_base/.../res/values/strings.xml``system_url_inner` / `weight_url`
- **签名**: `key.jks`(根目录),密码 `123321`,别名 `key`
- **模块独立运行**: `gradle.properties``isBuildModule=true`
### 错误排查流程
1. Import 错误 → 查上方 Import 速查表
2. 资源引用错误 → 检查 drawable/color/string 是否存在
3. DataBinding 错误 → 检查 import、枚举值、View 类导入
4. suspend function 错误 → 在 `viewModelScope.launch` 中调用
5. 仍有问题 → `./gradlew clean` 后重新构建