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

feat(用户):
- 新增用户指纹录入功能,支持多次(默认3次)采集以提高准确性,并限制最大录入数量。
- 新增用户人脸信息采集功能,提供预览、倒计时拍摄和重拍功能。
- 用户列表界面增加“录入工卡”、“录入指纹”、“录入人脸”的操作入口。
- 用户编辑/新增对话框增加“刷卡录入”按钮,支持通过RFID刷卡方式直接录入或更新用户工卡信息。
- 用户列表和相关数据模型中,将原`cardCodes`(卡号列表)调整为`cardNfc`(NFC卡号),并增加`fingerprintSize`(指纹数量)字段。

refactor(用户):
- 优化用户新增和编辑对话框的布局,调整了部分控件的宽度和对齐方式,以适应新的“刷卡录入”功能。
- 调整用户列表“操作”列的宽度占比,为新增操作按钮腾出空间。
- `UserManageViewModel`中增加处理工卡绑定、指纹数据增删改查的逻辑。
- `HardwareLogic`中更新用户工卡绑定逻辑,在绑定新卡时会清除用户已有的其他工卡绑定关系,并使用传入的用户名更新工卡信息。
- `UserLogic`中,用户列表数据获取和筛选逻辑同步更新,以支持新的`cardNfc`和`fingerprintSize`字段。
- 统一了`ArcSoftUtil`中摄像头初始化逻辑,移除冗余的`context`和`windowManager`参数,改为内部通过`SIKCore.getApplication()`和`DisplayUtils`获取。

fix(UI):
- 修复`SetFaceFragment`、`UserInfoFragment`和`CheckFaceDialog`中,人脸识别摄像头预览尺寸可能不正确的问题,通过调整`TextureView`布局参数修复。
- 修复作业执行时,如果只有一个上锁/解锁组,仍然会弹出选择框的问题,改为直接执行操作。
- 修复用户管理列表项中,操作按钮区域在某些情况下显示不完整的问题。
- 更新中英文相关提示文案,如:“刷卡录入”、“该工卡已被绑定”、“工卡绑定成功”等。

refactor(工具类):
- 新增`DisplayUtils`工具类,用于获取屏幕旋转角度,以支持`ArcSoftUtil`中摄像头方向的正确设置。

周文健 пре 1 месец
родитељ
комит
984a7562bf
38 измењених фајлова са 1100 додато и 125 уклоњено
  1. 1 2
      app/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt
  2. 38 16
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddUserDialog.kt
  3. 147 0
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt
  4. 37 11
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdateUserDialog.kt
  5. 185 5
      app/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt
  6. 22 16
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt
  7. 0 2
      app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFaceFragment.kt
  8. 0 1
      app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt
  9. 0 2
      app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/UserInfoFragment.kt
  10. 73 1
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/data_manage/UserManageViewModel.kt
  11. 199 0
      app/src/main/res/layout-land/dialog_register_face.xml
  12. 54 11
      app/src/main/res/layout/dialog_add_user.xml
  13. 190 0
      app/src/main/res/layout/dialog_register_face.xml
  14. 40 19
      app/src/main/res/layout/dialog_update_user.xml
  15. 1 1
      app/src/main/res/layout/fragment_set_face.xml
  16. 1 1
      app/src/main/res/layout/fragment_user_manage.xml
  17. 6 5
      app/src/main/res/layout/item_user_manage_user.xml
  18. 2 1
      app/src/main/res/values-en/strings.xml
  19. 1 0
      app/src/main/res/values-zh/strings.xml
  20. 1 0
      app/src/main/res/values/dimens.xml
  21. 2 0
      app/src/main/res/values/strings.xml
  22. 1 1
      data/src/main/java/com/grkj/data/dao/HardwareDao.kt
  23. 1 1
      data/src/main/java/com/grkj/data/dao/IsSopDao.kt
  24. 1 1
      data/src/main/java/com/grkj/data/dao/JobTicketDao.kt
  25. 11 3
      data/src/main/java/com/grkj/data/dao/UserDao.kt
  26. 18 3
      data/src/main/java/com/grkj/data/logic/impl/standard/HardwareLogic.kt
  27. 11 5
      data/src/main/java/com/grkj/data/logic/impl/standard/UserLogic.kt
  28. 1 1
      data/src/main/java/com/grkj/data/model/vo/AddUserDataVo.kt
  29. 1 1
      data/src/main/java/com/grkj/data/model/vo/JobUserVo.kt
  30. 1 1
      data/src/main/java/com/grkj/data/model/vo/UpdateUserDataVo.kt
  31. 2 1
      data/src/main/java/com/grkj/data/model/vo/UserManageVo.kt
  32. 5 0
      data/src/main/java/com/grkj/data/repository/UserRepository.kt
  33. 4 0
      data/src/main/java/com/grkj/data/repository/impl/UserRepositoryImpl.kt
  34. 2 13
      shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt
  35. 35 0
      shared/src/main/java/com/grkj/shared/utils/DisplayUtils.kt
  36. 2 0
      ui-base/src/main/res/values-en/strings.xml
  37. 2 0
      ui-base/src/main/res/values-zh/strings.xml
  38. 2 0
      ui-base/src/main/res/values/strings.xml

+ 1 - 2
app/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt

