Преглед изворни кода

feat(设置): 新增系统设置模块
- 新增设置页面,支持配置最大指纹录入数量和自动登出时间
- 指纹录入增加最大数量限制
- 新增闲置自动登出功能
- 登录页和主页增加相应逻辑
- 调整各角色权限,增加设置模块权限
- 补充相关国际化文案

周文健 пре 8 месеци
родитељ
комит
1653ebb7db

+ 30 - 0
app/src/main/assets/i18n/en-US.json

@@ -3992,5 +3992,35 @@
     "key": "show_in_map",
     "type": "text",
     "value": "Show in Map"
+  },
+  "max_fingerprint_insert_tip": {
+    "key": "max_fingerprint_insert_tip",
+    "type": "text",
+    "value": "(Up to {0} fingerprints can be entered)"
+  },
+  "fingerprint_limit_tip": {
+    "key": "fingerprint_limit_tip",
+    "type": "text",
+    "value": "The number of fingerprints has reached the upper limit"
+  },
+  "max_fingerprint_insert": {
+    "key": "max_fingerprint_insert",
+    "type": "text",
+    "value": "Max fingerprint entries:"
+  },
+  "auto_logout_time": {
+    "key": "auto_logout_time",
+    "type": "text",
+    "value": "Auto logout time(ms):"
+  },
+  "please_input_max_fingerprint_entries_size": {
+    "key": "please_input_max_fingerprint_entries_size",
+    "type": "text",
+    "value": "Please input max fingerprint entries size"
+  },
+  "please_input_auto_logout_time": {
+    "key": "please_input_auto_logout_time",
+    "type": "text",
+    "value": "Please input auto logout time"
   }
 }

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

@@ -3988,5 +3988,35 @@
     "key": "show_in_map",
     "type": "text",
     "value": "在地图中显示:"
+  },
+  "max_fingerprint_insert_tip": {
+    "key": "max_fingerprint_insert_tip",
+    "type": "text",
+    "value": "(指纹最多可录入{0}个)"
+  },
+  "fingerprint_limit_tip": {
+    "key": "fingerprint_limit_tip",
+    "type": "text",
+    "value": "指纹数量已达到上限"
+  },
+  "max_fingerprint_insert": {
+    "key": "max_fingerprint_insert",
+    "type": "text",
+    "value": "最大指纹录入数量:"
+  },
+  "auto_logout_time": {
+    "key": "auto_logout_time",
+    "type": "text",
+    "value": "自动登出时间(ms):"
+  },
+  "please_input_max_fingerprint_entries_size": {
+    "key": "please_input_max_fingerprint_entries_size",
+    "type": "text",
+    "value": "请输入最大指纹录入数量"
+  },
+  "please_input_auto_logout_time": {
+    "key": "please_input_auto_logout_time",
+    "type": "text",
+    "value": "请输入自动登出时间"
   }
 }

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

@@ -29,6 +29,7 @@ import com.grkj.iscs.features.login.dialog.LoginDialog
 import com.grkj.iscs.features.login.viewmodel.LoginViewModel
 import com.grkj.iscs.features.main.activity.MainActivity
 import com.grkj.shared.config.Constants
