浏览代码

feat: Basic framework construction and main interface preliminary completion

- Added main interface framework, including data management, exception management and user information related navigation.
- Added custom components: RequiredTextView, FingerprintFillView, CustomNavBar, ShadowTextView.
- Added i18n support.
- Added preset data: preset_sys_role.json.
- Optimized login interface UI details and login logic.
- Updated sikcomm version to 1.0.12.
周文健 2 月之前
父节点
当前提交
e68372690e
共有 96 个文件被更改,包括 2927 次插入142 次删除
  1. 30 0
      app/src/main/assets/i18n/zh-CN.json
  2. 104 0
      app/src/main/assets/preset/preset_sys_role.json
  3. 二进制
      app/src/main/assets/themes/Default/icons/icon_chevron_left.png
  4. 二进制
      app/src/main/assets/themes/Default/icons/icon_data_export.png
  5. 4 0
      app/src/main/assets/themes/Default/icons/icon_data_manage_switch_layout.svg
  6. 二进制
      app/src/main/assets/themes/Default/icons/icon_delete_circle.png
  7. 二进制
      app/src/main/assets/themes/Default/icons/icon_drop_down_tree_check.png
  8. 二进制
      app/src/main/assets/themes/Default/icons/icon_drop_down_tree_collapse.png
  9. 二进制
      app/src/main/assets/themes/Default/icons/icon_drop_down_tree_expand.png
  10. 二进制
      app/src/main/assets/themes/Default/icons/icon_drop_down_tree_point.png
  11. 二进制
      app/src/main/assets/themes/Default/icons/icon_hide.png
  12. 二进制
      app/src/main/assets/themes/Default/icons/icon_logo.png
  13. 二进制
      app/src/main/assets/themes/Default/icons/icon_overview_data.png
  14. 二进制
      app/src/main/assets/themes/Default/icons/icon_realtime_data.png
  15. 二进制
      app/src/main/assets/themes/Default/icons/icon_settings.png
  16. 二进制
      app/src/main/assets/themes/Default/icons/icon_show.png
  17. 二进制
      app/src/main/assets/themes/Default/icons/icon_switch_map_edit.png
  18. 二进制
      app/src/main/assets/themes/Default/icons/land-location.png
  19. 7 1
      app/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt
  20. 0 2
      app/src/main/java/com/grkj/iscs_mc/features/login/activity/LoginActivity.kt
  21. 4 2
      app/src/main/java/com/grkj/iscs_mc/features/login/adapter/TwoFragmentAdapter.kt
  22. 72 37
      app/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt
  23. 175 0
      app/src/main/java/com/grkj/iscs_mc/features/main/activity/MainActivity.kt
  24. 12 0
      app/src/main/java/com/grkj/iscs_mc/features/main/entity/TabConfig.kt
  25. 18 0
      app/src/main/java/com/grkj/iscs_mc/features/main/fragment/DataHomeFragment.kt
  26. 19 0
      app/src/main/java/com/grkj/iscs_mc/features/main/fragment/ExceptionHomeFragment.kt
  27. 18 0
      app/src/main/java/com/grkj/iscs_mc/features/main/fragment/HomeFragment.kt
  28. 12 0
      app/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/MainViewModel.kt
  29. 10 4
      app/src/main/java/com/grkj/iscs_mc/features/splash/activity/SplashActivity.kt
  30. 29 0
      app/src/main/java/com/grkj/iscs_mc/features/splash/viewmodel/SplashViewModel.kt
  31. 5 0
      app/src/main/res/drawable/bg_card_blue_radius_md.xml
  32. 9 0
      app/src/main/res/drawable/bg_home_header.xml
  33. 9 0
      app/src/main/res/drawable/bg_main.xml
  34. 3 3
      app/src/main/res/layout-land/activity_login.xml
  35. 107 0
      app/src/main/res/layout-land/activity_main.xml
  36. 5 5
      app/src/main/res/layout-land/fragment_login.xml
  37. 1 1
      app/src/main/res/layout-land/item_login_method.xml
  38. 3 3
      app/src/main/res/layout/activity_login.xml
  39. 107 0
      app/src/main/res/layout/activity_main.xml
  40. 4 0
      app/src/main/res/layout/fragment_data_home.xml
  41. 4 0
      app/src/main/res/layout/fragment_exception_home.xml
  42. 4 0
      app/src/main/res/layout/fragment_home.xml
  43. 7 5
      app/src/main/res/layout/fragment_login.xml
  44. 1 1
      app/src/main/res/layout/item_login_method.xml
  45. 11 0
      app/src/main/res/navigation/nav_data_manage.xml
  46. 11 0
      app/src/main/res/navigation/nav_exception_manage.xml
  47. 11 0
      app/src/main/res/navigation/nav_home.xml
  48. 6 0
      app/src/main/res/navigation/nav_user_info.xml
  49. 67 0
      data/src/main/java/com/grkj/data/common/CommonConstants.kt
  50. 6 0
      data/src/main/java/com/grkj/data/common/EventConstants.kt
  51. 10 0
      data/src/main/java/com/grkj/data/common/MMKVConstants.kt
  52. 6 0
      data/src/main/java/com/grkj/data/di/AppEntryPoint.kt
  53. 7 0
      data/src/main/java/com/grkj/data/di/DatabaseModule.kt
  54. 7 0
      data/src/main/java/com/grkj/data/di/LogicManager.kt
  55. 9 0
      data/src/main/java/com/grkj/data/di/LogicModule.kt
  56. 14 0
      data/src/main/java/com/grkj/data/di/RepositoryModule.kt
  57. 18 0
      data/src/main/java/com/grkj/data/domain/logic/ISysLogic.kt
  58. 21 0
      data/src/main/java/com/grkj/data/domain/logic/impl/SysLogic.kt
  59. 13 0
      data/src/main/java/com/grkj/data/enums/RoleEnum.kt
  60. 95 0
      data/src/main/java/com/grkj/data/enums/RoleFunctionalPermissionsEnum.kt
  61. 248 0
      data/src/main/java/com/grkj/data/hardware/can/CanCommand.kt
  62. 107 0
      data/src/main/java/com/grkj/data/hardware/can/CanHelper.kt
  63. 51 0
      data/src/main/java/com/grkj/data/hardware/can/CanSendDelayInterceptor.kt
  64. 22 0
      data/src/main/java/com/grkj/data/hardware/can/CustomCanConfig.kt
  65. 56 0
      data/src/main/java/com/grkj/data/local/dao/SysDao.kt
  66. 54 14
      data/src/main/java/com/grkj/data/local/database/ISCSDatabase.kt
  67. 29 0
      data/src/main/java/com/grkj/data/local/database/PresetData.kt
  68. 37 0
      data/src/main/java/com/grkj/data/local/dos/IsWorkstation.kt
  69. 53 0
      data/src/main/java/com/grkj/data/local/dos/SysMenu.kt
  70. 89 0
      data/src/main/java/com/grkj/data/local/dos/SysRole.kt
  71. 14 0
      data/src/main/java/com/grkj/data/local/dos/SysRoleMenu.kt
  72. 18 0
      data/src/main/java/com/grkj/data/repository/ISysRepository.kt
  73. 18 0
      data/src/main/java/com/grkj/data/repository/impl/network/NetworkSysRepository.kt
  74. 194 0
      data/src/main/java/com/grkj/data/repository/impl/standard/StandardSysRepository.kt
  75. 2 0
      gradle/libs.versions.toml
  76. 1 0
      shared/build.gradle.kts
  77. 150 0
      shared/src/main/java/com/grkj/shared/utils/CountdownTimer.kt
  78. 14 0
      shared/src/main/java/com/grkj/shared/utils/extension/Context.kt
  79. 19 1
      ui-base/src/main/java/com/grkj/ui_base/base/BaseNavActivity.kt
  80. 94 0
      ui-base/src/main/java/com/grkj/ui_base/utils/extension/View.kt
  81. 289 0
      ui-base/src/main/java/com/grkj/ui_base/widget/CustomNavBar.kt
  82. 81 0
      ui-base/src/main/java/com/grkj/ui_base/widget/FingerprintFillView.kt
  83. 76 0
      ui-base/src/main/java/com/grkj/ui_base/widget/RequiredTextView.kt
  84. 55 0
      ui-base/src/main/java/com/grkj/ui_base/widget/ShadowTextView.kt
  85. 2 2
      ui-base/src/main/res/drawable/common_divider_large_space_grid_land.xml
  86. 1 1
      ui-base/src/main/res/drawable/common_divider_normal_space_vertical.xml
  87. 1 1
      ui-base/src/main/res/layout-land/common_dialog_loading_progress.xml
  88. 3 3
      ui-base/src/main/res/layout-land/dialog_tip.xml
  89. 2 2
      ui-base/src/main/res/layout-land/dialog_wheel_date_range.xml
  90. 1 1
      ui-base/src/main/res/layout-land/dialog_wheel_time_pick.xml
  91. 1 1
      ui-base/src/main/res/layout/common_dialog_loading_progress.xml
  92. 3 3
      ui-base/src/main/res/layout/dialog_tip.xml
  93. 2 2
      ui-base/src/main/res/layout/dialog_wheel_date_range.xml
  94. 1 1
      ui-base/src/main/res/layout/dialog_wheel_time_pick.xml
  95. 39 46
      ui-base/src/main/res/values/colors_palette.xml
  96. 5 0
      ui-base/src/main/res/values/dimens.xml

+ 30 - 0
app/src/main/assets/i18n/zh-CN.json

@@ -133,5 +133,35 @@
     "key": "card_login",
     "type": "text",
     "value": "刷卡登录"
+  },
+  "please_input_account": {
+    "key": "please_input_account",
+    "type": "text",
+    "value": "请输入用户名"
+  },
+  "please_input_password": {
+    "key": "please_input_password",
+    "type": "text",
+    "value": "请输入密码"
+  },
+  "please_scan_face": {
+    "key": "please_scan_face",
+    "type": "text",
+    "value": "请刷脸"
+  },
+  "please_scan_fingerprint": {
+    "key": "please_scan_fingerprint",
+    "type": "text",
+    "value": "请刷指纹"
+  },
+  "please_swipe_card": {
+    "key": "please_swipe_card",
+    "type": "text",
+    "value": "请刷卡"
+  },
+  "doing_login": {
+    "key": "doing_login",
+    "type": "text",
+    "value": "正在登录······"
   }
 }

+ 104 - 0
app/src/main/assets/preset/preset_sys_role.json

@@ -0,0 +1,104 @@
+[
+  {
+    "roleId": 1,
+    "roleName": "超级管理员",
+    "roleKey": "admin",
+    "roleSort": 1,
+    "dataScope": "1",
+    "menuCheckStrictly": 1,
+    "deptCheckStrictly": 1,
+    "status": "0",
+    "delFlag": "0",
+    "createBy": "admin",
+    "createTime": "2024-09-04 15:11:42",
+    "updateBy": null,
+    "updateTime": null,
+    "remark": "超级管理员",
+    "marsDataScope": "1"
+  },
+  {
+    "roleId": 2,
+    "roleName": "作业管理员",
+    "roleKey": "jtdrawer",
+    "roleSort": 2,
+    "dataScope": "1",
+    "menuCheckStrictly": 1,
+    "deptCheckStrictly": 1,
+    "status": "0",
+    "delFlag": "0",
+    "createBy": "admin",
+    "createTime": "2024-09-04 15:11:42",
+    "updateBy": "admin",
+    "updateTime": "2025-06-03 16:26:03",
+    "remark": "普通角色",
+    "marsDataScope": "1"
+  },
+  {
+    "roleId": 3,
+    "roleName": "作业负责人",
+    "roleKey": "jtlocker",
+    "roleSort": 3,
+    "dataScope": "1",
+    "menuCheckStrictly": 1,
+    "deptCheckStrictly": 1,
+    "status": "0",
+    "delFlag": "0",
+    "createBy": "admin",
+    "createTime": "2024-09-06 10:56:30",
+    "updateBy": "admin",
+    "updateTime": "2025-06-03 16:27:23",
+    "remark": "开锁解锁",
+    "marsDataScope": "1"
+  },
+  {
+    "roleId": 4,
+    "roleName": "作业参与人",
+    "roleKey": "jtcolocker",
+    "roleSort": 4,
+    "dataScope": "1",
+    "menuCheckStrictly": 1,
+    "deptCheckStrictly": 1,
+    "status": "0",
+    "delFlag": "0",
+    "createBy": "admin",
+    "createTime": "2024-09-11 10:54:15",
+    "updateBy": "mars",
+    "updateTime": "2025-03-12 16:24:57",
+    "remark": "打卡,第一次打卡进场,第二次打卡出场",
+    "marsDataScope": "1"
+  },
+  {
+    "roleId": 5,
+    "roleName": "作业观察员",
+    "roleKey": "jtguard",
+    "roleSort": 5,
+    "dataScope": "1",
+    "menuCheckStrictly": 1,
+    "deptCheckStrictly": 1,
+    "status": "0",
+    "delFlag": "0",
+    "createBy": "admin",
+    "createTime": "2024-09-11 10:54:40",
+    "updateBy": "admin",
+    "updateTime": "2024-09-11 10:56:01",
+    "remark": "作业数据查询到处",
+    "marsDataScope": "1"
+  },
+  {
+    "roleId": 17,
+    "roleName": "系统配置员",
+    "roleKey": "sysconfig",
+    "roleSort": 0,
+    "dataScope": "1",
+    "menuCheckStrictly": 1,
+    "deptCheckStrictly": 1,
+    "status": "0",
+    "delFlag": "0",
+    "createBy": "mars",
+    "createTime": "2025-04-02 18:41:45",
+    "updateBy": "mars",
+    "updateTime": "2025-04-02 18:42:55",
+    "remark": "",
+    "marsDataScope": "1"
+  }
+]

二进制
app/src/main/assets/themes/Default/icons/icon_chevron_left.png


二进制
app/src/main/assets/themes/Default/icons/icon_data_export.png


+ 4 - 0
app/src/main/assets/themes/Default/icons/icon_data_manage_switch_layout.svg

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
+  <path d="m16.949,2.05c-1.321-1.322-3.079-2.05-4.949-2.05s-3.627.728-4.95,2.05c-2.729,2.729-2.729,7.17.008,9.907l2.495,2.44c.675.66,1.561.99,2.447.99s1.772-.33,2.447-.99l2.502-2.448c1.322-1.322,2.051-3.08,2.051-4.95s-.729-3.627-2.051-4.95Zm-4.949,7.94c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Zm-4.567,5.131l-.122.879H.628l.459-2.676c.246-1.435,1.23-2.577,2.524-3.066.449,1.156,1.14,2.219,2.049,3.129l1.773,1.734ZM.285,18h6.747l-.834,6h-2.198c-1.178,0-2.291-.516-3.052-1.415-.762-.899-1.087-2.081-.893-3.243l.23-1.342Zm14.862,0l.835,6h-7.765l.834-6h6.096Zm8.225-2h-6.485l-.146-1.049,1.622-1.587c.899-.899,1.584-1.954,2.03-3.104,1.292.49,2.274,1.63,2.52,3.064l.459,2.676Zm-.32,6.586c-.762.898-1.874,1.414-3.052,1.414h-2l-.835-6h6.55l.229,1.338c.194,1.167-.131,2.349-.893,3.248Z"/>
+</svg>

二进制
app/src/main/assets/themes/Default/icons/icon_delete_circle.png


二进制
app/src/main/assets/themes/Default/icons/icon_drop_down_tree_check.png


二进制
app/src/main/assets/themes/Default/icons/icon_drop_down_tree_collapse.png


二进制
app/src/main/assets/themes/Default/icons/icon_drop_down_tree_expand.png


二进制
app/src/main/assets/themes/Default/icons/icon_drop_down_tree_point.png


二进制
app/src/main/assets/themes/Default/icons/icon_hide.png


二进制
app/src/main/assets/themes/Default/icons/icon_logo.png


二进制
app/src/main/assets/themes/Default/icons/icon_overview_data.png


二进制
app/src/main/assets/themes/Default/icons/icon_realtime_data.png


二进制
app/src/main/assets/themes/Default/icons/icon_settings.png


二进制
app/src/main/assets/themes/Default/icons/icon_show.png


二进制
app/src/main/assets/themes/Default/icons/icon_switch_map_edit.png


二进制
app/src/main/assets/themes/Default/icons/land-location.png


+ 7 - 1
app/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt

@@ -9,6 +9,8 @@ import ch.qos.logback.classic.Level
 import com.drake.statelayout.StateConfig
 import com.grkj.data.common.EventConstants
 import com.grkj.data.di.LogicManager
+import com.grkj.data.hardware.can.CanHelper
+import com.grkj.data.hardware.can.CustomCanConfig
 import com.grkj.data.local.database.DbReadyGate
 import com.grkj.iscs_mc.features.splash.activity.SplashActivity
 import com.grkj.shared.model.EventBean
@@ -24,9 +26,12 @@ import com.kongzue.dialogx.DialogX
 import com.scwang.smart.refresh.footer.ClassicsFooter
 import com.scwang.smart.refresh.header.ClassicsHeader
 import com.scwang.smart.refresh.layout.SmartRefreshLayout
+import com.sik.comm.core.protocol.ProtocolManager
+import com.sik.comm.core.protocol.ProtocolType
+import com.sik.comm.impl_can.CanConfig
+import com.sik.comm.impl_can.CanProtocol
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.crash.GlobalCrashCatch
-import com.sik.sikcore.extension.toJson
 import com.sik.sikcore.log.LogUtils
 import com.sik.sikcore.thread.ThreadUtils
 import com.tencent.mmkv.MMKV
@@ -88,6 +93,7 @@ class ISCSMCApplication : Application() {
         ThreadUtils.runOnIO {
             DbReadyGate.await()
             LogicManager.init(this@ISCSMCApplication)
+            CanHelper.connect()
         }
     }
 

+ 0 - 2
app/src/main/java/com/grkj/iscs_mc/features/login/activity/LoginActivity.kt

@@ -2,11 +2,9 @@ package com.grkj.iscs_mc.features.login.activity
 
 import android.view.InputDevice
 import android.view.KeyEvent
-import androidx.activity.viewModels
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.ActivityLoginBinding
 import com.grkj.iscs_mc.features.login.adapter.TwoFragmentAdapter
-import com.grkj.iscs_mc.features.login.viewmodel.LoginViewModel
 import com.grkj.shared.utils.extension.toByteArrays
 import com.grkj.shared.utils.extension.toHexStrings
 import com.grkj.ui_base.base.BaseActivity

+ 4 - 2
app/src/main/java/com/grkj/iscs_mc/features/login/adapter/TwoFragmentAdapter.kt

@@ -7,7 +7,9 @@ import com.grkj.iscs_mc.features.login.fragment.LoginFragment
 import com.grkj.iscs_mc.features.login.fragment.MaterialShowFragment
 
 class TwoFragmentAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