@@ -146,8 +146,7 @@ class CheckFaceDialog(
     private fun startFace() {
         ArcSoftUtil.inDetecting = false
         ActivityTracker.getCurrentActivity()?.let { context ->
-            ArcSoftUtil.initCamera(
-                context, context.windowManager, mBinding.preview!!
+            ArcSoftUtil.initCamera(mBinding.preview!!
             ) { bitmap, faceSize, alive ->
                 bitmap?.let { itBitmap ->
                     if (faceSize == 0) {

+ 38 - 16
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddUserDialog.kt

@@ -1,13 +1,14 @@
 package com.grkj.iscs.features.main.dialog.data_manage
 
+import android.annotation.SuppressLint
 import android.view.View
 import androidx.core.view.isVisible
+import com.grkj.data.config.ISCSConfig
+import com.grkj.data.data.CommonConstants
 import com.grkj.data.model.vo.AddUserDataVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogAddUserBinding
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
-import com.grkj.data.config.ISCSConfig
-import com.grkj.data.data.CommonConstants
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.extension.tip
@@ -27,6 +28,7 @@ import com.sik.sikcore.string.RegexUtils
 class AddUserDialog(
     private val roleData: List<TextDropDownDialog.TextDropDownEntity>,
     private val workstationData: List<TextDropDownDialog.TextDropDownEntity>,
+    private val apply: AddUserDialog.() -> Unit,
     private val onConfirm: (AddUserDataVo, CustomDialog) -> Unit
 ) : OnBindView<CustomDialog>(R.layout.dialog_add_user) {
 
@@ -36,8 +38,8 @@ class AddUserDialog(
 
     override fun onBind(dialog: CustomDialog, v: View) {
         binding = DialogAddUserBinding.bind(v)
-        dialog?.isCancelable = false
-        dialog?.setMaskColor(CommonUtils.getColor(com.grkj.ui_base.R.attr.scrim))
+        dialog.isCancelable = false
+        dialog.setMaskColor(CommonUtils.getColor(com.grkj.ui_base.R.attr.scrim))
 
         // 根据配置显示/隐藏工作站选择
         binding.workstationTv.isVisible = ISCSConfig.isWorkstationOn
@@ -66,8 +68,14 @@ class AddUserDialog(
         }
 
         // 取消/关闭
-        binding.cancel.setDebouncedClickListener { dialog?.dismiss() }
-        binding.closeIv.setDebouncedClickListener { dialog?.dismiss() }
+        binding.cancel.setDebouncedClickListener {
+            addUserDialogView = null
+            dialog.dismiss()
+        }
+        binding.closeIv.setDebouncedClickListener {
+            addUserDialogView = null
+            dialog.dismiss()
+        }
 
         // 确认
         binding.confirm.setDebouncedClickListener {
@@ -79,13 +87,24 @@ class AddUserDialog(
                 if (password.isEmpty()) BCryptUtils.encryptPassword("123456") else BCryptUtils.encryptPassword(
                     password
                 ),
-                "",
+                binding.cardcodeEt.text.trim().toString(),
                 selectedRoles.mapNotNull { it.getId() },
                 selectedWorkstations.mapNotNull { it.getId() },
                 binding.statusRg.checkedRadioButtonId == binding.activateRb.id
             )
+            addUserDialogView = null
             onConfirm(vo, dialog)
         }
+        apply()
+    }
+
+    /**
+     * 刷新点位数据
+     */
+    fun refreshJobCardData(
+        rfid: String
+    ) {
+        binding.cardcodeEt.setText(rfid)
     }
 
     private fun checkData(): Boolean {
@@ -94,7 +113,7 @@ class AddUserDialog(
             return false
         }
         val username = binding.usernameEt.text.toString()
-        if (!RegexUtils.isMatch(username, CommonConstants.REGEX_USERNAME)){
+        if (!RegexUtils.isMatch(username, CommonConstants.REGEX_USERNAME)) {
             PopTip.build().tip(CommonUtils.getStr("username_regex_tip"))
             return false
         }
@@ -110,10 +129,6 @@ class AddUserDialog(
             PopTip.build().tip(CommonUtils.getStr("please_select_role"))
             return false
         }
-        if (ISCSConfig.isWorkstationOn && selectedWorkstations.isEmpty()) {
-            PopTip.build().tip(CommonUtils.getStr("please_select_area"))
-            return false
-        }
         if (binding.statusRg.checkedRadioButtonId == -1) {
             PopTip.build().tip(CommonUtils.getStr("please_select_status"))
             return false
@@ -122,6 +137,11 @@ class AddUserDialog(
     }
 
     companion object {
+
+        @SuppressLint("StaticFieldLeak")
+        @JvmStatic
+        private var addUserDialogView: AddUserDialog? = null
+
         /**
          * 显示新增用户对话框并设置确认回调
          */
@@ -129,11 +149,13 @@ class AddUserDialog(
         fun show(
             roleData: List<TextDropDownDialog.TextDropDownEntity>,
             workstationData: List<TextDropDownDialog.TextDropDownEntity>,
+            apply: AddUserDialog.() -> Unit,
             onConfirm: (AddUserDataVo, CustomDialog) -> Unit
-        ) {
-            CustomDialog.show(
-                AddUserDialog(roleData, workstationData, onConfirm),
-                CustomDialog.ALIGN.CENTER
+        ): CustomDialog {
+            return CustomDialog.show(
+                addUserDialogView ?: AddUserDialog(
+                    roleData, workstationData, apply, onConfirm
+                ).also { addUserDialogView = it }, CustomDialog.ALIGN.CENTER
             )
         }
     }

+ 147 - 0
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt

@@ -0,0 +1,147 @@
+package com.grkj.iscs.features.main.dialog.data_manage
+
+import android.graphics.Bitmap
+import android.view.Gravity
+import android.view.View
+import androidx.core.view.isVisible
+import com.grkj.data.data.CommonConstants
+import com.grkj.data.data.MainDomainData
+import com.grkj.data.utils.FileStorageUtils
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.DialogRegisterFaceBinding
+import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.shared.utils.CancellableTimer
+import com.grkj.ui_base.utils.CommonUtils
+import com.kongzue.dialogx.dialogs.CustomDialog
+import com.kongzue.dialogx.interfaces.OnBindView
+import com.sik.sikcore.date.TimeUtils
+import com.sik.sikcore.extension.setDebouncedClickListener
+import com.sik.sikimage.ImageConvertUtils
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * 人脸注册
+ */
+class RegisterFaceDialog(private val onConfirm: (String, String) -> Unit) :
+    OnBindView<CustomDialog>(R.layout.dialog_register_face) {
+    private val logger: Logger = LoggerFactory.getLogger(RegisterFaceDialog::class.java)
+    private lateinit var binding: DialogRegisterFaceBinding
+    private var mCapturedBitmap: Bitmap? = null
+    private var isFaceDetect: Boolean = false
+    private var isInCountDown: Boolean = false
+    private val captureTimer = CancellableTimer(4000, 1000, {
+        binding.countDownTip.text = "${(3000 - it) / 1000}"
+    }) {
+        ArcSoftUtil.inDetecting = true
+        isFaceDetect = true
+        binding.previewLayout.visibility = View.INVISIBLE
+        binding.image.visibility = View.VISIBLE
+        binding.recapture.visibility = View.VISIBLE
+        binding.confirm.visibility = View.VISIBLE
+        binding.countDownTip.text = CommonUtils.getStr("detect_face_tip")
+        binding.countDownTip.isVisible = false
+    }
+    private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
+        isFaceDetect = false
+        ArcSoftUtil.inDetecting = false
+        isInCountDown = false
+    }
+
+    override fun onBind(dialog: CustomDialog, v: View) {
+        binding = DialogRegisterFaceBinding.bind(v)
+        dialog.isCancelable = false
+        dialog.setMaskColor(CommonUtils.getColor(com.grkj.ui_base.R.attr.scrim))
+        binding.cancel.setDebouncedClickListener {
+            releaseFace()
+            dialog.dismiss()
+        }
+        binding.confirm.setDebouncedClickListener {
+            releaseFace()
+            val saveFileName =
+                "${MainDomainData.userInfo?.userId}_face_${TimeUtils.nowString("yyyyMMddHHmmss")}"
+            val imageData =
+                ImageConvertUtils.bitmapToBase64(mCapturedBitmap).toString()
+            FileStorageUtils.writeText(
+                CommonConstants.FACE_FOLDER,
+                saveFileName,
+                imageData
+            )
+            val savePath = FileStorageUtils.getFilePath(
+                CommonConstants.FACE_FOLDER,
+                saveFileName
+            )
+            onConfirm(imageData, savePath)
+        }
+        binding.recapture.setDebouncedClickListener {
+            reCaptureTimer.start()
+            binding.countDownTip.isVisible = false
+            binding.image.isVisible = false
+            binding.previewLayout.isVisible = true
+            binding.confirm.isVisible = false
+            binding.recapture.isVisible = false
+            binding.faceOverlayView.setFaceRect(null)
+        }
+        startFace()
+    }
+
+    private fun startFace() {
+        binding.previewLayout.isVisible = true
+        binding.image.isVisible = false
+        ArcSoftUtil.inDetecting = false
+        ArcSoftUtil.initCamera(
+            binding.preview,
+            binding.faceOverlayView,
+            true,
+        ) { bitmap, faceSize, alive ->
+            binding.tipTv.isVisible = faceSize > 1 || alive == false
+            logger.info("人脸检测结果: ${bitmap == null},$faceSize,$alive")
+            if (faceSize > 1) {
+                binding.tipTv.text = CommonUtils.getStr("only_one_person_allowed")
+                ArcSoftUtil.inDetecting = false
+                stopCountDown()
+                return@initCamera
+            }
+            if (alive == false) {
+                binding.tipTv.text =
+                    CommonUtils.getStr("real_person_verification_required")
+                ArcSoftUtil.inDetecting = false
+                stopCountDown()
+                return@initCamera
+            }
+            if (!isInCountDown) {
+                startCountDown()
+            }
+            if (!isFaceDetect) {
+                mCapturedBitmap = bitmap
+                binding.image.setImageBitmap(bitmap)
+            }
+            ArcSoftUtil.inDetecting = false
+        }
+    }
+
+    private fun startCountDown() {
+        isInCountDown = true
+        binding.countDownTip.text = CommonUtils.getStr("detect_face_tip")
+        binding.countDownTip.isVisible = true
+        captureTimer.start()
+    }
+
+    private fun stopCountDown() {
+        isInCountDown = false
+        binding.countDownTip.text = CommonUtils.getStr("detect_face_tip")
+        binding.countDownTip.isVisible = false
+        captureTimer.cancel()
+    }
+
+    private fun releaseFace() {
+        ArcSoftUtil.stop()
+    }
+
+    companion object {
+        @JvmStatic
+        fun show(onConfirm: (String, String) -> Unit) {
+            CustomDialog.show(RegisterFaceDialog(onConfirm), CustomDialog.ALIGN.CENTER)
+        }
+    }
+}

+ 37 - 11
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdateUserDialog.kt

@@ -27,6 +27,7 @@ class UpdateUserDialog(
     private val userVo: UserManageVo,
     private val roleData: List<TextDropDownDialog.TextDropDownEntity>,
     private val workstationData: List<TextDropDownDialog.TextDropDownEntity>,
+    private val apply: UpdateUserDialog.() -> Unit,
     private val onConfirm: (UpdateUserDataVo, CustomDialog) -> Unit
 ) : OnBindView<CustomDialog>(R.layout.dialog_update_user) {
 
@@ -36,21 +37,20 @@ class UpdateUserDialog(
 
     override fun onBind(dialog: CustomDialog, v: View) {
         binding = DialogUpdateUserBinding.bind(v)
-        dialog?.isCancelable = false
-        dialog?.setMaskColor(CommonUtils.getColor(com.grkj.ui_base.R.attr.scrim))
+        dialog.isCancelable = false
+        dialog.setMaskColor(CommonUtils.getColor(com.grkj.ui_base.R.attr.scrim))
 
         // 显示或隐藏工作站选项
         binding.workstationTv.isVisible = ISCSConfig.isWorkstationOn
         binding.workstationNameTv.isVisible = ISCSConfig.isWorkstationOn
 
-        binding.cardcodeEt.isEnabled = false
         binding.usernameEt.isEnabled = false
         binding.roleTv.isEnabled = userVo.userId != 1L
 
         // 预填数据
         binding.usernameEt.setText(userVo.userName)
         binding.nicknameEt.setText(userVo.nickName)
-        binding.cardcodeEt.setText(userVo.cardCodes.joinToString(","))
+        binding.cardcodeEt.setText(userVo.cardNfc)
         binding.roleTv.text = userVo.roleNames.filterNotNull().joinToString(",")
         binding.workstationNameTv.text = userVo.workstationNames.filterNotNull().joinToString(",")
         binding.activateRb.isChecked = userVo.getStatus()
@@ -92,8 +92,14 @@ class UpdateUserDialog(
         }
 
         // 取消/关闭
-        binding.cancel.setDebouncedClickListener { dialog?.dismiss() }
-        binding.closeIv.setDebouncedClickListener { dialog?.dismiss() }
+        binding.cancel.setDebouncedClickListener {
+            updateUserDialogView = null
+            dialog.dismiss()
+        }
+        binding.closeIv.setDebouncedClickListener {
+            updateUserDialogView = null
+            dialog.dismiss()
+        }
 
         // 确认
         binding.confirm.setDebouncedClickListener {
@@ -105,8 +111,6 @@ class UpdateUserDialog(
                 .tip(CommonUtils.getStr("please_input_nickname"))
             if (selectedRoles.isEmpty()) return@setDebouncedClickListener PopTip.build()
                 .tip(CommonUtils.getStr("please_select_role"))
-            if (ISCSConfig.isWorkstationOn && selectedWorkstations.isEmpty()) return@setDebouncedClickListener PopTip.build()
-                .tip(CommonUtils.getStr("please_select_area"))
             if (password.isNotEmpty() && password.length !in 6..20) {
                 PopTip.build().tip(CommonUtils.getStr("password_hint"))
                 return@setDebouncedClickListener
@@ -122,11 +126,26 @@ class UpdateUserDialog(
                 selectedWorkstations.mapNotNull { it.getId() },
                 isActive
             )
+            updateUserDialogView = null
             onConfirm(updateVo, dialog)
         }
+        apply()
+    }
+
+    /**
+     * 刷新点位数据
+     */
+    fun refreshJobCardData(
+        rfid: String
+    ) {
+        binding.cardcodeEt.setText(rfid)
     }
 
     companion object {
+
+        @JvmStatic
+        private var updateUserDialogView: UpdateUserDialog? = null
+
         /**
          * 显示更新用户对话框并设置回调
          */
@@ -135,10 +154,17 @@ class UpdateUserDialog(
             userVo: UserManageVo,
             roleData: List<TextDropDownDialog.TextDropDownEntity>,
             workstationData: List<TextDropDownDialog.TextDropDownEntity>,
+            apply: UpdateUserDialog.() -> Unit,
             onConfirm: (UpdateUserDataVo, CustomDialog) -> Unit
-        ) {
-            CustomDialog.show(
-                UpdateUserDialog(userVo, roleData, workstationData, onConfirm),
+        ): CustomDialog {
+            return CustomDialog.show(
+                updateUserDialogView ?: UpdateUserDialog(
+                    userVo,
+                    roleData,
+                    workstationData,
+                    apply,
+                    onConfirm
+                ).also { updateUserDialogView = it },
                 CustomDialog.ALIGN.CENTER
             )
         }

+ 185 - 5
app/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt

@@ -1,6 +1,8 @@
 package com.grkj.iscs.features.main.fragment.data_manage
 
+import android.graphics.Bitmap
 import android.graphics.Color
+import android.widget.TextView
 import androidx.fragment.app.viewModels
 import com.drake.brv.BindingAdapter
 import com.drake.brv.annotaion.DividerOrientation
@@ -8,23 +10,34 @@ 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.EventConstants
+import com.grkj.data.data.MMKVConstants
 import com.grkj.data.model.vo.UserManageVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentUserManageBinding
 import com.grkj.iscs.databinding.ItemUserManageUserBinding
+import com.grkj.iscs.features.main.dialog.SwipCardOperationTipDialog
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.iscs.features.main.dialog.data_manage.AddUserDialog
 import com.grkj.iscs.features.main.dialog.data_manage.FilterUserDialog
 import com.grkj.iscs.features.main.dialog.data_manage.UpdateUserDialog
+import com.grkj.iscs.features.main.dialog.user_info.AddFingerprintDialog
 import com.grkj.iscs.features.main.viewmodel.data_manage.UserManageViewModel
+import com.grkj.shared.model.EventBean
 import com.grkj.shared.utils.i18n.I18nManager
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog
 import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.extension.tip
-import com.kongzue.dialogx.dialogs.PopTip
+import com.grkj.ui_base.utils.event.RFIDCardReadEvent
+import com.grkj.ui_base.utils.fingerprint.FingerprintUtil
+import com.kongzue.dialogx.dialogs.CustomDialog
+import com.kongzue.dialogx.interfaces.DialogLifecycleCallback
+import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.setDebouncedClickListener
+import com.sik.sikimage.ImageConvertUtils
 import dagger.hilt.android.AndroidEntryPoint
+import java.util.UUID
 
 /**
  * 用户管理
@@ -32,6 +45,24 @@ import dagger.hilt.android.AndroidEntryPoint
 @AndroidEntryPoint
 class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
     private val viewModel: UserManageViewModel by viewModels()
+    private var fingerprintTempData = mutableListOf<ByteArray>()
+    private var mFingerprintPressTimes: Int = 0
+    private var mFingerprintInputErrorTimes: Int = 0
+    private var mFingerprintGroupName: String = ""
+    private val maxPressTimes = 3
+    private val inputFingerprintErrorTimes = 3
+    private var pressTip: TextView? = null
+    private val inputFingerprintIds: MutableList<Long> = mutableListOf()
+
+    private var addUserDialog: AddUserDialog? = null
+    private var updateUserDialog: UpdateUserDialog? = null
+
+    /**
+     * 最大指纹录入数
+     */
+    private val maxFingerprintInsertSize =
+        MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(CommonConstants.DEFAULT_MAX_FINGERPRINT_INSERT_SIZE)
+
     override fun getLayoutId(): Int {
         return R.layout.fragment_user_manage
     }
@@ -48,6 +79,8 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
             getUserData(false)
         }
         binding.addUser.setDebouncedClickListener {
+            viewModel.startReadCard = true
+            viewModel.isDialogRead = true
             viewModel.getRoleAndWorkStationData().observe(this) {
                 AddUserDialog.show(viewModel.roleData.map {
                     val i18NRoleName = CommonUtils.getStr(it.roleKey ?: "")
@@ -78,6 +111,8 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                                 }
                             })
                         }
+                }, {
+                    addUserDialog = this
                 }) { data, dialog ->
                     viewModel.validateUserData(data.username).observe(this) {
                         viewModel.addUser(data).observe(this) {
@@ -95,7 +130,13 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                             }
                         }
                     }
-                }
+                }.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
+                    override fun onDismiss(dialog: CustomDialog?) {
+                        viewModel.startReadCard = false
+                        viewModel.isDialogRead = false
+                        super.onDismiss(dialog)
+                    }
+                })
             }
         }
         binding.filterUser.setDebouncedClickListener {
@@ -136,7 +177,7 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
         val itemBinding = holder.getBinding<ItemUserManageUserBinding>()
         val item = holder.getModel<UserManageVo>()
         itemBinding.nickname.text = item.nickName
-        itemBinding.cardCode.text = item.cardCodes.joinToString(",")
+        itemBinding.cardCode.text = item.cardNfc
         itemBinding.role.text = item.roleNames.joinToString(",")
         itemBinding.select.setOnCheckedChangeListener(null)
         itemBinding.select.isChecked = item.isSelected
@@ -146,7 +187,44 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
             binding.selectAll.isChecked = viewModel.userManageDataList.all { it.isSelected }
             setSelectAllListener()
         }
+        itemBinding.registerCard.setDebouncedClickListener {
+            viewModel.startReadCard = true
+            viewModel.isDialogRead = false
+            viewModel.currentHandleUser = item
+            SwipCardOperationTipDialog.show().setDialogLifecycleCallback(object :
+                DialogLifecycleCallback<CustomDialog>() {
+                override fun onDismiss(dialog: CustomDialog?) {
+                    viewModel.startReadCard = false
+                    viewModel.isDialogRead = false
+                    super.onDismiss(dialog)
+                }
+            })
+        }
+        itemBinding.registerFingerprint.setDebouncedClickListener {
+            if (item.fingerprintSize >= maxFingerprintInsertSize) {
+                showToast(CommonUtils.getStr("fingerprint_limit_tip"))
+                return@setDebouncedClickListener
+            }
+            viewModel.currentHandleUser = item
+            mFingerprintPressTimes = 0
+            mFingerprintInputErrorTimes = 0
+            fingerprintTempData.clear()
+            AddFingerprintDialog.show({
+                FingerprintUtil.stop()
+                it.dismiss()
+            }) {
+                pressTip = it
+                it.text = CommonUtils.getStr(
+                    "fingerprint_scan_tip",
+                    maxPressTimes - mFingerprintPressTimes
+                )
+            }.apply {
+                startCaptureFingerprint(this)
+            }
+        }
         itemBinding.root.setDebouncedClickListener {
+            viewModel.startReadCard = true
+            viewModel.isDialogRead = true
             viewModel.getRoleAndWorkStationData().observe(this@UserManageFragment) {
                 UpdateUserDialog.show(item, viewModel.roleData.map {
                     val i18NRoleName = I18nManager.t(it.roleKey ?: "")
@@ -177,6 +255,8 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                                 }
                             })
                         }
+                }, {
+                    updateUserDialog = this
                 }) { data, dialog ->
                     viewModel.updateUser(data).observe(this@UserManageFragment) {
                         dialog.dismiss()
@@ -202,11 +282,77 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
                             )
                         }
                     }
-                }
+                }.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
+                    override fun onDismiss(dialog: CustomDialog?) {
+                        viewModel.startReadCard = false
+                        viewModel.isDialogRead = false
+                        super.onDismiss(dialog)
+                    }
+                })
             }
         }
     }
 
