Parcourir la source

refactor(更新)
- 异常作业的修改

周文健 il y a 4 mois
Parent
commit
7c20805bca
31 fichiers modifiés avec 631 ajouts et 272 suppressions
  1. 1 0
      app/build.gradle.kts
  2. 1 2
      app/src/main/java/com/grkj/iscs/features/main/entity/MenuItemEntity.kt
  3. 37 31
      app/src/main/java/com/grkj/iscs/features/main/fragment/exception_manage/ExceptionDetailFragment.kt
  4. 120 5
      app/src/main/java/com/grkj/iscs/features/main/fragment/exception_manage/ExceptionJobFragment.kt
  5. 1 1
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobExecuteFragment.kt
  6. 10 24
      app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobManageHomeFragment.kt
  7. 84 3
      app/src/main/java/com/grkj/iscs/features/main/viewmodel/exception_manage/ExceptionJobViewModel.kt
  8. 0 0
      app/src/main/res/drawable/badge_style.xml
  9. 2 2
      app/src/main/res/layout-land/item_home_menu.xml
  10. 2 2
      app/src/main/res/layout/dialog_drop_down_list.xml
  11. 2 2
      app/src/main/res/layout/item_home_menu.xml
  12. 5 0
      app/src/main/res/values-en/strings.xml
  13. 1 0
      app/src/main/res/values-land/dimens.xml
  14. 5 0
      app/src/main/res/values-zh/strings.xml
  15. 1 0
      app/src/main/res/values/dimens.xml
  16. 5 0
      app/src/main/res/values/strings.xml
  17. 1 0
      data/src/main/java/com/grkj/data/dao/JobTicketDao.kt
  18. 40 0
      data/src/main/java/com/grkj/data/dao/UserDao.kt
  19. 7 0
      data/src/main/java/com/grkj/data/enums/NextJobPrompt.kt
  20. 1 0
      data/src/main/java/com/grkj/data/model/vo/PointManageVo.kt
  21. 31 0
      data/src/main/java/com/grkj/data/repository/IJobTicketRepository.kt
  22. 5 0
      data/src/main/java/com/grkj/data/repository/IUserRepository.kt
  23. 27 0
      data/src/main/java/com/grkj/data/repository/impl/network/NetworkJobTicketRepository.kt
  24. 4 0
      data/src/main/java/com/grkj/data/repository/impl/network/NetworkUserRepository.kt
  25. 179 0
      data/src/main/java/com/grkj/data/repository/impl/standard/JobTicketRepository.kt
  26. 10 4
      data/src/main/java/com/grkj/data/repository/impl/standard/SysMenuRepository.kt
  27. 4 0
      data/src/main/java/com/grkj/data/repository/impl/standard/UserRepository.kt
  28. 2 0
      ui-base/build.gradle.kts
  29. 0 101
      ui-base/src/main/java/com/grkj/ui_base/ext/ViewBadgeExtensions.kt
  30. 9 0
      ui-base/src/main/java/com/grkj/ui_base/widget/BGABadgeInit.java
  31. 34 95
      ui-base/src/main/java/com/grkj/ui_base/widget/CustomNavBar.kt

+ 1 - 0
app/build.gradle.kts