-    override fun getItemCount() = 2
+//    override fun getItemCount() = 2
+    override fun getItemCount() = 1
     override fun createFragment(position: Int): Fragment =
-        if (position == 0) LoginFragment() else MaterialShowFragment()
+//        if (position == 0) LoginFragment() else MaterialShowFragment()
+        LoginFragment()
 }

+ 72 - 37
app/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt

@@ -7,6 +7,8 @@ import android.widget.LinearLayout
 import androidx.core.view.isVisible
 import androidx.core.widget.ImageViewCompat
 import androidx.fragment.app.viewModels
+import com.arcsoft.face.FaceEngine
+import com.arcsoft.face.model.ActiveDeviceInfo
 import com.drake.brv.BindingAdapter
 import com.drake.brv.annotaion.DividerOrientation
 import com.drake.brv.utils.divider
@@ -18,7 +20,11 @@ import com.grkj.data.common.EventConstants
 import com.grkj.data.common.MainDomainData
 import com.grkj.data.enums.LoginModeEnum
 import com.grkj.data.enums.LoginResultEnum
+import com.grkj.data.hardware.can.CanCommands
+import com.grkj.data.hardware.can.CanHelper
+import com.grkj.data.hardware.can.CustomCanConfig
 import com.grkj.data.hardware.fingerprint.FingerprintUtil
+import com.grkj.iscs_mc.ISCSMCApplication
 import com.grkj.iscs_mc.features.login.dialog.ChangeLangDialog
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentLoginBinding
@@ -26,9 +32,11 @@ import com.grkj.iscs_mc.databinding.ItemLoginMethodBinding
 import com.grkj.iscs_mc.features.login.dialog.LoginDialog
 import com.grkj.iscs_mc.features.login.entity.LoginMenuEntity
 import com.grkj.iscs_mc.features.login.viewmodel.LoginViewModel
+import com.grkj.iscs_mc.features.main.activity.MainActivity
 import com.grkj.iscs_mc.features.manage.activity.ManageActivity
 import com.grkj.iscs_mc.features.material_exchange.activity.MaterialExchangeActivity
 import com.grkj.shared.model.EventBean
+import com.grkj.shared.utils.extension.toByteArray
 import com.grkj.shared.utils.i18n.I18nManager
 import com.grkj.shared.utils.i18n.LanguageEntry
 import com.grkj.shared.utils.i18n.LanguageRegistry
@@ -40,9 +48,19 @@ import com.grkj.ui_base.utils.changeBgTint
 import com.grkj.ui_base.utils.event.LoadingEvent
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.grkj.ui_base.utils.extension.getAppVersionName
+import com.sik.comm.core.model.CommMessage
+import com.sik.comm.core.protocol.ProtocolManager
+import com.sik.comm.impl_can.CanProtocol
+import com.sik.comm.impl_can.SdoRequest
+import com.sik.comm.impl_can.SdoResponse
+import com.sik.comm.impl_can.toCommMessage
 import com.sik.sikcore.extension.setDebouncedClickListener
+import com.sik.sikcore.shell.ShellUtils
+import com.sik.sikcore.thread.ThreadUtils
 import com.sik.sikimage.ImageConvertUtils
 import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
 import java.util.Locale
 
 /**
@@ -54,14 +72,16 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
 
     //登录方式列表
     private val loginMenuList: MutableList<LoginMenuEntity> = mutableListOf()
+    private var isKeyDockOpen = true
 
     /**
      * 登录模式切换
      */
-    private var loginMode = LoginModeEnum.USER_MODE
+    private var loginMode = LoginModeEnum.ADMINISTRATOR_MODE
     private val changeModeClickTimes = 5
     private var currentChangeModeClickTimes = 0
     private var currentClickTime = 0L
+    private var currentSupportClickTime = 0L
 
     override fun getLayoutId(): Int {
         return R.layout.fragment_login
@@ -119,6 +139,33 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
             }
             binding.modeTip.isVisible = loginMode == LoginModeEnum.ADMINISTRATOR_MODE
         }
