# 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`

---

### 类型 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 组件
- 在每个页面布局时，如有截图，务必尽可能还原图片上的页面设计，而不是推测假想。如有困难一律要询问，禁止自己想象
- 常用资源: `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` 后重新构建