+    private fun startCaptureFingerprint(dialog: CustomDialog) {
+        FingerprintUtil.init(requireContext())
+        FingerprintUtil.start()
+        mFingerprintGroupName = UUID.randomUUID().toString()
+        FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
+            override fun onScan(bitmap: Bitmap) {
+                viewModel.saveUserFingerprint(
+                    ImageConvertUtils.bitmapToBase64(bitmap) ?: "",
+                    mFingerprintGroupName
+                ).observe(this@UserManageFragment) {
+                    if (it != null) {
+                        logger.info("添加指纹:${it}")
+                        inputFingerprintIds.add(it)
+                        mFingerprintPressTimes++
+                        if (mFingerprintPressTimes == maxPressTimes) {
+                            dialog?.dismiss()
+                            showToast(CommonUtils.getStr("fingerprint_add_success_tip"))
+                            getUserData(false)
+                        } else if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
+                            mFingerprintGroupName = UUID.randomUUID().toString()
+                            mFingerprintPressTimes = 0
+                            mFingerprintInputErrorTimes = 0
+                            pressTip?.text = CommonUtils.getStr(
+                                "fingerprint_scan_tip",
+                                maxPressTimes - mFingerprintPressTimes
+                            )
+                            viewModel.deleteFingerprintByIds(inputFingerprintIds)
+                                .observe(this@UserManageFragment) {
+                                    getUserData(false)
+                                }
+                            showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
+                        } else {
+                            pressTip?.text = CommonUtils.getStr(
+                                "fingerprint_scan_tip",
+                                maxPressTimes - mFingerprintPressTimes
+                            )
+                            showToast(CommonUtils.getStr("please_press_fingerprint_again"))
+                        }
+                    } else {
+                        mFingerprintInputErrorTimes++
+                        if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
+                            mFingerprintGroupName = UUID.randomUUID().toString()
+                            mFingerprintPressTimes = 0
+                            mFingerprintInputErrorTimes = 0
+                            pressTip?.text = CommonUtils.getStr(
+                                "fingerprint_scan_tip",
+                                maxPressTimes - mFingerprintPressTimes
+                            )
+                            viewModel.deleteFingerprintByIds(inputFingerprintIds)
+                                .observe(this@UserManageFragment) {
+                                    getUserData(false)
+                                }
+                            showToast(CommonUtils.getStr("please_re_press_fingerprint_again"))
+                        }
+                    }
+                }
+            }
+        })
+    }
+
     override fun initData() {
         super.initData()
         viewModel.userFilterData = null
@@ -270,4 +416,38 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
             }
         }
     }