+        binding.titleCn.setDebouncedClickListener {
+            val activeDeviceInfo = ActiveDeviceInfo()
+            FaceEngine.getActiveDeviceInfo(requireContext(), activeDeviceInfo)
+            ShellUtils.execCmd("echo ${activeDeviceInfo.deviceInfo} > /sdcard/iscs/activeDeviceInfo.txt")
+        }
+        CanHelper.checkNode()
+        binding.titleEn.setDebouncedClickListener {
+            runCatching {
+                CanHelper.writeTo(
+                    (CanCommands.forDevice(
+                        1
+                    ) as CanCommands.EKeyDockCommands).controlLatch(0,1)
+                ) {
+                    logger.info("返回:${it}")
+                }
+            }.onFailure {
+                logger.info("发送错误:${it}")
+            }
+
+        }
+        binding.tecSupport.setOnClickListener {
+            currentSupportClickTime++
+            if (currentSupportClickTime > 10) {
+                android.os.Process.killProcess(android.os.Process.myPid())
+            }
+        }
+        binding.modeTip.isVisible = false
     }
 
     private fun BindingAdapter.BindingViewHolder.onLoginMenuBinding(holder: BindingAdapter.BindingViewHolder) {
@@ -139,15 +186,13 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                         showToast(CommonUtils.getStr("fingerprint_login_success"))
                         if (loginMode == LoginModeEnum.USER_MODE) {
                             startActivity(
-                                Intent(
-                                    requireContext(), MaterialExchangeActivity::class.java
-                                )
+//                                Intent(requireContext(), MaterialExchangeActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         } else {
                             startActivity(
-                                Intent(
-                                    requireContext(), ManageActivity::class.java
-                                )
+//                                Intent(requireContext(), ManageActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         }
                         requireActivity().finish()
@@ -161,15 +206,13 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                         showToast(CommonUtils.getStr("face_login_success"))
                         if (loginMode == LoginModeEnum.USER_MODE) {
                             startActivity(
-                                Intent(
-                                    requireContext(), MaterialExchangeActivity::class.java
-                                )
+//                                Intent(requireContext(), MaterialExchangeActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         } else {
                             startActivity(
-                                Intent(
-                                    requireContext(), ManageActivity::class.java
-                                )
+//                                Intent(requireContext(), ManageActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         }
                         requireActivity().finish()
@@ -185,15 +228,13 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                         )
                         if (loginMode == LoginModeEnum.USER_MODE) {
                             startActivity(
-                                Intent(
-                                    requireContext(), MaterialExchangeActivity::class.java
-                                )
+//                                Intent(requireContext(), MaterialExchangeActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         } else {
                             startActivity(
-                                Intent(
-                                    requireContext(), ManageActivity::class.java
-                                )
+//                                Intent(requireContext(), ManageActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         }
                         requireActivity().finish()
@@ -211,15 +252,13 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                         showToast(CommonUtils.getStr("job_card_login_success"))
                         if (loginMode == LoginModeEnum.USER_MODE) {
                             startActivity(
-                                Intent(
-                                    requireContext(), MaterialExchangeActivity::class.java
-                                )
+//                                Intent(requireContext(), MaterialExchangeActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         } else {
                             startActivity(
-                                Intent(
-                                    requireContext(), ManageActivity::class.java
-                                )
+//                                Intent(requireContext(), ManageActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         }
                         requireActivity().finish()
@@ -301,15 +340,13 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                         showToast(CommonUtils.getStr("fingerprint_login_success"))
                         if (loginMode == LoginModeEnum.USER_MODE) {
                             startActivity(
-                                Intent(
-                                    requireContext(), MaterialExchangeActivity::class.java
-                                )
+//                                Intent(requireContext(), MaterialExchangeActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         } else {
                             startActivity(
-                                Intent(
-                                    requireContext(), ManageActivity::class.java
-                                )
+//                                Intent(requireContext(), ManageActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         }
                         requireActivity().finish()
@@ -342,15 +379,13 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                         )
                         if (loginMode == LoginModeEnum.USER_MODE) {
                             startActivity(
-                                Intent(
-                                    requireContext(), MaterialExchangeActivity::class.java
-                                )
+//                                Intent(requireContext(), MaterialExchangeActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         } else {
                             startActivity(
-                                Intent(
-                                    requireContext(), ManageActivity::class.java
-                                )
+//                                Intent(requireContext(), ManageActivity::class.java)
+                                Intent(requireContext(), MainActivity::class.java)
                             )
                         }
                         requireActivity().finish()

+ 175 - 0
app/src/main/java/com/grkj/iscs_mc/features/main/activity/MainActivity.kt

@@ -0,0 +1,175 @@
+package com.grkj.iscs_mc.features.main.activity
+
+import android.content.Intent
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MotionEvent
+import android.view.View
+import androidx.activity.viewModels
+import androidx.core.view.get
+import androidx.core.view.isNotEmpty
+import androidx.core.view.isVisible
+import coil.load
+import coil.transform.CircleCropTransformation
+import com.grkj.data.common.CommonConstants
+import com.grkj.data.common.EventConstants
+import com.grkj.data.common.MMKVConstants
+import com.grkj.data.common.MainDomainData
+import com.grkj.data.enums.RoleFunctionalPermissionsEnum
+import com.grkj.iscs_mc.R
+import com.grkj.iscs_mc.databinding.ActivityMainBinding
+import com.grkj.iscs_mc.features.login.activity.LoginActivity
+import com.grkj.iscs_mc.features.main.entity.TabConfig
+import com.grkj.iscs_mc.features.main.viewmodel.MainViewModel
+import com.grkj.shared.model.EventBean
+import com.grkj.shared.utils.CountdownTimer
+import com.grkj.shared.utils.extension.toByteArrays
+import com.grkj.shared.utils.extension.toHexStrings
+import com.grkj.shared.utils.i18n.I18nManager
+import com.grkj.ui_base.base.BaseActivity
+import com.grkj.ui_base.base.BaseNavActivity
+import com.grkj.ui_base.utils.event.RFIDCardReadEvent
+import com.grkj.ui_base.utils.extension.removeTint
+import com.sik.sikcore.extension.file
+import com.sik.sikcore.extension.getMMKVData
+import com.sik.sikcore.extension.setDebouncedClickListener
+import com.sik.sikimage.ImageConvertUtils
+import dagger.hilt.android.AndroidEntryPoint
+
+/**
+ * 首页
+ */
+@AndroidEntryPoint
+class MainActivity() : BaseNavActivity<ActivityMainBinding>() {
+    private val viewModel: MainViewModel by viewModels()
+    private var cardNo: String = ""
+    private var isFirstEnter: Boolean = true
+    private val tabConfigs = listOf(
+        TabConfig(
+            View.generateViewId(),
+            R.navigation.nav_home,
+            I18nManager.t(RoleFunctionalPermissionsEnum.HOME.description),
+            "icon_bottom_menu_home.svg",
+            RoleFunctionalPermissionsEnum.HOME.functionalPermission
+        ),
+        TabConfig(
+            View.generateViewId(),
+            R.navigation.nav_data_manage,
+            I18nManager.t(RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE.description),
+            "icon_bottom_menu_data_manage.svg",
+            RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE.functionalPermission
+        ),
+        TabConfig(
+            View.generateViewId(),
+            R.navigation.nav_exception_manage,
+            I18nManager.t(RoleFunctionalPermissionsEnum.EXCEPTION_HOME_MANAGE.description),
+            "icon_bottom_menu_exception_manage.svg",
+            RoleFunctionalPermissionsEnum.EXCEPTION_HOME_MANAGE.functionalPermission
+        ),
+    )
+
+    private val bottomNavDestinations = setOf(
+        R.id.homeFragment,
+        R.id.dataHomeFragment,
+        R.id.exceptionHomeFragment
+    )
+
+    override fun navHostFragmentId() = R.id.nav_host_fragment
+
+    override fun getLayoutId(): Int {
+        return R.layout.activity_main
+    }
+
+    override fun initView() {
+        val autoLogoutTime =
+            MMKVConstants.KEY_AUTO_LOGOUT_TIME.getMMKVData(CommonConstants.DEFAULT_AUTO_LOGOUT_TIME)
+        CountdownTimer.start(autoLogoutTime, onFinish = {
+            logout()
+        })
+        binding.nickname.text = MainDomainData.userInfo?.nickName ?: ""
+        (MainDomainData.userInfo?.avatar
+            ?: MainDomainData.userBiometricDataVo.find { it.type == "2" }?.content)?.let {
+            if (it.isNotEmpty()) {
+                val faceData = it.file().readText()
+                val avatar = ImageConvertUtils.base64ToBitmap(faceData)
+                binding.avatar.removeTint()
+                binding.avatar.load(avatar) {
+                    transformations(CircleCropTransformation())
+                }
+            }
+        }
+        binding.navBar.let {
+            it.menu.clear()
+            it.setIconData(tabConfigs.associate { it.title to it.icon })
+            tabConfigs.forEachIndexed { index, cfg ->
+                if (MainDomainData.permissions.contains(cfg.permission)) {
+                    binding.navBar.menu.add(Menu.NONE, cfg.id, index, cfg.title)
+                }
+            }
+            // 这里很关键:通知 NavBar 重建子视图
+            it.notifyMenuChanged()   // ★
+            // 构造 map: menuItemId -> navGraphId
+            val graphMap = tabConfigs.filter { binding.navBar.menu.findItem(it.id) != null }
+                .associate { it.id to it.graphRes }
+            setupBottomNavigation(it, graphMap)
+            // 默认选中第一个
+            if (binding.navBar.menu.isNotEmpty()) {
+                val firstId = binding.navBar.menu[0].itemId
+                binding.navBar.selectedItemId = firstId
+            }
+        }
+        binding.userInfoLayout.setDebouncedClickListener {
+            replaceNavGraph(R.navigation.nav_user_info)
+        }
+        navController.addOnDestinationChangedListener { _, destination, _ ->
+            binding.navBar.isVisible = bottomNavDestinations.contains(destination.id)
+        }
+    }
+
+    override fun onEvent(event: EventBean<Any>) {
+        super.onEvent(event)
+        when (event.code) {
+            EventConstants.EVENT_LOGOUT -> {
+                logout()
+            }
+        }
+    }
+
+    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        CountdownTimer.reset()
+        return super.dispatchTouchEvent(ev)
+    }
+
+    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+        if (event.action == KeyEvent.ACTION_UP && event.source == InputDevice.SOURCE_KEYBOARD) {
+            // 检测到回车开始处理
+            if (event.keyCode == 66) {
+                logger.info("Swipe card login Origin: $cardNo")
+                try {
+                    cardNo = cardNo.toLong().toByteArrays().toHexStrings(false)
+                    logger.info("Swipe card login: $cardNo")
+                    RFIDCardReadEvent.sendRFIDCardReadEvent(cardNo)
+                    // 重置cardNo
+                    cardNo = ""
+                } catch (e: Exception) {
+                    cardNo = ""
+                    logger.info("读卡失败: ${e.toString()}")
+                }
+                return super.dispatchKeyEvent(event)
+            }
+            cardNo += event.keyCharacterMap.getDisplayLabel(event.keyCode)
+        }
+        return super.dispatchKeyEvent(event)
+    }
+
+    /**
+     * 退出登录
+     */
+    private fun logout() {
+        startActivity(Intent(this, LoginActivity::class.java).apply {
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK
+        })
+        finish()
+    }
+}

+ 12 - 0
app/src/main/java/com/grkj/iscs_mc/features/main/entity/TabConfig.kt

@@ -0,0 +1,12 @@
+package com.grkj.iscs_mc.features.main.entity
+
+/**
+ * 底部页签
+ */
+data class TabConfig(
+    val id: Int,        // 唯一的 menuItemId
+    val graphRes: Int,        // nav graph resource id
+    val title: String,
+    val icon: String,
+    val permission: String    // 对应的运行时权限标识
+)

+ 18 - 0
app/src/main/java/com/grkj/iscs_mc/features/main/fragment/DataHomeFragment.kt

@@ -0,0 +1,18 @@
+package com.grkj.iscs_mc.features.main.fragment
+
+import com.grkj.iscs_mc.R
+import com.grkj.iscs_mc.databinding.FragmentDataHomeBinding
+import com.grkj.ui_base.base.BaseFragment
+
+/**
+ * 数据管理首页
+ */
+class DataHomeFragment: BaseFragment<FragmentDataHomeBinding>() {
+    override fun getLayoutId(): Int {
+        return R.layout.fragment_data_home
+    }
+
+    override fun initView() {
+        TODO("Not yet implemented")
+    }
+}

+ 19 - 0
app/src/main/java/com/grkj/iscs_mc/features/main/fragment/ExceptionHomeFragment.kt

@@ -0,0 +1,19 @@
+package com.grkj.iscs_mc.features.main.fragment
+
+import com.grkj.iscs_mc.R
+import com.grkj.iscs_mc.databinding.FragmentDataHomeBinding
+import com.grkj.iscs_mc.databinding.FragmentExceptionHomeBinding
+import com.grkj.ui_base.base.BaseFragment
+
+/**
+ * 异常首页
+ */
+class ExceptionHomeFragment : BaseFragment<FragmentExceptionHomeBinding>() {
+    override fun getLayoutId(): Int {
+        return R.layout.fragment_exception_home
+    }
+
+    override fun initView() {
+        TODO("Not yet implemented")
+    }
+}

+ 18 - 0
app/src/main/java/com/grkj/iscs_mc/features/main/fragment/HomeFragment.kt

@@ -0,0 +1,18 @@
+package com.grkj.iscs_mc.features.main.fragment
+
+import com.grkj.iscs_mc.R
+import com.grkj.iscs_mc.databinding.FragmentHomeBinding
+import com.grkj.ui_base.base.BaseFragment
+
+/**
+ * 主界面
+ */
+class HomeFragment: BaseFragment<FragmentHomeBinding>() {
+    override fun getLayoutId(): Int {
+        return R.layout.fragment_home
+    }
+
+    override fun initView() {
+        TODO("Not yet implemented")
+    }
+}

+ 12 - 0
app/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/MainViewModel.kt

@@ -0,0 +1,12 @@
+package com.grkj.iscs_mc.features.main.viewmodel
+
+import com.grkj.ui_base.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+/**
+ * 首页界面模型
+ */
+@HiltViewModel
+class MainViewModel @Inject constructor() : BaseViewModel() {
+}

+ 10 - 4
app/src/main/java/com/grkj/iscs_mc/features/splash/activity/SplashActivity.kt

@@ -2,6 +2,7 @@ package com.grkj.iscs_mc.features.splash.activity
 
 import android.content.Intent
 import android.view.Gravity
+import androidx.activity.viewModels
 import androidx.lifecycle.lifecycleScope
 import com.grkj.data.local.database.BackupScheduler
 import com.grkj.data.local.database.DbReadyGate
@@ -9,6 +10,7 @@ import com.grkj.data.local.database.ISCSDatabase
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.ActivitySplashBinding
 import com.grkj.iscs_mc.features.login.activity.LoginActivity
+import com.grkj.iscs_mc.features.splash.viewmodel.SplashViewModel
 import com.grkj.shared.config.Constants
 import com.grkj.ui_base.base.BaseActivity
 import com.kongzue.dialogx.DialogX
@@ -23,14 +25,13 @@ import kotlinx.coroutines.launch
 
 @AndroidEntryPoint
 class SplashActivity : BaseActivity<ActivitySplashBinding>() {
-
+    private val viewModel: SplashViewModel by viewModels()
     override fun getLayoutId(): Int {
         return R.layout.activity_splash
     }
 
     override fun initView() {
         // 应用启动时按已存配置安排下一次(默认=每天 00:00)
-        BackupScheduler.applySaved(this)
         val dialogXTextInfo = TextInfo()
         val dialogXTitleTextInfo = TextInfo().apply {
             isBold = true
@@ -61,9 +62,14 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
             }
         }
         lifecycleScope.launch {
+            BackupScheduler.applySaved(this@SplashActivity)
             DbReadyGate.await()
-            startActivity(Intent(this@SplashActivity, LoginActivity::class.java))
-            finish()
+            viewModel.checkPresetData().observe(this@SplashActivity){
+                viewModel.checkSysMenuAndRole().observe(this@SplashActivity) {
+                    startActivity(Intent(this@SplashActivity, LoginActivity::class.java))
+                    finish()
+                }
+            }
         }
     }
 }

+ 29 - 0
app/src/main/java/com/grkj/iscs_mc/features/splash/viewmodel/SplashViewModel.kt

@@ -0,0 +1,29 @@
+package com.grkj.iscs_mc.features.splash.viewmodel
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.liveData
+import com.grkj.data.domain.logic.ISysLogic
+import com.grkj.data.local.database.PresetData
+import com.grkj.ui_base.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Inject
+
+@HiltViewModel
+class SplashViewModel @Inject constructor(
+    val sysLogic: ISysLogic,
+) : BaseViewModel() {
+    fun checkPresetData(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            sysLogic.addPresetRoleData(PresetData.presetSysRole)
+            emit(true)
+        }
+    }
+
+    fun checkSysMenuAndRole(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            sysLogic.checkSysMenuAndRole()
+            emit(true)
+        }
+    }
+}

+ 5 - 0
app/src/main/res/drawable/bg_card_blue_radius_md.xml

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

+ 9 - 0
app/src/main/res/drawable/bg_home_header.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/iscs_radius_md" />
+    <gradient
+        android:angle="315"
+        android:endColor="?attr/colorContainerBg"
+        android:startColor="?attr/colorBg" />
+</shape>

+ 9 - 0
app/src/main/res/drawable/bg_main.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:angle="270"
+        android:centerColor="?attr/colorSecBg"
+        android:endColor="?attr/colorBg"
+        android:startColor="?attr/colorBg"
+        android:type="linear" />
+</shape>

+ 3 - 3
app/src/main/res/layout-land/activity_login.xml

@@ -14,10 +14,10 @@
             android:id="@+id/header_layout"
             android:layout_width="match_parent"
             android:layout_height="@dimen/header_height"
-            android:layout_marginHorizontal="@dimen/iscs_space_4"
-            android:layout_marginTop="@dimen/iscs_space_4"
+            android:layout_marginHorizontal="@dimen/iscs_space_2"
+            android:layout_marginTop="@dimen/iscs_space_2"
             android:background="@drawable/bg_card_login_header_bg_radius_md"
-            android:paddingHorizontal="@dimen/iscs_space_4">
+            android:paddingHorizontal="@dimen/iscs_space_2">
 
             <ImageView
                 android:layout_width="@dimen/header_logo_width"

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

@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/bg_main"
+        android:fitsSystemWindows="true"
+        android:orientation="vertical">
+
+        <FrameLayout
+            android:id="@+id/header_layout"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/header_height"
+            android:layout_margin="@dimen/iscs_space_2"
+            android:paddingHorizontal="@dimen/iscs_space_1">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="horizontal">
+
+                <FrameLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/bg_home_header"
+                    android:orientation="horizontal"
+                    android:paddingHorizontal="@dimen/iscs_space_4">
+
+                    <ImageView
+                        android:layout_width="@dimen/header_logo_width"
+                        android:layout_height="@dimen/header_logo_height"
+                        android:layout_gravity="center_vertical"
+                        app:skinSrc='@{"icon_logo.png"}' />
+
+                    <TextClock
+                        android:layout_width="wrap_content"
+                        android:layout_height="match_parent"
+                        android:layout_gravity="right"
+                        android:format12Hour="yyyy-MM-dd HH:mm:ss"
+                        android:format24Hour="yyyy-MM-dd HH:mm:ss"
+                        android:gravity="center"
+                        android:paddingHorizontal="@dimen/iscs_space_1"
+                        android:textColor="?attr/colorTextPrimary"
+                        android:textSize="@dimen/iscs_text_md" />
+                </FrameLayout>
+
+                <LinearLayout
+                    android:id="@+id/user_info_layout"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:layout_marginLeft="@dimen/iscs_space_2"
+                    android:background="@drawable/bg_card_blue_radius_md"
+                    android:orientation="horizontal"
+                    android:paddingHorizontal="@dimen/iscs_space_2">
+
+                    <ImageView
+                        android:id="@+id/avatar"
+                        android:layout_width="@dimen/avatar_size"
+                        android:layout_height="@dimen/avatar_size"
+                        android:layout_gravity="center_vertical"
+                        android:padding="@dimen/iscs_space_1"
+                        android:src="@mipmap/icon_avatar"
+                        android:tint="?attr/colorWhite" />
+
+                    <TextView
+                        android:id="@+id/nickname"
+                        android:layout_width="wrap_content"
+                        android:layout_height="match_parent"
+                        android:layout_marginLeft="@dimen/iscs_space_2"
+                        android:ellipsize="end"
+                        android:gravity="center"
+                        android:maxLength="4"
+                        android:singleLine="true"
+                        android:textColor="?attr/colorTextPrimary"
+                        android:textSize="@dimen/iscs_text_md"
+                        tools:text="张三" />
+                </LinearLayout>
+            </LinearLayout>
+        </FrameLayout>
+
+        <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/nav_host_fragment"
+            android:name="androidx.navigation.fragment.NavHostFragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_below="@+id/header_layout"
+            android:layout_toRightOf="@+id/nav_bar"
+            app:defaultNavHost="true"
+            app:navGraph="@navigation/nav_home" />
+
+        <com.grkj.ui_base.widget.CustomNavBar
+            android:id="@+id/nav_bar"
+            android:layout_width="@dimen/home_bottom_nav_size"
+            android:layout_height="match_parent"
+            android:layout_below="@+id/header_layout"
+            app:navBarBackground="?attr/colorSecBg"
+            app:navIconSelectedSize="@dimen/home_bottom_nav_icon_size"
+            app:navIconSize="@dimen/home_bottom_nav_icon_size"
+            app:navOrientation="vertical"
+            app:navTextSelectedSize="@dimen/iscs_text_sm"
+            app:navTextSize="@dimen/iscs_text_sm" />
+    </RelativeLayout>
+</layout>

+ 5 - 5
app/src/main/res/layout-land/fragment_login.xml

@@ -10,7 +10,7 @@
         <LinearLayout
             android:id="@+id/main_content"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
+            android:layout_height="match_parent"
             android:layout_above="@+id/tec_support"
             android:layout_marginTop="@dimen/iscs_space_2"
             android:orientation="vertical">
@@ -21,7 +21,7 @@
                 android:layout_height="wrap_content"
                 android:layout_gravity="right"
                 android:layout_marginTop="@dimen/iscs_space_2"
-                android:layout_marginRight="@dimen/iscs_space_4"
+                android:layout_marginRight="@dimen/iscs_space_2"
                 android:background="@drawable/common_btn_white_board"
                 android:paddingHorizontal="@dimen/iscs_space_2"
                 android:paddingVertical="@dimen/iscs_space_1"
@@ -89,8 +89,8 @@
                     android:showDividers="middle">
 
                     <View
-                        android:layout_width="@dimen/iscs_space_4"
-                        android:layout_height="@dimen/iscs_space_4"
+                        android:layout_width="@dimen/iscs_space_2"
+                        android:layout_height="@dimen/iscs_space_2"
                         android:background="@drawable/login_tip_circle" />
 
                     <TextView
@@ -124,7 +124,7 @@
             android:layout_alignParentRight="true"
             android:layout_alignParentBottom="true"
             android:layout_marginRight="@dimen/iscs_space_2"
-            android:layout_marginBottom="@dimen/iscs_space_4"
+            android:layout_marginBottom="@dimen/iscs_space_2"
             android:textColor="?attr/colorTextPrimary"
             android:textSize="@dimen/iscs_text_sm"
             tools:text="v1.0" />

+ 1 - 1
app/src/main/res/layout-land/item_login_method.xml

@@ -12,7 +12,7 @@
             android:layout_width="@dimen/login_circle_view_size"
             android:layout_height="@dimen/login_circle_view_size"
             android:layout_alignParentRight="true"
-            android:layout_marginTop="@dimen/iscs_space_4"
+            android:layout_marginTop="@dimen/iscs_space_2"
             android:layout_marginRight="@dimen/iscs_space_2"
             android:background="@drawable/login_tip_circle"
             android:visibility="gone" />

+ 3 - 3
app/src/main/res/layout/activity_login.xml

@@ -14,10 +14,10 @@
             android:id="@+id/header_layout"
             android:layout_width="match_parent"
             android:layout_height="@dimen/header_height"
-            android:layout_marginHorizontal="@dimen/iscs_space_4"
-            android:layout_marginTop="@dimen/iscs_space_4"
+            android:layout_marginHorizontal="@dimen/iscs_space_2"
+            android:layout_marginTop="@dimen/iscs_space_2"
             android:background="@drawable/bg_card_login_header_bg_radius_md"
-            android:paddingHorizontal="@dimen/iscs_space_4">
+            android:paddingHorizontal="@dimen/iscs_space_2">
 
             <ImageView
                 android:layout_width="@dimen/header_logo_width"

+ 107 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/bg_main"
+        android:fitsSystemWindows="true"
+        android:orientation="vertical">
+
+
+        <FrameLayout
+            android:id="@+id/header_layout"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/header_height"
+            android:layout_margin="@dimen/iscs_space_2"
+            android:paddingHorizontal="@dimen/iscs_space_1">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="horizontal">
+
+                <FrameLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/bg_home_header"
+                    android:orientation="horizontal">
+
+                    <ImageView
+                        android:layout_width="@dimen/header_logo_width"
+                        android:layout_height="@dimen/header_logo_height"
+                        android:layout_gravity="center_vertical"
+                        app:skinSrc='@{"icon_logo.png"}' />
+
+                    <TextClock
+                        android:layout_width="wrap_content"
+                        android:layout_height="match_parent"
+                        android:layout_gravity="right"
+                        android:format12Hour="yyyy-MM-dd HH:mm:ss"
+                        android:format24Hour="yyyy-MM-dd HH:mm:ss"
+                        android:gravity="center"
+                        android:paddingHorizontal="@dimen/iscs_space_1"
+                        android:textColor="?attr/colorTextPrimary"
+                        android:textSize="@dimen/iscs_text_md" />
+                </FrameLayout>
+
+                <LinearLayout
+                    android:id="@+id/user_info_layout"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:layout_marginLeft="@dimen/iscs_space_2"
+                    android:background="@drawable/bg_card_blue_radius_md"
+                    android:orientation="horizontal"
+                    android:paddingHorizontal="@dimen/iscs_space_2">
+
+                    <ImageView
+                        android:id="@+id/avatar"
+                        android:layout_width="@dimen/avatar_size"
+                        android:layout_height="@dimen/avatar_size"
+                        android:layout_gravity="center_vertical"
+                        android:padding="@dimen/iscs_space_1"
+                        android:src="@mipmap/icon_avatar"
+                        android:tint="?attr/colorWhite" />
+
+                    <TextView
+                        android:id="@+id/nickname"
+                        android:layout_width="wrap_content"
+                        android:layout_height="match_parent"
+                        android:layout_marginLeft="@dimen/iscs_space_2"
+                        android:ellipsize="end"
+                        android:gravity="center"
+                        android:maxLength="4"
+                        android:singleLine="true"
+                        android:textColor="?attr/colorTextPrimary"
+                        android:textSize="@dimen/iscs_text_md"
+                        tools:text="张三" />
+                </LinearLayout>
+            </LinearLayout>
+        </FrameLayout>
+
+        <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/nav_host_fragment"
+            android:name="androidx.navigation.fragment.NavHostFragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_above="@+id/nav_bar"
+            android:layout_below="@+id/header_layout"
+            app:defaultNavHost="true"
+            app:navGraph="@navigation/nav_home" />
+
+        <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"
+            app:navBarBackground="?attr/colorSecBg"
+            app:navIconSelectedSize="@dimen/home_bottom_nav_icon_size"
+            app:navIconSize="@dimen/home_bottom_nav_icon_size"
+            app:navOrientation="horizontal"
+            app:navTextSelectedSize="@dimen/iscs_text_sm"
+            app:navTextSize="@dimen/iscs_text_sm" />
+    </RelativeLayout>
+</layout>

+ 4 - 0
app/src/main/res/layout/fragment_data_home.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+</layout>

+ 4 - 0
app/src/main/res/layout/fragment_exception_home.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+</layout>

+ 4 - 0
app/src/main/res/layout/fragment_home.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+</layout>

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

@@ -20,7 +20,7 @@
                 android:layout_height="wrap_content"
                 android:layout_gravity="right"
                 android:layout_marginTop="@dimen/iscs_space_2"
-                android:layout_marginRight="@dimen/iscs_space_4"
+                android:layout_marginRight="@dimen/iscs_space_2"
                 android:background="@drawable/common_btn_white_board"
                 android:paddingHorizontal="@dimen/iscs_space_2"
                 android:paddingVertical="@dimen/iscs_space_1"
@@ -48,6 +48,7 @@
                 android:layout_gravity="center_horizontal"
                 android:layout_marginTop="@dimen/iscs_space_2"
                 android:gravity="center"
+                android:visibility="gone"
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_xs"
                 app:i18nKey='@{"administrator_mode"}' />
@@ -57,7 +58,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
-                android:layout_marginTop="@dimen/iscs_space_4"
+                android:layout_marginTop="@dimen/iscs_space_2"
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_lg"
                 android:textStyle="bold|italic"
@@ -80,8 +81,8 @@
                 android:showDividers="middle">
 
                 <View
-                    android:layout_width="@dimen/iscs_space_4"
-                    android:layout_height="@dimen/iscs_space_4"
+                    android:layout_width="@dimen/iscs_space_2"
+                    android:layout_height="@dimen/iscs_space_2"
                     android:background="@drawable/login_tip_circle" />
 
                 <TextView
@@ -100,6 +101,7 @@
             android:id="@+id/tec_support_layout"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
             android:layout_marginBottom="@dimen/iscs_space_2">
 
             <TextView
@@ -119,7 +121,7 @@
                 android:layout_alignParentRight="true"
                 android:layout_alignParentBottom="true"
                 android:layout_gravity="right"
-                android:layout_marginRight="@dimen/iscs_space_4"
+                android:layout_marginRight="@dimen/iscs_space_2"
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_sm"
                 tools:text="v1.0" />

+ 1 - 1
app/src/main/res/layout/item_login_method.xml

@@ -17,7 +17,7 @@
                 android:layout_width="@dimen/login_circle_view_size"
                 android:layout_height="@dimen/login_circle_view_size"
                 android:layout_alignParentRight="true"
-                android:layout_marginTop="@dimen/iscs_space_4"
+                android:layout_marginTop="@dimen/iscs_space_2"
                 android:layout_marginRight="@dimen/iscs_space_2"
                 android:background="@drawable/login_tip_circle"
                 android:visibility="gone" />

+ 11 - 0
app/src/main/res/navigation/nav_data_manage.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/nav_data_manage"
+    app:startDestination="@id/dataHomeFragment">
+
+    <fragment
+        android:id="@+id/dataHomeFragment"
+        android:name="com.grkj.iscs_mc.features.main.fragment.DataHomeFragment"
+        android:label="DataHomeFragment" />
+</navigation>

+ 11 - 0
app/src/main/res/navigation/nav_exception_manage.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/nav_exception_manage"
+    app:startDestination="@id/exceptionHomeFragment">
+
+    <fragment
+        android:id="@+id/exceptionHomeFragment"
+        android:name="com.grkj.iscs_mc.features.main.fragment.ExceptionHomeFragment"
+        android:label="ExceptionHomeFragment" />
+</navigation>

+ 11 - 0
app/src/main/res/navigation/nav_home.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/nav_home"
+    app:startDestination="@id/homeFragment">
+
+    <fragment
+        android:id="@+id/homeFragment"
+        android:name="com.grkj.iscs_mc.features.main.fragment.HomeFragment"
+        android:label="HomeFragment" />
+</navigation>

+ 6 - 0
app/src/main/res/navigation/nav_user_info.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/nav_user_info">
+
+</navigation>

+ 67 - 0
data/src/main/java/com/grkj/data/common/CommonConstants.kt

@@ -0,0 +1,67 @@
+package com.grkj.data.common
+
+/**
+ * 通用常量
+ */
+object CommonConstants {
+
+    /**
+     * http前缀
+     */
+    const val PREFIX_HTTP = "http://"
+
+    /**
+     * https前缀
+     */
+    const val PREFIX_HTTPS = "https://"
+
+    /**
+     * 指纹文件夹
+     */
+    const val FINGERPRINT_FOLDER = "fingerprint"
+
+    /**
+     * 默认最大指纹录入数量
+     */
+    const val DEFAULT_MAX_FINGERPRINT_INSERT_SIZE = 3
+
+    /**
+     * 默认自动登出时间
+     */
+    const val DEFAULT_AUTO_LOGOUT_TIME = 1800_000L
+
+    /**
+     * 人脸文件夹
+     */
+    const val FACE_FOLDER = "face"
+
+    /**
+     * 头像文件夹
+     */
+    const val AVATAR_FOLDER = "avatar"
+
+    /**
+     * 导入文件位置
+     */
+    val workflowModeZipFilePath = "/sdcard/iscs/workflowMode.zip"
+
+    /**
+     * 手机号正则
+     */
+    const val REGEX_MOBILE = "^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$"
+
+    /**
+     * 密码正则
+     */
+    const val REGEX_USERNAME = "^[A-Za-z0-9]{6,20}$"
+
+    /**
+     * 密码正则
+     */
+    const val REGEX_PASSWORD = "^[A-Za-z0-9!\"#\$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~]{6,20}$"
+
+    /**
+     * 蓝牙断连时间
+     */
+    const val BLE_DISCONNECT_DELAY_TIME = 60_000L
+}

+ 6 - 0
data/src/main/java/com/grkj/data/common/EventConstants.kt

@@ -14,10 +14,16 @@ object EventConstants {
      * 重启app
      */
     const val EVENT_RESTART_APP: Int = 100_000_002
+
     /**
      * 备份完成推送
      */
     const val EVENT_BACKUP_COMPLETE_CODE: Int = 100_000_003
+
+    /**
+     * 退出登录
+     */
+    const val EVENT_LOGOUT: Int = 100_000_004
     //---------------------------硬件通知------------------------
     /**
      * RFID读卡事件

+ 10 - 0
data/src/main/java/com/grkj/data/common/MMKVConstants.kt

@@ -9,4 +9,14 @@ object MMKVConstants {
      * 服务器地址
      */
     const val SERVER_ADDRESS = "server_address"
+
+    /**
+     * 最大指纹录入
+     */
+    const val KEY_MAX_FINGERPRINT_INSERT = "key_max_fingerprint_insert"
+
+    /**
+     * 自动退出时间
+     */
+    const val KEY_AUTO_LOGOUT_TIME = "key_auto_logout_time"
 }

+ 6 - 0
data/src/main/java/com/grkj/data/di/AppEntryPoint.kt

@@ -1,5 +1,6 @@
 package com.grkj.data.di
 
+import com.grkj.data.domain.logic.ISysLogic
 import com.grkj.data.domain.logic.IUserLogic
 import dagger.hilt.EntryPoint
 import dagger.hilt.InstallIn
@@ -12,4 +13,9 @@ interface AppEntryPoint {
      * 用户
      */
     fun userLogic(): IUserLogic
+
+    /**
+     * 系统
+     */
+    fun sysLogic(): ISysLogic
 }

+ 7 - 0
data/src/main/java/com/grkj/data/di/DatabaseModule.kt

@@ -1,6 +1,7 @@
 package com.grkj.data.di
 
 import com.grkj.data.local.dao.HardwareDao
+import com.grkj.data.local.dao.SysDao
 import com.grkj.data.local.dao.UserDao
 import com.grkj.data.local.database.ISCSDatabase
 import dagger.Module
@@ -31,6 +32,12 @@ object DatabaseModule {
     @Provides
     fun provideUserDao(db: ISCSDatabase): UserDao = db.userDao()
 
+    /**
+     * 系统表提供
+     */
+    @Provides
+    fun provideSysDao(db: ISCSDatabase): SysDao = db.sysDao()
+
     /**
      * 硬件表提供
      */

+ 7 - 0
data/src/main/java/com/grkj/data/di/LogicManager.kt

@@ -1,6 +1,7 @@
 package com.grkj.data.di
 
 import android.app.Application
+import com.grkj.data.domain.logic.ISysLogic
 import com.grkj.data.domain.logic.IUserLogic
 import com.grkj.data.domain.logic.impl.UserLogic
 import dagger.hilt.android.EntryPointAccessors
@@ -13,8 +14,14 @@ object LogicManager {
      * 用户
      */
     lateinit var userLogic: IUserLogic
+
+    /**
+     * 系统
+     */
+    lateinit var sysLogic: ISysLogic
     fun init(app: Application) {
         val ep = EntryPointAccessors.fromApplication(app, AppEntryPoint::class.java)
         userLogic = ep.userLogic()
+        sysLogic = ep.sysLogic()
     }
 }

+ 9 - 0
data/src/main/java/com/grkj/data/di/LogicModule.kt

@@ -1,6 +1,8 @@
 package com.grkj.data.di
 
+import com.grkj.data.domain.logic.ISysLogic
 import com.grkj.data.domain.logic.IUserLogic
+import com.grkj.data.domain.logic.impl.SysLogic
 import com.grkj.data.domain.logic.impl.UserLogic
 import dagger.Module
 import dagger.Provides
@@ -30,4 +32,11 @@ object LogicModule {
     @Provides
     @Singleton
     fun provideUserLogic(userLogic: UserLogic): IUserLogic = userLogic
+
+    /**
+     * 系统业务逻辑提供
+     */
+    @Provides
+    @Singleton
+    fun provideSysLogic(sysLogic: SysLogic): ISysLogic = sysLogic
 }

+ 14 - 0
data/src/main/java/com/grkj/data/di/RepositoryModule.kt

@@ -2,10 +2,13 @@ package com.grkj.data.di
 
 import com.grkj.data.common.MMKVConstants
 import com.grkj.data.repository.IHardwareRepository
+import com.grkj.data.repository.ISysRepository
 import com.grkj.data.repository.IUserRepository
 import com.grkj.data.repository.impl.network.NetworkHardwareRepository
+import com.grkj.data.repository.impl.network.NetworkSysRepository
 import com.grkj.data.repository.impl.network.NetworkUserRepository
 import com.grkj.data.repository.impl.standard.StandardHardwareRepository
+import com.grkj.data.repository.impl.standard.StandardSysRepository
 import com.grkj.data.repository.impl.standard.StandardUserRepository
 import com.sik.sikcore.extension.getMMKVData
 import dagger.Module
@@ -47,4 +50,15 @@ object RepositoryModule {
         network: NetworkHardwareRepository,
     ): IHardwareRepository =
         if (MMKVConstants.SERVER_ADDRESS.getMMKVData("").isNotEmpty()) network else standard
+
+    /**
+     * 系统仓储注入
+     */
+    @Provides
+    @Singleton
+    fun provideSysRepository(
+        standard: StandardSysRepository,
+        network: NetworkSysRepository,
+    ): ISysRepository =
+        if (MMKVConstants.SERVER_ADDRESS.getMMKVData("").isNotEmpty()) network else standard
 }

+ 18 - 0
data/src/main/java/com/grkj/data/domain/logic/ISysLogic.kt

@@ -0,0 +1,18 @@
+package com.grkj.data.domain.logic
+
+import com.grkj.data.local.dos.SysRole
+
+/**
+ * 系统业务
+ */
+interface ISysLogic {
+    /**
+     * 添加预设角色
+     */
+    fun addPresetRoleData(presetSysRole: List<SysRole>)
+
+    /**
+     * 检查系统菜单和角色
+     */
+    fun checkSysMenuAndRole()
+}

+ 21 - 0
data/src/main/java/com/grkj/data/domain/logic/impl/SysLogic.kt

@@ -0,0 +1,21 @@
+package com.grkj.data.domain.logic.impl
+
+import com.grkj.data.domain.logic.BaseLogic
+import com.grkj.data.domain.logic.ISysLogic
+import com.grkj.data.local.dos.SysRole
+import com.grkj.data.repository.ISysRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class SysLogic @Inject constructor(
+    val sysRepository: ISysRepository
+) : BaseLogic(), ISysLogic {
+    override fun addPresetRoleData(presetSysRole: List<SysRole>) {
+        sysRepository.addPresetRoleData(presetSysRole)
+    }
+
+    override fun checkSysMenuAndRole() {
+        sysRepository.checkSysMenuAndRole()
+    }
+}

+ 13 - 0
data/src/main/java/com/grkj/data/enums/RoleEnum.kt

@@ -0,0 +1,13 @@
+package com.grkj.data.enums
+
+/**
+ * 角色枚举
+ */
+enum class RoleEnum(val roleKey: String, val description: String) {
+    ADMIN("admin", "admin"),
+    JTDRAWER("jtdrawer", "jtdrawer"),
+    JTLOCKER("jtlocker", "jtlocker"),
+    JTCOLOCKER("jtcolocker", "jtcolocker"),
+    JTGUARD("jtguard", "jtguard"),
+    SYSCONFIG("sysconfig", "sysconfig");
+}

+ 95 - 0
data/src/main/java/com/grkj/data/enums/RoleFunctionalPermissionsEnum.kt

@@ -0,0 +1,95 @@
+package com.grkj.data.enums
+
+/**
+ * 功能权限(菜单)
+ */
+enum class RoleFunctionalPermissionsEnum(
+    val functionalPermission: String,
+    val description: String,
+    val level: Int,
+    val children: List<RoleFunctionalPermissionsEnum>
+) {
+    USER_MANAGE("data_manage:user_manage", "user_manage", 1, listOf()),
+    ROLE_MANAGE("data_manage:role_manage", "role_manage", 1, listOf()),
+    WORKSTATION_MANAGE(
+        "data_manage:workstation_manage",
+        "workstation_manage",
+        1,
+        listOf()
+    ),
+    EXCHANGE_RECORD(
+        "data_manage:exchange_record",
+        "exchange_record",
+        1,
+        listOf()
+    ),
+    USER_INFO("user_info:user_info", "user_info", 1, listOf()),
+    RESET_PASSWORD("user_info:reset_password", "reset_password", 1, listOf()),
+    FINGERPRINT_SETTING(
+        "user_info:fingerprint_setting",
+        "fingerprint_setting",
+        1,
+        listOf()
+    ),
+    FACE_SETTING("user_info:face_setting", "face_setting", 1, listOf()),
+    CARD_SETTING("user_info:card_setting", "card_setting", 1, listOf()),
+    SETTINGS("user_info:settings", "settings", 1, listOf()),
+    LOGOUT("user_info:logout", "logout", 1, listOf()),
+
+    USER_INFO_HOME(
+        "user_info",
+        "user_info",
+        0,
+        listOf(
+            USER_INFO,
+            RESET_PASSWORD,
+            FINGERPRINT_SETTING,
+            FACE_SETTING,
+            CARD_SETTING,
+            SETTINGS,
+            LOGOUT
+        )
+    ),
+    EXCEPTION_HOME_MANAGE(
+        "exception_manage",
+        "exception_manage", 0,
+        listOf()
+    ),
+    DATA_HOME_MANAGE(
+        "data_manage",
+        "data_manage", 0,
+        listOf(
+            USER_MANAGE,
+            ROLE_MANAGE,
+            WORKSTATION_MANAGE,
+            EXCHANGE_RECORD
+        )
+    ),
+    HOME("Home", "home", 0, listOf()),
+    ;
+
+    companion object {
+        /**
+         * 返回去除了指定枚举及其所有子节点后的列表
+         */
+        fun except(vararg excludes: RoleFunctionalPermissionsEnum): List<RoleFunctionalPermissionsEnum> {
+            // 收集所有要排除的枚举(包括自己和子孙)
+            val excludeSet = excludes
+                .flatMap { it.collectSelfAndChildren() }
+                .toSet()
+            // 过滤掉
+            return values().filter { it !in excludeSet }
+        }
+
+        /**
+         * 递归收集自己和所有子节点
+         */
+        private fun RoleFunctionalPermissionsEnum.collectSelfAndChildren(): Set<RoleFunctionalPermissionsEnum> {
+            val result = mutableSetOf(this)
+            for (child in children) {
+                result += child.collectSelfAndChildren()
+            }
+            return result
+        }
+    }
+}

+ 248 - 0
data/src/main/java/com/grkj/data/hardware/can/CanCommand.kt

@@ -0,0 +1,248 @@
+package com.grkj.data.hardware.can
+
+import com.sik.comm.impl_can.SdoDialect
+import com.sik.comm.impl_can.SdoRequest
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * 按设备类型分组的 CAN 指令集
+ *
+ * 使用方式:
+ * 1) 先调用 Common.getDeviceType(nodeId) 发读请求,拿到设备类型原始2字节;
+ * 2) 用 Common.parseDeviceType(payload) 得到 DeviceType;
+ * 3) 调用 CanCommands.forDevice(nodeId, deviceType) 获得对应命令集,然后用里面的方法发 SDO。
+ */
+object CanCommands {
+    /**
+     * SDO 协议指令集:
+     */
+    val sdoDialect: SdoDialect = SdoDialect(
+        READ = 0x40,
+        READ_1B = 0x2F,
+        READ_2B = 0x4B,
+        READ_4B = 0x23,
+        READ_ERROR = 0x80,
+        WRITE_1B = 0x2F,
+        WRITE_2B = 0x2B,
+        WRITE_4B = 0x23,
+        WRITE_ACK = 0x60,
+        WRITE_ERROR = 0x80
+    )
+
+    // ========= 公共区:所有设备通用 =========
+    object Common {
+        /** 设备类型 (R) → 0x6000/0x00, 2B */
+        fun getDeviceType(nodeId: Int): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6000, 0x00)
+
+        /** 解析设备类型(小端 16 位,低字节为主) */
+        fun parseDeviceType(payload: ByteArray): DeviceType {
+            if (payload.isEmpty()) return DeviceType.Unknown
+            val v =
+                if (payload.size >= 2) ((payload[1].toInt() and 0xFF) shl 8) or (payload[0].toInt() and 0xFF)
+                else (payload[0].toInt() and 0xFF)
+            return DeviceType.fromCode(v)
+        }
+
+        /** 版本 (R) → 0x6003/0x00, 4B: HW主,HW子,SW主,SW子 */
+        fun getDeviceVersion(nodeId: Int): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6003, 0x00)
+    }
+
+    // ========= 工厂:按类型返回对应命令集 =========
+    fun forDevice(nodeId: Int): CommandSet = when (CanHelper.getDeviceType(nodeId)) {
+        DeviceType.EKeyDock -> EKeyDockCommands(nodeId)
+        DeviceType.FiveLockDock -> FiveLockDockCommands(nodeId)
+        DeviceType.KeyCabinet -> KeyCabinetCommands(nodeId)
+        DeviceType.MaterialCabinet -> MaterialCabinetCommands(nodeId)
+        DeviceType.Unknown -> UnknownCommands(nodeId)
+    }
+
+    // ========= 类型定义 =========
+
+    /** 文档里的主控板类型(0/1/2/3),其余视为 Unknown */
+    enum class DeviceType(val code: Int) {
+        EKeyDock(0),          // 电子钥匙底座(左右位)
+        FiveLockDock(1),      // 5路挂锁底座(1..5位)
+        KeyCabinet(2),        // 钥匙柜控制板(多为5位同构,也可扩)
+        MaterialCabinet(3),   // 物资柜主控(支持RGB状态灯等)
+        Unknown(-1);
+
+        companion object {
+            fun fromCode(code: Int): DeviceType = when (code) {
+                0 -> EKeyDock
+                1 -> FiveLockDock
+                2 -> KeyCabinet
+                3 -> MaterialCabinet
+                else -> Unknown
+            }
+        }
+    }
+
+    /** 各设备命令集统一接口(可按需加通用方法) */
+    interface CommandSet {
+        val nodeId: Int
+
+        /** 设备当前状态(大多数设备复用 0x6010/0x00, 2B),具体位义由各实现说明 */
+        fun getStatus(): SdoRequest.Read
+
+        /** 获取版本通用接口 */
+        fun getVersion(): SdoRequest.Read = Common.getDeviceVersion(nodeId)
+    }
+
+    // ========= 电子钥匙底座(左右位) =========
+
+    class EKeyDockCommands(override val nodeId: Int) : CommandSet {
+        /** 状态 (R) 0x6010/0x00, 2B
+         *  bit0:左卡扣, bit1:左充电, bit4:右卡扣, bit5:右充电,
+         *  bit8/9/12/13 工作位标识等(读侧用)
+         */
+        override fun getStatus(): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6010, 0x00)
+
+        /** 读/写 控制/状态 (R/W) 0x6011/0x00, 2B(写:仅置相关位,其余位写0) */
+        fun readControlReg(): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6011, 0x00)
+
+        /** 设置左右卡扣(写 0x6011) */
+        fun setLatch(left: Boolean? = null, right: Boolean? = null): SdoRequest.Write {
+            var v = 0
+            if (left != null) v = v or ((if (left) 1 else 0) shl 0)
+            if (right != null) v = v or ((if (right) 1 else 0) shl 4)
+            return SdoRequest.Write(nodeId, 0x6011, 0x00, shortLE(v), 2)
+        }
+
+        /** 设置左右充电(写 0x6011) */
+        fun setCharge(leftOn: Boolean? = null, rightOn: Boolean? = null): SdoRequest.Write {
+            var v = 0
+            if (leftOn != null) v = v or ((if (leftOn) 1 else 0) shl 1)
+            if (rightOn != null) v = v or ((if (rightOn) 1 else 0) shl 5)
+            return SdoRequest.Write(nodeId, 0x6011, 0x00, shortLE(v), 2)
+        }
+
+        /** 单侧卡扣语法糖:keySlotId: 0左/1右;status: 0解锁/1锁住 */
+        fun controlLatch(keySlotId: Int, status: Int): SdoRequest.Write {
+            require(keySlotId in 0..1) { "keySlotId must be 0(left)/1(right)" }
+            require(status == 0 || status == 1) { "status must be 0/1" }
+            val bit = if (keySlotId == 0) 0 else 4
+            return SdoRequest.Write(nodeId, 0x6011, 0x00, shortLE((status and 1) shl bit), 2)
+        }
+
+        /** 左/右 RFID (R) 4B 小端 */
+        fun getLeftRfid(): SdoRequest.Read = SdoRequest.Read(nodeId, 0x6020, 0x00)
+        fun getRightRfid(): SdoRequest.Read = SdoRequest.Read(nodeId, 0x6024, 0x00)
+    }
+
+    // ========= 5 路挂锁底座(1..5位) =========
+
+    class FiveLockDockCommands(override val nodeId: Int) : CommandSet {
+        /** 锁位状态 (R) 0x6010/0x00, 2B: bit0..bit4 对应 1..5 号位已锁 */
+        override fun getStatus(): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6010, 0x00)
+
+        /** 控制/状态 (R/W) 0x6011/0x00, 2B:写 bit0..bit4 控制 1..5 号位 */
+        fun readControlReg(): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6011, 0x00)
+
+        /** 一次写入 5 位控制(低5位有效) */
+        fun setLatchBits(bits01to05: Int): SdoRequest.Write {
+            val v = bits01to05 and 0b1_1111
+            return SdoRequest.Write(nodeId, 0x6011, 0x00, shortLE(v), 2)
+        }
+
+        /** 单位控制(1..5) */
+        fun controlOne(slotIndex1to5: Int, locked: Boolean): SdoRequest.Write {
+            require(slotIndex1to5 in 1..5) { "slotIndex must be 1..5" }
+            val v = (if (locked) 1 else 0) shl (slotIndex1to5 - 1)
+            return SdoRequest.Write(nodeId, 0x6011, 0x00, shortLE(v), 2)
+        }
+
+        /** 各位 RFID (R) 0x6020..0x6024 /0x00, 4B 小端 */
+        fun getSlotRfid(slotIndex1to5: Int): SdoRequest.Read {
+            require(slotIndex1to5 in 1..5) { "slotIndex must be 1..5" }
+            return SdoRequest.Read(nodeId, 0x6020 + (slotIndex1to5 - 1), 0x00)
+        }
+    }
+
+    // ========= 钥匙柜控制板(通常与 FiveLock 类似,保留扩展位) =========
+
+    class KeyCabinetCommands(override val nodeId: Int) : CommandSet {
+        /** 状态 (R) 0x6010/0x00(实现与 FiveLock 类似,具体位义按柜体定义) */
+        override fun getStatus(): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6010, 0x00)
+
+        /** 控制/状态 (R/W) 0x6011/0x00,低5位通常对应 1..5 号位 */
+        fun setLatchBits(bits01to05: Int): SdoRequest.Write =
+            SdoRequest.Write(nodeId, 0x6011, 0x00, shortLE(bits01to05 and 0b1_1111), 2)
+
+        fun controlOne(slotIndex1to5: Int, locked: Boolean): SdoRequest.Write {
+            require(slotIndex1to5 in 1..5) { "slotIndex must be 1..5" }
+            val v = (if (locked) 1 else 0) shl (slotIndex1to5 - 1)
+            return SdoRequest.Write(nodeId, 0x6011, 0x00, shortLE(v), 2)
+        }
+
+        fun getSlotRfid(slotIndex1to5: Int): SdoRequest.Read {
+            require(slotIndex1to5 in 1..5) { "slotIndex must be 1..5" }
+            return SdoRequest.Read(nodeId, 0x6020 + (slotIndex1to5 - 1), 0x00)
+        }
+    }
+
+    // ========= 物资柜主控(RGB 状态灯等) =========
+
+    class MaterialCabinetCommands(override val nodeId: Int) : CommandSet {
+        /** 柜体状态 (R) 如需另定寄存器再扩展;暂复用 0x6010 */
+        override fun getStatus(): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6010, 0x00)
+
+        /** RGB 状态灯 (R/W) 0x6016/0x00, 4B: B[0..7],G[8..15],R[16..23], 模式[24..26], 时间[27..29], 单位[30], 锁定[31] */
+        fun getRgb(): SdoRequest.Read = SdoRequest.Read(nodeId, 0x6016, 0x00)
+
+        fun setRgb(
+            r: Int, g: Int, b: Int,      // 0..255
+            mode: Int,                    // 0关/1常亮/2闪烁/3呼吸/4流水
+            timeStep: Int,                // 0..7(实际=+1)
+            secondsUnit: Boolean,         // false=100ms, true=1000ms
+            lockControl: Boolean          // 是否锁定主控权
+        ): SdoRequest.Write {
+            val rb = clamp8(b)
+            val gg = clamp8(g)
+            val rr = clamp8(r)
+            val mm = clamp3(mode)
+            val tt = clamp3(timeStep)
+            var v = 0
+            v = v or (rb and 0xFF)
+            v = v or ((gg and 0xFF) shl 8)
+            v = v or ((rr and 0xFF) shl 16)
+            v = v or ((mm and 0x07) shl 24)
+            v = v or ((tt and 0x07) shl 27)
+            v = v or ((if (secondsUnit) 1 else 0) shl 30)
+            v = v or ((if (lockControl) 1 else 0) shl 31)
+            return SdoRequest.Write(nodeId, 0x6016, 0x00, intLE(v), 4)
+        }
+
+        /** 板载温湿度 (R) 2B,小端;温度有符号/10,湿度无符号/10 */
+        fun getTemperature(): SdoRequest.Read = SdoRequest.Read(nodeId, 0x6017, 0x00)
+        fun getHumidity(): SdoRequest.Read = SdoRequest.Read(nodeId, 0x6018, 0x00)
+    }
+
+    // ========= 未知类型占位,防空指针 =========
+    class UnknownCommands(override val nodeId: Int) : CommandSet {
+        override fun getStatus(): SdoRequest.Read =
+            SdoRequest.Read(nodeId, 0x6010, 0x00)
+    }
+
+    // ========= Byte 打包工具(LE) =========
+    private fun shortLE(v: Int): ByteArray =
+        byteArrayOf((v and 0xFF).toByte(), ((v ushr 8) and 0xFF).toByte())
+
+    private fun intLE(v: Int): ByteArray = byteArrayOf(
+        (v and 0xFF).toByte(),
+        ((v ushr 8) and 0xFF).toByte(),
+        ((v ushr 16) and 0xFF).toByte(),
+        ((v ushr 24) and 0xFF).toByte()
+    )
+
+    private fun clamp8(x: Int) = min(255, max(0, x))
+    private fun clamp3(x: Int) = min(7, max(0, x))
+}

