862 lines
31 KiB
Markdown
862 lines
31 KiB
Markdown
# 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 | **事件**: FlowBus(Flow)+ 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>
|
||
|
||
<!-- 第二行 KV(marginTop=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"运单号:"=4,row2"特码:"=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`
|
||
|
||
---
|
||
|
||
### 类型 4:Tab 详情页
|
||
|
||
**代表**: `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 组件
|
||
- 在每个页面布局时,如有截图,务必尽可能还原图片上的页面设计,而不是推测假想。如有困难一律要询问,禁止自己想象
|
||
- 工具栏图标尺寸规范: `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` 后重新构建
|