+
+    override fun onEvent(event: EventBean<Any>) {
+        super.onEvent(event)
+        when (event.code) {
+            EventConstants.EVENT_RFID_CARD_READ -> {
+                if (!viewModel.startReadCard) {
+                    return
+                }
+                val rfid = (event.data as RFIDCardReadEvent).rfidNo
+                if (viewModel.isDialogRead) {
+                    viewModel.checkJobCardInUse(rfid).observe(this) {
+                        if (it == null) {
+                            showToast(CommonUtils.getStr(com.grkj.ui_base.R.string.job_card_already_bind))
+                            return@observe
+                        }
+                        addUserDialog?.refreshJobCardData(rfid)
+                        updateUserDialog?.refreshJobCardData(rfid)
+                    }
+                } else {
+                    viewModel.changeJobCard(rfid).observe(this) {
+                        if (it) {
+                            TipDialog.showSuccess(
+                                CommonUtils.getStr(com.grkj.ui_base.R.string.bind_job_card_success),
+                                onConfirmClick = {
+                                    getUserData(nextPage = false)
+                                })
+                        } else {
+                            TipDialog.showError(CommonUtils.getStr(com.grkj.ui_base.R.string.job_card_already_bind))
+                        }
+                    }
+                }
+            }
+        }
+    }
 }

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

@@ -1,7 +1,6 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
 import android.graphics.drawable.Drawable
-import android.graphics.drawable.GradientDrawable
 import android.view.ViewGroup
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
@@ -43,7 +42,6 @@ import com.grkj.ui_base.utils.event.FlashTipEvent
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.grkj.ui_base.utils.event.UiEvent
 import com.grkj.ui_base.utils.extension.toggleExpandView
-import com.grkj.ui_base.utils.removeBgTint
 import com.kongzue.dialogx.dialogs.BottomMenu
 import com.sik.sikcore.data.GlobalDataTempStore
 import com.sik.sikcore.date.TimeUtils
@@ -118,26 +116,34 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
                 showToast(CommonUtils.getStr("not_group_can_lock").toString())
                 return@setDebouncedClickListener
             }
-            BottomMenu.show(viewModel.groupInfo.map { it.groupName })
-                .setTitle(CommonUtils.getStr("please_select_group"))
-                .setOnMenuItemClickListener { popMenu, itemText, position ->
-                    popMenu.dismiss()
-                    toLock(viewModel.groupInfo[position].groupId)
-                    true
-                }.show(requireActivity())
+            if (viewModel.groupInfo.size > 1) {
+                BottomMenu.show(viewModel.groupInfo.map { it.groupName })
+                    .setTitle(CommonUtils.getStr("please_select_group"))
+                    .setOnMenuItemClickListener { popMenu, itemText, position ->
+                        popMenu.dismiss()
+                        toLock(viewModel.groupInfo[position].groupId)
+                        true
+                    }.show(requireActivity())
+            } else {
+                toLock(viewModel.groupInfo[0].groupId)
+            }
         }
         binding.toUnlock.setDebouncedClickListener {
             if (viewModel.groupInfo.isEmpty()) {
                 showToast(CommonUtils.getStr("not_group_can_unlock").toString())
                 return@setDebouncedClickListener
             }
-            BottomMenu.show(viewModel.groupInfo.map { it.groupName })
-                .setTitle(CommonUtils.getStr("please_select_group"))
-                .setOnMenuItemClickListener { popMenu, itemText, position ->
-                    popMenu.dismiss()
-                    toUnLock(viewModel.groupInfo[position].groupId)
-                    true
-                }.show(requireActivity())
+            if (viewModel.groupInfo.size > 1) {
+                BottomMenu.show(viewModel.groupInfo.map { it.groupName })
+                    .setTitle(CommonUtils.getStr("please_select_group"))
+                    .setOnMenuItemClickListener { popMenu, itemText, position ->
+                        popMenu.dismiss()
+                        toUnLock(viewModel.groupInfo[position].groupId)
+                        true
+                    }.show(requireActivity())
+            } else {
+                toUnLock(viewModel.groupInfo[0].groupId)
+            }
         }
         binding.waitToColockRv.grid(3).dividerSpace(10, DividerOrientation.GRID).setup {
             addType<IsJobTicketUserDataVo>(R.layout.item_job_execute_colock)

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

@@ -146,8 +146,6 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
         binding.image.isVisible = false
         ArcSoftUtil.inDetecting = false
         ArcSoftUtil.initCamera(
-            requireContext(),
-            requireActivity().windowManager,
             binding.preview,
             binding.faceOverlayView,
             true,

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

@@ -38,7 +38,6 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
     private val viewModel: UserInfoViewModel by viewModels()
     private var fingerprintTempData = mutableListOf<ByteArray>()
 
-    private var mFingerprintLimit: Int = 5
     private var mFingerprintPressTimes: Int = 0
     private var mFingerprintInputErrorTimes: Int = 0
     private var mFingerprintGroupName: String = ""

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

@@ -185,8 +185,6 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
         binding.image.isVisible = false
         ArcSoftUtil.inDetecting = false
         ArcSoftUtil.initCamera(
-            requireContext(),
-            requireActivity().windowManager,
             binding.preview,
             binding.faceOverlayView,
             true,

+ 73 - 1
app/src/main/java/com/grkj/iscs/features/main/viewmodel/data_manage/UserManageViewModel.kt

@@ -2,6 +2,7 @@ package com.grkj.iscs.features.main.viewmodel.data_manage
 
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.liveData
+import com.grkj.data.data.MainDomainData
 import com.grkj.data.model.dos.SysRole
 import com.grkj.data.model.vo.AddUserDataVo
 import com.grkj.data.model.vo.UpdateUserDataVo
@@ -13,7 +14,7 @@ import com.grkj.data.logic.IJobTicketLogic
 import com.grkj.data.logic.IRoleLogic
 import com.grkj.data.logic.IUserLogic
 import com.grkj.data.logic.IWorkstationLogic
-import com.grkj.iscs.R
+import com.grkj.data.model.dos.SysUserCharacteristicDo
 import com.grkj.ui_base.base.BaseViewModel
 import com.grkj.ui_base.utils.CommonUtils
 import dagger.hilt.android.lifecycle.HiltViewModel
@@ -37,6 +38,9 @@ class UserManageViewModel @Inject constructor(
     var userFilterData: UserManageFilterVo? = null
     var roleData: List<SysRole> = listOf()
     var workstationData: List<WorkstationManageVo> = listOf()
+    var startReadCard: Boolean = false
+    var isDialogRead: Boolean = false
+    lateinit var currentHandleUser: UserManageVo
 
     /**
      * 删除选中用户
@@ -80,6 +84,11 @@ class UserManageViewModel @Inject constructor(
             userData.workstationId?.let {
                 workstationRepository.addUserWorkstationData(userId, it)
             }
+            userData.cardNfc?.let {
+                if (it.isNotEmpty()) {
+                    hardwareRepository.updateUserJobCard(it, userId)
+                }
+            }
             roleRepository.addUserRoleData(userId, userData.roleId)
             emit(true)
         }
@@ -110,6 +119,11 @@ class UserManageViewModel @Inject constructor(
                     it
                 )
             }
+            updateUserDataVo.cardNfc.let {
+                if (it.isNotEmpty()) {
+                    hardwareRepository.updateUserJobCard(it, updateUserDataVo.userId)
+                }
+            }
             roleRepository.addUserRoleData(updateUserDataVo.userId, updateUserDataVo.roleId)
             emit(true)
         }
@@ -140,4 +154,62 @@ class UserManageViewModel @Inject constructor(
             emit(hasUserInProgressJob)
         }
     }
+
+    /**
+     * 修改工卡绑定
+     */
+    fun changeJobCard(rfid: String): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            val isJobCard = hardwareRepository.getCardByCardRfid(rfid)
+            if (isJobCard == null) {
+                hardwareRepository.updateUserJobCard(
+                    rfid,
+                    currentHandleUser.userId
+                )
+                emit(true)
+            } else {
+                if (isJobCard.userId != null && isJobCard.userId != currentHandleUser.userId) {
+                    emit(false)
+                } else {
+                    hardwareRepository.updateUserJobCard(
+                        rfid,
+                        currentHandleUser.userId
+                    )
+                    emit(true)
+                }
+            }
+        }
+    }
+
+    /**
+     * 根据id删除指纹数据
+     */
+    fun deleteFingerprintByIds(fingerprintIds: List<Long>): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            userLogic.deleteFingerprintByIds(fingerprintIds)
+            emit(true)
+        }
+    }
+
+    /**
+     * 保存用户指纹
+     */
+    fun saveUserFingerprint(b64: String, group: String): LiveData<Long> {
+        return liveData(Dispatchers.IO) {
+            val sysUserCharacteristicDo = SysUserCharacteristicDo()
+            sysUserCharacteristicDo.userId = currentHandleUser.userId
+            sysUserCharacteristicDo.content = b64
+            sysUserCharacteristicDo.type = "1"
+            sysUserCharacteristicDo.group = group
+            val fingerprintId = userLogic.saveUserCharacteristic(sysUserCharacteristicDo)
+            emit(fingerprintId)
+        }
+    }
+
+    fun checkJobCardInUse(rfid: String): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            val isJobCard = hardwareRepository.getCardByCardRfid(rfid)
+            emit(isJobCard != null && isJobCard.userId != null)
+        }
+    }
 }