@@ -89,6 +89,7 @@ dependencies {
     implementation("com.github.loper7:DateTimePicker:0.6.3")
     implementation("com.google.dagger:hilt-android:2.56.2")
     ksp("com.google.dagger:hilt-android-compiler:2.56.2")
+    kapt("com.github.bingoogolapple.BGABadgeView-Android:compiler:1.2.0")
     implementation(project(":sync"))
     implementation(project(":ui-base"))
     implementation(project(":data"))

+ 1 - 2
app/src/main/java/com/grkj/iscs/features/main/entity/MenuItemEntity.kt

@@ -11,6 +11,5 @@ data class MenuItemEntity(
     val menuIconId: Int,
     val menuText: String,
     val permission: String,
-    var badgeNum: Int = 0,
-    var badgeDrawable: BadgeDrawable? = null
+    var badgeNum: Int = 0
 )

+ 37 - 31
app/src/main/java/com/grkj/iscs/features/main/fragment/exception_manage/ExceptionDetailFragment.kt

@@ -26,40 +26,46 @@ class ExceptionDetailFragment : BaseFragment<FragmentExceptionDetailBinding>() {
     override fun initView() {
         binding.back.setDebouncedClickListener { navController.popBackStack() }
         binding.handleException.setDebouncedClickListener {
-            viewModel.handleException().observe(this) {
-                if (it.first) {
-                    TipDialog.showSuccess(
-                        getString(R.string.handle_exception_success),
-                        onConfirmClick = {
-                            binding.handleException.isVisible = false
-                            binding.cancelException.isVisible = false
-                        },
-                        onCancelClick = {
-                            binding.handleException.isVisible = false
-                            binding.cancelException.isVisible = false
-                        })
-                } else {
-                    TipDialog.showError(it.second)
-                }
-            }
+            TipDialog.showInfo(
+                getString(R.string.confirm_handle_exception),
+                onConfirmClick = {
+                    viewModel.handleException().observe(this) {
+                        if (it.first) {
+                            TipDialog.showSuccess(
+                                getString(R.string.handle_exception_success),
+                                onConfirmClick = {
+                                    binding.handleException.isVisible = false
+                                    binding.cancelException.isVisible = false
+                                },
+                                onCancelClick = {
+                                    binding.handleException.isVisible = false
+                                    binding.cancelException.isVisible = false
+                                })
+                        } else {
+                            TipDialog.showError(it.second)
+                        }
+                    }
+                })
         }
         binding.cancelException.setDebouncedClickListener {
-            viewModel.cancelException().observe(this) {
-                if (it) {
-                    TipDialog.showSuccess(
-                        getString(R.string.cancel_exception_success),
-                        onConfirmClick = {
-                            binding.handleException.isVisible = false
-                            binding.cancelException.isVisible = false
-                        },
-                        onCancelClick = {
-                            binding.handleException.isVisible = false
-                            binding.cancelException.isVisible = false
-                        })
-                } else {
-                    TipDialog.showError(getString(R.string.cancel_exception_failed))
+            TipDialog.showInfo(getString(R.string.confirm_cancel_exception), onConfirmClick = {
+                viewModel.cancelException().observe(this) {
+                    if (it) {
+                        TipDialog.showSuccess(
+                            getString(R.string.cancel_exception_success),
+                            onConfirmClick = {
+                                binding.handleException.isVisible = false
+                                binding.cancelException.isVisible = false
+                            },
+                            onCancelClick = {
+                                binding.handleException.isVisible = false
+                                binding.cancelException.isVisible = false
+                            })
+                    } else {
+                        TipDialog.showError(getString(R.string.cancel_exception_failed))
+                    }
                 }
-            }
+            })
         }
     }
 

+ 120 - 5
app/src/main/java/com/grkj/iscs/features/main/fragment/exception_manage/ExceptionJobFragment.kt

@@ -12,6 +12,7 @@ import com.drake.brv.utils.grid
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
+import com.grkj.data.enums.NextJobPrompt
 import com.grkj.data.enums.RoleEnum
 import com.grkj.data.model.dos.WorkflowStep
 import com.grkj.data.model.vo.IsJobTicketPointsDataVo
@@ -67,13 +68,127 @@ class ExceptionJobFragment : BaseFragment<FragmentExceptionJobBinding>() {
             }
         }
         binding.cancelException.setDebouncedClickListener {
-            viewModel.cancelException().observe(this) {
-                navController.popBackStack()
-            }
+            TipDialog.showInfo(getString(R.string.confirm_cancel_exception), onConfirmClick = {
+                viewModel.cancelException().observe(this) {
+                    if (it) {
+                        TipDialog.showSuccess(
+                            getString(R.string.cancel_exception_success),
+                            onConfirmClick = {
+                                navController.popBackStack()
+                            },
+                            onCancelClick = {
+                                navController.popBackStack()
+                            })
+                    } else {
+                        TipDialog.showError(getString(R.string.cancel_exception_failed))
+                    }
+                }
+            })
         }
         binding.handleException.setDebouncedClickListener {
-            viewModel.handleException().observe(this) {
-                navController.popBackStack()
+            viewModel.checkJobNeedLockOrUnlock().observe(this) {
+                when (it) {
+                    NextJobPrompt.NO_NEW_JOB -> {
+                        TipDialog.showInfo(
+                            getString(R.string.confirm_handle_exception),
+                            onConfirmClick = {
+                                viewModel.handleException().observe(this) {
+                                    if (it.first) {
+                                        TipDialog.showSuccess(
+                                            getString(R.string.handle_exception_success),
+                                            onConfirmClick = {
+                                                navController.popBackStack()
+                                            },
+                                            onCancelClick = {
+                                                navController.popBackStack()
+                                            })
+                                    } else {
+                                        TipDialog.showError(it.second)
+                                    }
+                                }
+                            })
+                    }
+
+                    NextJobPrompt.CREATE_LOCK_JOB -> {
+                        TipDialog.showInfo(
+                            getString(R.string.confirm_handle_exception),
+                            onConfirmClick = {
+                                viewModel.handleException().observe(this) {
+                                    if (it.first) {
+                                        TipDialog.showInfo(
+                                            getString(R.string.confirm_create_lock_job),
+                                            onConfirmClick = {
+                                                viewModel.createLockJob().observe(this) {
+                                                    if (it) {
+                                                        TipDialog.showSuccess(
+                                                            getString(R.string.handle_exception_success),
+                                                            onConfirmClick = {
+                                                                navController.popBackStack()
+                                                            },
+                                                            onCancelClick = {
+                                                                navController.popBackStack()
+                                                            })
+                                                    } else {
+                                                        TipDialog.showError(getString(R.string.create_job_failed))
+                                                    }
+                                                }
+                                            }, onCancelClick = {
+                                                TipDialog.showSuccess(
+                                                    getString(R.string.handle_exception_success),
+                                                    onConfirmClick = {
+                                                        navController.popBackStack()
+                                                    },
+                                                    onCancelClick = {
+                                                        navController.popBackStack()
+                                                    })
+                                            })
+                                    } else {
+                                        TipDialog.showError(it.second)
+                                    }
+                                }
+                            })
+                    }
+
+                    NextJobPrompt.CREATE_UNLOCK_JOB -> {
+                        TipDialog.showInfo(
+                            getString(R.string.confirm_handle_exception),
+                            onConfirmClick = {
+                                viewModel.handleException().observe(this) {
+                                    if (it.first) {
+                                        TipDialog.showInfo(
+                                            getString(R.string.confirm_create_lock_job),
+                                            onConfirmClick = {
+                                                viewModel.createUnlockJob().observe(this) {
+                                                    if (it) {
+                                                        TipDialog.showSuccess(
+                                                            getString(R.string.handle_exception_success),
+                                                            onConfirmClick = {
+                                                                navController.popBackStack()
+                                                            },
+                                                            onCancelClick = {
+                                                                navController.popBackStack()
+                                                            })
+                                                    } else {
+                                                        TipDialog.showError(getString(R.string.create_job_failed))
+                                                    }
+                                                }
+                                            }, onCancelClick = {
+                                                TipDialog.showSuccess(
+                                                    getString(R.string.handle_exception_success),
+                                                    onConfirmClick = {
+                                                        navController.popBackStack()
+                                                    },
+                                                    onCancelClick = {
+                                                        navController.popBackStack()
+                                                    })
+                                            })
+                                    } else {
+                                        TipDialog.showError(it.second)
+                                    }
+                                }
+                            })
+                    }
+                }
             }
         }
         binding.waitToColockRv.grid(3).dividerSpace(10, DividerOrientation.GRID).setup {

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

@@ -524,7 +524,7 @@ class JobExecuteFragment : BaseFragment<FragmentJobExecuteBinding>() {
                     //步骤开启上锁,并且点位都还没有上锁
                 (currentStep?.enableLock == true && viewModel.ticketPoints.any { it.pointStatus == "0" }) ||
                         //步骤开启共锁,并且点位都不存在没有上锁的
-                        (currentStep?.enableColock == true && viewModel.ticketPoints.all { it.pointStatus != "0" }) ||
+                        (currentStep?.enableColock == true && viewModel.ticketPoints.all { it.pointStatus == "0" }) ||
                         (currentStep?.enableLock == true && viewModel.isUnlockFirst && viewModel.ticketPoints.all { it.pointStatus == "2" })
             //步骤开启解锁,并且点位都已经上锁,并且如果支持共锁都已经解除
             binding.toUnlock.isVisible =

+ 10 - 24
app/src/main/java/com/grkj/iscs/features/main/fragment/job_manage/JobManageHomeFragment.kt

@@ -1,14 +1,13 @@
 package com.grkj.iscs.features.main.fragment.job_manage
 
 import androidx.fragment.app.viewModels
+import cn.bingoogolapple.badgeview.BGABadgeViewHelper
 import com.drake.brv.BindingAdapter
 import com.drake.brv.annotaion.DividerOrientation
 import com.drake.brv.utils.dividerSpace
 import com.drake.brv.utils.grid
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
-import com.google.android.material.badge.BadgeDrawable
-import com.google.android.material.badge.BadgeUtils
 import com.grkj.data.data.MainDomainData
 import com.grkj.data.enums.RoleFunctionalPermissionsEnum
 import com.grkj.iscs.R
@@ -17,8 +16,6 @@ import com.grkj.iscs.databinding.ItemHomeMenuBinding
 import com.grkj.iscs.features.main.entity.MenuItemEntity
 import com.grkj.iscs.features.main.viewmodel.job_manage.JobManageHomeViewModel
 import com.grkj.ui_base.base.BaseFragment
-import com.grkj.ui_base.utils.CommonUtils
-import com.grkj.ui_base.utils.GraphicUtils
 import com.grkj.ui_base.utils.event.BottomNavVisibilityEvent
 import dagger.hilt.android.AndroidEntryPoint
 
@@ -104,29 +101,18 @@ class JobManageHomeFragment : BaseFragment<FragmentJobManageHomeBinding>() {
         val item = holder.getModel<MenuItemEntity>()
         itemBinding.homeMenuIv.setImageResource(item.menuIconId)
         itemBinding.homeMenuTv.text = item.menuText
-        if (item.badgeNum != 0) {
-            if (item.badgeDrawable == null) {
-                item.badgeDrawable =
-                    BadgeDrawable.createFromResource(requireContext(), R.xml.badge_style)
+        itemBinding.homeMenuLayout.apply {
+            this.badgeViewHelper.apply {
+                setBadgeGravity(BGABadgeViewHelper.BadgeGravity.RightTop)
+                setBadgeTextSizeSp(resources.getDimension(R.dimen.badge_text_size).toInt())
             }
-            item.badgeDrawable?.apply {
-                badgeGravity = BadgeDrawable.TOP_END
-                number = item.badgeNum
-                backgroundColor = CommonUtils.getColor(com.grkj.ui_base.R.color.common_status_red)
-                isVisible = item.badgeNum > 0
-                this.text
-                itemBinding.homeMenuLayout.post {
-                    BadgeUtils.attachBadgeDrawable(
-                        this,
-                        itemBinding.homeMenuLayout,
-                        itemBinding.homeMenuLayout
-                    )
-                    itemBinding.homeMenuLayout.post {
-                        GraphicUtils.adjustBadgeSize(this)
-                    }
-                }
+            if (item.badgeNum != 0) {
+                this.showTextBadge("${item.badgeNum}")
+            } else {
+                this.hiddenBadge()
             }
         }
+
         itemBinding.root.setOnClickListener {
             onMenuClick(item.type)
         }

+ 84 - 3
app/src/main/java/com/grkj/iscs/features/main/viewmodel/exception_manage/ExceptionJobViewModel.kt

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.liveData
 import com.grkj.data.data.DictConstants
 import com.grkj.data.enums.JobTicketStatusEnum
+import com.grkj.data.enums.NextJobPrompt
 import com.grkj.data.enums.RoleEnum
 import com.grkj.data.model.dos.IsJobTicketStep
 import com.grkj.data.model.dos.WorkflowMode
@@ -19,6 +20,7 @@ import com.grkj.data.model.vo.IsJobTicketUserDataVo
 import com.grkj.data.model.vo.UserManageVo
 import com.grkj.data.repository.IExceptionRepository
 import com.grkj.data.repository.IJobTicketRepository
+import com.grkj.data.repository.IUserRepository
 import com.grkj.data.repository.IWorkflowRepository
 import com.grkj.iscs.R
 import com.grkj.ui_base.base.BaseViewModel
@@ -36,8 +38,9 @@ import javax.inject.Inject
 class ExceptionJobViewModel @Inject constructor(
     val exceptionRepository: IExceptionRepository,
     val jobTicketRepository: IJobTicketRepository,
-    val workflowRepository: IWorkflowRepository
-) : BaseViewModel() {
+    val workflowRepository: IWorkflowRepository,
+    override val userRepository: IUserRepository
+) : BaseViewModel(userRepository) {
     var ticketId: Long = 0
     var exceptionId: Long = 0
     var ticketData: IsJobTicketDataVo? = null
@@ -121,7 +124,9 @@ class ExceptionJobViewModel @Inject constructor(
                         ticketData?.ticketStatus =
                             currentWorkflowStep?.getTicketStatus() ?: ""
                         jobTicketRepository.updateTicketDataStatus(
-                            ticketId, currentWorkflowStep?.getTicketStatus()?.toInt() ?: JobTicketStatusEnum.PROGRESSING.status.toInt()
+                            ticketId,
+                            currentWorkflowStep?.getTicketStatus()?.toInt()
+                                ?: JobTicketStatusEnum.PROGRESSING.status.toInt()
                         )
                     }
                 } else {
@@ -176,4 +181,80 @@ class ExceptionJobViewModel @Inject constructor(
             emit(true)
         }
     }
+
+    /**
+     * 检查作业是否需要上锁或者解锁
+     */
+    fun checkJobNeedLockOrUnlock(): LiveData<NextJobPrompt> {
+        return liveData(Dispatchers.IO) {
+            val nextJobPrompt = jobTicketRepository.isNextLockOrUnLock(ticketId)
+            emit(nextJobPrompt)
+        }
+    }
+
+    /**
+     * 创建上锁作业
+     */
+    fun createLockJob(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            val jobTicketData = jobTicketRepository.getJobTicketDataByTicketId(ticketId)
+            if (jobTicketData == null) {
+                emit(false)
+                return@liveData
+            }
+            val jobTicketPoints = jobTicketRepository.getTicketPointsByTicketId(ticketId)
+            val jobTicketUsers = jobTicketRepository.getJobTicketUserDataByTicketId(ticketId)
+            val locker =
+                userRepository.getUserDataByUserIds(jobTicketUsers.filter { it.userRole == RoleEnum.JTLOCKER.roleKey }
+                    .map { it.userId })
+            val coLocker =
+                userRepository.getUserDataByUserIds(jobTicketUsers.filter { it.userRole == RoleEnum.JTCOLOCKER.roleKey }
+                    .map {
+                        it
+                            .userId
+                    })
+            jobTicketRepository.createLockJob(
+                jobTicketPoints.filter { it.pointStatus == "0" },
+                locker,
+                coLocker,
+                jobTicketData.sopId,
+                jobTicketData.workstationId,
+                jobTicketData.ticketName
+            )
+            emit(true)
+        }
+    }
+
+    /**
+     * 创建解锁作业
+     */
+    fun createUnlockJob(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            val jobTicketData = jobTicketRepository.getJobTicketDataByTicketId(ticketId)
+            if (jobTicketData == null) {
+                emit(false)
+                return@liveData
+            }
+            val jobTicketPoints = jobTicketRepository.getTicketPointsByTicketId(ticketId)
+            val jobTicketUsers = jobTicketRepository.getJobTicketUserDataByTicketId(ticketId)
+            val locker =
+                userRepository.getUserDataByUserIds(jobTicketUsers.filter { it.userRole == RoleEnum.JTLOCKER.roleKey }
+                    .map { it.userId })
+            val coLocker =
+                userRepository.getUserDataByUserIds(jobTicketUsers.filter { it.userRole == RoleEnum.JTCOLOCKER.roleKey }
+                    .map {
+                        it
+                            .userId
+                    })
+            jobTicketRepository.createUnLockJob(
+                jobTicketPoints.filter { it.pointStatus == "0" },
+                locker,
+                coLocker,
+                jobTicketData.sopId,
+                jobTicketData.workstationId,
+                jobTicketData.ticketName
+            )
+            emit(true)
+        }
+    }
 }

+ 0 - 0
app/src/main/res/xml/badge_style.xml → app/src/main/res/drawable/badge_style.xml


+ 2 - 2
app/src/main/res/layout-land/item_home_menu.xml

@@ -8,7 +8,7 @@
         android:gravity="center_horizontal"
         android:orientation="vertical">
 
-        <FrameLayout
+        <cn.bingoogolapple.badgeview.BGABadgeFrameLayout
             android:id="@+id/home_menu_layout"
             android:layout_width="120dp"
             android:layout_height="120dp"
@@ -19,7 +19,7 @@
                 android:layout_width="100dp"
                 android:layout_height="100dp"
                 android:layout_gravity="center" />
-        </FrameLayout>
+        </cn.bingoogolapple.badgeview.BGABadgeFrameLayout>
 
         <TextView
             android:id="@+id/home_menu_tv"

+ 2 - 2
app/src/main/res/layout/dialog_drop_down_list.xml

@@ -3,7 +3,7 @@
 
     <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="@dimen/login_method_item_layout_height"
         android:background="@drawable/bg_text_drop_down"
         android:orientation="vertical">
 
@@ -23,6 +23,6 @@
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/drop_down_rv"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
+            android:layout_height="match_parent" />
     </LinearLayout>
 </layout>

+ 2 - 2
app/src/main/res/layout/item_home_menu.xml

@@ -8,7 +8,7 @@
         android:gravity="center_horizontal"
         android:orientation="vertical">
 
-        <FrameLayout
+        <cn.bingoogolapple.badgeview.BGABadgeFrameLayout
             android:id="@+id/home_menu_layout"
             android:layout_width="@dimen/home_menu_item_iv_layout_size"
             android:layout_height="@dimen/home_menu_item_iv_layout_size"
@@ -21,7 +21,7 @@
                 android:layout_width="@dimen/home_menu_item_iv_size"
                 android:layout_height="@dimen/home_menu_item_iv_size"
                 android:layout_gravity="center" />
-        </FrameLayout>
+        </cn.bingoogolapple.badgeview.BGABadgeFrameLayout>
 
         <TextView
             android:id="@+id/home_menu_tv"

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

@@ -445,5 +445,10 @@
     <string name="verify_failed">Verify failed</string>
     <string name="current_user_has_not_face_data">The current user does not have facial data</string>
     <string name="job_status">Job status</string>
+    <string name="confirm_cancel_exception">Are you sure to cancel the exception</string>
+    <string name="confirm_handle_exception">Are you sure to handle the exception</string>
+    <string name="handle_exception_failed">Handle exception failed</string>
+    <string name="confirm_create_lock_job">Confirm whether to create a lock job</string>
+    <string name="create_job_failed">Create job failed</string>
 
 </resources>

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

@@ -101,4 +101,5 @@
     <dimen name="slots_exception_report_dialog_content_height">340dp</dimen>
     <dimen name="avatar_size">150dp</dimen>
     <dimen name="dialog_check_face_close_size">80dp</dimen>
+    <dimen name="badge_text_size">28sp</dimen>
 </resources>

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

@@ -445,5 +445,10 @@
     <string name="verify_failed">验证失败</string>
     <string name="current_user_has_not_face_data">当前用户不存在人脸数据</string>
     <string name="job_status">作业状态</string>
+    <string name="confirm_cancel_exception">是否确认取消异常</string>
+    <string name="confirm_handle_exception">是否确认处理异常</string>
+    <string name="handle_exception_failed">处理异常失败</string>
+    <string name="confirm_create_lock_job">确认是否创建上锁作业</string>
+    <string name="create_job_failed">创建作业失败</string>
 
 </resources>

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

@@ -101,4 +101,5 @@
     <dimen name="slots_exception_report_dialog_content_height">200dp</dimen>
     <dimen name="avatar_size">80dp</dimen>
     <dimen name="dialog_check_face_close_size">60dp</dimen>
+    <dimen name="badge_text_size">18sp</dimen>
 </resources>

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

@@ -448,5 +448,10 @@
     <string name="verify_failed">验证失败</string>
     <string name="current_user_has_not_face_data">当前用户不存在人脸数据</string>
     <string name="job_status">作业状态</string>
+    <string name="confirm_cancel_exception">是否确认取消异常</string>
+    <string name="confirm_handle_exception">是否确认处理异常</string>
+    <string name="handle_exception_failed">处理异常失败</string>
+    <string name="confirm_create_lock_job">确认是否创建上锁作业</string>
+    <string name="create_job_failed">创建作业失败</string>
 
 </resources>

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

@@ -211,6 +211,7 @@ interface JobTicketDao {
         iw.workstation_id as workstationId,
         irt.rfid as rfidToken,
         irt.rfid_id as rfidId,
+        ijtp.point_status as pointStatus,
         iip.power_type as powerType
         from is_job_ticket_points ijtp
         left join  is_isolation_point iip on ijtp.point_id = iip.point_id

+ 40 - 0
data/src/main/java/com/grkj/data/dao/UserDao.kt

@@ -114,6 +114,46 @@ interface UserDao {
         size: Int
     ): List<UserManageVo>
 
+    /**
+     * 获取用户管理列表(带动态过滤 & 角色聚合)
+     */
+    @Query(
+        """
+    SELECT
+      su.user_id                         AS userId,
+      su.nick_name                       AS nickName,
+      su.user_name                       AS userName,
+              su.avatar,
+
+      -- 聚合所有卡号(如果没卡则结果里 cardCodes 会是 NULL 或空)
+      GROUP_CONCAT(DISTINCT ijc.card_code)       AS cardCodes,
+
+      GROUP_CONCAT(DISTINCT sr.role_id)           AS roleIds,
+      GROUP_CONCAT(DISTINCT sr.role_name)         AS roleNames,
+      GROUP_CONCAT(DISTINCT sr.role_key)          AS roleKeys,
+      GROUP_CONCAT(DISTINCT iw.workstation_id)    AS workstationIds,
+      GROUP_CONCAT(DISTINCT iw.workstation_name)  AS workstationNames,
+
+      su.status                            AS status
+    FROM sys_user su
+    LEFT JOIN sys_user_role sur ON su.user_id = sur.user_id
+    LEFT JOIN sys_role sr       ON sur.role_id = sr.role_id
+    LEFT JOIN is_job_card ijc   ON ijc.user_id = su.user_id
+    LEFT JOIN is_user_workstation iuw ON iuw.user_id = su.user_id
+    LEFT JOIN is_workstation iw ON iw.workstation_id = iuw.workstation_id
+    WHERE su.del_flag = 0
+    and su.user_id in (:userIds)
+
+    GROUP BY
+      su.user_id,
+      su.nick_name,
+      su.status
+  """
+    )
+    fun getUserDataByUserIds(
+        userIds: List<Long>,
+    ): List<UserManageVo>
+
     /**
      * 新增用户
      */

+ 7 - 0
data/src/main/java/com/grkj/data/enums/NextJobPrompt.kt

@@ -0,0 +1,7 @@
+package com.grkj.data.enums
+
+enum class NextJobPrompt {
+    NO_NEW_JOB,        // 不需要补单
+    CREATE_LOCK_JOB,   // 需要补单来上锁
+    CREATE_UNLOCK_JOB  // 需要补单来解锁
+}

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

@@ -14,6 +14,7 @@ class PointManageVo {
     var rfidId: Long? = 0
     var workstationId: Long? = 0
     var powerType: String? = ""
+    var pointStatus: String? = null
 
     @Ignore
     var lockId: Long = 0

+ 31 - 0
data/src/main/java/com/grkj/data/repository/IJobTicketRepository.kt

@@ -1,5 +1,6 @@
 package com.grkj.data.repository
 
+import com.grkj.data.enums.NextJobPrompt
 import com.grkj.data.model.dos.IsJobTicket
 import com.grkj.data.model.dos.IsJobTicketStep
 import com.grkj.data.model.req.LockPointUpdateReq
@@ -243,4 +244,34 @@ interface IJobTicketRepository {
      * 获取正在使用中的硬件数量
      */
     fun getUsedHardwareCount(workstationId: Long?, workflowModeId: Long?): Int
+
+    /**
+     * 下一步是上锁还是解锁
+     * true是上锁 false是解锁,null是没有
+     */
+    fun isNextLockOrUnLock(ticketId: Long): NextJobPrompt
+
+    /**
+     * 创建上锁作业
+     */
+    fun createLockJob(
+        selectedSopPoints: List<PointManageVo>,
+        selectedLockerData: List<UserManageVo>,
+        selectedColockerData: List<UserManageVo>,
+        sopId: Long?,
+        workstationId: Long,
+        jobName: String
+    ): Long
+
+    /**
+     * 创建解锁作业
+     */
+    fun createUnLockJob(
+        selectedSopPoints: List<PointManageVo>,
+        selectedLockerData: List<UserManageVo>,
+        selectedColockerData: List<UserManageVo>,
+        sopId: Long?,
+        workstationId: Long,
+        jobName: String
+    ): Long
 }

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

@@ -62,6 +62,11 @@ interface IUserRepository {
         size: Int
     ): List<UserManageVo>
 
+    /**
+     * 根据用户id获取用户数据
+     */
+    fun getUserDataByUserIds(userIds: List<Long>): List<UserManageVo>
+
     /**
      * 获取所有用户数据
      */

+ 27 - 0
data/src/main/java/com/grkj/data/repository/impl/network/NetworkJobTicketRepository.kt

@@ -1,5 +1,6 @@
 package com.grkj.data.repository.impl.network
 
+import com.grkj.data.enums.NextJobPrompt
 import com.grkj.data.model.dos.IsJobTicket
 import com.grkj.data.model.dos.IsJobTicketStep
 import com.grkj.data.model.req.LockPointUpdateReq
@@ -237,4 +238,30 @@ class NetworkJobTicketRepository  @Inject constructor() : BaseRepository(), IJob
     override fun getUsedHardwareCount(workstationId: Long?, workflowModeId: Long?): Int {
         TODO("Not yet implemented")
     }
+
+    override fun isNextLockOrUnLock(ticketId: Long): NextJobPrompt {
+        TODO("Not yet implemented")
+    }
+
+    override fun createLockJob(
+        selectedSopPoints: List<PointManageVo>,
+        selectedLockerData: List<UserManageVo>,
+        selectedColockerData: List<UserManageVo>,
+        sopId: Long?,
+        workstationId: Long,
+        jobName: String
+    ): Long {
+        TODO("Not yet implemented")
+    }
+
+    override fun createUnLockJob(
+        selectedSopPoints: List<PointManageVo>,
+        selectedLockerData: List<UserManageVo>,
+        selectedColockerData: List<UserManageVo>,
+        sopId: Long?,
+        workstationId: Long,
+        jobName: String
+    ): Long {
+        TODO("Not yet implemented")
+    }
 }

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

@@ -61,6 +61,10 @@ class NetworkUserRepository @Inject constructor()  : BaseRepository(), IUserRepo
         TODO("Not yet implemented")
     }
 
+    override fun getUserDataByUserIds(userIds: List<Long>): List<UserManageVo> {
+        TODO("Not yet implemented")
+    }
+
     override fun getAllUserDataWithWorkstation(workstationId: Long): List<UserManageVo> {
         TODO("Not yet implemented")
     }

+ 179 - 0
data/src/main/java/com/grkj/data/repository/impl/standard/JobTicketRepository.kt

@@ -8,6 +8,7 @@ import com.grkj.data.dao.JobTicketDao
 import com.grkj.data.dao.WorkflowStepDao
 import com.grkj.data.enums.IsolationPointPowerTypeEnum
 import com.grkj.data.enums.JobTicketStatusEnum
+import com.grkj.data.enums.NextJobPrompt
 import com.grkj.data.enums.RoleEnum
 import com.grkj.data.model.dos.IsJobTicket
 import com.grkj.data.model.dos.IsJobTicketKey
@@ -568,4 +569,182 @@ class JobTicketRepository @Inject constructor(
     override fun getUsedHardwareCount(workstationId: Long?, workflowModeId: Long?): Int {
         return jobTicketDao.getUsedHardwareCount(workstationId, workflowModeId)
     }
+
+    override fun isNextLockOrUnLock(ticketId: Long): NextJobPrompt {
+        // 1. 取工单与模式
+        val ticketData =
+            jobTicketDao.getTicketDataByTicketId(ticketId) ?: return NextJobPrompt.NO_NEW_JOB
+        val modeId = ticketData.modeId ?: return NextJobPrompt.NO_NEW_JOB
+
+        // 2. 取所有点位;无点位无法补单
+        val points = jobTicketDao.getTicketPointsByTicketId(ticketId)
+        if (points.isEmpty()) return NextJobPrompt.NO_NEW_JOB
+
+        // 3. 从 workflow 里找所有“上锁”和“解锁”步骤的索引
+        val stepDefs = workflowStepDao.getStepsByMode(modeId)
+        val lockIndices = stepDefs.filter { it.enableLock }.map { it.stepIndex }
+        val unlockIndices = stepDefs.filter { it.enableUnlock }.map { it.stepIndex }
+
+        // 4. 根据最大的索引值决定“最后一次”语义是上锁还是解锁
+        val lastLockIdx = lockIndices.maxOrNull()
+        val lastUnlockIdx = unlockIndices.maxOrNull()
+        val finalActionIsLock = when {
+            lastLockIdx == null && lastUnlockIdx == null -> return NextJobPrompt.NO_NEW_JOB
+            lastUnlockIdx == null -> true   // 只有上锁
+            lastLockIdx == null -> false  // 只有解锁
+            lastLockIdx > lastUnlockIdx -> true   // 上锁在后
+            else -> false  // 解锁在后
+        }
+
+        // 5. 判断点位现状:假设 pointStatus == "1" 表示已上锁
+        val allLocked = points.all { it.pointStatus == "1" }
+        val allUnlocked = points.all { it.pointStatus != "1" }
+
+        // 6. 最终提示逻辑
+        return if (finalActionIsLock) {
+            // 如果“最后要做”的是上锁,且没都上锁,就补上锁作业;全上锁了就不补
+            if (!allLocked) NextJobPrompt.CREATE_LOCK_JOB else NextJobPrompt.NO_NEW_JOB
+        } else {
+            // 如果“最后要做”的是解锁,且没都解锁,就补解锁作业;全解锁了就不补
+            if (!allUnlocked) NextJobPrompt.CREATE_UNLOCK_JOB else NextJobPrompt.NO_NEW_JOB
+        }
+    }
+
+    override fun createLockJob(
+        selectedSopPoints: List<PointManageVo>,
+        selectedLockerData: List<UserManageVo>,
+        selectedColockerData: List<UserManageVo>,
+        sopId: Long?,
+        workstationId: Long,
+        jobName: String
+    ): Long {
+        val modeId = workflowStepDao.getWorkflowModes().find { it.modeTitle == "解锁" }?.modeId!!
+        val isJobTicket = IsJobTicket()
+        isJobTicket.ticketName = jobName
+        isJobTicket.workstationId = workstationId
+        isJobTicket.modeId = modeId
+        isJobTicket.sopId = sopId
+        val ticketId = jobTicketDao.saveIsJobTicket(isJobTicket)
+        val isUnlockFirst = workflowStepDao.isUnlockBeforeLock(modeId)
+        val ticketPoints = selectedSopPoints.map {
+            val isJobTicketPoint = IsJobTicketPoints()
+            isJobTicketPoint.ticketId = ticketId
+            isJobTicketPoint.pointId = it.pointId
+            isJobTicketPoint.workstationId = workstationId
+            isJobTicketPoint.pointStatus = if (isUnlockFirst) "1" else "0"
+            isJobTicketPoint.lockId = it.lockId
+            isJobTicketPoint
+        }
+        val ticketPointIds = jobTicketDao.saveIsJobTicketPoints(ticketPoints)
+        val ticketLockerUsers = selectedLockerData.map {
+            val isJobticketUser = IsJobTicketUser()
+            isJobticketUser.userId = it.userId
+            isJobticketUser.ticketId = ticketId
+            isJobticketUser.userName = it.userName
+            isJobticketUser.userRole = RoleEnum.JTLOCKER.roleKey
+            isJobticketUser
+        }
+        val ticketColockerUsers = selectedColockerData.map {
+            val isJobticketUser = IsJobTicketUser()
+            isJobticketUser.userId = it.userId
+            isJobticketUser.ticketId = ticketId
+            isJobticketUser.userName = it.userName
+            isJobticketUser.userRole = RoleEnum.JTCOLOCKER.roleKey
+            isJobticketUser
+        }
+        jobTicketDao.saveIsJobTicketUser(ticketLockerUsers)
+        jobTicketDao.saveIsJobTicketUser(ticketColockerUsers)
+        val ticketLocks = mutableListOf<IsJobTicketLock>().apply {
+            ticketPointIds.forEach { point ->
+                val isJobTicketLock = IsJobTicketLock()
+                isJobTicketLock.ticketId = ticketId
+                isJobTicketLock.isolationPointId = point
+                add(isJobTicketLock)
+            }
+        }
+        jobTicketDao.saveIsJobTicketLock(ticketLocks)
+        val workflowStepList = workflowStepDao.getStepsByMode(modeId)
+        val ticketStep = mutableListOf<IsJobTicketStep>().apply {
+            workflowStepList.forEach { workflowStep ->
+                val isJobTicketStep = IsJobTicketStep()
+                isJobTicketStep.ticketId = ticketId
+                isJobTicketStep.stepIndex = workflowStep.stepIndex
+                isJobTicketStep.stepContent = workflowStep.stepTitle
+                isJobTicketStep.androidStepContent = workflowStep.stepTitleShort
+                isJobTicketStep.workflowStepId = workflowStep.stepId
+                add(isJobTicketStep)
+            }
+        }
+        jobTicketDao.saveIsJobTicketStep(ticketStep)
+        return ticketId
+    }
+
+    override fun createUnLockJob(
+        selectedSopPoints: List<PointManageVo>,
+        selectedLockerData: List<UserManageVo>,
+        selectedColockerData: List<UserManageVo>,
+        sopId: Long?,
+        workstationId: Long,
+        jobName: String
+    ): Long {
+        val modeId = workflowStepDao.getWorkflowModes().find { it.modeTitle == "解锁" }?.modeId!!
+        val isJobTicket = IsJobTicket()
+        isJobTicket.ticketName = jobName
+        isJobTicket.workstationId = workstationId
+        isJobTicket.modeId = modeId
+        isJobTicket.sopId = sopId
+        val ticketId = jobTicketDao.saveIsJobTicket(isJobTicket)
+        val isUnlockFirst = workflowStepDao.isUnlockBeforeLock(modeId)
+        val ticketPoints = selectedSopPoints.map {
+            val isJobTicketPoint = IsJobTicketPoints()
+            isJobTicketPoint.ticketId = ticketId
+            isJobTicketPoint.pointId = it.pointId
+            isJobTicketPoint.workstationId = workstationId
+            isJobTicketPoint.pointStatus = if (isUnlockFirst) "1" else "0"
+            isJobTicketPoint.lockId = it.lockId
+            isJobTicketPoint
+        }
+        val ticketPointIds = jobTicketDao.saveIsJobTicketPoints(ticketPoints)
+        val ticketLockerUsers = selectedLockerData.map {
+            val isJobticketUser = IsJobTicketUser()
+            isJobticketUser.userId = it.userId
+            isJobticketUser.ticketId = ticketId
+            isJobticketUser.userName = it.userName
+            isJobticketUser.userRole = RoleEnum.JTLOCKER.roleKey
+            isJobticketUser
+        }
+        val ticketColockerUsers = selectedColockerData.map {
+            val isJobticketUser = IsJobTicketUser()
+            isJobticketUser.userId = it.userId
+            isJobticketUser.ticketId = ticketId
+            isJobticketUser.userName = it.userName
+            isJobticketUser.userRole = RoleEnum.JTCOLOCKER.roleKey
+            isJobticketUser
+        }
+        jobTicketDao.saveIsJobTicketUser(ticketLockerUsers)
+        jobTicketDao.saveIsJobTicketUser(ticketColockerUsers)
+        val ticketLocks = mutableListOf<IsJobTicketLock>().apply {
+            ticketPointIds.forEach { point ->
+                val isJobTicketLock = IsJobTicketLock()
+                isJobTicketLock.ticketId = ticketId
+                isJobTicketLock.isolationPointId = point
+                add(isJobTicketLock)
+            }
+        }
+        jobTicketDao.saveIsJobTicketLock(ticketLocks)
+        val workflowStepList = workflowStepDao.getStepsByMode(modeId)
+        val ticketStep = mutableListOf<IsJobTicketStep>().apply {
+            workflowStepList.forEach { workflowStep ->
+                val isJobTicketStep = IsJobTicketStep()
+                isJobTicketStep.ticketId = ticketId
+                isJobTicketStep.stepIndex = workflowStep.stepIndex
+                isJobTicketStep.stepContent = workflowStep.stepTitle
+                isJobTicketStep.androidStepContent = workflowStep.stepTitleShort
+                isJobTicketStep.workflowStepId = workflowStep.stepId
+                add(isJobTicketStep)
+            }
+        }
+        jobTicketDao.saveIsJobTicketStep(ticketStep)
+        return ticketId
+    }
 }

+ 10 - 4
data/src/main/java/com/grkj/data/repository/impl/standard/SysMenuRepository.kt

@@ -64,7 +64,9 @@ class SysMenuRepository @Inject constructor(val sysMenuDao: SysMenuDao, val role
                         val roleMenuData = mutableListOf<SysRoleMenu>().apply {
                             for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
                                 RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE,
-                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE
+                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE,
+                                RoleFunctionalPermissionsEnum.EXCEPTION_JOB,
+                                RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE,
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()
@@ -81,7 +83,9 @@ class SysMenuRepository @Inject constructor(val sysMenuDao: SysMenuDao, val role
                         val roleMenuData = mutableListOf<SysRoleMenu>().apply {
                             for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
                                 RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE,
-                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE
+                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE,
+                                RoleFunctionalPermissionsEnum.EXCEPTION_JOB,
+                                RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE,
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()
@@ -98,7 +102,9 @@ class SysMenuRepository @Inject constructor(val sysMenuDao: SysMenuDao, val role
                         val roleMenuData = mutableListOf<SysRoleMenu>().apply {
                             for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
                                 RoleFunctionalPermissionsEnum.DATA_HOME_MANAGE,
-                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE
+                                RoleFunctionalPermissionsEnum.HARDWARE_HOME_MANAGE,
+                                RoleFunctionalPermissionsEnum.EXCEPTION_JOB,
+                                RoleFunctionalPermissionsEnum.EXCEPTION_MANAGE,
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()
@@ -115,7 +121,7 @@ class SysMenuRepository @Inject constructor(val sysMenuDao: SysMenuDao, val role
                         val roleMenuData = mutableListOf<SysRoleMenu>().apply {
                             for (permissionsEnum in RoleFunctionalPermissionsEnum.except(
                                 RoleFunctionalPermissionsEnum.JOB_TICKET_HOME_MANAGE,
-                                RoleFunctionalPermissionsEnum.EXCEPTION_HOME_MANAGE
+                                RoleFunctionalPermissionsEnum.EXCEPTION_HOME_MANAGE,
                             )) {
                                 sysMenuData.find { it.perms == permissionsEnum.functionalPermission }?.menuId?.let { menuId ->
                                     val sysRoleMenu = SysRoleMenu()

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

@@ -258,6 +258,10 @@ class UserRepository @Inject constructor(
         )
     }
 
+    override fun getUserDataByUserIds(userIds: List<Long>): List<UserManageVo> {
+        return userDao.getUserDataByUserIds(userIds)
+    }
+
     override fun getAllUserDataWithWorkstation(workstationId: Long): List<UserManageVo> {
         return userDao.getAllUserDataWithWorkstationId(workstationId)
     }

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

@@ -66,6 +66,8 @@ dependencies {
     api("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
     api("io.github.scwang90:refresh-footer-classics:3.0.0-alpha")
     api("com.google.android.flexbox:flexbox:3.0.0")
+    api("com.github.bingoogolapple.BGABadgeView-Android:api:1.2.0")
+    kapt("com.github.bingoogolapple.BGABadgeView-Android:compiler:1.2.0")
 //    api("com.licheedev:android-serialport:2.1.5")
     implementation(project(":data"))
     implementation(project(":shared"))

+ 0 - 101
ui-base/src/main/java/com/grkj/ui_base/ext/ViewBadgeExtensions.kt

@@ -1,101 +0,0 @@
-package com.grkj.ui_base.ext
-
-import android.view.View
-import android.view.ViewGroup
-import androidx.annotation.OptIn
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import com.google.android.material.badge.BadgeDrawable
-import com.google.android.material.badge.BadgeUtils
-import com.google.android.material.badge.ExperimentalBadgeUtils
-
-private const val TAG_BADGE_WRAPPER = -1001
-
-private class BadgeWrapper(
-    val badge: BadgeDrawable,
-    val attachListener: View.OnAttachStateChangeListener,
-    val lifecycleObserver: DefaultLifecycleObserver?
-)
-
-/**
- * 在任意 View 上显示 Badge(数字或小红点),并自动在 View 回收或 LifecycleOwner 销毁时移除。
- *
- * @param number       数字,传 0 则显示小红点;传负数则隐藏。
- * @param background   可选:角标背景色资源 ID,如 R.color.red。
- * @param textColor    可选:数字颜色资源 ID,如 R.color.white。
- * @param parent       可选:用于附着角标的父容器,默认取 view.parent(必须是 ViewGroup)。
- * @param lifecycleOwner 可选:若传入,会在其 onDestroy 时自动移除 Badge;
- *                      若不传且 view.context 是 LifecycleOwner,会自动绑定。
- * @return BadgeDrawable 你可以用它来后续隐藏或更新数字。
- */
-@OptIn(ExperimentalBadgeUtils::class)
-fun View.showBadge(
-    number: Int = 0,
-    background: Int? = null,
-    textColor: Int? = null,
-    parent: ViewGroup? = null,
-    lifecycleOwner: LifecycleOwner? = null
-): BadgeDrawable {
-    // 如果已有 badge, 先移除
-    removeBadge()
-
-    // 准备 BadgeDrawable
-    val badge = BadgeDrawable.create(context).apply {
-        isVisible = number >= 0
-        if (number > 0) this.number = number
-        background?.let { this.backgroundColor = ContextCompat.getColor(context, it) }
-        textColor?.let { this.badgeTextColor = ContextCompat.getColor(context, it) }
-    }
-
-    // 附加到父容器
-    val anchorParent = parent ?: (this.parent as? ViewGroup
-        ?: throw IllegalStateException("View(${this.id}) 的 parent 不是 ViewGroup,无法附加 Badge"))
-    BadgeUtils.attachBadgeDrawable(badge, this)
-
-    // 监听 View 分离,自动移除 badge
-    val attachListener = object : View.OnAttachStateChangeListener {
-        override fun onViewAttachedToWindow(v: View) {}
-        override fun onViewDetachedFromWindow(v: View) {
-            removeBadge()
-        }
-    }
-    addOnAttachStateChangeListener(attachListener)
-
-    // 监听 LifecycleOwner 销毁
-    val owner = lifecycleOwner ?: (context as? LifecycleOwner)
-    val lifecycleObserver = owner?.let { lcOwner ->
-        val obs = object : DefaultLifecycleObserver {
-            override fun onDestroy(owner: LifecycleOwner) {
-                removeBadge()
-                owner.lifecycle.removeObserver(this)
-            }
-        }
-        lcOwner.lifecycle.addObserver(obs)
-        obs
-    }
-
-    // 存储 wrapper
-    setTag(TAG_BADGE_WRAPPER, BadgeWrapper(badge, attachListener, lifecycleObserver))
-    return badge
-}
-
-/**
- * 从 View 上移除 Badge,并取消自动管理。
- */
-@OptIn(ExperimentalBadgeUtils::class)
-fun View.removeBadge() {
-    val wrapper = getTag(TAG_BADGE_WRAPPER) as? BadgeWrapper ?: return
-    // detach badge
-    val anchorParent = parent as? ViewGroup
-    anchorParent?.let { BadgeUtils.detachBadgeDrawable(wrapper.badge, this) }
-    // remove listener
-    removeOnAttachStateChangeListener(wrapper.attachListener)
-    // remove lifecycle observer
-    wrapper.lifecycleObserver?.let { obs ->
-        (context as? LifecycleOwner)?.lifecycle?.removeObserver(obs)
-    }
-    // remove tag
-    setTag(TAG_BADGE_WRAPPER, null)
-}

+ 9 - 0
ui-base/src/main/java/com/grkj/ui_base/widget/BGABadgeInit.java

@@ -0,0 +1,9 @@
+package com.grkj.ui_base.widget;
+
+import android.widget.FrameLayout;
+
+import cn.bingoogolapple.badgeview.annotation.BGABadge;
+
+@BGABadge({FrameLayout.class})
+public class BGABadgeInit {
+}

+ 34 - 95
ui-base/src/main/java/com/grkj/ui_base/widget/CustomNavBar.kt

@@ -2,7 +2,13 @@ package com.grkj.ui_base.widget
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.*
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.RectF
 import android.graphics.drawable.BitmapDrawable
 import android.util.AttributeSet
 import android.util.TypedValue
@@ -16,11 +22,9 @@ import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.annotation.MenuRes
 import androidx.appcompat.view.menu.MenuBuilder
+import androidx.core.view.size
 import androidx.palette.graphics.Palette
 import com.grkj.ui_base.R
-import android.view.LayoutInflater
-import androidx.core.view.get
-import androidx.core.view.size
 import kotlin.math.tan
 
 /**
@@ -49,13 +53,10 @@ class CustomNavBar @JvmOverloads constructor(
     private var textSizePx: Float = dp(12)
 
     private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
-    private val clearPaint =
-        Paint(Paint.ANTI_ALIAS_FLAG).apply {
-            xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
-            style = Paint.Style.FILL
-        }
-    private val ballPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
-    private val notchPath = Path()
+    private val selectedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+        style = Paint.Style.FILL
+        color = context.getColor(R.color.common_bg_white_70)
+    }
 
     private var length = 0
     private var selectedIdx = 0
@@ -76,7 +77,6 @@ class CustomNavBar @JvmOverloads constructor(
         }
         orientation = if (navOrientation == 0) HORIZONTAL else VERTICAL
         bgPaint.color = backgroundColor
-        ballPaint.color = backgroundColor
     }
 
     fun setOnItemSelectedListener(listener: (MenuItem) -> Boolean) {
@@ -146,96 +146,35 @@ class CustomNavBar @JvmOverloads constructor(
             cornerRadius,
             bgPaint
         )
-        super.onDraw(canvas)
-        // 凹槽
         if (length > 0) {
             val child = getChildAt(selectedIdx)
-            val r = (height - child.height) / 2f
-            if (navOrientation == 0) {
-                val cx = (child.left + child.right) / 2f
-                val startX = child.left.toFloat() + (child.right - child.left - iconSize) / 4
-                val endX = child.right.toFloat() - (child.right - child.left - iconSize) / 4
-                // 计算贝塞尔控制点比例 k = 4/3 * tan(pi/8)
-                val k = (4f / 3f) * tan(Math.PI / 8).toFloat()
-                notchPath.rewind()
-                notchPath.apply {
-                    moveTo(startX, 0f)
-                    // 左侧凸起
-                    cubicTo(
-                        startX + r * k, -r * (1 - k),
-                        startX + r, -r,
-                        startX + r, 0f
-                    )
-                    // 中段凹陷
-                    cubicTo(
-                        cx - r, r,
-                        cx + r, r,
-                        endX - r, 0f
-                    )
-                    // 右侧凸起
-                    cubicTo(
-                        endX - r, -r,
-                        endX - r * k, -r * (1 - k),
-                        endX, 0f
-                    )
-                }
-            } else {
-                // —— 垂直版,右侧挖槽 ——
-                val cy = (child.top + child.bottom) / 2f
-                val startY = child.top + (child.bottom - child.top - iconSize) / 4f
-                val endY = child.bottom - (child.bottom - child.top - iconSize) / 4f
-                val k = (4f / 3f) * tan(Math.PI / 8).toFloat()
-                val r = (width - (0..menuBuilder.size - 1).map { getChildAt(it) }
-                    .maxOf { it.width }) / 2f
-
-                notchPath.rewind()
-                notchPath.apply {
-                    // 在右侧边界,从 startY 开始
-                    moveTo(width.toFloat(), startY)
-
-                    // 上方凸起 (quarter circle outward to the right)
-                    cubicTo(
-                        width + r * (1 - k), startY + r * k,
-                        width + r, startY + r,
-                        width.toFloat(), startY + r
-                    )
-
-                    // 中段凹陷 (half circle inward to the left)
-                    cubicTo(
-                        width - r, cy - r,
-                        width - r, cy + r,
-                        width.toFloat(), endY - r
-                    )
-
-                    // 下方凸起 (quarter circle outward to the right)
-                    cubicTo(
-                        width + r, endY - r,
-                        width + r * (1 - k), endY - r * k,
-                        width.toFloat(), endY
-                    )
-                }
-            }
-            canvas.drawPath(notchPath, clearPaint)
-        }
-        // 小球
-        if (length > 0) {
-            val child = getChildAt(selectedIdx);
-            val cx = (child.left + child.right) / 2f
-            val cy = (child.top + child.bottom) / 2f
             if (navOrientation == 0) {
-                val r = (height - child.height) / 2f * 0.5f
-                canvas.drawCircle(
-                    cx,
-                    r / 2,
-                    r / 2,
-                    ballPaint
+                canvas.drawRoundRect(
+                    RectF(
+                        child.left.toFloat(),
+                        0f,
+                        child.right.toFloat(),
+                        height.toFloat()
+                    ),
+                    cornerRadius,
+                    cornerRadius,
+                    selectedPaint
                 )
             } else {
-                val r = (width - (0..menuBuilder.size - 1).map { getChildAt(it) }
-                    .maxOf { it.width }) / 2f * 0.5f
-                canvas.drawCircle(width - r / 2, cy, r / 2, ballPaint)
+                canvas.drawRoundRect(
+                    RectF(
+                        0f,
+                        child.top.toFloat(),
+                        width.toFloat(),
+                        child.bottom.toFloat(),
+                    ),
+                    cornerRadius,
+                    cornerRadius,
+                    selectedPaint
+                )
             }
         }
+        super.onDraw(canvas)
         canvas.restoreToCount(save)
     }