Files
aerologic-app/CLAUDE.md
YANGJIANKUAN 97ef9b4679 fix: 修复国际进港原始舱单接口解析及列表项布局规范
- API返回类型从BaseResultBean<PageInfo>改为PageInfo,匹配服务端实际响应
- GjjAirManifest字段maWbList重命名为maWb匹配JSON键名
- 列表项布局weight和completeSpace按国际出港规范对齐
- 移除页面初始自动加载,需用户输入航班号后手动查询
- CLAUDE.md补充列表项布局规范及常用字段weight参考表

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:06:20 +08:00

860 lines
31 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()
}
```
**参考文件**: `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"` |
---
## 开发检查清单
### 新页面开发必做
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 组件
- 在每个页面布局时,如有截图,务必尽可能还原图片上的页面设计,而不是推测假想。如有困难一律要询问,禁止自己想象
- 常用资源: `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` 后重新构建