+ 199 - 0
app/src/main/res/layout-land/dialog_register_face.xml

@@ -0,0 +1,199 @@
+<?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">
+
+    <com.google.android.material.card.MaterialCardView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        app:cardBackgroundColor="?attr/colorSecBg"
+        app:strokeColor="?attr/colorTransparent">
+
+        <LinearLayout
+            android:id="@+id/face_set_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:orientation="horizontal">
+
+                <FrameLayout
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_marginHorizontal="@dimen/iscs_space_5"
+                    android:layout_marginTop="@dimen/iscs_space_5"
+                    android:layout_weight="1"
+                    android:background="@drawable/common_card_bg">
+
+                    <FrameLayout
+                        android:id="@+id/preview_layout"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"
+                        android:visibility="visible">
+
+                        <TextureView
+                            android:id="@+id/preview"
+                            android:layout_width="match_parent"
+                            android:layout_height="match_parent" />
+
+
+                        <com.grkj.shared.widget.FaceOverlayView
+                            android:id="@+id/face_overlay_view"
+                            android:layout_width="match_parent"
+                            android:layout_height="match_parent" />
+
+                        <FrameLayout
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="bottom"
+                            android:background="?attr/colorTransparentHalf">
+
+                            <TextView
+                                android:id="@+id/count_down_tip"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_gravity="center"
+                                android:paddingVertical="@dimen/iscs_space_2"
+                                android:gravity="center"
+                                app:i18nKey='@{"detect_face_tip"}'
+                                android:textColor="@color/dialogxColorBlue"
+                                android:textSize="@dimen/iscs_text_xl"
+                                android:textStyle="bold"
+                                android:visibility="gone"
+                                tools:text="检测到人脸,即将拍摄" />
+
+                            <TextView
+                                android:id="@+id/tip_tv"
+                                android:layout_width="match_parent"
+                                android:layout_height="wrap_content"
+                                android:layout_gravity="center|bottom"
+                                android:paddingVertical="10dp"
+                                android:gravity="center"
+                                app:i18nKey='@{"only_one_person_allowed"}'
+                                android:textColor="?attr/colorStatusRed"
+                                android:textSize="@dimen/iscs_text_xl"
+                                android:visibility="gone"
+                                tools:text="请保证画面中只有自己" />
+                        </FrameLayout>
+
+                    </FrameLayout>
+
+                    <ImageView
+                        android:id="@+id/image"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"
+                        android:scaleType="centerCrop" />
+
+                </FrameLayout>
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_marginHorizontal="@dimen/iscs_space_5"
+                    android:layout_marginTop="@dimen/iscs_space_4"
+                    android:layout_weight="1"
+                    android:background="@drawable/common_card_bg"
+                    android:orientation="vertical"
+                    android:padding="@dimen/iscs_space_1">
+
+                    <LinearLayout
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:gravity="center_vertical"
+                        android:orientation="horizontal">
+
+                        <ImageView
+                            android:layout_width="@dimen/common_icon_size"
+                            android:layout_height="@dimen/common_icon_size"
+                            android:background="@mipmap/tip" />
+
+                        <TextView
+                            style="@style/CommonTextView"
+                            android:layout_marginLeft="@dimen/iscs_space_1"
+                            app:i18nKey='@{"capture_tip_title"}'
+                            android:textColor="?attr/colorTextPrimary"
+                            android:textSize="@dimen/iscs_text_sm" />
+                    </LinearLayout>
+
+                    <TextView
+                        style="@style/CommonTextView"
+                        android:layout_marginTop="@dimen/iscs_space_2"
+                        android:gravity="left"
+                        app:i18nKey='@{"capture_tip_content"}'
+                        android:textColor="?attr/colorTextPrimary"
+                        android:textSize="@dimen/iscs_text_sm" />
+
+                </LinearLayout>
+            </LinearLayout>
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingVertical="@dimen/iscs_space_4">
+
+                <TextView
+                    android:id="@+id/confirm"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/common_btn_confirm"
+                    android:drawableLeft="@mipmap/icon_confirm"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:drawableTint="?attr/colorWhite"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:paddingVertical="@dimen/iscs_space_2"
+                    app:i18nKey='@{"confirm"}'
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md"
+                    android:visibility="gone"
+                    app:layout_constraintEnd_toStartOf="@+id/recapture"
+                    app:layout_constraintHorizontal_bias="0.5"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <TextView
+                    android:id="@+id/recapture"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/common_btn_cancel"
+                    android:backgroundTint="@color/dialogxColorBlue"
+                    android:drawableLeft="@drawable/icon_camera"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:drawableTint="?attr/colorWhite"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:paddingVertical="@dimen/iscs_space_2"
+                    app:i18nKey='@{"recapture"}'
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md"
+                    android:visibility="gone"
+                    app:layout_constraintEnd_toStartOf="@+id/cancel"
+                    app:layout_constraintHorizontal_bias="0.5"
+                    app:layout_constraintStart_toEndOf="@+id/confirm"
+                    app:layout_constraintTop_toTopOf="@id/confirm" />
+
+                <TextView
+                    android:id="@+id/cancel"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/common_btn_cancel"
+                    android:drawableLeft="@drawable/icon_close"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:drawableTint="?attr/colorWhite"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:paddingVertical="@dimen/iscs_space_2"
+                    app:i18nKey='@{"cancel"}'
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintHorizontal_bias="0.5"
+                    app:layout_constraintStart_toEndOf="@+id/recapture"
+                    app:layout_constraintTop_toTopOf="@id/recapture" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+        </LinearLayout>
+    </com.google.android.material.card.MaterialCardView>
+</layout>

+ 54 - 11
app/src/main/res/layout/dialog_add_user.xml

@@ -42,10 +42,9 @@
             android:background="?attr/colorBlack" />
 
         <com.grkj.ui_base.widget.FormLayout
-            android:layout_width="wrap_content"
+            android:layout_width="@dimen/iscs_user_dialog_width"
             android:layout_height="match_parent"
             android:layout_weight="1"
-            android:minWidth="0dp"
             android:orientation="vertical"
             android:padding="@dimen/dialog_content_normal_padding_horizontal"
             app:columnSpacing="@dimen/iscs_space_2"
@@ -64,12 +63,12 @@
 
             <EditText
                 android:id="@+id/username_et"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
-                android:minWidth="@dimen/add_to_map_input_min_width"
                 android:background="@drawable/bg_common_input"
                 android:maxLines="1"
+                android:minWidth="@dimen/add_to_map_input_min_width"
                 android:paddingHorizontal="@dimen/iscs_space_2"
                 android:paddingVertical="2dp"
                 android:singleLine="true"
@@ -91,7 +90,7 @@
 
             <EditText
                 android:id="@+id/nickname_et"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
@@ -117,20 +116,64 @@
 
             <EditText
                 android:id="@+id/password_et"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
+                android:inputType="textPassword"
                 android:maxLines="1"
                 android:paddingHorizontal="@dimen/iscs_space_2"
                 android:paddingVertical="2dp"
                 android:singleLine="true"
-                android:inputType="textPassword"
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_md"
                 app:formRole="field"
                 app:i18nHint='@{"password_hint"}' />
 
+            <TextView
+                android:id="@+id/card_code_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='@{"user_manage_card_code"}' />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/iscs_space_2"
+                android:orientation="horizontal"
+                app:formRole="field">
+
+                <EditText
+                    android:id="@+id/cardcode_et"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:background="@drawable/bg_common_input"
+                    android:maxLines="1"
+                    android:paddingHorizontal="@dimen/iscs_space_2"
+                    android:paddingVertical="2dp"
+                    android:singleLine="true"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md" />
+
+                <TextView
+                    android:id="@+id/swip_card_click"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="@dimen/iscs_space_2"
+                    android:background="@drawable/common_btn_bg"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:gravity="center"
+                    android:minHeight="@dimen/common_btn_height"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:text="@string/swip_card_to_register"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md" />
+            </LinearLayout>
+
             <com.grkj.ui_base.widget.RequiredTextView
                 android:id="@+id/role_title_tv"
                 android:layout_width="wrap_content"
@@ -144,7 +187,7 @@
 
             <TextView
                 android:id="@+id/role_tv"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
@@ -167,11 +210,11 @@
                 app:formRole="label"
                 app:i18nKey='@{"user_manage_area"}'
                 app:markPosition="start"
-                app:required="true" />
+                app:required="false" />
 
             <TextView
                 android:id="@+id/workstation_name_tv"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
@@ -226,8 +269,8 @@
         <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:gravity="right"
             android:layout_gravity="right"
