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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +