# 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() { 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(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 ``` **参考文件**: `module_gjc/.../GjcBoxWeighingActivity.kt`、`GjcBoxWeighingViewModel.kt` #### 列表项布局规范 (`item_xxx.xml`) **整体结构**: 水平 LinearLayout → 左侧图标 + 中间内容区(多行 KV)+ 右侧箭头 ```xml ``` **单个 KV 组件模板**: ```xml ``` **关键对齐规则**: | 规则 | 说明 | 示例 | |------|------|------| | **列 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 ?: return@observeForever list.forEach { it.checked.set(checked) } pageModel.rv?.commonAdapter()?.notifyDataSetChanged() } } fun checkAllClick() { val list = pageModel.rv?.commonAdapter()?.items as? List ?: 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) ?.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 ``` 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 ?: 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)?.forEach { if (!it.storageUseList.isNullOrEmpty()) it.showMore.set(shouldExpand) } pageModel.rv?.commonAdapter()?.notifyDataSetChanged() } ``` 4. **子列表项 checkbox 样式**(必须使用 `_style` 系列,禁止使用 `_gray` 系列): ```xml ``` | 资源 | 含义 | 外观 | |------|------|------| | `radiobtn_checked_style` | 选中 | colorPrimary 蓝色实心圆 + 白色内环 | | `radiobtn_unchecked_style` | 未选中 | 透明 + 黑色边框圆 | | ~~`radiobtn_checked_gray`~~ | ❌ 禁用 | 灰色实心圆(错误样式) | **参考文件**: `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 ``` **参考文件**: `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>(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(ConstantEvent.EVENT_REFRESH).emit("refresh") } getTopActivity().finish() } } } fun cancel() { getTopActivity().finish() } } ``` **表单布局模式**: ```xml ``` **参考文件**: `module_gjc/.../GjcQueryEditActivity.kt`、`GjcQueryEditViewModel.kt` --- ### 类型 6:添加表单页(含输入回调) **代表**: `GjcBoxWeighingAddActivity` **结构**: 类型 5 基础上 + `setRefreshCallBack` 输入完成回调 + 实时计算 + 扫码自动填充 **与类型 5 的区别**: 1. **输入完成回调**(关键特性): ```xml ``` ```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(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, private val onConfirm: () -> Unit ) : BaseDialogModel(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 必须导入**: `` 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(...).emit(...) }` | | 图片上传字段错误 | 无 `url` 字段 | 用 `result.data?.newName` | | `pageType` 绑定失效 | 非 LiveData | 用 `MutableLiveData(DetailsPageType.Add)` | | `RecyclerView items` 报错 | 不支持该属性 | Activity 中手动 `rv.commonAdapter()?.refresh(data)` | | 资源引用不存在 | drawable/color/string 缺失 | 先检查资源是否存在,不存在则创建或用已有资源 | | `View.VISIBLE` 报错 | 未导入 | XML `` 中加 `` | | `textStyle` DataBinding 报错 | 不支持 | 用固定值 `android:textStyle="bold"` | --- ## 开发检查清单 ### 新页面开发必做 1. 创建 Bean(如需)→ API 接口 → ViewHolder(列表页)→ ViewModel → Activity → 布局 2. **在 `app/src/main/AndroidManifest.xml` 注册 Activity**(最易遗忘): ```xml ``` 3. 在 `ARouterConstants` 注册路由(如需) 4. 标题栏统一用 ``,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(ConstantEvent.EVENT_REFRESH).emit("refresh") } // 接收(Activity 中,必须导入 observe) FlowBus.with(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` 后重新构建