+ 107 - 0
data/src/main/java/com/grkj/data/hardware/can/CanHelper.kt

@@ -0,0 +1,107 @@
+package com.grkj.data.hardware.can
+
+import com.sik.comm.core.protocol.ProtocolManager
+import com.sik.comm.core.protocol.ProtocolType
+import com.sik.comm.impl_can.CanProtocol
+import com.sik.comm.impl_can.SdoRequest
+import com.sik.comm.impl_can.SdoResponse
+import com.sik.comm.impl_can.toCommMessage
+import com.sik.comm.impl_can.toSdoResponse
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * can总线帮助工具
+ */
+object CanHelper {
+    private val logger: Logger = LoggerFactory.getLogger(CanHelper::class.java)
+
+    /**
+     * 作用域
+     */
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+
+    /**
+     * 节点对应的设备类型列表
+     */
+    private val nodeMap: HashMap<Int, CanCommands.DeviceType> = hashMapOf()
+
+    /**
+     * 扫描节点范围
+     */
+    private val scanRange: IntRange = 1..4
+
+    /**
+     * 连接
+     */
+    fun connect() {
+        val canProtocol = CanProtocol()
+        ProtocolManager.register(ProtocolType.CAN, canProtocol)
+        // 绑定配置(每个节点一个 deviceId,建议 "can0@<nodeId>")
+        ProtocolManager.bindDeviceConfig(CustomCanConfig.instance)
+        canProtocol.registerConfig(CustomCanConfig.instance)
+        // 连接
+        ProtocolManager.connect(CustomCanConfig.instance.deviceId)
+    }
+
+    /**
+     * 检查节点
+     */
+    fun checkNode() {
+        for (i in scanRange) {
+            readFrom(CanCommands.Common.getDeviceType(i)) {
+                nodeMap[i] = CanCommands.Common.parseDeviceType(it.payload)
+            }
+        }
+    }
+
+    /**
+     * 根据节点id获取设备类型
+     */
+    fun getDeviceType(nodeId: Int): CanCommands.DeviceType {
+        return nodeMap[nodeId] ?: CanCommands.DeviceType.Unknown
+    }
+
+    /**
+     * 根据设备类型获取节点id
+     */
+    fun getNodeIdByDeviceType(deviceType: CanCommands.DeviceType): List<Int> {
+        return nodeMap.filter { it.value == deviceType }.map { it.key }
+    }
+
+    /**
+     * 读取
+     */
+    fun readFrom(req: SdoRequest.Read, callback: (SdoResponse.ReadData) -> Unit) {
+        scope.launch(Dispatchers.IO) {
+            runCatching {
+                ProtocolManager.getProtocol(CustomCanConfig.instance.deviceId)
+                    .send(CustomCanConfig.instance.deviceId, req.toCommMessage())
+            }.onSuccess { rsp ->
+                callback(rsp.toSdoResponse() as SdoResponse.ReadData)
+            }.onFailure {
+                logger.info("读取失败:${it}")
+            }
+        }
+    }
+
+    /**
+     * 写入到
+     */
+    fun writeTo(req: SdoRequest.Write, callback: (SdoResponse.WriteAck) -> Unit) {
+        scope.launch(Dispatchers.IO) {
+            runCatching {
+                ProtocolManager.getProtocol(CustomCanConfig.instance.deviceId)
+                    .send(CustomCanConfig.instance.deviceId, req.toCommMessage())
+            }.onSuccess { rsp ->
+                callback(rsp.toSdoResponse() as SdoResponse.WriteAck)
+            }.onFailure {
+                logger.info("写入失败:${it}")
+            }
+        }
+    }
+}