+            android:gravity="right"
             android:orientation="horizontal"
             android:padding="@dimen/iscs_space_2">
 

+ 190 - 0
app/src/main/res/layout/dialog_register_face.xml

@@ -0,0 +1,190 @@
+<?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">
+
+    <com.google.android.material.card.MaterialCardView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        app:cardBackgroundColor="?attr/colorSecBg"
+        app:strokeColor="?attr/colorTransparent">
+
+        <LinearLayout
+            android:id="@+id/face_set_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_marginHorizontal="@dimen/iscs_space_5"
+                android:layout_marginTop="@dimen/iscs_space_5"
+                android:layout_weight="1"
+                android:background="@drawable/common_card_bg">
+
+                <FrameLayout
+                    android:id="@+id/preview_layout"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:visibility="visible">
+
+                    <TextureView
+                        android:id="@+id/preview"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent" />
+
+                    <com.grkj.shared.widget.FaceOverlayView
+                        android:id="@+id/face_overlay_view"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent" />
+
+                    <FrameLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="bottom"
+                        android:background="?attr/colorTransparentHalf">
+
+                        <TextView
+                            android:id="@+id/count_down_tip"
+                            tools:text="检测到人脸,即将拍摄"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center"
+                            android:gravity="center"
+                            android:paddingVertical="@dimen/iscs_space_2"
+                            android:textColor="@color/dialogxColorBlue"
+                            android:textSize="@dimen/iscs_text_xl"
+                            android:textStyle="bold"
+                            android:visibility="gone"
+                            app:i18nKey='@{"detect_face_tip"}' />
+
+                        <TextView
+                            android:id="@+id/tip_tv"
+                            tools:text="请保证画面中只有自己"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center"
+                            android:layout_marginTop="10dp"
+                            android:layout_marginBottom="@dimen/iscs_space_2"
+                            android:gravity="center"
+                            android:paddingVertical="@dimen/iscs_space_2"
+                            android:textColor="?attr/colorStatusRed"
+                            android:textSize="@dimen/iscs_text_xl"
+                            android:visibility="gone"
+                            app:i18nKey='@{"only_one_person_allowed"}' />
+                    </FrameLayout>
+                </FrameLayout>
+
+                <ImageView
+                    android:id="@+id/image"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:scaleType="centerCrop" />
+
+            </FrameLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_marginHorizontal="@dimen/iscs_space_5"
+                android:layout_marginTop="@dimen/iscs_space_4"
+                android:layout_weight="1"
+                android:background="@drawable/common_card_bg"
+                android:orientation="vertical"
+                android:padding="@dimen/iscs_space_1">
+
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center_vertical"
+                    android:orientation="horizontal">
+
+                    <ImageView
+                        android:layout_width="@dimen/common_icon_size"
+                        android:layout_height="@dimen/common_icon_size"
+                        android:background="@mipmap/tip" />
+
+                    <TextView
+                        style="@style/CommonTextView"
+                        android:layout_marginLeft="@dimen/iscs_space_1"
+                        android:textColor="?attr/colorTextPrimary"
+                        android:textSize="@dimen/iscs_text_sm"
+                        app:i18nKey='@{"capture_tip_title"}' />
+                </LinearLayout>
+
+                <TextView
+                    style="@style/CommonTextView"
+                    android:layout_marginTop="@dimen/iscs_space_2"
+                    android:gravity="left"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_sm"
+                    app:i18nKey='@{"capture_tip_content"}' />
+            </LinearLayout>
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingVertical="@dimen/iscs_space_4">
+
+                <TextView
+                    android:id="@+id/confirm"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/common_btn_confirm"
+                    android:drawableLeft="@mipmap/icon_confirm"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:drawableTint="?attr/colorWhite"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:paddingVertical="@dimen/iscs_space_2"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md"
+                    android:visibility="gone"
+                    app:i18nKey='@{"confirm"}'
+                    app:layout_constraintEnd_toStartOf="@+id/recapture"
+                    app:layout_constraintHorizontal_bias="0.5"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <TextView
+                    android:id="@+id/recapture"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/common_btn_cancel"
+                    android:backgroundTint="@color/dialogxColorBlue"
+                    android:drawableLeft="@drawable/icon_camera"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:drawableTint="?attr/colorWhite"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:paddingVertical="@dimen/iscs_space_2"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md"
+                    android:visibility="gone"
+                    app:i18nKey='@{"recapture"}'
+                    app:layout_constraintEnd_toStartOf="@+id/cancel"
+                    app:layout_constraintHorizontal_bias="0.5"
+                    app:layout_constraintStart_toEndOf="@+id/confirm"
+                    app:layout_constraintTop_toTopOf="@id/confirm" />
+
+                <TextView
+                    android:id="@+id/cancel"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/common_btn_cancel"
+                    android:drawableLeft="@drawable/icon_close"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:drawableTint="?attr/colorWhite"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:paddingVertical="@dimen/iscs_space_2"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md"
+                    app:i18nKey='@{"cancel"}'
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintHorizontal_bias="0.5"
+                    app:layout_constraintStart_toEndOf="@+id/recapture"
+                    app:layout_constraintTop_toTopOf="@id/recapture" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+        </LinearLayout>
+    </com.google.android.material.card.MaterialCardView>
+</layout>

+ 40 - 19
app/src/main/res/layout/dialog_update_user.xml

@@ -41,7 +41,7 @@
             android:background="?attr/colorDivider" />
 
         <com.grkj.ui_base.widget.FormLayout
-            android:layout_width="wrap_content"
+            android:layout_width="@dimen/iscs_user_dialog_width"
             android:layout_height="match_parent"
             android:layout_weight="1"
             android:clipToPadding="false"
@@ -62,7 +62,7 @@
 
             <EditText
                 android:id="@+id/username_et"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
@@ -89,7 +89,7 @@
 
             <EditText
                 android:id="@+id/nickname_et"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
@@ -115,12 +115,12 @@
 
             <EditText
                 android:id="@+id/password_et"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
-                android:maxLines="1"
                 android:inputType="textPassword"
+                android:maxLines="1"
                 android:paddingHorizontal="@dimen/iscs_space_2"
                 android:paddingVertical="2dp"
                 android:singleLine="true"
@@ -138,19 +138,40 @@
                 app:formRole="label"
                 app:i18nKey='@{"user_manage_card_code"}' />
 
-            <EditText
-                android:id="@+id/cardcode_et"
-                android:layout_width="wrap_content"
+            <LinearLayout
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
-                android:background="@drawable/bg_common_input"
-                android:maxLines="1"
-                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" />
+                android:orientation="horizontal"
+                app:formRole="field">
+
+                <EditText
+                    android:id="@+id/cardcode_et"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:background="@drawable/bg_common_input"
+                    android:maxLines="1"
+                    android:paddingHorizontal="@dimen/iscs_space_2"
+                    android:paddingVertical="2dp"
+                    android:singleLine="true"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md" />
+
+                <TextView
+                    android:id="@+id/swip_card_click"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="@dimen/iscs_space_2"
+                    android:background="@drawable/common_btn_bg"
+                    android:drawablePadding="@dimen/iscs_space_2"
+                    android:gravity="center"
+                    android:minHeight="@dimen/common_btn_height"
+                    android:paddingHorizontal="@dimen/iscs_space_4"
+                    android:text="@string/swip_card_to_register"
+                    android:textColor="?attr/colorTextPrimary"
+                    android:textSize="@dimen/iscs_text_md" />
+            </LinearLayout>
 
             <com.grkj.ui_base.widget.RequiredTextView
                 android:id="@+id/role_title_tv"
@@ -165,7 +186,7 @@
 
             <TextView
                 android:id="@+id/role_tv"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"
@@ -188,11 +209,11 @@
                 app:formRole="label"
                 app:i18nKey='@{"user_manage_area"}'
                 app:markPosition="start"
-                app:required="true" />
+                app:required="false" />
 
             <TextView
                 android:id="@+id/workstation_name_tv"
-                android:layout_width="wrap_content"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="@dimen/iscs_space_2"
                 android:background="@drawable/bg_common_input"

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

@@ -122,7 +122,7 @@
                     <FrameLayout
                         android:id="@+id/preview_layout"
                         android:layout_width="match_parent"
-                        android:layout_height="match_parent"
+                        android:layout_height="0dp"
                         android:visibility="visible">
 
                         <TextureView

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

@@ -162,7 +162,7 @@
             <TextView
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
-                android:layout_weight="2"
+                android:layout_weight="1"
                 android:gravity="center"
                 android:text="@string/operation"
                 android:textColor="?attr/colorTextPrimary"

+ 6 - 5
app/src/main/res/layout/item_user_manage_user.xml

@@ -46,7 +46,7 @@
 
         <com.google.android.flexbox.FlexboxLayout
             android:layout_width="0dp"
-            android:layout_height="match_parent"
+            android:layout_height="wrap_content"
             android:layout_weight="1"
             android:gravity="center_vertical"
             app:alignItems="center"
@@ -59,7 +59,7 @@
             <TextView
                 android:id="@+id/register_card"
                 android:layout_width="wrap_content"
-                android:layout_height="match_parent"
+                android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:gravity="center"
                 android:singleLine="true"
@@ -71,7 +71,7 @@
             <TextView
                 android:id="@+id/register_fingerprint"
                 android:layout_width="wrap_content"
-                android:layout_height="match_parent"
+                android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:gravity="center"
                 android:singleLine="true"
@@ -83,12 +83,13 @@
             <TextView
                 android:id="@+id/register_face"
                 android:layout_width="wrap_content"
