Browse Source

refactor(更新)
- 扫描修复
- 首页导航栏修改
- 还钥匙修改状态逻辑修改

周文健 4 months ago
parent
commit
382412ff22
81 changed files with 1374 additions and 791 deletions
  1. 8 3
      app/src/main/assets/logback.xml
  2. 8 1
      app/src/main/java/com/grkj/iscs/ISCSApplication.kt
  3. 5 4
      app/src/main/java/com/grkj/iscs/features/init/activity/InitActivity.kt
  4. 4 3
      app/src/main/java/com/grkj/iscs/features/init/fragment/InitDeviceRegistrationKeyAndLockFragment.kt
  5. 0 1
      app/src/main/java/com/grkj/iscs/features/init/fragment/InitWelcomeFragment.kt
  6. 17 17
      app/src/main/java/com/grkj/iscs/features/init/viewmodel/InitDeviceRegistrationKeyAndLockViewModel.kt
  7. 3 4
      app/src/main/java/com/grkj/iscs/features/login/activity/LoginActivity.kt
  8. 12 35
      app/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt
  9. 0 1
      app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/AddCardDialog.kt
  10. 0 2
      app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterCardDialog.kt
  11. 0 3
      app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterKeyDialog.kt
  12. 0 2
      app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterLockDialog.kt
  13. 0 2
      app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterRfidTokenDialog.kt
  14. 0 1
      app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/UpdateKeyDialog.kt
  15. 0 1
      app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/UpdateLockDialog.kt
  16. 42 21
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateJobFragment.kt
  17. 31 19
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopFragment.kt
  18. 39 30
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopJobFragment.kt
  19. 71 62
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditJobFragment.kt
  20. 40 25
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopFragment.kt
  21. 53 29
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopJobFragment.kt
  22. 2 21
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/InProgressJobManageFragment.kt
  23. 92 33
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt
  24. 0 1
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/MainViewModel.kt
  25. 19 13
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/job_manage/JobExecuteViewModel.kt
  26. 6 0
      app/src/main/res/drawable/bg_main_nav_bar.xml
  27. 12 12
      app/src/main/res/layout-land/activity_main.xml
  28. 11 11
      app/src/main/res/layout/activity_main.xml
  29. 1 7
      app/src/main/res/layout/fragment_in_progress_job_manage.xml
  30. 7 5
      app/src/main/res/layout/item_device_registration_key.xml
  31. 4 3
      app/src/main/res/layout/item_device_registration_lock.xml
  32. 2 1
      app/src/main/res/layout/item_job_manage.xml
  33. BIN
      app/src/main/res/mipmap-xhdpi/icon_bottom_menu_data_manage.png
  34. BIN
      app/src/main/res/mipmap-xhdpi/icon_bottom_menu_exception_manage.png
  35. BIN
      app/src/main/res/mipmap-xhdpi/icon_bottom_menu_hardware_manage.png
  36. BIN
      app/src/main/res/mipmap-xhdpi/icon_bottom_menu_home.png
  37. BIN
      app/src/main/res/mipmap-xhdpi/icon_bottom_menu_job_manage.png
  38. 7 0
      app/src/main/res/values-en/strings.xml
  39. 5 1
      app/src/main/res/values-land/dimens.xml
  40. 3 0
      app/src/main/res/values-night/dimens.xml
  41. 7 0
      app/src/main/res/values-zh/strings.xml
  42. 5 1
      app/src/main/res/values/dimens.xml
  43. 7 0
      app/src/main/res/values/strings.xml
  44. 6 0
      data/src/main/java/com/grkj/data/dao/HardwareDao.kt
  45. 1 1
      data/src/main/java/com/grkj/data/dao/JobTicketDao.kt
  46. 3 1
      data/src/main/java/com/grkj/data/enums/LockStepEnum.kt
  47. 6 1
      data/src/main/java/com/grkj/data/repository/IJobTicketRepository.kt
  48. 1 1
      data/src/main/java/com/grkj/data/repository/impl/HardwareRepository.kt
  49. 9 1
      data/src/main/java/com/grkj/data/repository/impl/JobTicketRepository.kt
  50. 124 0
      shared/src/main/java/com/grkj/shared/utils/CRC16.kt
  51. 8 3
      shared/src/main/java/com/grkj/shared/utils/extension/ByteArray.kt
  52. 1 1
      shared/src/main/java/com/grkj/shared/utils/extension/Int.kt
  53. 1 1
      shared/src/main/java/com/grkj/shared/utils/extension/Long.kt
  54. 1 1
      shared/src/main/java/com/grkj/shared/utils/extension/String.kt
  55. 1 0
      ui-base/build.gradle.kts
  56. 7 8
      ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt
  57. 121 0
      ui-base/src/main/java/com/grkj/ui_base/base/BaseFormFragment.kt
  58. 55 19
      ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt
  59. 19 8
      ui-base/src/main/java/com/grkj/ui_base/business/ModbusBusinessManager.kt
  60. 10 0
      ui-base/src/main/java/com/grkj/ui_base/config/ISCSConfig.kt
  61. 0 122
      ui-base/src/main/java/com/grkj/ui_base/utils/CRC16.java
  62. 3 3
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleCmdManager.kt
  63. 57 165
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt
  64. 1 2
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleUtil.kt
  65. 1 2
      ui-base/src/main/java/com/grkj/ui_base/utils/extension/Context.kt
  66. 0 1
      ui-base/src/main/java/com/grkj/ui_base/utils/extension/DialogXExtension.kt
  67. 5 6
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/DockBean.kt
  68. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/FrameTask.kt
  69. 1 1
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/MBFrame.kt
  70. 93 61
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusController.kt
  71. 3 3
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusManager.kt
  72. 3 3
      ui-base/src/main/java/com/grkj/ui_base/utils/modbus/PortManager.kt
  73. 258 0
      ui-base/src/main/java/com/grkj/ui_base/widget/CustomNavBar.kt
  74. 29 0
      ui-base/src/main/res/layout/item_nav.xml
  75. 1 0
      ui-base/src/main/res/values-en/strings.xml
  76. 1 0
      ui-base/src/main/res/values-land/dimens.xml
  77. 1 0
      ui-base/src/main/res/values-zh/strings.xml
  78. 16 0
      ui-base/src/main/res/values/attrs.xml
  79. 1 0
      ui-base/src/main/res/values/colors.xml
  80. 1 0
      ui-base/src/main/res/values/dimens.xml
  81. 1 0
      ui-base/src/main/res/values/strings.xml

+ 8 - 3
app/src/main/assets/logback.xml

@@ -2,15 +2,20 @@
     xmlns="https://tony19.github.io/logback-android/xml"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd">
-
+    <property name="LOG_DIR" value="${EXTERNAL_STORAGE}/iscs/logs"/>
+    <contextListener class="ch.qos.logback.classic.joran.JoranConfigurator">
+        <onStart>
+            <mkdir dir="${LOG_DIR}" />
+        </onStart>
+    </contextListener>
     <!-- 文件日志输出,生成每日滚动日志 -->
     <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
         <!-- 日志文件路径,在 Android 中存储在应用的 filesDir/logs 目录 -->
-        <file>${DATA_DIR}/logs/app.log</file>
+        <file>${LOG_DIR}/app.log</file>
 
         <!-- 每日滚动日志策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <fileNamePattern>${DATA_DIR}/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <fileNamePattern>${LOG_DIR}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
             <maxHistory>7</maxHistory>  <!-- 最大保留 7 天日志 -->
             <totalSizeCap>100MB</totalSizeCap>  <!-- 总日志大小限制 -->
         </rollingPolicy>

+ 8 - 1
app/src/main/java/com/grkj/iscs/ISCSApplication.kt

@@ -2,6 +2,7 @@ package com.grkj.iscs
 
 import android.app.Application
 import android.content.Context
+import ch.qos.logback.classic.Level
 import com.grkj.data.data.EventConstants
 import com.grkj.data.di.RepositoryManager
 import com.grkj.shared.model.EventBean
@@ -19,6 +20,7 @@ import com.scwang.smart.refresh.layout.api.RefreshLayout
 import com.scwang.smart.refresh.layout.listener.DefaultRefreshFooterCreator
 import com.scwang.smart.refresh.layout.listener.DefaultRefreshHeaderCreator
 import com.sik.sikcore.SIKCore
+import com.sik.sikcore.log.LogUtils
 import com.sik.sikcore.thread.ThreadUtils
 import dagger.hilt.android.HiltAndroidApp
 import kotlinx.coroutines.delay
