| | |
| | | - 自定义绑定适配器 |
| | | - 表达式支持 |
| | | |
| | | ### RecyclerView 列表为空时的实现 |
| | | 在 RecyclerView 适配器中,当列表数据为空时,显示一个空视图(EmptyView)的实现方式: |
| | | |
| | | 1. 继承 BaseRecycleAdapter: |
| | | ```kotlin |
| | | class YourAdapter : BaseRecycleAdapter<RecyclerView.ViewHolder>() { |
| | | // 实现必要的方法 |
| | | } |
| | | ``` |
| | | |
| | | 2. 在适配器中定义视图类型常量(已在 BaseRecycleAdapter 中定义): |
| | | ```kotlin |
| | | companion object { |
| | | const val VIEW_TYPE_ITEM = 1 |
| | | const val VIEW_TYPE_EMPTY = 0 |
| | | } |
| | | ``` |
| | | |
| | | 3. 重写 getItemViewType 方法: |
| | | ```kotlin |
| | | override fun getItemViewType(position: Int): Int { |
| | | if (dataList.isEmpty()) { |
| | | return VIEW_TYPE_EMPTY |
| | | } |
| | | return VIEW_TYPE_ITEM |
| | | } |
| | | ``` |
| | | |
| | | 4. 重写 getItemCount 方法: |
| | | ```kotlin |
| | | override fun getItemCount(): Int { |
| | | if (dataList.isEmpty()) { |
| | | return 1 // 返回1表示显示空视图 |
| | | } |
| | | return dataList.size |
| | | } |
| | | ``` |
| | | |
| | | 5. 在 onCreateViewHolder 中处理不同类型的视图: |
| | | ```kotlin |
| | | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { |
| | | if (viewType == VIEW_TYPE_EMPTY) { |
| | | val emptyView: ItemNoMoreBinding = DataBindingUtil.inflate( |
| | | (parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)!!, |
| | | R.layout.item_no_more, |
| | | parent, |
| | | false |
| | | ) |
| | | return ViewHolderEmpty(emptyView) |
| | | } else { |
| | | val binding = ItemListBinding.inflate( |
| | | parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater, |
| | | parent, |
| | | false |
| | | ) |
| | | return ItemViewHolder(binding.root) |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 6. 重写 onBindViewHolder 方法: |
| | | ```kotlin |
| | | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { |
| | | if (holder is ViewHolderEmpty) { |
| | | // 空视图不需要绑定数据 |
| | | return |
| | | } |
| | | |
| | | // 绑定列表项数据 |
| | | if (holder is ItemViewHolder) { |
| | | val item = dataList[position] |
| | | holder.bind(item) |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 7. 空视图的布局文件示例(item_no_more.xml): |
| | | ```xml |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <layout xmlns:android="http://schemas.android.com/apk/res/android"> |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:gravity="center" |
| | | android:orientation="vertical" |
| | | android:padding="16dp"> |
| | | |
| | | <ImageView |
| | | android:layout_width="48dp" |
| | | android:layout_height="48dp" |
| | | android:src="@drawable/ic_empty" /> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginTop="8dp" |
| | | android:text="暂无数据" |
| | | android:textColor="@color/text_gray" |
| | | android:textSize="14sp" /> |
| | | </LinearLayout> |
| | | </layout> |
| | | ``` |
| | | |
| | | 这种实现方式的优点: |
| | | 1. 统一的空视图处理逻辑 |
| | | 2. 支持自定义空视图样式 |
| | | 3. 不影响列表正常数据的显示 |
| | | 4. 便于维护和扩展 |
| | | |
| | | ## 数据库结构 |
| | | |
| | | ### GeneralLibrary 数据库 |
| | |
| | | - PassWordCardBean: 密码卡数据 |
| | | - CardData: 卡片数据 |
| | | - ProjectDataBean: 项目数据 |
| | | |
| | | ## 数据类管理规范 |
| | | |
| | | ### Bean包结构组织 |
| | | |
| | | 项目中所有的数据类(Data Class)都应该统一放在`bean`包下的相应子目录中,按照功能和用途进行分类: |
| | | |
| | | ``` |
| | | generallibrary/src/main/java/com/dayu/general/bean/ |
| | | ├── net/ # 网络接口相关数据类 |
| | | ├── card/ # 卡片相关数据类 |
| | | ├── db/ # 数据库实体类 |
| | | └── ... # 其他功能模块数据类 |
| | | ``` |
| | | |
| | | #### 1. 网络接口数据类 (bean/net/) |
| | | |
| | | 所有API接口的请求和响应数据类都放在`net`目录下: |
| | | |
| | | ```kotlin |
| | | // 请求数据类示例 |
| | | data class RechargeRequest( |
| | | val rechargeType: Int, |
| | | val cardNum: String, |
| | | val money: String, |
| | | // ... 其他字段 |
| | | ) |
| | | |
| | | // 响应数据类示例 |
| | | data class CardCancelResult( |
| | | val projectNo: Int, |
| | | val cardNum: String, |
| | | val orderNo: String |
| | | ) |
| | | ``` |
| | | |
| | | #### 2. 卡片相关数据类 (bean/card/) |
| | | |
| | | 所有卡片操作相关的数据类放在`card`目录下: |
| | | |
| | | ```kotlin |
| | | data class UserCard( |
| | | var cardType: String = "", |
| | | var balance: Int = 0, |
| | | var userCode: String = "", |
| | | // ... 其他字段 |
| | | ) |
| | | ``` |
| | | |
| | | #### 3. 数据库实体类 (bean/db/) |
| | | |
| | | 所有Room数据库的实体类放在`db`目录下: |
| | | |
| | | ```kotlin |
| | | @Entity(tableName = "card_data") |
| | | data class CardData( |
| | | @PrimaryKey val id: Long, |
| | | val cardNumber: String, |
| | | // ... 其他字段 |
| | | ) |
| | | ``` |
| | | |
| | | ### 数据类命名规范 |
| | | |
| | | 1. **接口响应数据类**: 以`Result`结尾 |
| | | - `CardCancelResult` - 销卡接口响应 |
| | | - `RechargeResult` - 充值接口响应 |
| | | - `WaterPriceResult` - 水价接口响应 |
| | | |
| | | 2. **接口请求数据类**: 以`Request`结尾 |
| | | - `RechargeRequest` - 充值接口请求 |
| | | - `SearchUserBeanRequest` - 用户搜索请求 |
| | | |
| | | 3. **业务实体数据类**: 使用业务名称 |
| | | - `UserCard` - 用户卡片数据 |
| | | - `ClearCard` - 清零卡数据 |
| | | |
| | | 4. **数据库实体类**: 使用表名对应的实体名 |
| | | - `CardData` - 卡片数据表 |
| | | - `ProjectDataBean` - 项目数据表 |
| | | |
| | | ### 数据类创建最佳实践 |
| | | |
| | | #### 1. 文件头注释规范 |
| | | |
| | | ```kotlin |
| | | package com.dayu.general.bean.net |
| | | |
| | | /** |
| | | * 销卡结果数据类 |
| | | * @author: zuo |
| | | * @date: 2025/01/17 |
| | | * @description: 销卡接口返回数据 |
| | | */ |
| | | data class CardCancelResult( |
| | | val projectNo: Int, // 项目编号 |
| | | val cardNum: String, // 卡号 |
| | | val orderNo: String // 订单号 |
| | | ) |
| | | ``` |
| | | |
| | | #### 2. 字段注释规范 |
| | | |
| | | - 每个字段都应该添加行内注释说明用途 |
| | | - 对于枚举值或特殊含义的字段,详细说明取值范围 |
| | | |
| | | #### 3. 导入使用规范 |
| | | |
| | | 在Activity或其他类中使用数据类时,应该导入具体的数据类: |
| | | |
| | | ```kotlin |
| | | // 正确的导入方式 |
| | | import com.dayu.general.bean.net.CardCancelResult |
| | | import com.dayu.general.bean.net.RechargeResult |
| | | |
| | | // 在代码中使用 |
| | | private fun handleCancelResult(result: CardCancelResult) { |
| | | // 处理销卡结果 |
| | | } |
| | | ``` |
| | | |
| | | #### 4. 禁止内联定义 |
| | | |
| | | **禁止**在Activity或其他类中内联定义数据类: |
| | | |
| | | ```kotlin |
| | | // ❌ 错误做法 - 不要在Activity中内联定义数据类 |
| | | class CardCancelActivity : BaseNfcActivity() { |
| | | |
| | | // ❌ 禁止这样做 |
| | | data class CardCancelResult( |
| | | val projectNo: Int, |
| | | val cardNum: String, |
| | | val orderNo: String |
| | | ) |
| | | } |
| | | |
| | | // ✅ 正确做法 - 在bean包中定义数据类 |
| | | // 文件: generallibrary/src/main/java/com/dayu/general/bean/net/CardCancelResult.kt |
| | | data class CardCancelResult( |
| | | val projectNo: Int, |
| | | val cardNum: String, |
| | | val orderNo: String |
| | | ) |
| | | ``` |
| | | |
| | | ### 数据类管理优势 |
| | | |
| | | 1. **统一管理**: 所有数据类集中在bean包下,便于查找和维护 |
| | | 2. **职责分离**: 业务逻辑和数据结构分离,代码结构更清晰 |
| | | 3. **复用性强**: 数据类可以在多个模块间复用 |
| | | 4. **易于重构**: 数据结构变更时只需修改一个文件 |
| | | 5. **类型安全**: 编译期类型检查,减少运行时错误 |
| | | |
| | | ### 迁移现有代码 |
| | | |
| | | 对于已经存在的内联数据类,应该按照以下步骤进行迁移: |
| | | |
| | | 1. 在bean包的相应子目录下创建数据类文件 |
| | | 2. 移动数据类定义到新文件 |
| | | 3. 在原文件中添加导入语句 |
| | | 4. 测试确保功能正常 |
| | | 5. 提交代码变更 |
| | | |
| | | 通过统一的数据类管理规范,可以提高代码的可维护性和可读性,使项目结构更加清晰规范。 |
| | | |
| | | ## 开发环境要求 |
| | | |
| | |
| | | 2. 点击事件监听 |
| | | ```kotlin |
| | | // 方式1:使用类型常量(推荐) |
| | | titleBar.setOnItemclickListner(TitleBar.ClickType_LEFT_IMAGE) { finish() } |
| | | titleBar.setOnItemclickListner(TitleBar.ClickType_RIGHT_TEXT) { showMenu() } |
| | | titleBar.setOnItemclickListner(TitleBar.ClickType_LEFT_IMAGE) { |
| | | finish() |
| | | } |
| | | titleBar.setOnItemclickListner(TitleBar.ClickType_RIGHT_TEXT) { |
| | | showMenu() |
| | | } |
| | | |
| | | // 或者使用完整的OnClickListener |
| | | titleBar.setOnItemclickListner(TitleBar.ClickType_LEFT_IMAGE, View.OnClickListener { |
| | | finish() |
| | | }) |
| | | |
| | | // 方式2:使用位置和类型常量(已废弃) |
| | | titleBar.setOnItemclickListner(TitleBar.IMAGE, TitleBar.LEFT) { finish() } |
| | |
| | | 2. 设置文本或图片时,如果传入null或0,对应的视图将被隐藏 |
| | | 3. 组件默认使用垂直线性布局,确保在布局文件中设置合适的高度 |
| | | |
| | | #### 常见使用示例 |
| | | ```kotlin |
| | | // 在Activity的initView方法中设置 |
| | | private fun initView() { |
| | | // 设置返回按钮点击事件 |
| | | binding.titleBar.setOnItemclickListner(TitleBar.ClickType_LEFT_IMAGE) { |
| | | finish() |
| | | } |
| | | |
| | | // 设置右侧文本按钮点击事件 |
| | | binding.titleBar.setOnItemclickListner(TitleBar.ClickType_RIGHT_TEXT) { |
| | | // 处理右侧按钮点击逻辑 |
| | | handleRightButtonClick() |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ## 支付方式动态获取功能 |
| | | |
| | | 项目中实现了支付方式的动态获取和显示功能,支持从服务器获取支付方式列表并动态创建RadioButton。 |
| | | |
| | | ### 数据结构 |
| | | |
| | | ```kotlin |
| | | // 支付方式数据类 |
| | | data class PaymentMethod( |
| | | val id: Long, |
| | | val name: String, |
| | | val remarks: String, |
| | | val deleted: Int |
| | | ) |
| | | |
| | | // 支付方式接口返回数据类 |
| | | data class PaymentMethodResponse( |
| | | val itemTotal: Any?, |
| | | val obj: List<PaymentMethod>, |
| | | val pageCurr: Any?, |
| | | val pageSize: Any?, |
| | | val pageTotal: Any? |
| | | ) |
| | | ``` |
| | | |
| | | ### 使用方式 |
| | | |
| | | #### 1. 在Activity中添加支付方式相关属性 |
| | | |
| | | ```kotlin |
| | | class YourActivity : AppCompatActivity() { |
| | | // 支付方式相关属性 |
| | | private var paymentMethod: String = "现金" |
| | | private var paymentId: Long = 0 |
| | | private var paymentMethodList: List<PaymentMethod> = listOf() |
| | | |
| | | // ... 其他代码 |
| | | } |
| | | ``` |
| | | |
| | | #### 2. 获取支付方式列表 |
| | | |
| | | ```kotlin |
| | | /** |
| | | * 获取支付方式列表 |
| | | */ |
| | | private fun getPaymentMethods() { |
| | | ApiManager.getInstance().requestGetLoading( |
| | | this, |
| | | "terminal/paymentmethod/get", |
| | | PaymentMethodResponse::class.java, |
| | | null, |
| | | object : SubscriberListener<BaseResponse<PaymentMethodResponse>>() { |
| | | override fun onNext(response: BaseResponse<PaymentMethodResponse>) { |
| | | if (response.success) { |
| | | val paymentMethods = response.content?.obj ?: listOf() |
| | | if (paymentMethods.isNotEmpty()) { |
| | | paymentMethodList = paymentMethods |
| | | updatePaymentMethodRadioGroup() |
| | | } |
| | | } else { |
| | | Toast.makeText(this@YourActivity, "获取支付方式失败: ${response.msg}", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | |
| | | override fun onError(e: Throwable?) { |
| | | super.onError(e) |
| | | Toast.makeText(this@YourActivity, "获取支付方式失败: ${e?.message ?: "网络异常"}", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | ) |
| | | } |
| | | ``` |
| | | |
| | | #### 3. 动态创建RadioButton |
| | | |
| | | ```kotlin |
| | | /** |
| | | * 更新支付方式RadioGroup |
| | | */ |
| | | private fun updatePaymentMethodRadioGroup() { |
| | | // 清空原有RadioButton |
| | | binding.paymentMethodRadioGroup.removeAllViews() |
| | | |
| | | // 动态添加RadioButton |
| | | paymentMethodList.forEachIndexed { index, method -> |
| | | val radioButton = RadioButton(this) |
| | | radioButton.id = View.generateViewId() |
| | | radioButton.layoutParams = LinearLayout.LayoutParams(0, resources.getDimensionPixelSize(R.dimen.dimen_40), 1.0f) |
| | | |
| | | // 设置样式 |
| | | radioButton.text = method.name |
| | | radioButton.background = resources.getDrawable(R.drawable.radio_selector) |
| | | radioButton.buttonDrawable = null |
| | | radioButton.gravity = Gravity.CENTER |
| | | radioButton.setTextColor(resources.getColorStateList(R.color.radio_button_text_color)) |
| | | radioButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) |
| | | |
| | | // 添加到RadioGroup |
| | | binding.paymentMethodRadioGroup.addView(radioButton) |
| | | |
| | | // 默认选中第一个 |
| | | if (index == 0) { |
| | | radioButton.isChecked = true |
| | | paymentMethod = method.name |
| | | paymentId = method.id |
| | | } |
| | | } |
| | | |
| | | // 设置选择监听 |
| | | binding.paymentMethodRadioGroup.setOnCheckedChangeListener { group, checkedId -> |
| | | for (i in 0 until group.childCount) { |
| | | val radioButton = group.getChildAt(i) as RadioButton |
| | | if (radioButton.id == checkedId) { |
| | | paymentMethod = radioButton.text.toString() |
| | | paymentId = paymentMethodList[i].id |
| | | break |
| | | } |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### 4. 布局文件配置 |
| | | |
| | | ```xml |
| | | <RadioGroup |
| | | android:id="@+id/paymentMethodRadioGroup" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="horizontal"> |
| | | <!-- 动态添加RadioButton,不需要预定义 --> |
| | | </RadioGroup> |
| | | ``` |
| | | |
| | | ### 功能特点 |
| | | |
| | | 1. **动态获取**: 从服务器动态获取支付方式列表,支持后台配置 |
| | | 2. **自动布局**: 根据支付方式数量自动调整RadioButton布局 |
| | | 3. **样式统一**: 所有动态创建的RadioButton使用统一的样式 |
| | | 4. **默认选择**: 自动选中第一个支付方式作为默认选项 |
| | | 5. **事件处理**: 支持选择变化监听,实时更新当前选中的支付方式 |
| | | |
| | | ### 注意事项 |
| | | |
| | | 1. 确保在调用`getPaymentMethods()`前已经初始化了相关的UI组件 |
| | | 2. 动态创建的RadioButton需要设置唯一的ID,使用`View.generateViewId()` |
| | | 3. 在Activity销毁时注意清理相关资源,避免内存泄漏 |
| | | 4. 网络请求失败时要有相应的错误处理机制 |
| | | |
| | | ## 注意事项 |
| | | |
| | | 1. 数据库迁移 |
| | |
| | | - 敏感数据需要加密存储 |
| | | - 注意用户数据的安全处理 |
| | | |
| | | 4. 异常处理 |
| | | - 所有try catch块中必须使用`CrashReport.postCatchedException(e)`上报异常 |
| | | - 确保异常信息被正确记录和上报 |
| | | - 避免异常信息泄露敏感数据 |
| | | |
| | | 5. API异常处理最佳实践 |
| | | - 针对特定错误码提供友好的用户提示 |
| | | - 区分网络异常、业务异常和系统异常 |
| | | - 异常发生后及时重置UI状态,允许用户重试 |
| | | - 提供明确的操作指引,如"请先进行开卡操作" |
| | | |
| | | ### API异常处理示例 |
| | | |
| | | ```kotlin |
| | | // 在API回调中处理不同类型的异常 |
| | | object : SubscriberListener<BaseResponse<YourDataType>>() { |
| | | override fun onNext(response: BaseResponse<YourDataType>) { |
| | | if (response.success) { |
| | | // 处理成功情况 |
| | | handleSuccess(response.content) |
| | | } else { |
| | | // 处理业务异常 |
| | | handleBusinessError(response.code, response.msg) |
| | | } |
| | | } |
| | | |
| | | override fun onError(e: Throwable?) { |
| | | super.onError(e) |
| | | // 处理网络异常 |
| | | handleNetworkError(e) |
| | | // 重置UI状态 |
| | | resetViewState() |
| | | } |
| | | } |
| | | |
| | | // 业务异常处理方法 |
| | | private fun handleBusinessError(code: String?, msg: String?) { |
| | | when (code) { |
| | | "1081" -> ToastUtil.show("该卡片未在系统中注册,请先进行开卡操作") |
| | | "1001" -> ToastUtil.show("权限不足,请联系管理员") |
| | | "1002" -> ToastUtil.show("账户余额不足") |
| | | else -> { |
| | | val errorMsg = when { |
| | | msg.isNullOrBlank() -> "操作失败,请重试" |
| | | msg.contains("数据不存在") -> "数据不存在,请检查输入信息" |
| | | msg.contains("网络") -> "网络连接异常,请检查网络后重试" |
| | | msg.contains("超时") -> "请求超时,请重试" |
| | | else -> "操作失败: $msg" |
| | | } |
| | | ToastUtil.show(errorMsg) |
| | | } |
| | | } |
| | | // 重置界面状态 |
| | | resetViewState() |
| | | } |
| | | |
| | | // 网络异常处理方法 |
| | | private fun handleNetworkError(e: Throwable?) { |
| | | val errorMsg = when { |
| | | e?.message?.contains("timeout") == true -> "网络请求超时,请检查网络连接" |
| | | e?.message?.contains("network") == true -> "网络连接失败,请检查网络设置" |
| | | e?.message?.contains("host") == true -> "服务器连接失败,请稍后重试" |
| | | else -> "网络异常: ${e?.message ?: "未知错误"}" |
| | | } |
| | | ToastUtil.show(errorMsg) |
| | | } |
| | | |
| | | ### Dialog弹窗使用最佳实践 |
| | | |
| | | 项目中提供了多种Dialog组件,用于不同的交互场景。推荐使用项目已有的Dialog组件来保持UI风格的一致性。 |
| | | |
| | | #### 常用Dialog组件 |
| | | |
| | | 1. **ConfirmDialog**: 确认对话框,用于重要操作的二次确认 |
| | | 2. **TipDialog**: 提示对话框,用于显示提示信息 |
| | | 3. **EdtDialog**: 输入对话框,用于获取用户输入 |
| | | 4. **自定义Dialog**: 继承Dialog类实现特定功能 |
| | | |
| | | #### ConfirmDialog使用示例 |
| | | |
| | | ```kotlin |
| | | // 基本用法 - 只显示消息 |
| | | val dialog = ConfirmDialog(context, "操作成功") |
| | | dialog.show() |
| | | |
| | | // 带标题的用法 |
| | | val dialog = ConfirmDialog(context, "提示", "确认要删除这条记录吗?") { |
| | | // 点击确认按钮的回调 |
| | | deleteRecord() |
| | | dialog.dismiss() |
| | | } |
| | | dialog.show() |
| | | |
| | | // 在异常处理中使用 |
| | | private fun handleError(title: String, message: String) { |
| | | activity?.let { activity -> |
| | | val confirmDialog = ConfirmDialog(activity, title, message) { |
| | | // 点击确认后的操作 |
| | | resetViewState() |
| | | } |
| | | confirmDialog.show() |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### TipDialog使用示例 |
| | | |
| | | ```kotlin |
| | | // 简单提示 |
| | | val tipDialog = TipDialog(context, "操作完成") |
| | | tipDialog.show() |
| | | |
| | | // 带回调的提示 |
| | | val tipDialog = TipDialog(context, "确认退出应用?", object : TipUtil.TipListener { |
| | | override fun onCancle() { |
| | | // 取消操作 |
| | | } |
| | | }) |
| | | tipDialog.show() |
| | | ``` |
| | | |
| | | #### 自定义Dialog最佳实践 |
| | | |
| | | ```kotlin |
| | | class CustomDialog(context: Context) : Dialog(context, R.style.ws_pay_showSelfDialog) { |
| | | |
| | | private lateinit var binding: DialogCustomBinding |
| | | |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | binding = DialogCustomBinding.inflate(layoutInflater) |
| | | setContentView(binding.root) |
| | | |
| | | // 设置对话框属性 |
| | | setupDialog() |
| | | |
| | | // 初始化视图 |
| | | initViews() |
| | | } |
| | | |
| | | private fun setupDialog() { |
| | | // 设置对话框宽度为屏幕宽度的85% |
| | | window?.apply { |
| | | val params = attributes |
| | | params.width = (context.resources.displayMetrics.widthPixels * 0.85).toInt() |
| | | params.height = ViewGroup.LayoutParams.WRAP_CONTENT |
| | | params.gravity = Gravity.CENTER |
| | | attributes = params |
| | | setBackgroundDrawableResource(android.R.color.transparent) |
| | | } |
| | | |
| | | // 设置点击外部不取消 |
| | | setCanceledOnTouchOutside(false) |
| | | } |
| | | |
| | | private fun initViews() { |
| | | binding.btnConfirm.setOnClickListener { |
| | | // 处理确认逻辑 |
| | | dismiss() |
| | | } |
| | | |
| | | binding.btnCancel.setOnClickListener { |
| | | dismiss() |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### Dialog使用注意事项 |
| | | |
| | | 1. **内存泄漏防护**: 确保在Activity销毁时关闭Dialog |
| | | ```kotlin |
| | | override fun onDestroy() { |
| | | super.onDestroy() |
| | | dialog?.dismiss() |
| | | } |
| | | ``` |
| | | |
| | | 2. **生命周期管理**: 在Fragment中使用Dialog时注意生命周期 |
| | | ```kotlin |
| | | // 在Fragment中安全显示Dialog |
| | | activity?.let { activity -> |
| | | if (!activity.isFinishing && !activity.isDestroyed) { |
| | | dialog.show() |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 3. **样式一致性**: 使用项目统一的Dialog样式 |
| | | ```kotlin |
| | | // 使用项目定义的Dialog样式 |
| | | super(context, R.style.ws_pay_showSelfDialog) |
| | | ``` |
| | | |
| | | 4. **用户体验优化**: |
| | | - 重要操作使用ConfirmDialog进行二次确认 |
| | | - 错误信息使用带标题的Dialog,提供清晰的错误分类 |
| | | - 长时间操作显示加载Dialog,避免用户误操作 |
| | | |
| | | ### 金额格式化最佳实践 |
| | | |
| | | 在处理金额相关的数据时,应该统一使用两位小数格式,确保显示的一致性和准确性。 |
| | | |
| | | #### 金额格式化方法 |
| | | |
| | | ```kotlin |
| | | // 使用String.format格式化金额为两位小数 |
| | | val amount = 123.4 |
| | | val formattedAmount = String.format("%.2f", amount) |
| | | // 结果: "123.40" |
| | | |
| | | // 在显示时添加货币单位 |
| | | binding.balanceText.text = "${formattedAmount} 元" |
| | | ``` |
| | | |
| | | #### 金额输入验证 |
| | | |
| | | ```kotlin |
| | | private fun validateAmount(amountStr: String): Double? { |
| | | if (amountStr.isEmpty()) { |
| | | ToastUtil.show("请输入金额") |
| | | return null |
| | | } |
| | | |
| | | val amount = try { |
| | | amountStr.toDouble() |
| | | } catch (e: NumberFormatException) { |
| | | ToastUtil.show("请输入有效的金额") |
| | | return null |
| | | } |
| | | |
| | | if (amount <= 0) { |
| | | ToastUtil.show("金额必须大于0") |
| | | return null |
| | | } |
| | | |
| | | // 检查小数位数不超过2位 |
| | | val decimalPlaces = amountStr.substringAfter('.', "").length |
| | | if (decimalPlaces > 2) { |
| | | ToastUtil.show("金额最多保留两位小数") |
| | | return null |
| | | } |
| | | |
| | | return amount |
| | | } |
| | | ``` |
| | | |
| | | #### 金额计算和显示示例 |
| | | |
| | | ```kotlin |
| | | private fun calculateAndDisplayAmounts() { |
| | | val rechargeAmount = validateAmount(binding.rechargeAmount.text.toString()) ?: return |
| | | val bonusAmount = validateAmount(binding.bonusAmount.text.toString()) ?: 0.0 |
| | | |
| | | // 计算总金额 |
| | | val totalAmount = rechargeAmount + bonusAmount |
| | | |
| | | // 格式化显示 |
| | | val formattedRecharge = String.format("%.2f", rechargeAmount) |
| | | val formattedBonus = String.format("%.2f", bonusAmount) |
| | | val formattedTotal = String.format("%.2f", totalAmount) |
| | | |
| | | // 更新UI显示 |
| | | binding.rechargeAmountText.text = "${formattedRecharge} 元" |
| | | binding.bonusAmountText.text = "${formattedBonus} 元" |
| | | binding.totalAmountText.text = "${formattedTotal} 元" |
| | | } |
| | | ``` |
| | | |
| | | #### EditText金额输入限制 |
| | | |
| | | 在布局文件中设置金额输入的限制: |
| | | |
| | | ```xml |
| | | <EditText |
| | | android:id="@+id/amountEditText" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:inputType="numberDecimal" |
| | | android:digits="0123456789." |
| | | android:maxLength="10" |
| | | android:hint="请输入金额" /> |
| | | ``` |
| | | |
| | | #### TextWatcher实现小数位限制 |
| | | |
| | | 使用TextWatcher来限制用户只能输入最多两位小数: |
| | | |
| | | ```kotlin |
| | | /** |
| | | * 设置金额输入限制,最多保留两位小数 |
| | | */ |
| | | private fun setupAmountInputLimit(editText: EditText) { |
| | | editText.addTextChangedListener(object : TextWatcher { |
| | | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} |
| | | |
| | | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} |
| | | |
| | | override fun afterTextChanged(s: Editable?) { |
| | | val text = s.toString() |
| | | if (text.isEmpty()) return |
| | | |
| | | // 检查是否包含小数点 |
| | | if (text.contains(".")) { |
| | | val parts = text.split(".") |
| | | if (parts.size == 2) { |
| | | val decimalPart = parts[1] |
| | | // 如果小数位超过2位,截取前两位 |
| | | if (decimalPart.length > 2) { |
| | | val newText = "${parts[0]}.${decimalPart.substring(0, 2)}" |
| | | editText.removeTextChangedListener(this) |
| | | editText.setText(newText) |
| | | editText.setSelection(newText.length) |
| | | editText.addTextChangedListener(this) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 防止输入多个小数点 |
| | | val dotCount = text.count { it == '.' } |
| | | if (dotCount > 1) { |
| | | val newText = text.substring(0, text.lastIndexOf('.')) |
| | | editText.removeTextChangedListener(this) |
| | | editText.setText(newText) |
| | | editText.setSelection(newText.length) |
| | | editText.addTextChangedListener(this) |
| | | } |
| | | |
| | | // 防止以小数点开头 |
| | | if (text.startsWith(".")) { |
| | | editText.removeTextChangedListener(this) |
| | | editText.setText("0$text") |
| | | editText.setSelection(editText.text.length) |
| | | editText.addTextChangedListener(this) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 在initView中调用 |
| | | setupAmountInputLimit(binding.rechargeAmount) |
| | | setupAmountInputLimit(binding.bonusAmount) |
| | | ``` |
| | | |
| | | #### 金额格式化注意事项 |
| | | |
| | | 1. **统一格式**: 所有金额显示都使用两位小数格式 |
| | | 2. **输入验证**: 验证用户输入的金额格式和范围 |
| | | 3. **计算精度**: 使用Double类型进行金额计算,避免精度丢失 |
| | | 4. **显示单位**: 金额显示时统一添加货币单位(如"元") |
| | | 5. **边界检查**: 检查金额的最大值和最小值限制 |
| | | |
| | | ## 充值接口使用说明 |
| | | |
| | | 项目中实现了完整的充值功能,包括充值接口调用和NFC写卡操作。 |
| | | |
| | | ### 充值接口详情 |
| | | |
| | | **接口地址**: `/terminal/card/termRecharge` |
| | | **请求方式**: POST |
| | | **接口描述**: 终端充值接口,用于创建充值订单并返回写卡所需信息 |
| | | |
| | | #### 请求参数 |
| | | |
| | | ```kotlin |
| | | data class RechargeRequest( |
| | | val rechargeType: Int, // 充值类型 (固定值: 2) |
| | | val cardNum: String, // 卡号 |
| | | val money: String, // 充值金额 (格式: "300.0") |
| | | val amount: String, // 充值数量 |
| | | val gift: String, // 赠送金额 |
| | | val paymentId: String, // 支付方式ID |
| | | val price: String, // 单价 (格式: "0.90") |
| | | val remarks: String, // 备注 (默认: "充值") |
| | | val operator: Long // 操作员ID |
| | | ) |
| | | ``` |
| | | |
| | | #### 请求示例 |
| | | |
| | | ```json |
| | | { |
| | | "rechargeType": 2, |
| | | "cardNum": "53232810100600001", |
| | | "money": "300.0", |
| | | "amount": "50", |
| | | "gift": "5", |
| | | "paymentId": "1838466162264350722", |
| | | "price": "0.90", |
| | | "remarks": "充值", |
| | | "operator": 2024090516595200300 |
| | | } |
| | | ``` |
| | | |
| | | #### 返回参数 |
| | | |
| | | ```kotlin |
| | | data class RechargeResult( |
| | | val projectNo: Int, // 项目编号 |
| | | val cardNum: String, // 卡号 |
| | | val orderNo: String, // 订单号 |
| | | val waterPrice: Double, // 水价 |
| | | val time: String // 时间 |
| | | ) |
| | | ``` |
| | | |
| | | #### 返回示例 |
| | | |
| | | ```json |
| | | { |
| | | "code": "0001", |
| | | "content": { |
| | | "projectNo": 10, |
| | | "cardNum": "53232810100600001", |
| | | "orderNo": "2506041414250065", |
| | | "waterPrice": 0.9, |
| | | "time": "2025-05-08 17:31:02" |
| | | }, |
| | | "msg": "请求成功", |
| | | "success": true |
| | | } |
| | | ``` |
| | | |
| | | ### 充值功能实现 |
| | | |
| | | #### 1. 在Activity中调用充值接口 |
| | | |
| | | ```kotlin |
| | | /** |
| | | * 调用充值接口 |
| | | */ |
| | | private fun callRechargeApi(rechargeAmount: Double, bonusAmount: Double) { |
| | | val cardNum = cardInfo?.cardNum ?: cardAddress ?: "" |
| | | |
| | | // 构建充值请求参数 |
| | | val params = mapOf( |
| | | "rechargeType" to 2, |
| | | "cardNum" to cardNum, |
| | | "money" to String.format("%.1f", rechargeAmount), |
| | | "amount" to String.format("%.0f", bonusAmount), |
| | | "gift" to String.format("%.0f", bonusAmount), |
| | | "paymentId" to paymentId.toString(), |
| | | "price" to "0.90", |
| | | "remarks" to "充值", |
| | | "operator" to 2024090516595200300L |
| | | ) |
| | | |
| | | ApiManager.getInstance().requestPostLoading( |
| | | this, |
| | | "terminal/card/termRecharge", |
| | | RechargeResult::class.java, |
| | | params, |
| | | object : SubscriberListener<BaseResponse<RechargeResult>>() { |
| | | override fun onNext(response: BaseResponse<RechargeResult>) { |
| | | if (response.success && response.code == "0001") { |
| | | // 充值成功,跳转到写卡界面 |
| | | response.content?.let { rechargeResult -> |
| | | startWriteCardActivity(rechargeResult, rechargeAmount, bonusAmount) |
| | | } |
| | | } else { |
| | | ToastUtil.show("充值失败: ${response.msg ?: "未知错误"}") |
| | | } |
| | | } |
| | | |
| | | override fun onError(e: Throwable?) { |
| | | super.onError(e) |
| | | ToastUtil.show("充值失败: ${e?.message ?: "网络异常"}") |
| | | } |
| | | } |
| | | ) |
| | | } |
| | | ``` |
| | | |
| | | #### 2. 跳转到写卡界面 |
| | | |
| | | ```kotlin |
| | | /** |
| | | * 启动写卡界面 |
| | | */ |
| | | private fun startWriteCardActivity(rechargeResult: RechargeResult, rechargeAmount: Double, bonusAmount: Double) { |
| | | // 创建UserCard对象用于写卡 |
| | | val userCard = UserCard().apply { |
| | | cardInfo?.let { info -> |
| | | userCode = info.cardNum ?: "" |
| | | balance = ((rechargeAmount + bonusAmount) * 100).toInt() // 转换为分 |
| | | } |
| | | |
| | | // 设置其他必要信息 |
| | | projectCode = rechargeResult.projectNo |
| | | waterPrice = rechargeResult.waterPrice.toFloat() |
| | | rechargeDate = java.util.Calendar.getInstance() |
| | | } |
| | | |
| | | // 启动写卡Activity |
| | | val intent = Intent(this, NfcWreatActivity::class.java).apply { |
| | | putExtra("cardType", "USER_CARD") |
| | | putExtra("cardAddr", cardAddress) |
| | | putExtra("operationTypeCode", CardOperationType.Recharge.code) |
| | | putExtra("orderNumber", rechargeResult.orderNo) |
| | | putExtra("userCard", userCard) |
| | | } |
| | | |
| | | startActivity(intent) |
| | | } |
| | | ``` |
| | | |
| | | #### 3. NFC写卡处理 |
| | | |
| | | 在`NfcWreatActivity`中,充值操作会被自动处理: |
| | | |
| | | ```kotlin |
| | | CardOperationType.Recharge -> { |
| | | nfcWreatHelper.writeUserDataAsync(userCard, object : NFCCallBack { |
| | | override fun isSusses(flag: Boolean, msg: String?) { |
| | | runOnUiThread { |
| | | if (flag) { |
| | | postCardData(cardType, cardAddr) |
| | | ToastUtil.show("充值写卡成功!") |
| | | } else { |
| | | ToastUtil.show("充值写卡失败: ${msg ?: "未知错误"}") |
| | | } |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | ``` |
| | | |
| | | ### 充值流程说明 |
| | | |
| | | 1. **用户输入**: 用户在充值界面输入充值金额和赠送金额 |
| | | 2. **参数验证**: 验证输入的金额格式和有效性 |
| | | 3. **接口调用**: 调用`/terminal/card/termRecharge`接口创建充值订单 |
| | | 4. **订单创建**: 服务器创建充值订单并返回订单信息 |
| | | 5. **跳转写卡**: 成功后跳转到NFC写卡界面 |
| | | 6. **NFC写卡**: 用户贴卡进行NFC写卡操作 |
| | | 7. **写卡完成**: 写卡成功后更新卡片余额 |
| | | |
| | | ### 注意事项 |
| | | |
| | | 1. **金额格式**: 充值金额使用一位小数格式,如"300.0" |
| | | 2. **操作员ID**: 需要根据实际登录用户设置正确的操作员ID |
| | | 3. **支付方式**: 支付方式ID需要从支付方式接口获取 |
| | | 4. **错误处理**: 充值失败时要提供明确的错误信息 |
| | | 5. **写卡验证**: 写卡前要验证卡号一致性 |
| | | 6. **订单跟踪**: 保存订单号用于后续查询和跟踪 |
| | | |
| | | ### 相关数据类 |
| | | |
| | | ```kotlin |
| | | // 充值请求数据类 |
| | | data class RechargeRequest( |
| | | val rechargeType: Int, |
| | | val cardNum: String, |
| | | val money: String, |
| | | val amount: String, |
| | | val gift: String, |
| | | val paymentId: String, |
| | | val price: String, |
| | | val remarks: String, |
| | | val operator: Long |
| | | ) |
| | | |
| | | // 充值结果数据类 |
| | | data class RechargeResult( |
| | | val projectNo: Int, |
| | | val cardNum: String, |
| | | val orderNo: String, |
| | | val waterPrice: Double, |
| | | val time: String |
| | | ) |
| | | ``` |
| | | |
| | | ## 水价接口使用说明 |
| | | |
| | | 项目中实现了水价的动态获取和管理功能,支持从服务器获取最新水价并在全局范围内使用。 |
| | | |
| | | ### 水价接口详情 |
| | | |
| | | **接口地址**: `/terminal/client/getWaterPrice` |
| | | **请求方式**: GET |
| | | **接口描述**: 获取当前系统水价信息 |
| | | |
| | | #### 返回参数 |
| | | |
| | | ```kotlin |
| | | data class WaterPriceResult( |
| | | val price: Double // 水价 |
| | | ) |
| | | ``` |
| | | |
| | | #### 返回示例 |
| | | |
| | | ```json |
| | | { |
| | | "code": "0001", |
| | | "content": { |
| | | "price": 0.9 |
| | | }, |
| | | "msg": "请求成功", |
| | | "success": true |
| | | } |
| | | ``` |
| | | |
| | | ### 水价功能实现 |
| | | |
| | | #### 1. BaseApplication中的水价存储 |
| | | |
| | | ```kotlin |
| | | class BaseApplication { |
| | | companion object { |
| | | // 水价信息 |
| | | var waterPrice: Double = 0.0 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### 2. MainActivity中延时获取水价 |
| | | |
| | | ```kotlin |
| | | class MainActivity : BaseNfcActivity() { |
| | | private val handler = Handler(Looper.getMainLooper()) |
| | | |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | // ... 其他初始化代码 ... |
| | | |
| | | // 延时20秒后获取水价,避免影响启动性能 |
| | | handler.postDelayed({ |
| | | getWaterPrice() |
| | | }, 20000) // 20秒延时 |
| | | } |
| | | |
| | | /** |
| | | * 获取水价信息 |
| | | */ |
| | | private fun getWaterPrice() { |
| | | ApiManager.getInstance().requestGetLoading( |
| | | this, |
| | | "terminal/client/getWaterPrice", |
| | | WaterPriceResult::class.java, |
| | | null, |
| | | object : SubscriberListener<BaseResponse<WaterPriceResult>>() { |
| | | override fun onNext(response: BaseResponse<WaterPriceResult>) { |
| | | if (response.success && response.code == "0001") { |
| | | // 获取水价成功,保存到BaseApplication |
| | | response.content?.let { waterPriceResult -> |
| | | BaseApplication.waterPrice = waterPriceResult.price |
| | | } |
| | | } |
| | | } |
| | | |
| | | override fun onError(e: Throwable?) { |
| | | super.onError(e) |
| | | // 网络异常时不显示错误信息,避免影响用户体验 |
| | | } |
| | | } |
| | | ) |
| | | } |
| | | |
| | | override fun onDestroy() { |
| | | super.onDestroy() |
| | | // 清理Handler回调,防止内存泄漏 |
| | | handler.removeCallbacksAndMessages(null) |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### 3. RechargeDetailActivity中的水价检查和使用 |
| | | |
| | | ```kotlin |
| | | class RechargeDetailActivity : AppCompatActivity() { |
| | | |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | // ... 其他初始化代码 ... |
| | | |
| | | // 检查并获取水价 |
| | | checkAndGetWaterPrice() |
| | | } |
| | | |
| | | /** |
| | | * 检查并获取水价 |
| | | */ |
| | | private fun checkAndGetWaterPrice() { |
| | | // 如果BaseApplication中的水价为空或为0,则重新获取 |
| | | if (BaseApplication.waterPrice <= 0.0) { |
| | | getWaterPrice() |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取水价信息 |
| | | */ |
| | | private fun getWaterPrice() { |
| | | ApiManager.getInstance().requestGetLoading( |
| | | this, |
| | | "terminal/client/getWaterPrice", |
| | | WaterPriceResult::class.java, |
| | | null, |
| | | object : SubscriberListener<BaseResponse<WaterPriceResult>>() { |
| | | override fun onNext(response: BaseResponse<WaterPriceResult>) { |
| | | if (response.success && response.code == "0001") { |
| | | // 获取水价成功,保存到BaseApplication |
| | | response.content?.let { waterPriceResult -> |
| | | BaseApplication.waterPrice = waterPriceResult.price |
| | | } |
| | | } else { |
| | | // 获取水价失败,使用默认值 |
| | | if (BaseApplication.waterPrice <= 0.0) { |
| | | BaseApplication.waterPrice = 0.9 // 设置默认水价 |
| | | } |
| | | } |
| | | } |
| | | |
| | | override fun onError(e: Throwable?) { |
| | | super.onError(e) |
| | | // 网络异常,使用默认值 |
| | | if (BaseApplication.waterPrice <= 0.0) { |
| | | BaseApplication.waterPrice = 0.9 // 设置默认水价 |
| | | } |
| | | } |
| | | } |
| | | ) |
| | | } |
| | | |
| | | /** |
| | | * 在充值接口中使用水价 |
| | | */ |
| | | private fun callRechargeApi(rechargeAmount: Double, bonusAmount: Double) { |
| | | // 获取当前水价,如果为0则使用默认值 |
| | | val currentWaterPrice = if (BaseApplication.waterPrice > 0.0) { |
| | | BaseApplication.waterPrice |
| | | } else { |
| | | 0.9 // 默认水价 |
| | | } |
| | | |
| | | // 构建充值请求参数 |
| | | val params = mapOf( |
| | | // ... 其他参数 ... |
| | | "price" to String.format("%.2f", currentWaterPrice), // 使用获取到的水价 |
| | | // ... 其他参数 ... |
| | | ) |
| | | |
| | | // ... 接口调用代码 ... |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ### 水价管理流程 |
| | | |
| | | 1. **应用启动**: MainActivity启动后延时20秒获取水价,避免影响启动性能 |
| | | 2. **全局存储**: 获取到的水价存储在BaseApplication.waterPrice中 |
| | | 3. **按需获取**: 在需要使用水价的界面(如充值界面)检查水价是否为空 |
| | | 4. **重新获取**: 如果水价为空或为0,则重新调用接口获取 |
| | | 5. **默认处理**: 如果获取失败,使用默认水价0.9元 |
| | | 6. **实时使用**: 在充值等业务中使用最新的水价信息 |
| | | |
| | | ### 技术特点 |
| | | |
| | | 1. **性能优化**: 延时获取避免影响应用启动速度 |
| | | 2. **全局共享**: 水价信息在BaseApplication中全局共享 |
| | | 3. **按需刷新**: 在需要时检查并重新获取水价 |
| | | 4. **容错处理**: 获取失败时使用默认水价,确保业务正常进行 |
| | | 5. **内存管理**: 正确处理Handler回调,防止内存泄漏 |
| | | |
| | | ### 使用注意事项 |
| | | |
| | | 1. **延时设置**: MainActivity中的20秒延时可根据实际需求调整 |
| | | 2. **默认水价**: 默认水价0.9元可根据实际业务需求修改 |
| | | 3. **错误处理**: 水价获取失败时不显示错误信息,避免影响用户体验 |
| | | 4. **数据格式**: 水价使用Double类型存储,使用时格式化为两位小数 |
| | | 5. **生命周期**: 注意在Activity销毁时清理相关资源 |
| | | |
| | | ### 相关数据类 |
| | | |
| | | ```kotlin |
| | | // 水价结果数据类 |
| | | data class WaterPriceResult( |
| | | val price: Double // 水价 |
| | | ) |
| | | ``` |
| | | |
| | | ## 贡献指南 |
| | | |
| | | 1. Fork 项目 |