-                android:layout_height="match_parent"
+                android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:gravity="center"
                 android:singleLine="true"
                 android:paddingHorizontal="@dimen/common_spacing"
-                android:text="@string/register_job_card"
+                android:paddingVertical="@dimen/iscs_space_1"
+                android:text="@string/register_face"
                 android:textColor="?attr/colorTextClick"
                 android:textSize="@dimen/iscs_text_md" />
         </com.google.android.flexbox.FlexboxLayout>

+ 2 - 1
app/src/main/res/values-en/strings.xml

@@ -3,5 +3,6 @@
     <string name="register_job_card">Register Job Card</string>
     <string name="register_fingerprint">Register Fingerprint</string>
     <string name="register_face">Register Face</string>
-    <string name="register_point_rfid">录入RFID</string>
+    <string name="register_point_rfid">Register Point RFID</string>
+    <string name="swip_card_to_register">Swip Card to Register</string>
 </resources>

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

@@ -4,4 +4,5 @@
     <string name="register_fingerprint">录入指纹</string>
     <string name="register_face">录入人脸</string>
     <string name="register_point_rfid">录入RFID</string>
+    <string name="swip_card_to_register">刷卡录入</string>
 </resources>

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

@@ -79,4 +79,5 @@
     <dimen name="item_locker_group_min_width">100dp</dimen>
     <dimen name="locker_item_min_height">50dp</dimen>
     <dimen name="common_badge_icon_size">15dp</dimen>
+    <dimen name="iscs_user_dialog_width">400dp</dimen>
 </resources>

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

@@ -2,5 +2,7 @@
 <resources>
     <string name="register_job_card">录入工卡</string>
     <string name="register_fingerprint">录入指纹</string>
+    <string name="register_face">录入人脸</string>
     <string name="register_point_rfid">录入RFID</string>
+    <string name="swip_card_to_register">刷卡录入</string>
 </resources>

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

@@ -368,7 +368,7 @@ interface HardwareDao {
     /**
      * 根据用户id获取工卡数据
      */
-    @Query("select * from is_job_card ijc left join sys_user su on ijc.user_name = su.user_name where su.user_id = :userId")
+    @Query("select * from is_job_card ijc where user_id = :userId")
     fun getIsJobCardByUserId(userId: Long): MutableList<IsJobCard>
 
     /**

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

@@ -106,7 +106,7 @@ interface IsSopDao {
               isu.group_id                       AS groupId,
               isg.group_name                     AS groupName,
               -- 聚合所有卡号(如果没卡则结果里 cardCodes 会是 NULL 或空)
-              GROUP_CONCAT(DISTINCT ijc.card_code)       AS cardCodes,
+              ijc.card_nfc       AS cardNfc,
         
               GROUP_CONCAT(DISTINCT sr.role_id)           AS roleIds,
               GROUP_CONCAT(DISTINCT sr.role_name)         AS roleNames,

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

@@ -163,7 +163,7 @@ interface JobTicketDao {
               su.user_name                       AS userName,
               su.avatar,
               -- 聚合所有卡号(如果没卡则结果里 cardCodes 会是 NULL 或空)
-              GROUP_CONCAT(DISTINCT ijc.card_code)       AS cardCodes,
+              ijc.card_nfc      AS cardNfc,
         
               GROUP_CONCAT(DISTINCT sr.role_id)           AS roleIds,
               GROUP_CONCAT(DISTINCT sr.role_name)         AS roleNames,

+ 11 - 3
data/src/main/java/com/grkj/data/dao/UserDao.kt

@@ -70,7 +70,8 @@ interface UserDao {
               su.avatar,
 
       -- 聚合所有卡号(如果没卡则结果里 cardCodes 会是 NULL 或空)
-      GROUP_CONCAT(DISTINCT ijc.card_code)       AS cardCodes,
+      ijc.card_nfc       AS cardNfc,
+      (select count(1) from sys_user_characteristic where type = 1 and user_id = su.user_id and del_flag = 0)       AS fingerprintSize,
 
       GROUP_CONCAT(DISTINCT sr.role_id)           AS roleIds,
       GROUP_CONCAT(DISTINCT sr.role_name)         AS roleNames,
@@ -128,7 +129,7 @@ interface UserDao {
               su.user_name                       AS userName,
               su.avatar,
               -- 聚合所有卡号(如果没卡则结果里 cardCodes 会是 NULL 或空)
-              GROUP_CONCAT(DISTINCT ijc.card_code)       AS cardCodes,
+              ijc.card_nfc       AS cardNfc,
         
               GROUP_CONCAT(DISTINCT sr.role_id)           AS roleIds,
               GROUP_CONCAT(DISTINCT sr.role_name)         AS roleNames,
@@ -183,7 +184,8 @@ interface UserDao {
       su.user_name AS userName,
       su.password,
       su.avatar,
-      GROUP_CONCAT(DISTINCT ijc.card_code) AS cardCodes,
+      ijc.card_nfc AS cardNfc,
+      (select count(1) from sys_user_characteristic where type = 1 and user_id = su.user_id and del_flag = 0) AS fingerprintSize,
       GROUP_CONCAT(DISTINCT sr.role_id) AS roleIds,
       GROUP_CONCAT(DISTINCT sr.role_name) AS roleNames,
       GROUP_CONCAT(DISTINCT sr.role_key) AS roleKeys,
@@ -317,4 +319,10 @@ interface UserDao {
      */
     @Query("select * from sys_user where user_id in (:userIds)")
     fun getUserInfosByUserIds(userIds: List<Long>): List<SysUserDo>
+
+    /**
+     * 获取所有用户特征数据
+     */
+    @Query("select * from sys_user_characteristic where del_flag = 0")
+    fun getAllUserCharacteristic(): List<SysUserCharacteristicDo>
 }

+ 18 - 3
data/src/main/java/com/grkj/data/logic/impl/standard/HardwareLogic.kt

@@ -3,6 +3,7 @@ package com.grkj.data.logic.impl.standard
 import com.grkj.data.dao.HardwareDao
 import com.grkj.data.dao.IsolationPointDao
 import com.grkj.data.dao.JobTicketDao
+import com.grkj.data.dao.UserDao
 import com.grkj.data.data.MMKVConstants
 import com.grkj.data.data.MainDomainData
 import com.grkj.data.enums.CommonDictDataEnum
