Files
aerologic-app/CLAUDE.md
2025-11-14 12:15:33 +08:00

535 lines
14 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
项目开发指南 - 航空物流App
## 项目概述
**AirLogistics** - Android原生应用航空物流全流程管理
- **包名**: com.lukouguoji.aerologic
- **版本**: 1.7.9 (API 24-30)
- **架构**: MVVM + 组件化 + Kotlin + DataBinding
- **屏幕**: 横屏 1152dp × 720dp
## 快速构建
```bash
./gradlew assembleDebug # 构建Debug版本
./gradlew clean # 清理构建
```
## 核心架构
### MVVM层级
```
Activity → BaseBindingActivity → ViewModel → BaseViewModel/BasePageViewModel → API
```
### 关键基类
- **BaseBindingActivity**: DataBinding + ViewModel自动绑定
- **BaseViewModel**: Loading管理、协程支持
- **BasePageViewModel**: 分页列表(含PageModel)
- **CommonAdapter + BaseViewHolder**: 列表适配器
- **PadSearchLayout**: 搜索区域输入控件
- **PadDataLayout**: 数据展示/编辑控件
### 标准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
// 初始化UI
}
}
```
### 标准ViewModel模板
**列表页ViewModel:**
```kotlin
class XxxListViewModel : BasePageViewModel() {
val searchText = MutableLiveData<String>()
val itemLayoutId = R.layout.item_xxx
val itemViewHolder = XxxViewHolder::class.java
override fun getData() {
val params = mapOf(
"page" to pageModel.page,
"limit" to pageModel.limit,
"searchText" to searchText.value
).toRequestBody()
launchLoadingCollect({ NetApply.api.getXxxList(params) }) {
onSuccess = { pageModel.handleListBean(it) }
}
}
override fun onItemClick(position: Int, type: Int) {
val bean = pageModel.rv!!.commonAdapter()!!.getItem(position) as XxxBean
// 跳转详情
}
}
```
**详情页ViewModel:**
```kotlin
class XxxDetailsViewModel : BaseViewModel() {
var id = ""
val dataBean = MutableLiveData<XxxBean>()
fun initOnCreated(intent: Intent) {
id = intent.getStringExtra(Constant.Key.ID) ?: ""
getData()
}
private fun getData() {
launchLoadingCollect({ NetApply.api.getXxxDetails(id) }) {
onSuccess = { dataBean.value = it.data ?: XxxBean() }
}
}
}
```
**编辑页ViewModel:**
```kotlin
class XxxAddViewModel : BaseViewModel() {
val pageType = MutableLiveData(DetailsPageType.Add) // 必须用LiveData
var id = ""
val dataBean = MutableLiveData(XxxBean())
fun initOnCreated(intent: Intent) {
pageType.value = DetailsPageType.valueOf(
intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
)
if (pageType.value != DetailsPageType.Add) {
id = intent.getStringExtra(Constant.Key.ID) ?: ""
loadData()
}
}
fun submit() {
val bean = dataBean.value ?: return
if (bean.name.verifyNullOrEmpty("请输入名称")) return
launchLoadingCollect({
val params = mapOf("id" to id, "name" to bean.name)
.toRequestBody(removeEmptyOrNull = true)
NetApply.api.saveXxx(params)
}) {
onSuccess = {
showToast("保存成功")
viewModelScope.launch {
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
}
getTopActivity().finish()
}
}
}
}
```
## 网络请求
### 请求方法
```kotlin
// 带Loading请求
launchLoadingCollect({ NetApply.api.saveXxx(params) }) {
onSuccess = { /* 成功处理 */ }
onFailed = { code, msg -> /* 失败处理 */ }
}
// 无Loading请求(后台刷新)
launchCollect({ NetApply.api.getXxx() }) {
onSuccess = { /* 成功处理 */ }
}
// 参数转换
val params = mapOf("key" to "value").toRequestBody(removeEmptyOrNull = true)
```
### API接口定义
```kotlin
// 位置: module_base/.../http/net/Api.kt
@POST("api/xxx/list")
suspend fun getXxxList(@Body data: RequestBody): BaseListBean<XxxBean>
@POST("api/xxx/details")
suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean<XxxBean>
@POST("api/xxx/save")
suspend fun saveXxx(@Body data: RequestBody): BaseResultBean<SimpleResultBean>
```
## 核心UI组件
### PadSearchLayout - 搜索输入框
```xml
<!-- 文本输入+扫码 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.waybillNo}"
hint="@{`请输入运单号`}"
icon="@{@mipmap/scan_code}"
setOnIconClickListener="@{(v)-> viewModel.scanWaybill()}" />
<!-- 日期选择 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
type="@{SearchLayoutType.DATE}"
value="@={viewModel.date}"
icon="@{@mipmap/calendar}" />
<!-- 下拉选择 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
type="@{SearchLayoutType.SPINNER}"
list="@{viewModel.statusList}"
value="@={viewModel.status}" />
```
**类型**: `INPUT` / `INTEGER` / `SPINNER` / `DATE`
### PadDataLayout - 数据展示/编辑
```xml
<!-- 文本输入 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
type="@{DataLayoutType.INPUT}"
title='@{"运单号:"}'
titleLength="@{5}"
value='@={viewModel.bean.waybillNo}'
enable="@{viewModel.pageType != DetailsPageType.Details}"
required="@{true}"
maxLength="@{11}" />
<!-- 下拉选择 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
type="@{DataLayoutType.SPINNER}"
title='@{"状态:"}'
list="@{viewModel.statusList}"
value='@={viewModel.bean.status}' />
<!-- 多行输入 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
type="@{DataLayoutType.INPUT}"
inputHeight="@{100}"
value='@={viewModel.bean.remark}' />
```
**类型**: `INPUT` / `SPINNER` / `DATE`
## 开发检查清单
### ⚠️ 重要提醒
**新建Activity后必须在AndroidManifest.xml中注册否则会报ActivityNotFoundException错误**
### 列表页开发(8步)
1. **创建Bean** (`module_base/.../bean/XxxBean.kt`)
2. **添加API接口** (`Api.kt``getXxxList()`)
3. **创建ViewHolder** (继承`BaseViewHolder`)
4. **创建ViewModel** (继承`BasePageViewModel`)
5. **创建Activity** (继承`BaseBindingActivity`)
6. **创建Layout** (`activity_xxx_list.xml` + `item_xxx.xml`)
7. **注册路由** (`ARouterConstants`)
8. **⚠️ 在AndroidManifest.xml中注册Activity** (`app/src/main/AndroidManifest.xml`)
**AndroidManifest.xml注册示例:**
```xml
<!-- 在app/src/main/AndroidManifest.xml的<application>标签内添加 -->
<activity
android:name="com.lukouguoji.gnc.page.xxx.XxxActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="userLandscape" />
```
**关键代码:**
```kotlin
// Activity中绑定分页
viewModel.pageModel.bindSmartRefreshLayout(
binding.srl, binding.recyclerView, viewModel, this
)
binding.recyclerView.addOnItemClickListener(viewModel)
```
### 详情页开发(5步)
1. **添加API接口** (`getXxxDetails()`)
2. **创建ViewModel** (继承`BaseViewModel`)
3. **创建Activity** (含`companion object`静态start方法)
4. **创建Layout**
5. **⚠️ 在AndroidManifest.xml中注册Activity**
**静态启动方法:**
```kotlin
companion object {
@JvmStatic
fun start(context: Context, id: String) {
val starter = Intent(context, XxxDetailsActivity::class.java)
.putExtra(Constant.Key.ID, id)
context.startActivity(starter)
}
}
```
### 编辑页开发(6步)
1. **添加API接口** (`saveXxx()` + `getXxxDetails()`)
2. **创建ViewModel** (`pageType`使用`MutableLiveData`)
3. **创建Activity** (多个静态start方法: `startForAdd/Edit/Details`)
4. **创建Layout** (根据`pageType`控制enable)
5. **FlowBus发送刷新事件**
6. **⚠️ 在AndroidManifest.xml中注册Activity**
**Activity多入口:**
```kotlin
companion object {
@JvmStatic
fun startForAdd(context: Context) {
val starter = Intent(context, XxxAddActivity::class.java)
.putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Add.name)
context.startActivity(starter)
}
@JvmStatic
fun startForEdit(context: Context, id: String) {
/* ... DetailsPageType.Modify ... */
}
}
```
## 常见业务场景
### 扫码
```kotlin
fun scanWaybill() {
ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == Constant.RequestCode.WAYBILL && resultCode == Activity.RESULT_OK) {
waybillNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT)
search()
}
}
```
### 图片上传
```kotlin
val result = UploadUtil.upload(filePath)
if (result.verifySuccess()) {
val imageUrl = result.data?.newName ?: "" // 注意是newName不是url
}
```
### 列表刷新事件
```kotlin
// 发送事件(在ViewModel中)
viewModelScope.launch {
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
}
// 接收事件(在Activity中)
import com.lukouguoji.module_base.impl.observe // 必须导入
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) {
viewModel.refresh()
}
```
## 常用扩展函数
```kotlin
// Toast
showToast("提示信息")
// 验证非空
if (text.verifyNullOrEmpty("请输入内容")) return
// 空处理
val text = nullableString.noNull("默认值")
// 日期格式化
val dateStr = Date().formatDate() // "2025-11-12"
// 权限申请
permission(Manifest.permission.CAMERA) { openCamera() }
```
## 常见编译错误
### 1. DetailsPageType包名错误
```xml
<!-- ❌ 错误 -->
<import type="com.lukouguoji.module_base.constant.DetailsPageType" />
<!-- ✅ 正确 -->
<import type="com.lukouguoji.module_base.common.DetailsPageType" />
```
### 2. DataLayoutType枚举值错误
```xml
<!-- ❌ 错误: INTEGER不存在 -->
type="@{DataLayoutType.INTEGER}"
<!-- ✅ 正确: 使用INPUT -->
type="@{DataLayoutType.INPUT}"
```
**可用类型**: `INPUT` / `SPINNER` / `DATE`
### 3. DetailsPageType枚举值错误
```kotlin
// ❌ 错误: Edit不存在
DetailsPageType.Edit
// ✅ 正确: 使用Modify
DetailsPageType.Modify
```
**可用类型**: `Add` / `Modify` / `Details`
### 4. IOnItemClickListener包名错误
```kotlin
// ❌ 错误
import com.lukouguoji.module_base.impl.IOnItemClickListener
// ✅ 正确
import com.lukouguoji.module_base.interfaces.IOnItemClickListener
```
### 5. FlowBus使用错误
```kotlin
// ❌ 错误: observe需要单独导入
import com.lukouguoji.module_base.impl.FlowBus
// ✅ 正确
import com.lukouguoji.module_base.impl.FlowBus
import com.lukouguoji.module_base.impl.observe
// ❌ 错误: emit必须在协程中
FlowBus.with<String>(event).emit("data")
// ✅ 正确
viewModelScope.launch {
FlowBus.with<String>(event).emit("data")
}
```
### 6. 图片上传字段错误
```kotlin
// ❌ 错误: UploadBean没有url字段
val imageUrl = result.data?.url
// ✅ 正确: 使用newName字段
val imageUrl = result.data?.newName
```
### 7. pageType必须用LiveData
```kotlin
// ❌ 错误: DataBinding无法绑定
var pageType: DetailsPageType = DetailsPageType.Add
// ✅ 正确: 使用LiveData
val pageType = MutableLiveData(DetailsPageType.Add)
```
### 8. RecyclerView不支持items属性
```xml
<!-- ❌ 错误: items属性会导致编译错误 -->
<RecyclerView
items="@{viewModel.list}" />
<!-- ✅ 正确: 在Activity中手动更新 -->
<RecyclerView android:id="@+id/recyclerView" />
```
```kotlin
// Activity中
viewModel.list.observe(this) { data ->
binding.recyclerView.commonAdapter()?.refresh(data)
}
```
### 9. Constant.Key.PAGE_TYPE未定义
`module_base/.../common/Constant.kt`中添加:
```kotlin
object Key {
const val ID = "id"
const val PAGE_TYPE = "pageType" // 添加这个
}
```
## 错误排查流程
1. **DataBinding错误** → 检查import包名、枚举值
2. **Unresolved reference** → 检查import语句、常量定义
3. **suspend function错误** → 在`viewModelScope.launch`中调用
4. **仍有问题**`./gradlew clean` 后重新构建
## 快速修复命令
```bash
# 查找DetailsPageType位置
grep -r "enum class DetailsPageType" module_base/src --include="*.kt"
# 查找IOnItemClickListener位置
find module_base/src -name "IOnItemClickListener.kt"
# 查找DataLayoutType枚举值
grep -A 5 "enum class DataLayoutType" module_base/src --include="*.kt"
```
## 开发原则
- ✅ 优先使用项目现有基类和封装
- ✅ 充分利用PadDataLayout和PadSearchLayout
- ✅ 遵循统一命名规范
- ✅ pageType用LiveData不用普通变量
- ✅ FlowBus.emit()必须在协程中调用
- ✅ 图片上传使用newName字段
- ✅ RecyclerView手动更新adapter不用items属性
## 技术栈
- Kotlin + 协程 1.6.0
- Retrofit 2.6.1 + OkHttp 3.12.12
- DataBinding + LiveData
- ARouter 1.5.2
- SmartRefreshLayout 2.0.3
- Glide 4.15.1
---
**签名配置**: `key.jks` / 密码: `123321` / 别名: `key`