diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 709befa..2a2427a 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -17,7 +17,8 @@
"Bash(tee:*)",
"Bash(git stash:*)",
"Bash(jar tf:*)",
- "Bash(xargs -I {} sh -c 'echo \"\"\"\"=== {} ===\"\"\"\" && jar tf {} 2>/dev/null | grep -i \"\"\"\"gprinter\"\"\"\" | head -5')"
+ "Bash(xargs -I {} sh -c 'echo \"\"\"\"=== {} ===\"\"\"\" && jar tf {} 2>/dev/null | grep -i \"\"\"\"gprinter\"\"\"\" | head -5')",
+ "Bash(xmllint:*)"
],
"deny": [],
"ask": []
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 41c0dca..332d4aa 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,2096 +1,89 @@
# CLAUDE.md
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+项目开发指南 - 航空物流App
## 项目概述
-**AirLogistics (航空物流信息App)** - Android原生应用,用于管理航空物流的全流程操作,包括国内外货物进出港、仓储管理、车辆调度等核心业务。
+**AirLogistics** - Android原生应用,航空物流全流程管理
- **包名**: com.lukouguoji.aerologic
-- **当前版本**: 1.7.9 (versionCode 79)
-- **开发语言**: Kotlin + Java混合
-- **架构模式**: MVVM + 组件化
-- **最低SDK**: Android 7.0 (API 24)
-- **目标SDK**: Android 10 (API 30)
+- **版本**: 1.7.9 (API 24-30)
+- **架构**: MVVM + 组件化 + Kotlin + DataBinding
+- **屏幕**: 横屏 1152dp × 720dp
-## 构建与运行
-
-### 环境准备
-
-1. **依赖下载问题解决**:
- - 下载gradle-7.3.3-bin.zip: https://pan.baidu.com/s/18wsuGRlNxjMYbxLhBH9yeg (提取码: 1029)
- - 打开 Settings -> Build, Execution, Deployment > Build Tools > Gradle
- - 将下载的文件解压后替换到 "Gradle user home" 目录中
-
-2. **配置IP地址**:
- - 内网地址配置在 `module_base/src/main/res/values/strings.xml` 中的 `system_url_inner`
- - 地磅地址: `weight_url`
- - 运行时可通过SharedPreferences修改IP地址
-
-### 构建命令
+## 快速构建
```bash
-# 组件化开发模式切换
-# 编辑 gradle.properties 中的 isBuildModule
-# true: 模块可独立运行调试
-# false: 模块作为library集成(默认)
-
-# 构建Debug版本
-./gradlew assembleDebug
-
-# 构建Release版本(已签名)
-./gradlew assembleRelease
-
-# 安装到设备
-./gradlew installDebug
-
-# 清理构建
-./gradlew clean
-```
-
-### 测试命令
-
-```bash
-# 运行单元测试
-./gradlew test
-
-# 运行特定模块的测试
-./gradlew :module_base:test
-./gradlew :app:test
-
-# 运行UI测试
-./gradlew connectedAndroidTest
+./gradlew assembleDebug # 构建Debug版本
+./gradlew clean # 清理构建
```
## 核心架构
-### 模块化结构
-
-项目采用**组件化架构**,通过`isBuildModule`参数控制模块独立运行或作为library集成:
-
-- **app**: 应用壳层,整合所有业务模块,提供主界面框架
-- **module_base**: 核心基础库(可独立运行),提供所有通用能力
-- **module_gnc**: 国内出港业务(收运、复磅、装机等)
-- **module_gnj**: 国内进港业务(卸机、提货、移库等)
-- **module_gjc**: 国际出港业务(板箱组装、ULD管理等)
-- **module_gjj**: 国际进港业务(舱单、理货、交接等)
-- **module_hangban**: 航班查询管理
-- **module_cargo**: 货物追踪查询
-- **module_mit**: 监装监卸管理
-- **module_p**: PDA专用功能
-- **Printer**: 蓝牙打印模块(佳博SDK)
-- **MPChartLib**: 定制图表库
-
-### MVVM架构模式
-
-所有业务页面遵循统一的MVVM模式:
+### MVVM层级
```
-Activity/Fragment (View层)
- ↓ 继承
-BaseBindingActivity
- ↓ 持有
-ViewModel (业务逻辑层)
- ↓ 继承
-BaseViewModel / BasePageViewModel
- ↓ 调用
-Repository (数据层: Retrofit API)
+Activity → BaseBindingActivity → ViewModel → BaseViewModel/BasePageViewModel → API
```
-**关键基类**:
-- `BaseActivity`: 协程支持、Loading管理、扫码功能、键盘控制
-- `BaseBindingActivity`: 提供DataBinding和ViewModel自动绑定
-- `BaseViewModel`: 提供Loading管理、Lifecycle感知、Activity结果处理
-- `BasePageViewModel`: 扩展分页列表功能、PageModel集成
-- `CommonAdapter + BaseViewHolder`: 列表适配器统一封装
+### 关键基类
-## 基类架构详解
+- **BaseBindingActivity**: DataBinding + ViewModel自动绑定
+- **BaseViewModel**: Loading管理、协程支持
+- **BasePageViewModel**: 分页列表(含PageModel)
+- **CommonAdapter + BaseViewHolder**: 列表适配器
+- **PadSearchLayout**: 搜索区域输入控件
+- **PadDataLayout**: 数据展示/编辑控件
-### BaseActivity
+### 标准Activity模板
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt`
-
-**核心能力**:
-- **协程支持**: 实现`CoroutineScope`,自动管理协程生命周期
-- **Loading管理**: 内置LoadingDialog,支持30秒超时自动关闭
-- **扫码功能**: 封装ZXing扫码,自动处理相机权限申请
-- **键盘控制**: 点击空白区域自动隐藏软键盘
-- **Activity管理**: 通过ActivityCollector统一管理生命周期
-- **字体锁定**: 强制字体大小不随系统设置变化
-- **屏幕适配**: 集成AutoSize自动适配横屏1152dp × 720dp
-
-**关键方法**:
-```kotlin
-// 显示/隐藏Loading
-fun loading()
-fun loadingCancel()
-
-// 扫码功能
-fun scanCode(requestCode: Int)
-
-// 设置标题栏
-open fun setBackArrow(title: String)
-```
-
-### BaseBindingActivity
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt`
-
-**设计特点**:
-- 使用**ViewBinding/DataBinding**自动绑定视图
-- 自动创建和管理ViewModel
-- 生命周期自动绑定到lifecycleOwner
-- 简化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("页面标题")
-
- // 绑定ViewModel到布局
binding.viewModel = viewModel
-
- // 初始化其他UI组件
- initRecyclerView()
- initListeners()
+ // 初始化UI
}
}
```
-### BaseViewModel
+### 标准ViewModel模板
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BaseViewModel.kt`
+**列表页ViewModel:**
-**核心能力**:
-- **Loading管理**: 提供`showLoading()`/`dismissLoading()`
-- **Activity结果处理**: `onActivityResult()`回调
-- **顶层Activity获取**: `getTopActivity()`
-- **生命周期感知**: 继承自AndroidX ViewModel
-
-**核心方法**:
-```kotlin
-abstract class BaseViewModel : ViewModel(), ILoading {
- // Loading管理
- override fun showLoading()
- override fun dismissLoading()
-
- // Activity结果处理
- open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
-
- // 获取顶层Activity
- fun getTopActivity(): Activity
-}
-```
-
-### BasePageViewModel
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BasePageViewModel.kt`
-
-**核心特性**:
-- 继承自`BaseViewModel`
-- 集成**PageModel**自动处理分页逻辑
-- 实现**IGetData**接口,统一`getData()`方法
-- 实现**IOnItemClickListener**接口,处理列表点击
-
-**标准使用模板**:
```kotlin
class XxxListViewModel : BasePageViewModel() {
-
- // LiveData定义
- val searchText = MutableLiveData()
- val dataList = MutableLiveData>()
-
- // 适配器配置(在布局中使用)
- val itemLayoutId = R.layout.item_xxx
- val itemViewHolder = XxxViewHolder::class.java
-
- // 实现数据加载
- override fun getData() {
- val requestBody = mapOf(
- "page" to pageModel.page,
- "limit" to pageModel.limit,
- "searchText" to searchText.value
- ).toRequestBody()
-
- launchLoadingCollect({
- NetApply.api.getXxxList(requestBody)
- }) {
- onSuccess = {
- pageModel.handleListBean(it) // 自动处理分页数据
- }
- }
- }
-
- // 实现列表点击
- override fun onItemClick(position: Int, type: Int) {
- val bean = pageModel.rv!!.commonAdapter()!!.getItem(position) as XxxBean
- // 跳转详情页
- XxxDetailsActivity.start(getTopActivity(), bean.id)
- }
-}
-```
-
-### PageModel - 分页工具类
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/model/PageModel.kt`
-
-**核心功能**:
-```kotlin
-class PageModel {
- var page: Int = 1 // 当前页码
- var limit: Int = 20 // 每页数量
- var rv: RecyclerView? // 列表引用
-
- // 绑定SmartRefreshLayout(在Activity中调用)
- fun bindSmartRefreshLayout(
- srl: SmartRefreshLayout,
- rv: RecyclerView,
- getData: IGetData,
- lifecycleOwner: LifecycleOwner
- )
-
- // 处理列表数据(在ViewModel中调用)
- fun handleListBean(listBean: BaseListBean<*>?)
-}
-```
-
-**在Activity中使用**:
-```kotlin
-override fun initOnCreate(savedInstanceState: Bundle?) {
- setBackArrow("列表页面")
- binding.viewModel = viewModel
-
- // 绑定分页逻辑
- viewModel.pageModel.bindSmartRefreshLayout(
- binding.srl, // SmartRefreshLayout
- binding.recyclerView, // RecyclerView
- viewModel, // 实现了IGetData的ViewModel
- this // LifecycleOwner
- )
-
- // 绑定列表点击
- binding.recyclerView.addOnItemClickListener(viewModel)
-}
-```
-
-### CommonAdapter - 通用列表适配器
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/CommonAdapter.kt`
-
-**核心方法**:
-```kotlin
-class CommonAdapter(
- val context: Context,
- private val layoutId: Int,
- private val viewHolderClass: Class>
-) : RecyclerView.Adapter>()
-
-// 数据管理
-fun refresh(list: List?) // 刷新数据(清空后重新加载)
-fun loadMore(list: List?) // 加载更多(追加数据)
-fun addItem(item: Any?) // 添加单个项
-fun removeItem(position: Int) // 删除项
-fun getItem(position: Int): Any? // 获取项
-
-// 事件监听
-fun addOnItemClickListener(listener: IOnItemClickListener)
-```
-
-**使用示例**:
-```kotlin
-// 在Activity中创建适配器
-val adapter = CommonAdapter(
- context = this,
- layoutId = R.layout.item_xxx,
- viewHolderClass = XxxViewHolder::class.java
-)
-binding.recyclerView.adapter = adapter
-
-// 刷新数据
-adapter.refresh(dataList)
-
-// 添加点击事件
-adapter.addOnItemClickListener(viewModel)
-```
-
-### BaseViewHolder - ViewHolder基类
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BaseViewHolder.kt`
-
-**标准实现模板**:
-```kotlin
-class XxxViewHolder(view: View) :
- BaseViewHolder(view) {
-
- override fun onBind(item: Any?, position: Int) {
- // 获取数据Bean
- val bean = getItemBean(item) ?: return
-
- // 使用DataBinding绑定数据
- binding.apply {
- this.bean = bean
- tvName.text = bean.name
- tvDate.text = bean.date
-
- // 设置整个条目可点击
- notifyItemClick(position, root)
-
- // 或设置特定按钮可点击
- notifyItemClick(position, btnEdit)
- notifyItemClick(position, btnDelete)
- }
- }
-}
-```
-
-## 网络请求架构详解
-
-### ServiceCreator - Retrofit创建器
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/http/user/ServiceCreator.kt`
-
-**核心配置**:
-```kotlin
-object ServiceCreator {
- // 动态获取IP地址(可在运行时修改)
- var ipAddress = SharedPreferenceUtil.getString(Constant.Share.ipAddress)
- .noNull(MyApplication.context.getString(R.string.system_url_inner))
-
- fun createRetrofit(): Retrofit {
- return Retrofit.Builder()
- .baseUrl(ipAddress)
- .client(httpClientBuilder(SelfLoginInterceptor).build())
- .addConverterFactory(FastJsonConverterFactory())
- .build()
- }
-}
-```
-
-**拦截器功能**:
-- 自动添加`Authorization: Bearer {token}`
-- 添加`timestamp`时间戳
-- 统一处理401(未登录)自动跳转登录页
-- 统一处理500错误并Toast提示
-- 打印请求和响应日志(Debug模式)
-
-### NetApply - API统一入口
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/http/net/NetApply.kt`
-
-```kotlin
-object NetApply {
- private const val DEFAULT_TIMEOUT: Long = 30000L
- var api: Api by Delegates.notNull()
-
- // Gson实例(支持日期格式化、空值处理)
- val gson: Gson = GsonBuilder()
- .setDateFormat(DevFinal.TIME.yyyyMMddHHmmss_HYPHEN)
- .registerTypeAdapterFactory(NullStringAdapterFactory())
- .serializeNulls()
- .create()
-}
-```
-
-### Api接口定义
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt`
-
-**标准接口模式**:
-```kotlin
-interface Api {
- companion object {
- var BASE_URL = ServiceCreator.ipAddress
- }
-
- // 通用POST请求
- @POST
- suspend fun simplePost(
- @Url url: String,
- @Body body: RequestBody = mapOf("" to "").toRequestBody()
- ): BaseResultBean
-
- // 具体业务接口示例
-
- // 列表查询(返回分页数据)
- @POST("DomExpCheckIn/search")
- suspend fun getGncShouYunList(@Body data: RequestBody): BaseListBean
-
- // 详情查询(返回单个对象)
- @POST("DomExpCheckIn/queryWbByNo")
- suspend fun getGncShouYunDetails(@Query("wbNo") wbNo: String): BaseResultBean
-
- // 新增/编辑/删除(返回成功标志)
- @POST("DomExpCheckIn/save")
- suspend fun saveGncShouYun(@Body data: RequestBody): BaseResultBean
-
- // 文件上传
- @Multipart
- @POST("api/upload")
- suspend fun uploadFile(@Part file: MultipartBody.Part): BaseResultBean
-}
-```
-
-### RequestKtx - 请求扩展函数
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ktx/RequestKtx.kt`
-
-#### (1) launchCollect - 无Loading请求
-
-适用场景:后台刷新、非关键操作、不需要阻塞用户操作的请求
-
-```kotlin
-fun Any.launchCollect(
- block: suspend () -> T,
- resultBuilder: ResultBuilder.() -> Unit
-)
-```
-
-**使用示例**:
-```kotlin
-launchCollect({
- NetApply.api.getXxxList(params.toRequestBody())
-}) {
- onSuccess = { result ->
- // 成功处理
- dataList.value = result.data
- }
- onFailed = { code, message ->
- // 失败处理(默认已showToast)
- Log.e("TAG", "请求失败: $message")
- }
- onComplete = {
- // 请求完成(无论成功失败)
- isRefreshing.value = false
- }
-}
-```
-
-#### (2) launchLoadingCollect - 带Loading请求
-
-适用场景:关键操作、提交表单、需要用户等待的请求
-
-```kotlin
-fun ILoading.launchLoadingCollect(
- block: suspend () -> T,
- resultBuilder: ResultBuilder.() -> Unit
-)
-```
-
-**使用示例**:
-```kotlin
-launchLoadingCollect({
- NetApply.api.saveXxx(params.toRequestBody())
-}) {
- onSuccess = { result ->
- showToast("保存成功")
- finish() // 关闭当前页面
- }
- onFailed = { code, message ->
- // 失败处理(已自动显示Toast和关闭Loading)
- }
-}
-```
-
-#### (3) toRequestBody - 数据转换
-
-```kotlin
-fun Any.toRequestBody(removeEmptyOrNull: Boolean = false): RequestBody
-```
-
-**功能**:
-- 将Map或Bean自动转换为JSON格式的RequestBody
-- `removeEmptyOrNull = true`: 移除空字符串和null值字段
-
-**使用示例**:
-```kotlin
-val params = mapOf(
- "page" to 1,
- "limit" to 20,
- "waybillNo" to "", // 空字符串
- "status" to null // null值
-)
-
-// 不移除空值
-params.toRequestBody()
-// 生成: {"page":1,"limit":20,"waybillNo":"","status":null}
-
-// 移除空值
-params.toRequestBody(removeEmptyOrNull = true)
-// 生成: {"page":1,"limit":20}
-```
-
-### BaseResultBean - 统一返回格式
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/bean/BaseResultBean.kt`
-
-```kotlin
-open class BaseResultBean {
- var msg: String? = null // 消息提示
- var status: String = "" // 状态码("1"=成功,其他=失败)
- var data: T? = null // 实际数据
-
- fun verifySuccess(): Boolean {
- return status == "1" // 判断是否成功
- }
-}
-```
-
-**BaseListBean - 分页返回**:
-```kotlin
-class BaseListBean {
- var pages = 1 // 总页数
- var total = 0 // 总数量
- var list: ArrayList? = null // 数据列表
-}
-```
-
-## 统一UI组件规范
-
-### 自定义控件
-
-#### 1. PadSearchLayout - 搜索输入框组合控件
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayout.kt`
-
-**功能**: 集成多种输入类型的搜索框组件
-
-**支持的类型**:
-- `SearchLayoutType.INPUT` - 文本输入
-- `SearchLayoutType.INTEGER` - 数字输入
-- `SearchLayoutType.SPINNER` - 下拉选择
-- `SearchLayoutType.DATE` - 日期选择
-
-**核心属性**:
-```kotlin
-type: SearchLayoutType // 控件类型
-value: String // 绑定的值(支持双向绑定@={})
-hint: String // 提示文字
-list: List // 下拉选项列表
-required: Boolean // 是否必填(显示*号)
-icon: Int // 右侧图标资源ID
-enable: Boolean // 是否可编辑
-refreshCallBack: () -> Unit // 刷新回调
-onIconClickListener: (View) -> Unit // 图标点击回调
-```
-
-**使用示例**:
-```xml
-
-
-
-
-
-
-
-
-```
-
-#### 2. PadDataLayout - 数据展示/编辑组合控件
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ui/weight/data/layout/PadDataLayout.kt`
-
-**功能**: 带标题的数据展示/编辑控件(标题+输入框/下拉/日期)
-
-**支持的类型**:
-- `DataLayoutType.INPUT` - 文本输入
-- `DataLayoutType.SPINNER` - 下拉选择
-- `DataLayoutType.DATE` - 日期选择
-
-**核心属性**:
-```kotlin
-type: DataLayoutType // 控件类型
-title: String // 左侧标题
-titleLength: Int // 标题长度(用于对齐,单位:汉字个数)
-value: String // 绑定的值(支持双向绑定@={})
-hint: String // 提示文字
-list: List // 下拉选项列表
-required: Boolean // 是否必填(显示*号)
-icon: Int // 右侧图标
-enable: Boolean // 是否可编辑
-inputHeight: Int // 多行输入高度
-maxLength: Int // 最大长度
-```
-
-**使用示例**:
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-#### 3. StatusView - 状态栏占位View
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ui/weight/StatusView.kt`
-
-**功能**: 自动适配状态栏高度的占位View(用于沉浸式状态栏)
-
-**使用方式**:
-```xml
-
-```
-
-### 通用样式定义
-
-#### 文本样式 (module_base/res/values/styles.xml)
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-#### 按钮样式
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-#### 布局样式
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-### 颜色规范 (module_base/res/values/colors.xml)
-
-```xml
-
-#FF1C8CF5
-#0A80FC
-
-
-#1E1E1E
-#333333
-#666666
-#999999
-#FF999999
-#3CB5F3
-
-
-#FFFFFFFF
-#FF000000
-#d9001b
-
-
-#FFEDEDED
-#FFEDEDED
-#FFF6FBFF
-#FFEDEDED
-#F1F1F1
-#5c6890
-```
-
-### Drawable背景规范
-
-#### 输入框背景
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-**使用说明**:
-- `bg_search_layout`: 搜索区域输入框专用(8dp圆角)
-- `bg_data_layout`: 数据展示区域输入框专用(4dp圆角)
-- `bg_input`: 通用输入框背景(白色+灰色边框+8dp圆角)
-
-#### 按钮背景
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-**常用按钮背景**:
-- `@drawable/bg_btn_bottom` - 底部按钮(蓝色/灰色选择器)
-- `@drawable/submit_shape` - 提交按钮(蓝色+5dp圆角)
-- `@drawable/bg_red_radius_5` - 红色按钮(5dp圆角)
-- `@drawable/bg_primary_radius_4` - 主色按钮(4dp圆角)
-
-#### 通用形状背景
-
-- `@drawable/bg_white_radius_8` - 白色圆角背景(8dp)
-- `@drawable/bg_white_circle` - 白色圆形背景
-- `@drawable/bg_dialog_round` - 对话框圆角背景
-- `@drawable/bg_confirm_dialog` - 确认对话框背景
-
-### 通用布局组件
-
-#### 标题栏 (title_tool_bar.xml)
-
-**使用方式**:
-```xml
-
-```
-
-在Activity中设置标题:
-```kotlin
-setBackArrow("页面标题")
-```
-
-## DataBinding适配器
-
-项目中提供了丰富的BindingAdapter,简化DataBinding使用。
-
-### 图片加载 (BindingAdapter.kt)
-
-```xml
-
-```
-
-**支持的属性**:
-- `loadImage`: 图片地址(URL或本地路径)
-- `loadError`: 错误图片
-- `loadPlaceholder`: 占位图
-- `loadCircle`: 是否圆形(true/false)
-- `loadRadius`: 圆角(dp)
-- `loadWidth`/`loadHeight`: 指定宽高
-
-### View可见性 (ViewAdapter.kt)
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-### TextView文本对齐 (TextViewAdapter.kt)
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-### Spinner下拉框 (SpinnerAdapter.kt)
-
-```xml
-
-```
-
-### Shape动态背景 (ShapeBindingAdapter.kt)
-
-```xml
-
-
-
-
-
-
-
-
-```
-
-### EditText扩展 (EditTextKtx.kt)
-
-```xml
-
-
-
-
-
-```
-
-## Kotlin扩展函数
-
-### Toast扩展 (ToastKtx.kt)
-
-```kotlin
-// 显示Toast(默认长时间)
-showToast("操作成功")
-
-// 短时间Toast
-showToast("提示信息", isShort = true)
-```
-
-### Dialog扩展 (DialogKtx.kt)
-
-```kotlin
-// 确认对话框
-showConfirmDialog(
- message = "确定要删除吗?",
- title = "提示",
- confirmText = "确定",
- cancelText = "取消"
-) {
- // 确认操作
- deleteItem()
-}
-```
-
-### 字符串处理 (CommonKtx.kt)
-
-```kotlin
-// 空处理
-val text = nullableString.noNull("默认值")
-val text2 = nullableString.noNull() // 空字符串
-
-// 验证非空
-if (waybillNo.verifyNullOrEmpty("请输入运单号")) {
- return // 验证失败会自动Toast
-}
-```
-
-### 布尔转换
-
-```kotlin
-// Boolean -> Int
-val intValue = booleanValue.toInt() // true=1, false=0
-
-// String -> Boolean
-val bool = "1".toBoolean() // "1"=true, 其他=false
-
-// Int -> Boolean
-val bool = 1.toBoolean() // 1=true, 其他=false
-```
-
-### 日期格式化
-
-```kotlin
-// 格式化日期
-val dateStr = Date().formatDate() // "2025-11-12"
-val dateStr2 = Date().formatDate("yyyy/MM/dd") // "2025/11/12"
-
-// 格式化日期时间
-val dateTimeStr = Date().formatDateTime() // "2025-11-12 14:30:00"
-```
-
-### JSON处理
-
-```kotlin
-// 对象转JSON
-val json = userBean.toJson()
-val jsonFormatted = userBean.toJson(format = true) // 格式化输出
-
-// 对象转Map
-val map = userBean.toMap()
-
-// Map去除空值
-val cleanMap = map.trim(deep = true)
-```
-
-### 权限申请
-
-```kotlin
-// 申请单个权限
-permission(Manifest.permission.CAMERA) {
- // 获取权限后操作
- openCamera()
-}
-
-// 申请多个权限
-permission(
- Manifest.permission.CAMERA,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
-) {
- // 所有权限获取后操作
- takePicture()
-}
-
-// 处理拒绝情况
-permission(
- Manifest.permission.CAMERA,
- onDenied = {
- showToast("需要相机权限才能扫码")
- },
- onGranted = {
- openCamera()
- }
-)
-```
-
-## 路由系统
-
-### ARouter路由注册
-
-**路由常量定义**: `module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt`
-
-```kotlin
-object ARouterConstants {
- // 国内出港
- const val ACTIVITY_URL_GNC_SHOUYUN_LIST = "/gnc/GncShouYunListActivity"
- const val ACTIVITY_URL_GNC_STASH_DETAILS = "/gnc/GncStashDetailsActivity"
-
- // 国际进港
- const val ACTIVITY_URL_GJJ_TALLY_LIST = "/gjj/GjjTallyListActivity"
- const val ACTIVITY_URL_GJJ_GOODS_LIST = "/gjj/GjjGoodsListActivity"
-
- // 通用功能
- const val ACTIVITY_URL_SCAN = "/common/ScanActivity"
-}
-```
-
-### 使用方式
-
-```kotlin
-// 1. 在Activity上添加注解
-@Route(path = ARouterConstants.ACTIVITY_URL_GNC_SHOUYUN_LIST)
-class GncShouYunListActivity : BaseBindingActivity<...>() {
- // ...
-}
-
-// 2. 跳转页面
-ARouter.getInstance()
- .build(ARouterConstants.ACTIVITY_URL_GNC_SHOUYUN_LIST)
- .withString("waybillNo", "12345678901")
- .withInt("type", 1)
- .navigation()
-
-// 3. 接收参数
-@Autowired(name = "waybillNo")
-@JvmField
-var waybillNo: String? = null
-
-@Autowired(name = "type")
-@JvmField
-var type: Int = 0
-
-override fun initOnCreate(savedInstanceState: Bundle?) {
- ARouter.getInstance().inject(this) // 注入参数
- // 使用参数
- Log.d("TAG", "waybillNo: $waybillNo, type: $type")
-}
-```
-
-## 事件通信
-
-### FlowBus事件总线
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/impl/FlowBus.kt`
-
-```kotlin
-// 定义事件常量(在ConstantEvent.kt中)
-object ConstantEvent {
- const val EVENT_REFRESH_LIST = "event_refresh_list"
- const val EVENT_UPDATE_STATUS = "event_update_status"
-}
-
-// 发送事件
-FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh")
-
-// 接收事件(在ViewModel中)
-FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).observe(this) { data ->
- // 处理事件
- loadData()
-}
-
-// 粘性事件(先发送后接收也能收到)
-FlowBus.withSticky("user_info").emit(userBean)
-FlowBus.withSticky("user_info").observe(this) { user ->
- // 处理用户信息
-}
-```
-
-## 开发规范
-
-### 代码组织规范
-
-#### 目录结构标准
-
-```
-module_xxx/
-└── src/main/java/com/lukouguoji/xxx/
- ├── page/ # 页面目录(推荐新模式)
- │ └── feature/ # 功能模块
- │ ├── list/ # 列表页
- │ │ ├── XxxListActivity.kt
- │ │ ├── XxxListViewModel.kt
- │ │ └── XxxListViewHolder.kt
- │ ├── details/ # 详情页
- │ │ ├── XxxDetailsActivity.kt
- │ │ └── XxxDetailsViewModel.kt
- │ └── add/ # 新增/编辑页
- │ ├── XxxAddActivity.kt
- │ └── XxxAddViewModel.kt
- ├── activity/ # Activity目录(旧模式)
- ├── viewModel/ # ViewModel目录(旧模式)
- └── holder/ # ViewHolder目录
-```
-
-#### 命名规范
-
-**Activity命名**:
-- 列表页: `XxxListActivity`
-- 详情页: `XxxDetailsActivity`
-- 新增页: `XxxAddActivity`
-- 编辑页: `XxxEditActivity`
-
-**ViewModel命名**:
-- 列表: `XxxListViewModel`
-- 详情: `XxxDetailsViewModel`
-- 新增: `XxxAddViewModel`
-
-**ViewHolder命名**:
-- 格式: `XxxViewHolder` 或 `XxxListViewHolder`
-
-**Layout命名**:
-- Activity: `activity_xxx_list.xml`
-- Fragment: `fragment_xxx.xml`
-- Item: `item_xxx.xml`
-
-### Bean定义规范
-
-**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/bean/`
-
-```kotlin
-// 使用Kotlin data class
-data class XxxBean(
- var id: String = "",
- var name: String = "",
- var date: String = "",
- var status: String = ""
-)
-
-// 如果需要DataBinding双向绑定,使用BaseObservable
-class XxxBean : BaseObservable() {
-
- @Bindable
- var name: String = ""
- set(value) {
- field = value
- notifyPropertyChanged(BR.name)
- }
-
- @Bindable
- var weight: String = ""
- set(value) {
- field = value
- notifyPropertyChanged(BR.weight)
- }
-
- // 使用ObservableBoolean/ObservableInt等
- val checked = ObservableBoolean(false)
- val count = ObservableInt(0)
-
- // 计算属性
- val displayName: String
- get() = "【$name】"
-}
-```
-
-## 实际业务开发示例
-
-### 列表页完整示例
-
-#### Activity层
-
-```kotlin
-@Route(path = ARouterConstants.ACTIVITY_URL_GNC_SHOUYUN_LIST)
-class GncShouYunListActivity :
- BaseBindingActivity() {
-
- override fun layoutId() = R.layout.activity_gnc_shouyun_list
-
- override fun viewModelClass() = GncShouYunListViewModel::class.java
-
- override fun initOnCreate(savedInstanceState: Bundle?) {
- setBackArrow("国内出港收运记录")
-
- // 绑定ViewModel
- binding.viewModel = viewModel
-
- // 绑定分页逻辑
- viewModel.pageModel.bindSmartRefreshLayout(
- binding.srl,
- binding.recyclerView,
- viewModel,
- this
- )
-
- // 绑定列表点击
- binding.recyclerView.addOnItemClickListener(viewModel)
-
- // 初始加载
- viewModel.refresh()
- }
-}
-```
-
-#### ViewModel层
-
-```kotlin
-class GncShouYunListViewModel : BasePageViewModel() {
-
- // LiveData定义
- val date = MutableLiveData(DateUtils.getCurrentTime().formatDate())
- val dest = MutableLiveData("")
- val waybillNo = MutableLiveData("")
- val count = MutableLiveData("0")
-
- // 适配器配置(在布局中使用)
- val itemLayoutId = R.layout.item_gnc_shouyun
- val itemViewHolder = GncShouYunListViewHolder::class.java
-
- // 数据加载
- override fun getData() {
- val requestBody = mapOf(
- "page" to pageModel.page,
- "limit" to pageModel.limit,
- "opDate" to date.value,
- "dest" to dest.value,
- "wbNo" to waybillNo.value
- ).toRequestBody()
-
- launchLoadingCollect({
- NetApply.api.getGncShouYunList(requestBody)
- }) {
- onSuccess = {
- pageModel.handleListBean(it)
- count.value = it.total.toString()
- }
- }
- }
-
- // 条目点击
- override fun onItemClick(position: Int, type: Int) {
- val bean = pageModel.rv!!.commonAdapter()!!.getItem(position) as GncShouYunBean
- GncStashDetailsActivity.start(getTopActivity(), bean.whId)
- }
-
- // 搜索
- fun search() {
- refresh()
- }
-
- // 扫码
- fun scanWaybill() {
- ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL)
- }
-
- // 处理扫码结果
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (resultCode == Activity.RESULT_OK && data != null) {
- when (requestCode) {
- Constant.RequestCode.WAYBILL -> {
- waybillNo.value = data.getStringExtra(Constant.Result.CODED_CONTENT)
- search()
- }
- }
- }
- }
-}
-```
-
-#### ViewHolder层
-
-```kotlin
-class GncShouYunListViewHolder(view: View) :
- BaseViewHolder(view) {
-
- override fun onBind(item: Any?, position: Int) {
- val bean = getItemBean(item) ?: return
-
- // DataBinding绑定数据
- binding.bean = bean
-
- // 整个条目可点击
- notifyItemClick(position, binding.shouyunItem)
-
- // 特定控件点击
- binding.ivIcon.setOnClickListener {
- bean.checked.set(!bean.checked.get())
- }
- }
-}
-```
-
-#### Layout层
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-### 详情页完整示例
-
-#### Activity层
-
-```kotlin
-@Route(path = ARouterConstants.ACTIVITY_URL_GNC_STASH_DETAILS)
-class GncStashDetailsActivity :
- BaseBindingActivity() {
-
- override fun layoutId() = R.layout.activity_gnc_stash_details
-
- override fun viewModelClass() = GncStashDetailsViewModel::class.java
-
- override fun initOnCreate(savedInstanceState: Bundle?) {
- setBackArrow("国内出港运单详情")
-
- // 初始化ViewModel(传递参数)
- viewModel.initOnCreated(intent)
-
- // 绑定ViewModel
- binding.viewModel = viewModel
- }
-
- companion object {
- /**
- * 静态启动方法(推荐方式)
- */
- @JvmStatic
- fun start(context: Context, whId: String) {
- val starter = Intent(context, GncStashDetailsActivity::class.java)
- .putExtra(Constant.Key.ID, whId)
- context.startActivity(starter)
- }
- }
-}
-```
-
-#### ViewModel层
-
-```kotlin
-class GncStashDetailsViewModel : BaseViewModel() {
-
- var id = ""
- val dataBean = MutableLiveData()
-
- fun initOnCreated(intent: Intent) {
- id = intent.getStringExtra(Constant.Key.ID) ?: ""
- getData()
- }
-
- private fun getData() {
- launchLoadingCollect({
- NetApply.api.getGncStashDetails(id)
- }) {
- onSuccess = {
- dataBean.value = it.data ?: GncStashBean()
- }
- }
- }
-
- // 打印标签
- fun printLabel() {
- dataBean.value?.let { bean ->
- PrinterModel.printWaybillLabel(bean)
- }
- }
-}
-```
-
-#### Layout层
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-### 新增/编辑页完整示例
-
-#### Activity层
-
-```kotlin
-@Route(path = ARouterConstants.ACTIVITY_URL_CAR_ADD)
-class CarAddActivity :
- BaseBindingActivity() {
-
- override fun layoutId() = R.layout.activity_car_add
-
- override fun viewModelClass() = CarAddViewModel::class.java
-
- override fun initOnCreate(savedInstanceState: Bundle?) {
- viewModel.initOnCreated(intent)
-
- // 根据页面类型设置标题
- when (viewModel.pageType) {
- DetailsPageType.Add -> setBackArrow("新增车辆")
- DetailsPageType.Edit -> setBackArrow("编辑车辆")
- DetailsPageType.Details -> setBackArrow("车辆详情")
- }
-
- binding.viewModel = viewModel
- }
-
- companion object {
- @JvmStatic
- fun startForAdd(context: Context) {
- val starter = Intent(context, CarAddActivity::class.java)
- .putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Add.name)
- context.startActivity(starter)
- }
-
- @JvmStatic
- fun startForEdit(context: Context, carId: String) {
- val starter = Intent(context, CarAddActivity::class.java)
- .putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Edit.name)
- .putExtra(Constant.Key.ID, carId)
- context.startActivity(starter)
- }
-
- @JvmStatic
- fun startForDetails(context: Context, carId: String) {
- val starter = Intent(context, CarAddActivity::class.java)
- .putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Details.name)
- .putExtra(Constant.Key.ID, carId)
- context.startActivity(starter)
- }
- }
-}
-```
-
-#### ViewModel层
-
-```kotlin
-class CarAddViewModel : BaseViewModel() {
-
- var pageType: DetailsPageType = DetailsPageType.Add
- var carId = ""
-
- val carBean = MutableLiveData(CarBean())
- val statusList = MutableLiveData>()
-
- fun initOnCreated(intent: Intent) {
- // 获取页面类型
- pageType = DetailsPageType.valueOf(
- intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
- )
-
- // 加载下拉列表
- loadStatusList()
-
- // 如果是编辑或详情,加载数据
- if (pageType != DetailsPageType.Add) {
- carId = intent.getStringExtra(Constant.Key.ID) ?: ""
- loadData()
- }
- }
-
- private fun loadStatusList() {
- statusList.value = listOf(
- KeyValue("1", "正常"),
- KeyValue("2", "维修中"),
- KeyValue("3", "停用")
- )
- }
-
- private fun loadData() {
- launchLoadingCollect({
- NetApply.api.getCarDetails(carId)
- }) {
- onSuccess = {
- carBean.value = it.data ?: CarBean()
- }
- }
- }
-
- fun submit() {
- val bean = carBean.value ?: return
-
- // 验证
- if (bean.carId.verifyNullOrEmpty("请输入车辆编号")) return
- if (bean.status.verifyNullOrEmpty("请选择状态")) return
-
- // 提交
- launchLoadingCollect({
- val params = mapOf(
- "id" to carId,
- "carId" to bean.carId,
- "status" to bean.status,
- "remark" to bean.remark
- ).toRequestBody(removeEmptyOrNull = true)
-
- NetApply.api.saveCar(params)
- }) {
- onSuccess = {
- showToast(if (pageType == DetailsPageType.Add) "新增成功" else "编辑成功")
-
- // 发送刷新事件
- FlowBus.with(ConstantEvent.EVENT_REFRESH_CAR_LIST).emit("refresh")
-
- // 关闭页面
- getTopActivity().finish()
- }
- }
- }
-}
-```
-
-#### Layout层
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-## 开发检查清单
-
-### 列表页开发清单(7步)
-
-**步骤1: 创建Bean**
-```kotlin
-// 位置: module_base/src/main/java/com/lukouguoji/module_base/bean/
-data class XxxBean(
- var id: String = "",
- var name: String = "",
- var date: String = "",
- var status: String = ""
-)
-```
-
-**步骤2: 在Api中添加接口**
-```kotlin
-// 位置: module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt
-@POST("api/xxx/list")
-suspend fun getXxxList(@Body data: RequestBody): BaseListBean
-```
-
-**步骤3: 创建ViewHolder**
-```kotlin
-// 位置: module_xxx/src/main/java/.../holder/
-class XxxViewHolder(view: View) :
- BaseViewHolder(view) {
-
- override fun onBind(item: Any?, position: Int) {
- val bean = getItemBean(item) ?: return
- binding.bean = bean
- notifyItemClick(position, binding.root)
- }
-}
-```
-
-**步骤4: 创建ViewModel**
-```kotlin
-// 继承BasePageViewModel
-class XxxListViewModel : BasePageViewModel() {
-
val searchText = MutableLiveData()
val itemLayoutId = R.layout.item_xxx
val itemViewHolder = XxxViewHolder::class.java
override fun getData() {
- val requestBody = mapOf(
+ val params = mapOf(
"page" to pageModel.page,
"limit" to pageModel.limit,
"searchText" to searchText.value
).toRequestBody()
- launchLoadingCollect({
- NetApply.api.getXxxList(requestBody)
- }) {
- onSuccess = {
- pageModel.handleListBean(it)
- }
+ 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
- // 跳转详情页
+ // 跳转详情
}
}
```
-**步骤5: 创建Activity**
-```kotlin
-@Route(path = ARouterConstants.ACTIVITY_URL_XXX_LIST)
-class XxxListActivity :
- BaseBindingActivity() {
+**详情页ViewModel:**
- override fun layoutId() = R.layout.activity_xxx_list
- override fun viewModelClass() = XxxListViewModel::class.java
-
- override fun initOnCreate(savedInstanceState: Bundle?) {
- setBackArrow("列表页面")
- binding.viewModel = viewModel
-
- viewModel.pageModel.bindSmartRefreshLayout(
- binding.srl,
- binding.recyclerView,
- viewModel,
- this
- )
-
- binding.recyclerView.addOnItemClickListener(viewModel)
- }
-}
-```
-
-**步骤6: 创建Layout**
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-**步骤7: 注册路由**
-```kotlin
-// 位置: module_base/.../router/ARouterConstants.kt
-const val ACTIVITY_URL_XXX_LIST = "/xxx/XxxListActivity"
-```
-
-### 详情页开发清单(4步)
-
-**步骤1: 在Api中添加接口**
-```kotlin
-@POST("api/xxx/details")
-suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean
-```
-
-**步骤2: 创建ViewModel**
```kotlin
class XxxDetailsViewModel : BaseViewModel() {
-
var id = ""
val dataBean = MutableLiveData()
@@ -2100,127 +93,45 @@ class XxxDetailsViewModel : BaseViewModel() {
}
private fun getData() {
- launchLoadingCollect({
- NetApply.api.getXxxDetails(id)
- }) {
- onSuccess = {
- dataBean.value = it.data ?: XxxBean()
- }
+ launchLoadingCollect({ NetApply.api.getXxxDetails(id) }) {
+ onSuccess = { dataBean.value = it.data ?: XxxBean() }
}
}
}
```
-**步骤3: 创建Activity(含静态start方法)**
-```kotlin
-@Route(path = ARouterConstants.ACTIVITY_URL_XXX_DETAILS)
-class XxxDetailsActivity :
- BaseBindingActivity() {
+**编辑页ViewModel:**
- override fun layoutId() = R.layout.activity_xxx_details
- override fun viewModelClass() = XxxDetailsViewModel::class.java
-
- override fun initOnCreate(savedInstanceState: Bundle?) {
- setBackArrow("详情页面")
- viewModel.initOnCreated(intent)
- binding.viewModel = viewModel
- }
-
- companion object {
- @JvmStatic
- fun start(context: Context, id: String) {
- val starter = Intent(context, XxxDetailsActivity::class.java)
- .putExtra(Constant.Key.ID, id)
- context.startActivity(starter)
- }
- }
-}
-```
-
-**步骤4: 创建Layout**
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-### 新增/编辑页开发清单(5步)
-
-**步骤1: 在Api中添加接口**
-```kotlin
-@POST("api/xxx/save")
-suspend fun saveXxx(@Body data: RequestBody): BaseResultBean
-
-@POST("api/xxx/details")
-suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean
-```
-
-**步骤2: 创建ViewModel**
```kotlin
class XxxAddViewModel : BaseViewModel() {
-
- var pageType: DetailsPageType = DetailsPageType.Add
+ val pageType = MutableLiveData(DetailsPageType.Add) // 必须用LiveData
var id = ""
-
val dataBean = MutableLiveData(XxxBean())
- val optionList = MutableLiveData>()
fun initOnCreated(intent: Intent) {
- pageType = DetailsPageType.valueOf(
+ pageType.value = DetailsPageType.valueOf(
intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
)
-
- loadOptions()
-
- if (pageType != DetailsPageType.Add) {
+ if (pageType.value != DetailsPageType.Add) {
id = intent.getStringExtra(Constant.Key.ID) ?: ""
loadData()
}
}
- private fun loadData() {
- launchLoadingCollect({
- NetApply.api.getXxxDetails(id)
- }) {
- onSuccess = {
- dataBean.value = it.data ?: XxxBean()
- }
- }
- }
-
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)
-
+ val params = mapOf("id" to id, "name" to bean.name)
+ .toRequestBody(removeEmptyOrNull = true)
NetApply.api.saveXxx(params)
}) {
onSuccess = {
- showToast(if (pageType == DetailsPageType.Add) "新增成功" else "保存成功")
- FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh")
+ showToast("保存成功")
+ viewModelScope.launch {
+ FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh")
+ }
getTopActivity().finish()
}
}
@@ -2228,785 +139,396 @@ class XxxAddViewModel : BaseViewModel() {
}
```
-**步骤3: 创建Activity(含多个静态start方法)**
+## 网络请求
+
+### 请求方法
+
```kotlin
-@Route(path = ARouterConstants.ACTIVITY_URL_XXX_ADD)
-class XxxAddActivity :
- BaseBindingActivity() {
+// 带Loading请求
+launchLoadingCollect({ NetApply.api.saveXxx(params) }) {
+ onSuccess = { /* 成功处理 */ }
+ onFailed = { code, msg -> /* 失败处理 */ }
+}
- override fun layoutId() = R.layout.activity_xxx_add
- override fun viewModelClass() = XxxAddViewModel::class.java
+// 无Loading请求(后台刷新)
+launchCollect({ NetApply.api.getXxx() }) {
+ onSuccess = { /* 成功处理 */ }
+}
- override fun initOnCreate(savedInstanceState: Bundle?) {
- viewModel.initOnCreated(intent)
+// 参数转换
+val params = mapOf("key" to "value").toRequestBody(removeEmptyOrNull = true)
+```
- when (viewModel.pageType) {
- DetailsPageType.Add -> setBackArrow("新增")
- DetailsPageType.Edit -> setBackArrow("编辑")
- DetailsPageType.Details -> setBackArrow("详情")
- }
+### API接口定义
- binding.viewModel = viewModel
- }
+```kotlin
+// 位置: module_base/.../http/net/Api.kt
+@POST("api/xxx/list")
+suspend fun getXxxList(@Body data: RequestBody): BaseListBean
- 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)
- }
+@POST("api/xxx/details")
+suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean
- @JvmStatic
- fun startForEdit(context: Context, id: String) {
- val starter = Intent(context, XxxAddActivity::class.java)
- .putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Edit.name)
- .putExtra(Constant.Key.ID, id)
- context.startActivity(starter)
- }
+@POST("api/xxx/save")
+suspend fun saveXxx(@Body data: RequestBody): BaseResultBean
+```
- @JvmStatic
- fun startForDetails(context: Context, id: String) {
- val starter = Intent(context, XxxAddActivity::class.java)
- .putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Details.name)
- .putExtra(Constant.Key.ID, id)
- context.startActivity(starter)
- }
+## 核心UI组件
+
+### PadSearchLayout - 搜索输入框
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+**类型**: `INPUT` / `INTEGER` / `SPINNER` / `DATE`
+
+### PadDataLayout - 数据展示/编辑
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+**类型**: `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
+
+
+```
+
+**关键代码:**
+
+```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)
}
}
```
-**步骤4: 创建Layout**
-```xml
-
-
-
-
-
-
+### 编辑页开发(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多入口:**
-
-
-
-
-
-
-
-
-
-
-```
-
-**步骤5: 注册路由并发送刷新事件**
```kotlin
-// 注册路由
-const val ACTIVITY_URL_XXX_ADD = "/xxx/XxxAddActivity"
+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)
+ }
-// 在列表页接收刷新事件
-FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).observe(this) {
- viewModel.refresh()
+ @JvmStatic
+ fun startForEdit(context: Context, id: String) {
+ /* ... DetailsPageType.Modify ... */
+ }
}
```
## 常见业务场景
-### 扫码后查询
+### 扫码
```kotlin
-// 在ViewModel中
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) {
- val code = data?.getStringExtra(Constant.Result.CODED_CONTENT)
- waybillNo.value = code
- search() // 自动搜索
+ waybillNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT)
+ search()
}
}
```
-### 打印标签
-
-```kotlin
-// 绑定打印服务
-bindService(Intent(this, PrinterService::class.java), serviceConnection, BIND_AUTO_CREATE)
-
-// 打印
-val printData = DataForSendToPrinter()
-printData.addText("运单号: ${waybillNo}")
-printData.addText("重量: ${weight}kg")
-printData.addBarcode(waybillNo, 2, 100)
-printerService?.sendPrintData(printData)
-```
-
### 图片上传
```kotlin
-// 选择图片
-PictureSelector.create(this)
- .openGallery(SelectMimeType.ofImage())
- .setMaxSelectNum(1)
- .setImageEngine(GlideEngine.createGlideEngine())
- .forResult { result ->
- val path = result[0].realPath
- uploadImage(path)
- }
-
-// 上传接口
-private fun uploadImage(path: String) {
- val file = File(path)
- val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
- val part = MultipartBody.Part.createFormData("file", file.name, requestFile)
-
- launchLoadingCollect({
- NetApply.api.uploadFile(part)
- }) {
- onSuccess = { result ->
- imageUrl.value = result.data?.url
- showToast("上传成功")
- }
- }
+val result = UploadUtil.upload(filePath)
+if (result.verifySuccess()) {
+ val imageUrl = result.data?.newName ?: "" // 注意是newName不是url
}
```
-## 关键技术点
-
-### 屏幕适配
-
-- **横屏设计尺寸**: 1152dp × 720dp(主要场景)
-- **竖屏设计尺寸**: 720dp × 1280dp
-- **强制横屏**: 所有Activity在AndroidManifest中配置 `android:screenOrientation="userLandscape"`
-- **适配库**: AutoSize 1.2.+
-
-### 蓝牙打印
-
-**核心类**:
-- `Printer` 模块:独立的打印服务
-- `PrinterService`: 后台打印服务
-- `PrinterConfig`: 打印机配置管理
-- 使用佳博SDK 2.0.4
-
-**打印流程**:
-1. 启动PrinterService
-2. 扫描/绑定蓝牙打印机
-3. 构建打印数据(DataForSendToPrinter)
-4. 发送打印任务
-
-### 扫码功能
-
-- 使用 ZXing 2.2.9 库
-- 扫描条形码/二维码
-- 主要用于:运单号、ULD编号、车辆编号、板箱编号
-
-### 权限管理
-
-使用 AndPermission 2.0.2 或扩展函数:
+### 列表刷新事件
```kotlin
-// 方式1: 使用扩展函数(推荐)
-permission(Manifest.permission.CAMERA) {
- openCamera()
+// 发送事件(在ViewModel中)
+viewModelScope.launch {
+ FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh")
}
-// 方式2: 使用AndPermission
-AndPermission.with(this)
- .runtime()
- .permission(Permission.CAMERA)
- .onGranted { }
- .onDenied { }
- .start()
-```
-
-### 图片选择
-
-使用 PictureSelector v3.11.2 + Glide 4.15.1:
-
-```kotlin
-PictureSelector.create(this)
- .openGallery(SelectMimeType.ofImage())
- .setMaxSelectNum(9)
- .setImageEngine(GlideEngine.createGlideEngine())
- .forResult { result ->
- // 处理选择结果
- }
-```
-
-## 重要配置文件
-
-### 签名配置
-- **KeyStore**: `key.jks` (项目根目录)
-- **密码**: storePassword/keyPassword均为 `123321`
-- **别名**: `key`
-
-### 网络配置
-- **超时时间**: 30秒(连接/读取/写入)
-- **认证方式**: Bearer Token(通过拦截器自动添加)
-- **Token存储**: SharedPreferences (key: Constant.Share.token)
-- **网络安全配置**: `res/xml/network_security_config.xml` (支持HTTP)
-
-### 数据持久化
-- **SharedPreferences**: IP地址、Token、用户信息、角色
-- **关键常量**: 定义在 `Constant.kt` 和 `ConstantEvent.kt`
-
-## Git分支管理
-
-- **当前开发分支**: feature/hefei
-- **主分支**: develop(用于PR)
-- **提交前**: 确保代码通过编译,无明显错误
-
-## 技术栈速查
-
-- **协程**: kotlinx-coroutines 1.6.0
-- **网络**: Retrofit 2.6.1 + OkHttp 3.12.12
-- **JSON**: FastJSON 1.2.73 + Gson 2.10.1
-- **路由**: ARouter 1.5.2
-- **下拉刷新**: SmartRefreshLayout 2.0.3
-- **图表**: MPAndroidChart (定制版)
-- **弹窗**: XPopup 2.9.19
-- **图片**: Glide 4.15.1 + PictureSelector v3.11.2
-- **扫码**: ZXing 2.2.9
-- **权限**: AndPermission 2.0.2
-- **打印**: 佳博SDK 2.0.4
-- **日志**: Timber 5.0.1
-- **事件**: EventBus 3.1.1 + FlowBus
-
----
-
-## 总结
-
-本开发指南涵盖了Android航空物流App的完整开发流程,包括:
-
-1. **基类架构**: 统一的MVVM架构,减少样板代码
-2. **网络请求**: 协程+Flow的现代化异步方案
-3. **UI组件**: 高度封装的输入、展示控件,保证界面一致性
-4. **DataBinding**: 全面的适配器支持,简化视图绑定
-5. **扩展函数**: 丰富的Kotlin扩展,提高开发效率
-6. **开发清单**: 详细的步骤指引,确保不遗漏关键环节
-
-**开发原则**:
-- ✅ 优先使用项目现有的基类和封装
-- ✅ 充分利用PadDataLayout和PadSearchLayout组件
-- ✅ 遵循统一的命名和目录组织规范
-- ✅ 使用DataBinding简化代码
-- ✅ 利用扩展函数处理通用逻辑
-- ✅ 不重复造轮子,保持架构一致性
-
----
-
-## 常见编译错误及解决方案
-
-### 1. DataBinding错误:Cannot resolve type 'DetailsPageType'
-
-**错误信息**:
-```
-ERROR: Cannot resolve type 'DetailsPageType' file://app/src/main/res/layout/activity_xxx.xml Line:XX
-```
-
-**错误原因**:
-在XML布局文件中import的包名错误。
-
-**错误示例**:
-```xml
-
-```
-
-**正确写法**:
-```xml
-
-```
-
-**注意**: `DetailsPageType`位于`common`包,不是`constant`包!
-
----
-
-### 2. DataBinding错误:Could not find accessor DataLayoutType.INTEGER
-
-**错误信息**:
-```
-ERROR: Could not find accessor com.lukouguoji.module_base.ui.weight.data.layout.DataLayoutType.INTEGER
-```
-
-**错误原因**:
-`DataLayoutType`枚举中不存在`INTEGER`类型。
-
-**错误示例**:
-```xml
-
-```
-
-**正确写法**:
-```xml
-
-```
-
-**可用类型**:
-- `DataLayoutType.INPUT` - 文本输入(可输入数字)
-- `DataLayoutType.SPINNER` - 下拉选择
-- `DataLayoutType.DATE` - 日期选择
-
----
-
-### 3. Kotlin编译错误:Unresolved reference: PAGE_TYPE
-
-**错误信息**:
-```
-e: Unresolved reference: PAGE_TYPE
-```
-
-**错误原因**:
-`Constant.Key`对象中缺少`PAGE_TYPE`常量。
-
-**解决方案**:
-在`module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt`中添加:
-
-```kotlin
-object Key {
- // ... 其他常量
-
- // ID
- const val ID = "id"
-
- // 页面类型
- const val PAGE_TYPE = "pageType"
-
- // ... 其他常量
-}
-```
-
-**使用示例**:
-```kotlin
-val starter = Intent(context, XxxActivity::class.java)
- .putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Add.name)
- .putExtra(Constant.Key.ID, id)
-```
-
----
-
-### 4. Kotlin编译错误:Unresolved reference: Edit
-
-**错误信息**:
-```
-e: Unresolved reference: Edit
-```
-
-**错误原因**:
-`DetailsPageType`枚举中不存在`Edit`值。
-
-**错误示例**:
-```kotlin
-when (viewModel.pageType) {
- DetailsPageType.Add -> setBackArrow("新增")
- DetailsPageType.Edit -> setBackArrow("编辑") // ❌ 错误
- DetailsPageType.Details -> setBackArrow("详情")
-}
-```
-
-**正确写法**:
-```kotlin
-when (viewModel.pageType) {
- DetailsPageType.Add -> setBackArrow("新增")
- DetailsPageType.Modify -> setBackArrow("编辑") // ✅ 正确
- DetailsPageType.Details -> setBackArrow("详情")
-}
-```
-
-**DetailsPageType枚举值**:
-```kotlin
-enum class DetailsPageType(val title: String) {
- Add("新增"), // 新增页面
- Modify("编辑"), // 编辑页面(注意:不是Edit!)
- Details("详情") // 详情页面
-}
-```
-
----
-
-### 5. Kotlin编译错误:Unresolved reference: IOnItemClickListener
-
-**错误信息**:
-```
-e: Unresolved reference: IOnItemClickListener
-```
-
-**错误原因**:
-import的包名错误,`IOnItemClickListener`在`interfaces`包,不是`impl`包。
-
-**错误示例**:
-```kotlin
-import com.lukouguoji.module_base.impl.IOnItemClickListener // ❌ 错误
-```
-
-**正确写法**:
-```kotlin
-import com.lukouguoji.module_base.interfaces.IOnItemClickListener // ✅ 正确
-```
-
-**使用场景**:
-```kotlin
-class XxxViewModel : BaseViewModel(), IOnItemClickListener {
- override fun onItemClick(position: Int, type: Int) {
- // 处理点击事件
- }
-}
-```
-
----
-
-### 6. FlowBus使用错误
-
-#### 错误A:Unresolved reference: observe
-
-**错误信息**:
-```
-e: Unresolved reference: observe
-```
-
-**错误原因**:
-缺少`observe`扩展函数的import。
-
-**解决方案**:
-```kotlin
-import com.lukouguoji.module_base.impl.FlowBus
-import com.lukouguoji.module_base.impl.observe // ✅ 添加这一行
-
-// 在Activity中使用
-FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).observe(this) {
+// 接收事件(在Activity中)
+import com.lukouguoji.module_base.impl.observe // 必须导入
+FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) {
viewModel.refresh()
}
```
-#### 错误B:Suspend function 'emit' should be called only from a coroutine
+## 常用扩展函数
-**错误信息**:
-```
-e: Suspend function 'emit' should be called only from a coroutine or another suspend function
-```
-
-**错误原因**:
-`emit()`是suspend函数,需要在协程中调用。
-
-**错误示例**:
```kotlin
-FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh") // ❌ 错误
+// Toast
+showToast("提示信息")
+
+// 验证非空
+if (text.verifyNullOrEmpty("请输入内容")) return
+
+// 空处理
+val text = nullableString.noNull("默认值")
+
+// 日期格式化
+val dateStr = Date().formatDate() // "2025-11-12"
+
+// 权限申请
+permission(Manifest.permission.CAMERA) { openCamera() }
```
-**正确写法**:
-```kotlin
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.launch
+## 常见编译错误
-// 在ViewModel中
+### 1. DetailsPageType包名错误
+
+```xml
+
+
+
+
+
+```
+
+### 2. DataLayoutType枚举值错误
+
+```xml
+
+type="@{DataLayoutType.INTEGER}"
+
+
+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(event).emit("data")
+
+// ✅ 正确
viewModelScope.launch {
- FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh") // ✅ 正确
+ FlowBus.with(event).emit("data")
}
```
----
+### 6. 图片上传字段错误
-### 7. 图片上传字段错误
-
-**错误信息**:
-```
-e: Unresolved reference: url
-```
-
-**错误原因**:
-`UploadBean`返回的字段名是`newName`,不是`url`。
-
-**UploadBean结构**:
```kotlin
-class UploadBean {
- var newName: String = "" // ✅ 正确字段名
- var zipFileName: String = ""
-}
+// ❌ 错误: UploadBean没有url字段
+val imageUrl = result.data?.url
+
+// ✅ 正确: 使用newName字段
+val imageUrl = result.data?.newName
```
-**错误示例**:
+### 7. pageType必须用LiveData
+
```kotlin
-val result = UploadUtil.upload(filePath)
-if (result.verifySuccess()) {
- val imageUrl = result.data?.url ?: "" // ❌ 错误:没有url字段
-}
+// ❌ 错误: DataBinding无法绑定
+var pageType: DetailsPageType = DetailsPageType.Add
+
+// ✅ 正确: 使用LiveData
+val pageType = MutableLiveData(DetailsPageType.Add)
```
-**正确写法**:
-```kotlin
-val result = UploadUtil.upload(filePath)
-if (result.verifySuccess()) {
- val imageUrl = result.data?.newName ?: "" // ✅ 正确:使用newName
-}
-```
+### 8. RecyclerView不支持items属性
-**完整上传示例**:
-```kotlin
-launchLoadingCollect({
- val uploadedUrls = mutableListOf()
- imageList.forEach { fileBean ->
- if (fileBean.path.startsWith("http")) {
- // 已上传的图片,直接使用URL
- uploadedUrls.add(fileBean.path)
- } else {
- // 本地图片,需要上传
- val result = UploadUtil.upload(fileBean.path)
- if (result.verifySuccess()) {
- uploadedUrls.add(result.data?.newName ?: "") // 使用newName
- }
- }
- }
-
- // 提交时将图片URL列表用逗号拼接
- val params = mapOf(
- "images" to uploadedUrls.joinToString(",")
- ).toRequestBody()
-
- NetApply.api.saveData(params)
-}) {
- onSuccess = {
- showToast("保存成功")
- }
-}
-```
-
----
-
-### 8. RecyclerView DataBinding items属性问题
-
-**问题现象**:
-在DataBinding中使用`items`属性绑定数据会导致编译错误。
-
-**错误示例**:
```xml
-
+
+
+
+
+
```
-**正确做法**:
-移除`items`属性,在Activity中手动更新adapter。
-
-**XML布局**:
-```xml
-
-```
-
-**Activity代码**:
```kotlin
-import com.lukouguoji.module_base.ktx.commonAdapter
-
-override fun initOnCreate(savedInstanceState: Bundle?) {
- setBackArrow("页面标题")
- binding.viewModel = viewModel
-
- // 监听数据变化并手动更新adapter
- viewModel.imageList.observe(this) { images ->
- binding.rvImages.commonAdapter()?.refresh(images)
- }
+// Activity中
+viewModel.list.observe(this) { data ->
+ binding.recyclerView.commonAdapter()?.refresh(data)
}
```
----
+### 9. Constant.Key.PAGE_TYPE未定义
-### 9. pageType在DataBinding中的正确使用
+在`module_base/.../common/Constant.kt`中添加:
-**问题**:
-如果`pageType`声明为普通变量,DataBinding无法正确绑定。
-
-**错误示例**:
```kotlin
-class XxxViewModel : BaseViewModel() {
- var pageType: DetailsPageType = DetailsPageType.Add // ❌ 普通变量
+object Key {
+ const val ID = "id"
+ const val PAGE_TYPE = "pageType" // 添加这个
}
```
-**正确写法**:
-```kotlin
-class XxxViewModel : BaseViewModel() {
- val pageType = MutableLiveData(DetailsPageType.Add) // ✅ 使用LiveData
-}
-```
-
-**ViewModel中访问**:
-```kotlin
-fun initOnCreated(intent: Intent) {
- pageType.value = DetailsPageType.valueOf(
- intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
- )
-
- if (pageType.value != DetailsPageType.Add) {
- loadData()
- }
-}
-```
-
-**Activity中访问**:
-```kotlin
-override fun initOnCreate(savedInstanceState: Bundle?) {
- viewModel.initOnCreated(intent)
-
- when (viewModel.pageType.value) {
- DetailsPageType.Add -> setBackArrow("新增")
- DetailsPageType.Modify -> setBackArrow("编辑")
- DetailsPageType.Details -> setBackArrow("详情")
- }
-}
-```
-
-**XML中使用**:
-```xml
-
-```
-
-**注意**: 在XML的DataBinding表达式中,直接使用`viewModel.pageType`即可,不需要`.value`。
-
----
-
## 错误排查流程
-当遇到编译错误时,按以下顺序排查:
-
-### 第1步:检查DataBinding错误
-
-如果看到`android.databinding.tool.util.LoggedErrorException`:
-
-1. **检查import语句的包名**
- - ✅ `com.lukouguoji.module_base.common.DetailsPageType`
- - ❌ `com.lukouguoji.module_base.constant.DetailsPageType`
-
-2. **检查枚举值是否正确**
- - ✅ `DataLayoutType.INPUT`、`SPINNER`、`DATE`
- - ❌ `DataLayoutType.INTEGER`(不存在)
- - ✅ `DetailsPageType.Modify`(编辑)
- - ❌ `DetailsPageType.Edit`(不存在)
-
-3. **移除不支持的属性**
- - 移除RecyclerView的`items`属性
- - 改为在Activity中手动更新adapter
-
-### 第2步:检查Kotlin编译错误
-
-如果看到`Unresolved reference`:
-
-1. **检查import语句**
- - `IOnItemClickListener` → `com.lukouguoji.module_base.interfaces.IOnItemClickListener`
- - `observe` → `com.lukouguoji.module_base.impl.observe`
-
-2. **检查常量是否存在**
- - 确认`Constant.Key.PAGE_TYPE`已定义
- - 确认`Constant.Key.ID`已定义
-
-3. **检查字段名称**
- - `UploadBean`使用`newName`,不是`url`
-
-### 第3步:检查协程相关错误
-
-如果看到suspend function相关错误:
-
-1. **添加必要的import**
- ```kotlin
- import androidx.lifecycle.viewModelScope
- import kotlinx.coroutines.launch
- ```
-
-2. **在协程中调用emit**
- ```kotlin
- viewModelScope.launch {
- FlowBus.with(event).emit(data)
- }
- ```
-
-### 第4步:清理并重新构建
-
-如果上述都检查过,仍有问题:
-
-```bash
-# 清理项目
-./gradlew clean
-
-# 重新构建
-./gradlew assembleDebug
-```
-
----
-
-## 最佳实践建议
-
-### 1. 开发前检查清单
-
-- [ ] 确认`DetailsPageType`在`common`包
-- [ ] 确认`IOnItemClickListener`在`interfaces`包
-- [ ] 确认`Constant.Key.PAGE_TYPE`常量已定义
-- [ ] 熟悉`DataLayoutType`和`DetailsPageType`的枚举值
-
-### 2. 代码编写规范
-
-- ✅ 使用`MutableLiveData`声明`pageType`,不用普通变量
-- ✅ RecyclerView不使用`items`属性,改用手动更新adapter
-- ✅ FlowBus的`emit()`必须在`viewModelScope.launch`中调用
-- ✅ `observe`扩展函数需要单独import
-- ✅ 图片上传使用`UploadBean.newName`字段
-
-### 3. 参考已有代码
-
-遇到问题时,优先参考项目中已有的类似实现:
-
-- 查看`AccidentVisaDetailsViewModel`了解`pageType`的LiveData用法
-- 查看`GncShouYunListActivity`了解FlowBus的正确使用
-- 查看现有的编辑页面了解图片上传的完整流程
-
----
+1. **DataBinding错误** → 检查import包名、枚举值
+2. **Unresolved reference** → 检查import语句、常量定义
+3. **suspend function错误** → 在`viewModelScope.launch`中调用
+4. **仍有问题** → `./gradlew clean` 后重新构建
## 快速修复命令
```bash
-# 查找DetailsPageType的正确包名
+# 查找DetailsPageType位置
grep -r "enum class DetailsPageType" module_base/src --include="*.kt"
-# 查找IOnItemClickListener的正确包名
+# 查找IOnItemClickListener位置
find module_base/src -name "IOnItemClickListener.kt"
-# 查找DataLayoutType的枚举值
+# 查找DataLayoutType枚举值
grep -A 5 "enum class DataLayoutType" module_base/src --include="*.kt"
-
-# 查找UploadBean的字段定义
-grep -A 10 "class UploadBean" module_base/src --include="*.kt"
```
-通过遵循这些规范和检查清单,可以避免大部分常见的编译错误。
\ No newline at end of file
+## 开发原则
+
+- ✅ 优先使用项目现有基类和封装
+- ✅ 充分利用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`
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0958b18..0f34ecd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -94,6 +94,11 @@
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="userLandscape" />
+
- it.getJSONArray(m.id) != null && it.getJSONArray(m.id).size > 0
- }
- list = authsFilter as ArrayList
- }
+// getAuthsObj()?.let {
+// val authsFilter = list.filter { m ->
+// it.getJSONArray(m.id) != null && it.getJSONArray(m.id).size > 0
+// }
+// list = authsFilter as ArrayList
+// }
return list
}
@@ -267,6 +267,7 @@ class HomeFragment : Fragment() {
Constant.AuthName.AppDomExpDeposit -> {
GncDepositListActivity.start(requireContext())
}
+
/**
* 国内进港
*/
@@ -337,6 +338,11 @@ class HomeFragment : Fragment() {
ARouter.getInstance().build(ARouterConstants.ACTIVITY_URL_GJC_GOODS_LIST)
.navigation()
}
+ // 收运检查
+ Constant.AuthName.GjcInspectionActivity -> {
+ ARouter.getInstance().build(ARouterConstants.ACTIVITY_URL_GJC_INSPECTION)
+ .navigation()
+ }
/**
* 国际进港
*/
@@ -629,6 +635,13 @@ class HomeFragment : Fragment() {
"货物交接"
)
)
+ list.add(
+ RightMenu(
+ Constant.AuthName.GjcInspectionActivity,
+ R.mipmap.gnc_cha,
+ "收运检查"
+ )
+ )
}
Constant.AuthName.IntImp -> {
@@ -775,11 +788,11 @@ class HomeFragment : Fragment() {
/**
* 右侧菜单 权限过滤
*/
- val authsFilter = list.filter { m ->
- SharedPreferenceUtil.getString(Constant.Share.authList).contains(m.id)
- }
// TODO: 暂时关闭权限筛选
- list = authsFilter as ArrayList
+// val authsFilter = list.filter { m ->
+// SharedPreferenceUtil.getString(Constant.Share.authList).contains(m.id)
+// }
+// list = authsFilter as ArrayList
return list
}
diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcInspectionBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcInspectionBean.kt
new file mode 100644
index 0000000..58095c8
--- /dev/null
+++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcInspectionBean.kt
@@ -0,0 +1,75 @@
+package com.lukouguoji.module_base.bean
+
+import androidx.databinding.ObservableBoolean
+import com.lukouguoji.module_base.interfaces.ICheck
+
+/**
+ * 国际出港收运检查数据Bean
+ * 对应后端 GjcMaWb 对象
+ */
+class GjcInspectionBean : ICheck {
+ var maWbId: Long = 0 // 主键ID GJC_MAWB.MAWBID
+ var wbNo: String = "" // 11位运单号
+ var no: String = "" // 运单号(含前缀)
+ var prefix: String = "" // 运单前缀
+
+ var agentCode: String = "" // 代理人
+ var agentName: String = "" // 代理人名称
+ var spCode: String = "" // 特码
+ var pc: Long = 0 // 预配件数
+ var weight: Double = 0.0 // 预配重量
+ var volume: Double = 0.0 // 预配体积
+
+ var flight: String = "" // 航班(格式: 航班日期/航班号)
+ var fdate: String = "" // 航班日期
+ var fno: String = "" // 航班号
+ var range: String = "" // 航程
+ var dep: String = "" // 始发站
+ var dest: String = "" // 最终目的站
+ var scheduledTackOff: String = "" // 计划起飞时间
+ var scheduledArrival: String = "" // 预计到达时间
+
+ var businessType: String = "" // 业务类型
+ var businessName: String = "" // 业务类型名称(中)
+ var awbType: String = "" // 运单类型
+ var awbName: String = "" // 运单类型名称(中)
+
+ var reviewStatus: String = "" // 审核状态(0:未审核;1:通过;2:退回)
+ var checkIn: String = "" // 收运状态(0:待收运,1:已收运,2:收运中)
+
+ var goods: String = "" // 品名(英)
+ var goodsCn: String = "" // 品名(中)
+ var origin: String = "" // 货源地
+ var consignee: String = "" // 收货人
+ var remark: String = "" // 备注
+
+ // 多选状态绑定
+ val checked = ObservableBoolean(false)
+
+ override fun getCheckObservable(): ObservableBoolean {
+ return checked
+ }
+
+ /**
+ * 获取审核状态名称
+ */
+ fun getReviewStatusName(): String {
+ return when (reviewStatus) {
+ "1" -> "已通过"
+ "2" -> "退回"
+ "0" -> "未审核"
+ else -> "未知"
+ }
+ }
+
+ /**
+ * 获取审核状态颜色
+ */
+ fun getReviewStatusColor(): String {
+ return when (reviewStatus) {
+ "1" -> "#4CAF50" // 绿色-已通过
+ "2" -> "#F44336" // 红色-退回
+ else -> "#9E9E9E" // 灰色-未审核
+ }
+ }
+}
diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GncInspectionBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GncInspectionBean.kt
new file mode 100644
index 0000000..cea69f3
--- /dev/null
+++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GncInspectionBean.kt
@@ -0,0 +1,47 @@
+package com.lukouguoji.module_base.bean
+
+import androidx.databinding.ObservableBoolean
+import com.lukouguoji.module_base.interfaces.ICheck
+
+/**
+ * 国内出港收运检查数据Bean
+ */
+class GncInspectionBean : ICheck {
+ var id: String = "" // 主键ID
+ var mawbId: String = "" // 主运单ID
+ var wbNo: String = "" // 运单号
+ var agentCode: String = "" // 代理人
+ var spCode: String = "" // 特码
+ var apc: String = "" // 预配件数
+ var weight: String = "" // 预配重量(kg)
+ var flight: String = "" // 计划航班(格式: 20240216/MU2026)
+ var fdate: String = "" // 航班日期
+ var fno: String = "" // 航班号
+ var route: String = "" // 航程(格式: HFE - PEK)
+ var origin: String = "" // 始发港
+ var dest: String = "" // 目的港
+ var scheduledTackOff: String = "" // 预计起飞时间
+ var businessType: String = "" // 业务类型
+ var auditStatus: String = "" // 审核状态编码
+ var auditStatusName: String = "" // 审核状态名称(已通过/退回/未审核)
+ var remark: String = "" // 备注
+
+ // 多选状态绑定
+ val checked = ObservableBoolean(false)
+
+ override fun getCheckObservable(): ObservableBoolean {
+ return checked
+ }
+
+ /**
+ * 获取审核状态颜色
+ * 已通过=绿色、退回=红色、未审核=灰色
+ */
+ fun getAuditStatusColor(): String {
+ return when (auditStatusName) {
+ "已通过" -> "#4CAF50" // 绿色
+ "退回" -> "#F44336" // 红色
+ else -> "#9E9E9E" // 灰色
+ }
+ }
+}
diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/StatisticsBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/StatisticsBean.kt
new file mode 100644
index 0000000..c653766
--- /dev/null
+++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/StatisticsBean.kt
@@ -0,0 +1,10 @@
+package com.lukouguoji.module_base.bean
+
+/**
+ * 统计数据Bean
+ */
+class StatisticsBean {
+ var totalCount: String = "" // 合计票数
+ var totalPc: String = "" // 总件数
+ var totalWeight: String = "" // 总重量
+}
diff --git a/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt b/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt
index 9c5adc9..09f1b63 100644
--- a/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt
+++ b/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt
@@ -211,6 +211,7 @@ interface Constant {
const val AppDomExpAssemble = "AppDomExpAssemble"//国内出港 组装
const val AppDomExpDistribution = "AppDomExpDistribution"//国内出港 分配
const val AppDomExpDeposit = "AppDomExpDeposit"//国内出港 存放
+ const val AppDomExpInspection = "AppDomExpInspection"//国内出港 收运检查
/**
* 国内进港
@@ -238,6 +239,7 @@ interface Constant {
const val GjcBanXListActivity = "AppIntExpBox" //板箱
const val GjcGoodsListActivity = "AppIntExpGoods" //货物交接
+ const val GjcInspectionActivity = "AppIntExpInspection" //收运检查
/**
* 国际进港
diff --git a/module_base/src/main/java/com/lukouguoji/module_base/common/ConstantEvent.kt b/module_base/src/main/java/com/lukouguoji/module_base/common/ConstantEvent.kt
index af40d14..9cbf53d 100644
--- a/module_base/src/main/java/com/lukouguoji/module_base/common/ConstantEvent.kt
+++ b/module_base/src/main/java/com/lukouguoji/module_base/common/ConstantEvent.kt
@@ -34,4 +34,7 @@ object ConstantEvent {
// 国内进港移库列表刷新
const val EVENT_REFRESH_GNJ_YIKU_LIST = "event_refresh_gnj_yiku_list"
+
+ // 通用刷新事件
+ const val EVENT_REFRESH = "event_refresh"
}
\ No newline at end of file
diff --git a/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt b/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt
index 96ecc68..4df13a1 100644
--- a/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt
+++ b/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt
@@ -42,6 +42,8 @@ import com.lukouguoji.module_base.bean.GncAssembleListBean
import com.lukouguoji.module_base.bean.GncCunFangBean
import com.lukouguoji.module_base.bean.GncDistributionBean
import com.lukouguoji.module_base.bean.GncFuBangBean
+import com.lukouguoji.module_base.bean.GjcInspectionBean
+import com.lukouguoji.module_base.bean.GncInspectionBean
import com.lukouguoji.module_base.bean.GncQueryBean
import com.lukouguoji.module_base.bean.GncQueryDetailsBean
import com.lukouguoji.module_base.bean.GncShouYunBean
@@ -61,6 +63,7 @@ import com.lukouguoji.module_base.bean.PackageBean
import com.lukouguoji.module_base.bean.SYWaybillBean
import com.lukouguoji.module_base.bean.ShouYunSyncBean
import com.lukouguoji.module_base.bean.SimpleResultBean
+import com.lukouguoji.module_base.bean.StatisticsBean
import com.lukouguoji.module_base.bean.TelegramBean
import com.lukouguoji.module_base.bean.TransportLogBean
import com.lukouguoji.module_base.bean.ULDBean
@@ -353,6 +356,23 @@ interface Api {
@PartMap map: MutableMap? = null
): BaseResultBean
+ ///////////////////////////////////////////////////////////////////////////
+ // 国际出 - 收运检查
+ ///////////////////////////////////////////////////////////////////////////
+ /**
+ * 获取-国际出港-收运检查-列表(分页)
+ * 接口路径: /IntExpCheckInCheck/pageQuery
+ */
+ @POST("IntExpCheckInCheck/pageQuery")
+ suspend fun getGjcInspectionList(@Body data: RequestBody): BaseListBean
+
+ /**
+ * 批量审核-国际出港-收运检查(通过/退回)
+ * TODO: 需要确认审核接口路径
+ */
+ @POST("IntExpCheckInCheck/audit")
+ suspend fun auditGjcInspection(@Body data: RequestBody): BaseResultBean
+
///////////////////////////////////////////////////////////////////////////
// 国际进-电报解析
///////////////////////////////////////////////////////////////////////////
@@ -893,6 +913,24 @@ interface Api {
@POST("DomExpSearch/searchById")
suspend fun getGncQueryDetails(@Query("id") id: String): BaseResultBean
+ /**
+ * 获取-国内出港-收运检查-列表
+ */
+ @POST("DomExpInspection/search")
+ suspend fun getGncInspectionList(@Body data: RequestBody): BaseListBean
+
+ /**
+ * 获取-国内出港-收运检查-统计数据
+ */
+ @POST("DomExpInspection/statistics")
+ suspend fun getGncInspectionStatistics(@Body data: RequestBody): BaseResultBean
+
+ /**
+ * 批量审核-国内出港-收运检查(通过/退回)
+ */
+ @POST("DomExpInspection/audit")
+ suspend fun auditGncInspection(@Body data: RequestBody): BaseResultBean
+
/**
* 获取-国内出港-查询-根据主运单Id查询运单详细信息,带入库重量、入库件数
diff --git a/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt b/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt
index 9bcb357..8bce4ed 100644
--- a/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt
+++ b/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt
@@ -80,6 +80,9 @@ object ARouterConstants {
// 存放
const val ACTIVITY_URL_GNC_DEPOSIT = "/gnc/GncDepositListActivity"
+ // 收运检查
+ const val ACTIVITY_URL_GNC_INSPECTION = "/gnc/GncInspectionActivity"
+
///////////////////// 国内进港模块
/**
* 国内进港模块
@@ -121,7 +124,8 @@ object ARouterConstants {
const val ACTIVITY_URL_GJC_YI_KU = "/gjc/GjcYiKuListActivity" //国际出港 移库
const val ACTIVITY_URL_GJC_BOX_ASSEMBLE = "/gjc/GjcBoxAssembleListActivity" //国际出港 板箱组装
- const val ACTIVITY_URL_GJC_GOODS_LIST = "/gjc/GjcGoodsListActivity" //国际出港 板箱组装
+ const val ACTIVITY_URL_GJC_GOODS_LIST = "/gjc/GjcGoodsListActivity" //国际出港 货物交接
+ const val ACTIVITY_URL_GJC_INSPECTION = "/gjc/GjcInspectionActivity" //国际出港 收运检查
///////////////// 国际进港模块
/**
diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcInspectionActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcInspectionActivity.kt
new file mode 100644
index 0000000..2b1ab22
--- /dev/null
+++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcInspectionActivity.kt
@@ -0,0 +1,57 @@
+package com.lukouguoji.gjc.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import com.alibaba.android.arouter.facade.annotation.Route
+import com.lukouguoji.gjc.R
+import com.lukouguoji.gjc.databinding.ActivityGjcInspectionBinding
+import com.lukouguoji.gjc.viewModel.GjcInspectionViewModel
+import com.lukouguoji.module_base.base.BaseBindingActivity
+import com.lukouguoji.module_base.common.ConstantEvent
+import com.lukouguoji.module_base.impl.FlowBus
+import com.lukouguoji.module_base.impl.observe
+import com.lukouguoji.module_base.ktx.getLifecycleOwner
+import com.lukouguoji.module_base.router.ARouterConstants
+
+/**
+ * 国际出港收运检查列表页
+ */
+@Route(path = ARouterConstants.ACTIVITY_URL_GJC_INSPECTION)
+class GjcInspectionActivity :
+ BaseBindingActivity() {
+
+ override fun layoutId() = R.layout.activity_gjc_inspection
+
+ override fun viewModelClass() = GjcInspectionViewModel::class.java
+
+ override fun initOnCreate(savedInstanceState: Bundle?) {
+ setBackArrow("国际出港收运检查")
+
+ binding.viewModel = viewModel
+
+ // 绑定分页逻辑
+ viewModel.pageModel
+ .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner())
+
+ // 监听刷新事件
+ FlowBus.with(ConstantEvent.EVENT_REFRESH)
+ .observe(this) {
+ viewModel.refresh()
+ }
+
+ // 初始化代理列表
+ viewModel.initAgentList()
+
+ // 初始加载
+ viewModel.refresh()
+ }
+
+ companion object {
+ @JvmStatic
+ fun start(context: Context) {
+ val starter = Intent(context, GjcInspectionActivity::class.java)
+ context.startActivity(starter)
+ }
+ }
+}
diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcInspectionViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcInspectionViewHolder.kt
new file mode 100644
index 0000000..e3498a4
--- /dev/null
+++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcInspectionViewHolder.kt
@@ -0,0 +1,28 @@
+package com.lukouguoji.gjc.holder
+
+import android.graphics.Color
+import android.view.View
+import com.lukouguoji.gjc.databinding.ItemGjcInspectionBinding
+import com.lukouguoji.module_base.base.BaseViewHolder
+import com.lukouguoji.module_base.bean.GjcInspectionBean
+
+/**
+ * 国际出港收运检查列表 ViewHolder
+ */
+class GjcInspectionViewHolder(view: View) :
+ BaseViewHolder(view) {
+
+ override fun onBind(item: Any?, position: Int) {
+ val bean = getItemBean(item)!!
+ binding.bean = bean
+
+ // 点击checkbox切换选中状态
+ binding.ivIcon.setOnClickListener {
+ bean.checked.set(!bean.checked.get())
+ }
+
+ // 设置审核状态文本和颜色
+ binding.tvStatus.text = bean.getReviewStatusName()
+ binding.tvStatus.setTextColor(Color.parseColor(bean.getReviewStatusColor()))
+ }
+}
diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcInspectionViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcInspectionViewModel.kt
new file mode 100644
index 0000000..0fe99b9
--- /dev/null
+++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcInspectionViewModel.kt
@@ -0,0 +1,197 @@
+package com.lukouguoji.gjc.viewModel
+
+import android.app.Activity
+import android.content.Intent
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.lukouguoji.gjc.R
+import com.lukouguoji.gjc.holder.GjcInspectionViewHolder
+import com.lukouguoji.module_base.base.BasePageViewModel
+import com.lukouguoji.module_base.bean.GjcInspectionBean
+import com.lukouguoji.module_base.bean.StatisticsBean
+import com.lukouguoji.module_base.common.Constant
+import com.lukouguoji.module_base.common.ConstantEvent
+import com.lukouguoji.module_base.http.net.NetApply
+import com.lukouguoji.module_base.impl.FlowBus
+import com.lukouguoji.module_base.ktx.commonAdapter
+import com.lukouguoji.module_base.ktx.launchCollect
+import com.lukouguoji.module_base.ktx.launchLoadingCollect
+import com.lukouguoji.module_base.ktx.noNull
+import com.lukouguoji.module_base.ktx.showConfirmDialog
+import com.lukouguoji.module_base.ktx.showToast
+import com.lukouguoji.module_base.ktx.toRequestBody
+import com.lukouguoji.module_base.model.ScanModel
+import com.lukouguoji.module_base.util.CheckUtil
+import dev.utils.app.info.KeyValue
+import kotlinx.coroutines.launch
+
+/**
+ * 国际出港收运检查 ViewModel
+ */
+class GjcInspectionViewModel : BasePageViewModel() {
+
+ // 搜索条件
+ val flightDate = MutableLiveData("") // 航班日期
+ val flightNo = MutableLiveData("") // 航班号
+ val agentId = MutableLiveData("") // 代理ID
+ val auditStatus = MutableLiveData("") // 审核状态
+ val waybillNo = MutableLiveData("") // 运单号
+
+ // 代理下拉列表(需要从API获取,暂时用空列表)
+ val agentList = MutableLiveData(listOf(KeyValue("全部", "")))
+
+ // 审核状态下拉列表
+ val auditStatusList = MutableLiveData(
+ listOf(
+ KeyValue("全部", ""),
+ KeyValue("已通过", "1"),
+ KeyValue("退回", "2"),
+ KeyValue("未审核", "0"),
+ )
+ )
+
+ // 适配器配置
+ val itemViewHolder = GjcInspectionViewHolder::class.java
+ val itemLayoutId = R.layout.item_gjc_inspection
+
+ // 统计数据
+ val totalCount = MutableLiveData("0") // 合计票数
+ val totalPc = MutableLiveData("0") // 总件数
+ val totalWeight = MutableLiveData("0") // 总重量
+
+ ///////////////////////////////////////////////////////////////////////////
+ // 方法区
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * 扫码输入运单号
+ */
+ fun waybillScanClick() {
+ ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL)
+ }
+
+ /**
+ * 搜索按钮点击
+ */
+ fun searchClick() {
+ refresh()
+ }
+
+ /**
+ * 获取列表数据
+ */
+ override fun getData() {
+ val body = mapOf(
+ "pageNum" to pageModel.page,
+ "pageSize" to pageModel.limit,
+ "fdate" to flightDate.value!!.ifEmpty { null },
+ "fno" to flightNo.value!!.ifEmpty { null },
+ "agentCode" to agentId.value!!.ifEmpty { null },
+ "reviewStatus" to auditStatus.value!!.ifEmpty { null },
+ "wbNo" to waybillNo.value!!.ifEmpty { null },
+ ).toRequestBody()
+
+ launchLoadingCollect({
+ NetApply.api.getGjcInspectionList(body)
+ }) {
+ onSuccess = {
+ pageModel.handleListBean(it)
+ // 更新统计数据(包含在返回结果中)
+ totalCount.value = (it.total ?: 0).toString()
+ totalPc.value = (it.totalPc ?: 0).toString()
+ totalWeight.value = (it.totalWeight ?: 0.0).toString()
+ }
+ }
+ }
+
+ /**
+ * 处理扫码结果
+ */
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ when (requestCode) {
+ Constant.RequestCode.WAYBILL -> {
+ waybillNo.value = data.getStringExtra(Constant.Result.CODED_CONTENT)
+ refresh()
+ }
+ }
+ }
+ }
+
+ /**
+ * 批量审核 - 通过
+ */
+ fun auditPassClick() {
+ val list = pageModel.rv!!.commonAdapter()!!.items as List
+ val filter = list.filter { it.checked.get() }
+ if (filter.isEmpty()) {
+ showToast("请选择数据")
+ return
+ }
+ getTopActivity().showConfirmDialog("确定要通过选中的 ${filter.size} 条数据吗?") {
+ performAudit(filter, "1", "通过")
+ }
+ }
+
+ /**
+ * 批量审核 - 退回
+ */
+ fun auditRejectClick() {
+ val list = pageModel.rv!!.commonAdapter()!!.items as List
+ val filter = list.filter { it.checked.get() }
+ if (filter.isEmpty()) {
+ showToast("请选择数据")
+ return
+ }
+ getTopActivity().showConfirmDialog("确定要退回选中的 ${filter.size} 条数据吗?") {
+ performAudit(filter, "2", "退回")
+ }
+ }
+
+ /**
+ * 执行审核操作
+ * @param items 选中的数据列表
+ * @param status 审核状态(1:通过, 2:退回)
+ * @param action 操作名称(用于提示)
+ */
+ private fun performAudit(items: List, status: String, action: String) {
+ launchLoadingCollect({
+ NetApply.api.auditGjcInspection(
+ mapOf(
+ "ids" to items.map { it.maWbId },
+ "reviewStatus" to status,
+ ).toRequestBody()
+ )
+ }) {
+ onSuccess = {
+ showToast(it.msg.noNull("${action}成功"))
+ viewModelScope.launch {
+ FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh")
+ }
+ refresh()
+ }
+ }
+ }
+
+ /**
+ * 全选/全不选
+ */
+ fun checkAllClick() {
+ val list = pageModel.rv!!.commonAdapter()!!.items as List
+ CheckUtil.handleAllCheck(list)
+ }
+
+ /**
+ * 初始化代理下拉列表(从API获取)
+ */
+ fun initAgentList() {
+ // TODO: 调用API获取代理列表
+ // 暂时使用模拟数据
+ agentList.value = listOf(
+ KeyValue("全部", ""),
+ KeyValue("SF", "SF"),
+ KeyValue("YTO", "YTO"),
+ KeyValue("ZTO", "ZTO"),
+ )
+ }
+}
diff --git a/module_gjc/src/main/res/layout/activity_gjc_inspection.xml b/module_gjc/src/main/res/layout/activity_gjc_inspection.xml
new file mode 100644
index 0000000..78857f4
--- /dev/null
+++ b/module_gjc/src/main/res/layout/activity_gjc_inspection.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/module_gjc/src/main/res/layout/item_gjc_inspection.xml b/module_gjc/src/main/res/layout/item_gjc_inspection.xml
new file mode 100644
index 0000000..7181add
--- /dev/null
+++ b/module_gjc/src/main/res/layout/item_gjc_inspection.xml
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionActivity.kt b/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionActivity.kt
new file mode 100644
index 0000000..7825f77
--- /dev/null
+++ b/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionActivity.kt
@@ -0,0 +1,56 @@
+package com.lukouguoji.gnc.page.inspection
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import com.alibaba.android.arouter.facade.annotation.Route
+import com.lukouguoji.gnc.R
+import com.lukouguoji.gnc.databinding.ActivityGncInspectionBinding
+import com.lukouguoji.module_base.base.BaseBindingActivity
+import com.lukouguoji.module_base.common.ConstantEvent
+import com.lukouguoji.module_base.impl.FlowBus
+import com.lukouguoji.module_base.impl.observe
+import com.lukouguoji.module_base.ktx.getLifecycleOwner
+import com.lukouguoji.module_base.router.ARouterConstants
+
+/**
+ * 国内出港收运检查列表页
+ */
+@Route(path = ARouterConstants.ACTIVITY_URL_GNC_INSPECTION)
+class GncInspectionActivity :
+ BaseBindingActivity() {
+
+ override fun layoutId() = R.layout.activity_gnc_inspection
+
+ override fun viewModelClass() = GncInspectionViewModel::class.java
+
+ override fun initOnCreate(savedInstanceState: Bundle?) {
+ setBackArrow("出港收运审核")
+
+ binding.viewModel = viewModel
+
+ // 绑定分页逻辑
+ viewModel.pageModel
+ .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner())
+
+ // 监听刷新事件
+ FlowBus.with(ConstantEvent.EVENT_REFRESH)
+ .observe(this) {
+ viewModel.refresh()
+ }
+
+ // 初始化代理列表
+ viewModel.initAgentList()
+
+ // 初始加载
+ viewModel.refresh()
+ }
+
+ companion object {
+ @JvmStatic
+ fun start(context: Context) {
+ val starter = Intent(context, GncInspectionActivity::class.java)
+ context.startActivity(starter)
+ }
+ }
+}
diff --git a/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionViewHolder.kt b/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionViewHolder.kt
new file mode 100644
index 0000000..d342788
--- /dev/null
+++ b/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionViewHolder.kt
@@ -0,0 +1,31 @@
+package com.lukouguoji.gnc.page.inspection
+
+import android.graphics.Color
+import android.view.View
+import com.lukouguoji.gnc.databinding.ItemGncInspectionBinding
+import com.lukouguoji.module_base.base.BaseViewHolder
+import com.lukouguoji.module_base.bean.GncInspectionBean
+
+/**
+ * 国内出港收运检查列表 ViewHolder
+ */
+class GncInspectionViewHolder(view: View) :
+ BaseViewHolder(view) {
+
+ override fun onBind(item: Any?, position: Int) {
+ val bean = getItemBean(item)!!
+ binding.bean = bean
+
+ // 点击checkbox切换选中状态
+ binding.ivIcon.setOnClickListener {
+ bean.checked.set(!bean.checked.get())
+ }
+
+ // 根据审核状态设置颜色
+ when (bean.auditStatusName) {
+ "已通过" -> binding.tvStatus.setTextColor(Color.parseColor("#4CAF50")) // 绿色
+ "退回" -> binding.tvStatus.setTextColor(Color.parseColor("#F44336")) // 红色
+ else -> binding.tvStatus.setTextColor(Color.parseColor("#9E9E9E")) // 灰色
+ }
+ }
+}
diff --git a/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionViewModel.kt b/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionViewModel.kt
new file mode 100644
index 0000000..07f8721
--- /dev/null
+++ b/module_gnc/src/main/java/com/lukouguoji/gnc/page/inspection/GncInspectionViewModel.kt
@@ -0,0 +1,206 @@
+package com.lukouguoji.gnc.page.inspection
+
+import android.app.Activity
+import android.content.Intent
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.lukouguoji.gnc.R
+import com.lukouguoji.module_base.base.BasePageViewModel
+import com.lukouguoji.module_base.bean.GncInspectionBean
+import com.lukouguoji.module_base.bean.StatisticsBean
+import com.lukouguoji.module_base.common.Constant
+import com.lukouguoji.module_base.common.ConstantEvent
+import com.lukouguoji.module_base.http.net.NetApply
+import com.lukouguoji.module_base.impl.FlowBus
+import com.lukouguoji.module_base.ktx.commonAdapter
+import com.lukouguoji.module_base.ktx.launchCollect
+import com.lukouguoji.module_base.ktx.launchLoadingCollect
+import com.lukouguoji.module_base.ktx.noNull
+import com.lukouguoji.module_base.ktx.showConfirmDialog
+import com.lukouguoji.module_base.ktx.showToast
+import com.lukouguoji.module_base.ktx.toRequestBody
+import com.lukouguoji.module_base.model.ScanModel
+import com.lukouguoji.module_base.util.CheckUtil
+import dev.utils.app.info.KeyValue
+import kotlinx.coroutines.launch
+
+/**
+ * 国内出港收运检查 ViewModel
+ */
+class GncInspectionViewModel : BasePageViewModel() {
+
+ // 搜索条件
+ val flightDate = MutableLiveData("") // 航班日期
+ val flightNo = MutableLiveData("") // 航班号
+ val agentId = MutableLiveData("") // 代理ID
+ val auditStatus = MutableLiveData("") // 审核状态
+ val waybillNo = MutableLiveData("") // 运单号
+
+ // 代理下拉列表(需要从API获取,暂时用空列表)
+ val agentList = MutableLiveData(listOf(KeyValue("全部", "")))
+
+ // 审核状态下拉列表
+ val auditStatusList = MutableLiveData(
+ listOf(
+ KeyValue("全部", ""),
+ KeyValue("已通过", "1"),
+ KeyValue("退回", "2"),
+ KeyValue("未审核", "0"),
+ )
+ )
+
+ // 适配器配置
+ val itemViewHolder = GncInspectionViewHolder::class.java
+ val itemLayoutId = R.layout.item_gnc_inspection
+
+ // 统计数据
+ val totalCount = MutableLiveData("0") // 合计票数
+ val totalPc = MutableLiveData("0") // 总件数
+ val totalWeight = MutableLiveData("0") // 总重量
+
+ ///////////////////////////////////////////////////////////////////////////
+ // 方法区
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * 扫码输入运单号
+ */
+ fun waybillScanClick() {
+ ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL)
+ }
+
+ /**
+ * 搜索按钮点击
+ */
+ fun searchClick() {
+ refresh()
+ }
+
+ /**
+ * 获取列表数据
+ */
+ override fun getData() {
+ val body = mapOf(
+ "page" to pageModel.page,
+ "limit" to pageModel.limit,
+ "flightDate" to flightDate.value!!.ifEmpty { null },
+ "flightNo" to flightNo.value!!.ifEmpty { null },
+ "agentId" to agentId.value!!.ifEmpty { null },
+ "auditStatus" to auditStatus.value!!.ifEmpty { null },
+ "waybillNo" to waybillNo.value!!.ifEmpty { null },
+ ).toRequestBody()
+
+ launchLoadingCollect({
+ NetApply.api.getGncInspectionList(body)
+ }) {
+ onSuccess = {
+ pageModel.handleListBean(it)
+ }
+ }
+ getStatistics(body)
+ }
+
+ /**
+ * 获取统计数据
+ */
+ private fun getStatistics(body: okhttp3.RequestBody) {
+ launchCollect({
+ NetApply.api.getGncInspectionStatistics(body)
+ }) {
+ onSuccess = {
+ val stats = it.data ?: StatisticsBean()
+ totalCount.value = stats.totalCount.noNull("0")
+ totalPc.value = stats.totalPc.noNull("0")
+ totalWeight.value = stats.totalWeight.noNull("0")
+ }
+ }
+ }
+
+ /**
+ * 处理扫码结果
+ */
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ when (requestCode) {
+ Constant.RequestCode.WAYBILL -> {
+ waybillNo.value = data.getStringExtra(Constant.Result.CODED_CONTENT)
+ refresh()
+ }
+ }
+ }
+ }
+
+ /**
+ * 批量审核 - 通过
+ */
+ fun auditPassClick() {
+ val list = pageModel.rv!!.commonAdapter()!!.items as List
+ val filter = list.filter { it.checked.get() }
+ if (filter.isEmpty()) {
+ showToast("请选择数据")
+ return
+ }
+ getTopActivity().showConfirmDialog("确定要通过选中的 ${filter.size} 条数据吗?") {
+ performAudit(filter, "1", "通过")
+ }
+ }
+
+ /**
+ * 批量审核 - 退回
+ */
+ fun auditRejectClick() {
+ val list = pageModel.rv!!.commonAdapter()!!.items as List
+ val filter = list.filter { it.checked.get() }
+ if (filter.isEmpty()) {
+ showToast("请选择数据")
+ return
+ }
+ getTopActivity().showConfirmDialog("确定要退回选中的 ${filter.size} 条数据吗?") {
+ performAudit(filter, "2", "退回")
+ }
+ }
+
+ /**
+ * 执行审核操作
+ */
+ private fun performAudit(items: List, status: String, action: String) {
+ launchLoadingCollect({
+ NetApply.api.auditGncInspection(
+ mapOf(
+ "ids" to items.map { it.id },
+ "auditStatus" to status,
+ ).toRequestBody()
+ )
+ }) {
+ onSuccess = {
+ showToast(it.msg.noNull("${action}成功"))
+ viewModelScope.launch {
+ FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh")
+ }
+ refresh()
+ }
+ }
+ }
+
+ /**
+ * 全选/全不选
+ */
+ fun checkAllClick() {
+ val list = pageModel.rv!!.commonAdapter()!!.items as List
+ CheckUtil.handleAllCheck(list)
+ }
+
+ /**
+ * 初始化代理下拉列表(从API获取)
+ */
+ fun initAgentList() {
+ // TODO: 调用API获取代理列表
+ // 暂时使用模拟数据
+ agentList.value = listOf(
+ KeyValue("全部", ""),
+ KeyValue("SF", "SF"),
+ KeyValue("YTO", "YTO"),
+ KeyValue("ZTO", "ZTO"),
+ )
+ }
+}
diff --git a/module_gnc/src/main/res/layout/activity_gnc_inspection.xml b/module_gnc/src/main/res/layout/activity_gnc_inspection.xml
new file mode 100644
index 0000000..46a5b48
--- /dev/null
+++ b/module_gnc/src/main/res/layout/activity_gnc_inspection.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/module_gnc/src/main/res/layout/item_gnc_inspection.xml b/module_gnc/src/main/res/layout/item_gnc_inspection.xml
new file mode 100644
index 0000000..d73ce6a
--- /dev/null
+++ b/module_gnc/src/main/res/layout/item_gnc_inspection.xml
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+