@@ -51,6 +52,7 @@ import javax.inject.Singleton
 @Singleton
 class HardwareLogic @Inject constructor(
     val hardwareDao: HardwareDao,
+    val userDao: UserDao,
     val isolationPointDao: IsolationPointDao,
     val jobTicketDao: JobTicketDao,
     val workstationLogic: WorkstationLogic
@@ -516,14 +518,27 @@ class HardwareLogic @Inject constructor(
     override fun updateUserJobCard(rfidNo: String, userId: Long) {
         val userJobCardData = hardwareDao.getIsJobCardByUserId(userId)
         if (userJobCardData.any { it.cardNfc == rfidNo }) {
-            logger.info("检测到用户工卡")
+            userJobCardData.filter { it.cardNfc != rfidNo }.forEach {
+                it.userId = null
+                it.userName = null
+                hardwareDao.updateCardInfo(it)
+            }
+            logger.info("检测到用户工卡,清除其他工卡")
             return
+        }else{
+            logger.info("检测到用户工卡,清除工卡")
+            userJobCardData.forEach {
+                it.userId = null
+                it.userName = null
+                hardwareDao.updateCardInfo(it)
+            }
         }
         var jobCardData = hardwareDao.getCardDataByRfid(rfidNo)
+        val sysUserDo = userDao.getUserInfoByUserId(userId)
         if (jobCardData != null) {
             logger.info("工卡存在,设置值")
             jobCardData.userId = userId
-            jobCardData.userName = MainDomainData.userInfo?.userName
+            jobCardData.userName = sysUserDo?.userName
             hardwareDao.updateCardInfo(jobCardData)
         } else {
             logger.info("没有检测到工卡,重新创建工卡")
@@ -532,7 +547,7 @@ class HardwareLogic @Inject constructor(
             jobCardData.cardCode = "CARD_${defaultCardCodeSize + 1}"
             jobCardData.userId = userId
             jobCardData.cardNfc = rfidNo
-            jobCardData.userName = MainDomainData.userInfo?.userName
+            jobCardData.userName = sysUserDo?.userName
             jobCardData.exStatus =
                 CommonDictDataEnum.JOB_CARD_STATUS.commonDictRes.find {
                     I18nManager.t(it.dictLabel) == I18nManager.t(

+ 11 - 5
data/src/main/java/com/grkj/data/logic/impl/standard/UserLogic.kt

@@ -25,7 +25,6 @@ import com.grkj.shared.utils.BCryptUtils
 import com.grkj.shared.utils.BiometricVerifier
 import com.grkj.shared.utils.i18n.I18nManager
 import com.sik.sikcore.data.BeanUtils
-import com.sik.sikcore.extension.deleteIfExists
 import com.sik.sikcore.extension.file
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
@@ -290,6 +289,7 @@ class UserLogic @Inject constructor(
         //3.获取所有角色信息和用户角色关联信息
         val roleDos = roleDao.getAllRole()
         val userRoleDos = roleDao.getAllUserRoles()
+        val userCharacteristicDos = userRepository.getAllUserCharacteristic()
         //4.获取所有卡片数据
         val jobCardDos = hardwareDao.getAllJobCard()
         val userManageVos = BeanUtils.copyList(sysUserDos, UserManageVo::class.java)
@@ -313,7 +313,10 @@ class UserLogic @Inject constructor(
             }
             user.workstationIds = userWorkstations.map { it.workstationId }
             user.workstationNames = userWorkstations.map { it.workstationName }
-            user.cardCodes = jobCardDos.filter { it.userId == user.userId }.map { it.cardCode }
+            user.cardNfc = jobCardDos.find { it.userId == user.userId }?.cardCode ?: ""
+            user.fingerprintSize =
+                userCharacteristicDos.groupBy { it.group }
+                    .count { it.value.any { it.userId == user.userId && it.type == "1" } }
         }
         val nickname = userManageFilterData?.nickname
         val status = userManageFilterData?.status
@@ -323,7 +326,7 @@ class UserLogic @Inject constructor(
         val filteredList = userManageVos.filter { user ->
             (nickname.isNullOrEmpty() || user.nickName.contains(nickname)) &&
                     (status == null || user.status == (if (status) "1" else "0")) &&
-                    (cardCode.isNullOrEmpty() || user.cardCodes.any { it?.contains(cardCode) == true }) &&
+                    (cardCode.isNullOrEmpty() || user.cardNfc.contains(cardCode)) &&
                     (workstationName.isNullOrEmpty() || user.workstationNames.any {
                         it?.contains(
                             workstationName
@@ -369,7 +372,7 @@ class UserLogic @Inject constructor(
             }
             user.workstationIds = userWorkstations.map { it.workstationId }
             user.workstationNames = userWorkstations.map { it.workstationName }
-            user.cardCodes = jobCardDos.filter { it.userId == user.userId }.map { it.cardCode }
+            user.cardNfc = jobCardDos.find { it.userId == user.userId }?.cardNfc ?: ""
             user.groupId = needJobTicketUser.find { it.userId == user.userId }?.groupId ?: 0
             user.groupName = jobTicketGroups.find { it.id == user.groupId }?.groupName ?: ""
         }
@@ -385,6 +388,7 @@ class UserLogic @Inject constructor(
         //3.获取所有角色信息和用户角色关联信息
         val roleDos = roleDao.getAllRole()
         val userRoleDos = roleDao.getAllUserRoles()
+        val userCharacteristicDos = userRepository.getAllUserCharacteristic()
         //4.获取所有卡片数据
         val jobCardDos = hardwareDao.getAllJobCard()
         val userManageVos = BeanUtils.copyList(sysUserDos, UserManageVo::class.java)
@@ -401,7 +405,9 @@ class UserLogic @Inject constructor(
             }
             user.workstationIds = userWorkstations.map { it.workstationId }
             user.workstationNames = userWorkstations.map { it.workstationName }
-            user.cardCodes = jobCardDos.filter { it.userId == user.userId }.map { it.cardCode }
+            user.cardNfc = jobCardDos.find { it.userId == user.userId }?.cardCode ?: ""
+            user.fingerprintSize =
+                userCharacteristicDos.count { it.userId == user.userId && it.type == "1" }
         }
         return userManageVos.filter { it.workstationIds.contains(workstationId) }
     }

+ 1 - 1
data/src/main/java/com/grkj/data/model/vo/AddUserDataVo.kt

@@ -7,7 +7,7 @@ data class AddUserDataVo(
     val username: String,
     val nickname: String,
     val password: String,
-    val cardCode: String?,
+    val cardNfc: String?,
     val roleId: List<Long>,
     val workstationId: List<Long>?,
     val status: Boolean

+ 1 - 1
data/src/main/java/com/grkj/data/model/vo/JobUserVo.kt

@@ -12,7 +12,7 @@ class JobUserVo {
     var groupId: Long = 0
     var groupName: String? = ""
     var avatar: String? = null
-    var cardCodes: List<String?> = listOf()
+    var cardNfc: String = ""
     var roleIds: List<Long?> = listOf()
     var roleNames: List<String?> = listOf()
     var roleKeys: List<String?> = listOf()

+ 1 - 1
data/src/main/java/com/grkj/data/model/vo/UpdateUserDataVo.kt

@@ -8,7 +8,7 @@ data class UpdateUserDataVo(
     val username: String,
     val password: String,
     val nickname: String,
-    val cardCode: String,
+    val cardNfc: String,
     val roleId: List<Long>,
     val workstationId: List<Long>?,
     val status: Boolean

+ 2 - 1
data/src/main/java/com/grkj/data/model/vo/UserManageVo.kt

@@ -12,13 +12,14 @@ class UserManageVo {
     var userName: String = ""
     var password: String = BCryptUtils.encryptPassword("123456")
     var avatar: String? = null
-    var cardCodes: List<String?> = listOf()
+    var cardNfc: String = ""
     var roleIds: List<Long?> = listOf()
     var roleNames: List<String?> = listOf()
     var roleKeys: List<String?> = listOf()
     var workstationIds: List<Long?> = listOf()
     var workstationNames: List<String?> = listOf()
     var status: String? = null
+    var fingerprintSize: Int = 0
 
     @Ignore
     var isSelected: Boolean = false

+ 5 - 0
data/src/main/java/com/grkj/data/repository/UserRepository.kt

@@ -106,4 +106,9 @@ interface UserRepository {
      * 根据用户id列表获取特征数据
      */
     fun getSysUserCharacteristicDosByUserIds(userIds: List<Long>): List<SysUserCharacteristicDo>
+
+    /**
+     * 获取所有用户特征数据
+     */
+    fun getAllUserCharacteristic(): List<SysUserCharacteristicDo>
 }

+ 4 - 0
data/src/main/java/com/grkj/data/repository/impl/UserRepositoryImpl.kt

@@ -74,6 +74,10 @@ class UserRepositoryImpl @Inject constructor(
         return userDao.getUserBiometricDataByUserIds(userIds)
     }
 
+    override fun getAllUserCharacteristic(): List<SysUserCharacteristicDo> {
+        return userDao.getAllUserCharacteristic()
+    }
+
     override fun deleteFingerprintByIds(fingerprintIds: List<Long>) {
         userDao.deleteFingerprintByIds(fingerprintIds)
     }

+ 2 - 13
shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt

@@ -26,6 +26,7 @@ import com.grkj.shared.utils.extension.isInCenterArea
 import com.grkj.shared.utils.face.arcsoft.CameraHelper
 import com.grkj.shared.utils.face.arcsoft.CameraListener
 import com.grkj.shared.widget.FaceOverlayView
+import com.sik.sikcore.SIKCore
 import com.sik.sikimage.ImageConvertUtils
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -188,8 +189,6 @@ object ArcSoftUtil {
 
     @JvmOverloads
     fun initCamera(
-        context: Context,
-        windowManager: WindowManager,
         preview: View,
         faceOverlayView: FaceOverlayView? = null,
         needCheckCenter: Boolean = false,
@@ -283,7 +282,7 @@ object ArcSoftUtil {
 
         cameraHelper = CameraHelper.Builder()
             .previewViewSize(Point(cameraWidth, cameraHeight))
-            .rotation(windowManager.defaultDisplay.rotation)
+            .rotation(DisplayUtils.getRotation(SIKCore.getApplication()))
             .specificCameraId(rgbCameraId ?: Camera.CameraInfo.CAMERA_FACING_FRONT)
             .isMirror(false)
             .previewOn(preview)
@@ -375,16 +374,6 @@ object ArcSoftUtil {
         cameraHelper!!.start()
     }
 
-    fun start(
-        context: Context,
-        windowManager: WindowManager,
-        preview: View,
-        callBack: (Bitmap?, Int, Boolean) -> Unit
-    ) {
-        initEngine(context)
-        initCamera(context, windowManager, preview, null, false, callBack)
-    }
-
     fun stop() {
         cameraHelper?.release()
         cameraHelper = null

+ 35 - 0
shared/src/main/java/com/grkj/shared/utils/DisplayUtils.kt

@@ -0,0 +1,35 @@
+package com.grkj.shared.utils
+
+import android.app.Activity
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.view.Display
+import android.view.Surface
+import android.view.WindowManager
+
+object DisplayUtils {
+
+    /**
+     * 获取默认屏幕的 rotation(0, 90, 180, 270)
+     */
+    fun getRotation(context: Context): Int {
+        return when {
+            // Android 11+ 有 Activity.display
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && context is Activity -> {
+                context.display?.rotation ?: Surface.ROTATION_0
+            }
+            // Android 4.0+ ~ Android 10 推荐用 WindowManager
+            Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> {
+                val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+                @Suppress("DEPRECATION")
+                wm.defaultDisplay.rotation
+            }
+            else -> {
+                // 兜底:DisplayManager 取 Display.DEFAULT_DISPLAY
+                val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+                dm.getDisplay(Display.DEFAULT_DISPLAY)?.rotation ?: Surface.ROTATION_0
+            }
+        }
+    }
+}

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

@@ -838,4 +838,6 @@
     <string name="year"></string>
     <string name="you_are_not_locker_tip">You are not the locker and cannot perform this operation</string>
     <string name="zone">Area Range</string>
+    <string name="job_card_already_bind">Job card already bind</string>
+    <string name="bind_job_card_success">Job card bind success</string>
 </resources>

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

@@ -838,4 +838,6 @@
     <string name="year">年</string>
     <string name="you_are_not_locker_tip">您不是上锁人,无法执行此操作</string>
     <string name="zone">区域范围</string>
+    <string name="job_card_already_bind">该工卡已被绑定</string>
+    <string name="bind_job_card_success">工卡绑定成功</string>
 </resources>

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

@@ -838,4 +838,6 @@
     <string name="year">年</string>
     <string name="you_are_not_locker_tip">您不是上锁人,无法执行此操作</string>
     <string name="zone">区域范围</string>
+    <string name="job_card_already_bind">该工卡已被绑定</string>
+    <string name="bind_job_card_success">工卡绑定成功</string>
 </resources>