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