+ 51 - 0
data/src/main/java/com/grkj/data/hardware/can/CanSendDelayInterceptor.kt

@@ -0,0 +1,51 @@
+package com.grkj.data.hardware.can
+
+import com.sik.comm.core.interceptor.CommInterceptor
+import com.sik.comm.core.interceptor.InterceptorChain
+import com.sik.comm.core.model.CommMessage
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import android.os.SystemClock
+import kotlin.math.max
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * CAN 总线发送最小间隔拦截器(带协程锁)
+ */
+class CanSendDelayInterceptor(
+    private val minIntervalMs: Long = 50L  // 可配置
+) : CommInterceptor {
+
+    private val logger: Logger = LoggerFactory.getLogger(CanSendDelayInterceptor::class.java)
+
+    companion object {
+        // 用单一互斥量串行化“检查→等待→发送→更新时间”
+        private val mutex = Mutex()
+        // 用 elapsedRealtime 计时间隔,避免受系统时间调整影响
+        @Volatile var lastSendTick: Long = 0L
+    }
+
+    override suspend fun intercept(chain: InterceptorChain, original: CommMessage): CommMessage {
+        return mutex.withLock {
+            val now = SystemClock.elapsedRealtime()
+            val elapsed = now - lastSendTick
+            val waitMs = max(0L, minIntervalMs - elapsed)
+
+            if (waitMs > 0) {
+                logger.info("发送间隔 ${elapsed}ms < ${minIntervalMs}ms,延迟 ${waitMs}ms")
+                delay(waitMs)
+            } else {
+                logger.info("发送间隔 ${elapsed}ms,>= ${minIntervalMs}ms,直接发送")
+            }
+
+            // 发送
+            val rsp = chain.proceed(original)
+
+            // 注意:以“发送完成时刻”作为下次基准;如果你更想“开始发送时刻”,把这行挪到 proceed() 之前即可
+            lastSendTick = SystemClock.elapsedRealtime()
+            rsp
+        }
+    }
+}

+ 22 - 0
data/src/main/java/com/grkj/data/hardware/can/CustomCanConfig.kt

@@ -0,0 +1,22 @@
+package com.grkj.data.hardware.can
+
+import com.sik.comm.core.interceptor.CommInterceptor
+import com.sik.comm.impl_can.CanConfig
+
+/**
+ * 自定义can配置
+ */
+class CustomCanConfig : CanConfig(
+    deviceId = "can0@1",
+    interfaceName = "can0",
+    defaultNodeId = 1,
+    sdo = CanCommands.sdoDialect
+) {
+    companion object {
+        val instance by lazy { CustomCanConfig() }
+    }
+
+    private val canSendDelayInterceptor = CanSendDelayInterceptor()
+    override val additionalInterceptors: List<CommInterceptor>
+        get() = super.additionalInterceptors + canSendDelayInterceptor
+}

+ 56 - 0
data/src/main/java/com/grkj/data/local/dao/SysDao.kt

@@ -0,0 +1,56 @@
+package com.grkj.data.local.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+import com.grkj.data.local.dos.SysMenu
+import com.grkj.data.local.dos.SysRole
+import com.grkj.data.local.dos.SysRoleMenu
+
+/**
+ * 系统相关表
+ */
+@Dao
+interface SysDao {
+    /**
+     * 添加预设角色
+     */
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    fun addPresetRoleData(presetSysRole: List<SysRole>)
+
+    /** 根据 perms 查找对应的菜单记录(假定 perms 字段在数据库中唯一) */
+    @Query("SELECT * FROM sys_menu WHERE perms = :perms LIMIT 1")
+    fun findByPerms(perms: String): SysMenu?
+
+    /**
+     * 更新菜单
+     */
+    @Update(onConflict = OnConflictStrategy.REPLACE)
+    fun updateMenu(existing: SysMenu)
+
+    /**
+     * 插入菜单
+     */
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    fun insertMenu(newMenu: SysMenu): Long
+
+    /**
+     * 获取所有菜单
+     */
+    @Query("SELECT * FROM sys_menu")
+    fun getAllMenu(): List<SysMenu>
+
+    /**
+     * 获取所有角色
+     */
+    @Query("SELECT * FROM sys_role where del_flag = 0")
+    fun getRoleData(): List<SysRole>
+
+    /**
+     * 插入角色菜单
+     */
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    fun insertRoleMenus(roleMenuData: MutableList<SysRoleMenu>)
+}

+ 54 - 14
data/src/main/java/com/grkj/data/local/database/ISCSDatabase.kt

@@ -10,8 +10,13 @@ import androidx.room.TypeConverters
 import androidx.sqlite.db.SupportSQLiteDatabase
 import com.grkj.data.converters.Converters
 import com.grkj.data.local.dao.HardwareDao
+import com.grkj.data.local.dao.SysDao
 import com.grkj.data.local.dao.UserDao
 import com.grkj.data.local.dos.IsJobCardDo
+import com.grkj.data.local.dos.IsWorkstation
+import com.grkj.data.local.dos.SysMenu
+import com.grkj.data.local.dos.SysRole
+import com.grkj.data.local.dos.SysRoleMenu
 import com.grkj.data.local.dos.SysUserCharacteristicDo
 import com.grkj.data.local.dos.SysUserDo
 import com.grkj.shared.config.AESConfig
@@ -30,7 +35,8 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase as CipherDB
  * 本地数据库(SQLCipher)
  */
 @Database(
-    entities = [SysUserDo::class, IsJobCardDo::class, SysUserCharacteristicDo::class],
+    entities = [SysUserDo::class, IsJobCardDo::class, SysUserCharacteristicDo::class, IsWorkstation::class,
+        SysMenu::class, SysRoleMenu::class, SysRole::class],
     version = ISCSMigrations.VERSION,
     exportSchema = true
 )