@@ -49,7 +51,12 @@ class ISCSApplication : Application() {
         if (!EventBus.getDefault().isRegistered(this)) {
             EventBus.getDefault().register(this)
         }
-        BleUtil.instance?.initBle(this)
+        if (ISCSConfig.DEBUG) {
+            LogUtils.setGlobalLogLevel(Level.DEBUG)
+        }
+        if (ISCSConfig.isInit){
+            BleUtil.instance?.initBle(this)
+        }
         AutoSizeConfig.getInstance().isCustomFragment = true
         ThreadUtils.runOnIO {
             ModbusBusinessManager.registerMainListener()

+ 5 - 4
app/src/main/java/com/grkj/iscs/features/init/activity/InitActivity.kt

@@ -6,11 +6,11 @@ import com.grkj.iscs.R
 import com.grkj.iscs.databinding.ActivityInitBinding
 import com.grkj.shared.config.Constants
 import com.grkj.ui_base.base.BaseActivity
-import com.grkj.ui_base.config.ISCSConfig
+import com.grkj.ui_base.utils.ble.BleUtil
 import com.grkj.ui_base.utils.event.CardSwipeEvent
-import com.grkj.ui_base.utils.extension.toByteArrays
-import com.grkj.ui_base.utils.extension.toHexStrings
-import com.sik.sikandroid.permission.PermissionUtils
+import com.grkj.shared.utils.extension.toByteArrays
+import com.grkj.shared.utils.extension.toHexStrings
+import com.sik.sikcore.SIKCore
 import dagger.hilt.android.AndroidEntryPoint
 
 /**
@@ -30,6 +30,7 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
     override fun initView() {
         requestPermissionsIfNeeded(*Constants.needPermission) {
             if (it) {
+                BleUtil.instance?.initBle(SIKCore.getApplication())
                 logger.info("权限获取成功")
             } else {
                 logger.info("权限获取失败")

+ 4 - 3
app/src/main/java/com/grkj/iscs/features/init/fragment/InitDeviceRegistrationKeyAndLockFragment.kt

@@ -21,6 +21,7 @@ import com.grkj.iscs.features.init.model.DockData.KeyDock
 import com.grkj.iscs.features.init.viewmodel.InitDeviceRegistrationKeyAndLockViewModel
 import com.grkj.shared.model.EventBean
 import com.grkj.ui_base.base.BaseFragment
+import com.grkj.ui_base.config.ISCSConfig
 import com.grkj.ui_base.utils.modbus.DeviceConst
 import com.grkj.ui_base.utils.modbus.DockBean
 import com.grkj.ui_base.utils.modbus.ModBusController
@@ -45,7 +46,7 @@ class InitDeviceRegistrationKeyAndLockFragment :
             logger.debug("设备录入-初始化检测任务分发完成")
         }
         binding.reRecognize.setDebouncedClickListener {
-            viewModel.isStartCheckKey = false
+            ISCSConfig.canInitDevice = true
         }
         binding.previousBtn.setDebouncedClickListener {
             navController.popBackStack()
@@ -178,9 +179,9 @@ class InitDeviceRegistrationKeyAndLockFragment :
     override fun onEvent(event: EventBean<Any>) {
         super.onEvent(event)
         when (event.code) {
-            EventConstants.EVENT_START_MODBUS -> {
+            EventConstants.EVENT_START_MODBUS_COMPLETE -> {
                 ThreadUtils.runOnMainDelayed(1000) {
-                    viewModel.isStartCheckKey = false
+                    ISCSConfig.canInitDevice = true
                 }
             }
         }

+ 0 - 1
app/src/main/java/com/grkj/iscs/features/init/fragment/InitWelcomeFragment.kt

@@ -7,7 +7,6 @@ import com.grkj.iscs.features.init.viewmodel.InitDeviceRegistrationKeyAndLockVie
 import com.grkj.ui_base.base.BaseFragment
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.extension.setDebouncedClickListener
-import com.sik.sikcore.thread.ThreadUtils
 import dagger.hilt.android.AndroidEntryPoint
 
 /**

+ 17 - 17
app/src/main/java/com/grkj/iscs/features/init/viewmodel/InitDeviceRegistrationKeyAndLockViewModel.kt

@@ -8,6 +8,7 @@ import com.grkj.data.data.MMKVConstants
 import com.grkj.data.repository.IHardwareRepository
 import com.grkj.ui_base.base.BaseViewModel
 import com.grkj.ui_base.business.ModbusBusinessManager
+import com.grkj.ui_base.config.ISCSConfig
 import com.grkj.ui_base.utils.ble.BleConnectionManager
 import com.grkj.ui_base.utils.modbus.DeviceConst
 import com.grkj.ui_base.utils.modbus.DockBean
@@ -26,7 +27,6 @@ import kotlin.coroutines.resume
 class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(val hardwareRepository: IHardwareRepository) :
     BaseViewModel() {
     val isLoadComplete: MutableLiveData<Boolean> = MutableLiveData(false)
-    var isStartCheckKey: Boolean = false
     var isDestroy: Boolean = false
     var newHardwareKeySize: Int = 0
     var newHardwareLockSize: Int = 0
@@ -38,13 +38,13 @@ class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(val hardware
             device.mac = null
             hardwareRepository.getKeyInfo(device.rfid.toString()) {
                 device.newHardware =
-                    it?.keyNfc?.isEmpty() == true || it?.macAddress?.isEmpty() == true
+                    it == null || it.keyNfc?.isEmpty() == true || it.macAddress?.isEmpty() == true
                 device.mac = it?.macAddress
                 callback()
             }
         } else if (device is DockBean.LockBean) {
             hardwareRepository.getLockInfo(device.rfid.toString()) {
-                device.newHardware = it?.lockNfc?.isEmpty() == true
+                device.newHardware = it == null || it.lockNfc?.isEmpty() == true
                 callback()
             }
         }
@@ -55,18 +55,17 @@ class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(val hardware
      */
     fun registerInitListener(): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
-            isStartCheckKey = false
             newHardwareKeyBean.clear()
             alreadyUsedMac.clear()
             isLoadComplete.postValue(false)
             ModbusBusinessManager.registerInitListener {
-                if (isStartCheckKey) {
+                if (!ISCSConfig.canInitDevice) {
                     return@registerInitListener
                 }
-                isStartCheckKey = true
+                ISCSConfig.canInitDevice = false
                 val dockList = ModBusController.dockList
                 if (dockList.size < DockBean.dockConfig.size) {
-                    isStartCheckKey = false
+                    ISCSConfig.canInitDevice = true
                     return@registerInitListener
                 }
                 ThreadUtils.runOnIO {
@@ -106,9 +105,6 @@ class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(val hardware
                     .filter { it.mac?.isNotEmpty() == true }.mapNotNull { it.mac }
             )
             if (newHardwareKeyBean.isNotEmpty()) {
-                val allDeviceCloseCmdSend =
-                    BleConnectionManager.scanOnlineKeyLockMacAndSwitchModeToClose()
-                logger.info("设备录入-是否所有关闭命令发送成功:${allDeviceCloseCmdSend}")
                 BleManager.getInstance().disconnectAllDevice()
                 logger.info("断开所有蓝牙设备")
             }
@@ -133,7 +129,11 @@ class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(val hardware
     /**
      * 打开充电并扫描蓝牙
      */
-    suspend fun openChargeAndScanMac(addr: Byte, keyBean: DockBean.KeyBean,retryCount: Int = 3): Boolean {
+    suspend fun openChargeAndScanMac(
+        addr: Byte,
+        keyBean: DockBean.KeyBean,
+        retryCount: Int = 3
+    ): Boolean {
         return suspendCancellableCoroutine<Boolean> { cont ->
             logger.info("设备录入-关闭充电:${addr},${keyBean.idx}")
             ModBusController.controlKeyCharge(false, keyBean.idx, addr) {
@@ -162,9 +162,9 @@ class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(val hardware
                                         "设备录入-设备空:${keyBean.rfid}"
                                     )
                                     ThreadUtils.runOnIO {
-                                        if (retryCount>0){
-                                            openChargeAndScanMac(addr, keyBean,retryCount-1)
-                                        }else{
+                                        if (retryCount > 0) {
+                                            openChargeAndScanMac(addr, keyBean, retryCount - 1)
+                                        } else {
                                             cont.resume(false)
                                         }
                                     }
@@ -174,9 +174,9 @@ class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(val hardware
                                             "设备录入-设备mac空:${keyBean.rfid}"
                                         )
                                         ThreadUtils.runOnIO {
-                                            if (retryCount>0){
-                                                openChargeAndScanMac(addr, keyBean,retryCount-1)
-                                            }else{
+                                            if (retryCount > 0) {
+                                                openChargeAndScanMac(addr, keyBean, retryCount - 1)
+                                            } else {
                                                 cont.resume(false)
                                             }
                                         }

+ 3 - 4
app/src/main/java/com/grkj/iscs/features/login/activity/LoginActivity.kt

@@ -26,10 +26,9 @@ import com.grkj.shared.config.Constants
 import com.grkj.ui_base.base.BaseActivity
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.event.LoadingEvent
-import com.grkj.ui_base.utils.event.StartModbusEvent
 import com.grkj.ui_base.utils.extension.getAppVersionName
-import com.grkj.ui_base.utils.extension.toByteArrays
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.toByteArrays
+import com.grkj.shared.utils.extension.toHexStrings
 import com.grkj.ui_base.utils.fingerprint.FingerprintUtil
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikimage.ImageConvertUtils
@@ -94,7 +93,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
     override fun initData() {
         super.initData()
         //todo 测试用,直接创建管理员账号
-        viewModel.insertAdminAccount().observe(this){}
+//        viewModel.insertAdminAccount().observe(this){}
         requestPermissionsIfNeeded(*Constants.needPermission) {
             if (it) {
                 logger.info("权限获取成功")

+ 12 - 35
app/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt

@@ -20,8 +20,8 @@ import com.grkj.iscs.features.main.viewmodel.MainViewModel
 import com.grkj.shared.model.EventBean
 import com.grkj.ui_base.base.BaseActivity
 import com.grkj.ui_base.utils.event.BottomNavVisibilityEvent
-import com.grkj.ui_base.utils.extension.toByteArrays
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.toByteArrays
+import com.grkj.shared.utils.extension.toHexStrings
 import dagger.hilt.android.AndroidEntryPoint
 
 /**
@@ -77,53 +77,31 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
     override fun initView() {
         binding.nickname.text = MainDomainData.userInfo?.nickName ?: ""
         // 1. 拆出两个可选控件
-        val bottomNav = binding.bottomNav
-        val navRail = binding.navRail
         // 2. 清空原有菜单
-        bottomNav?.let {
-            it.isItemActiveIndicatorEnabled = false
-            it.menu.clear()
-        }
-        navRail?.let {
-            it.isItemActiveIndicatorEnabled = false
+        binding.navBar.let {
             it.menu.clear()
         }
 
-        bottomNav?.let {
+        binding.navBar.let {
             tabConfigs.forEachIndexed { index, cfg ->
                 if (MainDomainData.permissions.contains(cfg.permission)) {
-                    bottomNav.menu.add(Menu.NONE, cfg.id, index, cfg.title).setIcon(cfg.icon)
+                    binding.navBar.menu.add(Menu.NONE, cfg.id, index, cfg.title)
+                        .setIcon(cfg.icon)
                 }
             }
             // 构造 map: menuItemId -> navGraphId
-            val graphMap = tabConfigs.filter { bottomNav.menu.findItem(it.id) != null }
-                .associate { it.id to it.graphRes }
-            setupBottomNavigation(it, graphMap)
-            // 默认选中第一个
-            if (bottomNav.menu.isNotEmpty()) {
-                val firstId = bottomNav.menu[0].itemId
-                bottomNav.selectedItemId = firstId
-            }
-        }
-        navRail?.let {
-            tabConfigs.forEachIndexed { index, cfg ->
-                if (MainDomainData.permissions.contains(cfg.permission)) {
-                    navRail.menu.add(Menu.NONE, cfg.id, index, cfg.title).setIcon(cfg.icon)
-                }
-            }
-            val graphMap = tabConfigs.filter { navRail.menu.findItem(it.id) != null }
+            val graphMap = tabConfigs.filter { binding.navBar.menu.findItem(it.id) != null }
                 .associate { it.id to it.graphRes }
             setupBottomNavigation(it, graphMap)
             // 默认选中第一个
-            if (navRail.menu.isNotEmpty()) {
-                val firstId = navRail.menu[0].itemId
-                navRail.selectedItemId = firstId
+            if (binding.navBar.menu.isNotEmpty()) {
+                val firstId = binding.navBar.menu[0].itemId
+                binding.navBar.selectedItemId = firstId
             }
         }
         binding.nickname.setOnClickListener {
             if (MainDomainData.permissions.contains(RoleFunctionalPermissionsEnum.USER_INFO_HOME.functionalPermission)) {
-                bottomNav?.isVisible = true
-                navRail?.isVisible = true
+                binding.navBar.isVisible = true
                 replaceNavGraph(R.navigation.nav_user_info)
             }
         }
@@ -133,8 +111,7 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
         super.onEvent(event)
         when (event.code) {
             EventConstants.EVENT_BOTTOM_NAV_VISIBILITY -> {
-                binding.bottomNav?.isVisible = (event.data as BottomNavVisibilityEvent).show
-                binding.navRail?.isVisible = (event.data as BottomNavVisibilityEvent).show
+                binding.navBar.isVisible = (event.data as BottomNavVisibilityEvent).show
             }
 
             EventConstants.EVENT_LOGOUT -> {

+ 0 - 1
app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/AddCardDialog.kt

@@ -1,7 +1,6 @@
 package com.grkj.iscs.features.main.dialog.hardware_manage
 
 import android.view.View
-import androidx.core.view.isVisible
 import com.grkj.data.model.vo.AddCardDataVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogAddCardBinding

+ 0 - 2
app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterCardDialog.kt

@@ -7,9 +7,7 @@ import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogFilterCardBinding
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.extension.tipDialog
 import com.kongzue.dialogx.dialogs.CustomDialog
-import com.kongzue.dialogx.dialogs.PopTip
 import com.kongzue.dialogx.interfaces.OnBindView
 
 /**

+ 0 - 3
app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterKeyDialog.kt

@@ -1,14 +1,11 @@
 package com.grkj.iscs.features.main.dialog.hardware_manage
 
 import android.view.View
-import androidx.core.view.isVisible
 import com.grkj.data.model.vo.KeyManageFilterVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogFilterKeyBinding
 import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.extension.tipDialog
 import com.kongzue.dialogx.dialogs.CustomDialog
-import com.kongzue.dialogx.dialogs.PopTip
 import com.kongzue.dialogx.interfaces.OnBindView
 
 /**

+ 0 - 2
app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterLockDialog.kt

@@ -5,9 +5,7 @@ import com.grkj.data.model.vo.LockManageFilterVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogFilterLockBinding
 import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.extension.tipDialog
 import com.kongzue.dialogx.dialogs.CustomDialog
-import com.kongzue.dialogx.dialogs.PopTip
 import com.kongzue.dialogx.interfaces.OnBindView
 
 /**

+ 0 - 2
app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/FilterRfidTokenDialog.kt

@@ -5,9 +5,7 @@ import com.grkj.data.model.vo.RfidTokenManageFilterVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogFilterRfidTokenBinding
 import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.extension.tipDialog
 import com.kongzue.dialogx.dialogs.CustomDialog
-import com.kongzue.dialogx.dialogs.PopTip
 import com.kongzue.dialogx.interfaces.OnBindView
 
 /**

+ 0 - 1
app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/UpdateKeyDialog.kt

@@ -1,7 +1,6 @@
 package com.grkj.iscs.features.main.dialog.hardware_manage
 
 import android.view.View
-import androidx.core.view.isVisible
 import com.grkj.data.enums.CommonDictDataEnum
 import com.grkj.data.model.dos.IsKey
 import com.grkj.data.model.vo.UpdateKeyDataVo

+ 0 - 1
app/src/main/java/com/grkj/iscs/features/main/dialog/hardware_manage/UpdateLockDialog.kt

@@ -1,7 +1,6 @@
 package com.grkj.iscs.features.main.dialog.hardware_manage
 
 import android.view.View
-import androidx.core.view.isVisible
 import com.grkj.data.enums.CommonDictDataEnum
 import com.grkj.data.model.dos.IsLock
 import com.grkj.data.model.vo.UpdateLockDataVo

+ 42 - 21
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateJobFragment.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
+import android.view.View
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
@@ -21,7 +22,7 @@ import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.iscs.features.main.viewmodel.job_manage.JobViewModel
 import com.grkj.iscs.utils.getLockModeStr
 import com.grkj.iscs.utils.getLockModeType
-import com.grkj.ui_base.base.BaseFragment
+import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.dialogs.PopTip
@@ -31,13 +32,12 @@ import com.sik.sikcore.thread.ThreadUtils
 import dagger.hilt.android.AndroidEntryPoint
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
-import kotlin.getValue
 
 /**
  * 新建作业
  */
 @AndroidEntryPoint
-class CreateJobFragment : BaseFragment<FragmentCreateJobBinding>() {
+class CreateJobFragment : BaseFormFragment<FragmentCreateJobBinding>() {
     private val viewModel: JobViewModel by viewModels()
     private var selectedLockMode: String? = null
     private var selectedWorkstationId: Long? = null
@@ -48,26 +48,33 @@ class CreateJobFragment : BaseFragment<FragmentCreateJobBinding>() {
         return R.layout.fragment_create_job
     }
 
+    override val needWatchObject: List<Any?> by lazy {
+        listOf(
+            binding.jobNameEt,
+            binding.workstationTv,
+            binding.lockModeTv,
+            selectedLockMode,
+            selectedWorkstationId,
+            selectedPointData,
+            selectedLockerData,
+            selectedColockerData
+        )
+    }
+
     override fun initView() {
         binding.back.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.cancel.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.save.setDebouncedClickListener {
             if (checkData()) {
@@ -292,6 +299,12 @@ class CreateJobFragment : BaseFragment<FragmentCreateJobBinding>() {
                                     .toString(),
                                 dialogType = TipDialog.DialogType.SUCCESS,
                                 countDownTime = 10,
+                                onConfirmClick = {
+                                    navController.popBackStack()
+                                },
+                                onCancelClick = {
+                                    navController.popBackStack()
+                                }
                             )
                         } else {
                             TipDialog.show(
@@ -312,6 +325,12 @@ class CreateJobFragment : BaseFragment<FragmentCreateJobBinding>() {
                         msg = CommonUtils.getStr(R.string.job_create_succeed).toString(),
                         dialogType = TipDialog.DialogType.SUCCESS,
                         countDownTime = 10,
+                        onConfirmClick = {
+                            navController.popBackStack()
+                        },
+                        onCancelClick = {
+                            navController.popBackStack()
+                        }
                     )
                 }
             } else {
@@ -436,7 +455,8 @@ class CreateJobFragment : BaseFragment<FragmentCreateJobBinding>() {
                         dataId = it.workstationId,
                         dataText = it.workstationName
                     )
-                },binding.workstationTv) {
+                }, binding.workstationTv
+            ) {
                 binding.workstationTv.text = it.getShowText()
                 selectedWorkstationId = it.getId()
             }
@@ -450,7 +470,8 @@ class CreateJobFragment : BaseFragment<FragmentCreateJobBinding>() {
                     dataTag = it.getLockModeType(),
                     dataText = it.getLockModeStr()
                 )
-            },binding.lockModeTv) {
+            }, binding.lockModeTv
+        ) {
             binding.lockModeTv.text = it.getShowText()
             selectedLockMode = it.getTag()
             binding.selectColockerLayout.isVisible = selectedLockMode?.contains(

+ 31 - 19
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopFragment.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
+import android.view.View
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
@@ -21,6 +22,7 @@ import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.iscs.features.main.viewmodel.job_manage.SopViewModel
 import com.grkj.iscs.utils.getLockModeStr
 import com.grkj.iscs.utils.getLockModeType
+import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
@@ -33,7 +35,7 @@ import dagger.hilt.android.AndroidEntryPoint
  * 新建SOP
  */
 @AndroidEntryPoint
-class CreateSopFragment : BaseFragment<FragmentCreateSopBinding>() {
+class CreateSopFragment : BaseFormFragment<FragmentCreateSopBinding>() {
     private val viewModel: SopViewModel by viewModels()
     private var selectedLockMode: String? = null
     private var selectedWorkstationId: Long? = null
@@ -44,26 +46,33 @@ class CreateSopFragment : BaseFragment<FragmentCreateSopBinding>() {
         return R.layout.fragment_create_sop
     }
 
+    override val needWatchObject: List<Any?> by lazy {
+        listOf(
+            binding.sopNameEt,
+            binding.workstationTv,
+            binding.lockModeTv,
+            selectedLockMode,
+            selectedWorkstationId,
+            selectedPointData,
+            selectedLockerData,
+            selectedColockerData
+        )
+    }
+
     override fun initView() {
         binding.back.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.cancel.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.confirm.setDebouncedClickListener {
             if (checkData()) {
@@ -276,6 +285,7 @@ class CreateSopFragment : BaseFragment<FragmentCreateSopBinding>() {
         binding.workstationTv.text = ""
         binding.noSelectedMemberLayout.isVisible = true
         binding.noSelectedPointLayout.isVisible = true
+        resetFormDirty()
     }
 
     /**
@@ -374,7 +384,8 @@ class CreateSopFragment : BaseFragment<FragmentCreateSopBinding>() {
                         dataId = it.workstationId,
                         dataText = it.workstationName
                     )
-                },binding.workstationTv) {
+                }, binding.workstationTv
+            ) {
                 binding.workstationTv.text = it.getShowText()
                 selectedWorkstationId = it.getId()
             }
@@ -388,7 +399,8 @@ class CreateSopFragment : BaseFragment<FragmentCreateSopBinding>() {
                     dataTag = it.getLockModeType(),
                     dataText = it.getLockModeStr()
                 )
-            },binding.lockModeTv) {
+            }, binding.lockModeTv
+        ) {
             binding.lockModeTv.text = it.getShowText()
             selectedLockMode = it.getTag()
             binding.selectColockerLayout.isVisible = selectedLockMode?.contains(

+ 39 - 30
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/CreateSopJobFragment.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
+import android.view.View
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
@@ -19,6 +20,7 @@ import com.grkj.iscs.databinding.ItemSelectMemberBinding
 import com.grkj.iscs.databinding.ItemSelectPointBinding
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.iscs.features.main.viewmodel.job_manage.SopJobViewModel
+import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
@@ -32,7 +34,7 @@ import kotlin.getValue
  * 新建SOP作业
  */
 @AndroidEntryPoint
-class CreateSopJobFragment : BaseFragment<FragmentCreateSopJobBinding>() {
+class CreateSopJobFragment : BaseFormFragment<FragmentCreateSopJobBinding>() {
     private val viewModel: SopJobViewModel by viewModels()
     private var selectedWorkstationId: Long? = null
     private var selectedSopId: Long? = null
@@ -44,26 +46,34 @@ class CreateSopJobFragment : BaseFragment<FragmentCreateSopJobBinding>() {
         return R.layout.fragment_create_sop_job
     }
 
+    override val needWatchObject: List<Any?> by lazy {
+        listOf(
+            binding.sopTv,
+            binding.workstationTv,
+            binding.jobNameEt,
+            selectedWorkstationId,
+            selectedSopId,
+            selectedSop,
+            selectedPointData,
+            selectedLockerData,
+            selectedColockerData
+        )
+    }
+
     override fun initView() {
         binding.back.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty){
+                showUnsavedConfirmDialog()
+            }else{
+                navController.popBackStack()
+            }
         }
         binding.cancel.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty){
+                showUnsavedConfirmDialog()
+            }else{
+                navController.popBackStack()
+            }
         }
         binding.save.setDebouncedClickListener {
             if (checkData()) {
@@ -183,6 +193,12 @@ class CreateSopJobFragment : BaseFragment<FragmentCreateSopJobBinding>() {
                                     .toString(),
                                 dialogType = TipDialog.DialogType.SUCCESS,
                                 countDownTime = 10,
+                                onConfirmClick = {
+                                    navController.popBackStack()
+                                },
+                                onCancelClick = {
+                                    navController.popBackStack()
+                                }
                             )
                         } else {
                             TipDialog.show(
@@ -202,6 +218,12 @@ class CreateSopJobFragment : BaseFragment<FragmentCreateSopJobBinding>() {
                         msg = CommonUtils.getStr(R.string.sop_job_save_succeed).toString(),
                         dialogType = TipDialog.DialogType.SUCCESS,
                         countDownTime = 10,
+                        onConfirmClick = {
+                            navController.popBackStack()
+                        },
+                        onCancelClick = {
+                            navController.popBackStack()
+                        }
                     )
                 }
             } else {
@@ -216,19 +238,6 @@ class CreateSopJobFragment : BaseFragment<FragmentCreateSopJobBinding>() {
         }
     }
 
-    private fun clearData() {
-        binding.jobNameEt.setText("")
-        selectedLockerData = listOf()
-        selectedColockerData = listOf()
-        selectedPointData = listOf()
-        selectedSopId = null
-        selectedWorkstationId = null
-        binding.sopTv.text = ""
-        binding.workstationTv.text = ""
-        binding.noSelectedMemberLayout.isVisible = true
-        binding.noSelectedPointLayout.isVisible = true
-    }
-
     /**
      * 检查数据
      */

+ 71 - 62
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditJobFragment.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
+import android.view.View
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
@@ -21,6 +22,7 @@ import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.iscs.features.main.viewmodel.job_manage.JobViewModel
 import com.grkj.iscs.utils.getLockModeStr
 import com.grkj.iscs.utils.getLockModeType
+import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
@@ -34,7 +36,7 @@ import kotlin.getValue
  * 编辑作业
  */
 @AndroidEntryPoint
-class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
+class EditJobFragment : BaseFormFragment<FragmentEditJobBinding>() {
     private val viewModel: JobViewModel by viewModels()
     private var selectedLockMode: String? = null
     private var selectedWorkstationId: Long? = null
@@ -45,33 +47,40 @@ class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
         return R.layout.fragment_edit_job
     }
 
+    override val needWatchObject: List<Any?> by lazy {
+        listOf(
+            binding.jobNameEt, binding.workstationTv, binding.lockModeTv,
+            selectedLockMode,
+            selectedWorkstationId,
+            selectedPointData,
+            selectedLockerData,
+            selectedColockerData
+        )
+    }
+
     override fun initView() {
         binding.back.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.cancel.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.save.setDebouncedClickListener {
             if (checkData()) {
                 TipDialog.show(
                     title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                    msg = CommonUtils.getStr(R.string.job_save_tip,
-                        args = listOf<String>(binding.jobNameEt.text.toString()).toTypedArray()).toString(),
+                    msg = CommonUtils.getStr(
+                        R.string.job_save_tip,
+                        args = listOf<String>(binding.jobNameEt.text.toString()).toTypedArray()
+                    ).toString(),
                     dialogType = TipDialog.DialogType.INFO,
                     countDownTime = 10,
                     onConfirmClick = {
@@ -105,23 +114,19 @@ class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
                 PopTip.tip(R.string.please_select_job_workstation)
                 return@setDebouncedClickListener
             }
-            GlobalDataTempStore.getInstance()
-                .saveData(
-                    DataTransferConstants.KEY_SELECT_POINT_WORKSTATION_ID,
-                    selectedWorkstationId!!
-                )
+            GlobalDataTempStore.getInstance().saveData(
+                DataTransferConstants.KEY_SELECT_POINT_WORKSTATION_ID, selectedWorkstationId!!
+            )
             GlobalDataTempStore.getInstance()
                 .saveData(DataTransferConstants.KEY_SELECTED_POINT_DATA, selectedPointData)
-            GlobalDataTempStore.getInstance()
-                .saveData(
-                    DataTransferConstants.KEY_PREVIEW_STEP_TITLE_DATA,
-                    CommonUtils.getStr(R.string.create_job_title).toString()
-                )
-            GlobalDataTempStore.getInstance()
-                .saveData(
-                    DataTransferConstants.KEY_PREVIEW_STEP_ICON_DATA,
-                    R.mipmap.icon_data_manage_menu_point_manage
-                )
+            GlobalDataTempStore.getInstance().saveData(
+                DataTransferConstants.KEY_PREVIEW_STEP_TITLE_DATA,
+                CommonUtils.getStr(R.string.create_job_title).toString()
+            )
+            GlobalDataTempStore.getInstance().saveData(
+                DataTransferConstants.KEY_PREVIEW_STEP_ICON_DATA,
+                R.mipmap.icon_data_manage_menu_point_manage
+            )
             navController.navigate(R.id.action_editJobFragment_to_selectPointFragment)
         }
         binding.selectMemberTv.setDebouncedClickListener {
@@ -138,28 +143,22 @@ class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
                     LockStepEnum.COLOCK.type.toString()
                 ) == true
             )
-            GlobalDataTempStore.getInstance()
-                .saveData(
-                    DataTransferConstants.KEY_SELECT_POINT_WORKSTATION_ID,
-                    selectedWorkstationId!!
-                )
+            GlobalDataTempStore.getInstance().saveData(
+                DataTransferConstants.KEY_SELECT_POINT_WORKSTATION_ID, selectedWorkstationId!!
+            )
             GlobalDataTempStore.getInstance()
                 .saveData(DataTransferConstants.KEY_SELECTED_MEMBER_LOCKER_DATA, selectedLockerData)
-            GlobalDataTempStore.getInstance()
-                .saveData(
-                    DataTransferConstants.KEY_SELECTED_MEMBER_COLOCKER_DATA,
-                    selectedColockerData
-                )
-            GlobalDataTempStore.getInstance()
-                .saveData(
-                    DataTransferConstants.KEY_PREVIEW_STEP_TITLE_DATA,
-                    CommonUtils.getStr(R.string.edit_job_title).toString()
-                )
-            GlobalDataTempStore.getInstance()
-                .saveData(
-                    DataTransferConstants.KEY_PREVIEW_STEP_ICON_DATA,
-                    R.mipmap.icon_data_manage_menu_point_manage
-                )
+            GlobalDataTempStore.getInstance().saveData(
+                DataTransferConstants.KEY_SELECTED_MEMBER_COLOCKER_DATA, selectedColockerData
+            )
+            GlobalDataTempStore.getInstance().saveData(
+                DataTransferConstants.KEY_PREVIEW_STEP_TITLE_DATA,
+                CommonUtils.getStr(R.string.edit_job_title).toString()
+            )
+            GlobalDataTempStore.getInstance().saveData(
+                DataTransferConstants.KEY_PREVIEW_STEP_ICON_DATA,
+                R.mipmap.icon_data_manage_menu_point_manage
+            )
             navController.navigate(R.id.action_editJobFragment_to_selectMemberFragment)
         }
         binding.pointRv.grid(6).setup {
@@ -201,7 +200,12 @@ class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
                                     .toString(),
                                 dialogType = TipDialog.DialogType.SUCCESS,
                                 countDownTime = 10,
-                            )
+                                onConfirmClick = {
+                                    navController.popBackStack()
+                                },
+                                onCancelClick = {
+                                    navController.popBackStack()
+                                })
                         } else {
                             TipDialog.show(
                                 title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_failed)
@@ -221,7 +225,12 @@ class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
                         msg = CommonUtils.getStr(R.string.job_create_succeed).toString(),
                         dialogType = TipDialog.DialogType.SUCCESS,
                         countDownTime = 10,
-                    )
+                        onConfirmClick = {
+                            navController.popBackStack()
+                        },
+                        onCancelClick = {
+                            navController.popBackStack()
+                        })
                 }
             } else {
                 TipDialog.show(
@@ -351,10 +360,10 @@ class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
             TextDropDownDialog.showSingle(
                 viewModel.workstationData.map {
                     TextDropDownDialog.SimpleTextDropDownEntity(
-                        dataId = it.workstationId,
-                        dataText = it.workstationName
+                        dataId = it.workstationId, dataText = it.workstationName
                     )
-                },binding.workstationTv){
+                }, binding.workstationTv
+            ) {
                 binding.workstationTv.text = it.getShowText()
                 selectedWorkstationId = it.getId()
             }
@@ -365,10 +374,10 @@ class EditJobFragment : BaseFragment<FragmentEditJobBinding>() {
         TextDropDownDialog.showSingle(
             LockModeEnum.values().map {
                 TextDropDownDialog.SimpleTextDropDownEntity(
-                    dataTag = it.getLockModeType(),
-                    dataText = it.getLockModeStr()
+                    dataTag = it.getLockModeType(), dataText = it.getLockModeStr()
                 )
-            },binding.lockModeTv){
+            }, binding.lockModeTv
+        ) {
             binding.lockModeTv.text = it.getShowText()
             selectedLockMode = it.getTag()
             binding.selectColockerLayout.isVisible = selectedLockMode?.contains(

+ 40 - 25
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopFragment.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
+import android.view.View
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
@@ -8,34 +9,33 @@ import com.drake.brv.utils.grid
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
+import com.grkj.data.enums.LockModeEnum
+import com.grkj.data.enums.LockStepEnum
+import com.grkj.data.enums.RoleEnum
 import com.grkj.data.model.vo.PointManageVo
 import com.grkj.data.model.vo.UserManageVo
 import com.grkj.iscs.R
 import com.grkj.iscs.common.DataTransferConstants
+import com.grkj.iscs.databinding.FragmentEditSopBinding
 import com.grkj.iscs.databinding.ItemSelectMemberBinding
 import com.grkj.iscs.databinding.ItemSelectPointBinding
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
-import com.grkj.data.enums.LockModeEnum
-import com.grkj.data.enums.LockStepEnum
-import com.grkj.data.enums.RoleEnum
-import com.grkj.iscs.databinding.FragmentEditSopBinding
 import com.grkj.iscs.features.main.viewmodel.job_manage.SopViewModel
 import com.grkj.iscs.utils.getLockModeStr
 import com.grkj.iscs.utils.getLockModeType
-import com.grkj.ui_base.base.BaseFragment
+import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.extension.setDebouncedClickListener
 import dagger.hilt.android.AndroidEntryPoint
-import kotlin.getValue
 
 /**
  * 修改SOP
  */
 @AndroidEntryPoint
-class EditSopFragment : BaseFragment<FragmentEditSopBinding>() {
+class EditSopFragment : BaseFormFragment<FragmentEditSopBinding>() {
     private val viewModel: SopViewModel by viewModels()
     private var selectedLockMode: String? = null
     private var selectedWorkstationId: Long? = null
@@ -46,26 +46,33 @@ class EditSopFragment : BaseFragment<FragmentEditSopBinding>() {
         return R.layout.fragment_edit_sop
     }
 
+    override val needWatchObject: List<Any?> by lazy {
+        listOf(
+            binding.sopNameEt,
+            binding.workstationTv,
+            binding.lockModeTv,
+            selectedLockMode,
+            selectedWorkstationId,
+            selectedPointData,
+            selectedLockerData,
+            selectedColockerData
+        )
+    }
+
     override fun initView() {
         binding.back.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.cancel.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.confirm.setDebouncedClickListener {
             if (checkData()) {
@@ -183,6 +190,12 @@ class EditSopFragment : BaseFragment<FragmentEditSopBinding>() {
                     msg = CommonUtils.getStr(R.string.sop_save_succeed).toString(),
                     dialogType = TipDialog.DialogType.SUCCESS,
                     countDownTime = 10,
+                    onConfirmClick = {
+                        navController.popBackStack()
+                    },
+                    onCancelClick = {
+                        navController.popBackStack()
+                    }
                 )
             } else {
                 TipDialog.show(
@@ -319,7 +332,8 @@ class EditSopFragment : BaseFragment<FragmentEditSopBinding>() {
                         dataId = it.workstationId,
                         dataText = it.workstationName
                     )
-                },binding.workstationTv) {
+                }, binding.workstationTv
+            ) {
                 binding.workstationTv.text = it.getShowText()
                 selectedWorkstationId = it.getId()
             }
@@ -333,7 +347,8 @@ class EditSopFragment : BaseFragment<FragmentEditSopBinding>() {
                     dataTag = it.getLockModeType(),
                     dataText = it.getLockModeStr()
                 )
-            },binding.lockModeTv){
+            }, binding.lockModeTv
+        ) {
             binding.lockModeTv.text = it.getShowText()
             selectedLockMode = it.getTag()
             binding.selectColockerLayout.isVisible = selectedLockMode?.contains(

+ 53 - 29
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/EditSopJobFragment.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
+import android.view.View
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
@@ -19,6 +20,7 @@ import com.grkj.iscs.databinding.ItemSelectMemberBinding
 import com.grkj.iscs.databinding.ItemSelectPointBinding
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.iscs.features.main.viewmodel.job_manage.SopJobViewModel
+import com.grkj.ui_base.base.BaseFormFragment
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
@@ -32,7 +34,7 @@ import kotlin.getValue
  * 编辑SOP作业
  */
 @AndroidEntryPoint
-class EditSopJobFragment : BaseFragment<FragmentEditSopJobBinding>() {
+class EditSopJobFragment : BaseFormFragment<FragmentEditSopJobBinding>() {
     private val viewModel: SopJobViewModel by viewModels()
     private var selectedWorkstationId: Long? = null
     private var selectedSopId: Long? = null
@@ -44,26 +46,34 @@ class EditSopJobFragment : BaseFragment<FragmentEditSopJobBinding>() {
         return R.layout.fragment_edit_sop_job
     }
 
+    override val needWatchObject: List<Any?> by lazy {
+        listOf(
+            binding.sopTv,
+            binding.workstationTv,
+            binding.jobNameEt,
+            selectedWorkstationId,
+            selectedSopId,
+            selectedSop,
+            selectedPointData,
+            selectedLockerData,
+            selectedColockerData
+        )
+    }
+
     override fun initView() {
         binding.back.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.cancel.setDebouncedClickListener {
-            TipDialog.show(
-                title = CommonUtils.getStr(com.grkj.ui_base.R.string.action_hint).toString(),
-                msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
-                dialogType = TipDialog.DialogType.ERROR,
-                countDownTime = 10,
-                onConfirmClick = {
-                    navController.popBackStack()
-                })
+            if (isFormDirty) {
+                showUnsavedConfirmDialog()
+            } else {
+                navController.popBackStack()
+            }
         }
         binding.save.setDebouncedClickListener {
             if (checkData()) {
@@ -119,21 +129,21 @@ class EditSopJobFragment : BaseFragment<FragmentEditSopJobBinding>() {
                 ) == true
             )
             GlobalDataTempStore.getInstance().saveData(
-                    DataTransferConstants.KEY_SELECT_POINT_WORKSTATION_ID, selectedWorkstationId!!
-                )
+                DataTransferConstants.KEY_SELECT_POINT_WORKSTATION_ID, selectedWorkstationId!!
+            )
             GlobalDataTempStore.getInstance()
                 .saveData(DataTransferConstants.KEY_SELECTED_MEMBER_LOCKER_DATA, selectedLockerData)
             GlobalDataTempStore.getInstance().saveData(
-                    DataTransferConstants.KEY_SELECTED_MEMBER_COLOCKER_DATA, selectedColockerData
-                )
+                DataTransferConstants.KEY_SELECTED_MEMBER_COLOCKER_DATA, selectedColockerData
+            )
             GlobalDataTempStore.getInstance().saveData(
-                    DataTransferConstants.KEY_PREVIEW_STEP_TITLE_DATA,
-                    CommonUtils.getStr(R.string.create_sop_title).toString()
-                )
+                DataTransferConstants.KEY_PREVIEW_STEP_TITLE_DATA,
+                CommonUtils.getStr(R.string.create_sop_title).toString()
+            )
             GlobalDataTempStore.getInstance().saveData(
-                    DataTransferConstants.KEY_PREVIEW_STEP_ICON_DATA,
-                    R.mipmap.icon_data_manage_menu_point_manage
-                )
+                DataTransferConstants.KEY_PREVIEW_STEP_ICON_DATA,
+                R.mipmap.icon_data_manage_menu_point_manage
+            )
             navController.navigate(R.id.action_editSopJobFragment_to_selectMemberFragment)
         }
         binding.pointRv.grid(6).setup {
@@ -175,6 +185,12 @@ class EditSopJobFragment : BaseFragment<FragmentEditSopJobBinding>() {
                                     .toString(),
                                 dialogType = TipDialog.DialogType.SUCCESS,
                                 countDownTime = 10,
+                                onConfirmClick = {
+                                    navController.popBackStack()
+                                },
+                                onCancelClick = {
+                                    navController.popBackStack()
+                                }
                             )
                         } else {
                             TipDialog.show(
@@ -194,6 +210,12 @@ class EditSopJobFragment : BaseFragment<FragmentEditSopJobBinding>() {
                         msg = CommonUtils.getStr(R.string.sop_job_save_succeed).toString(),
                         dialogType = TipDialog.DialogType.SUCCESS,
                         countDownTime = 10,
+                        onConfirmClick = {
+                            navController.popBackStack()
+                        },
+                        onCancelClick = {
+                            navController.popBackStack()
+                        }
                     )
                 }
             } else {
@@ -315,7 +337,8 @@ class EditSopJobFragment : BaseFragment<FragmentEditSopJobBinding>() {
                     TextDropDownDialog.SimpleTextDropDownEntity(
                         dataId = it.workstationId, dataText = it.workstationName
                     )
-                },binding.workstationTv) {
+                }, binding.workstationTv
+            ) {
                 binding.workstationTv.text = it.getShowText()
                 selectedWorkstationId = it.getId()
             }
@@ -331,7 +354,8 @@ class EditSopJobFragment : BaseFragment<FragmentEditSopJobBinding>() {
                 TextDropDownDialog.SimpleTextDropDownEntity(
                     dataId = it.sopId, dataObject = it, dataText = it.sopName ?: ""
                 )
-            },binding.sopTv) {
+            }, binding.sopTv
+        ) {
             binding.sopTv.text = it.getShowText()
             selectedSopId = it.getId()
             selectedSop = it.getData() as SopManageVo

+ 2 - 21
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/InProgressJobManageFragment.kt

@@ -1,6 +1,7 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
 import android.graphics.Color
+import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
 import com.drake.brv.BindingAdapter
 import com.drake.brv.annotaion.DividerOrientation
@@ -52,14 +53,6 @@ class InProgressJobManageFragment : BaseFragment<FragmentInProgressJobManageBind
                 onListDataBinding(this)
             }
         }
-        setSelectAllListener()
-    }
-
-    private fun setSelectAllListener() {
-        binding.selectAll.setOnCheckedChangeListener { v, checked ->
-            viewModel.jobManageDataList.forEach { it.isSelected = checked }
-            binding.listRv.adapter?.notifyDataSetChanged()
-        }
     }
 
     private fun BindingAdapter.BindingViewHolder.onListDataBinding(holder: BindingAdapter.BindingViewHolder) {
@@ -67,14 +60,7 @@ class InProgressJobManageFragment : BaseFragment<FragmentInProgressJobManageBind
         val item = holder.getModel<JobTicketManageVo>()
         itemBinding.jobName.text = item.ticketName
         itemBinding.status.text = JobTicketStatusEnum.getTicketStatusStr(item.ticketStatus)
-        itemBinding.select.setOnCheckedChangeListener(null)
-        itemBinding.select.isChecked = item.isSelected
-        itemBinding.select.setOnCheckedChangeListener { _, checked ->
-            item.isSelected = checked
-            binding.selectAll.setOnCheckedChangeListener(null)
-            binding.selectAll.isChecked = viewModel.jobManageDataList.all { it.isSelected }
-            setSelectAllListener()
-        }
+        itemBinding.select.isVisible = false
         itemBinding.view.setDebouncedClickListener {
             GlobalDataTempStore.getInstance()
                 .saveData(DataTransferConstants.KEY_JOB_TICKET_ID, item.ticketId)
@@ -89,11 +75,6 @@ class InProgressJobManageFragment : BaseFragment<FragmentInProgressJobManageBind
 
     private fun getData(nextPage: Boolean = true) {
         viewModel.getData(nextPage).observe(this) {
-            if (!nextPage) {
-                binding.selectAll.setOnCheckedChangeListener(null)
-                binding.selectAll.isChecked = false
-                setSelectAllListener()
-            }
             binding.refreshLayout.finishRefresh()
             binding.refreshLayout.finishLoadMore()
             binding.listRv.models = viewModel.jobManageDataList.filter {

+ 92 - 33
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt

@@ -28,6 +28,7 @@ import com.grkj.shared.model.EventBean
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.data.data.EventConstants
 import com.grkj.ui_base.dialog.TipDialog
+import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.data.GlobalDataTempStore
@@ -136,14 +137,14 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
             if (item.stepIndex == LockStepEnum.SELECT_MEMBER.type) {
                 GlobalDataTempStore.getInstance().saveData(
                     DataTransferConstants.KEY_CAN_SELECT_COLOCKER,
-                    viewModel.ticketData.lockMode?.contains(
+                    viewModel.ticketData!!.lockMode?.contains(
                         LockStepEnum.COLOCK.type.toString()
                     ) == true
                 )
                 GlobalDataTempStore.getInstance()
                     .saveData(
                         DataTransferConstants.KEY_SELECT_POINT_WORKSTATION_ID,
-                        viewModel.ticketData.workstationId
+                        viewModel.ticketData!!.workstationId
                     )
                 GlobalDataTempStore.getInstance()
                     .saveData(
@@ -158,7 +159,7 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
                 GlobalDataTempStore.getInstance()
                     .saveData(
                         DataTransferConstants.KEY_PREVIEW_STEP_TITLE_DATA,
-                        viewModel.ticketData.ticketName
+                        viewModel.ticketData!!.ticketName
                     )
                 GlobalDataTempStore.getInstance()
                     .saveData(
@@ -220,9 +221,51 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
                 (event.data as RFIDCardReadEvent).let {
                     logger.info("读卡器获取卡片RFID:${it.rfidNo}")
                     if (viewModel.currentStepData?.stepIndex == LockStepEnum.COLOCK.type) {
-                        logger.info("添加共锁")
+                        viewModel.getUserIdByCardRfid(it.rfidNo).observe(this) { userId ->
+                            userId?.let {
+                                val isJobCardUser =
+                                    viewModel.ticketUser.find { it.userId == userId }
+                                isJobCardUser?.let { colocker ->
+                                    if (colocker.jobStatus == "0") {
+                                        logger.info("添加共锁")
+                                        TipDialog.showInfo(
+                                            msg = CommonUtils.getStr(com.grkj.ui_base.R.string.confirm_to_colock)
+                                                .toString(), countDownTime = 10, onConfirmClick = {
+                                                colocker.jobStatus = "1"
+                                                viewModel.colockerStatusChange(colocker)
+                                                    .observe(this) {
+                                                        if (it) {
+                                                            PopTip.tip(R.string.colock_complete)
+                                                        } else {
+                                                            PopTip.tip(R.string.colock_failed)
+                                                        }
+                                                        refreshTicketUser()
+                                                    }
+                                            })
+                                    } else if (colocker.jobStatus == "1") {
+                                        logger.info("解除共锁")
+                                        TipDialog.showInfo(
+                                            msg = CommonUtils.getStr(com.grkj.ui_base.R.string.confirm_to_uncolock)
+                                                .toString(), countDownTime = 10, onConfirmClick = {
+                                                colocker.jobStatus = "2"
+                                                viewModel.colockerStatusChange(colocker)
+                                                    .observe(this) {
+                                                        if (it) {
+                                                            PopTip.tip(R.string.uncolock_complete)
+                                                        } else {
+                                                            PopTip.tip(R.string.uncolock_failed)
+                                                        }
+                                                        refreshTicketUser()
+                                                    }
+                                            })
+                                    } else {
+                                        PopTip.tip(R.string.currently_unable_to_lock_together)
+                                    }
+                                } ?: PopTip.tip(R.string.invalid_user)
+                            } ?: PopTip.tip(R.string.invalid_card)
+                        }
                     } else {
-                        logger.info("当前阶段无法共锁")
+                        PopTip.tip(R.string.currently_unable_to_lock_together)
                     }
                 }
             }
@@ -290,35 +333,51 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
 
     private fun getData() {
         viewModel.getJobTicketData().observe(this) {
-            binding.jobNameTv.text = viewModel.ticketData.ticketName
-            binding.listRv.models = viewModel.ticketPoints
-            binding.stepRv.models = viewModel.ticketStep
-            viewModel.currentStepData = viewModel.ticketStep.firstOrNull { it.stepStatus == "0" }
-            binding.spaceView.isVisible =
-                viewModel.ticketData.lockMode?.contains(LockStepEnum.COLOCK.type.toString()) == false
-            binding.colockerLayout.isVisible =
-                viewModel.ticketData.lockMode?.contains(LockStepEnum.COLOCK.type.toString()) == true
-            viewModel.ticketUser.let {
-                binding.waitToColockRv.models =
-                    it.filter { it.jobStatus == "0" && it.userRole == RoleEnum.JTCOLOCKER.roleKey }
-                binding.waitToColock.text =
-                    getString(
-                        R.string.wait_to_colock,
-                        it.count { it.jobStatus == "0" && it.userRole == RoleEnum.JTCOLOCKER.roleKey })
-                binding.alreadyColockRv.models =
-                    it.filter { it.jobStatus == "1" && it.userRole == RoleEnum.JTCOLOCKER.roleKey }
-                binding.alreadyColock.text =
-                    getString(
-                        R.string.already_colock,
-                        it.count { it.jobStatus == "1" && it.userRole == RoleEnum.JTCOLOCKER.roleKey })
-                binding.alreadyUncolockRv.models =
-                    it.filter { it.jobStatus == "2" && it.userRole == RoleEnum.JTCOLOCKER.roleKey }
-                binding.alreadyUncolock.text =
-                    getString(
-                        R.string.already_uncolock,
-                        it.count { it.jobStatus == "2" && it.userRole == RoleEnum.JTCOLOCKER.roleKey })
+            if (it) {
+                binding.jobNameTv.text = viewModel.ticketData!!.ticketName
+                binding.listRv.models = viewModel.ticketPoints
+                binding.stepRv.models = viewModel.ticketStep
+                viewModel.currentStepData =
+                    viewModel.ticketStep.firstOrNull { it.stepStatus == "0" }
+                binding.spaceView.isVisible =
+                    viewModel.ticketData!!.lockMode?.contains(LockStepEnum.COLOCK.type.toString()) == false
+                binding.colockerLayout.isVisible =
+                    viewModel.ticketData!!.lockMode?.contains(LockStepEnum.COLOCK.type.toString()) == true
+                refreshTicketUser()
+                checkCurrentStep()
+            } else {
+                TipDialog.showError(
+                    msg = CommonUtils.getStr(com.grkj.ui_base.R.string.ticket_lost).toString(),
+                    onConfirmClick = {
+                        navController.popBackStack()
+                    },
+                    onCancelClick = {
+                        navController.popBackStack()
+                    })
             }
-            checkCurrentStep()
+        }
+    }
+
+    private fun refreshTicketUser() {
+        viewModel.ticketUser.let {
+            binding.waitToColockRv.models =
+                it.filter { it.jobStatus == "0" && it.userRole == RoleEnum.JTCOLOCKER.roleKey }
+            binding.waitToColock.text =
+                getString(
+                    R.string.wait_to_colock,
+                    it.count { it.jobStatus == "0" && it.userRole == RoleEnum.JTCOLOCKER.roleKey })
+            binding.alreadyColockRv.models =
+                it.filter { it.jobStatus == "1" && it.userRole == RoleEnum.JTCOLOCKER.roleKey }
+            binding.alreadyColock.text =
+                getString(
+                    R.string.already_colock,
+                    it.count { it.jobStatus == "1" && it.userRole == RoleEnum.JTCOLOCKER.roleKey })
+            binding.alreadyUncolockRv.models =
+                it.filter { it.jobStatus == "2" && it.userRole == RoleEnum.JTCOLOCKER.roleKey }
+            binding.alreadyUncolock.text =
+                getString(
+                    R.string.already_uncolock,
+                    it.count { it.jobStatus == "2" && it.userRole == RoleEnum.JTCOLOCKER.roleKey })
         }
     }
 

+ 0 - 1
app/src/main/java/com/grkj/iscs/features/main/viewmodel/MainViewModel.kt

@@ -76,7 +76,6 @@ class MainViewModel @Inject constructor() : BaseViewModel() {
                                                             4,
                                                             it.bleDevice
                                                         )
-                                                        BleConnectionManager.getBatteryPower(it.bleDevice)
                                                     }
                                                 }
                                             } else {

+ 19 - 13
app/src/main/java/com/grkj/iscs/features/main/viewmodel/job_manage/JobExecuteViewModel.kt

@@ -35,7 +35,7 @@ import java.util.concurrent.atomic.AtomicInteger
 class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicketRepository) :
     BaseViewModel() {
     var ticketId: Long = 0
-    lateinit var ticketData: IsJobTicketDataVo
+    var ticketData: IsJobTicketDataVo? = null
     lateinit var ticketKey: List<IsJobTicketKeyDataVo>
     lateinit var ticketLock: List<IsJobTicketLockDataVo>
     lateinit var ticketPoints: List<IsJobTicketPointsDataVo>
@@ -52,6 +52,10 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
     fun getJobTicketData(): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
             ticketData = jobTicketRepository.getJobTicketDataByTicketId(ticketId)
+            if (ticketData == null) {
+                emit(false)
+                return@liveData
+            }
             ticketKey = jobTicketRepository.getJobTicketKeyDataByTicketId(ticketId)
             ticketLock = jobTicketRepository.getJobTicketLockDataByTicketId(ticketId)
             ticketPoints = jobTicketRepository.getJobTicketPointsDataByTicketId(ticketId)
@@ -77,7 +81,7 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
     fun updateLockerAndColockerData(): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
             jobTicketRepository.updateClockerAndColockerData(
-                ticketData.ticketId, currentStepData, selectedLockerData, selecteColockerData
+                ticketData!!.ticketId, currentStepData, selectedLockerData, selecteColockerData
             )
             ticketStep = jobTicketRepository.getJobTicketStepDataByTicketId(ticketId)
             jobTicketRepository.updateTicketDataStatus(
@@ -113,13 +117,9 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
      */
     fun toLock(): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
-            LoadingEvent.sendLoadingEvent(
-                CommonUtils.getStr(com.grkj.ui_base.R.string.check_key_and_lock),
-                true
-            )
             ModbusBusinessManager.checkEquipCount(ticketPoints.count {
                 it.pointStatus == "0" || (it.pointStatus == "2" && LockModeEnum.isUnLockFirst(
-                    ticketData.lockMode.toString()
+                    ticketData?.lockMode.toString()
                 ))
             }, true) { keyMap, lockMap ->
                 if (lockMap.isEmpty()) {
@@ -151,7 +151,7 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
                         }
                     )
                     return@checkEquipCount
-                }else{
+                } else {
                     ModbusBusinessManager.addDeviceTake(
                         DeviceConst.DEVICE_TYPE_KEY, ticketId, keyMap.second?.rfid!!
                     )
@@ -193,10 +193,6 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
      */
     fun toUnLock(): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
-            LoadingEvent.sendLoadingEvent(
-                CommonUtils.getStr(com.grkj.ui_base.R.string.check_key_and_lock),
-                true
-            )
             if (checkBeforeToUnlock()) {
                 ModbusBusinessManager.checkEquipCount(0, true) { keyMap, _ ->
                     LoadingEvent.sendLoadingEvent()
@@ -213,7 +209,7 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
                     ModbusBusinessManager.addDeviceTake(
                         DeviceConst.DEVICE_TYPE_KEY,
                         ticketId,
-                        keyMap.second?.rfid ?: ""
+                        keyMap.second?.rfid!!
                     )
                     val deviceTakeUpdate = DeviceTakeUpdate(
                         DeviceConst.DEVICE_TYPE_KEY,
@@ -253,4 +249,14 @@ class JobExecuteViewModel @Inject constructor(val jobTicketRepository: IJobTicke
             emit(true)
         }
     }
+
+    /**
+     * 根据卡片id获取用户id
+     */
+    fun getUserIdByCardRfid(rfid: String): LiveData<Long?> {
+        return liveData(Dispatchers.IO){
+            val userId = jobTicketRepository.getUserIdByCardRfid(rfid)
+            emit(userId)
+        }
+    }
 }

+ 6 - 0
app/src/main/res/drawable/bg_main_nav_bar.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/white80" />
+    <corners android:radius="@dimen/common_spacing_2x" />
+</shape>

+ 12 - 12
app/src/main/res/layout-land/activity_main.xml

@@ -68,22 +68,22 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_below="@+id/header_line"
-            android:layout_toRightOf="@+id/navRail"
+            android:layout_toRightOf="@+id/nav_bar"
             app:defaultNavHost="true"
             app:navGraph="@navigation/nav_home" />
 
-        <com.google.android.material.navigationrail.NavigationRailView
-            android:id="@+id/navRail"
+        <com.grkj.ui_base.widget.CustomNavBar
+            android:id="@+id/nav_bar"
             android:layout_width="@dimen/home_bottom_nav_size"
-            android:layout_below="@+id/header_line"
             android:layout_height="match_parent"
-            android:background="@color/white"
-            android:textSize="@dimen/home_bottom_nav_text_size"
-            app:itemBackground="@color/white"
-            app:itemIconSize="@dimen/home_bottom_nav_icon_size"
-            app:itemIconTint="@color/nav_item_color"
-            app:itemTextColor="@color/nav_item_color"
-            app:labelVisibilityMode="labeled"
-            app:menuGravity="center" />
+            android:layout_below="@+id/header_line"
+            android:layout_margin="@dimen/common_spacing"
+            app:navBackgroundColor="@color/white80"
+            app:navBallDiameter="@dimen/home_bottom_nav_ball_size"
+            app:navCornerRadius="@dimen/home_bottom_nav_corner_radius"
+            app:navIconSize="@dimen/home_bottom_nav_icon_size"
+            app:navNotchHeight="@dimen/home_bottom_nav_notch_height"
+            app:navOrientation="vertical"
+            app:navTextSize="@dimen/home_bottom_nav_text_size" />
     </RelativeLayout>
 </layout>

+ 11 - 11
app/src/main/res/layout/activity_main.xml

@@ -69,23 +69,23 @@
             android:name="androidx.navigation.fragment.NavHostFragment"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_above="@+id/bottom_nav"
+            android:layout_above="@+id/nav_bar"
             android:layout_below="@+id/header_line"
             app:defaultNavHost="true"
             app:navGraph="@navigation/nav_home" />
 
-        <com.google.android.material.bottomnavigation.BottomNavigationView
-            android:id="@+id/bottom_nav"
+        <com.grkj.ui_base.widget.CustomNavBar
+            android:id="@+id/nav_bar"
             android:layout_width="match_parent"
             android:layout_height="@dimen/home_bottom_nav_size"
             android:layout_alignParentBottom="true"
-            android:layout_gravity="bottom"
-            android:background="@color/white"
-            android:textSize="@dimen/home_bottom_nav_text_size"
-            app:itemBackground="@color/white"
-            app:itemIconSize="@dimen/home_bottom_nav_icon_size"
-            app:itemIconTint="@color/nav_item_color"
-            app:itemTextColor="@color/nav_item_color"
-            app:labelVisibilityMode="labeled" />
+            android:layout_margin="@dimen/common_spacing"
+            app:navBackgroundColor="@color/white80"
+            app:navBallDiameter="@dimen/home_bottom_nav_ball_size"
+            app:navCornerRadius="@dimen/home_bottom_nav_corner_radius"
+            app:navIconSize="@dimen/home_bottom_nav_icon_size"
+            app:navNotchHeight="@dimen/home_bottom_nav_notch_height"
+            app:navOrientation="horizontal"
+            app:navTextSize="@dimen/home_bottom_nav_text_size" />
     </RelativeLayout>
 </layout>

+ 1 - 7
app/src/main/res/layout/fragment_in_progress_job_manage.xml

@@ -57,18 +57,12 @@
             android:divider="@drawable/divider_table"
             android:showDividers="middle">
 
-            <CheckBox
-                android:id="@+id/select_all"
-                android:layout_width="30dp"
-                android:layout_height="30dp"
-                android:layout_gravity="center"
-                android:layout_margin="@dimen/common_spacing" />
-
             <TextView
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1"
                 android:gravity="center"
+                android:paddingVertical="@dimen/common_spacing"
                 android:text="@string/job_name"
                 android:textSize="@dimen/common_text_size" />
 

+ 7 - 5
app/src/main/res/layout/item_device_registration_key.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <RelativeLayout
         android:layout_width="wrap_content"
@@ -19,10 +20,10 @@
             android:layout_alignLeft="@+id/iv_key"
             android:layout_alignRight="@+id/iv_key"
             android:layout_marginTop="15dp"
-            android:textSize="@dimen/common_text_size"
+            android:textSize="@dimen/item_device_text_size"
             android:background="@drawable/common_btn_red_bg"
             android:text="@string/new_device"
-            android:visibility="invisible" />
+            android:visibility="visible" />
 
         <TextView
             android:id="@+id/tv_new_device_mac"
@@ -31,9 +32,10 @@
             android:layout_alignLeft="@+id/iv_key"
             android:layout_alignRight="@+id/iv_key"
             android:layout_marginTop="15dp"
-            android:textSize="@dimen/common_text_size"
+            tools:text="AA:BB:CC:DD:EE:FF"
+            android:textSize="@dimen/item_device_text_size"
             android:background="@drawable/common_btn_green_bg"
-            android:visibility="invisible" />
+            android:visibility="visible" />
 
         <View
             android:id="@+id/v_buckle_status"

+ 4 - 3
app/src/main/res/layout/item_device_registration_lock.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <RelativeLayout
         android:layout_width="wrap_content"
@@ -22,8 +23,8 @@
             android:layout_marginTop="15dp"
             android:background="@drawable/common_btn_red_bg"
             android:text="@string/new_device"
-            android:textSize="@dimen/common_text_size"
-            android:visibility="invisible" />
+            android:textSize="@dimen/item_device_text_size"
+            android:visibility="visible" />
     </RelativeLayout>
 
 </layout>

+ 2 - 1
app/src/main/res/layout/item_job_manage.xml

@@ -10,7 +10,7 @@
         <CheckBox
             android:id="@+id/select"
             android:layout_width="30dp"
-            android:layout_height="30dp"
+            android:layout_height="wrap_content"
             android:layout_gravity="center"
             android:layout_margin="@dimen/common_spacing" />
 
@@ -21,6 +21,7 @@
             android:layout_weight="1"
             android:ellipsize="end"
             android:gravity="center"
+            android:paddingVertical="@dimen/common_spacing"
             android:singleLine="true"
             android:textSize="@dimen/common_text_size" />
 

BIN
app/src/main/res/mipmap-xhdpi/icon_bottom_menu_data_manage.png


BIN
app/src/main/res/mipmap-xhdpi/icon_bottom_menu_exception_manage.png


BIN
app/src/main/res/mipmap-xhdpi/icon_bottom_menu_hardware_manage.png


BIN
app/src/main/res/mipmap-xhdpi/icon_bottom_menu_home.png


BIN
app/src/main/res/mipmap-xhdpi/icon_bottom_menu_job_manage.png


+ 7 - 0
app/src/main/res/values-en/strings.xml

@@ -325,5 +325,12 @@
     <string name="update_point_succeed">Update point success</string>
     <string name="update_point_failed">Update point failed</string>
     <string name="please_input_key_word">Please input keyword</string>
+    <string name="colock_complete">Colock complete</string>
+    <string name="colock_failed">Colock failed</string>
+    <string name="uncolock_failed">Uncolock failed</string>
+    <string name="uncolock_complete">Uncolock complete</string>
+    <string name="invalid_card">Invalid card</string>
+    <string name="invalid_user">Invalid User</string>
+    <string name="currently_unable_to_lock_together">Currently unable to colock or uncolock</string>
 
 </resources>

+ 5 - 1
app/src/main/res/values-land/dimens.xml

@@ -49,7 +49,7 @@
     <dimen name="init_set_admin_account_et_height">51dp</dimen>
     <dimen name="init_key_iv_width">50dp</dimen>
     <dimen name="init_key_iv_height">35dp</dimen>
-    <dimen name="init_lock_iv_width">20dp</dimen>
+    <dimen name="init_lock_iv_width">30dp</dimen>
     <dimen name="init_lock_iv_height">70dp</dimen>
     <dimen name="login_method_item_layout_width">341dp</dimen>
     <dimen name="login_method_item_layout_height">384dp</dimen>
@@ -78,4 +78,8 @@
     <dimen name="step_item_height">306dp</dimen>
     <dimen name="step_item_icon_size">119dp</dimen>
     <dimen name="step_item_index_size">51dp</dimen>
+    <dimen name="home_bottom_nav_ball_size">8dp</dimen>
+    <dimen name="home_bottom_nav_corner_radius">10dp</dimen>
+    <dimen name="home_bottom_nav_notch_height">28dp</dimen>
+    <dimen name="item_device_text_size">12sp</dimen>
 </resources>

+ 3 - 0
app/src/main/res/values-night/dimens.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>

+ 7 - 0
app/src/main/res/values-zh/strings.xml

@@ -325,5 +325,12 @@
     <string name="update_point_succeed">保存点位成功</string>
     <string name="update_point_failed">保存点位失败</string>
     <string name="please_input_key_word">请输入关键字</string>
+    <string name="colock_complete">添加共锁完成</string>
+    <string name="colock_failed">添加共锁失败</string>
+    <string name="uncolock_failed">解除共锁失败</string>
+    <string name="uncolock_complete">解除共锁成功</string>
+    <string name="invalid_card">卡片无效</string>
+    <string name="invalid_user">用户不存在</string>
+    <string name="currently_unable_to_lock_together">当前阶段无法共锁</string>
 
 </resources>

+ 5 - 1
app/src/main/res/values/dimens.xml

@@ -49,7 +49,7 @@
     <dimen name="init_set_admin_account_et_height">30dp</dimen>
     <dimen name="init_key_iv_width">50dp</dimen>
     <dimen name="init_key_iv_height">35dp</dimen>
-    <dimen name="init_lock_iv_width">20dp</dimen>
+    <dimen name="init_lock_iv_width">30dp</dimen>
     <dimen name="init_lock_iv_height">70dp</dimen>
     <dimen name="login_method_item_layout_width">201dp</dimen>
     <dimen name="login_method_item_layout_height">226dp</dimen>
@@ -78,4 +78,8 @@
     <dimen name="step_item_height">180dp</dimen>
     <dimen name="step_item_icon_size">70dp</dimen>
     <dimen name="step_item_index_size">30dp</dimen>
+    <dimen name="home_bottom_nav_ball_size">8dp</dimen>
+    <dimen name="home_bottom_nav_corner_radius">30dp</dimen>
+    <dimen name="home_bottom_nav_notch_height">28dp</dimen>
+    <dimen name="item_device_text_size">12sp</dimen>
 </resources>

+ 7 - 0
app/src/main/res/values/strings.xml

@@ -328,5 +328,12 @@
     <string name="update_point_succeed">保存点位成功</string>
     <string name="update_point_failed">保存点位失败</string>
     <string name="please_input_key_word">请输入关键字</string>
+    <string name="colock_complete">添加共锁完成</string>
+    <string name="colock_failed">添加共锁失败</string>
+    <string name="uncolock_failed">解除共锁失败</string>
+    <string name="uncolock_complete">解除共锁成功</string>
+    <string name="invalid_card">卡片无效</string>
+    <string name="invalid_user">用户不存在</string>
+    <string name="currently_unable_to_lock_together">当前阶段无法共锁</string>
 
 </resources>

+ 6 - 0
data/src/main/java/com/grkj/data/dao/HardwareDao.kt

@@ -395,4 +395,10 @@ interface HardwareDao {
      */
     @Query("select * from is_lock where lock_id in (:lockIds)")
     fun getLockDataByLockIds(lockIds: List<Long>): List<IsLock>
+
+    /**
+     * 根据卡片rfid获取用户id
+     */
+    @Query("select user_id from is_job_card ijc where ijc.card_nfc = :cardRfid")
+    fun getUserIdByCardRfid(cardRfid: String): Long?
 }

+ 1 - 1
data/src/main/java/com/grkj/data/dao/JobTicketDao.kt

@@ -210,7 +210,7 @@ interface JobTicketDao {
      * 根据作业id获取作业详细数据
      */
     @Query("select * from is_job_ticket where ticket_id = :ticketId")
-    fun getJobTicketDataByTicketId(ticketId: Long): IsJobTicketDataVo
+    fun getJobTicketDataByTicketId(ticketId: Long): IsJobTicketDataVo?
 
     /**
      * 根据作业id获取作业钥匙详细数据

+ 3 - 1
data/src/main/java/com/grkj/data/enums/LockStepEnum.kt

@@ -8,5 +8,7 @@ enum class LockStepEnum(val type: Int, val description: String) {
     LOCK(1, "上锁"),
     COLOCK(2, "共锁"),
     UNLOCK(3, "解锁"),
-    UNLOCKED(4, "已解锁");
+    UNLOCKED(4, "已解锁"),
+    LOCKED(5, "已上锁")
+    ;
 }

+ 6 - 1
data/src/main/java/com/grkj/data/repository/IJobTicketRepository.kt

@@ -83,7 +83,7 @@ interface IJobTicketRepository {
     /**
      * 根据作业id获取作业详细数据
      */
-    fun getJobTicketDataByTicketId(ticketId: Long): IsJobTicketDataVo
+    fun getJobTicketDataByTicketId(ticketId: Long): IsJobTicketDataVo?
 
     /**
      * 根据作业id获取作业钥匙详细数据
@@ -194,4 +194,9 @@ interface IJobTicketRepository {
      * 更新步骤数据
      */
     fun updateTicketStepData(step: IsJobTicketStep)
+
+    /**
+     * 根据卡片id获取用户id
+     */
+    fun getUserIdByCardRfid(rfid: String): Long?
 }

+ 1 - 1
data/src/main/java/com/grkj/data/repository/impl/HardwareRepository.kt

@@ -117,7 +117,7 @@ class HardwareRepository @Inject constructor(
         callback: (Boolean) -> Unit
     ) {
         lockTakeList.forEach { lockTakeInfo ->
-            logger.info("取出的挂锁信息:${lockTakeInfo}")
+            logger.debug("取出的挂锁信息:${lockTakeInfo}")
             lockTakeInfo.lockNfc?.let {
                 val lockInfo = getLockInfo(it)
                 lockInfo?.lockId?.let { lockId ->

+ 9 - 1
data/src/main/java/com/grkj/data/repository/impl/JobTicketRepository.kt

@@ -192,6 +192,10 @@ class JobTicketRepository @Inject constructor(
         callback: (TicketDetailRes?) -> Unit
     ) {
         val ticketData = getJobTicketDataByTicketId(ticketId)
+        if (ticketData == null) {
+            callback(null)
+            return
+        }
         val ticketDetailRes = BeanUtils.copyProperties(ticketData, TicketDetailRes::class.java)
         val ticketKeyData = getJobTicketKeyDataByTicketId(ticketId)
         val jobTicketKeyDataList =
@@ -285,7 +289,7 @@ class JobTicketRepository @Inject constructor(
         return jobTicketDao.getTicketPointsByTicketId(ticketId)
     }
 
-    override fun getJobTicketDataByTicketId(ticketId: Long): IsJobTicketDataVo {
+    override fun getJobTicketDataByTicketId(ticketId: Long): IsJobTicketDataVo? {
         return jobTicketDao.getJobTicketDataByTicketId(ticketId)
     }
 
@@ -492,6 +496,10 @@ class JobTicketRepository @Inject constructor(
         jobTicketDao.updateTicketStepData(step)
     }
 
+    override fun getUserIdByCardRfid(rfid: String): Long? {
+        return hardwareDao.getUserIdByCardRfid(rfid)
+    }
+
     override fun updateKeyReturn(
         ticketId: Long,
         keyNfc: String,

+ 124 - 0
shared/src/main/java/com/grkj/shared/utils/CRC16.kt

@@ -0,0 +1,124 @@
+package com.grkj.shared.utils
+
+object CRC16 {
+    private val auchCRCHi = byteArrayOf(
+        0x00, 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte()
+    )
+
+    private val auchCRCLo = byteArrayOf(
+        0x00.toByte(), 0xC0.toByte(), 0xC1.toByte(),
+        0x01.toByte(), 0xC3.toByte(), 0x03.toByte(), 0x02.toByte(), 0xC2.toByte(),
+        0xC6.toByte(), 0x06.toByte(), 0x07.toByte(), 0xC7.toByte(), 0x05.toByte(),
+        0xC5.toByte(), 0xC4.toByte(), 0x04.toByte(), 0xCC.toByte(), 0x0C.toByte(),
+        0x0D.toByte(), 0xCD.toByte(), 0x0F.toByte(), 0xCF.toByte(), 0xCE.toByte(),
+        0x0E.toByte(), 0x0A.toByte(), 0xCA.toByte(), 0xCB.toByte(), 0x0B.toByte(),
+        0xC9.toByte(), 0x09.toByte(), 0x08.toByte(), 0xC8.toByte(), 0xD8.toByte(),
+        0x18.toByte(), 0x19.toByte(), 0xD9.toByte(), 0x1B.toByte(), 0xDB.toByte(),
+        0xDA.toByte(), 0x1A.toByte(), 0x1E.toByte(), 0xDE.toByte(), 0xDF.toByte(),
+        0x1F.toByte(), 0xDD.toByte(), 0x1D.toByte(), 0x1C.toByte(), 0xDC.toByte(),
+        0x14.toByte(), 0xD4.toByte(), 0xD5.toByte(), 0x15.toByte(), 0xD7.toByte(),
+        0x17.toByte(), 0x16.toByte(), 0xD6.toByte(), 0xD2.toByte(), 0x12.toByte(),
+        0x13.toByte(), 0xD3.toByte(), 0x11.toByte(), 0xD1.toByte(), 0xD0.toByte(),
+        0x10.toByte(), 0xF0.toByte(), 0x30.toByte(), 0x31.toByte(), 0xF1.toByte(),
+        0x33.toByte(), 0xF3.toByte(), 0xF2.toByte(), 0x32.toByte(), 0x36.toByte(),
+        0xF6.toByte(), 0xF7.toByte(), 0x37.toByte(), 0xF5.toByte(), 0x35.toByte(),
+        0x34.toByte(), 0xF4.toByte(), 0x3C.toByte(), 0xFC.toByte(), 0xFD.toByte(),
+        0x3D.toByte(), 0xFF.toByte(), 0x3F.toByte(), 0x3E.toByte(), 0xFE.toByte(),
+        0xFA.toByte(), 0x3A.toByte(), 0x3B.toByte(), 0xFB.toByte(), 0x39.toByte(),
+        0xF9.toByte(), 0xF8.toByte(), 0x38.toByte(), 0x28.toByte(), 0xE8.toByte(),
+        0xE9.toByte(), 0x29.toByte(), 0xEB.toByte(), 0x2B.toByte(), 0x2A.toByte(),
+        0xEA.toByte(), 0xEE.toByte(), 0x2E.toByte(), 0x2F.toByte(), 0xEF.toByte(),
+        0x2D.toByte(), 0xED.toByte(), 0xEC.toByte(), 0x2C.toByte(), 0xE4.toByte(),
+        0x24.toByte(), 0x25.toByte(), 0xE5.toByte(), 0x27.toByte(), 0xE7.toByte(),
+        0xE6.toByte(), 0x26.toByte(), 0x22.toByte(), 0xE2.toByte(), 0xE3.toByte(),
+        0x23.toByte(), 0xE1.toByte(), 0x21.toByte(), 0x20.toByte(), 0xE0.toByte(),
+        0xA0.toByte(), 0x60.toByte(), 0x61.toByte(), 0xA1.toByte(), 0x63.toByte(),
+        0xA3.toByte(), 0xA2.toByte(), 0x62.toByte(), 0x66.toByte(), 0xA6.toByte(),
+        0xA7.toByte(), 0x67.toByte(), 0xA5.toByte(), 0x65.toByte(), 0x64.toByte(),
+        0xA4.toByte(), 0x6C.toByte(), 0xAC.toByte(), 0xAD.toByte(), 0x6D.toByte(),
+        0xAF.toByte(), 0x6F.toByte(), 0x6E.toByte(), 0xAE.toByte(), 0xAA.toByte(),
+        0x6A.toByte(), 0x6B.toByte(), 0xAB.toByte(), 0x69.toByte(), 0xA9.toByte(),
+        0xA8.toByte(), 0x68.toByte(), 0x78.toByte(), 0xB8.toByte(), 0xB9.toByte(),
+        0x79.toByte(), 0xBB.toByte(), 0x7B.toByte(), 0x7A.toByte(), 0xBA.toByte(),
+        0xBE.toByte(), 0x7E.toByte(), 0x7F.toByte(), 0xBF.toByte(), 0x7D.toByte(),
+        0xBD.toByte(), 0xBC.toByte(), 0x7C.toByte(), 0xB4.toByte(), 0x74.toByte(),
+        0x75.toByte(), 0xB5.toByte(), 0x77.toByte(), 0xB7.toByte(), 0xB6.toByte(),
+        0x76.toByte(), 0x72.toByte(), 0xB2.toByte(), 0xB3.toByte(), 0x73.toByte(),
+        0xB1.toByte(), 0x71.toByte(), 0x70.toByte(), 0xB0.toByte(), 0x50.toByte(),
+        0x90.toByte(), 0x91.toByte(), 0x51.toByte(), 0x93.toByte(), 0x53.toByte(),
+        0x52.toByte(), 0x92.toByte(), 0x96.toByte(), 0x56.toByte(), 0x57.toByte(),
+        0x97.toByte(), 0x55.toByte(), 0x95.toByte(), 0x94.toByte(), 0x54.toByte(),
+        0x9C.toByte(), 0x5C.toByte(), 0x5D.toByte(), 0x9D.toByte(), 0x5F.toByte(),
+        0x9F.toByte(), 0x9E.toByte(), 0x5E.toByte(), 0x5A.toByte(), 0x9A.toByte(),
+        0x9B.toByte(), 0x5B.toByte(), 0x99.toByte(), 0x59.toByte(), 0x58.toByte(),
+        0x98.toByte(), 0x88.toByte(), 0x48.toByte(), 0x49.toByte(), 0x89.toByte(),
+        0x4B.toByte(), 0x8B.toByte(), 0x8A.toByte(), 0x4A.toByte(), 0x4E.toByte(),
+        0x8E.toByte(), 0x8F.toByte(), 0x4F.toByte(), 0x8D.toByte(), 0x4D.toByte(),
+        0x4C.toByte(), 0x8C.toByte(), 0x44.toByte(), 0x84.toByte(), 0x85.toByte(),
+        0x45.toByte(), 0x87.toByte(), 0x47.toByte(), 0x46.toByte(), 0x86.toByte(),
+        0x82.toByte(), 0x42.toByte(), 0x43.toByte(), 0x83.toByte(), 0x41.toByte(),
+        0x81.toByte(), 0x80.toByte(), 0x40.toByte()
+    )
+
+    fun crc16(puchMsg: ByteArray, from: Int, to: Int): Int {
+        var uchCRCHi = 0xFF.toByte()
+        var uchCRCLo = 0xFF.toByte()
+        for (i in from..<to) {
+            val uIndex = (uchCRCHi.toInt() xor puchMsg[i].toInt()) and 0xff
+            uchCRCHi = (uchCRCLo.toInt() xor auchCRCHi[uIndex].toInt()).toByte()
+            uchCRCLo = auchCRCLo[uIndex]
+        }
+        return (((uchCRCHi.toInt()) shl 8 or ((uchCRCLo.toInt()) and 0xff))) and 0xffff
+    }
+}

+ 8 - 3
ui-base/src/main/java/com/grkj/ui_base/utils/extension/ByteArray.kt → shared/src/main/java/com/grkj/shared/utils/extension/ByteArray.kt

@@ -1,7 +1,7 @@
-package com.grkj.ui_base.utils.extension
+package com.grkj.shared.utils.extension
 
 import android.util.Base64
-import com.grkj.ui_base.utils.CRC16
+import com.grkj.shared.utils.CRC16
 import java.io.ByteArrayOutputStream
 import java.lang.Exception
 import kotlin.collections.indices
@@ -43,7 +43,7 @@ fun ByteArray.toHexStrings(space: Boolean = true) : String {
                 out.write(' '.toInt())
             }
         }
-    } catch (e: java.lang.Exception) {
+    } catch (e: Exception) {
         throw IllegalStateException("exception encoding Hex string: " + e.message, e)
     }
     val bytes = out.toByteArray()
@@ -92,6 +92,11 @@ private fun from62(b: Byte): Int {
     throw IllegalArgumentException("$b is not a num char")
 }
 
+fun ByteArray.isPureZero(): Boolean {
+    // 如果数组为空 or 全部元素都是 0,就返回 true
+    return isEmpty() || all { it.toInt() == 0 }
+}
+
 /**
  * 计算 [from, to) 部分的字节做 的 CRC16 校验值
  * @return 两字节的校验值

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/extension/Int.kt → shared/src/main/java/com/grkj/shared/utils/extension/Int.kt

@@ -1,4 +1,4 @@
-package com.grkj.ui_base.utils.extension
+package com.grkj.shared.utils.extension
 
 import kotlin.ranges.until
 

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/extension/Long.kt → shared/src/main/java/com/grkj/shared/utils/extension/Long.kt

@@ -1,4 +1,4 @@
-package com.grkj.ui_base.utils.extension
+package com.grkj.shared.utils.extension
 
 import java.nio.ByteBuffer
 import java.nio.ByteOrder

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/extension/String.kt → shared/src/main/java/com/grkj/shared/utils/extension/String.kt

@@ -1,4 +1,4 @@
-package com.grkj.ui_base.utils.extension
+package com.grkj.shared.utils.extension
 
 import kotlin.collections.copyOf
 import kotlin.takeIf

+ 1 - 0
ui-base/build.gradle.kts

@@ -61,6 +61,7 @@ dependencies {
     api(libs.sik.extension.android)
     api(libs.dialogx)
     api(libs.fastble)
+    api("androidx.palette:palette-ktx:1.0.0")
     api("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
     api("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
     api("io.github.scwang90:refresh-footer-classics:3.0.0-alpha")

+ 7 - 8
ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt

@@ -11,8 +11,6 @@ import androidx.databinding.DataBindingUtil
 import androidx.databinding.ViewDataBinding
 import androidx.navigation.NavController
 import androidx.navigation.fragment.NavHostFragment
-import com.google.android.material.bottomnavigation.BottomNavigationView
-import com.google.android.material.navigation.NavigationBarView
 import com.grkj.shared.model.EventBean
 import com.grkj.data.data.EventConstants
 import com.grkj.shared.utils.KeyboardUtils
@@ -21,6 +19,7 @@ import com.grkj.ui_base.dialog.LoadingDialog
 import com.grkj.ui_base.utils.event.JumpViewEvent
 import com.grkj.ui_base.utils.event.LoadingEvent
 import com.grkj.ui_base.utils.extension.checkPermissions
+import com.grkj.ui_base.widget.CustomNavBar
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikandroid.permission.PermissionUtils
 import me.jessyan.autosize.internal.CustomAdapt
@@ -35,7 +34,7 @@ import org.slf4j.LoggerFactory
  */
 abstract class BaseActivity<V : ViewDataBinding> : AppCompatActivity(), CustomAdapt {
     protected val logger: Logger = LoggerFactory.getLogger(this::class.java)
-    private var bottomNav: NavigationBarView? = null
+    private var navBar: CustomNavBar? = null
     private var graphMap: Map<Int, Int>? = null
     protected lateinit var binding: V
 
@@ -102,12 +101,12 @@ abstract class BaseActivity<V : ViewDataBinding> : AppCompatActivity(), CustomAd
 
     /** 配置 BottomNavigation 切换 Graph,map: menuItemId -> navGraphId */
     protected fun setupBottomNavigation(
-        bottomNav: NavigationBarView,
+        navBar: CustomNavBar,
         graphMap: Map<Int, Int>
     ) {
-        this.bottomNav = bottomNav
+        this.navBar = navBar
         this.graphMap = graphMap
-        bottomNav.setOnItemSelectedListener { item ->
+        navBar.setOnItemSelectedListener { item ->
             graphMap[item.itemId]?.let {
                 replaceNavGraph(it)
                 true
@@ -132,8 +131,8 @@ abstract class BaseActivity<V : ViewDataBinding> : AppCompatActivity(), CustomAd
             EventConstants.EVENT_JUMP_TO -> {
                 (event.data as JumpViewEvent).apply {
                     graphMap?.filter { it.value == navGraphId }?.firstNotNullOf {
-                        bottomNav?.menu?.findItem(it.key)?.let {
-                            bottomNav?.selectedItemId = it.itemId
+                        navBar?.menu?.findItem(it.key)?.let {
+                            navBar?.selectedItemId = it.itemId
                             navController.navigate(targetId)
                         }
                     }

+ 121 - 0
ui-base/src/main/java/com/grkj/ui_base/base/BaseFormFragment.kt

@@ -0,0 +1,121 @@
+package com.grkj.ui_base.base
+
+import android.view.MotionEvent
+import android.view.View
+import android.widget.CompoundButton
+import android.widget.EditText
+import android.widget.SeekBar
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.widget.addTextChangedListener
+import androidx.databinding.ViewDataBinding
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.grkj.ui_base.R
+import com.grkj.ui_base.dialog.TipDialog
+import com.grkj.ui_base.utils.CommonUtils
+
+/**
+ * 表单基类
+ */
+abstract class BaseFormFragment<V : ViewDataBinding> : BaseFragment<V>() {
+
+    /** 子类 override:要监控的任意对象列表(View、List、Map、Pojo…) */
+    protected open val needWatchObject: List<Any?>
+        get() = emptyList()
+
+    /** 首次进入时的快照 */
+    private var originalValues: List<Any?> = emptyList()
+
+    override fun initListeners() {
+        super.initListeners()
+        recordOriginalValues()
+        watchObjectViews()
+    }
+
+    /** 记录所有对象的“原始值”快照 */
+    private fun recordOriginalValues() {
+        originalValues = needWatchObject.map { getObjectValue(it) }
+    }
+
+    /** 给 needWatchObject 中的 View 注册监听,交互时马上 onViewDirty */
+    private fun watchObjectViews() {
+        needWatchObject.forEach { obj ->
+            if (obj is View) {
+                when (obj) {
+                    is EditText -> obj.addTextChangedListener { onViewDirty(obj) }
+                    else -> {
+                        // 仅拦截触摸,不消费事件,保留原有点击/选中逻辑
+                        obj.setOnTouchListener { v, ev ->
+                            if (ev.action == MotionEvent.ACTION_UP) onViewDirty(v)
+                            false
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 根据对象类型提取“当前值”:
+     * View → 文本/选中/进度…
+     * RecyclerView → adapter.currentList
+     * List/Map → toList()/toMap()
+     * 其它 → 原样返回(Pojo, Int, Boolean…)
+     */
+    @Suppress("UNCHECKED_CAST")
+    protected open fun getObjectValue(obj: Any?): Any? = when (obj) {
+        is EditText -> obj.text.toString()
+        is TextView -> obj.text.toString()
+        is CompoundButton -> obj.isChecked
+        is Spinner -> obj.selectedItemPosition
+        is SeekBar -> obj.progress
+        is RecyclerView ->
+            (obj.adapter as? ListAdapter<Any, *>)?.currentList?.toList()
+
+        is List<*> -> obj.toList()
+        is Map<*, *> -> (obj as Map<Any?, Any?>).toMap()
+        else -> obj
+    }
+
+    /** 实时判断脏:只要任何一个值前后不等,就认为改动过 */
+    protected val isFormDirty: Boolean
+        get() = needWatchObject.indices.any { idx ->
+            originalValues.getOrNull(idx) != getObjectValue(needWatchObject[idx])
+        }
+
+    /** 手动重置“干净”状态(保存成功后调用) */
+    protected fun resetFormDirty() {
+        recordOriginalValues()
+    }
+
+    /** 交互后立刻高亮并标记脏 */
+    private fun onViewDirty(view: View) {
+        // 只标记一次
+        if (!isFormDirty) {
+            // 高亮:仅示例,按你项目逻辑改
+            view.background?.let {
+                DrawableCompat.setTint(it.mutate(), resolveDirtyTintColor())
+            }
+        }
+    }
+
+    protected open fun resolveDirtyTintColor(): Int {
+        return CommonUtils.getColor(R.color.dirtyColor)
+    }
+
+    protected fun showUnsavedConfirmDialog(
+        onConfirm: () -> Unit = {
+            navController.popBackStack()
+        }
+    ) {
+        TipDialog.show(
+            title = CommonUtils.getStr(R.string.action_hint).toString(),
+            msg = CommonUtils.getStr(R.string.not_save_tip).toString(),
+            dialogType = TipDialog.DialogType.ERROR,
+            countDownTime = 10,
+            onConfirmClick = onConfirm
+        )
+    }
+}

+ 55 - 19
ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt

@@ -4,7 +4,13 @@ import com.clj.fastble.BleManager
 import com.clj.fastble.data.BleDevice
 import com.clj.fastble.exception.BleException
 import com.google.gson.Gson
+import com.grkj.data.data.DictConstants
+import com.grkj.data.data.MainDomainData
 import com.grkj.data.di.RepositoryManager
+import com.grkj.data.enums.LockModeEnum
+import com.grkj.data.enums.LockStepEnum
+import com.grkj.data.enums.RoleEnum
+import com.grkj.data.model.dos.IsJobTicketStep
 import com.grkj.data.model.local.DeviceTakeUpdate
 import com.grkj.data.model.local.UpdateKeyReturn
 import com.grkj.data.model.local.WorkTicketGet
@@ -13,11 +19,6 @@ import com.grkj.data.model.local.WorkTicketSend.LockListBO
 import com.grkj.data.model.req.LockPointUpdateReq
 import com.grkj.data.model.res.TicketDetailRes
 import com.grkj.ui_base.R
-import com.grkj.data.data.DictConstants
-import com.grkj.data.data.MainDomainData
-import com.grkj.data.enums.LockStepEnum
-import com.grkj.data.enums.RoleEnum
-import com.grkj.data.model.dos.IsJobTicketStep
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.Executor
@@ -33,7 +34,7 @@ import com.grkj.ui_base.utils.event.LoadingEvent
 import com.grkj.ui_base.utils.event.TicketFinishedEvent
 import com.grkj.ui_base.utils.event.UpdateTicketProgressEvent
 import com.grkj.ui_base.utils.extension.serialNo
-import com.grkj.ui_base.utils.extension.startsWith
+import com.grkj.shared.utils.extension.startsWith
 import com.grkj.ui_base.utils.modbus.DeviceConst
 import com.grkj.ui_base.utils.modbus.ModBusController
 import com.kongzue.dialogx.dialogs.PopTip
@@ -44,7 +45,6 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
 import kotlinx.coroutines.withContext
 import org.slf4j.LoggerFactory
-import kotlin.math.log
 
 /**
  * 蓝牙业务
@@ -90,10 +90,11 @@ object BleBusinessManager {
             byteArray.startsWith(BleConst.RSP_WORK_TICKET_RESULT) && byteArray[3] == 0x02.toByte() -> handleTicketStatus(
                 bleBean.bleDevice, byteArray, isNeedLoading
             )
-
+            //电池电量返回
             byteArray.startsWith(BleConst.RSP_POWER_STATUS) -> {
                 val power = byteArray[4].toInt()
                 ModBusController.updateKeyPower(power, bleBean.bleDevice.mac)
+                logger.info("电量:${power}")
                 if (power < 50) {//如果电量小于50就打开仓位充电
                     ModBusController.controlKeyCharge(true, bleBean.bleDevice.mac) {
                         logger.info("钥匙: ${bleBean.bleDevice.mac} 开始充电")
@@ -271,7 +272,10 @@ object BleBusinessManager {
                     PopTip.tip(R.string.send_ticket_fail)
                     logger.error("Send ticket fail")
                     ModBusController.getKeyByMac(bleBean.bleDevice.mac)?.let { itKey ->
-                        ModbusBusinessManager.mDeviceTakeList.removeIf { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.nfc == itKey.rfid }
+                        ModbusBusinessManager.removeDeviceTake(
+                            DeviceConst.DEVICE_TYPE_KEY,
+                            itKey.rfid
+                        )
                     }
                 }
             }
@@ -352,9 +356,18 @@ object BleBusinessManager {
                 // 根据情况看是否需要下发工作票
                 ModBusController.getKeyByMac(currentModeEvent.bleBean.bleDevice.mac)?.let { key ->
                     // 判断是否有待取的钥匙
-                    val updateBo =
-                        ModbusBusinessManager.mDeviceTakeList.find { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && key.rfid == it.nfc }
-                    logger.info("是否有代取的钥匙:${updateBo}")
+                    val updateBo = ModbusBusinessManager.getWaitTakeDeviceByRfid(
+                        DeviceConst.DEVICE_TYPE_KEY,
+                        key.rfid.toString()
+                    )
+                    if (ModbusBusinessManager.hasAnyDeviceWaitTakeByTicketId(
+                            DeviceConst.DEVICE_TYPE_LOCK,
+                            updateBo?.ticketId
+                        )
+                    ) {
+                        //todo 如果有钥匙待取但是对应的作业票的锁还有的,就不发
+                        return
+                    }
                     updateBo?.let { itBO ->
                         ThreadUtils.runOnIO {
                             RepositoryManager.jobTicketRepo.getStepDetail(itBO.ticketId) {
@@ -404,8 +417,8 @@ object BleBusinessManager {
                             false, currentModeEvent.bleBean.bleDevice.mac
                         )
                         LoadingEvent.sendLoadingEvent()
-                        //连上之后没有工作票要下发就断开
-                        if (BleConnectionManager.hasConnectWait()) {
+                        //连上之后没有工作票要下发就断开 看是否还有设备等待连接并且连接数是否大于等于预期,没有就不断开,有就让路,一般是初始化的时候
+                        if (BleConnectionManager.hasConnectWait() && BleManager.getInstance().allConnectedDevice.size >= BleConst.MAX_KEY_CONNECT_COUNT) {
                             BleManager.getInstance().disconnect(currentModeEvent.bleBean.bleDevice)
                         }
                     }
@@ -511,7 +524,6 @@ object BleBusinessManager {
                 return@handleTicketStatus
             }
 
-
             // 判断WorkTicketGet里是否有未完成的
             ThreadUtils.runOnIO {
                 val finishedStatus = workTicketGet.hasFinished()
@@ -519,7 +531,6 @@ object BleBusinessManager {
                 if (finishedStatus.first) {
                     Executor.delayOnIO(500) {
                         handleKeyReturn(bleDevice, workTicketGet, finishedStatus.second)
-                        //是否存在代发工作票
                         ModbusBusinessManager.checkTicketAndSendTicket(bleDevice.mac)
                     }
                 } else {
@@ -544,6 +555,7 @@ object BleBusinessManager {
 
     /**
      * 处理钥匙归还
+     * todo 自定义作业流程需要修改
      */
     private fun handleKeyReturn(
         bleDevice: BleDevice,
@@ -604,10 +616,31 @@ object BleBusinessManager {
                                 }
                                 PopTip.tip(R.string.key_return_success)
                             } else {
-                                //更新作业票的状态,如果是上锁就更新到共锁,如果是解锁就更新到解锁之后的步骤
+                                //更新作业票的状态
+                                val jobTicketData =
+                                    RepositoryManager.jobTicketRepo.getTicketDataByTicketId(data.taskCode?.toLong()!!)
                                 RepositoryManager.jobTicketRepo.updateTicketDataStatus(
                                     data.taskCode?.toLong()!!,
-                                    if (data.dataList?.any { it.status == 0 } == true) LockStepEnum.COLOCK.type else LockStepEnum.UNLOCKED.type
+                                    if (data.dataList?.any { it.status == 0 } == true) {
+                                        //如果是解锁优先就更新到已上锁,否则更新如果有共锁更新到共锁,不然就更新到解锁
+                                        jobTicketData?.lockMode?.let {
+                                            if (LockModeEnum.isUnLockFirst(it)) {
+                                                LockStepEnum.LOCKED.type
+                                            } else if (it.contains(LockStepEnum.COLOCK.type.toString())) {
+                                                LockStepEnum.COLOCK.type
+                                            } else {
+                                                LockStepEnum.UNLOCKED.type
+                                            }
+                                        } ?: LockStepEnum.COLOCK.type
+                                    } else {
+                                        jobTicketData?.let {
+                                            if (LockModeEnum.isUnLockFirst(it.lockMode.toString())) {
+                                                LockStepEnum.LOCK.type
+                                            } else {
+                                                LockStepEnum.COLOCK.type
+                                            }
+                                        } ?: LockStepEnum.UNLOCKED.type
+                                    }
                                 )
                                 val ticketStepDataVo =
                                     RepositoryManager.jobTicketRepo.getJobTicketStepDataByTicketId(
@@ -627,6 +660,10 @@ object BleBusinessManager {
                                     logger.info("刷新界面:${it}")
                                     UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(it)
                                 }
+                                ModbusBusinessManager.removeDeviceTake(
+                                    DeviceConst.DEVICE_TYPE_KEY,
+                                    keyNfc
+                                )
                                 // 确认归还,切换为待机模式
                                 switchReadyMode(bleDevice)
                                 PopTip.tip(R.string.key_return_success)
@@ -638,7 +675,6 @@ object BleBusinessManager {
                             fun keyReturnErrorConfirm() {
                                 LoadingEvent.sendLoadingEvent()
                                 PopTip.tip(R.string.continue_the_ticket)
-                                BleManager.getInstance().disconnect(bleDevice)
                                 // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
                                 if (workTicketGet.data?.all { it.dataList?.all { it.closed == 1 } == true } == true) {
                                     workTicketGet.data?.firstOrNull()?.taskCode?.toLong()

+ 19 - 8
ui-base/src/main/java/com/grkj/ui_base/business/ModbusBusinessManager.kt

@@ -17,9 +17,9 @@ import com.grkj.ui_base.utils.ble.BleConst
 import com.grkj.ui_base.utils.event.DeviceTakeUpdateEvent
 import com.grkj.ui_base.utils.event.LoadingEvent
 import com.grkj.ui_base.utils.event.UpdateTicketProgressEvent
-import com.grkj.ui_base.utils.extension.removeLeadingZeros
+import com.grkj.shared.utils.extension.removeLeadingZeros
 import com.grkj.ui_base.utils.extension.serialNo
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.toHexStrings
 import com.grkj.ui_base.utils.modbus.DeviceConst
 import com.grkj.ui_base.utils.modbus.DockBean
 import com.grkj.ui_base.utils.modbus.ModBusController
@@ -30,7 +30,6 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
 import kotlinx.coroutines.withContext
 import org.slf4j.LoggerFactory
-import kotlin.math.log
 import kotlin.plus
 
 /**
@@ -41,7 +40,7 @@ object ModbusBusinessManager {
 
     // 设备待取列表(需要报给后台的列表,等实际取完再上报)
     @JvmStatic
-    val mDeviceTakeList = mutableListOf<DeviceTakeUpdate>()
+    private val mDeviceTakeList = mutableListOf<DeviceTakeUpdate>()
 
     private val listeners = ArrayList<DeviceListener>()
 
@@ -97,6 +96,20 @@ object ModbusBusinessManager {
         mDeviceTakeList.removeIf { it.deviceType == deviceType && it.nfc == nfc }
     }
 
+    /**
+     * 获取待取设备
+     */
+    fun getWaitTakeDeviceByRfid(deviceType: Int, rfid: String): DeviceTakeUpdate? {
+        return mDeviceTakeList.find { it.deviceType == deviceType && rfid == it.nfc }
+    }
+
+    /**
+     * 根据作业票获取是否有指定设备未取
+     */
+    fun hasAnyDeviceWaitTakeByTicketId(deviceType: Int, ticketId: Long?): Boolean {
+        return ModbusBusinessManager.mDeviceTakeList.any { it.deviceType == deviceType && it.ticketId == ticketId }
+    }
+
     /**
      * 处理设备取出
      */
@@ -113,7 +126,6 @@ object ModbusBusinessManager {
                             info.ticketId, info.nfc, SIKCore.getApplication().serialNo()!!
                         ) { isSuccess ->
                             if (isSuccess) {
-                                mDeviceTakeList.removeIf { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.nfc == info.nfc }
                                 UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(info.ticketId)
                                 //钥匙取出之后重新再连一把钥匙待机
                                 ModBusController.getKeyByRfid(
@@ -178,7 +190,6 @@ object ModbusBusinessManager {
                                     return@runOnMain
                                 }
                                 // 检查有无当前工作票的钥匙
-                                logger.info("检查有无当前工作票的钥匙:${info.ticketId},${mDeviceTakeList.find { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.ticketId == info.ticketId }}")
                                 mDeviceTakeList.find { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.ticketId == info.ticketId }
                                     ?.let { itKey ->
                                         LoadingEvent.sendLoadingEvent(
@@ -285,7 +296,7 @@ object ModbusBusinessManager {
      * 5、蓝牙数据通讯
      */
     private fun deviceStatusHandle(res: Any) {
-        logger.info("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
+        logger.debug("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
         if (res.isEmpty() || res.any { it.isEmpty() }) {
             var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
             val addressList = mutableListOf<String>()
@@ -300,7 +311,6 @@ object ModbusBusinessManager {
         }
         res.forEachIndexed { index, bytes ->
             val dockBean = ModBusController.updateStatus(bytes) ?: return@forEachIndexed
-            ModBusController.isInitReady = true
             if (MainDomainData.userInfo == null) {
                 return@forEachIndexed
             }
@@ -554,6 +564,7 @@ object ModbusBusinessManager {
     fun checkTicketAndSendTicket(keyMac: String) {
         logger.info("开始检查是否存在作业票")
         val keyBean = ModBusController.getKeyByMac(keyMac)
+        logger.info("待发设备:${mDeviceTakeList}")
         mDeviceTakeList.find { it.nfc == keyBean?.rfid }?.let { itKey ->
             logger.info("存在作业票,下发作业票")
             BleBusinessManager.handleGiveKey(itKey)

+ 10 - 0
ui-base/src/main/java/com/grkj/ui_base/config/ISCSConfig.kt

@@ -12,11 +12,21 @@ object ISCSConfig {
      */
     var isTestMode: Boolean = false
 
+    /**
+     * Debug模式
+     */
+    const val DEBUG: Boolean = true
+
     /**
      * 是否在注册设备
      */
     var isDeviceRegistration: Boolean = false
 
+    /**
+     * 是否可以初始化设备
+     */
+    var canInitDevice: Boolean = false
+
     /**
      * 是否初始化中
      */

+ 0 - 122
ui-base/src/main/java/com/grkj/ui_base/utils/CRC16.java

@@ -1,122 +0,0 @@
-package com.grkj.ui_base.utils;
-
-public class CRC16 {
-
-    private static final byte[] auchCRCHi = { 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
-            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01,
-            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
-            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01,
-            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0,
-            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
-            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
-            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
-            (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01,
-            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
-            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
-            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
-            (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01,
-            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
-            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
-            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
-            (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
-            (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
-            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
-            (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
-            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
-            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
-            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
-            (byte) 0xC1, (byte) 0x81, (byte) 0x40 };
-
-    private static final byte[] auchCRCLo = { (byte) 0x00, (byte) 0xC0, (byte) 0xC1,
-            (byte) 0x01, (byte) 0xC3, (byte) 0x03, (byte) 0x02, (byte) 0xC2,
-            (byte) 0xC6, (byte) 0x06, (byte) 0x07, (byte) 0xC7, (byte) 0x05,
-            (byte) 0xC5, (byte) 0xC4, (byte) 0x04, (byte) 0xCC, (byte) 0x0C,
-            (byte) 0x0D, (byte) 0xCD, (byte) 0x0F, (byte) 0xCF, (byte) 0xCE,
-            (byte) 0x0E, (byte) 0x0A, (byte) 0xCA, (byte) 0xCB, (byte) 0x0B,
-            (byte) 0xC9, (byte) 0x09, (byte) 0x08, (byte) 0xC8, (byte) 0xD8,
-            (byte) 0x18, (byte) 0x19, (byte) 0xD9, (byte) 0x1B, (byte) 0xDB,
-            (byte) 0xDA, (byte) 0x1A, (byte) 0x1E, (byte) 0xDE, (byte) 0xDF,
-            (byte) 0x1F, (byte) 0xDD, (byte) 0x1D, (byte) 0x1C, (byte) 0xDC,
-            (byte) 0x14, (byte) 0xD4, (byte) 0xD5, (byte) 0x15, (byte) 0xD7,
-            (byte) 0x17, (byte) 0x16, (byte) 0xD6, (byte) 0xD2, (byte) 0x12,
-            (byte) 0x13, (byte) 0xD3, (byte) 0x11, (byte) 0xD1, (byte) 0xD0,
-            (byte) 0x10, (byte) 0xF0, (byte) 0x30, (byte) 0x31, (byte) 0xF1,
-            (byte) 0x33, (byte) 0xF3, (byte) 0xF2, (byte) 0x32, (byte) 0x36,
-            (byte) 0xF6, (byte) 0xF7, (byte) 0x37, (byte) 0xF5, (byte) 0x35,
-            (byte) 0x34, (byte) 0xF4, (byte) 0x3C, (byte) 0xFC, (byte) 0xFD,
-            (byte) 0x3D, (byte) 0xFF, (byte) 0x3F, (byte) 0x3E, (byte) 0xFE,
-            (byte) 0xFA, (byte) 0x3A, (byte) 0x3B, (byte) 0xFB, (byte) 0x39,
-            (byte) 0xF9, (byte) 0xF8, (byte) 0x38, (byte) 0x28, (byte) 0xE8,
-            (byte) 0xE9, (byte) 0x29, (byte) 0xEB, (byte) 0x2B, (byte) 0x2A,
-            (byte) 0xEA, (byte) 0xEE, (byte) 0x2E, (byte) 0x2F, (byte) 0xEF,
-            (byte) 0x2D, (byte) 0xED, (byte) 0xEC, (byte) 0x2C, (byte) 0xE4,
-            (byte) 0x24, (byte) 0x25, (byte) 0xE5, (byte) 0x27, (byte) 0xE7,
-            (byte) 0xE6, (byte) 0x26, (byte) 0x22, (byte) 0xE2, (byte) 0xE3,
-            (byte) 0x23, (byte) 0xE1, (byte) 0x21, (byte) 0x20, (byte) 0xE0,
-            (byte) 0xA0, (byte) 0x60, (byte) 0x61, (byte) 0xA1, (byte) 0x63,
-            (byte) 0xA3, (byte) 0xA2, (byte) 0x62, (byte) 0x66, (byte) 0xA6,
-            (byte) 0xA7, (byte) 0x67, (byte) 0xA5, (byte) 0x65, (byte) 0x64,
-            (byte) 0xA4, (byte) 0x6C, (byte) 0xAC, (byte) 0xAD, (byte) 0x6D,
-            (byte) 0xAF, (byte) 0x6F, (byte) 0x6E, (byte) 0xAE, (byte) 0xAA,
-            (byte) 0x6A, (byte) 0x6B, (byte) 0xAB, (byte) 0x69, (byte) 0xA9,
-            (byte) 0xA8, (byte) 0x68, (byte) 0x78, (byte) 0xB8, (byte) 0xB9,
-            (byte) 0x79, (byte) 0xBB, (byte) 0x7B, (byte) 0x7A, (byte) 0xBA,
-            (byte) 0xBE, (byte) 0x7E, (byte) 0x7F, (byte) 0xBF, (byte) 0x7D,
-            (byte) 0xBD, (byte) 0xBC, (byte) 0x7C, (byte) 0xB4, (byte) 0x74,
-            (byte) 0x75, (byte) 0xB5, (byte) 0x77, (byte) 0xB7, (byte) 0xB6,
-            (byte) 0x76, (byte) 0x72, (byte) 0xB2, (byte) 0xB3, (byte) 0x73,
-            (byte) 0xB1, (byte) 0x71, (byte) 0x70, (byte) 0xB0, (byte) 0x50,
-            (byte) 0x90, (byte) 0x91, (byte) 0x51, (byte) 0x93, (byte) 0x53,
-            (byte) 0x52, (byte) 0x92, (byte) 0x96, (byte) 0x56, (byte) 0x57,
-            (byte) 0x97, (byte) 0x55, (byte) 0x95, (byte) 0x94, (byte) 0x54,
-            (byte) 0x9C, (byte) 0x5C, (byte) 0x5D, (byte) 0x9D, (byte) 0x5F,
-            (byte) 0x9F, (byte) 0x9E, (byte) 0x5E, (byte) 0x5A, (byte) 0x9A,
-            (byte) 0x9B, (byte) 0x5B, (byte) 0x99, (byte) 0x59, (byte) 0x58,
-            (byte) 0x98, (byte) 0x88, (byte) 0x48, (byte) 0x49, (byte) 0x89,
-            (byte) 0x4B, (byte) 0x8B, (byte) 0x8A, (byte) 0x4A, (byte) 0x4E,
-            (byte) 0x8E, (byte) 0x8F, (byte) 0x4F, (byte) 0x8D, (byte) 0x4D,
-            (byte) 0x4C, (byte) 0x8C, (byte) 0x44, (byte) 0x84, (byte) 0x85,
-            (byte) 0x45, (byte) 0x87, (byte) 0x47, (byte) 0x46, (byte) 0x86,
-            (byte) 0x82, (byte) 0x42, (byte) 0x43, (byte) 0x83, (byte) 0x41,
-            (byte) 0x81, (byte) 0x80, (byte) 0x40 };
-
-    public static int crc16(byte[] puchMsg, int from, int to) {
-        byte uchCRCHi = (byte) 0xFF;
-        byte uchCRCLo = (byte) 0xFF;
-        for (int i = from; i < to; i++) {
-            int uIndex = (uchCRCHi ^ puchMsg[i]) & 0xff;
-            uchCRCHi = (byte) (uchCRCLo ^ auchCRCHi[uIndex]);
-            uchCRCLo = auchCRCLo[uIndex];
-        }
-        return ((((int) uchCRCHi) << 8 | (((int) uchCRCLo) & 0xff))) & 0xffff;
-    }
-
-}

+ 3 - 3
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleCmdManager.kt

@@ -3,9 +3,9 @@ package com.grkj.ui_base.utils.ble
 import com.clj.fastble.data.BleDevice
 import com.clj.fastble.exception.BleException
 import com.grkj.ui_base.utils.event.GetTicketStatusEvent
-import com.grkj.ui_base.utils.extension.crc16
-import com.grkj.ui_base.utils.extension.toByteArray
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.crc16
+import com.grkj.shared.utils.extension.toByteArray
+import com.grkj.shared.utils.extension.toHexStrings
 import com.sik.sikcore.thread.ThreadUtils
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory

+ 57 - 165
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt

@@ -5,14 +5,12 @@ import com.clj.fastble.BleManager
 import com.clj.fastble.data.BleDevice
 import com.clj.fastble.exception.BleException
 import com.grkj.ui_base.R
-import com.grkj.ui_base.business.BleBusinessManager
-import com.grkj.ui_base.business.DataBusiness
 import com.grkj.ui_base.config.ISCSConfig
-import com.grkj.ui_base.utils.modbus.ModBusController
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.event.LoadingEvent
-import com.grkj.ui_base.utils.extension.startsWith
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.startsWith
+import com.grkj.shared.utils.extension.toHexStrings
+import com.grkj.ui_base.utils.modbus.ModBusController
 import com.sik.sikcore.activity.ActivityTracker
 import com.sik.sikcore.thread.ThreadUtils
 import kotlinx.coroutines.Dispatchers
@@ -64,8 +62,12 @@ object BleConnectionManager {
     /**
      * 蓝牙的通信返回监听
      */
-    @Volatile
-    private var bleIndicateListeners: HashMap<Any, BleIndicateListener> = hashMapOf()
+    @JvmStatic
+    private val bleIndicateListeners: HashMap<Any, BleIndicateListener> by lazy {
+        hashMapOf<Any, BleIndicateListener>().apply {
+            put(this, baseIndicateListeners)
+        }
+    }
 
     /**
      * 基础的蓝牙通信返回监听
@@ -84,11 +86,39 @@ object BleConnectionManager {
                 ) { isSuccess ->
                     if (isSuccess) {
                         prepareDoneCallBack?.invoke(true, bleBean)
+                        //尝试使用命令作为心跳 ,获取token完成之后就要建立心跳了
+                        tryHeartBeatWithCmd(bleBean.bleDevice)
+                    }
+                }
+
+                //电池电量返回
+                byteArray.startsWith(BleConst.RSP_POWER_STATUS) -> {
+                    val power = byteArray[4].toInt()
+                    ModBusController.updateKeyPower(power, bleBean.bleDevice.mac)
+                    logger.info("电量(${bleBean.bleDevice.mac}):${power}")
+                    ThreadUtils.runOnIODelayed(30 * 1000) {
+                        //尝试使用命令作为心跳
+                        tryHeartBeatWithCmd(bleBean.bleDevice)
+                    }
+                    if (power < 50) {//如果电量小于50就打开仓位充电
+                        ModBusController.controlKeyCharge(true, bleBean.bleDevice.mac) {
+                            logger.info("钥匙: ${bleBean.bleDevice.mac} 开始充电")
+                        }
+                    } else {
+                        ModBusController.controlKeyCharge(false, bleBean.bleDevice.mac) {
+                            logger.info("钥匙: ${bleBean.bleDevice.mac} 关闭充电")
+                        }
                     }
                 }
             }
         }
+    }
 
+    /**
+     * 尝试使用命令作为心跳,命令暂未获取电量 没30s发送一次
+     */
+    private fun tryHeartBeatWithCmd(bleDevice: BleDevice) {
+        getBatteryPower(bleDevice, true)
     }
 
     /**
@@ -104,6 +134,12 @@ object BleConnectionManager {
     ) {
         logger.info("蓝牙连接-开始连接 : $mac")
         // 已连接且已获取 token
+        logger.info("蓝牙连接-记录的设备:${deviceList}")
+        logger.info(
+            "蓝牙连接-是否连接:${
+                BleManager.getInstance().isConnected(mac)
+            },记录的设备是否存在:${deviceList.any { it.bleDevice.mac == mac }}"
+        )
         deviceList.find {
             it.bleDevice.mac == mac && BleManager.getInstance().isConnected(mac) && it.token != null
         }?.let { bean ->
@@ -210,7 +246,7 @@ object BleConnectionManager {
                 currentConnectingMac = null
                 if (!isDone) {
                     // 判断是否仍然待连,防止拿走;移到末尾,防止循环影响
-                    if (checkProcess(listener.mac,false)) {
+                    if (checkProcess(listener.mac, false)) {
                         listener.callBack?.invoke(false, null)
                         unregisterConnectListener(listener.mac)
                     }
@@ -236,7 +272,7 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        if (!checkProcess(mac,false)) {
+        if (!checkProcess(mac, false)) {
             logger.error("蓝牙连接-Prepare is canceled : $mac")
             return
         }
@@ -251,7 +287,7 @@ object BleConnectionManager {
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
         logger.info("蓝牙连接-doScanBle:$mac")
-        if (!checkProcess(mac,false)) {
+        if (!checkProcess(mac, false)) {
             logger.error("蓝牙连接-Prepare is canceled : $mac")
             return
         }
@@ -261,7 +297,9 @@ object BleConnectionManager {
                 // 蓝牙未启动重试
                 logger.info("蓝牙连接-参数:${promptStr}")
                 BleManager.getInstance().enableBluetooth()
-                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+                ThreadUtils.runOnIODelayed(5000) {
+                    doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+                }
             }
 
             override fun onScanStarted(success: Boolean) {
@@ -310,7 +348,7 @@ object BleConnectionManager {
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
         logger.info("蓝牙连接-doConnect : ${bleDevice.mac}")
-        if (!checkProcess(bleDevice.mac,false)) {
+        if (!checkProcess(bleDevice.mac, false)) {
             logger.error("蓝牙连接-Prepare is canceled : ${bleDevice.mac}")
             return
         }
@@ -354,7 +392,7 @@ object BleConnectionManager {
                             removeExceptionKey(it.mac)
                             // 设置MTU
                             ThreadUtils.runOnMainDelayed(200) {
-                                if (!checkProcess(bleDevice.mac,false)) {
+                                if (!checkProcess(bleDevice.mac, false)) {
                                     logger.error("Prepare is canceled : ${bleDevice.mac}")
                                     return@runOnMainDelayed
                                 }
@@ -455,16 +493,14 @@ object BleConnectionManager {
     /**
      * 获取电池电量
      */
-    fun getBatteryPower(bleDevice: BleDevice) {
-        logger.info("获取电池电量:${bleDevice.mac}")
+    fun getBatteryPower(bleDevice: BleDevice, isHeartBeat: Boolean = false) {
+        logger.info("获取电池电量:${bleDevice.mac},是否为心跳:${isHeartBeat}")
         BleCmdManager.getPower(bleDevice.mac, object : CustomBleWriteCallback() {
             override fun onWriteSuccess(p0: Int, p1: Int, p2: ByteArray?) {
-                logger.info("发送获取电池电量命令成功:${bleDevice.mac}")
             }
 
             override fun onWriteFailure(p0: BleException?) {
                 ThreadUtils.runOnIODelayed(500) {
-                    logger.info("发送获取电池电量命令失败:${bleDevice.mac}")
                     getBatteryPower(bleDevice)
                 }
             }
@@ -502,7 +538,7 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        if (!checkProcess(bleBean?.bleDevice?.mac,false)) {
+        if (!checkProcess(bleBean?.bleDevice?.mac, false)) {
             logger.error("蓝牙连接-Prepare is canceled : ${bleBean?.bleDevice?.mac}")
             return
         }
@@ -542,7 +578,7 @@ object BleConnectionManager {
 
                     override fun onCharacteristicChanged(data: ByteArray?) {
                         logger.info("蓝牙连接-onCharacteristicChanged : ${data?.toHexStrings()}")
-                        if (bleIndicateListeners.isEmpty) {
+                        if (bleIndicateListeners.isEmpty()) {
                             bleIndicateListeners.put(this, baseIndicateListeners)
                         }
                         data?.let { itData ->
@@ -658,150 +694,6 @@ object BleConnectionManager {
             return@withContext secondTry
         }
 
-    /**
-     * 扫描在线的蓝牙钥匙并发送指令关机
-     */
-    suspend fun scanOnlineKeyLockMacAndSwitchModeToClose(): Boolean {
-        return suspendCancellableCoroutine { parentCont ->
-            BleUtil.instance?.scan(object : CustomBleScanCallback() {
-                override fun onPrompt(promptStr: String?) {
-                    // 蓝牙未启动重试
-                    logger.debug("设备录入-参数:${promptStr}")
-                    BleManager.getInstance().enableBluetooth()
-                    ThreadUtils.runOnMainDelayed(300) {
-                        scanOnlineKeyLockMacAndSwitchModeToClose()
-                    }
-                }
-
-                override fun onScanStarted(success: Boolean) {
-                    logger.debug("设备录入-onScanStarted:${success}")
-                    if (!success) {
-                        ThreadUtils.runOnMainDelayed(300) {
-                            scanOnlineKeyLockMacAndSwitchModeToClose()
-                        }
-                    }
-                }
-
-                override fun onScanning(bleDevice: BleDevice?) {
-                    val mac = bleDevice?.mac ?: return
-                    logger.debug("设备录入-onScanning:$mac")
-                }
-
-                override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
-                    val devicesSnapshot = scanResultList?.toList().orEmpty()
-                    ThreadUtils.runOnIO {
-                        devicesSnapshot.forEach {
-                            val connected =
-                                tryConnectWithOptionalCharge(
-                                    it.mac,
-                                    false
-                                )
-                            if (connected) {
-                                val sendSuccess = sendEmptyTicketJson(it)
-                                logger.debug("设备录入-发送切换工作模式:${it.mac},${sendSuccess}")
-                            }
-                        }
-                        if (parentCont.isActive) {
-                            parentCont.resume(true)
-                        }
-                    }
-                }
-            })
-        }
-    }
-
-    /**
-     * 发送空作业票
-     */
-    private suspend fun sendEmptyTicketJson(bleDevice: BleDevice): Boolean {
-        return suspendCancellableCoroutine<Boolean> { cont ->
-            BleCmdManager.sendWorkTicket(
-                BleBusinessManager.generateEmptyTicketSendJson(),
-                bleDevice = bleDevice,
-                callback = object : CustomBleWriteCallback() {
-                    override fun onWriteSuccess(
-                        current: Int,
-                        total: Int,
-                        justWrite: ByteArray?
-                    ) {
-                        ThreadUtils.runOnIO {
-                            delay(3000)
-                            if (cont.isActive) {
-                                cont.resume(switchWorkMode(bleDevice))
-                            }
-                        }
-                    }
-
-                    override fun onWriteFailure(exception: BleException?) {
-                        ThreadUtils.runOnMainDelayed(300) {
-                            if (cont.isActive) {
-                                cont.resume(sendEmptyTicketJson(bleDevice))
-                            }
-                        }
-                    }
-                })
-        }
-    }
-
-    /**
-     * 切换工作模式
-     */
-    private suspend fun switchWorkMode(bleDevice: BleDevice): Boolean {
-        return suspendCancellableCoroutine<Boolean> { cont ->
-            BleCmdManager.switchMode(
-                BleConst.STATUS_WORK,
-                bleDevice,
-                object : CustomBleWriteCallback() {
-                    override fun onWriteSuccess(
-                        current: Int,
-                        total: Int,
-                        justWrite: ByteArray?
-                    ) {
-                        BleManager.getInstance().disconnect(bleDevice)
-                        ThreadUtils.runOnIO {
-                            delay(800)
-                            if (cont.isActive) {
-                                cont.resume(true)
-                            }
-                        }
-                        logger.debug("设备录入-切换模式发送成功 : ${bleDevice.mac}")
-                    }
-
-                    override fun onWriteFailure(exception: BleException?) {
-                        logger.debug("设备录入-切换模式发送失败 : ${bleDevice.mac}")
-                        ThreadUtils.runOnMainDelayed(300) {
-                            if (cont.isActive) {
-                                cont.resume(sendEmptyTicketJson(bleDevice))
-                            }
-                        }
-                    }
-                })
-        }
-    }
-
-    /**
-     * 切换待机模式
-     */
-    fun switchReadyMode(bleDevice: BleDevice) {
-        BleCmdManager.switchMode(
-            BleConst.STATUS_READY,
-            bleDevice,
-            object : CustomBleWriteCallback() {
-                override fun onWriteSuccess(
-                    current: Int,
-                    total: Int,
-                    justWrite: ByteArray?
-                ) {
-                    BleManager.getInstance().disconnect(bleDevice)
-                    logger.debug("设备录入-切换模式发送成功 : ${bleDevice.mac}")
-                }
-
-                override fun onWriteFailure(exception: BleException?) {
-                    logger.debug("设备录入-切换模式发送失败 : ${bleDevice.mac}")
-                }
-            })
-    }
-
     /**
      * 扫描在线的蓝牙
      */
@@ -811,7 +703,7 @@ object BleConnectionManager {
                 // 蓝牙未启动重试
                 logger.info("设备录入-参数:${promptStr}")
                 BleManager.getInstance().enableBluetooth()
-                ThreadUtils.runOnMainDelayed(300) {
+                ThreadUtils.runOnMainDelayed(5000) {
                     scanOnlineKeyLockMac(existsMac, callback)
                 }
             }
@@ -834,7 +726,7 @@ object BleConnectionManager {
             }
 
             override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
-                logger.info("设备录入-扫描完成:${scanResultList?.joinToString(","){it.mac}}")
+                logger.info("设备录入-扫描完成:${scanResultList?.joinToString(",") { it.mac }}")
                 callback(scanResultList?.find { it.mac !in existsMac })
             }
         })

+ 1 - 2
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleUtil.kt

@@ -10,7 +10,7 @@ import com.clj.fastble.callback.BleMtuChangedCallback
 import com.clj.fastble.data.BleDevice
 import com.clj.fastble.exception.BleException
 import com.clj.fastble.scan.BleScanRuleConfig
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.toHexStrings
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
@@ -58,7 +58,6 @@ class BleUtil private constructor() {
             if (BleManager.getInstance().isBlueEnable) {
                 BleManager.getInstance().scan(bleScanCallback)
             } else {
-                BleManager.getInstance().enableBluetooth()
                 bleScanCallback.onPrompt("请打开您的蓝牙后重试")
             }
         } else {

+ 1 - 2
ui-base/src/main/java/com/grkj/ui_base/utils/extension/Context.kt

@@ -4,7 +4,6 @@ import android.Manifest
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.pm.PackageManager
-import android.content.res.Configuration
 import android.os.Build
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
@@ -55,7 +54,7 @@ fun Context.getAppVersionName(): String {
     return try {
         // Android T+ (API 33+) 推荐用 PackageInfoFlags
         val pkgInfo =
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                 packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
             } else {
                 @Suppress("DEPRECATION")

+ 0 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/extension/DialogXExtension.kt

@@ -1,6 +1,5 @@
 package com.grkj.ui_base.utils.extension
 
-import com.kongzue.dialogx.DialogX
 import com.kongzue.dialogx.dialogs.PopTip
 
 /**

+ 5 - 6
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/DockBean.kt

@@ -50,7 +50,7 @@ class DockBean(
                     val isLeftCharging = (byteArray[4].toInt() shr 1) and 0x1 == 1
                     val rightHasKey = (byteArray[3].toInt() shr 0) and 0x1 == 1
                     val isRightCharging = (byteArray[3].toInt() shr 1) and 0x1 == 1
-                    logger.info("钥匙刷新状态 : $leftHasKey - $isLeftCharging - $rightHasKey - $isRightCharging")
+                    logger.debug("钥匙刷新状态 : $leftHasKey - $isLeftCharging - $rightHasKey - $isRightCharging")
                     if (getKeyList().isEmpty()) {
                         deviceList.add(
                             KeyBean(
@@ -134,7 +134,7 @@ class DockBean(
                         }
                     }
 
-                    logger.info("锁具刷新状态 : $changeList")
+                    logger.debug("锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.addr == addr }?.row?.toInt() ?: 0,
@@ -233,7 +233,6 @@ class DockBean(
 
                 DeviceConst.DOCK_TYPE_COLLECT -> {
                     val working = (byteArray[4].toInt() shr 0) and 0x1 == 1
-                    logger.info("开关量采集板是否工作 : $working")
                     return DockBean(
                         addr,
                         dockConfig.find { it.addr == addr }?.row?.toInt() ?: 0,
@@ -300,7 +299,7 @@ class DockBean(
                         getLockList()[i].lockEnabled = tempList[i]
                     }
 
-                    logger.info("锁具刷新状态 : $changeList")
+                    logger.debug("锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.addr == addr }?.row?.toInt() ?: 0,
@@ -328,7 +327,7 @@ class DockBean(
                         getLockList()[i].lockEnabled = tempList[i]
                     }
 
-                    logger.info("电磁锁具刷新状态 : $changeList")
+                    logger.debug("电磁锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.addr == addr }?.row?.toInt() ?: 0,
@@ -455,7 +454,7 @@ class DockBean(
                         getLockList()[getLockList().size - 2 + i].lockEnabled = tempList[i]
                     }
 
-                    logger.info("锁具刷新状态 : $changeList")
+                    logger.debug("锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.addr == addr }?.row?.toInt() ?: 0,

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/FrameTask.kt

@@ -1,6 +1,6 @@
 package com.grkj.ui_base.utils.modbus
 
-import com.grkj.ui_base.utils.extension.crc16
+import com.grkj.shared.utils.extension.crc16
 
 class FrameTask(
     val req: ByteArray,

+ 1 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/MBFrame.kt

@@ -1,6 +1,6 @@
 package com.grkj.ui_base.utils.modbus
 
-import com.grkj.ui_base.utils.extension.crc16
+import com.grkj.shared.utils.extension.crc16
 
 /**
  * ModBus 数据帧

+ 93 - 61
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusController.kt

@@ -3,14 +3,16 @@ package com.grkj.ui_base.utils.modbus
 import com.clj.fastble.BleManager
 import com.grkj.data.di.RepositoryManager
 import com.grkj.data.model.res.CabinetSlotsRecord
+import com.grkj.shared.utils.extension.isPureZero
 import com.grkj.ui_base.R
 import com.grkj.ui_base.config.ISCSConfig
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.Executor
 import com.grkj.ui_base.utils.ble.BleConnectionManager
 import com.grkj.ui_base.utils.event.ModbusInitCompleteEvent
-import com.grkj.ui_base.utils.extension.removeLeadingZeros
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.removeLeadingZeros
+import com.grkj.shared.utils.extension.toHexStrings
+import com.grkj.ui_base.utils.event.StartModbusEvent
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.thread.ThreadUtils
 import org.slf4j.Logger
@@ -25,11 +27,6 @@ import java.util.stream.Collectors
 object ModBusController {
     private val logger: Logger = LoggerFactory.getLogger(ModBusController::class.java)
 
-    /**
-     * 是否初始化完成
-     */
-    var isInitReady = false
-
     /**
      * 底座列表
      */
@@ -72,23 +69,28 @@ object ModBusController {
     fun start() {
         modBusManager?.stop()
         PortManager.openCtrlBord()?.let { pm ->
-            return@let ModBusManager(pm, true)
-        }
-            // 间隔 1 秒读一遍桶的状态
-            ?.repeatSendToAll(MBFrame.READ_STATUS, {
-                interruptReadStatus
-            }, { res ->
-                logger.info("****************************************************************************")
-                // 过滤非空的数据,重置slaveCount
-                // 不再使用slaveCount,改用地址池
-                for (l in listeners) {
-                    if (l.type == LISTENER_TYPE_STATUS) {
-                        l.listener(res)
-                    }
+            return@let ModBusManager(pm, false)
+        }.also { modBusManager = it }?.start()
+    }
+
+    /**
+     * 开始轮询读取状态
+     */
+    private fun startLoopReadStatus() {
+        logger.info("开始轮询读取状态")
+        // 间隔 1 秒读一遍桶的状态
+        modBusManager?.repeatSendToAll(MBFrame.READ_STATUS, {
+            interruptReadStatus
+        }, { res ->
+            logger.debug("****************************************************************************")
+            // 过滤非空的数据,重置slaveCount
+            // 不再使用slaveCount,改用地址池
+            for (l in listeners) {
+                if (l.type == LISTENER_TYPE_STATUS) {
+                    l.listener(res)
                 }
-            }, REPEAT_FREQUENCY)?.also {
-                modBusManager = it
-            }?.start()
+            }
+        }, REPEAT_FREQUENCY)
     }
 
     /**
@@ -124,34 +126,46 @@ object ModBusController {
      */
     fun initDevicesStatus() {
         logger.info("发送设备类型读取")
-        readDeviceType { res ->
-            logger.info("获取设备类型")
-            res.forEach { bytes ->
-                if (bytes.size < 5) return@forEach
-                // 设备具体数据由0x0011寄存器提供
-                updateDeviceType(bytes[0], bytes[4])
-                val type = when (bytes[4]) {
-                    DeviceConst.DOCK_TYPE_KEY -> "钥匙底座"
-                    DeviceConst.DOCK_TYPE_LOCK -> "锁具底座"
-                    DeviceConst.DOCK_TYPE_ELEC_LOCK_BOARD -> "电磁锁控制板"
-                    DeviceConst.DOCK_TYPE_PORTABLE -> "便携式底座"
-                    DeviceConst.DOCK_TYPE_COLLECT -> "开关量采集板"
-                    else -> "未知"
+        fun readDeviceType() {
+            readDeviceType { res ->
+                logger.info("获取设备类型")
+                if (res.any { it.isPureZero() } || res.size != modBusManager?.mSlaveAddressList?.size) {
+                    readDeviceType()
+                    return@readDeviceType
                 }
-                logger.info("initDevicesStatus 设备(${bytes[0].toInt()})类型:$type")
-            }
-            controlAllKeyBuckleOpen()
-            // TODO 待完善
-            Executor.repeatOnMain({
-                if (isInitReady) {
-                    initLock()    // 打开所有无锁的卡扣、关闭所有有锁的卡扣、读取所有锁的RFID
-                    initKey()     // 打开所有无钥匙的卡扣、关闭所有有钥匙的卡扣、读取所有钥匙的RFID
-                    return@repeatOnMain false
-                } else {
-                    return@repeatOnMain true
+                res.forEach { bytes ->
+                    if (bytes.size < 5) return@forEach
+                    // 设备具体数据由0x0011寄存器提供
+                    updateDeviceType(bytes[0], bytes[4])
+                    val type = when (bytes[4]) {
+                        DeviceConst.DOCK_TYPE_KEY -> "钥匙底座"
+                        DeviceConst.DOCK_TYPE_LOCK -> "锁具底座"
+                        DeviceConst.DOCK_TYPE_ELEC_LOCK_BOARD -> "电磁锁控制板"
+                        DeviceConst.DOCK_TYPE_PORTABLE -> "便携式底座"
+                        DeviceConst.DOCK_TYPE_COLLECT -> "开关量采集板"
+                        else -> "未知"
+                    }
+                    logger.info("initDevicesStatus 设备(${bytes[0].toInt()})类型:$type")
                 }
-            }, REPEAT_FREQUENCY, true)
+                controlAllKeyBuckleOpen()
+                fun initDevice() {
+                    modBusManager?.sendToAll(MBFrame.READ_STATUS) { res ->
+                        listeners.forEach {
+                            it.listener(res)
+                        }
+                        if (res.any { it.isPureZero() }) {
+                            initDevice()
+                            return@sendToAll
+                        }
+                        initLock()    // 打开所有无锁的卡扣、关闭所有有锁的卡扣、读取所有锁的RFID
+                        initKey()     // 打开所有无钥匙的卡扣、关闭所有有钥匙的卡扣、读取所有钥匙的RFID
+                        startLoopReadStatus()
+                    }
+                }
+                initDevice()
+            }
         }
+        readDeviceType()
     }
 
     /**
@@ -223,6 +237,11 @@ object ModBusController {
                                     updateKeyMac(dockBean.addr, key.idx, keyInfo.macAddress!!)
                                     //已经初始化完成才会去连接
                                     if (ISCSConfig.isInit) {
+                                        controlKeyCharge(true, key.idx, dockBean.addr) {
+                                            ThreadUtils.runOnIODelayed(3000) {
+                                                controlKeyCharge(false, key.idx, dockBean.addr)
+                                            }
+                                        }
                                         ThreadUtils.runOnIO {
                                             val isConnect =
                                                 BleConnectionManager.tryConnectWithOptionalCharge(
@@ -236,7 +255,6 @@ object ModBusController {
                                                         BleConnectionManager.getCurrentStatus(
                                                             3, it.bleDevice
                                                         )
-                                                        BleConnectionManager.getBatteryPower(it.bleDevice)
                                                     }
                                                 }
                                             }
@@ -278,7 +296,7 @@ object ModBusController {
     fun updateAllBuckleStatus(done: () -> Unit) {
         val remaining = AtomicInteger(2)
         modBusManager?.sendToAll(MBFrame.READ_BUCKLE_STATUS) { res ->
-            logger.info("****************************************************************************")
+            logger.debug("****************************************************************************")
             // 过滤非空的数据,重置slaveCount
             // 不再使用slaveCount,改用地址池
             lockBuckleStatus(res)
@@ -288,7 +306,7 @@ object ModBusController {
             }
         }
         modBusManager?.sendToAll(MBFrame.READ_LOCK_BUCKLE_EXTRA_STATUS) { res ->
-            logger.info("****************************************************************************")
+            logger.debug("****************************************************************************")
             // 过滤非空的数据,重置slaveCount
             // 不再使用slaveCount,改用地址池
             lockBuckleExtraStatus(res)
@@ -305,7 +323,7 @@ object ModBusController {
     fun updateSwitchStatus(done: () -> Unit) {
         modBusManager?.mSlaveAddressList?.find { it == (0xA1).toByte() }?.let {
             modBusManager?.sendTo(it, MBFrame.READ_BUCKLE_STATUS) { res ->
-                logger.info("****************************************************************************")
+                logger.debug("****************************************************************************")
                 // 过滤非空的数据,重置slaveCount
                 // 不再使用slaveCount,改用地址池
                 switchStatus(res, done)
@@ -317,7 +335,7 @@ object ModBusController {
      * 第9,10锁位卡扣状态
      */
     private fun lockBuckleExtraStatus(res: Any) {
-        logger.info("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
+        logger.debug("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
         if (res.isEmpty() || res.any { it.isEmpty() }) {
             var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
             val addressList = mutableListOf<String>()
@@ -371,7 +389,7 @@ object ModBusController {
      * 第1-8锁位卡扣状态和钥匙
      */
     private fun lockBuckleStatus(res: Any) {
-        logger.info("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
+        logger.info("debug:${(res as List<ByteArray>).map { it.toHexStrings() }}")
         if (res.isEmpty() || res.any { it.isEmpty() }) {
             var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
             val addressList = mutableListOf<String>()
@@ -693,9 +711,19 @@ object ModBusController {
         done: ((res: ByteArray) -> Unit)? = null
     ) {
         slaveAddress?.let {
-            ModBusCMDHelper.generateKeyBuckleCmd(isOpen, idx)?.let { cmd ->
-                modBusManager?.sendTo(it, cmd) { res ->
-                    done?.invoke(res)
+            if (isOpen) {
+                controlKeyCharge(false, idx, slaveAddress) {
+                    ModBusCMDHelper.generateKeyBuckleCmd(isOpen, idx)?.let { cmd ->
+                        modBusManager?.sendTo(slaveAddress, cmd) { res ->
+                            done?.invoke(res)
+                        }
+                    }
+                }
+            } else {
+                ModBusCMDHelper.generateKeyBuckleCmd(isOpen, idx)?.let { cmd ->
+                    modBusManager?.sendTo(it, cmd) { res ->
+                        done?.invoke(res)
+                    }
                 }
             }
         }
@@ -709,7 +737,6 @@ object ModBusController {
             .forEach { dock ->
                 dock.type?.let { dockType ->
                     ModBusCMDHelper.generateAllKeyBuckleOpenCmd(dockType).let { cmd ->
-                        logger.info("硬件:打开所有钥匙锁仓,${dock.addr},${cmd.data}")
                         modBusManager?.sendTo(dock.addr, cmd) { res ->
                         }
                     }
@@ -995,7 +1022,7 @@ object ModBusController {
             dockList.filter { it.type == DeviceConst.DOCK_TYPE_KEY || it.type == DeviceConst.DOCK_TYPE_PORTABLE }
                 .sortedBy { it.addr }.onEach { it.deviceList.sortBy { dev -> dev.idx } }
 
-        val keyList = keyDockList.flatMap { it.deviceList }.apply {
+        var keyList = keyDockList.flatMap { it.deviceList }.apply {
             logger.info("keyStatus:${this}")
         }.filterIsInstance<DockBean.KeyBean>()
             .filterIndexed { idx, _ -> (idx + 1) !in slotCols }.filter { kb ->
@@ -1006,8 +1033,13 @@ object ModBusController {
         if (keyList.isEmpty()) {
             return null
         }
-        keyList.sortedByDescending { it.power }
-            .sortedBy { BleConnectionManager.getBleDeviceByMac(it.mac)?.token != null }
+        keyList = keyList.sortedWith(
+            compareByDescending<DockBean.KeyBean> {
+                BleManager.getInstance().isConnected(it.mac)
+            }    // 主键:在线优先
+                .thenByDescending { BleConnectionManager.getBleDeviceByMac(it.mac)?.token != null } // 次键:有 token 优先
+                .thenByDescending { it.power }                                                // 三级:信号强度越大越前
+        )
 
         for (kb in keyList) {
             val mac = kb.mac ?: continue

+ 3 - 3
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/ModBusManager.kt

@@ -4,7 +4,7 @@ import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import com.grkj.data.data.MMKVConstants
 import com.grkj.ui_base.utils.Executor
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.toHexStrings
 import com.sik.sikcore.extension.getMMKVData
 import kotlinx.coroutines.*
 import kotlinx.coroutines.channels.Channel
@@ -44,7 +44,7 @@ class ModBusManager(
     init {
         // 串口监听,回调在单独线程中执行
         portManager?.listen { res ->
-            if (verbose) logger.info("接收:${res.toHexStrings()}")
+            if (verbose) logger.debug("接收:${res.toHexStrings()}")
             synchronized(lock) {
                 sending?.run {
                     if (match(res) && running) {
@@ -82,7 +82,7 @@ class ModBusManager(
                 if (shouldSend()) {
                     if (portManager?.send(req) == true) {
                         afterSent()
-                        if (verbose) logger.info("发送:${req.toHexStrings()}")
+                        if (verbose) logger.debug("发送:${req.toHexStrings()}")
                     } else {
                         logger.warn("无法与主控板通讯")
                     }

+ 3 - 3
ui-base/src/main/java/com/grkj/ui_base/utils/modbus/PortManager.kt

@@ -3,7 +3,7 @@ package com.grkj.ui_base.utils.modbus
 import androidx.annotation.WorkerThread
 import com.epton.sdk.SerialPort
 import com.grkj.data.data.MMKVConstants
-import com.grkj.ui_base.utils.extension.toHexStrings
+import com.grkj.shared.utils.extension.toHexStrings
 import com.kongzue.dialogx.dialogs.PopTip
 import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.saveMMKVData
@@ -286,7 +286,7 @@ class PortManager private constructor(
             if (!isDetectMode) {
                 return
             }
-            logger.info("发送命令:${cmd.toHexStrings()}")
+            logger.debug("发送命令:${cmd.toHexStrings()}")
             output.write(cmd); output.flush()
 
             val buffer = ByteArray(BUFFER_SIZE)
@@ -295,7 +295,7 @@ class PortManager private constructor(
             if (!isDetectMode) {
                 return
             }
-            logger.info("接受指令:${data.toHexStrings()}")
+            logger.debug("接受指令:${data.toHexStrings()}")
             if (data.size < 5) return
             val type = when (data[4]) {
                 DeviceConst.DOCK_TYPE_KEY -> "钥匙底座"

+ 258 - 0
ui-base/src/main/java/com/grkj/ui_base/widget/CustomNavBar.kt

@@ -0,0 +1,258 @@
+package com.grkj.ui_base.widget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.*
+import android.graphics.drawable.BitmapDrawable
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.annotation.MenuRes
+import androidx.appcompat.view.menu.MenuBuilder
+import androidx.palette.graphics.Palette
+import com.grkj.ui_base.R
+import android.view.LayoutInflater
+import androidx.core.view.size
+import kotlin.math.tan
+
+/**
+ * CustomNavBar: 支持动态菜单、曲线凹槽透明、圆角背景、顶部小球
+ * 支持横向 Bottom 和 纵向 Side 布局,图标+文本居中均分
+ * 图标大小、文字大小、文字颜色可通过 XML 属性配置,文字颜色使用 Palette 提取 Icon 主色
+ */
+class CustomNavBar @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : LinearLayout(context, attrs, defStyle) {
+
+    @SuppressLint("RestrictedApi")
+    private val menuBuilder = MenuBuilder(context)
+    val menu: Menu get() = menuBuilder
+    private var onItemSelected: ((MenuItem) -> Boolean)? = null
+
+    // XML 可配置属性
+    private var backgroundColor: Int = Color.WHITE
+    private var cornerRadius: Float = dp(16)
+    private var notchHeight: Float = dp(32)
+    private var ballDiameter: Float = dp(16)
+    private var navOrientation: Int = 0  // 0: horizontal(bottom),1:vertical(side)
+    private var iconSize: Float = dp(24)
+    private var textSizePx: Float = dp(12)
+
+    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
+    private val clearPaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).apply {
+            xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
+            style = Paint.Style.FILL
+        }
+    private val ballPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
+    private val notchPath = Path()
+
+    private var length = 0
+    private var selectedIdx = 0
+
+    init {
+        setWillNotDraw(false)
+        gravity = Gravity.CENTER
+        orientation = HORIZONTAL
+        context.obtainStyledAttributes(attrs, R.styleable.CustomNavBar, defStyle, 0).apply {
+            backgroundColor = getColor(R.styleable.CustomNavBar_navBackgroundColor, backgroundColor)
+            cornerRadius = getDimension(R.styleable.CustomNavBar_navCornerRadius, cornerRadius)
+            notchHeight = getDimension(R.styleable.CustomNavBar_navNotchHeight, notchHeight)
+            ballDiameter = getDimension(R.styleable.CustomNavBar_navBallDiameter, ballDiameter)
+            navOrientation = getInt(R.styleable.CustomNavBar_navOrientation, 0)
+            iconSize = getDimension(R.styleable.CustomNavBar_navIconSize, iconSize)
+            textSizePx = getDimension(R.styleable.CustomNavBar_navTextSize, textSizePx)
+            recycle()
+        }
+        orientation = if (navOrientation == 0) HORIZONTAL else VERTICAL
+        bgPaint.color = backgroundColor
+        ballPaint.color = backgroundColor
+    }
+
+    fun setOnItemSelectedListener(listener: (MenuItem) -> Boolean) {
+        onItemSelected = listener
+    }
+
+    fun inflateMenu(@MenuRes menuRes: Int) {
+        menuBuilder.clear()
+        MenuInflater(context).inflate(menuRes, menuBuilder)
+        buildItems()
+    }
+
+    @SuppressLint("RestrictedApi")
+    private fun buildItems() {
+        removeAllViews()
+        length = menuBuilder.size()
+        for (i in 0 until length) {
+            val item = menuBuilder.getItem(i)
+            val container = LinearLayout(context).apply {
+                orientation = VERTICAL
+                gravity = Gravity.CENTER
+                layoutParams = if (navOrientation == 0) {
+                    LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
+                } else {
+                    LayoutParams(LayoutParams.WRAP_CONTENT, 0, 1f)
+                }
+                setOnClickListener { if (onItemSelected?.invoke(item) == true) selectIndex(i) }
+            }
+            val iv = ImageView(context).apply {
+                setImageDrawable(item.icon)
+                layoutParams = LayoutParams(iconSize.toInt(), iconSize.toInt())
+            }
+            val tv = TextView(context).apply {
+                text = item.title
+                setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx)
+                layoutParams = LayoutParams(
+                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT
+                ).apply {
+                    if (navOrientation == 0) topMargin = dp(4).toInt() else leftMargin =
+                        dp(4).toInt()
+                }
+            }
+            // 提取 icon 主色调给文字
+            (iv.drawable as? BitmapDrawable)?.bitmap?.let { bmp ->
+                Palette.from(bmp).generate { p ->
+                    p?.vibrantSwatch?.let { tv.setTextColor(it.rgb) }
+                }
+            }
+            container.addView(iv)
+            container.addView(tv)
+            addView(container)
+        }
+        if (length > 0) selectIndex(0)
+    }
+
+    private fun selectIndex(idx: Int) {
+        selectedIdx = idx; invalidate()
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        if (menuBuilder.size() != length) buildItems()
+        val save = canvas.saveLayer(null, null)
+        // 背景
+        canvas.drawRoundRect(
+            RectF(0f, 0f, width.toFloat(), height.toFloat()),
+            cornerRadius,
+            cornerRadius,
+            bgPaint
+        )
+        super.onDraw(canvas)
+        // 凹槽
+        if (length > 0) {
+            val child = getChildAt(selectedIdx)
+            val r = (height - child.height) / 2f
+            if (navOrientation == 0) {
+                val cx = (child.left + child.right) / 2f
+                val startX = child.left.toFloat() + (child.right - child.left - iconSize) / 4
+                val endX = child.right.toFloat() - (child.right - child.left - iconSize) / 4
+                // 计算贝塞尔控制点比例 k = 4/3 * tan(pi/8)
+                val k = (4f / 3f) * tan(Math.PI / 8).toFloat()
+                notchPath.rewind()
+                notchPath.apply {
+                    moveTo(startX, 0f)
+                    // 左侧凸起
+                    cubicTo(
+                        startX + r * k, -r * (1 - k),
+                        startX + r, -r,
+                        startX + r, 0f
+                    )
+                    // 中段凹陷
+                    cubicTo(
+                        cx - r, r,
+                        cx + r, r,
+                        endX - r, 0f
+                    )
+                    // 右侧凸起
+                    cubicTo(
+                        endX - r, -r,
+                        endX - r * k, -r * (1 - k),
+                        endX, 0f
+                    )
+                }
+            } else {
+                // —— 垂直版,右侧挖槽 ——
+                val cy = (child.top + child.bottom) / 2f
+                val startY = child.top + (child.bottom - child.top - iconSize) / 4f
+                val endY = child.bottom - (child.bottom - child.top - iconSize) / 4f
+                val k = (4f / 3f) * tan(Math.PI / 8).toFloat()
+                val r = (width - (0..menuBuilder.size - 1).map { getChildAt(it) }
+                    .maxOf { it.width }) / 2f
+
+                notchPath.rewind()
+                notchPath.apply {
+                    // 在右侧边界,从 startY 开始
+                    moveTo(width.toFloat(), startY)
+
+                    // 上方凸起 (quarter circle outward to the right)
+                    cubicTo(
+                        width + r * (1 - k), startY + r * k,
+                        width + r, startY + r,
+                        width.toFloat(), startY + r
+                    )
+
+                    // 中段凹陷 (half circle inward to the left)
+                    cubicTo(
+                        width - r, cy - r,
+                        width - r, cy + r,
+                        width.toFloat(), endY - r
+                    )
+
+                    // 下方凸起 (quarter circle outward to the right)
+                    cubicTo(
+                        width + r, endY - r,
+                        width + r * (1 - k), endY - r * k,
+                        width.toFloat(), endY
+                    )
+                }
+            }
+            canvas.drawPath(notchPath, clearPaint)
+        }
+        // 小球
+        if (length > 0) {
+            val child = getChildAt(selectedIdx);
+            val cx = (child.left + child.right) / 2f
+            val cy = (child.top + child.bottom) / 2f
+            if (navOrientation == 0) {
+                val r = (height - child.height) / 2f * 0.5f
+                canvas.drawCircle(
+                    cx,
+                    r / 2,
+                    r / 2,
+                    ballPaint
+                )
+            } else {
+                val r = (width - (0..menuBuilder.size - 1).map { getChildAt(it) }
+                    .maxOf { it.width }) / 2f * 0.5f
+                canvas.drawCircle(width - r / 2, cy, r / 2, ballPaint)
+            }
+        }
+        canvas.restoreToCount(save)
+    }
+
+    var selectedItemId: Int
+        @SuppressLint("RestrictedApi")
+        get() = menuBuilder.getItemOrNull(selectedIdx)?.itemId ?: View.NO_ID
+        set(id) {
+            val idx =
+                (0 until menuBuilder.size()).firstOrNull { menuBuilder.getItem(it).itemId == id }
+                    ?: return; selectIndex(idx)
+        }
+
+    private fun dp(dp: Int): Float = TypedValue.applyDimension(
+        TypedValue.COMPLEX_UNIT_DIP,
+        dp.toFloat(),
+        resources.displayMetrics
+    )
+
+    private fun MenuBuilder.getItemOrNull(i: Int): MenuItem? =
+        if (i in 0 until size()) getItem(i) else null
+}

+ 29 - 0
ui-base/src/main/res/layout/item_nav.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:orientation="vertical"
+        android:padding="4dp">
+
+        <!-- 菜单图标 -->
+        <ImageView
+            android:id="@+id/nav_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null" />
+
+        <!-- 菜单文字 -->
+        <TextView
+            android:id="@+id/nav_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="2dp"
+            android:ellipsize="end"
+            android:singleLine="true"
+            android:textColor="?attr/colorOnSurface"
+            android:textSize="12sp" />
+    </LinearLayout>
+</layout>

+ 1 - 0
ui-base/src/main/res/values-en/strings.xml

@@ -363,4 +363,5 @@
     <string name="select_coloker">select colocker</string>
     <string name="ticket_data_error">Ticket data error</string>
     <string name="job_already_finished">job already finished</string>
+    <string name="not_save_tip">Data not save,Do you want to lost save and leave?</string>
 </resources>

+ 1 - 0
ui-base/src/main/res/values-land/dimens.xml

@@ -12,6 +12,7 @@
     <dimen name="common_radius_small">8.5dp</dimen>
     <dimen name="common_spacing">17dp</dimen>
     <dimen name="common_spacing_2x">34dp</dimen>
+    <dimen name="common_spacing_3x">30dp</dimen>
     <dimen name="common_spacing_big">25.5dp</dimen>
     <dimen name="common_spacing_small">8.5dp</dimen>
     <dimen name="common_spacing_smallest">3.4dp</dimen>

+ 1 - 0
ui-base/src/main/res/values-zh/strings.xml

@@ -363,4 +363,5 @@
     <string name="select_coloker">请选择共锁人</string>
     <string name="ticket_data_error">工作票数据损坏</string>
     <string name="job_already_finished">该作业已被结束</string>
+    <string name="not_save_tip">数据还没有保存,您确定要放弃保存,离开当前页面吗?</string>
 </resources>

+ 16 - 0
ui-base/src/main/res/values/attrs.xml

@@ -5,4 +5,20 @@
         <attr name="shadowDx" format="dimension"/>
         <attr name="shadowDy" format="dimension"/>
     </declare-styleable>
+    <declare-styleable name="CustomNavBar">
+        <attr name="navOrientation" format="enum">
+            <enum name="horizontal" value="0"/>
+            <enum name="vertical"   value="1"/>
+        </attr>
+        <!-- 背景颜色 -->
+        <attr name="navBackgroundColor" format="color"/>
+        <!-- 圆角半径 -->
+        <attr name="navCornerRadius" format="dimension"/>
+        <!-- 凹槽高度(半径) -->
+        <attr name="navNotchHeight" format="dimension"/>
+        <!-- 顶部小球直径 -->
+        <attr name="navBallDiameter" format="dimension"/>
+        <attr name="navIconSize" format="dimension"/>
+        <attr name="navTextSize" format="dimension"/>
+    </declare-styleable>
 </resources>

+ 1 - 0
ui-base/src/main/res/values/colors.xml

@@ -49,4 +49,5 @@
     <color name="common_tip_dialog_error">#e03131</color>
     <color name="common_tip_dialog_success">#099268</color>
     <color name="home_card_title_bg">#228be6</color>
+    <color name="dirtyColor">#F44336</color>
 </resources>

+ 1 - 0
ui-base/src/main/res/values/dimens.xml

@@ -12,6 +12,7 @@
     <dimen name="common_radius_small">5dp</dimen>
     <dimen name="common_spacing">10dp</dimen>
     <dimen name="common_spacing_2x">20dp</dimen>
+    <dimen name="common_spacing_3x">30dp</dimen>
     <dimen name="common_spacing_big">15dp</dimen>
     <dimen name="common_spacing_small">5dp</dimen>
     <dimen name="common_spacing_smallest">2dp</dimen>

+ 1 - 0
ui-base/src/main/res/values/strings.xml

@@ -363,4 +363,5 @@
     <string name="select_coloker">请选择共锁人</string>
     <string name="ticket_data_error">工作票数据损坏</string>
     <string name="job_already_finished">该作业已被结束</string>
+    <string name="not_save_tip">数据还没有保存,您确定要放弃保存,离开当前页面吗?</string>
 </resources>