diff --git a/.idea/misc.xml b/.idea/misc.xml
index 9cafd9a..01415b9 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -10,7 +10,7 @@
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2f903fd..7094ea5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -18,9 +18,9 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- ndk {
- abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
- }
+// ndk {
+// abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
+// }
}
buildTypes {
@@ -62,11 +62,9 @@ dependencies {
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions)
- // 权限申请
- implementation(libs.accompanist.permissions)
// hilt注入
- implementation(libs.hilt.android)
- ksp(libs.hilt.android.compiler)
+// implementation(libs.hilt.android)
+// ksp(libs.hilt.android.compiler)
// retrofit网络请求
implementation(libs.retrofit)
@@ -82,4 +80,8 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
+
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.fragment.ktx)
+// implementation(libs.adapter.coroutines)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6e88fdb..c6f91cb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet"
tools:targetApi="31">
+
-
-
-
-
+
+
+
+
+
+
diff --git a/app/src/main/java/com/sw/platecabinet/GlobalData.kt b/app/src/main/java/com/sw/platecabinet/GlobalData.kt
new file mode 100644
index 0000000..ce742a2
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/GlobalData.kt
@@ -0,0 +1,14 @@
+package com.sw.platecabinet
+
+object GlobalData {
+
+}
+
+/**
+ *
+ */
+object GlobalKey {
+ const val KEY_TOKEN = "tokenKey"
+ const val KEY_FIRST_RUN = "firstRun"
+ const val KEY_USER_INFO = "userInfoKey"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/MyApp.kt b/app/src/main/java/com/sw/platecabinet/MyApp.kt
index b386470..cd64514 100644
--- a/app/src/main/java/com/sw/platecabinet/MyApp.kt
+++ b/app/src/main/java/com/sw/platecabinet/MyApp.kt
@@ -1,9 +1,16 @@
package com.sw.platecabinet
-import android.app.Application
-import android.content.Context
import com.sw.plate.App
+import timber.log.Timber
class MyApp : App() {
+ companion object {
+ const val DEBUG: Boolean = true
+ }
+ override fun onCreate() {
+ super.onCreate()
+ Timber.plant(Timber.DebugTree())
+ Timber.d("初始化")
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt
index b8fd174..477f7a5 100644
--- a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt
+++ b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt
@@ -1,36 +1,39 @@
package com.sw.platecabinet.activity
+import android.app.Dialog
import android.content.Context
import android.os.Bundle
-import android.view.WindowManager
+import android.text.TextUtils
+import android.view.View
+import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
+import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
+import com.sw.platecabinet.ext.setClickListeners
+import com.sw.platecabinet.utils.PermissionHelper
+import com.sw.platecabinet.view.CustomDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
+import timber.log.Timber
/**
* activity 基类
*/
abstract class BaseActivity : AppCompatActivity() {
-
protected lateinit var binding: VB
- private set
private var headerBinding: ItemTitleTimeBinding? = null
protected lateinit var context: Context
private var timeJob: Job? = null
+ private var mDialogWaiting: CustomDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
- window.setFlags(
- WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN
- )
context = this
binding = inflateViewBinding()
headerBinding = inflateTitleBinding()
@@ -41,6 +44,11 @@ abstract class BaseActivity : AppCompatActivity() {
fun updateTime() {
headerBinding?.let {
+ headerBinding!!.tvRightTime.setClickListeners(
+ onDoubleClick = {
+ MainActivity.start(context, pageType = PageType.SETTING_LIST)
+ }
+ )
val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow()
@@ -52,6 +60,45 @@ abstract class BaseActivity : AppCompatActivity() {
}
}
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ // 统一处理权限拒绝情况
+ val allGranted = PermissionHelper.handlePermissionResult(
+ this,
+ requestCode,
+ permissions,
+ grantResults
+ ) { permanentlyDeniedPermissions ->
+ // 权限被永久拒绝的统一处理
+ Timber.e("用户永久拒绝权限 $permanentlyDeniedPermissions")
+ }
+ }
+
+ /**
+ * 显示等待提示框
+ */
+ fun showWaitingDialog(tip: String?): Dialog? {
+ hideWaitingDialog()
+ val view = View.inflate(this, R.layout.dialog_waiting, null)
+ if (!TextUtils.isEmpty(tip)) (view.findViewById(R.id.tvTip) as TextView).setText(tip)
+ mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
+ mDialogWaiting!!.show()
+ mDialogWaiting!!.setCancelable(true)
+ return mDialogWaiting
+ }
+
+ /**
+ * 隐藏等待提示框
+ */
+ fun hideWaitingDialog() {
+ mDialogWaiting?.dismiss()
+ mDialogWaiting = null
+ }
+
protected abstract fun inflateViewBinding(): VB
protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding?
diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
index 4f3d806..c065a7a 100644
--- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
+++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
@@ -1,9 +1,11 @@
package com.sw.platecabinet.activity
+import android.Manifest
import android.content.Intent
import android.graphics.Outline
import android.view.View
import android.view.ViewOutlineProvider
+import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
@@ -11,6 +13,8 @@ import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
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 timber.log.Timber
/**
@@ -18,6 +22,10 @@ import timber.log.Timber
*/
class LoginByFaceActivity : BaseActivity() {
private lateinit var cameraProviderFuture: ListenableFuture
+ private val viewModel by viewModels()
+
+ private val CAMERA_PERMISSION_REQUEST_CODE = 100
+ private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater)
@@ -28,14 +36,25 @@ class LoginByFaceActivity : BaseActivity() {
}
override fun initialize() {
+ // 请求权限
+ checkCameraPermission()
initView()
- startCamera()
+ viewModel.generateToken()
binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java)
startActivity(intent)
}
}
+ private fun checkCameraPermission() {
+ PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
+ .onGranted {
+ // 权限已授予
+ startCamera()
+ }
+ .checkAndRequest()
+ }
+
private fun initView() {
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt
index b7aff2f..a7d1a05 100644
--- a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt
+++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt
@@ -14,6 +14,9 @@ class LoginByPwdActivity : BaseActivity() {
}
override fun initialize() {
+ binding.btnLogin.setOnClickListener {
+ MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
+ }
binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent)
diff --git a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt
index e60becd..cb3ec95 100644
--- a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt
+++ b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt
@@ -1,9 +1,20 @@
package com.sw.platecabinet.activity
+import android.content.Context
+import android.content.Intent
+import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityMainBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
+import com.sw.platecabinet.fragment.BindPlateFragment
+import com.sw.platecabinet.fragment.PlateCabinetFullFragment
+import com.sw.platecabinet.fragment.PlateOpenFragment
+import com.sw.platecabinet.fragment.SettingListFragment
+import com.sw.platecabinet.fragment.UnBindPlateFragment
+import com.sw.platecabinet.model.response.EquipmentUserInfo
+import com.sw.platecabinet.utils.FragmentHelper
class MainActivity : BaseActivity() {
+ private lateinit var fragmentHelper: FragmentHelper
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
@@ -13,7 +24,65 @@ class MainActivity : BaseActivity() {
return binding.includeHeader
}
- override fun initialize() {
-
+ // 静态启动方法
+ companion object {
+ private const val PARAM_PAGE_TYPE = "pageType"
+ private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
+ fun start(
+ context: Context,
+ pageType: PageType,
+ equipmentUserInfo: EquipmentUserInfo? = null
+ ) {
+ val intent = Intent(context, MainActivity::class.java)
+ intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
+ intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo)
+ context.startActivity(intent)
+ }
}
-}
\ No newline at end of file
+
+ override fun initialize() {
+ val param = intent.getStringExtra(PARAM_PAGE_TYPE)
+ val equipmentUserInfo = intent.getParcelableExtra(PARAM_EQUIPMENT_INFO)
+ val pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
+ fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
+ val page = when (pageType) {
+ PageType.SETTING_LIST -> SettingListFragment()
+ PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
+ PageType.PLATE_OPEN -> PlateOpenFragment()
+ PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
+ PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment()
+ }
+ fragmentHelper.addFragment(page)
+ }
+}
+
+/**
+ * 界面类型
+ */
+enum class PageType {
+ /**
+ * 餐盘柜打开
+ */
+ PLATE_OPEN,
+
+ /**
+ * 设置列表
+ */
+ SETTING_LIST,
+
+ /**
+ * 绑定餐盘
+ */
+ BIND_PLATE,
+
+ /**
+ * 解绑餐盘
+ */
+ UNBIND_PLATE,
+
+ /**
+ * 餐盘柜已满
+ */
+ PLATE_CABINET_FULL
+}
+
diff --git a/app/src/main/java/com/sw/platecabinet/activity/TestActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/TestActivity.kt
new file mode 100644
index 0000000..3414411
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/activity/TestActivity.kt
@@ -0,0 +1,97 @@
+package com.sw.platecabinet.activity
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.sw.platecabinet.R
+import kotlin.math.ceil
+
+class TestActivity : AppCompatActivity() {
+ private lateinit var parentRecyclerView: RecyclerView
+ private lateinit var pageAdapter: PageAdapter
+ private val itemsPerPage = 22 // 每行2个,每列11个,共22个
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_test)
+
+ parentRecyclerView = findViewById(R.id.parentRecyclerView)
+ parentRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
+
+ // 假设有100个数据项
+ val allItems = (1..30).map { "Item $it" }
+ val pages = allItems.chunked(itemsPerPage)
+
+ pageAdapter = PageAdapter(pages)
+ parentRecyclerView.adapter = pageAdapter
+ }
+
+ private fun convertToColumnFirst(original: List, cols: Int): List {
+ val rows = ceil(original.size.toDouble() / cols).toInt()
+ return List(original.size) { pos ->
+ val row = pos % rows
+ val col = pos / rows
+ val originalPos = col + row * cols
+ original.getOrElse(originalPos) { "" }
+ }
+ }
+
+ inner class PageAdapter(private val pages: List>) :
+ RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.page_item_layout, parent, false)
+ return PageViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
+ holder.bind(pages[position])
+ }
+
+ override fun getItemCount(): Int = pages.size
+
+ inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val childRecyclerView: RecyclerView =
+ itemView.findViewById(R.id.childRecyclerView)
+
+ fun bind(items: List) {
+ childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
+ childRecyclerView.adapter = ChildAdapter(items)
+ }
+ }
+ }
+
+ inner class ChildAdapter(private val items: List) :
+ RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_bind_view, parent, false)
+ return ItemViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
+ holder.bind(position, items[position])
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val llRoot: LinearLayout = itemView.findViewById(R.id.ll_root)
+ private val tvNum: TextView = itemView.findViewById(R.id.tv_num)
+
+ fun bind(position: Int, item: String) {
+ tvNum.text = item
+ llRoot.setBackgroundResource(if (position % 3 == 0) R.drawable.grid_item_bind else R.drawable.grid_item_unbind)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/adapter/GenericPageAdapter.kt b/app/src/main/java/com/sw/platecabinet/adapter/GenericPageAdapter.kt
new file mode 100644
index 0000000..5f0bc85
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/adapter/GenericPageAdapter.kt
@@ -0,0 +1,84 @@
+package com.sw.platecabinet.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import com.sw.platecabinet.R
+
+/**
+ * 通用分页适配器(支持ViewBinding)
+ */
+class GenericPageAdapter(
+ private var pages: List>,
+ private val pageLayoutId: Int,
+ private val itemBindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
+ private val itemBindCallback: VB.(item: T, position: Int) -> Unit
+) : RecyclerView.Adapter.PageViewHolder>() {
+
+ fun updatePages(newPages: List>) {
+ this.pages = newPages
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(pageLayoutId, parent, false)
+ return PageViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
+ holder.bind(pages[position])
+ }
+
+ override fun getItemCount(): Int = pages.size
+
+ inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
+
+ fun bind(items: List) {
+ childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
+
+ // 添加间距装饰(12dp)
+ childRecyclerView.addItemDecoration(
+ GridSpacingItemDecoration(
+ spanCount = 2, // 2列
+ spacing = itemView.context.dpToPx(12), // 12dp
+ includeEdge = true // 包含边缘间距
+ )
+ )
+
+ childRecyclerView.adapter =
+ GenericChildAdapter(items, itemBindingInflater, itemBindCallback)
+ }
+ }
+}
+
+/**
+ * 通用子项适配器(支持ViewBinding)
+ */
+class GenericChildAdapter(
+ private val items: List,
+ private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
+ private val bindCallback: VB.(item: T, position: Int) -> Unit,
+) : RecyclerView.Adapter.ViewHolder>() {
+
+ inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: T, position: Int) {
+ binding.bindCallback(item, position)
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = bindingInflater(LayoutInflater.from(parent.context), parent, false)
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(items[position], position)
+
+ }
+
+ override fun getItemCount(): Int = items.size
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/adapter/GridSpacingItemDecoration.kt b/app/src/main/java/com/sw/platecabinet/adapter/GridSpacingItemDecoration.kt
new file mode 100644
index 0000000..4ac9d2f
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/adapter/GridSpacingItemDecoration.kt
@@ -0,0 +1,54 @@
+package com.sw.platecabinet.adapter
+
+import android.content.Context
+import android.graphics.Rect
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * 为RecyclerView添加间距的ItemDecoration
+ */
+class GridSpacingItemDecoration(
+ private val spanCount: Int, // 列数
+ private val spacing: Int, // 间距(dp)
+ private val includeEdge: Boolean // 是否包含边缘
+) : RecyclerView.ItemDecoration() {
+
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ val position = parent.getChildAdapterPosition(view) // item position
+ val column = position % spanCount // item column
+
+ if (includeEdge) {
+ // 左右间距计算
+ outRect.left = spacing - column * spacing / spanCount
+ outRect.right = (column + 1) * spacing / spanCount
+
+ // 第一行添加顶部间距
+ if (position < spanCount) {
+ outRect.top = spacing
+ }
+ outRect.bottom = spacing // 每行底部都添加间距
+ } else {
+ // 左右间距计算(不包含边缘)
+ outRect.left = column * spacing / spanCount
+ outRect.right = spacing - (column + 1) * spacing / spanCount
+
+ // 不是第一行时添加顶部间距
+ if (position >= spanCount) {
+ outRect.top = spacing
+ }
+ }
+ }
+}
+
+/**
+ * 将dp值转换为px
+ */
+fun Context.dpToPx(dp: Int): Int {
+ return (dp * resources.displayMetrics.density).toInt()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/ext/CommonExt.kt b/app/src/main/java/com/sw/platecabinet/ext/CommonExt.kt
index ef20401..3f3c359 100644
--- a/app/src/main/java/com/sw/platecabinet/ext/CommonExt.kt
+++ b/app/src/main/java/com/sw/platecabinet/ext/CommonExt.kt
@@ -46,4 +46,8 @@ val Float.sp: Float
TypedValue.COMPLEX_UNIT_SP,
this,
Resources.getSystem().displayMetrics
- )
\ No newline at end of file
+ )
+
+fun formatNumber(num: String): String {
+ return num.padStart(2, '0')
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt b/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt
new file mode 100644
index 0000000..7a9f4a0
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt
@@ -0,0 +1,33 @@
+package com.sw.platecabinet.ext
+
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import android.widget.TextView
+
+/**
+ * 设置 TextView 的双击和单击事件监听器
+ * @param doubleClickInterval 双击间隔时间(毫秒),默认 300ms
+ * @param onDoubleClick 双击回调
+ * @param onSingleClick 单击回调
+ */
+fun TextView.setClickListeners(
+ doubleClickInterval: Long = 300,
+ onDoubleClick: (View) -> Unit = {},
+ onSingleClick: (View) -> Unit = {}
+) {
+ var clickCount = 0
+ val handler = Handler(Looper.getMainLooper())
+
+ setOnClickListener { view ->
+ clickCount++
+ handler.postDelayed({
+ if (clickCount == 1) {
+ onSingleClick(view)
+ } else if (clickCount == 2) {
+ onDoubleClick(view)
+ }
+ clickCount = 0 // 重置计数
+ }, doubleClickInterval)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/fragment/BaseFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/BaseFragment.kt
new file mode 100644
index 0000000..64d45f3
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/fragment/BaseFragment.kt
@@ -0,0 +1,41 @@
+package com.sw.platecabinet.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.viewbinding.ViewBinding
+
+abstract class BaseFragment(
+ private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB
+) : Fragment() {
+ private var _binding: VB? = null
+ protected val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ _binding = bindingInflater(inflater, container, false)
+ initialize()
+ return binding.root
+ }
+
+ abstract fun initialize()
+
+ class ViewModelFactory(private val creator: () -> T) :
+ ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return creator() as T
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt
new file mode 100644
index 0000000..063f024
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/fragment/BindPlateFragment.kt
@@ -0,0 +1,65 @@
+package com.sw.platecabinet.fragment
+
+import android.os.Bundle
+import com.sw.platecabinet.R
+import com.sw.platecabinet.adapter.GenericChildAdapter
+import com.sw.platecabinet.databinding.FragmentBindPlateBinding
+import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
+import com.sw.platecabinet.model.response.EquipmentUserInfo
+import timber.log.Timber
+
+
+/**
+ * 餐盘柜绑定
+ */
+class BindPlateFragment : BaseFragment(
+ FragmentBindPlateBinding::inflate
+) {
+ private lateinit var adapter: GenericChildAdapter
+ internal val ARG_PARAM1 = "param1"
+ private var info: EquipmentUserInfo? = null
+
+ companion object {
+ @JvmStatic
+ fun newInstance(param1: EquipmentUserInfo?) =
+ BindPlateFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(ARG_PARAM1, param1)
+ }
+ }
+ }
+
+ override fun initialize() {
+ arguments?.let {
+ info = it.getParcelable(ARG_PARAM1)
+ info?.let { binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}" }
+ }
+ adapter = createAdapter()
+ registerDateChange()
+ binding.tvBack.setOnClickListener { activity?.finish() }
+ }
+
+ private fun registerDateChange() {
+
+ }
+
+ private fun createAdapter(): GenericChildAdapter {
+ return GenericChildAdapter(
+ items = emptyList(),
+ bindingInflater = ItemSearchUserInfoBinding::inflate,
+ bindCallback = { item, position ->
+ this.tvName.text = item.name
+ this.tvPhone.text = item.phone
+ if (item.isBound()) {
+ this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
+ } else {
+ this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
+ }
+
+ this.llRoot.setOnClickListener {
+ Timber.d("itemClick ${item.name}, position = $position")
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/fragment/PlateCabinetFullFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/PlateCabinetFullFragment.kt
new file mode 100644
index 0000000..5af0cb6
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/fragment/PlateCabinetFullFragment.kt
@@ -0,0 +1,29 @@
+package com.sw.platecabinet.fragment
+
+import android.os.CountDownTimer
+import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
+
+/**
+ * 餐盘柜已满
+ */
+class PlateCabinetFullFragment :
+ BaseFragment(FragmentPlateCabinetFullBinding::inflate) {
+ private var totalTimeInMillis: Long = 5 * 1000
+
+ override fun initialize() {
+ initCountTime()
+ }
+
+ fun initCountTime() {
+ object : CountDownTimer(totalTimeInMillis, 1000) {
+ override fun onTick(millisUntilFinished: Long) {
+ binding.tvAutoClose.text = "${(millisUntilFinished / 1000).toInt()}秒后返回主屏"
+ }
+
+ override fun onFinish() {
+ activity?.finish()
+ }
+ }.start()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/fragment/PlateOpenFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/PlateOpenFragment.kt
new file mode 100644
index 0000000..2feb7f4
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/fragment/PlateOpenFragment.kt
@@ -0,0 +1,28 @@
+package com.sw.platecabinet.fragment
+
+import android.os.CountDownTimer
+import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
+
+/**
+ * 餐盘柜打开界面
+ */
+class PlateOpenFragment :
+ BaseFragment(FragmentPlateOpenBinding::inflate) {
+ private var totalTimeInMillis: Long = 5 * 1000
+
+ override fun initialize() {
+ initCountTime()
+ }
+
+ fun initCountTime() {
+ object : CountDownTimer(totalTimeInMillis, 1000) {
+ override fun onTick(millisUntilFinished: Long) {
+ binding.tvAutoClose.text = (millisUntilFinished / 1000).toInt().toString()
+ }
+
+ override fun onFinish() {
+ activity?.finish()
+ }
+ }.start()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/fragment/SettingListFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/SettingListFragment.kt
new file mode 100644
index 0000000..e942345
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/fragment/SettingListFragment.kt
@@ -0,0 +1,135 @@
+package com.sw.platecabinet.fragment
+
+import android.view.View
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.sw.inbound.utils.DateTimeUtils
+import com.sw.platecabinet.R
+import com.sw.platecabinet.activity.MainActivity
+import com.sw.platecabinet.activity.PageType
+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.model.response.EquipmentUserInfo
+import com.sw.platecabinet.viewmodel.SettingViewModel
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import kotlin.math.ceil
+
+/**
+ * 餐盘柜绑定的用户列表界面
+ */
+class SettingListFragment :
+ BaseFragment(FragmentSettingListBinding::inflate) {
+ private val viewModel: SettingViewModel by viewModels()
+
+
+ private lateinit var pageAdapter: GenericPageAdapter
+ private val itemsPerPage = 22 // 每行2个,每列11个,共22个
+
+ private fun registerDateChange() {
+ lifecycleScope.launch {
+ // 使用 repeatOnLifecycle 确保只在特定生命周期状态收集
+// repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.equipmentList.collect { it ->
+ Timber.d("registerDateChange updateAdapter it = $it")
+ updateAdapter(it)
+ }
+// }
+ }
+ }
+
+ override fun initialize() {
+ binding.mainRecyclerView.apply {
+ layoutManager = LinearLayoutManager(context)
+ pageAdapter = createAdapter()
+ adapter = pageAdapter
+ }
+ registerDateChange()
+ viewModel.getEquipmentList(EquipmentParam(equipmentCode = "202501171634"))
+ }
+
+ private fun createAdapter(): GenericPageAdapter {
+ return GenericPageAdapter(
+ pages = emptyList(),
+ pageLayoutId = R.layout.page_item_layout,
+ itemBindingInflater = ItemBindViewBinding::inflate,
+ itemBindCallback = { item, position ->
+ this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
+ if (item.isBound()) {
+ val date = DateTimeUtils.parseDateTime(item.updateTime)
+ val timeInMillis = date?.time ?: 0L
+ val isOldTime = DateTimeUtils.isMoreThan36HoursFromNow(timeInMillis)
+
+ this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
+ this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
+ this.tvLastTime.setTextColor(
+ if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor(
+ R.color.bind_585868
+ )
+ )
+ this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
+ this.tvOpen.setTextColor(resources.getColor(R.color.bind_F0C8B4))
+ this.llBindInfo.visibility = View.VISIBLE
+ this.tvUnbind.visibility = View.GONE
+
+ this.tvName.text = item.name
+ this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
+ } else {
+ this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
+ this.tvNum.setTextColor(resources.getColor(R.color.bind_F0C8B4))
+ this.llOpen.setBackgroundResource(R.drawable.grid_button_unbind)
+ this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
+ this.llBindInfo.visibility = View.GONE
+ this.tvUnbind.visibility = View.VISIBLE
+ }
+ this.llRoot.setOnClickListener {
+ Timber.d("itemClick llRoot ${item.name}, position = $position")
+ if (item.isBound()) {
+ MainActivity.start(requireContext(), pageType = PageType.UNBIND_PLATE)
+ } else {
+ MainActivity.start(requireContext(), pageType = PageType.BIND_PLATE)
+ }
+ }
+ this.llOpen.setOnClickListener {
+ Timber.d("itemClick llOpen ${item.equipmentCode}")
+ MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN)
+ }
+ }
+ )
+ }
+
+ fun updateAdapter(items: List) {
+ val pages = if (items.size > itemsPerPage) {
+ items.chunked(itemsPerPage) {
+ convertToColumnFirst(it, 11)
+ }
+ } else {
+ listOf(
+ // 根据条件判断是否需要重新排列
+// items
+ convertToColumnFirst(items, 11)
+ )
+ }
+ pageAdapter.updatePages(pages)
+ }
+
+ /**
+ * 重新排列数组
+ */
+ private fun convertToColumnFirst(
+ original: List,
+ cols: Int
+ ): List {
+ val rows = ceil(original.size.toDouble() / cols).toInt()
+ return List(original.size) { pos ->
+ val row = pos % rows
+ val col = pos / rows
+ val originalPos = col + row * cols
+ original[originalPos]
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/fragment/UnBindPlateFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/UnBindPlateFragment.kt
new file mode 100644
index 0000000..104a17e
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/fragment/UnBindPlateFragment.kt
@@ -0,0 +1,42 @@
+package com.sw.platecabinet.fragment
+
+import android.os.Bundle
+import com.sw.inbound.utils.DateTimeUtils
+import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
+import com.sw.platecabinet.model.response.EquipmentUserInfo
+
+/**
+ * 餐盘柜绑定
+ */
+class UnBindPlateFragment private constructor() : BaseFragment(
+ FragmentUnbindPlateBinding::inflate
+) {
+ internal val ARG_PARAM1 = "param1"
+ private var param1: EquipmentUserInfo? = null
+ private var info: EquipmentUserInfo? = null
+
+ companion object {
+ @JvmStatic
+ fun newInstance(param1: EquipmentUserInfo?) =
+ UnBindPlateFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(ARG_PARAM1, param1)
+ }
+ }
+ }
+
+ override fun initialize() {
+ arguments?.let {
+ info = it.getParcelable(ARG_PARAM1)
+ info?.let {
+ binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
+ binding.tvPlateNumberValue.text = it.plateNumber
+ binding.tvNameValue.text = it.name
+ binding.tvPhoneValue.text = it.phone
+ val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
+ val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
+ binding.tvUpdateTimeValue.text = timeAgo
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/model/request/BindParam.kt b/app/src/main/java/com/sw/platecabinet/model/request/BindParam.kt
new file mode 100644
index 0000000..1aef0f8
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/request/BindParam.kt
@@ -0,0 +1,35 @@
+package com.sw.platecabinet.model.request
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ *
+ * 设备与用户餐盘信息绑定解绑请求参数
+ */
+@Parcelize
+data class BindParam(
+ /**
+ *设备盒子编号
+ */
+ @SerializedName("equipmentBoxCode")
+ val equipmentBoxCode: String? = "",
+ /**
+ *设备编号
+ */
+ @SerializedName("equipmentCode")
+ val equipmentCode: String? = "",
+ /**
+ *
+ * 会员Id
+ */
+ @SerializedName("memberId")
+ val memberId: Int? = 0,
+ /**
+ *餐盘编号
+ */
+ @SerializedName("plateNumber")
+ val plateNumber: String? = ""
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/model/request/EquipmentParam.kt b/app/src/main/java/com/sw/platecabinet/model/request/EquipmentParam.kt
new file mode 100644
index 0000000..ed5695e
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/request/EquipmentParam.kt
@@ -0,0 +1,15 @@
+package com.sw.platecabinet.model.request
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * 根据设备ID查询当前用户的餐盘信息,用户信息,设备盒子信息请求参数
+ */
+@Parcelize
+data class EquipmentParam(
+ /**
+ * 设备ID
+ */
+ var equipmentCode: String = ""
+) : Parcelable
diff --git a/app/src/main/java/com/sw/platecabinet/model/request/LoginParam.kt b/app/src/main/java/com/sw/platecabinet/model/request/LoginParam.kt
new file mode 100644
index 0000000..048a4cb
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/request/LoginParam.kt
@@ -0,0 +1,33 @@
+package com.sw.platecabinet.model.request
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ * 登录参数
+ */
+@Parcelize
+data class LoginParam(
+ /**
+ * 设备ID
+ */
+ @SerializedName("equipmentCode")
+ val equipmentCode: String? = "",
+ /**
+ * 会员信息
+ */
+ @SerializedName("memberId")
+ val memberId: String? = "",
+ /**
+ * 密码
+ */
+ @SerializedName("password")
+ val password: String? = "",
+ /**
+ * 手机号
+ */
+ @SerializedName("phone")
+ val phone: String? = ""
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/model/request/SearchParam.kt b/app/src/main/java/com/sw/platecabinet/model/request/SearchParam.kt
new file mode 100644
index 0000000..6334c14
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/request/SearchParam.kt
@@ -0,0 +1,25 @@
+package com.sw.platecabinet.model.request
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ * (手机号/会员信息/会员卡号)模糊搜索
+ */
+@Parcelize
+data class SearchParam(
+ @SerializedName("consumptionTime")
+ val consumptionTime: List? = listOf(),
+ @SerializedName("createTime")
+ val createTime: List? = listOf(),
+ @SerializedName("memberFrom")
+ val memberFrom: Int? = 0,
+ @SerializedName("pageNum")
+ val pageNum: Int? = 0,
+ @SerializedName("pageSize")
+ val pageSize: Int? = 0,
+ @SerializedName("param")
+ val `param`: String? = ""
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/model/response/ApiResponse.kt b/app/src/main/java/com/sw/platecabinet/model/response/ApiResponse.kt
new file mode 100644
index 0000000..3258796
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/response/ApiResponse.kt
@@ -0,0 +1,10 @@
+package com.sw.platecabinet.model.response
+
+data class ApiResponse(
+ val code: Int,
+ val success: Boolean? = false,
+ val msg: String? = "",
+ val data: T? = null
+) {
+ fun isSuccess(): Boolean = code == 200
+}
diff --git a/app/src/main/java/com/sw/platecabinet/model/response/EquipmentUserInfo.kt b/app/src/main/java/com/sw/platecabinet/model/response/EquipmentUserInfo.kt
new file mode 100644
index 0000000..67c33ef
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/response/EquipmentUserInfo.kt
@@ -0,0 +1,65 @@
+package com.sw.platecabinet.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ * 设备绑定用户信息
+ */
+@Parcelize
+data class EquipmentUserInfo(
+ /**
+ * 设备盒子编号
+ */
+ @SerializedName("equipmentBoxCode")
+ val equipmentBoxCode: String? = "",
+ /**
+ * 设备编号
+ */
+ @SerializedName("equipmentCode")
+ val equipmentCode: String? = "",
+ /**
+ *
+ * 设备Id
+ */
+ @SerializedName("equipmentId")
+ val equipmentId: String? = "",
+ /**
+ * 设备名称
+ */
+ @SerializedName("equipmentName")
+ val equipmentName: String? = "",
+ /**
+ * 主键
+ */
+ @SerializedName("id")
+ val id: Int? = 0,
+ /**
+ * 会员Id
+ */
+ @SerializedName("memberId")
+ val memberId: Int? = 0,
+
+ @SerializedName("name")
+ val name: String? = "",
+ @SerializedName("phone")
+ val phone: String? = "",
+ /**
+ * 餐盘编号
+ */
+ @SerializedName("plateNumber")
+ val plateNumber: String? = "",
+ /**
+ *
+ * 更新时间
+ */
+ @SerializedName("updateTime")
+ val updateTime: String? = ""
+) : Parcelable {
+
+ fun isBound(): Boolean {
+ return memberId != null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/model/response/SearchResult.kt b/app/src/main/java/com/sw/platecabinet/model/response/SearchResult.kt
new file mode 100644
index 0000000..86cbcdd
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/response/SearchResult.kt
@@ -0,0 +1,78 @@
+package com.sw.platecabinet.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class SearchResult(
+ @SerializedName("endRow")
+ val endRow: Int? = 0,
+ @SerializedName("hasNextPage")
+ val hasNextPage: Boolean? = false,
+ @SerializedName("hasPreviousPage")
+ val hasPreviousPage: Boolean? = false,
+ @SerializedName("isFirstPage")
+ val isFirstPage: Boolean? = false,
+ @SerializedName("isLastPage")
+ val isLastPage: Boolean? = false,
+ @SerializedName("list")
+ val list: List? = listOf(),
+ @SerializedName("navigateFirstPage")
+ val navigateFirstPage: Int? = 0,
+ @SerializedName("navigateLastPage")
+ val navigateLastPage: Int? = 0,
+ @SerializedName("navigatePages")
+ val navigatePages: Int? = 0,
+ @SerializedName("navigatepageNums")
+ val navigatepageNums: List? = listOf(),
+ @SerializedName("nextPage")
+ val nextPage: Int? = 0,
+ @SerializedName("pageNum")
+ val pageNum: Int? = 0,
+ @SerializedName("pageSize")
+ val pageSize: Int? = 0,
+ @SerializedName("pages")
+ val pages: Int? = 0,
+ @SerializedName("prePage")
+ val prePage: Int? = 0,
+ @SerializedName("size")
+ val size: Int? = 0,
+ @SerializedName("startRow")
+ val startRow: Int? = 0,
+ @SerializedName("total")
+ val total: Int? = 0
+) : Parcelable {
+ @Parcelize
+ data class Member(
+ @SerializedName("cardBalance")
+ val cardBalance: String? = "",
+ @SerializedName("cardCode")
+ val cardCode: String? = "",
+ @SerializedName("consumptionCount")
+ val consumptionCount: Int? = 0,
+ @SerializedName("consumptionTotal")
+ val consumptionTotal: String? = "",
+ @SerializedName("createTime")
+ val createTime: String? = "",
+ @SerializedName("firstTopUpTime")
+ val firstTopUpTime: String? = "",
+ @SerializedName("id")
+ val id: Int? = 0,
+ @SerializedName("integralBalance")
+ val integralBalance: Int? = 0,
+ @SerializedName("lastConsumptionTime")
+ val lastConsumptionTime: String? = "",
+ @SerializedName("memberFrom")
+ val memberFrom: Int? = 0,
+ @SerializedName("name")
+ val name: String? = "",
+ @SerializedName("phone")
+ val phone: String? = "",
+ @SerializedName("rewardBalance")
+ val rewardBalance: String? = "",
+ @SerializedName("topUpBalance")
+ val topUpBalance: String? = ""
+ ) : Parcelable
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/model/response/UserFaceModel.kt b/app/src/main/java/com/sw/platecabinet/model/response/UserFaceModel.kt
new file mode 100644
index 0000000..eccfa65
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/model/response/UserFaceModel.kt
@@ -0,0 +1,23 @@
+package com.sw.platecabinet.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ * 用户人脸信息
+ */
+@Parcelize
+data class UserFaceModel(
+ @SerializedName("faceFeature")
+ val faceFeature: String? = "",
+ @SerializedName("faceFeatureString")
+ val faceFeatureString: String? = "",
+ @SerializedName("faceType")
+ val faceType: String? = "",
+ @SerializedName("userFaceId")
+ val userFaceId: String? = "",
+ @SerializedName("userId")
+ val userId: String? = ""
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/network/ApiClient.kt b/app/src/main/java/com/sw/platecabinet/network/ApiClient.kt
new file mode 100644
index 0000000..5449287
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/network/ApiClient.kt
@@ -0,0 +1,45 @@
+package com.sw.platecabinet.network
+
+import com.sw.platecabinet.MyApp
+import com.sw.platecabinet.network.api.ApiService
+import com.sw.platecabinet.network.interceptor.RequestInterceptor
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+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 = "http://192.168.1.8:9092"
+ private const val TIME_OUT = 30L // 超时时间(秒)
+
+ private val okHttpClient = OkHttpClient.Builder()
+ .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .readTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .addNetworkInterceptor(HttpLoggingInterceptor(logger = {
+ Timber.d("okhttp logger ==>${it}")
+ }).apply {
+ level = if (MyApp.DEBUG) {
+ HttpLoggingInterceptor.Level.BODY
+ } else {
+ HttpLoggingInterceptor.Level.NONE
+ }
+ })
+ .addInterceptor(RequestInterceptor())
+ .build()
+
+ private val retrofit = Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(okHttpClient)
+ .addConverterFactory(GsonConverterFactory.create())
+// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
+ .build()
+
+ val apiService: ApiService by lazy {
+ retrofit.create(ApiService::class.java)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt b/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt
new file mode 100644
index 0000000..ffb0e95
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt
@@ -0,0 +1,54 @@
+package com.sw.platecabinet.network.api
+
+import com.sw.platecabinet.model.request.BindParam
+import com.sw.platecabinet.model.request.EquipmentParam
+import com.sw.platecabinet.model.request.LoginParam
+import com.sw.platecabinet.model.request.SearchParam
+import com.sw.platecabinet.model.response.ApiResponse
+import com.sw.platecabinet.model.response.EquipmentUserInfo
+import com.sw.platecabinet.model.response.SearchResult
+import com.sw.platecabinet.model.response.UserFaceModel
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Query
+
+interface ApiService {
+
+ /**
+ * 生成token
+ */
+ @GET("scales/generateToken")
+ suspend fun generateToken(@Query("deviceId") deviceId: String): ApiResponse
+
+ /**
+ * 获取人脸数据
+ */
+ @GET("scales/getUserFaceCache")
+ suspend fun getUserFaceCache(): ApiResponse>
+
+ /**
+ * 餐盘用户信息获取
+ */
+ @POST("swEquipmentRelUser/equipmentBoxLogin")
+ suspend fun equipmentBoxLogin(@Body param: LoginParam): ApiResponse
+
+ /**
+ * 餐盘用户信息列表查询
+ */
+ @POST("swEquipmentRelUser/list")
+ suspend fun getEquipmentList(@Body param: EquipmentParam): ApiResponse>
+
+ /**
+ * 餐盘用户信息绑定解绑
+ */
+ @POST("swEquipmentRelUser/addOrEdit")
+ suspend fun bindEquipment(@Body param: BindParam): ApiResponse
+
+ /**
+ * 用户信息模糊搜索
+ */
+ @POST("swclientUserInfoShop/selectList")
+ suspend fun searchUser(@Body param: SearchParam): ApiResponse
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/network/interceptor/RequestInterceptor.kt b/app/src/main/java/com/sw/platecabinet/network/interceptor/RequestInterceptor.kt
new file mode 100644
index 0000000..7886682
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/network/interceptor/RequestInterceptor.kt
@@ -0,0 +1,32 @@
+package com.sw.platecabinet.network.interceptor
+
+import com.sw.inbound.utils.SPUtil
+import com.sw.plate.App
+import com.sw.platecabinet.GlobalKey
+import okhttp3.Interceptor
+import okhttp3.Response
+
+/**
+ * 请求拦截器
+ */
+class RequestInterceptor : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+ val requestBuilder = originalRequest.newBuilder()
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json")
+// .header("Authorization", "Bearer ${getToken()}")
+ .header("Authorization", getToken())
+
+ val newRequest = requestBuilder.build()
+
+ return chain.proceed(newRequest)
+ }
+
+ private fun getToken(): String {
+ // 从本地获取token的逻辑
+ val spUtil = SPUtil.getInstance(context = App.getContext())
+ return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
+// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/repository/BaseRepository.kt b/app/src/main/java/com/sw/platecabinet/repository/BaseRepository.kt
new file mode 100644
index 0000000..c2b0ca9
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/repository/BaseRepository.kt
@@ -0,0 +1,50 @@
+package com.sw.platecabinet.repository
+
+import com.google.gson.JsonParseException
+import com.sw.platecabinet.model.response.ApiResponse
+import retrofit2.HttpException
+import timber.log.Timber
+import java.io.IOException
+import java.net.ConnectException
+import java.net.SocketTimeoutException
+import javax.net.ssl.SSLHandshakeException
+
+abstract class BaseRepository {
+ suspend fun safeApiCall(apiCall: suspend () -> ApiResponse): ApiResponse {
+ return try {
+ apiCall()
+ } catch (e: Exception) {
+ Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
+
+ when (e) {
+ is HttpException -> {
+ ApiResponse(code = e.code(), msg = e.message())
+ }
+
+ is SocketTimeoutException -> {
+ ApiResponse(code = -2, msg = "请求超时: ${e.message}")
+ }
+
+ is ConnectException -> {
+ ApiResponse(code = -3, msg = "连接失败: ${e.message}")
+ }
+
+ is SSLHandshakeException -> {
+ ApiResponse(code = -4, msg = "SSL握手失败: ${e.message}")
+ }
+
+ is JsonParseException -> {
+ ApiResponse(code = -5, msg = "JSON解析错误: ${e.message}")
+ }
+
+ is IOException -> {
+ ApiResponse(code = -6, msg = "网络IO错误: ${e.message}")
+ }
+
+ else -> {
+ ApiResponse(code = -1, msg = "未知错误: ${e.message ?: "无错误信息"}")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt
new file mode 100644
index 0000000..0ac59cd
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt
@@ -0,0 +1,62 @@
+package com.sw.platecabinet.repository
+
+import com.sw.platecabinet.model.request.BindParam
+import com.sw.platecabinet.model.request.EquipmentParam
+import com.sw.platecabinet.model.request.LoginParam
+import com.sw.platecabinet.model.request.SearchParam
+import com.sw.platecabinet.model.response.ApiResponse
+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
+
+/**
+ * 远程数据处理
+ */
+class RemoteRepository constructor(
+ private val apiService: ApiService
+) : BaseRepository() {
+ /**
+ * 生成token
+ */
+ suspend fun generateToken(deviceId: String): ApiResponse {
+ return safeApiCall { apiService.generateToken(deviceId) }
+ }
+
+ /**
+ * 获取人脸数据
+ */
+ suspend fun getUserFaceCache(): ApiResponse> {
+ return safeApiCall { apiService.getUserFaceCache() }
+ }
+
+ /**
+ * 登录
+ */
+ suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse {
+ return safeApiCall { apiService.equipmentBoxLogin(param) }
+ }
+
+ /**
+ * 获取设备绑定用户列表
+ */
+ suspend fun getEquipmentList(param: EquipmentParam): ApiResponse> {
+ return safeApiCall { apiService.getEquipmentList(param) }
+ }
+
+ /**
+ * 餐盘用户信息绑定解绑
+ */
+ suspend fun bindEquipment(param: BindParam): ApiResponse {
+ return safeApiCall { apiService.bindEquipment(param) }
+ }
+
+ /**
+ * 用户信息模糊搜索
+ */
+ @POST("/swclientUserInfoShop/selectList")
+ suspend fun searchUser(param: SearchParam): ApiResponse {
+ return safeApiCall { apiService.searchUser(param) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt b/app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt
index 5244f5f..ffda7d2 100644
--- a/app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt
+++ b/app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.flow
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
+import kotlin.math.abs
/**
* 时间格式化工具类
@@ -47,4 +48,66 @@ object DateTimeUtils {
delay(intervalMillis)
}
}
+
+ /**
+ * 解析日期时间字符串
+ * @param timeString 格式为 "yyyy-MM-dd HH:mm:ss" 的字符串
+ * @return Date 对象,解析失败返回 null
+ */
+ fun parseDateTime(timeString: String?): Date? {
+ return try {
+ if (timeString == null) return null
+ val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+ format.parse(timeString)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ /**
+ * 判断给定时间是否距离当前时间超过36小时
+ * @param timeInMillis 时间戳(毫秒)
+ * @return true 表示超过36小时,false 表示未超过
+ */
+ fun isMoreThan36HoursFromNow(timeInMillis: Long): Boolean {
+ val currentTime = System.currentTimeMillis()
+ val timeDifference = currentTime - timeInMillis
+ val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
+ return hoursDifference >= 36
+ }
+
+ /**
+ * 获取时间间隔描述
+ */
+ fun getTimeAgo(date: Date?): String {
+ if (date == null) return "未知时间"
+
+ val now = Date()
+ val diffMillis = now.time - date.time
+
+ // 如果是未来时间
+ if (diffMillis < 0) {
+ val futureHours = abs(diffMillis) / (1000 * 60 * 60)
+ return if (futureHours < 24) {
+ "未来 $futureHours 小时"
+ } else {
+ val days = futureHours / 24
+ "未来 $days 天"
+ }
+ }
+
+ // 过去时间
+ val hours = diffMillis / (1000 * 60 * 60)
+ return when {
+ hours < 1 -> "刚刚"
+ hours < 24 -> "${hours}小时前"
+// hours < 48 -> "昨天"
+// hours < 72 -> "前天"
+ else -> {
+ val days = hours / 24
+ "${days}天前"
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/utils/FragmentHelper.kt b/app/src/main/java/com/sw/platecabinet/utils/FragmentHelper.kt
new file mode 100644
index 0000000..b0f81f1
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/utils/FragmentHelper.kt
@@ -0,0 +1,141 @@
+package com.sw.platecabinet.utils
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+
+class FragmentHelper(
+ private val fragmentManager: FragmentManager,
+ private val containerId: Int
+) {
+
+ /**
+ * 添加Fragment到容器中
+ * @param fragment 要添加的Fragment
+ * @param addToBackStack 是否添加到返回栈
+ * @param tag Fragment的标签
+ */
+ fun addFragment(fragment: Fragment, addToBackStack: Boolean = false, tag: String? = null) {
+ fragmentManager.beginTransaction().apply {
+ add(containerId, fragment, tag)
+ if (addToBackStack) {
+ addToBackStack(tag)
+ }
+ commit()
+ }
+ }
+
+ /**
+ * 替换容器中的Fragment
+ * @param fragment 要替换的Fragment
+ * @param addToBackStack 是否添加到返回栈
+ * @param tag Fragment的标签
+ * @param animEnter 进入动画
+ * @param animExit 退出动画
+ * @param animPopEnter 弹出进入动画
+ * @param animPopExit 弹出退出动画
+ */
+ fun replaceFragment(
+ fragment: Fragment,
+ addToBackStack: Boolean = true,
+ tag: String? = null,
+ animEnter: Int = 0,
+ animExit: Int = 0,
+ animPopEnter: Int = 0,
+ animPopExit: Int = 0
+ ) {
+ fragmentManager.beginTransaction().apply {
+ if (animEnter != 0 || animExit != 0 || animPopEnter != 0 || animPopExit != 0) {
+ setCustomAnimations(animEnter, animExit, animPopEnter, animPopExit)
+ }
+ replace(containerId, fragment, tag)
+ if (addToBackStack) {
+ addToBackStack(tag)
+ }
+ commit()
+ }
+ }
+
+ /**
+ * 显示Fragment
+ * @param fragment 要显示的Fragment
+ */
+ fun showFragment(fragment: Fragment) {
+ fragmentManager.beginTransaction()
+ .show(fragment)
+ .commit()
+ }
+
+ /**
+ * 隐藏Fragment
+ * @param fragment 要隐藏的Fragment
+ */
+ fun hideFragment(fragment: Fragment) {
+ fragmentManager.beginTransaction()
+ .hide(fragment)
+ .commit()
+ }
+
+ /**
+ * 移除Fragment
+ * @param fragment 要移除的Fragment
+ */
+ fun removeFragment(fragment: Fragment) {
+ fragmentManager.beginTransaction()
+ .remove(fragment)
+ .commit()
+ }
+
+ /**
+ * 返回上一个Fragment
+ * @return 是否成功返回
+ */
+ fun popBackStack(): Boolean {
+ return fragmentManager.popBackStackImmediate()
+ }
+
+ /**
+ * 返回到指定的Fragment
+ * @param tag Fragment的标签
+ * @param inclusive 是否包含指定的Fragment
+ */
+ fun popBackStackTo(tag: String, inclusive: Boolean = false): Boolean {
+ return fragmentManager.popBackStackImmediate(
+ tag,
+ if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
+ )
+ }
+
+ /**
+ * 获取当前显示的Fragment
+ */
+ fun getCurrentFragment(): Fragment? {
+ return fragmentManager.findFragmentById(containerId)
+ }
+
+ /**
+ * 通过tag查找Fragment
+ */
+ fun findFragmentByTag(tag: String): Fragment? {
+ return fragmentManager.findFragmentByTag(tag)
+ }
+
+ /**
+ * 清除所有Fragment
+ */
+ fun clearAllFragments() {
+ // 清除所有回退栈中的Fragment
+ fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+
+ // 清除所有已添加的Fragment
+ fragmentManager.fragments.forEach { fragment ->
+ removeFragment(fragment)
+ }
+ }
+
+ /**
+ * 检查返回栈是否为空
+ */
+ fun isBackStackEmpty(): Boolean {
+ return fragmentManager.backStackEntryCount == 0
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/utils/PermissionHelper.kt b/app/src/main/java/com/sw/platecabinet/utils/PermissionHelper.kt
new file mode 100644
index 0000000..59d957e
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/utils/PermissionHelper.kt
@@ -0,0 +1,156 @@
+package com.sw.platecabinet.utils
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+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,
+ private val requestCode: Int,
+ private val rationale: String? = null
+) {
+ private var onGranted: (() -> Unit)? = null
+
+ companion object {
+ /**
+ * 创建权限请求构建器
+ * @param context 上下文(Activity或Fragment)
+ * @param permissions 需要请求的权限数组
+ * @param requestCode 请求码
+ */
+ fun with(context: Context, permissions: Array, requestCode: Int): Builder {
+ return Builder(context, permissions, requestCode)
+ }
+
+ /**
+ * 检查是否已授予所有权限
+ */
+ fun areAllPermissionsGranted(context: Context, permissions: Array): Boolean {
+ return permissions.all { permission ->
+ ContextCompat.checkSelfPermission(
+ context,
+ permission
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+ }
+
+ /**
+ * 处理权限请求结果(供BaseActivity/BaseFragment调用)
+ */
+ fun handlePermissionResult(
+ context: Context,
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ onPermissionPermanentlyDenied: (List) -> Unit
+ ): Boolean {
+ if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
+ return true
+ }
+
+ val permanentlyDeniedPermissions = mutableListOf()
+ permissions.forEachIndexed { index, permission ->
+ if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
+ if (!ActivityCompat.shouldShowRequestPermissionRationale(
+ context as Activity,
+ permission
+ )
+ ) {
+ permanentlyDeniedPermissions.add(permission)
+ }
+ }
+ }
+
+ if (permanentlyDeniedPermissions.isNotEmpty()) {
+ onPermissionPermanentlyDenied(permanentlyDeniedPermissions)
+ }
+ return false
+ }
+
+ /**
+ * 跳转到应用设置页面
+ */
+ fun openAppSettings(context: Context) {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", context.packageName, null)
+ }
+ if (context is Activity) {
+ context.startActivity(intent)
+ } else if (context is Fragment) {
+ context.startActivity(intent)
+ }
+ }
+ }
+
+ class Builder(
+ private val context: Context,
+ private val permissions: Array,
+ private val requestCode: Int
+ ) {
+ private var rationale: String? = null
+
+ /**
+ * 设置权限说明(当需要向用户解释权限用途时显示)
+ */
+ fun setRationale(rationale: String): Builder {
+ this.rationale = rationale
+ return this
+ }
+
+ /**
+ * 设置权限授予回调
+ */
+ fun onGranted(callback: () -> Unit): PermissionHelper {
+ val helper = PermissionHelper(context, permissions, requestCode, rationale)
+ helper.onGranted = callback
+ return helper
+ }
+ }
+
+ /**
+ * 检查并请求权限
+ */
+ fun checkAndRequest() {
+ if (areAllPermissionsGranted(context, permissions)) {
+ onGranted?.invoke()
+ } else {
+ requestPermissions()
+ }
+ }
+
+ 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)
+ .setPositiveButton("确定") { _, _ ->
+ doRequestPermissions(activity)
+ }
+ .setNegativeButton("取消", null)
+ .show()
+ } else {
+ // 直接请求权限
+ doRequestPermissions(activity)
+ }
+ }
+
+ private fun doRequestPermissions(activity: Activity) {
+ ActivityCompat.requestPermissions(activity, permissions, requestCode)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/view/CustomDialog.java b/app/src/main/java/com/sw/platecabinet/view/CustomDialog.java
new file mode 100644
index 0000000..a61fee2
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/view/CustomDialog.java
@@ -0,0 +1,88 @@
+package com.sw.platecabinet.view;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+public class CustomDialog extends Dialog {
+
+
+ /**
+ * 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
+ */
+ public CustomDialog(Context context, View layout, int style) {
+
+ super(context, style);
+
+ setContentView(layout);
+
+ Window window = getWindow();
+
+ WindowManager.LayoutParams params = window.getAttributes();
+
+ params.gravity = Gravity.CENTER;
+
+ window.setAttributes(params);
+
+ }
+
+ /**
+ * 宽高由该方法的参数设置
+ */
+ public CustomDialog(Context context, int width, int height, View layout,
+ int style) {
+ super(context, style);
+ // 设置内容
+ setContentView(layout);
+ // 设置窗口属性
+ Window window = getWindow();
+ WindowManager.LayoutParams params = window.getAttributes();
+ // 设置宽度、高度、密度、对齐方式
+ float density = getDensity(context);
+ params.width = (int) (width * density);
+ params.height = (int) (height * density);
+ params.gravity = Gravity.CENTER;
+ window.setAttributes(params);
+
+ }
+
+ /**
+ * 获取显示密度
+ *
+ * @param context
+ * @return
+ */
+ public float getDensity(Context context) {
+ Resources res = context.getResources();
+ DisplayMetrics dm = res.getDisplayMetrics();
+ return dm.density;
+ }
+
+ private void fullScreenImmersive(View view) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_FULLSCREEN;
+ view.setSystemUiVisibility(uiOptions);
+ }
+
+ }
+
+ @Override
+
+ public void show() {
+ this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ super.show();
+// fullScreenImmersive(getWindow().getDecorView());
+// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/BaseViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/BaseViewModel.kt
new file mode 100644
index 0000000..9051b83
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/viewmodel/BaseViewModel.kt
@@ -0,0 +1,65 @@
+package com.sw.platecabinet.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.sw.plate.utils.ToastUtils
+import com.sw.platecabinet.model.response.ApiResponse
+import com.sw.platecabinet.network.ApiClient
+import com.sw.platecabinet.repository.RemoteRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+abstract class BaseViewModel() : ViewModel() {
+ protected val repository = RemoteRepository(ApiClient.apiService)
+
+ // 显示进度条
+ private val _showLoading = MutableStateFlow(false)
+ val showLoading: StateFlow = _showLoading
+
+ /**
+ * 带进度条的请求
+ */
+ protected fun launchWithLoading(block: suspend () -> Unit) {
+ viewModelScope.launch {
+ try {
+ _showLoading.value = true
+ block()
+ } catch (e: Exception) {
+ // 错误处理可被子类重写
+ handleError(e)
+ } finally {
+ _showLoading.value = false
+ }
+ }
+ }
+
+ /**
+ * 不带进度条的请求
+ */
+ protected fun launch(block: suspend () -> Unit) {
+ viewModelScope.launch() {
+ try {
+ block()
+ } catch (e: Exception) {
+ // 错误处理可被子类重写
+ handleError(e)
+ }
+ }
+ }
+
+ protected open fun parseResponse(response: ApiResponse<*>): Boolean {
+ if (response.isSuccess()) {
+ return true
+ }
+ Timber.d("msg = ${response.msg}, code = ${response.code}")
+ ToastUtils.showToast("${response.msg}(${response.code})")
+ return false
+ }
+
+
+ fun handleError(exception: Exception) {
+ Timber.d("handleError ${exception.message}")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt
new file mode 100644
index 0000000..f3001ab
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/viewmodel/SettingViewModel.kt
@@ -0,0 +1,21 @@
+package com.sw.platecabinet.viewmodel
+
+import com.sw.platecabinet.model.request.EquipmentParam
+import com.sw.platecabinet.model.response.EquipmentUserInfo
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class SettingViewModel : BaseViewModel() {
+ // 设备绑定列表
+ private val _equipmentList = MutableStateFlow>(emptyList())
+ val equipmentList: StateFlow> = _equipmentList
+
+ fun getEquipmentList(param: EquipmentParam) {
+ launchWithLoading {
+ val response = repository.getEquipmentList(param)
+ if (parseResponse(response)) {
+ _equipmentList.value = response.data ?: emptyList()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt
new file mode 100644
index 0000000..f26f0bb
--- /dev/null
+++ b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt
@@ -0,0 +1,43 @@
+package com.sw.platecabinet.viewmodel
+
+import com.sw.inbound.utils.SPUtil
+import com.sw.platecabinet.GlobalKey
+
+class UserViewModel : BaseViewModel() {
+
+ fun generateToken(deviceId: String = "SWSN:88:12:AC:4E:D9:CC") {
+ launchWithLoading {
+ val response = repository.generateToken(deviceId)
+ if (parseResponse(response)) {
+ // 缓存token
+ SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.data)
+ // 首次运行获取人脸数据
+ if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
+ getUserFaceCache()
+ }
+ }
+ }
+ }
+
+ fun getUserFaceCache() {
+ launchWithLoading {
+ val response = repository.getUserFaceCache()
+ if (parseResponse(response)) {
+ // 获取成功一次后缓存状态
+ SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
+ }
+ }
+ }
+
+ /**
+ * 激活 arcsoft 人脸
+ */
+ fun activeEngine(
+ appId: String = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj",
+ sdkKey: String = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k",
+ activeKey: String = "085F-118G-Q391-53YL"
+ ) {
+// val runtimeABI: RuntimeABI? = FaceEngine.getRuntimeABI()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dialog_waiting.xml b/app/src/main/res/drawable/dialog_waiting.xml
new file mode 100644
index 0000000..b47a21c
--- /dev/null
+++ b/app/src/main/res/drawable/dialog_waiting.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/grid_button_bind.xml b/app/src/main/res/drawable/grid_button_bind.xml
new file mode 100644
index 0000000..52c8802
--- /dev/null
+++ b/app/src/main/res/drawable/grid_button_bind.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/grid_button_unbind.xml b/app/src/main/res/drawable/grid_button_unbind.xml
new file mode 100644
index 0000000..3d93674
--- /dev/null
+++ b/app/src/main/res/drawable/grid_button_unbind.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/grid_item_bind.xml b/app/src/main/res/drawable/grid_item_bind.xml
new file mode 100644
index 0000000..4fef8cb
--- /dev/null
+++ b/app/src/main/res/drawable/grid_item_bind.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/grid_item_unbind.xml b/app/src/main/res/drawable/grid_item_unbind.xml
new file mode 100644
index 0000000..12e0c8e
--- /dev/null
+++ b/app/src/main/res/drawable/grid_item_unbind.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/rounded_border_transparent_8c7c75.xml b/app/src/main/res/drawable/rounded_border_transparent_8c7c75.xml
new file mode 100644
index 0000000..be02efa
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_border_transparent_8c7c75.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_login_by_pwd.xml b/app/src/main/res/layout/activity_login_by_pwd.xml
index b836271..1f241f4 100644
--- a/app/src/main/res/layout/activity_login_by_pwd.xml
+++ b/app/src/main/res/layout/activity_login_by_pwd.xml
@@ -2,7 +2,7 @@
@@ -36,6 +36,7 @@
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index c9b55b2..3e22785 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,12 +1,20 @@
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_test.xml b/app/src/main/res/layout/activity_test.xml
new file mode 100644
index 0000000..053a3a4
--- /dev/null
+++ b/app/src/main/res/layout/activity_test.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_waiting.xml b/app/src/main/res/layout/dialog_waiting.xml
new file mode 100644
index 0000000..e006f4b
--- /dev/null
+++ b/app/src/main/res/layout/dialog_waiting.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_bind_plate.xml b/app/src/main/res/layout/fragment_bind_plate.xml
new file mode 100644
index 0000000..65b3842
--- /dev/null
+++ b/app/src/main/res/layout/fragment_bind_plate.xml
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_plate_cabinet_full.xml b/app/src/main/res/layout/fragment_plate_cabinet_full.xml
new file mode 100644
index 0000000..c866ce5
--- /dev/null
+++ b/app/src/main/res/layout/fragment_plate_cabinet_full.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_plate_open.xml b/app/src/main/res/layout/fragment_plate_open.xml
new file mode 100644
index 0000000..c1cd2ac
--- /dev/null
+++ b/app/src/main/res/layout/fragment_plate_open.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_setting_list.xml b/app/src/main/res/layout/fragment_setting_list.xml
new file mode 100644
index 0000000..330c8a8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_setting_list.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_unbind_plate.xml b/app/src/main/res/layout/fragment_unbind_plate.xml
new file mode 100644
index 0000000..acc8b52
--- /dev/null
+++ b/app/src/main/res/layout/fragment_unbind_plate.xml
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_bind_view.xml b/app/src/main/res/layout/item_bind_view.xml
new file mode 100644
index 0000000..a26a56f
--- /dev/null
+++ b/app/src/main/res/layout/item_bind_view.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_search_user_info.xml b/app/src/main/res/layout/item_search_user_info.xml
new file mode 100644
index 0000000..f8be2b9
--- /dev/null
+++ b/app/src/main/res/layout/item_search_user_info.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_unbind_view.xml b/app/src/main/res/layout/item_unbind_view.xml
new file mode 100644
index 0000000..0cb559e
--- /dev/null
+++ b/app/src/main/res/layout/item_unbind_view.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/page_item_layout.xml b/app/src/main/res/layout/page_item_layout.xml
new file mode 100644
index 0000000..ebc08f1
--- /dev/null
+++ b/app/src/main/res/layout/page_item_layout.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/app/src/main/res/mipmap-xxhdpi/bg.png b/app/src/main/res/mipmap-xxhdpi/bg.png
deleted file mode 100644
index 5979fa6..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/bg.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_bind_img.png b/app/src/main/res/mipmap-xxhdpi/ic_bind_img.png
new file mode 100644
index 0000000..84d8bbc
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_bind_img.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_full.png b/app/src/main/res/mipmap-xxhdpi/ic_full.png
new file mode 100644
index 0000000..05ad2d7
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_full.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_plate_open.png b/app/src/main/res/mipmap-xxhdpi/ic_plate_open.png
new file mode 100644
index 0000000..9f4d6b4
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_plate_open.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_unbind_img.png b/app/src/main/res/mipmap-xxhdpi/ic_unbind_img.png
new file mode 100644
index 0000000..0e6e594
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_unbind_img.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index d4108e0..166feb8 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,11 @@
#FF000000
#FFFFFFFF
#00000000
+ #FF03DAC5
+ #4E535D
+ #585868
+ #FF9632
+ #F0C8B4
+ #F0C8B4
+ #32283C
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f99fe97..1a6924e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,5 @@
SmartPlateCabinet
+
+ Hello blank fragment
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 6634bd2..54b043e 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,9 +1,25 @@
-
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 58c4a55..16128de 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,7 @@
[versions]
+activityKtx = "1.9.0"
agp = "8.10.1"
+fragmentKtx = "1.5.6"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
@@ -22,7 +24,9 @@ gson = "2.13.1"
hiltAndroid = "2.56.2"
[libraries]
+androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -34,7 +38,6 @@ material = { group = "com.google.android.material", name = "material", version.r
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
-accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" }
androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCore" }
diff --git a/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java b/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java
new file mode 100644
index 0000000..1708f9e
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java
@@ -0,0 +1,167 @@
+package com.sw.plate.utils;
+
+import static android.content.Context.INPUT_SERVICE;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+
+/**
+ * 扫描枪事件处理
+ */
+public class ScanGunKeyEventHelper {
+ private final static long MESSAGE_DELAY = 500; //延迟500ms,判断扫码是否完成。
+ private final StringBuffer mStringBufferResult; //扫码内容
+ private boolean mCaps; //大小写区分
+ private final Handler mHandler;
+ private final Runnable mScanningFishedRunnable;
+ private OnScanSuccessListener mOnScanSuccessListener;
+ private final Context mContext;
+
+ // private String mDeviceName = "TMC HIDKeyBoard";
+ private String mDeviceName = "Linux 3.4.35 with ak-hsudc Composite Gadget (ACM + HID)";
+
+ public ScanGunKeyEventHelper(Context context, OnScanSuccessListener onScanSuccessListener) {
+
+ mContext = context;
+ mOnScanSuccessListener = onScanSuccessListener;
+ mStringBufferResult = new StringBuffer();
+ mHandler = new Handler();
+ mScanningFishedRunnable = this::performScanSuccess;
+ }
+
+ /**
+ * 返回扫码成功后的结果
+ */
+ private void performScanSuccess() {
+ String barcode = mStringBufferResult.toString();
+ if (mOnScanSuccessListener != null && !TextUtils.isEmpty(barcode))
+ mOnScanSuccessListener.onScanSuccess(barcode);
+ mStringBufferResult.setLength(0);
+ }
+
+ /**
+ * 扫码枪事件解析
+ *
+ * @param event
+ */
+ public void analysisKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ //字母大小写判断
+ checkLetterStatus(event);
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ char aChar = getInputCode(event);
+ if (aChar != 0) {
+ mStringBufferResult.append(aChar);
+ }
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ //若为回车键,直接返回
+ mHandler.removeCallbacks(mScanningFishedRunnable);
+ mHandler.post(mScanningFishedRunnable);
+ } else {
+ //延迟post,若500ms内,有其他事件
+ mHandler.removeCallbacks(mScanningFishedRunnable);
+ mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);
+ }
+
+ }
+ }
+
+ //检查shift键
+ private void checkLetterStatus(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ //按着shift键,表示大写
+ mCaps = true;
+ } else {
+ //松开shift键,表示小写
+ mCaps = false;
+ }
+ }
+ }
+
+ /**
+ * 获取扫描内容
+ *
+ * @param event
+ * @return
+ */
+ private char getInputCode(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ char aChar;
+ if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
+ //字母
+ aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A);
+ } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
+ //数字
+ aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
+ } else {
+ //其他符号
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_PERIOD:
+ aChar = '.';
+ break;
+ case KeyEvent.KEYCODE_MINUS:
+ aChar = mCaps ? '_' : '-';
+ break;
+ case KeyEvent.KEYCODE_SLASH:
+ aChar = '/';
+ break;
+ case KeyEvent.KEYCODE_BACKSLASH:
+ aChar = mCaps ? '|' : '\\';
+ break;
+ default:
+ aChar = 0;
+ break;
+ }
+ }
+ return aChar;
+ }
+
+ public interface OnScanSuccessListener {
+ void onScanSuccess(String barcode);
+ }
+
+ public void onDestroy() {
+ mHandler.removeCallbacks(mScanningFishedRunnable);
+ mOnScanSuccessListener = null;
+ }
+
+ /**
+ * 输入设备是否存在
+ *
+ * @param deviceName
+ * @return
+ */
+ public boolean isInputDeviceExist(String deviceName) {
+
+ InputManager inputManager = (InputManager) mContext.getSystemService(INPUT_SERVICE);
+ int[] deviceIds = inputManager.getInputDeviceIds();
+ for (int id : deviceIds) {
+ if (inputManager.getInputDevice(id).getName().equals(deviceName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 是否为扫码枪事件(部分机型KeyEvent获取的名字错误)
+ *
+ * @param event
+ * @return
+ */
+ public boolean isScanGunEvent(KeyEvent event) {
+// L.e("event===" + event.getDevice().getName() +
+// "===Char===" + event.getCharacters() +
+// "===Action===" + event.getAction());
+// return event.getDevice().getName().equals(mDeviceName);
+
+ return true;
+ }
+
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java
new file mode 100644
index 0000000..b9d7adf
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java
@@ -0,0 +1,105 @@
+package com.sw.plate.utils;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.sw.plate.App;
+import com.sw.plate.R;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+
+/**
+ * The type Toast utils.
+ */
+public class ToastUtils {
+
+ private static Toast toast;
+ @SuppressLint("StaticFieldLeak")
+ private static TextView textCenterView;
+
+ /**
+ * Show center toast.
+ *
+ * @param text the text
+ */
+ public static void showToast(String text) {
+ Context context = App.getContext();
+ if (toast == null) {
+ View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+ textCenterView = view.findViewById(R.id.toast_tv);
+ toast = new Toast(context);
+ toast.setGravity(Gravity.CENTER, 0, 20);
+ toast.setDuration(Toast.LENGTH_SHORT);
+ toast.setView(view);
+ }
+
+ textCenterView.setText(text);
+ toast.show();
+ }
+
+ /**
+ * Show center toast.
+ *
+ * @param text the text
+ */
+ public static void showToast(String text, int duration) {
+ Context context = App.getContext();
+ if (toast == null) {
+ View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+ textCenterView = view.findViewById(R.id.toast_tv);
+ toast = new Toast(context);
+ toast.setGravity(Gravity.CENTER, 0, 20);
+ toast.setView(view);
+ }
+
+ textCenterView.setText(text);
+ toast.setDuration(duration);
+ toast.show();
+ }
+
+ public static void showLongToast(String text) {
+ Context context = App.getContext();
+ if (toast == null) {
+ View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
+ textCenterView = view.findViewById(R.id.toast_tv);
+ toast = new Toast(context);
+ toast.setGravity(Gravity.TOP, 0, 20);
+ toast.setDuration(Toast.LENGTH_LONG);
+ toast.setView(view);
+ }
+ textCenterView.setText(text);
+ showMyToast(toast, 1000000 * 30);
+ }
+
+
+ //自定义Toast控件
+ private static void showMyToast(final Toast toast, final int cnt) {
+ final Timer timer = new Timer();
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ toast.show();
+ }
+ }, 0, Toast.LENGTH_LONG);
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ toast.cancel();
+ timer.cancel();
+ }
+ }, cnt);
+ }
+
+ public static void showSystemToast(String text) {
+ Context context = App.getContext();
+ Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
+ }
+
+}
diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java
new file mode 100644
index 0000000..cc1f68b
--- /dev/null
+++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java
@@ -0,0 +1,79 @@
+package com.sw.plate.utils.arcface;
+
+import android.content.Context;
+
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.enums.RuntimeABI;
+import com.sw.plate.App;
+import com.sw.plate.utils.L;
+import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
+import com.sw.plate.utils.arcface.facedb.FaceDatabase;
+import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FaceApi {
+ public interface ActiveCallback {
+ void onSuccess(int code);
+
+ void onFail(Exception e);
+ }
+
+ /**
+ * 更新人脸数据
+ *
+ * @param index
+ * @param list
+ */
+ public void updateFaceData(int index, List list) {
+ if (index == 0) {
+ FaceDatabase.getInstance(App.getContext()).faceDao().deleteAll();
+ FaceDatabase.getInstance(App.getContext()).faceDao().resetId();
+ }
+
+ List faceEntities = new ArrayList<>();
+ FaceDatabase.getInstance(App.getContext()).faceDao().insert(faceEntities);
+ }
+
+ /**
+ * 激活arcsoft 人脸
+ *
+ * @param context
+ * @param arcsoftAppId
+ * @param arcsoftSdkKey
+ * @param arcsoftActiveKey
+ * @param callback
+ */
+ public void activeEngine(Context context, String arcsoftAppId,
+ String arcsoftSdkKey,
+ String arcsoftActiveKey, ActiveCallback callback) {
+ new Thread(() -> {
+ try {
+ RuntimeABI runtimeABI = FaceEngine.getRuntimeABI();
+ L.e("subscribe: getRuntimeABI() " + runtimeABI);
+
+ long start = System.currentTimeMillis();
+ int activeCode = FaceEngine.activeOnline(context, arcsoftActiveKey,
+ arcsoftAppId, arcsoftSdkKey);
+ L.e("subscribe cost: " + (System.currentTimeMillis() - start));
+ callback.onSuccess(activeCode);
+ } catch (Exception e) {
+ callback.onFail(e);
+ }
+ }).start();
+ }
+
+ /**
+ * 传入可见光相机预览数据
+ *
+ * @param nv21 可见光相机预览数据
+ * @param doRecognize 是否进行识别
+ * @return 当前帧的检测结果信息
+ */
+ public List onPreviewFrame(byte[] nv21, boolean doRecognize) {
+
+// List facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true);
+ return null;
+ }
+}
diff --git a/lib_face/src/main/res/layout/toast_bg.xml b/lib_face/src/main/res/layout/toast_bg.xml
new file mode 100644
index 0000000..e3ba3f5
--- /dev/null
+++ b/lib_face/src/main/res/layout/toast_bg.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib_face/src/main/res/values/colors.xml b/lib_face/src/main/res/values/colors.xml
index 5b95e02..706e518 100644
--- a/lib_face/src/main/res/values/colors.xml
+++ b/lib_face/src/main/res/values/colors.xml
@@ -1,4 +1,5 @@
#80000000
+ #000000
\ No newline at end of file