@@ -46,6 +52,11 @@ abstract class ISCSDatabase : RoomDatabase() {
      */
     abstract fun hardwareDao(): HardwareDao
 
+    /**
+     * 系统相关表
+     */
+    abstract fun sysDao(): SysDao
+
     companion object {
         const val DB_NAME = "iscs_mc_database.db"
         private val logger: Logger = LoggerFactory.getLogger(ISCSDatabase::class.java)
@@ -73,8 +84,9 @@ abstract class ISCSDatabase : RoomDatabase() {
         private fun buildDatabase(mode: ReopenMode): ISCSDatabase {
             val context = SIKCore.getApplication()
 
-            // 目录与首启落地(保持你原逻辑)
-            val parentDir = EXTERNAL_DB_FILE.parentFile
+            // 1) 确保目录
+            val parentDir =
+                EXTERNAL_DB_FILE.parentFile
             if (parentDir != null && !parentDir.exists()) {
                 if (!parentDir.mkdirs()) {
                     Log.e("ISCSDatabase", "无法创建目录: ${parentDir.absolutePath}")
@@ -82,20 +94,47 @@ abstract class ISCSDatabase : RoomDatabase() {
                     logger.info("创建目录: ${parentDir.absolutePath}")
                 }
             }
+
+            // 2) 复制预置库(可选)
+            var hasPrepackaged = false
             if (!EXTERNAL_DB_FILE.exists()) {
                 runCatching {
                     context.assets.open("data.db").use { input ->
-                        FileOutputStream(EXTERNAL_DB_FILE).use { output -> input.copyTo(output) }
+                        FileOutputStream(EXTERNAL_DB_FILE).use { output ->
+                            input.copyTo(
+                                output
+                            )
+                        }
                     }
+                    hasPrepackaged = true
                     logger.info("已从 assets 复制数据库到: ${EXTERNAL_DB_FILE.absolutePath}")
                 }.onFailure { e ->
-                    // ✅ 改成 info,提示可选库未提供
-                    logger.info("未提供初始 data.db,直接新建数据库")
+                    // 复制失败很正常(比如没放 data.db),这里仅记录日志,不要创建空文件
+                    logger.warn(
+                        "未找到 assets/data.db,将创建全新加密数据库",
+                        e
+                    )
                 }
+            } else {
+                // 外部已经有一个文件(可能是历史明文库)
+                hasPrepackaged = true
             }
-            ensureCiphered(EXTERNAL_DB_FILE)
 
-            // SQLCipher Hook
+            // 3) 仅当确实有“现成文件”时才尝试加密迁移
+            //    - 若是明文预置库:ensureCiphered(...) 里做 ATTACH/导出/替换
+            //    - 若不存在任何文件:跳过这步,交给 Room+SQLCipher 创建全新加密库
+            if (hasPrepackaged && EXTERNAL_DB_FILE.exists()) {
+                runCatching {
+                    ensureCiphered(EXTERNAL_DB_FILE)
+                }.onFailure { e ->
+                    // 不要让这里阻断启动;最坏情况:让 Room 直接用密钥打开(若是已加密)或报错日志
+                    logger.error("ensureCiphered 处理预置库失败,请检查是否是明文库或损坏文件", e)
+                }
+            } else {
+                logger.info("未检测到预置库,将由 Room+SQLCipher 创建全新加密数据库")
+            }
+
+            // 4) SQLCipher Hook
             val passphrase: ByteArray = AESConfig.defaultConfig.key()
             val hook = object : SQLiteDatabaseHook {
                 override fun preKey(connection: SQLiteConnection) { /* no-op */
@@ -107,16 +146,16 @@ abstract class ISCSDatabase : RoomDatabase() {
                     connection.execute("PRAGMA synchronous=NORMAL", null, null)
                 }
             }
-            // ✅ 修正:把 hook 传进来
             val factory = SupportOpenHelperFactory(passphrase, hook, true)
 
+            // 5) 构建 Room(让它去“创建或打开”)
             val builder = Room.databaseBuilder(
                 context,
                 ISCSDatabase::class.java,
-                EXTERNAL_DB_FILE.absolutePath // 这里也给绝对路径,配合上面的工厂双保险
+                EXTERNAL_DB_FILE.absolutePath
             )
                 .openHelperFactory(factory)
-                .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)
+                .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)
                 .addMigrations(*ISCSMigrations.migrationData)
                 .addCallback(object : RoomDatabase.Callback() {
                     override fun onOpen(db: SupportSQLiteDatabase) {
@@ -124,15 +163,16 @@ abstract class ISCSDatabase : RoomDatabase() {
                     }
                 })
 
-            // 恢复模式下**不要** fallbackToDestructiveMigration,避免清库
             if (mode == ReopenMode.NORMAL) {
-                // 如需保留 fallback,可在正常运行模式启用:
+                // 如需保留 fallback 可在正常模式启用:
                 // builder.fallbackToDestructiveMigration()
             }
 
             val db = builder.build()
-            // 触发实际打开
+
+            // 6) 触发实际打开(若不存在文件:此处会直接创建“已加密”的新库)
             db.openHelper.writableDatabase
+
             return db
         }
 

+ 29 - 0
data/src/main/java/com/grkj/data/local/database/PresetData.kt

@@ -0,0 +1,29 @@
+package com.grkj.data.local.database
+
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.grkj.data.local.dos.SysRole
+import com.grkj.shared.utils.extension.readJsonFromAssets
+import com.sik.sikcore.SIKCore
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * 预设数据
+ */
+object PresetData {
+    private val gson = Gson()
+    private const val ASSETS_DIR = "preset"
+
+    private val logger: Logger = LoggerFactory.getLogger(PresetData::class.java)
+
+    /**
+     * 预设角色
+     */
+    val presetSysRole: List<SysRole> = run {
+        val presetSysRole =
+            SIKCore.getApplication().readJsonFromAssets("$ASSETS_DIR/preset_sys_role.json")
+        logger.info("预设数据:$presetSysRole")
+        gson.fromJson(presetSysRole, object : TypeToken<List<SysRole>>() {}.type)
+    }
+}

+ 37 - 0
data/src/main/java/com/grkj/data/local/dos/IsWorkstation.kt

@@ -0,0 +1,37 @@
+package com.grkj.data.local.dos
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity("is_workstation")
+class IsWorkstation : BaseBean() {
+    @PrimaryKey(autoGenerate = true)
+    @ColumnInfo("workstation_id")
+    var workstationId: Long = 0
+
+    @ColumnInfo("workstation_code")
+    var workstationCode: String? = null
+
+    @ColumnInfo("workstation_name")
+    var workstationName: String = ""
+
+    @ColumnInfo("workstation_type")
+    var workstationType: String? = null
+
+    @ColumnInfo("parent_id")
+    var parentId: Long? = null
+
+    @ColumnInfo("ancestors")
+    var ancestors: String? = null
+
+    @ColumnInfo("order_num")
+    var orderNum: Int? = 0
+
+    @ColumnInfo("status")
+    var status: String? = "0"
+
+    @ColumnInfo("del_flag")
+    var delFlag: String? = "0"
+
+}

+ 53 - 0
data/src/main/java/com/grkj/data/local/dos/SysMenu.kt

@@ -0,0 +1,53 @@
+package com.grkj.data.local.dos
+
+import androidx.room.*
+
+@Entity(tableName = "sys_menu")
+class SysMenu : BaseBean() {
+
+    @PrimaryKey(autoGenerate = true)
+    @ColumnInfo(name = "menu_id")
+    var menuId: Long = 0
+
+    @ColumnInfo(name = "menu_name")
+    var menuName: String = ""
+
+    @ColumnInfo(name = "parent_id")
+    var parentId: Long? = null
+
+    @ColumnInfo(name = "order_num")
+    var orderNum: Int? = null
+
+    @ColumnInfo(name = "path")
+    var path: String? = null
+
+    @ColumnInfo(name = "component")
+    var component: String? = null
+
+    @ColumnInfo(name = "query")
+    var query: String? = null
+
+    @JvmField
+    @ColumnInfo(name = "is_frame")
+    var isFrame: Int? = null
+
+    @JvmField
+    @ColumnInfo(name = "is_cache")
+    var isCache: Int? = null
+
+    @ColumnInfo(name = "menu_type")
+    var menuType: String? = null
+
+    @ColumnInfo(name = "visible")
+    var visible: String? = null
+
+    @ColumnInfo(name = "status")
+    var status: String? = null
+
+    @ColumnInfo(name = "perms")
+    var perms: String? = null
+
+    @ColumnInfo(name = "icon")
+    var icon: String? = null
+
+}

+ 89 - 0
data/src/main/java/com/grkj/data/local/dos/SysRole.kt

@@ -0,0 +1,89 @@
+package com.grkj.data.local.dos
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.grkj.data.utils.ExcelColumn
+import com.grkj.data.utils.ExcelIgnore
+import com.grkj.data.utils.ExcelSheet
+
+
+/**
+ * 角色表 sys_role
+ *
+ * @author ruoyi
+ */
+@ExcelSheet("role")
+@Entity(tableName = "sys_role")
+class SysRole : BaseBean() {
+    /**
+     * 角色ID
+     */
+    @ExcelIgnore
+    @PrimaryKey(autoGenerate = true)
+    @ColumnInfo("role_id")
+    var roleId: Long = 0
+
+    /**
+     * 角色名称
+     */
+    @ExcelColumn("role_manage_role_name", order = 0)
+    @ColumnInfo("role_name")
+    var roleName: String = ""
+
+    /**
+     * 角色权限
+     */
+    @ExcelColumn("role_manage_permission_string", order = 1)
+    @ColumnInfo("role_key")
+    var roleKey: String = ""
+
+    /**
+     * 角色排序
+     */
+    @ExcelIgnore
+    @ColumnInfo("role_sort")
+    var roleSort: Int = 0
+
+    /**
+     * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+     */
+    @ExcelIgnore
+    @ColumnInfo("data_scope")
+    var dataScope: String? = null
+
+    /**
+     * 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示)
+     */
+    @ExcelIgnore
+    @ColumnInfo("menu_check_strictly")
+    var menuCheckStrictly: Int? = null
+
+    /**
+     * 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 )
+     */
+    @ExcelIgnore
+    @ColumnInfo("dept_check_strictly")
+    var deptCheckStrictly: Int? = null
+
+    /**
+     * 角色状态(0正常 1停用)
+     */
+    @ExcelColumn("status", order = 2)
+    var status: String = "0"
+
+    /**
+     * 删除标志(0代表存在 2代表删除)
+     */
+    @ExcelIgnore
+    @ColumnInfo("del_flag")
+    var delFlag: String? = "0"
+
+    @ExcelIgnore
+    @ColumnInfo("mars_data_scope")
+    var marsDataScope: String? = null
+
+    companion object {
+        const val serialVersionUID = 1L
+    }
+}

+ 14 - 0
data/src/main/java/com/grkj/data/local/dos/SysRoleMenu.kt

@@ -0,0 +1,14 @@
+package com.grkj.data.local.dos
+
+import androidx.room.*
+
+@Entity(tableName = "sys_role_menu", primaryKeys = ["role_id", "menu_id"])
+class SysRoleMenu {
+
+    @ColumnInfo(name = "role_id")
+    var roleId: Long = 0
+
+    @ColumnInfo(name = "menu_id")
+    var menuId: Long = 0
+
+}

+ 18 - 0
data/src/main/java/com/grkj/data/repository/ISysRepository.kt

@@ -0,0 +1,18 @@
+package com.grkj.data.repository
+
+import com.grkj.data.local.dos.SysRole
+
+/**
+ * 系统仓储
+ */
+interface ISysRepository {
+    /**
+     * 添加预设角色
+     */
+    fun addPresetRoleData(presetSysRole: List<SysRole>)
+
+    /**
+     * 检查系统菜单和角色
+     */
+    fun checkSysMenuAndRole()
+}

+ 18 - 0
data/src/main/java/com/grkj/data/repository/impl/network/NetworkSysRepository.kt

@@ -0,0 +1,18 @@
+package com.grkj.data.repository.impl.network
+
+import com.grkj.data.local.dos.SysRole
+import com.grkj.data.repository.BaseRepository
+import com.grkj.data.repository.ISysRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class NetworkSysRepository @Inject constructor() : BaseRepository(), ISysRepository {
+    override fun addPresetRoleData(presetSysRole: List<SysRole>) {
+        TODO("Not yet implemented")
+    }
+
+    override fun checkSysMenuAndRole() {
+        TODO("Not yet implemented")
+    }
+}

+ 194 - 0
data/src/main/java/com/grkj/data/repository/impl/standard/StandardSysRepository.kt

@@ -0,0 +1,194 @@
+package com.grkj.data.repository.impl.standard
+
+import com.grkj.data.enums.RoleEnum
+import com.grkj.data.enums.RoleFunctionalPermissionsEnum
+import com.grkj.data.local.dao.SysDao
+import com.grkj.data.local.dos.SysMenu
+import com.grkj.data.local.dos.SysRole
+import com.grkj.data.local.dos.SysRoleMenu
+import com.grkj.data.repository.BaseRepository
+import com.grkj.data.repository.ISysRepository
+import com.grkj.shared.utils.i18n.I18nManager
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class StandardSysRepository @Inject constructor(
+    val sysDao: SysDao
+) : BaseRepository(), ISysRepository {
+    override fun addPresetRoleData(presetSysRole: List<SysRole>) {
+        sysDao.addPresetRoleData(presetSysRole)
+    }
+
+    override fun checkSysMenuAndRole() {
+        // 找出所有顶层菜单(level == 0),依次递归插入/更新
+        RoleFunctionalPermissionsEnum.values()
+            .filter { it.level == 0 }
+            .forEach { topEnum ->
+                processMenuEnumRecursive(topEnum, parentId = null)
+            }
+        val sysMenuData = sysDao.getAllMenu()
+        sysDao.getRoleData().filter { it.roleKey in RoleEnum.values().map { it.roleKey } }
+            .forEach { roleData ->
+                when (roleData.roleKey) {
+                    //超管权限
+                    RoleEnum.ADMIN.roleKey -> {
+                        val roleMenuData = mutableListOf<SysRoleMenu>().apply {
+                            for (permissionsEnum in RoleFunctionalPermissionsEnum.except()) {
+                                sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
+                                    val sysRoleMenu = SysRoleMenu()
+                                    sysRoleMenu.roleId = roleData.roleId
+                                    sysRoleMenu.menuId = menuId
+                                    add(sysRoleMenu)
+                                }
+                            }
+                        }
+                        sysDao.insertRoleMenus(roleMenuData)
+                    }
+                    //作业管理员权限
+                    RoleEnum.JTDRAWER.roleKey -> {
+                        val roleMenuData = mutableListOf<SysRoleMenu>().apply {
+                            for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
+                                RoleFunctionalPermissionsEnum.USER_MANAGE,
+                                RoleFunctionalPermissionsEnum.ROLE_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS
+                            )) {
+                                sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
+                                    val sysRoleMenu = SysRoleMenu()
+                                    sysRoleMenu.roleId = roleData.roleId
+                                    sysRoleMenu.menuId = menuId
+                                    add(sysRoleMenu)
+                                }
+                            }
+                        }
+                        sysDao.insertRoleMenus(roleMenuData)
+                    }
+                    //作业负责人权限
+                    RoleEnum.JTLOCKER.roleKey -> {
+                        val roleMenuData = mutableListOf<SysRoleMenu>().apply {
+                            for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
+                                RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS,
+                            )) {
+                                sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
+                                    val sysRoleMenu = SysRoleMenu()
+                                    sysRoleMenu.roleId = roleData.roleId
+                                    sysRoleMenu.menuId = menuId
+                                    add(sysRoleMenu)
+                                }
+                            }
+                        }
+                        sysDao.insertRoleMenus(roleMenuData)
+                    }
+                    //作业参与人权限
+                    RoleEnum.JTCOLOCKER.roleKey -> {
+                        val roleMenuData = mutableListOf<SysRoleMenu>().apply {
+                            for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
+                                RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS,
+                            )) {
+                                sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
+                                    val sysRoleMenu = SysRoleMenu()
+                                    sysRoleMenu.roleId = roleData.roleId
+                                    sysRoleMenu.menuId = menuId
+                                    add(sysRoleMenu)
+                                }
+                            }
+                        }
+                        sysDao.insertRoleMenus(roleMenuData)
+                    }
+
+                    //作业观察员权限
+                    RoleEnum.JTGUARD.roleKey -> {
+                        val roleMenuData = mutableListOf<SysRoleMenu>().apply {
+                            for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
+                                RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS,
+                            )) {
+                                sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
+                                    val sysRoleMenu = SysRoleMenu()
+                                    sysRoleMenu.roleId = roleData.roleId
+                                    sysRoleMenu.menuId = menuId
+                                    add(sysRoleMenu)
+                                }
+                            }
+                        }
+                        sysDao.insertRoleMenus(roleMenuData)
+                    }
+
+                    //系统配置员权限
+                    RoleEnum.SYSCONFIG.roleKey -> {
+                        val roleMenuData = mutableListOf<SysRoleMenu>().apply {
+                            for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
+                                RoleFunctionalPermissionsEnum.EXCEPTION_HOME_MANAGE,
+                            )) {
+                                sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
+                                    val sysRoleMenu = SysRoleMenu()
+                                    sysRoleMenu.roleId = roleData.roleId
+                                    sysRoleMenu.menuId = menuId
+                                    add(sysRoleMenu)
+                                }
+                            }
+                        }
+                        sysDao.insertRoleMenus(roleMenuData)
+                    }
+                }
+            }
+    }
+
+    /**
+     * 递归“插入或更新”一个枚举项,以及它的子节点。
+     *
+     * @param menuEnum 当前要处理的枚举项
+     * @param parentId 父菜单在数据库里的 menuId (顶层传 null)
+     * @return 返回在数据库里最终存在的 SysMenu 对象
+     */
+    private fun processMenuEnumRecursive(
+        menuEnum: RoleFunctionalPermissionsEnum,
+        parentId: Long?
+    ): SysMenu {
+        // 1. 查找数据库里是否已存在相同 perms 的菜单
+        val existing: SysMenu? = sysDao.findByPerms(menuEnum.functionalPermission)
+
+        val currentMenu: SysMenu = if (existing == null) {
+            // 不存在,构造一个新的实体并插入
+            val newMenu = SysMenu().apply {
+                menuName = I18nManager.t(menuEnum.description)
+                perms = menuEnum.functionalPermission
+                // 这里用 level 做 orderNum(仅示意),可根据业务自由调整
+                orderNum = menuEnum.level
+                this.parentId = parentId
+                // 其它字段(path、component、isFrame、icon 等)可在此处赋值
+                // 例如:path = "/${menuEnum.functionalPermission.replace(":", "/")}"
+            }
+            val newId: Long = sysDao.insertMenu(newMenu)
+            if (newId <= 0) {
+                // 如果 onConflict = IGNORE 导致插入被忽略(并发、重复等),则重新查询一次
+                sysDao.findByPerms(menuEnum.functionalPermission)
+                    ?: throw IllegalStateException(
+                        "插入 SysMenu(${menuEnum.functionalPermission}) 失败,且数据库中没有已存在记录"
+                    )
+            } else {
+                // 插入成功,Room 会自动把自增主键分配给 newMenu.menuId
+                newMenu.menuId = newId
+                newMenu
+            }
+        } else {
+            // 已经存在,检查 parentId 是否需要更新
+            if (existing.parentId != parentId) {
+                existing.parentId = parentId
+                sysDao.updateMenu(existing)
+            }
+            existing
+        }
+
+        // 2. 递归处理它的子节点
+        if (menuEnum.children.isNotEmpty()) {
+            menuEnum.children.forEach { childEnum ->
+                processMenuEnumRecursive(childEnum, parentId = currentMenu.menuId)
+            }
+        }
+
+        return currentMenu
+    }
+}

+ 2 - 0
gradle/libs.versions.toml

@@ -14,6 +14,7 @@ sikextension = "1.1.66"
 sikcamera = "1.0.11"
 sikcronjob = "1.0.3"
 sikfontmanager = "1.0.2"
+sikcomm = "1.0.12"
 avi_library = "2.1.5"
 brv = "1.6.1"
 androidautosize = "v1.2.1"
