添加了界面及逻辑

This commit is contained in:
zxj
2025-07-22 15:48:12 +08:00
parent 9d14b5499b
commit 5ad82eb224
40 changed files with 2157 additions and 182 deletions
+6 -4
View File
@@ -25,10 +25,6 @@
android:exported="true"></activity>
<activity
android:name=".activity.LoginByPwdActivity"
android:exported="false"
android:launchMode="singleTask" />
<activity
android:name=".activity.LoginByFaceActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
@@ -37,6 +33,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.LoginByFaceActivity"
android:exported="true"
android:launchMode="singleTask">
</activity>
<activity
android:name=".activity.MainActivity"
android:exported="true">
@@ -1,7 +1,25 @@
package com.sw.platecabinet
object GlobalData {
/**
* 同一设备全局使用的设备编号
*/
var globalEquipmentCode: String = "202501171634"
/**
* 横排数量
*/
var arrayCross: Int = 2
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式
*/
var arrayMode: Int = 0
}
/**
@@ -2,12 +2,16 @@ package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.R
@@ -19,7 +23,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* activity 基类
@@ -30,54 +33,123 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var context: Context
private var timeJob: Job? = null
private var mDialogWaiting: CustomDialog? = null
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
context = this
disableSystemUICompletely()
// 确保内容延伸到导航栏区域
WindowCompat.setDecorFitsSystemWindows(window, false)
binding = inflateViewBinding()
headerBinding = inflateTitleBinding()
setContentView(binding.root)
updateTime()
registerDataChange()
initialize()
}
fun updateTime() {
headerBinding?.let {
headerBinding!!.tvRightTime.setClickListeners(
it.tvLeftDate.setClickListeners(
onDoubleClick = {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
onLeftDoubleClick()
}
)
it.tvRightTime.setClickListeners(
onDoubleClick = {
onRightDoubleClick()
}
)
val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow()
.collect { (date, time) ->
headerBinding!!.tvLeftDate.text = date
headerBinding!!.tvRightTime.text = time
it.tvLeftDate.text = date
it.tvRightTime.text = time
}
}
}
}
open fun onLeftDoubleClick() {
}
open fun onRightDoubleClick() {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// 统一处理权限拒绝情况
val helper = permissionHelpers[requestCode]
val allGranted = PermissionHelper.handlePermissionResult(
this,
requestCode,
permissions,
grantResults
) { permanentlyDeniedPermissions ->
// 权限被永久拒绝的统一处理
Timber.e("用户永久拒绝权限 $permanentlyDeniedPermissions")
grantResults,
permissionHelper = helper
)
if (allGranted) {
permissionHelpers.remove(requestCode)
}
}
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
override fun onResume() {
super.onResume()
// 关键点2:防止某些场景下系统栏恢复
enforceImmersiveMode()
}
private fun disableSystemUICompletely() {
// 禁用系统手势(Android 10+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
// 全屏+隐藏导航栏(所有版本通用)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
// 禁止窗口扩展至系统栏区域(彻底锁定)
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
private fun enforceImmersiveMode() {
// 持续强制隐藏系统栏(防止手势触发)
window.decorView.postDelayed({
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}, 100) // 延迟100ms确保覆盖手势触发
}
/**
* 显示等待提示框
*/
@@ -105,6 +177,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected abstract fun initialize()
protected open fun registerDataChange() {}
override fun onDestroy() {
timeJob?.cancel()
super.onDestroy()
@@ -2,30 +2,53 @@ package com.sw.platecabinet.activity
import android.Manifest
import android.content.Intent
import android.graphics.Outline
import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewOutlineProvider
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 人脸识别
*/
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener {
private val viewModel by viewModels<UserViewModel>()
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater)
@@ -38,7 +61,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
override fun initialize() {
// 请求权限
checkCameraPermission()
initView()
initArcViewModel()
initArcView()
openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true)
viewModel.generateToken()
binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java)
@@ -46,53 +72,314 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
}
}
private fun checkCameraPermission() {
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
// 权限已授予
startCamera()
override fun registerDataChange() {
lifecycleScope.launch {
viewModel.currentUserInfo.collect {
Timber.d("currentUserInfo it = $it")
if (it != null) {
ToastUtils.showToast("登录成功")
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
finish()
}
}
.checkAndRequest()
}
private fun initView() {
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
outline.setOval(0, 0, view.width, view.height)
}
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
binding.previewView.clipToOutline = true
}
private fun startCamera() {
Timber.d("startCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 创建预览用例
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
private fun checkCameraPermission() {
val permissionHelper =
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
// 权限已授予
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
}
.build()
registerPermissionHelper(permissionHelper)
permissionHelper.checkAndRequest()
}
// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
try {
// 解绑所有用例
cameraProvider.unbindAll()
private fun initArcViewModel() {
// 绑定到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
)
} catch (exc: Exception) {
Timber.e(exc, "Use case binding failed")
Timber.e("ftInitCode observe = $error")
ToastUtils.showToast(error)
}
}, ContextCompat.getMainExecutor(this))
})
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(this, Observer { userId: String? ->
Timber.i("recognizeUserId observe userId = $userId")
viewModel.getUserInfoById(memberId = userId?.toInt() ?: 0)
})
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
override fun onDestroy() {
if (rgbCameraHelper != null) {
rgbCameraHelper!!.release()
rgbCameraHelper = null
}
recognizeViewModel.destroy()
super.onDestroy()
}
/**
* 调整View的宽高,使2个预览同时显示
*
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
val layoutParams = previewView.layoutParams
val measuredWidth = previewView.measuredWidth
val measuredHeight = previewView.measuredHeight
var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
if (ratio > 1) {
ratio = 1 / ratio
}
if (displayOrientation % 180 == 0) {
layoutParams.width = measuredWidth
layoutParams.height = (measuredWidth * ratio).toInt()
} else {
layoutParams.height = measuredHeight
layoutParams.width = (measuredHeight * ratio).toInt()
}
if (scale < 1f) {
val rgbParam = rgbPreview.getLayoutParams()
layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt()
} else {
layoutParams.width = (layoutParams.width * scale).toInt()
layoutParams.height = (layoutParams.height * scale).toInt()
}
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
if (layoutParams.width >= metrics.widthPixels) {
val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
if (layoutParams.height >= metrics.heightPixels) {
val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 1.0f
)
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
layoutParams.width,
layoutParams.height,
displayOrientation,
cameraId,
isMirror,
ConfigUtil.isDrawRgbRectHorizontalMirror(context),
ConfigUtil.isDrawRgbRectVerticalMirror(context)
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
if (!isRecognition) {
return
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Timber.i("onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.i("onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.i("onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.i("onCameraConfigurationChanged: $cameraID $displayOrientation")
}
}
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewRgb.measuredWidth,
binding.dualCameraTexturePreviewRgb.measuredHeight
)
)
.rotation(windowManager.defaultDisplay.rotation)
.additionalRotation(90) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper!!.init()
rgbCameraHelper!!.start()
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
}
fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
}
override fun onGlobalLayout() {
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
// countDownTimer.start();
}
// @Override
// protected void onResume() {
// super.onResume();
// resumeCamera();
// }
private fun resumeCamera() {
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
}
}
protected override fun onPause() {
pauseCamera()
super.onPause()
}
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
}
@@ -1,10 +1,18 @@
package com.sw.platecabinet.activity
import android.content.Intent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
private val viewModel by viewModels<UserViewModel>()
override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater)
}
@@ -15,7 +23,13 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun initialize() {
binding.btnLogin.setOnClickListener {
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
val phone = binding.etPhone.text.toString()
val pwd = binding.etPwd.text.toString()
if (phone.isEmpty() || pwd.isEmpty()) {
ToastUtils.showToast("手机或校验码不能为空")
return@setOnClickListener
}
viewModel.loginWithPwd(phone = phone, password = pwd)
}
binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java)
@@ -23,4 +37,26 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
}
}
override fun registerDataChange() {
lifecycleScope.launch {
viewModel.currentUserInfo.collect {
Timber.d("currentUserInfo it = $it")
if (it != null) {
ToastUtils.showToast("登录成功")
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
finish()
}
}
}
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
}
@@ -15,6 +15,7 @@ import com.sw.platecabinet.utils.FragmentHelper
class MainActivity : BaseActivity<ActivityMainBinding>() {
private lateinit var fragmentHelper: FragmentHelper
private var pageType = PageType.SETTING_LIST
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
@@ -43,7 +44,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
val pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) {
PageType.SETTING_LIST -> SettingListFragment()
@@ -54,6 +55,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
fragmentHelper.addFragment(page)
}
override fun onLeftDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
finish()
}
}
override fun onRightDoubleClick() {
}
}
/**
@@ -40,17 +40,16 @@ class GenericPageAdapter<T, VB : ViewBinding>(
fun bind(items: List<T>) {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
// 添加间距装饰(12dp)
childRecyclerView.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2, // 2列
spacing = itemView.context.dpToPx(12), // 12dp
spacing = itemView.context.dpToPx(6), // 12dp
includeEdge = true // 包含边缘间距
)
)
childRecyclerView.adapter =
GenericChildAdapter(items, itemBindingInflater, itemBindCallback)
GenericItemAdapter(items, itemBindingInflater, itemBindCallback)
}
}
}
@@ -58,11 +57,11 @@ class GenericPageAdapter<T, VB : ViewBinding>(
/**
* 通用子项适配器(支持ViewBinding
*/
class GenericChildAdapter<T, VB : ViewBinding>(
private val items: List<T>,
class GenericItemAdapter<T, VB : ViewBinding>(
private var items: List<T>,
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val bindCallback: VB.(item: T, position: Int) -> Unit,
) : RecyclerView.Adapter<GenericChildAdapter<T, VB>.ViewHolder>() {
) : RecyclerView.Adapter<GenericItemAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T, position: Int) {
@@ -70,6 +69,11 @@ class GenericChildAdapter<T, VB : ViewBinding>(
}
}
fun updateData(newItem: List<T>) {
this.items = newItem
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = bindingInflater(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
@@ -77,7 +81,6 @@ class GenericChildAdapter<T, VB : ViewBinding>(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
@@ -0,0 +1,37 @@
package com.sw.platecabinet.ext
/**
* 姓名脱敏处理
* 2个字:张*
* 多于2个字:张*某
*/
fun String?.maskName(): String {
if (this == null || this.isEmpty()) return ""
return when {
length == 2 -> "${this[0]}*"
length > 2 -> "${this[0]}*${this[length - 1]}"
else -> this // 1个字的情况原样返回
}
}
/**
* 手机号脱敏处理
* 将第4-7位替换为*
* 例如:138****1234
*/
fun String?.maskPhone(): String {
if (this == null || this.isEmpty()) return ""
if (this.length < 11) {
return this
}
val start = 3 // 第4位(索引从0开始)
val end = 6 // 第7位
val sb = StringBuilder(this)
for (i in start..end) {
sb.setCharAt(i, '*')
}
return sb.toString()
}
@@ -1,19 +1,23 @@
package com.sw.platecabinet.fragment
import android.app.Dialog
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
import com.sw.platecabinet.view.CustomDialog
abstract class BaseFragment<VB : ViewBinding>(
private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB
) : Fragment() {
private var _binding: VB? = null
protected val binding get() = _binding!!
private var mDialogWaiting: CustomDialog? = null
override fun onCreateView(
inflater: LayoutInflater,
@@ -22,16 +26,37 @@ abstract class BaseFragment<VB : ViewBinding>(
): View? {
_binding = bindingInflater(inflater, container, false)
initialize()
registerDataChange()
return binding.root
}
/**
* 注册数据变化监听
*/
open fun registerDataChange() {}
abstract fun initialize()
class ViewModelFactory<T : ViewModel>(private val creator: () -> T) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return creator() as T
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(requireContext(), R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomDialog(requireContext(), view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
override fun onDestroyView() {
@@ -1,23 +1,42 @@
package com.sw.platecabinet.fragment
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.R
import com.sw.platecabinet.adapter.GenericChildAdapter
import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.databinding.FragmentBindPlateBinding
import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 餐盘柜绑定
*/
class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
FragmentBindPlateBinding::inflate
) {
private lateinit var adapter: GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding>
private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
private val viewModel by viewModels<SettingViewModel>()
internal val ARG_PARAM1 = "param1"
private var info: EquipmentUserInfo? = null
private var checkedItem: SearchResult.Member? = null
private var lastText = ""
companion object {
@JvmStatic
@@ -32,25 +51,147 @@ class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
override fun initialize() {
arguments?.let {
info = it.getParcelable(ARG_PARAM1)
info?.let { binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}" }
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
}
}
initView()
initListener()
}
private fun initView() {
adapter = createAdapter()
registerDateChange()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(6),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItems = layoutManager.itemCount
if (!viewModel.isLoading && viewModel.canLoadMore
&& lastVisibleItem >= totalItems - 3
) {
viewModel.getSearchMemberList(
param = lastText,
pageNum = viewModel.currentPage
)
}
}
})
binding.etUserInfo.addTextChangedListener(object : TextWatcher {
private val handler = Handler(Looper.getMainLooper())
private val debounceDelay = 500L // 延迟 500 毫秒
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
override fun afterTextChanged(s: Editable?) {
val currentText = s.toString()
if (currentText == lastText) return // 内容未变化时不处理
lastText = currentText
handler.removeCallbacksAndMessages(null) // 取消之前的延迟任务
handler.postDelayed({
// if (currentText.isNotEmpty()) {
viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// }
}, debounceDelay)
}
})
}
private fun initListener() {
binding.llBind.setOnClickListener {
if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
ToastUtils.showToast("传入的设备信息异常")
return@setOnClickListener
}
val plateNumber = binding.etCode.text.toString()
if (plateNumber.isEmpty()) {
ToastUtils.showToast("请输入餐盘号")
return@setOnClickListener
}
if (checkedItem == null) {
ToastUtils.showToast("请选择要绑定的会员")
return@setOnClickListener
}
if (checkedItem!!.id == null) {
ToastUtils.showToast("选中的会员信息异常")
return@setOnClickListener
}
viewModel.bindPlate(
equipmentCode = info!!.equipmentCode!!,
equipmentBoxCode = info!!.equipmentBoxCode!!,
memberId = checkedItem!!.id!!,
plateNumber = plateNumber
)
}
binding.tvBack.setOnClickListener { activity?.finish() }
}
private fun registerDateChange() {
/**
* 监听数据变化
*/
override fun registerDataChange() {
lifecycleScope.launch {
viewModel.searchMemberList.collect {
adapter.updateData(it)
}
}
lifecycleScope.launch {
viewModel.bindStateChange.collect {
if (it.first == null) return@collect // 解绑状态过滤
val errorInfo = it.second
if (errorInfo.isSuccess()) {
ToastUtils.showToast("绑定成功")
activity?.finish()
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
private fun createAdapter(): GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding> {
return GenericChildAdapter(
private fun createAdapter(): GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchUserInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.name
this.tvPhone.text = item.phone
if (item.isBound()) {
this.tvName.text = item.name.maskName()
this.tvPhone.text = item.phone.maskPhone()
if (item.id != checkedItem?.id) {
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
@@ -58,6 +199,8 @@ class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.name}, position = $position")
checkedItem = item
adapter.notifyDataSetChanged()
}
}
)
@@ -2,6 +2,7 @@ package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
import timber.log.Timber
/**
* 餐盘柜已满
@@ -11,6 +12,7 @@ class PlateCabinetFullFragment :
private var totalTimeInMillis: Long = 5 * 1000
override fun initialize() {
Timber.d("initialize")
initCountTime()
}
@@ -5,14 +5,18 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GenericPageAdapter
import com.sw.platecabinet.databinding.FragmentSettingListBinding
import com.sw.platecabinet.databinding.ItemBindViewBinding
import com.sw.platecabinet.ext.formatNumber
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch
@@ -25,31 +29,41 @@ import kotlin.math.ceil
class SettingListFragment :
BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
private val viewModel: SettingViewModel by viewModels<SettingViewModel>()
private lateinit var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>
private lateinit var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>
private val itemsPerPage = 22 // 每行2个,每列11个,共22个
private fun registerDateChange() {
override fun registerDataChange() {
lifecycleScope.launch {
// 使用 repeatOnLifecycle 确保只在特定生命周期状态收集
// repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.equipmentList.collect { it ->
Timber.d("registerDateChange updateAdapter it = $it")
updateAdapter(it)
}
// }
}
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
override fun initialize() {
binding.mainRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
pageAdapter = createAdapter()
adapter = pageAdapter
}
registerDateChange()
viewModel.getEquipmentList(EquipmentParam(equipmentCode = "202501171634"))
}
override fun onResume() {
super.onResume()
viewModel.getEquipmentList()
}
private fun createAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
@@ -76,7 +90,7 @@ class SettingListFragment :
this.llBindInfo.visibility = View.VISIBLE
this.tvUnbind.visibility = View.GONE
this.tvName.text = item.name
this.tvName.text = item.name.maskName()
this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
@@ -103,15 +117,29 @@ class SettingListFragment :
}
}
this.llOpen.setOnClickListener {
Timber.d("itemClick llOpen ${item.equipmentCode}")
MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN)
Timber.d("itemClick llOpen equipmentBoxCode = ${item.equipmentBoxCode}")
val equipmentBoxCode: Int = item.equipmentBoxCode?.toInt() ?: -1
if (equipmentBoxCode < 0) {
ToastUtils.showToast("餐盘柜编号错误")
return@setOnClickListener
}
SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback {
override fun onSuccess() {
MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN)
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("柜门打开失败")
}
})
}
}
)
}
fun updateAdapter(items: List<EquipmentUserInfo>) {
val pages = if (items.size > itemsPerPage) {
// if (GlobalData.arrayCross > 2) {
var pages = if (items.size > itemsPerPage) {
items.chunked(itemsPerPage) {
convertToColumnFirst(it, 11)
}
@@ -123,6 +151,9 @@ class SettingListFragment :
)
}
pageAdapter.updatePages(pages)
// }else{
//
// }
}
/**
@@ -1,9 +1,16 @@
package com.sw.platecabinet.fragment
import android.os.Bundle
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch
/**
* 餐盘柜绑定
@@ -12,8 +19,8 @@ class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPla
FragmentUnbindPlateBinding::inflate
) {
internal val ARG_PARAM1 = "param1"
private var param1: EquipmentUserInfo? = null
private var info: EquipmentUserInfo? = null
private val viewModel by viewModels<SettingViewModel>()
companion object {
@JvmStatic
@@ -31,15 +38,46 @@ class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPla
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
binding.tvPlateNumberValue.text = it.plateNumber
binding.tvNameValue.text = it.name
binding.tvPhoneValue.text = it.phone
binding.tvNameValue.text = it.name.maskName()
binding.tvPhoneValue.text = it.phone.maskPhone()
val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
binding.tvUpdateTimeValue.text = timeAgo
}
}
binding.llUnbind.setOnClickListener {
if (info == null) return@setOnClickListener
viewModel.unbindPlate(
equipmentBoxCode = info!!.equipmentBoxCode!!,
equipmentCode = info!!.equipmentCode!!
)
}
binding.tvBack.setOnClickListener {
activity?.finish()
}
}
override fun registerDataChange() {
lifecycleScope.launch {
viewModel.bindStateChange.collect {
if (it.first == null) return@collect // 绑定状态过滤
val errorInfo = it.second
if (errorInfo.isSuccess()) {
ToastUtils.showToast("解绑成功")
activity?.finish()
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
}
@@ -0,0 +1,10 @@
package com.sw.platecabinet.model
/**
* 错误信息 code == 200 成功
*/
data class ErrorInfo(val code: Int = 200, val msg: String = "") {
fun isSuccess(): Boolean {
return code == 200
}
}
@@ -26,10 +26,10 @@ data class BindParam(
* 会员Id
*/
@SerializedName("memberId")
val memberId: Int? = 0,
val memberId: Int? = null,
/**
*餐盘编号
*/
@SerializedName("plateNumber")
val plateNumber: String? = ""
val plateNumber: String? = null
) : Parcelable
@@ -19,15 +19,15 @@ data class LoginParam(
* 会员信息
*/
@SerializedName("memberId")
val memberId: String? = "",
val memberId: Int? = null,
/**
* 密码
*/
@SerializedName("password")
val password: String? = "",
val password: String? = null,
/**
* 手机号
*/
@SerializedName("phone")
val phone: String? = ""
val phone: String? = null
) : Parcelable
@@ -11,7 +11,7 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "https://vip.shuziweidao.com/shuwei-zhct/"
private const val BASE_URL = "https://vip.shuziweidao.com"
// private const val BASE_URL = "http://192.168.1.8:9092"
private const val TIME_OUT = 30L // 超时时间(秒)
@@ -18,37 +18,37 @@ interface ApiService {
/**
* 生成token
*/
@GET("scales/generateToken")
@GET("/shuwei-zhct/scales/generateToken")
suspend fun generateToken(@Query("deviceId") deviceId: String): ApiResponse<String>
/**
* 获取人脸数据
*/
@GET("scales/getUserFaceCache")
@GET("/shuwei-zhct/scales/getUserFaceCache")
suspend fun getUserFaceCache(): ApiResponse<List<UserFaceModel>>
/**
* 餐盘用户信息获取
*/
@POST("swEquipmentRelUser/equipmentBoxLogin")
@POST("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
suspend fun equipmentBoxLogin(@Body param: LoginParam): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息列表查询
*/
@POST("swEquipmentRelUser/list")
@POST("/shuwei-zhct/swEquipmentRelUser/list")
suspend fun getEquipmentList(@Body param: EquipmentParam): ApiResponse<List<EquipmentUserInfo>>
/**
* 餐盘用户信息绑定解绑
*/
@POST("swEquipmentRelUser/addOrEdit")
@POST("/shuwei-zhct/swEquipmentRelUser/addOrEdit")
suspend fun bindEquipment(@Body param: BindParam): ApiResponse<EquipmentUserInfo?>
/**
* 用户信息模糊搜索
*/
@POST("swclientUserInfoShop/selectList")
@POST("/shuwei-user/swclientUserInfoShop/selectList")
suspend fun searchUser(@Body param: SearchParam): ApiResponse<SearchResult>
}
@@ -9,7 +9,6 @@ import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.network.api.ApiService
import retrofit2.http.POST
/**
* 远程数据处理
@@ -55,7 +54,6 @@ class RemoteRepository constructor(
/**
* 用户信息模糊搜索
*/
@POST("/swclientUserInfoShop/selectList")
suspend fun searchUser(param: SearchParam): ApiResponse<SearchResult> {
return safeApiCall { apiService.searchUser(param) }
}
@@ -9,21 +9,19 @@ import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
class PermissionHelper private constructor(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int,
private val rationale: String? = null
internal val requestCode: Int,
private val rationale: String? = null,
private val onGranted: (() -> Unit)?
) {
private var onGranted: (() -> Unit)? = null
companion object {
/**
* 创建权限请求构建器
* @param context 上下文(Activity或Fragment
* @param permissions 需要请求的权限数组
* @param requestCode 请求码
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
@@ -43,15 +41,17 @@ class PermissionHelper private constructor(
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
* @return 是否所有权限都已授予
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
onPermissionPermanentlyDenied: (List<String>) -> Unit
permissionHelper: PermissionHelper? = null
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionHelper?.onGranted?.invoke()
return true
}
@@ -67,10 +67,6 @@ class PermissionHelper private constructor(
}
}
}
if (permanentlyDeniedPermissions.isNotEmpty()) {
onPermissionPermanentlyDenied(permanentlyDeniedPermissions)
}
return false
}
@@ -83,6 +79,8 @@ class PermissionHelper private constructor(
}
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
@@ -93,9 +91,10 @@ class PermissionHelper private constructor(
private val requestCode: Int
) {
private var rationale: String? = null
private var onGranted: (() -> Unit)? = null
/**
* 设置权限说明(当需要向用户解释权限用途时显示)
* 设置权限说明
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
@@ -105,10 +104,16 @@ class PermissionHelper private constructor(
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): PermissionHelper {
val helper = PermissionHelper(context, permissions, requestCode, rationale)
helper.onGranted = callback
return helper
fun onGranted(callback: () -> Unit): Builder {
this.onGranted = callback
return this
}
/**
* 构建PermissionHelper实例
*/
fun build(): PermissionHelper {
return PermissionHelper(context, permissions, requestCode, rationale, onGranted)
}
}
@@ -126,13 +131,11 @@ class PermissionHelper private constructor(
private fun requestPermissions() {
val activity = context as? Activity ?: return
// 检查是否需要显示权限说明
val shouldShowRationale = permissions.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale && rationale != null) {
// 显示解释对话框
AlertDialog.Builder(activity)
.setTitle("权限说明")
.setMessage(rationale)
@@ -142,7 +145,6 @@ class PermissionHelper private constructor(
.setNegativeButton("取消", null)
.show()
} else {
// 直接请求权限
doRequestPermissions(activity)
}
}
@@ -1,7 +1,12 @@
package com.sw.platecabinet.viewmodel
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -10,12 +15,99 @@ class SettingViewModel : BaseViewModel() {
private val _equipmentList = MutableStateFlow<List<EquipmentUserInfo>>(emptyList())
val equipmentList: StateFlow<List<EquipmentUserInfo>> = _equipmentList
fun getEquipmentList(param: EquipmentParam) {
private val _searchMemberList = MutableStateFlow<List<SearchResult.Member>>(emptyList())
val searchMemberList: StateFlow<List<SearchResult.Member>> = _searchMemberList
var currentPage = 1
var isLoading = false
var canLoadMore = true
/**
* 绑定/解绑状态 <绑定/解绑, 错误信息,>
*/
private val _bindStateChange = MutableStateFlow<Pair<Boolean?, ErrorInfo>>(null to ErrorInfo())
val bindStateChange: StateFlow<Pair<Boolean?, ErrorInfo>> = _bindStateChange
/**
* 获取设备绑定用户信息列表
*/
fun getEquipmentList(equipmentCode: String = globalEquipmentCode) {
launchWithLoading {
val param = EquipmentParam(equipmentCode = equipmentCode)
val response = repository.getEquipmentList(param)
if (parseResponse(response)) {
_equipmentList.value = response.data ?: emptyList()
}
}
}
/**
* 搜索会员信息列表
*/
fun getSearchMemberList(param: String, pageNum: Int = 1, pageSize: Int = 50) {
currentPage = pageNum
launch {
val param = SearchParam(
param = param,
pageNum = pageNum,
pageSize = pageSize
)
isLoading = true
val response = repository.searchUser(param)
isLoading = false
if (parseResponse(response)) {
response.data?.let {
canLoadMore = it.hasNextPage != false
val newList = it.list ?: emptyList()
if (pageNum == 1) {
_searchMemberList.value = newList
} else {
_searchMemberList.value = _searchMemberList.value.plus(newList)
}
currentPage++
}
}
}
}
fun bindPlate(
equipmentBoxCode: String,
equipmentCode: String = globalEquipmentCode,
memberId: Int,
plateNumber: String
) {
launchWithLoading {
val bindParam = BindParam(
equipmentBoxCode = equipmentBoxCode,
equipmentCode = equipmentCode,
memberId = memberId,
plateNumber = plateNumber
)
val response = repository.bindEquipment(bindParam)
if (parseResponse(response)) {
_bindStateChange.value = true to ErrorInfo()
} else {
_bindStateChange.value = true to ErrorInfo(response.code, response.msg.toString())
}
}
}
fun unbindPlate(
equipmentBoxCode: String,
equipmentCode: String = globalEquipmentCode
) {
launchWithLoading {
val bindParam = BindParam(
equipmentBoxCode = equipmentBoxCode,
equipmentCode = equipmentCode
)
val response = repository.bindEquipment(bindParam)
if (parseResponse(response)) {
_bindStateChange.value = false to ErrorInfo()
} else {
_bindStateChange.value = false to ErrorInfo(response.code, response.msg.toString())
}
}
}
}
@@ -1,10 +1,31 @@
package com.sw.platecabinet.viewmodel
import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
class UserViewModel : BaseViewModel() {
// 通过人脸获取到的用户信息
private val _currentUserInfo = MutableStateFlow<EquipmentUserInfo?>(null)
val currentUserInfo: StateFlow<EquipmentUserInfo?> = _currentUserInfo
private val faceApi: FaceApi = FaceApi()
/**
* 获取token
*/
fun generateToken(deviceId: String = "SWSN:88:12:AC:4E:D9:CC") {
launchWithLoading {
val response = repository.generateToken(deviceId)
@@ -19,25 +40,91 @@ class UserViewModel : BaseViewModel() {
}
}
fun getUserFaceCache() {
/**
* 获取人脸数据
*/
fun getUserFaceCache(index: Int = 0) {
launchWithLoading {
val response = repository.getUserFaceCache()
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureString))
}
faceApi.updateFaceData(index, faceEntity)
activeEngine()
}
}
}
/**
* 激活 arcsoft 人脸
* 激活人脸识别引擎
*/
fun activeEngine(
appId: String = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj",
sdkKey: String = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k",
activeKey: String = "085F-118G-Q391-53YL"
) {
// val runtimeABI: RuntimeABI? = FaceEngine.getRuntimeABI()
private fun activeEngine() {
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q391-53YL"
faceApi.activeEngine(
App.getContext(),
appId,
sdkKey,
activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
})
}
/**
* 校验码登录
*/
fun loginWithPwd(equipmentCode: String = globalEquipmentCode, phone: String, password: String) {
launchWithLoading {
val loginParam = LoginParam(
equipmentCode = equipmentCode,
phone = phone,
password = password
)
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
}
}
}
/**
* 通过用户id获取用户信息
*/
fun getUserInfoById(equipmentCode: String = globalEquipmentCode, memberId: Int) {
launchWithLoading {
val loginParam = LoginParam(
equipmentCode = equipmentCode,
memberId = memberId
)
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
}
}
}
}
@@ -40,6 +40,7 @@
android:src="@mipmap/ic_phone" />
<EditText
android:id="@+id/et_phone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
@@ -79,6 +80,7 @@
android:src="@mipmap/ic_password" />
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
@@ -6,6 +6,7 @@
android:layout_height="match_parent"
android:background="#14141E"
android:orientation="vertical"
android:fitsSystemWindows="false"
tools:context=".activity.LoginByFaceActivity">
<include
@@ -22,7 +23,7 @@
android:id="@+id/tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="159dp"
android:layout_marginTop="129dp"
android:text="请正视屏幕"
android:textColor="#fff0c8b4"
android:textSize="36sp" />
@@ -34,9 +35,19 @@
android:padding="10dp"
android:visibility="visible">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_margin="10dp"
<!-- <androidx.camera.view.PreviewView-->
<!-- android:id="@+id/previewView"-->
<!-- android:layout_margin="10dp"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent" />-->
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
+17 -10
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -20,22 +21,22 @@
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
tools:text="1-12"
android:layout_marginTop="20dp"
android:textColor="#fff0c8b4"
android:textSize="36sp"
android:textStyle="bold" />
android:textStyle="bold"
tools:text="1-12" />
<ImageView
android:layout_width="280dp"
android:layout_height="24dp"
android:layout_marginTop="24dp"
android:layout_marginTop="22dp"
android:src="@mipmap/ic_bind_img" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="54dp"
android:layout_marginTop="52dp"
android:orientation="horizontal">
<TextView
@@ -61,7 +62,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:layout_marginTop="21dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
@@ -71,6 +72,7 @@
android:layout_height="match_parent"
android:background="@color/transparent"
android:gravity="center"
android:maxLines="1"
android:textColor="#fff0c8b4"
android:textSize="26sp"
android:textStyle="bold"
@@ -80,7 +82,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_marginTop="22dp"
android:orientation="horizontal">
<TextView
@@ -106,7 +108,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:layout_marginTop="22dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
@@ -117,6 +119,9 @@
android:background="@color/transparent"
android:gravity="center"
android:hint="请输入会员姓名或手机号"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:textColor="#F0C8B4"
android:textColorHint="#ff7c6d6a"
android:textSize="26sp" />
@@ -126,7 +131,9 @@
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="188dp"
android:layout_marginTop="24dp" />
android:layout_marginTop="24dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2" />
<LinearLayout
android:id="@+id/ll_bind"
@@ -149,7 +156,7 @@
android:id="@+id/tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginTop="46dp"
android:paddingHorizontal="20dp"
android:text="返回"
android:textColor="#ff887872"
@@ -7,10 +7,6 @@
android:orientation="vertical"
tools:context=".fragment.SettingListFragment">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainRecyclerView"
android:layout_width="match_parent"
+6 -6
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="75dp"
android:id="@+id/ll_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_unbind"
android:gravity="center_vertical"
android:orientation="horizontal"
@@ -27,10 +27,10 @@
android:layout_marginStart="24dp"
android:layout_weight="1"
android:text="未绑定"
android:visibility="gone"
android:textColor="#fff0c8b4"
android:textSize="18sp"
android:textStyle="bold" />
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_bind_info"
@@ -55,10 +55,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="10dp"
android:text="9小时前使用"
android:textColor="#585868"
android:textSize="18sp"
android:textStyle="bold" />
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_bind"
android:padding="18dp"
android:padding="12dp"
android:id="@+id/ll_root"
android:orientation="vertical">
+3 -1
View File
@@ -4,13 +4,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/tvLeftDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:textColor="#F0C8B4"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
@@ -22,6 +23,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:textColor="#F0C8B4"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
+3 -1
View File
@@ -1,5 +1,7 @@
<resources>
<string name="app_name">SmartPlateCabinet</string>
<string name="app_name">餐盘柜</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="specific_engine_init_failed">%s 初始化失败,错误码:%d\n错误码常量名:%s</string>
</resources>