+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
@@ -175,7 +176,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
     override fun initData() {
         super.initData()
         viewModel.registerFaceFeature().observe(this) {}
-        viewModel.checkPresetWorkflowStepIcon().observe(this){}
+        viewModel.checkPresetWorkflowStepIcon().observe(this) {}
         //todo 测试用,直接创建管理员账号
 //        viewModel.insertAdminAccount().observe(this) {}
         requestPermissionsIfNeeded(*Constants.needPermission) {
@@ -233,6 +234,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
 
     override fun onResume() {
         super.onResume()
+        CountdownTimer.cancel()
         ISCSApplication.checkKeyInfoTask.isInLogin = true
         MainDomainData.clear()
         FingerprintUtil.init(this)

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

@@ -11,7 +11,9 @@ import androidx.core.view.isNotEmpty
 import androidx.core.view.isVisible
 import coil.load
 import coil.transform.CircleCropTransformation
+import com.grkj.data.data.CommonConstants
 import com.grkj.data.data.EventConstants
+import com.grkj.data.data.MMKVConstants
 import com.grkj.data.data.MainDomainData
 import com.grkj.data.enums.RoleFunctionalPermissionsEnum
 import com.grkj.data.model.local.TabConfig
@@ -20,6 +22,7 @@ import com.grkj.iscs.databinding.ActivityMainBinding
 import com.grkj.iscs.features.login.activity.LoginActivity
 import com.grkj.iscs.features.main.viewmodel.MainViewModel
 import com.grkj.shared.model.EventBean
+import com.grkj.shared.utils.CountdownTimer
 import com.grkj.ui_base.base.BaseActivity
 import com.grkj.shared.utils.extension.toByteArrays
 import com.grkj.shared.utils.extension.toHexStrings
@@ -29,6 +32,7 @@ import com.grkj.ui_base.utils.event.FlashTipEvent
 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
@@ -95,6 +99,11 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
     }
 
     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 {
@@ -102,7 +111,7 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
                 val faceData = it.file().readText()
                 val avatar = ImageConvertUtils.base64ToBitmap(faceData)
                 binding.avatar.removeTint()
-                binding.avatar.load(avatar){
+                binding.avatar.load(avatar) {
                     transformations(CircleCropTransformation())
                 }
             }
@@ -188,6 +197,7 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
     }
 
     override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+        CountdownTimer.reset()
         if (event.action == KeyEvent.ACTION_UP && event.source == InputDevice.SOURCE_KEYBOARD) {
             // 检测到回车开始处理
             if (event.keyCode == 66) {
@@ -214,7 +224,7 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
      */
     private fun logout() {
         viewModel.unregisterStatusListener()
-        BleSendDispatcher.disconnectAll(60_0000L)
+        BleSendDispatcher.disconnectAll(60_000L)
         viewModel.removeBleIndicate()
         startActivity(Intent(this, LoginActivity::class.java).apply {
             flags = Intent.FLAG_ACTIVITY_NEW_TASK

+ 17 - 0
app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt

@@ -9,6 +9,8 @@ import com.drake.brv.utils.divider
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
+import com.grkj.data.data.CommonConstants
+import com.grkj.data.data.MMKVConstants
 import com.grkj.data.model.vo.FingerprintDataVo
 import com.grkj.data.model.vo.SysBiometricDataVo
 import com.grkj.iscs.R
@@ -22,6 +24,7 @@ import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.fingerprint.FingerprintUtil
 import com.kongzue.dialogx.dialogs.CustomDialog
 import com.sik.sikcore.extension.deleteIfExists
+import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.setDebouncedClickListener
 import com.sik.sikimage.ImageConvertUtils
 import dagger.hilt.android.AndroidEntryPoint
@@ -44,6 +47,12 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
     private var pressTip: TextView? = null
     private val inputFingerprintIds: MutableList<Long> = mutableListOf()
 
+    /**
+     * 最大指纹录入数
+     */
+    private val maxFingerprintInsertSize =
+        MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(CommonConstants.DEFAULT_MAX_FINGERPRINT_INSERT_SIZE)
+
     override fun getLayoutId(): Int {
         return R.layout.fragment_set_fingerprint
     }
@@ -52,7 +61,15 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
         binding.back.setDebouncedClickListener {
             navController.popBackStack()
         }
+        binding.maxFingerprintInsertTip.text = CommonUtils.getStr(
+            "max_fingerprint_insert_tip",
+            maxFingerprintInsertSize
+        )
         binding.add.setDebouncedClickListener {
+            if (viewModel.fingerprintData.size >= maxFingerprintInsertSize) {
+                showToast(CommonUtils.getStr("fingerprint_limit_tip"))
+                return@setDebouncedClickListener
+            }
             mFingerprintPressTimes = 0
             mFingerprintInputErrorTimes = 0
             fingerprintTempData.clear()

+ 57 - 0
app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SettingsFragment.kt

@@ -0,0 +1,57 @@
+package com.grkj.iscs.features.main.fragment.user_info
+
+import com.google.android.gms.common.internal.service.Common
+import com.grkj.data.data.MMKVConstants
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.FragmentSettingsBinding
+import com.grkj.shared.utils.CountdownTimer
+import com.grkj.ui_base.base.BaseFragment
+import com.grkj.ui_base.utils.CommonUtils
+import com.sik.sikcore.extension.saveMMKVData
+import com.sik.sikcore.extension.setDebouncedClickListener
+import dagger.hilt.android.AndroidEntryPoint
+
+/**
+ * 设置界面
+ */
+@AndroidEntryPoint
+class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
+    override fun getLayoutId(): Int {
+        return R.layout.fragment_settings
+    }
+
+    override fun initView() {
+        binding.back.setDebouncedClickListener {
+            navController.popBackStack()
+        }
+        binding.confirm.setDebouncedClickListener {
+            if (checkData()) {
+                MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.saveMMKVData(
+                    binding.maxFingerprintInsert.text.toString().toInt()
+                )
+                val autoLogoutTime =
+                    binding.autoLogoutTime.text.toString().toLong()
+                MMKVConstants.KEY_AUTO_LOGOUT_TIME.saveMMKVData(
+                    autoLogoutTime
+                )
+                CountdownTimer.reset(autoLogoutTime)
+                showToast(CommonUtils.getStr("save_success"))
+            }
+        }
+    }
+
+    /**
+     * 检查数据
+     */
+    private fun checkData(): Boolean {
+        if (binding.maxFingerprintInsert.text.toString().isEmpty()) {
+            showToast(CommonUtils.getStr("please_input_max_fingerprint_entries_size"))
+            return false
+        }
+        if (binding.autoLogoutTime.text.toString().isEmpty()) {
+            showToast(CommonUtils.getStr("please_input_auto_logout_time"))
+            return false
+        }
+        return true
+    }
+}

+ 7 - 1
app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/UserInfoHomeFragment.kt

@@ -59,6 +59,12 @@ class UserInfoHomeFragment : BaseFragment<FragmentUserInfoHomeBinding>() {
             I18nManager.t(RoleFunctionalPermissionsEnum.CARD_SETTING.description),
             RoleFunctionalPermissionsEnum.CARD_SETTING.functionalPermission
         ),
+        MenuItemEntity(
+            5,
+            "icon_settings.png",
+            I18nManager.t(RoleFunctionalPermissionsEnum.SETTINGS.description),
+            RoleFunctionalPermissionsEnum.SETTINGS.functionalPermission
+        ),
         MenuItemEntity(
             6,
             "leave.svg",
@@ -128,7 +134,7 @@ class UserInfoHomeFragment : BaseFragment<FragmentUserInfoHomeBinding>() {
             }
 
             5 -> {
-
+                navController.navigate(R.id.action_userInfoHomeFragment_to_settingsFragment)
             }
 
             6 -> {

+ 21 - 13
app/src/main/res/layout/fragment_set_fingerprint.xml

@@ -28,10 +28,10 @@
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:layout_weight="1"
-                app:i18nKey='@{"set_fingerprint_title"}'
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_md"
-                android:textStyle="bold" />
+                android:textStyle="bold"
+                app:i18nKey='@{"set_fingerprint_title"}' />
 
             <TextView
                 android:id="@+id/back"
@@ -41,14 +41,14 @@
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/common_btn_secondary"
                 android:drawableLeft="@mipmap/icon_back"
-                android:drawableTint="?attr/colorPrimary"
                 android:drawablePadding="@dimen/iscs_space_2"
+                android:drawableTint="?attr/colorPrimary"
                 android:gravity="center"
                 android:minHeight="@dimen/common_btn_height"
                 android:paddingHorizontal="@dimen/iscs_space_4"
-                app:i18nKey='@{"back"}'
                 android:textColor="?attr/colorTextPrimary"
-                android:textSize="@dimen/iscs_text_md" />
+                android:textSize="@dimen/iscs_text_md"
+                app:i18nKey='@{"back"}' />
         </LinearLayout>
 
         <View
@@ -71,9 +71,9 @@
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/common_btn_secondary"
                 android:paddingHorizontal="@dimen/iscs_space_4"
-                app:i18nKey='@{"insert"}'
                 android:textColor="?attr/colorTextPrimary"
-                android:textSize="@dimen/iscs_text_md" />
+                android:textSize="@dimen/iscs_text_md"
+                app:i18nKey='@{"insert"}' />
 
 
             <TextView
@@ -83,10 +83,18 @@
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/common_btn_secondary"
                 android:paddingHorizontal="@dimen/iscs_space_4"
-                app:i18nKey='@{"delete"}'
                 android:textColor="?attr/colorTextPrimary"
-                android:textSize="@dimen/iscs_text_md" />
+                android:textSize="@dimen/iscs_text_md"
+                app:i18nKey='@{"delete"}' />
 
+            <TextView
+                android:id="@+id/max_fingerprint_insert_tip"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_sm" />
         </LinearLayout>
 
         <LinearLayout
@@ -111,8 +119,8 @@
                 android:layout_weight="1"
                 android:gravity="center"
                 android:textColor="?attr/colorTextPrimary"
-                app:i18nKey='@{"fingerprint_code"}'
-                android:textSize="@dimen/iscs_text_md" />
+                android:textSize="@dimen/iscs_text_md"
+                app:i18nKey='@{"fingerprint_code"}' />
 
             <TextView
                 android:layout_width="0dp"
@@ -120,8 +128,8 @@
                 android:layout_weight="1"
                 android:gravity="center"
                 android:textColor="?attr/colorTextPrimary"
-                app:i18nKey='@{"operation"}'
-                android:textSize="@dimen/iscs_text_md" />
+                android:textSize="@dimen/iscs_text_md"
+                app:i18nKey='@{"operation"}' />
         </LinearLayout>
 
         <com.drake.statelayout.StateLayout

+ 147 - 0
app/src/main/res/layout/fragment_settings.xml

@@ -0,0 +1,147 @@
+<?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">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="@dimen/iscs_space_2"
+        android:background="@drawable/home_card_bg"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:id="@+id/title_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            android:paddingHorizontal="@dimen/iscs_space_2">
+
+            <ImageView
+                android:layout_width="@dimen/title_icon_size"
+                android:layout_height="@dimen/title_icon_size"
+                android:tint="?attr/colorPrimary"
+                app:skinSrc='@{"icon_settings.png"}' />
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                android:layout_weight="1"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                android:textStyle="bold"
+                app:i18nKey='@{"settings"}' />
+
+            <TextView
+                android:id="@+id/back"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginVertical="5dp"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                android:background="@drawable/common_btn_secondary"
+                android:drawableLeft="@mipmap/icon_back"
+                android:drawablePadding="@dimen/iscs_space_2"
+                android:drawableTint="?attr/colorPrimary"
+                android:gravity="center"
+                android:minHeight="@dimen/common_btn_height"
+                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:i18nKey='@{"back"}' />
+        </LinearLayout>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/divider_line_space"
+            android:background="?attr/colorBlack" />
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:paddingHorizontal="@dimen/iscs_space_2"
+            android:paddingVertical="@dimen/iscs_space_2">
+
+            <TextView
+                android:id="@+id/max_fingerprint_insert_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="label"
+                app:i18nKey='@{"max_fingerprint_insert"}'
+                app:markPosition="start"
+                app:required="true" />
+
+            <EditText
+                android:id="@+id/max_fingerprint_insert"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/max_fingerprint_insert_tv"
+                android:layout_alignLeft="@+id/max_fingerprint_insert_tv"
+                android:layout_marginTop="@dimen/iscs_space_2"
+                android:background="@drawable/bg_common_input"
+                android:inputType="number"
+                android:maxLines="1"
+                android:minWidth="@dimen/add_to_map_input_min_width"
+                android:paddingHorizontal="@dimen/iscs_space_2"
+                android:paddingVertical="2dp"
+                android:singleLine="true"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="field"
+                app:i18nHint='@{"please_input_max_fingerprint_entries_size"}' />
+
+            <TextView
+                android:id="@+id/auto_logout_time_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/max_fingerprint_insert"
+                android:layout_marginTop="@dimen/iscs_space_4"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="label"
+                app:i18nKey='@{"auto_logout_time"}'
+                app:markPosition="start"
+                app:required="true" />
+
+            <EditText
+                android:id="@+id/auto_logout_time"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/auto_logout_time_tv"
+                android:layout_alignLeft="@+id/auto_logout_time_tv"
+                android:layout_marginTop="@dimen/iscs_space_2"
+                android:background="@drawable/bg_common_input"
+                android:inputType="number"
+                android:maxLines="1"
+                android:minWidth="@dimen/add_to_map_input_min_width"
+                android:paddingHorizontal="@dimen/iscs_space_2"
+                android:paddingVertical="2dp"
+                android:singleLine="true"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:formRole="field"
+                app:i18nHint='@{"please_input_auto_logout_time"}' />
+
+            <TextView
+                android:id="@+id/confirm"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:layout_alignParentBottom="true"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                android:background="@drawable/common_btn_confirm"
+                android:drawableLeft="@mipmap/icon_confirm"
+                android:drawablePadding="@dimen/iscs_space_2"
+                android:gravity="center"
+                android:minHeight="@dimen/common_btn_height"
+                android:paddingHorizontal="@dimen/iscs_space_4"
+                android:textColor="?attr/colorTextPrimary"
+                android:textSize="@dimen/iscs_text_md"
+                app:i18nKey='@{"confirm"}' />
+        </RelativeLayout>
+    </LinearLayout>
+</layout>

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

@@ -26,6 +26,9 @@
         <action
             android:id="@+id/action_userInfoHomeFragment_to_setFingerprintFragment"
             app:destination="@id/setFingerprintFragment" />
+        <action
+            android:id="@+id/action_userInfoHomeFragment_to_settingsFragment"
+            app:destination="@id/settingsFragment" />
     </fragment>
     <fragment
         android:id="@+id/userInfoFragment"
@@ -47,4 +50,8 @@
         android:id="@+id/setJobCardFragment"
         android:name="com.grkj.iscs.features.main.fragment.user_info.SetJobCardFragment"
         android:label="SetJobCardFragment" />
+    <fragment
+        android:id="@+id/settingsFragment"
+        android:name="com.grkj.iscs.features.main.fragment.user_info.SettingsFragment"
+        android:label="SettingsFragment" />
 </navigation>

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

@@ -20,6 +20,16 @@ object CommonConstants {
      */
     const val FINGERPRINT_FOLDER = "fingerprint"
 
+    /**
+     * 默认最大指纹录入数量
+     */
+    const val DEFAULT_MAX_FINGERPRINT_INSERT_SIZE = 3
+
+    /**
+     * 默认自动登出时间
+     */
+    const val DEFAULT_AUTO_LOGOUT_TIME = 120_0000L
+
     /**
      * 人脸文件夹
      */
@@ -44,6 +54,7 @@ object CommonConstants {
      * 密码正则
      */
     const val REGEX_USERNAME = "^[A-Za-z0-9]{6,20}$"
+
     /**
      * 密码正则
      */

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

@@ -55,4 +55,13 @@ object MMKVConstants {
      */
     val KEY_DEFAULT_WORKFLOW_ID get() = "${MainDomainData.userInfo?.userId ?: 0L}_key_default_workflow_id"
 
+    /**
+     * 最大指纹录入
+     */
+    const val KEY_MAX_FINGERPRINT_INSERT = "key_max_fingerprint_insert"
+
+    /**
+     * 自动退出时间
+     */
+    const val KEY_AUTO_LOGOUT_TIME = "key_auto_logout_time"
 }

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

@@ -69,7 +69,7 @@ enum class RoleFunctionalPermissionsEnum(
         listOf()
     ),
     USER_INFO("user_info:user_info", "user_info", 1, listOf()),
-    RESET_PASSWORD("user_info:reset_password","reset_password", 1, listOf()),
+    RESET_PASSWORD("user_info:reset_password", "reset_password", 1, listOf()),
     FINGERPRINT_SETTING(
         "user_info:fingerprint_setting",
         "fingerprint_setting",
@@ -78,6 +78,7 @@ enum class RoleFunctionalPermissionsEnum(
     ),
     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()),
 
     TODO_LIST("home:todo_list", "todo_list", 1, listOf()),
@@ -92,6 +93,7 @@ enum class RoleFunctionalPermissionsEnum(
             FINGERPRINT_SETTING,
             FACE_SETTING,
             CARD_SETTING,
+            SETTINGS,
             LOGOUT
         )
     ),

+ 5 - 1
data/src/main/java/com/grkj/data/logic/impl/standard/SysMenuLogic.kt

@@ -54,7 +54,8 @@ class SysMenuLogic @Inject constructor(val sysMenuDao: SysMenuDao, val roleDao:
                                 RoleFunctionalPermissionsEnum.USER_MANAGE,
                                 RoleFunctionalPermissionsEnum.ROLE_MANAGE,
                                 RoleFunctionalPermissionsEnum.BACKUP_AND_RESTORE,
-                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE
+                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()
@@ -74,6 +75,7 @@ class SysMenuLogic @Inject constructor(val sysMenuDao: SysMenuDao, val roleDao:
                                 RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE,
                                 RoleFunctionalPermissionsEnum.EXCEPTION_JOB,
                                 RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS,
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()
@@ -93,6 +95,7 @@ class SysMenuLogic @Inject constructor(val sysMenuDao: SysMenuDao, val roleDao:
                                 RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE,
                                 RoleFunctionalPermissionsEnum.EXCEPTION_JOB,
                                 RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS,
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()
@@ -113,6 +116,7 @@ class SysMenuLogic @Inject constructor(val sysMenuDao: SysMenuDao, val roleDao:
                                 RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE,
                                 RoleFunctionalPermissionsEnum.EXCEPTION_JOB,
                                 RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE,
+                                RoleFunctionalPermissionsEnum.SETTINGS,
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()

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

@@ -0,0 +1,147 @@
+package com.grkj.shared.utils
+
+import android.os.SystemClock
+import kotlinx.coroutines.*
+
+/**
+ * 单例倒计时:
+ * - 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 = 0L,
+        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)
+    }
+}