@@ -46,6 +47,7 @@ sik-extension-sensors = { group = "com.github.SilverIceKey.SIKExtension", name =
 sik-camera = { group = "com.github.SilverIceKey", name = "SIKCamera", version.ref = "sikcamera" }
 sik-cronjob = { group = "com.github.SilverIceKey", name = "SIKCronJob", version.ref = "sikcronjob" }
 sik-fontmanager = { group = "com.github.SilverIceKey", name = "SIKFontManager", version.ref = "sikfontmanager" }
+sik-comm = { group = "com.github.SilverIceKey", name = "SIKComm", version.ref = "sikcomm" }
 
 brv = { group = "com.github.liangjingkanji", name = "brv", version.ref = "brv" }
 dialogx = { group = "com.github.kongzue.DialogX", name = "DialogX", version.ref = "dialogx" }

+ 1 - 0
shared/build.gradle.kts

@@ -54,6 +54,7 @@ dependencies {
     implementation(libs.androidx.core.ktx)
     compileOnly(libs.androidx.appcompat)
     api(libs.sik.extension.core)
+    api(libs.sik.comm)
     api(libs.sik.extension.encrypt)
     api(libs.sik.extension.image)
     api("org.mindrot:jbcrypt:0.4")

+ 150 - 0
shared/src/main/java/com/grkj/shared/utils/CountdownTimer.kt

@@ -0,0 +1,150 @@
+package com.grkj.shared.utils
+
+import android.os.SystemClock
+import kotlinx.coroutines.*
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+/**
+ * 单例倒计时:
+ * - start(durationMillis, tickMillis?, onTick?, onFinish)
+ * - reset(newDurationMillis?):重置并重新开始(保留上次回调)
+ * - cancel():取消且不触发 onFinish
+ * - isRunning / remainingMillis()
+ *
+ * 说明:
+ * - 用 elapsedRealtime,避免系统时间变动影响
+ * - tickMillis <= 0 表示只在结束时回调 onFinish(不发 tick)
+ * - 单实例,只允许一个倒计时;需要多个请让我给你写 Manager 版
+ */
+object CountdownTimer {
+
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
+    @Volatile private var job: Job? = null
+
+    private var startAt: Long = 0L
+    private var endAt: Long = 0L
+    private var durationMs: Long = 0L
+
+    private var tickIntervalMs: Long = 0L
+    private var onTickCb: ((remainingMs: Long) -> Unit)? = null
+    private var onFinishCb: (() -> Unit)? = null
+
+    val isRunning: Boolean
+        get() = job?.isActive == true
+
+    /** 开始(会打断上一个) */
+    @Synchronized
+    @JvmOverloads
+    fun start(
+        durationMillis: Long,
+        tickMillis: Long = 1000L,
+        onTick: ((remainingMs: Long) -> Unit)? = null,
+        onFinish: () -> Unit,
+    ) {
+        require(durationMillis >= 0) { "durationMillis must be >= 0" }
+        cancelInternal()
+
+        durationMs = durationMillis
+        tickIntervalMs = tickMillis.coerceAtLeast(0L)
+        onTickCb = onTick
+        onFinishCb = onFinish
+
+        startAt = SystemClock.elapsedRealtime()
+        endAt = startAt + durationMs
+
+        job = scope.launch {
+            if (tickIntervalMs > 0 && onTickCb != null) {
+                onTickCb?.invoke(remainingNow())
+            }
+            runLoop()
+        }
+    }
+
+    /** 重置并重新开始;不传则沿用上次时长与回调 */
+    @Synchronized
+    @JvmOverloads
+    fun reset(newDurationMillis: Long? = null) {
+        val finish = onFinishCb ?: return
+        val d = newDurationMillis ?: durationMs
+        val t = tickIntervalMs
+        val onTick = onTickCb
+        start(d, t, onTick, finish)
+    }
+
+    /** 取消(不触发 onFinish) */
+    @Synchronized
+    fun cancel() {
+        cancelInternal()
+        clearState()
+    }
+
+    /** 剩余毫秒;未运行返回 0 */
+    fun remainingMillis(): Long {
+        val j = job
+        if (j == null || !j.isActive) return 0L
+        return remainingNow()
+    }
+
+    /** 可选:释放作用域(通常不需要) */
+    fun close() {
+        cancel()
+        scope.cancel()
+    }
+
+    // ---------- 内部 ----------
+
+    /** 协程循环,基于协程上下文 isActive 判断存活 */
+    private suspend fun CoroutineScope.runLoop() {
+        val tick = tickIntervalMs
+        if (tick <= 0L || onTickCb == null) {
+            val wait = remainingNow()
+            if (wait > 0) delay(wait)
+            finishIfStillRunning()
+            return
+        }
+
+        while (isActive) {
+            val left = remainingNow()
+            if (left <= 0L) {
+                finishIfStillRunning()
+                return
+            }
+            delay(minOf(tick, left))
+            val nowLeft = remainingNow()
+            if (nowLeft > 0) onTickCb?.invoke(nowLeft)
+        }
+    }
+
+    private fun finishIfStillRunning() {
+        val j = job ?: return
+        if (!j.isActive) return
+        val cb = onFinishCb
+        clearState(keepCallbacks = true) // 方便 reset
+        cb?.invoke()
+    }
+
+    @Synchronized
+    private fun cancelInternal() {
+        job?.cancel()
+        job = null
+    }
+
+    private fun clearState(keepCallbacks: Boolean = false) {
+        startAt = 0L
+        endAt = 0L
+        if (!keepCallbacks) {
+            durationMs = 0L
+            tickIntervalMs = 0L
+            onTickCb = null
+            onFinishCb = null
+        }
+    }
+
+    /** 不依赖 job 状态,基于单调时钟计算剩余 */
+    private fun remainingNow(): Long {
+        val now = SystemClock.elapsedRealtime()
+        return (endAt - now).coerceAtLeast(0L)
+    }
+}

+ 14 - 0
shared/src/main/java/com/grkj/shared/utils/extension/Context.kt

@@ -0,0 +1,14 @@
+package com.grkj.shared.utils.extension
+
+import android.content.Context
+import java.io.BufferedReader
+import java.io.InputStreamReader
+
+/**
+ * 从 assets 中读取 JSON 文件并返回字符串
+ */
+fun Context.readJsonFromAssets(path: String): String {
+    return assets.open(path).use { inputStream ->
+        BufferedReader(InputStreamReader(inputStream)).readText()
+    }
+}

+ 19 - 1
ui-base/src/main/java/com/grkj/ui_base/base/BaseNavActivity.kt

@@ -3,12 +3,14 @@ package com.grkj.ui_base.base
 import androidx.databinding.ViewDataBinding
 import androidx.navigation.NavController
 import androidx.navigation.fragment.NavHostFragment
+import com.grkj.ui_base.widget.CustomNavBar
 
 /**
  * 导航栏
  */
-abstract class BaseNavActivity<V : ViewDataBinding> : BaseActivity<V>(){
+abstract class BaseNavActivity<V : ViewDataBinding> : BaseActivity<V>() {
     private var graphMap: Map<Int, Int>? = null
+    private var navBar: CustomNavBar? = null
 
     /** NavHostFragment 容器 ID,子类返回对应 fragment 布局 ID */
     protected open fun navHostFragmentId(): Int = 0
@@ -18,8 +20,24 @@ abstract class BaseNavActivity<V : ViewDataBinding> : BaseActivity<V>(){
                 as NavHostFragment
         host.navController
     }
+
     /** 动态切换 Nav Graph */
     open protected fun replaceNavGraph(graphId: Int) {
         navController.setGraph(graphId)
     }
+
+    /** 配置 BottomNavigation 切换 Graph,map: menuItemId -> navGraphId */
+    protected fun setupBottomNavigation(
+        navBar: CustomNavBar,
+        graphMap: Map<Int, Int>
+    ) {
+        this.navBar = navBar
+        this.graphMap = graphMap
+        navBar.setOnItemSelectedListener { item ->
+            graphMap[item.itemId]?.let {
+                replaceNavGraph(it)
+                true
+            } == true
+        }
+    }
 }

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

@@ -1,9 +1,19 @@
 package com.grkj.ui_base.utils.extension
 
 import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.core.widget.ImageViewCompat
+import androidx.fragment.app.Fragment
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.LinearSmoothScroller
 import androidx.recyclerview.widget.RecyclerView
+import com.grkj.ui_base.R
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
@@ -77,6 +87,90 @@ fun RecyclerView.smoothScrollToSmartPosition(
     lm.startSmoothScroll(scroller)
 }
 
+fun Fragment.toggleExpandView(
+    rootFrame: FrameLayout,
+    targetView: View,
+    isExpanded: Boolean,
+    expandedWidthRatio: Float = 1f,
+    expandedHeightRatio: Float = 1f,
+    animDuration: Long = 400L
+): Boolean {
+    // 唯一的 key,避免和别的 tag 冲突
+    val KEY_PARENT = R.id.expand_tag_parent
+    val KEY_LP     = R.id.expand_tag_lp
+    val KEY_INDEX  = R.id.expand_tag_index
+
+    if (!isExpanded) {
+        // —— 记录原始数据 ——
+        val parent = targetView.parent as ViewGroup
+        targetView.setTag(KEY_PARENT, parent)
+        // 记录原始 LayoutParams(可直接复用)
+        targetView.setTag(KEY_LP, targetView.layoutParams)
+        // 记录原来在父容器的索引
+        targetView.setTag(KEY_INDEX, parent.indexOfChild(targetView))
+
+        // 构造新的全屏 LayoutParams
+        val newLp = FrameLayout.LayoutParams(
+            (rootFrame.width * expandedWidthRatio).toInt(),
+            (rootFrame.height * expandedHeightRatio).toInt()
+        ).apply { gravity = Gravity.CENTER }
+
+        // 移除 & 添加到 rootFrame
+        parent.removeView(targetView)
+        rootFrame.addView(targetView, newLp)
+
+        // 初始动画状态
+        targetView.scaleX = 0.8f
+        targetView.scaleY = 0.8f
+        targetView.alpha  = 0f
+        targetView.elevation = 20f.dpToPx(requireContext())
+
+        // 播放展开动画
+        targetView.animate()
+            .scaleX(1f)
+            .scaleY(1f)
+            .alpha(1f)
+            .setDuration(animDuration)
+            .start()
+
+    } else {
+        // —— 收回动画 ——
+        targetView.animate()
+            .scaleX(0.8f)
+            .scaleY(0.8f)
+            .alpha(0f)
+            .setDuration(animDuration)
+            .withEndAction {
+                // 取回记录
+                val originalParent = targetView.getTag(KEY_PARENT) as ViewGroup
+                val originalLp     = targetView.getTag(KEY_LP)     as ViewGroup.LayoutParams
+                val originalIndex  = targetView.getTag(KEY_INDEX)  as Int
+
+                // 清除阴影
+                targetView.elevation = 0f
+
+                // 从 rootFrame 移除,恢复到原索引位置
+                rootFrame.removeView(targetView)
+                originalParent.addView(targetView, originalIndex, originalLp)
+
+                // 恢复属性
+                targetView.scaleX = 1f
+                targetView.scaleY = 1f
+                targetView.alpha  = 1f
+            }
+            .start()
+    }
+
+    return !isExpanded
+}
+
 // dp 转 px
 fun Float.dpToPx(context: Context): Float =
     this * context.resources.displayMetrics.density
+
+/**
+ * 移除 ImageView 的 Tint 效果
+ */
+fun ImageView.removeTint() {
+    ImageViewCompat.setImageTintList(this, null)
+}

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

@@ -0,0 +1,289 @@
+package com.grkj.ui_base.widget
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Paint
+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.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.annotation.AttrRes
+import androidx.annotation.MenuRes
+import androidx.appcompat.view.menu.MenuBuilder
+import androidx.core.view.isEmpty
+import com.grkj.ui_base.R
+import com.grkj.ui_base.config.ISCSConfig
+import com.grkj.ui_base.skin.loadSkinIcon
+import com.sik.sikcore.extension.setDebouncedClickListener
+import me.jessyan.autosize.AutoSize
+import me.jessyan.autosize.utils.AutoSizeUtils
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * 微信风格导航栏:
+ * - 扁平背景,无圆角卡片
+ * - 选中不整块铺色,仅 icon/text 变色
+ * - 可选顶部细分割线
+ * - 横/竖屏均分
+ */
+class CustomNavBar @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
+) : LinearLayout(context, attrs, defStyle) {
+    private val logger: Logger = LoggerFactory.getLogger(this::class.java)
+
+    @SuppressLint("RestrictedApi")
+    private val menuBuilder = MenuBuilder(context)
+    val menu: Menu get() = menuBuilder
+    private var onItemSelected: ((MenuItem) -> Boolean)? = null
+
+    // 风格参数(保留你原有的大小可在 XML 配)
+    private var navOrientation: Int = 0  // 0: horizontal(bottom), 1: vertical(side)
+    private var iconSize: Int = dp(24f)
+    private var iconSelectedSize: Int = dp(24f)
+    private var textSizePx: Int = sp(12f)
+    private var textSelectedSizePx: Int = sp(12f)
+    private var showTopDivider: Boolean = true
+    private var dividerHeightPx: Int = dp(0.5f)
+    private var iconData: Map<String, String> = mapOf()
+
+    // —— 主题颜色(走 attr,后续皮肤包覆盖)——
+    private val colorBarBg = resolveColorAttr(R.attr.navBarBackground, 0xFFFFFFFF.toInt())
+    private val colorDivider = resolveColorAttr(R.attr.navDivider, 0x11000000)
+    private val colorIconNorm = resolveColorAttr(R.attr.navIconTint, 0x99000000.toInt())
+    private val colorIconSel =
+        resolveColorAttr(R.attr.navIconTintSelected, 0xFF07C160.toInt()) // 微信绿默认
+    private val colorTextNorm = resolveColorAttr(R.attr.navTextColor, 0x99000000.toInt())
+    private val colorTextSel = resolveColorAttr(R.attr.navTextColorSelected, colorIconSel)
+
+    private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+        style = Paint.Style.FILL; color = colorDivider
+    }
+
+    private var length = 0
+    private var selectedIdx = 0
+
+    init {
+        setWillNotDraw(false) // ★ 需要绘制分割线/选中态
+        isClickable = true
+        isFocusable = true
+        val isLand = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+        AutoSize.autoConvertDensity(context as Activity, ISCSConfig.designSize, !isLand)
+        context.obtainStyledAttributes(attrs, R.styleable.CustomNavBar, defStyle, 0).apply {
+            navOrientation = getInt(R.styleable.CustomNavBar_navOrientation, 0)
+            iconSize =
+                getDimensionPixelSize(R.styleable.CustomNavBar_navIconSize, iconSize)
+            iconSelectedSize =
+                getDimensionPixelSize(R.styleable.CustomNavBar_navIconSelectedSize, iconSize)
+            textSizePx = getDimensionPixelSize(R.styleable.CustomNavBar_navTextSize, textSizePx)
+            textSelectedSizePx =
+                getDimensionPixelSize(R.styleable.CustomNavBar_navTextSelectedSize, textSizePx)
+            showTopDivider = getBoolean(R.styleable.CustomNavBar_navShowTopDivider, true)
+            dividerHeightPx =
+                getDimensionPixelSize(R.styleable.CustomNavBar_navDividerHeight, dividerHeightPx)
+            recycle()
+        }
+        orientation = if (navOrientation == 0) HORIZONTAL else VERTICAL
+
+        // 扁平背景
+        setBackgroundColor(colorBarBg)
+    }
+
+    /** 公开一个通知方法,外部 add 完菜单后调它 */
+    fun notifyMenuChanged() {               // ★
+        buildItems()
+    }
+
+    fun setOnItemSelectedListener(listener: (MenuItem) -> Boolean) {
+        onItemSelected = listener
+    }
+
+    /**
+     * 设置图标数据
+     */
+    fun setIconData(iconData: Map<String, String>) {
+        this.iconData = iconData
+    }
+
+    @SuppressLint("RestrictedApi")
+    fun inflateMenu(@MenuRes menuRes: Int) {
+        menuBuilder.clear()
+        MenuInflater(context).inflate(menuRes, menuBuilder)
+        buildItems()
+    }
+
+    @SuppressLint("RestrictedApi")
+    private fun buildItems() {
+        removeAllViews()
+        length = menuBuilder.size()
+        if (length == 0) {
+            invalidate(); return
+        }
+
+        val iconTintState = colorStateList(selected = colorIconSel, normal = colorIconNorm)
+        val textColorState = colorStateList(selected = colorTextSel, normal = colorTextNorm)
+
+        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.MATCH_PARENT, 1f)
+                    else LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)
+                isSelected = (i == selectedIdx)
+                isClickable = true
+                isFocusable = true
+                setDebouncedClickListener {
+                    // 回调可拦截;未拦截则默认切换
+                    val handled = onItemSelected?.invoke(item) == true
+                    if (!handled) selectIndex(i)
+                    else selectIndex(i)
+                }
+                // ripple(可选):若你的 theme 有 selectableItemBackgroundBorderless,可设置 foreground
+                val outValue = TypedValue()
+                if (context.theme.resolveAttribute(
+                        android.R.attr.selectableItemBackgroundBorderless, outValue, true
+                    )
+                ) {
+                    foreground = context.getDrawable(outValue.resourceId)
+                }
+            }
+
+            val iv = ImageView(context).apply {
+                iconData[item.title]?.let {
+                    loadSkinIcon(it)
+                }
+                layoutParams = LayoutParams(iconSize, iconSize)
+                imageTintList = iconTintState
+                scaleType = ImageView.ScaleType.FIT_CENTER
+                adjustViewBounds = true
+                // 让选中状态能传给自己(驱动 ColorStateList)
+                isDuplicateParentStateEnabled = true
+            }
+            val tv = TextView(context).apply {
+                text = item.title
+                setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx.toFloat())
+                setTextColor(textColorState)
+                layoutParams = LayoutParams(
+                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT
+                ).apply {
+                    // 不分横竖,一律用 topMargin;别用 leftMargin 了
+                    topMargin = dp(5f)
+                }
+                isSingleLine = true
+                gravity = Gravity.CENTER
+                isDuplicateParentStateEnabled = true
+            }
+
+            container.addView(iv)
+            container.addView(tv)
+            addView(container)
+            container.isSelected = (i == selectedIdx)          // ★ 放到 addView 之后
+            container.refreshDrawableState()                   // ★ 强刷一次
+            for (c in 0 until container.childCount) {
+                container.getChildAt(c).refreshDrawableState() // ★ 子也刷一下
+            }
+        }
+        if (length > 0) selectIndex(selectedIdx.coerceIn(0, length - 1))
+    }
+
+    private fun selectIndex(idx: Int) {
+        if (idx == selectedIdx) {
+            invalidate(); return
+        }
+        selectedIdx = idx
+        for (i in 0 until childCount) {
+            val v = getChildAt(i)
+            v.isSelected = (i == selectedIdx)
+            v.refreshDrawableState()                   // ★
+            for (c in 0 until (v as ViewGroup).childCount) {
+                val childView = v.getChildAt(c)
+                when (childView) {
+                    is ImageView -> {
+                        if (v.isSelected) {
+                            childView.layoutParams = childView.layoutParams.apply {
+                                width = iconSelectedSize.toInt()
+                                height = iconSelectedSize.toInt()
+                            }
+                        } else {
+                            childView.layoutParams = childView.layoutParams.apply {
+                                width = iconSize.toInt()
+                                height = iconSize.toInt()
+                            }
+                        }
+                    }
+
+                    is TextView -> {
+                        if (v.isSelected) {
+                            childView.setTextSize(
+                                TypedValue.COMPLEX_UNIT_PX, textSelectedSizePx.toFloat()
+                            )
+                        } else {
+                            childView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx.toFloat())
+                        }
+                    }
+                }
+                childView.refreshDrawableState() // ★
+            }
+        }
+        invalidate()
+    }
+
+    @SuppressLint("RestrictedApi")
+    override fun onDraw(canvas: Canvas) {
+        // 菜单发生变化(比如运行时 add/remove)时自动重建
+        if (menuBuilder.size() != length) { // ★ 自检补救
+            buildItems()
+        }
+        super.onDraw(canvas)
+        // 顶部分割线(可选)
+        if (showTopDivider && navOrientation == HORIZONTAL) {
+            canvas.drawRect(0f, 0f, width.toFloat(), dividerHeightPx.toFloat(), dividerPaint)
+        }
+    }
+
+    var selectedItemId: Int
+        @SuppressLint("RestrictedApi") get() = menuBuilder.getItemOrNull(selectedIdx)?.itemId
+            ?: View.NO_ID
+        @SuppressLint("RestrictedApi") set(id) {
+            if (isEmpty() && menuBuilder.size() > 0) buildItems()  // ★
+            val idx =
+                (0 until menuBuilder.size()).firstOrNull { menuBuilder.getItem(it).itemId == id }
+                    ?: return
+            selectIndex(idx)
+            onItemSelected?.invoke(menuBuilder.getItem(idx))
+        }
+
+    // —— 工具 —— //
+    private fun dp(dp: Float): Int = AutoSizeUtils.dp2px(context, dp)
+    private fun sp(sp: Float): Int = AutoSizeUtils.sp2px(context, sp)
+
+    private fun colorStateList(selected: Int, normal: Int) = ColorStateList(
+        arrayOf(intArrayOf(android.R.attr.state_selected), intArrayOf()),
+        intArrayOf(selected, normal)
+    )
+
+    private fun resolveColorAttr(@AttrRes attr: Int, fallback: Int): Int {
+        val tv = TypedValue()
+        return if (context.theme.resolveAttribute(attr, tv, true)) {
+            if (tv.resourceId != 0) context.getColor(tv.resourceId) else tv.data
+        } else fallback
+    }
+
+    @SuppressLint("RestrictedApi")
+    private fun MenuBuilder.getItemOrNull(i: Int): MenuItem? =
+        if (i in 0 until size()) getItem(i) else null
+}

+ 81 - 0
ui-base/src/main/java/com/grkj/ui_base/widget/FingerprintFillView.kt

@@ -0,0 +1,81 @@
+package com.grkj.ui_base.widget
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import android.view.animation.AccelerateDecelerateInterpolator
+import androidx.core.graphics.PathParser
+import com.grkj.ui_base.R
+import com.grkj.ui_base.utils.CommonUtils
+import kotlin.math.ceil
+
+class FingerprintFillView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : View(context, attrs, defStyle) {
+
+    // 1. 读取 pathData 并解析成 Path 对象列表
+    private val pathDataArray = context.resources.getStringArray(R.array.fingerprint_paths)
+    private val paths = pathDataArray.map {
+        PathParser.createPathFromPathData(it)
+    }
+
+    // 2. 两支画笔:灰底 & 绿填
+    private val greyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+        style = Paint.Style.FILL
+        color = CommonUtils.getColor(R.attr.fingerprint_grey) // #888888
+    }
+    private val greenPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+        style = Paint.Style.FILL
+        color = CommonUtils.getColor(R.attr.fingerprint_green) // #34C759
+    }
+
+    // 3. 总步数 & 当前进度([0, totalSteps])
+    private var totalSteps = 8
+    private var progress = 0f
+
+    /** 设置分段总数 */
+    fun setTotalSteps(steps: Int) {
+        totalSteps = steps.coerceAtLeast(1)
+        invalidate()
+    }
+
+    /** 设置当前填充步数,可选动画 */
+    fun setProgress(stepsFilled: Int, animate: Boolean = true) {
+        val target = stepsFilled.coerceIn(0, totalSteps).toFloat()
+        if (!animate) {
+            progress = target
+            invalidate()
+            return
+        }
+        ObjectAnimator.ofFloat(this, "progress", progress, target).apply {
+            duration = 600
+            interpolator = AccelerateDecelerateInterpolator()
+            addUpdateListener { invalidate() }
+            start()
+        }
+    }
+
+    // 用于 ObjectAnimator 修改 progress
+    @Suppress("unused")
+    fun setProgress(value: Float) {
+        progress = value
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+        // 4. 先画灰色底
+        for (path in paths) {
+            canvas.drawPath(path, greyPaint)
+        }
+        // 5. 然后画绿色,计算当前要填几条
+        val count = ceil(paths.size * (progress / totalSteps)).toInt()
+        for (i in 0 until count.coerceAtMost(paths.size)) {
+            canvas.drawPath(paths[i], greenPaint)
+        }
+    }
+}

+ 76 - 0
ui-base/src/main/java/com/grkj/ui_base/widget/RequiredTextView.kt

@@ -0,0 +1,76 @@
+package com.grkj.ui_base.widget
+
+import android.content.Context
+import android.graphics.Color
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.style.ForegroundColorSpan
+import android.util.AttributeSet
+import androidx.appcompat.widget.AppCompatTextView
+import com.grkj.ui_base.R
+
+class RequiredTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyle: Int = android.R.attr.textViewStyle
+) : AppCompatTextView(context, attrs, defStyle) {
+
+    private var required     = false
+    private var markText     = "*"
+    private var markColor    = Color.RED
+    private var markPosition = 0  // 0 = start, 1 = end
+
+    init {
+        context.obtainStyledAttributes(attrs, R.styleable.RequiredTextView).apply {
+            required     = getBoolean(R.styleable.RequiredTextView_required, false)
+            getString(R.styleable.RequiredTextView_markText)?.let { markText = it }
+            markColor    = getColor(R.styleable.RequiredTextView_markColor, Color.RED)
+            markPosition = getInt(R.styleable.RequiredTextView_markPosition, 0)
+            recycle()
+        }
+        if (required && text.isNotEmpty()) {
+            super.setText(buildMarkedText(text.toString()), BufferType.SPANNABLE)
+        }
+    }
+
+    override fun setText(text: CharSequence?, type: BufferType?) {
+        if (required && !text.isNullOrEmpty()) {
+            val raw = text.toString()
+            // 如果已经带了星号,就不再重复添加
+            val alreadyMarked = (markPosition == 0 && raw.startsWith(markText))
+                    || (markPosition == 1 && raw.endsWith(markText))
+            if (alreadyMarked) {
+                super.setText(raw, type)
+            } else {
+                super.setText(buildMarkedText(raw), type)
+            }
+        } else {
+            super.setText(text, type)
+        }
+    }
+
+    private fun buildMarkedText(raw: String): SpannableStringBuilder {
+        val sb = SpannableStringBuilder()
+        if (markPosition == 0) {
+            // 前面加星号
+            sb.append(markText)
+            sb.setSpan(
+                ForegroundColorSpan(markColor),
+                0, markText.length,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+            )
+            sb.append(raw)
+        } else {
+            // 后面加星号
+            sb.append(raw)
+            sb.append(markText)
+            val start = raw.length
+            sb.setSpan(
+                ForegroundColorSpan(markColor),
+                start, start + markText.length,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+            )
+        }
+        return sb
+    }
+}

+ 55 - 0
ui-base/src/main/java/com/grkj/ui_base/widget/ShadowTextView.kt

@@ -0,0 +1,55 @@
+package com.grkj.ui_base.widget
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import androidx.appcompat.widget.AppCompatTextView
+import com.grkj.ui_base.R
+
+class ShadowTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = android.R.attr.textViewStyle
+) : AppCompatTextView(context, attrs, defStyleAttr) {
+
+    private var shadowColor: Int = context.getColor(R.color.palette_black)
+    private var shadowDx: Float = 4f          // default 2px
+    private var shadowDy: Float = 4f          // default 2px
+
+    // 保存原文字颜色
+    private var originalTextColor: Int = currentTextColor
+
+    init {
+        // 读取自定义属性
+        val ta = context.obtainStyledAttributes(attrs, R.styleable.ShadowTextView)
+        shadowColor = ta.getColor(
+            R.styleable.ShadowTextView_shadowColor,
+            shadowColor
+        )
+        shadowDx = ta.getDimension(
+            R.styleable.ShadowTextView_shadowDx,
+            shadowDx
+        )
+        shadowDy = ta.getDimension(
+            R.styleable.ShadowTextView_shadowDy,
+            shadowDy
+        )
+        ta.recycle()
+
+        // 记录原字体颜色
+        originalTextColor = currentTextColor
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        // 第一遍:绘制阴影文字(偏移 + 改色)
+        setTextColor(shadowColor)
+        canvas.save()
+        canvas.translate(shadowDx, shadowDy)
+        super.onDraw(canvas)
+        canvas.restore()
+
+        // 第二遍:绘制原文字
+        setTextColor(originalTextColor)
+        super.onDraw(canvas)
+    }
+}

+ 2 - 2
ui-base/src/main/res/drawable/common_divider_large_space_grid_land.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
     <size
-        android:width="@dimen/iscs_space_4"
-        android:height="@dimen/iscs_space_4" />
+        android:width="@dimen/iscs_space_2"
+        android:height="@dimen/iscs_space_2" />
 </shape>

+ 1 - 1
ui-base/src/main/res/drawable/common_divider_normal_space_vertical.xml

@@ -2,5 +2,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
     <size
         android:width="@dimen/iscs_stroke_sm"
-        android:height="@dimen/iscs_space_4" />
+        android:height="@dimen/iscs_space_2" />
 </shape>

+ 1 - 1
ui-base/src/main/res/layout-land/common_dialog_loading_progress.xml

@@ -8,7 +8,7 @@
         android:layout_height="wrap_content"
         android:gravity="center_horizontal"
         android:orientation="vertical"
-        android:paddingHorizontal="@dimen/iscs_space_4"
+        android:paddingHorizontal="@dimen/iscs_space_2"
         android:paddingVertical="3dp"
         tools:background="@android:color/darker_gray">
 

+ 3 - 3
ui-base/src/main/res/layout-land/dialog_tip.xml

@@ -27,7 +27,7 @@
             android:layout_below="@+id/title"
             android:gravity="center_vertical"
             android:paddingHorizontal="40dp"
-            android:paddingVertical="@dimen/iscs_space_4"
+            android:paddingVertical="@dimen/iscs_space_2"
             android:textColor="?attr/colorTextPrimary"
             android:textSize="@dimen/iscs_text_md"
             tools:text="登录成功" />
@@ -54,7 +54,7 @@
                 android:drawablePadding="@dimen/iscs_space_2"
                 android:gravity="center"
                 android:minHeight="@dimen/common_btn_height"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 app:i18nKey='@{"confirm"}'
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_md" />
@@ -69,7 +69,7 @@
                 android:drawablePadding="@dimen/iscs_space_2"
                 android:gravity="center"
                 android:minHeight="@dimen/common_btn_height"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 app:i18nKey='@{"cancel"}'
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_md" />

+ 2 - 2
ui-base/src/main/res/layout-land/dialog_wheel_date_range.xml

@@ -43,7 +43,7 @@
             <View
                 android:layout_width="2dp"
                 android:layout_height="match_parent"
-                android:layout_marginHorizontal="@dimen/iscs_space_4"
+                android:layout_marginHorizontal="@dimen/iscs_space_2"
                 android:background="?attr/colorBlack20" />
 
             <LinearLayout
@@ -80,7 +80,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:gravity="center"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 app:i18nKey='@{"confirm"}'
                 android:textColor="?attr/colorPrimary"
                 android:textSize="@dimen/iscs_text_md" />

+ 1 - 1
ui-base/src/main/res/layout-land/dialog_wheel_time_pick.xml

@@ -29,7 +29,7 @@
                 android:layout_height="wrap_content"
                 android:layout_alignParentRight="true"
                 android:gravity="center"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 android:textColor="?attr/colorPrimary"
                 android:textSize="@dimen/iscs_text_md"
                 app:i18nKey='@{"confirm"}' />

+ 1 - 1
ui-base/src/main/res/layout/common_dialog_loading_progress.xml

@@ -8,7 +8,7 @@
         android:layout_height="wrap_content"
         android:gravity="center_horizontal"
         android:orientation="vertical"
-        android:paddingHorizontal="@dimen/iscs_space_4"
+        android:paddingHorizontal="@dimen/iscs_space_2"
         android:paddingVertical="3dp"
         tools:background="@android:color/darker_gray">
 

+ 3 - 3
ui-base/src/main/res/layout/dialog_tip.xml

@@ -27,7 +27,7 @@
             android:layout_below="@+id/title"
             android:gravity="center_vertical"
             android:paddingHorizontal="40dp"
-            android:paddingVertical="@dimen/iscs_space_4"
+            android:paddingVertical="@dimen/iscs_space_2"
             android:textColor="?attr/colorTextPrimary"
             android:textSize="@dimen/iscs_text_md"
             tools:text="登录成功" />
@@ -54,7 +54,7 @@
                 android:drawablePadding="@dimen/iscs_space_2"
                 android:gravity="center"
                 android:minHeight="@dimen/common_btn_height"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 app:i18nKey='@{"confirm"}'
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_md" />
@@ -69,7 +69,7 @@
                 android:drawablePadding="@dimen/iscs_space_2"
                 android:gravity="center"
                 android:minHeight="@dimen/common_btn_height"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 app:i18nKey='@{"cancel"}'
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_md" />

+ 2 - 2
ui-base/src/main/res/layout/dialog_wheel_date_range.xml

@@ -43,7 +43,7 @@
             <View
                 android:layout_width="2dp"
                 android:layout_height="match_parent"
-                android:layout_marginHorizontal="@dimen/iscs_space_4"
+                android:layout_marginHorizontal="@dimen/iscs_space_2"
                 android:background="?attr/colorBlack20" />
 
             <LinearLayout
@@ -80,7 +80,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:gravity="center"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 app:i18nKey='@{"confirm"}'
                 android:textColor="?attr/colorPrimary"
                 android:textSize="@dimen/iscs_text_md" />

+ 1 - 1
ui-base/src/main/res/layout/dialog_wheel_time_pick.xml

@@ -29,7 +29,7 @@
                 android:layout_height="wrap_content"
                 android:layout_alignParentRight="true"
                 android:gravity="center"
-                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:paddingHorizontal="@dimen/iscs_space_2"
                 android:textColor="?attr/colorPrimary"
                 android:textSize="@dimen/iscs_text_md"
                 app:i18nKey='@{"confirm"}' />

+ 39 - 46
ui-base/src/main/res/values/colors_palette.xml

@@ -1,62 +1,55 @@
 <resources>
-    <!-- 基础 -->
-    <color name="palette_black">#000000</color>
-    <color name="palette_transparent">#00000000</color>
-    <color name="palette_transparent_half">#80000000</color>
-
-    <!-- 暗色层级(更中性、降低饱和,不偏蓝) -->
-    <color name="palette_dark_gray">#121417</color>         <!-- 原 #2D2C3C -->
-    <color name="palette_dark_gray_deep">#0F1115</color>    <!-- 原 #2b2d42 -->
-    <color name="palette_dark_gray_bright_1">#1B1F27</color><!-- 原 #4b4b5a -->
-    <color name="palette_dark_gray_bright">#1A1F2B</color>  <!-- 原 #3d466f 过亮 -->
-
-    <color name="palette_surface_dark">#151821</color>
-    <!-- 白(含常用透明度) -->
+    <!-- —— 基础底色与层级:转深蓝灰体系 —— -->
+    <color name="palette_dark_gray">#1D293D</color>            <!-- 基底(原 colorBg) -->
+    <color name="palette_dark_gray_deep">#162235</color>       <!-- 更深一层(抽屉/蒙层下层) -->
+    <color name="palette_dark_gray_bright_1">#22324A</color>   <!-- 输入/卡片次级底 -->
+    <color name="palette_dark_gray_bright">#243754</color>     <!-- 浅一层的容器或次级页背景 -->
+    <color name="palette_surface_dark">#202C43</color>         <!-- Surface:与Bg拉开≈4~6% 亮度差 -->
+
+    <!-- —— 文本/白透明度:保持 —— -->
     <color name="palette_white">#FFFFFF</color>
     <color name="palette_white_20">#33FFFFFF</color>
     <color name="palette_white_30">#4DFFFFFF</color>
-    <!-- 38% 白(更标准) -->
     <color name="palette_white_40">#66FFFFFF</color>
     <color name="palette_white_80">#CCFFFFFF</color>
     <color name="palette_white_90">#E6FFFFFF</color>
-    <color name="palette_divider_white12">#1FFFFFFF</color> <!-- 12% 白 -->
-    <color name="palette_stroke_white8">#14FFFFFF</color>  <!-- 8% 白 -->
-    <color name="palette_text_primary_dark">#E6FFFFFF</color>   <!-- 90% 白 -->
-    <color name="palette_text_secondary_dark">#99FFFFFF</color> <!-- 60% 白 -->
-    <color name="palette_text_disabled_dark">#61FFFFFF</color>  <!-- 38% 白 -->
-
-    <!-- 灰(用于 hint/禁用等) -->
-    <color name="palette_grey">#8FA3B4</color>        <!-- 冷灰蓝,提高可读性 -->
-
-    <!-- 黑透明(保留以兼容,少用于暗色描边) -->
+    <color name="palette_divider_white12">#1FFFFFFF</color>
+    <color name="palette_stroke_white8">#14FFFFFF</color>
+    <color name="palette_text_primary_dark">#E6FFFFFF</color>
+    <color name="palette_text_secondary_dark">#99FFFFFF</color>
+    <color name="palette_text_disabled_dark">#61FFFFFF</color>
+
+    <!-- —— 中性灰:往冷蓝一点,避免发脏 —— -->
+    <color name="palette_grey">#93A7BC</color>         <!-- hint/禁用文案更清晰 -->
+    <color name="palette_light_gray">#8396AD</color>   <!-- 次级标签/弱分隔文案 -->
+    <color name="palette_selected_gray">#33FFFFFF</color>  <!-- 20%白:保留 -->
+
+    <!-- —— 品牌/主色:微偏青、略提亮,暗底可读性更好 —— -->
+    <color name="palette_blue">#5AA9FF</color>         <!-- 原#4BA3FF → 提亮一点避免糊底 -->
+    <color name="palette_blue_light">#8CC7FF</color>   <!-- 强调/悬浮态 -->
+    <color name="palette_blue_80">#CC5AA9FF</color>    <!-- 80%不透明主色 -->
+    <color name="palette_light_blue">#BBDFFF</color>   <!-- 选中浅底/Tag背景 -->
+    <color name="palette_navy">#2B4F87</color>         <!-- 深蓝系块背景/图标填充 -->
+    <color name="palette_purple">#6F63C9</color>       <!-- 保留冷调,少许提亮 -->
+
+    <!-- —— 状态色:提高鲜明度,保证对比 —— -->
+    <color name="palette_green_bright">#28D8A0</color>  <!-- 成功高亮 -->
+    <color name="palette_green_deep">#16C07A</color>    <!-- 成功主/按钮底 -->
+    <color name="palette_red_bright">#FF5A5F</color>    <!-- 错误/警告突出红:保留 -->
+    <color name="palette_yellow_bright">#F5D24A</color> <!-- 进行中/提醒,略降橙感防脏 -->
+
+    <!-- —— 黑透明:保留兼容 —— -->
+    <color name="palette_black">#000000</color>
+    <color name="palette_transparent">#00000000</color>
+    <color name="palette_transparent_half">#80000000</color>
     <color name="palette_black_20">#33000000</color>
     <color name="palette_black_50">#80000000</color>
     <color name="palette_black_60">#99000000</color>
     <color name="palette_black_80">#CC000000</color>
 
-    <!-- 中性色 -->
-    <color name="palette_light_gray">#7A8A99</color>
-    <color name="palette_selected_gray">#33FFFFFF</color>   <!-- 20% 白替代 #80808080 -->
-
-    <!-- 品牌/主色(提亮降灰,暗底更干净) -->
-    <color name="palette_blue">#4BA3FF</color>
-    <color name="palette_blue_light">#77BDFF</color>
-    <color name="palette_blue_80">#CC4BA3FF</color>
-    <color name="palette_light_blue">#A5D8FF</color>
-    <color name="palette_navy">#245689</color>
-    <color name="palette_purple">#744CAB</color>
-
-    <!-- 状态色(亮一些,避免糊底) -->
-    <color name="palette_green_bright">#26D38E</color>
-    <color name="palette_green_deep">#15B56B</color>
-
-    <color name="palette_red_bright">#FF5A5F</color>
-
-    <color name="palette_yellow_bright">#F8D141</color>
-
-    <!-- 你的自定义基色(保持语义) -->
+    <!-- —— 自定义基色:沿用/对齐 —— -->
     <color name="dirtyColor_base">@color/palette_red_bright</color>
     <color name="fingerprint_grey_base">@color/palette_grey</color>
     <color name="fingerprint_green_base">@color/palette_green_deep</color>
-    <color name="scrim_base">#B3000000</color> <!-- 70% 黑,做沉浸遮罩更自然 -->
+    <color name="scrim_base">#B3000000</color> <!-- 70%黑:暗底也够沉 -->
 </resources>

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

@@ -28,6 +28,11 @@
     <dimen name="tip_dialog_height">280dp</dimen>
     <dimen name="loading_size">152dp</dimen>
 
+
+    <dimen name="avatar_size">30dp</dimen>
+    <dimen name="home_bottom_nav_size">90dp</dimen>
+    <dimen name="home_bottom_nav_icon_size">40dp</dimen>
+
     <!--    统一数值-->
     <!-- 圆角(dp) -->
     <dimen name="iscs_radius_xs">3dp</dimen>