Author SHA1 Message Date
马增飞 582be4e9b7 绑定解绑功能调试;其它优化 2026-01-05 18:38:10 +08:00
马增飞 17d47a876f 增加配置接口;调试人脸识别;验证手机号密码登录和人脸登录功能; 2025-12-29 18:41:48 +08:00
马增飞 6c28260069 Merge remote-tracking branch 'refs/remotes/origin/main_新版_lvmeng_1222' into main_新版_lvmeng_1229 2025-12-29 09:57:05 +08:00
马增飞 7097bbf907 优化 2025-12-29 09:55:49 +08:00
lvmeng c046441d4e 优化 2025-12-26 14:16:01 +08:00
lvmeng 7a6c4012d1 餐盘柜接口改造 2025-12-26 14:05:41 +08:00
mazengfei 5e4dc4baa4 处理人脸数据不同步 2025-11-07 20:37:50 +08:00
mazengfei 4231c68cdc 添加方法 2025-11-07 15:18:15 +08:00
mazengfei 55ba11bf34 修改人脸同步逻辑 2025-11-07 10:55:48 +08:00
马增飞 abe594fb66 设备联调 2025-11-06 19:34:42 +08:00
lvmeng fb074c1aaa 修改开机初始化页面 2025-09-26 16:11:00 +08:00
lvmeng 73456046c5 增加初始化页面 2025-09-17 17:42:38 +08:00
lvmeng 7f56d4d1a1 修改待机也按钮名称 2025-09-17 15:45:00 +08:00
lvmeng 1658b6fe6f logo修改 2025-09-12 13:32:10 +08:00
lvmeng e9a366b52a 增加心跳处理人脸数据增减 2025-09-09 15:59:29 +08:00
lvmeng 3b492cb710 待机页面优化 2025-09-05 17:59:30 +08:00
lvmeng eaafeab1d8 增加余额不足判断 2025-09-05 15:29:43 +08:00
zxj 578a9aaced 添加注释 2025-08-22 16:28:20 +08:00
zxj cd375bfaf8 修复了登录成功后跳转界面错误问题 2025-08-21 15:50:05 +08:00
zxj 7a832ec40d 调整了初始化界面逻辑 2025-08-14 13:36:45 +08:00
zxj b83a442fe3 修改了配色,添加了解除绑定提示 2025-08-08 17:26:09 +08:00
zxj 2c10731b8d 修复了识别追踪信息功能 2025-08-08 10:50:11 +08:00
zxj 47f6e814a6 添加init界面,调整了认证后逻辑,调整了扫描枪扫描处理 2025-08-01 18:23:54 +08:00
zxj d20ee771b0 排序放入协程 2025-07-28 18:25:23 +08:00
zxj 6f18245c4f 密码登录后关闭密码界面 2025-07-28 18:05:06 +08:00
zxj 9a89fff9dd 工具类优化 2025-07-28 17:35:21 +08:00
zxj cc317117bc 修复了排序问题 2025-07-28 17:09:24 +08:00
zxj ca081f659b 初步实现功能 2025-07-28 15:47:55 +08:00
zxj 307fc70ae2 实现了垂直排序 2025-07-28 10:50:15 +08:00
zxj 8780f5d463 倒计时+1 2025-07-25 16:33:50 +08:00
zxj c0a628c51f 优化了键盘操作 2025-07-25 15:58:11 +08:00
zxj c2bae2f3e8 调整了餐盘柜3种情况处理 2025-07-25 13:35:17 +08:00
zxj c89efd2d6f 完善了功能 2025-07-25 10:36:30 +08:00
zxj 5b3773d277 调整了人脸识别界面 2025-07-24 18:02:42 +08:00
zxj 082011a939 调整了餐盘在不同设备提示 2025-07-24 08:40:36 +08:00
zxj 7fd2ccb960 调整了部分问题 2025-07-23 14:22:11 +08:00
zxj 5ad82eb224 添加了界面及逻辑 2025-07-22 15:48:12 +08:00
134 changed files with 5579 additions and 689 deletions
Generated
+1
View File
@@ -0,0 +1 @@
SmartPlateCabinet
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
+9 -1
View File
@@ -2,8 +2,16 @@
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="SmartPlateCabinet.app">
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-06T09:21:30.965681400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=?;connection=3c39678a" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
+1 -1
View File
@@ -6,7 +6,7 @@
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="17" />
<option name="gradleJvm" value="azul-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
+1 -2
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+12 -5
View File
@@ -22,14 +22,14 @@ android {
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// ndk {
// abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
// }
setProperty("archivesBaseName", "智能餐盘柜_${versionName}")
ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
setProperty("archivesBaseName", "zncpg_${versionName}")
}
buildTypes {
@@ -93,4 +93,11 @@ dependencies {
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
// implementation(libs.adapter.coroutines)
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation(libs.core)
implementation(libs.android.core)
implementation("org.greenrobot:eventbus:3.3.1")
}
+33 -12
View File
@@ -8,21 +8,45 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/ic_logo512"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/ic_logo512"
android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".activity.TestActivity"
android:exported="true"></activity>
android:name=".activity.UnBindDialogActivity"
android:exported="false"
android:theme="@style/DialogActivity" />
<activity
android:name=".activity.InitActivity"
android:exported="false"
android:launchMode="singleTask">
</activity>
<activity
android:name=".activity.LoginByPwdActivity"
android:exported="false"
@@ -30,6 +54,10 @@
<activity
android:name=".activity.LoginByFaceActivity"
android:exported="true"
android:launchMode="singleTask"/>
<activity
android:name=".activity.DeviceInitActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -39,14 +67,7 @@
</activity>
<activity
android:name=".activity.MainActivity"
android:exported="true">
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.MAIN" /> -->
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
<!-- </intent-filter> -->
android:exported="false">
</activity>
</application>
@@ -1,14 +1,64 @@
package com.sw.platecabinet
object GlobalData {
/**
* 同一设备全局使用的设备编号
*/
// var globalEquipmentCode: String = "202501171634"
var globalEquipmentCode: String = "202507231144"
/**
* app版本号
*/
var appVersion: String = ""
/**
* 横排数量
*/
var arrayCross: Int = 2
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
var arrayMode: Int = 0
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey ="085F-118G-Q3J6-35UX" //"085F-118G-Q391-53YL"
/**
* 具体业务baseurl
*/
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 设备id
*/
var deviceId: String = ""
var restId: String = ""
}
/**
*
* 全局常量
*/
object Constants {
/**
* 自动关闭时间
*/
const val AUTO_CLOSE_TIME: Long = 3
}
/**
* key
*/
object GlobalKey {
const val KEY_EQUIPMENT_INFO = "equipmentInfo"
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
}
@@ -1,6 +1,9 @@
package com.sw.platecabinet
import android.util.Log
import com.sw.plate.App
import com.sw.plate.utils.AppUtil
import com.sw.platecabinet.utils.CrashHandler
import timber.log.Timber
class MyApp : App() {
@@ -12,5 +15,24 @@ class MyApp : App() {
super.onCreate()
Timber.plant(Timber.DebugTree())
Timber.d("初始化")
initGlobalData()
// 初始化崩溃处理器
CrashHandler.init(this)
}
/**
* 初始化全局数据
*/
private fun initGlobalData() {
var deviceId = AppUtil.getUDID(this)
deviceId = "be154831-3466-3ba2-a2ea-57652c919fed"
Log.d("MyApp", "initialize: deviceId=$deviceId")
GlobalData.deviceId = deviceId
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
GlobalData.globalEquipmentCode = "202501171634"
// GlobalData.appBaseUrl = "http://192.168.1.201:14801"
}
}
@@ -2,19 +2,41 @@ package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ScanGunKeyEventHelper
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.facedb.FaceDatabase
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.ext.setClickListeners
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.IntervalExecutor
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.view.CustomDialog
import com.sw.platecabinet.utils.SpTool
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -29,55 +51,189 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
private var headerBinding: ItemTitleTimeBinding? = null
protected lateinit var context: Context
private var timeJob: Job? = null
private var mDialogWaiting: CustomDialog? = null
private var mDialogWaiting: CustomLoadingDialog? = null
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
protected var keyEventHelper: ScanGunKeyEventHelper? = null
// 管理员对应的viewmodel
private val settingViewModel by viewModels<SettingViewModel>()
// 用户对应的viewModel
protected val viewModel by viewModels<UserViewModel>()
private var startTime: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
startTime = System.currentTimeMillis()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
context = this
disableSystemUICompletely()
// 确保内容延伸到导航栏区域
WindowCompat.setDecorFitsSystemWindows(window, false)
//保持亮屏
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = inflateViewBinding()
headerBinding = inflateTitleBinding()
setContentView(binding.root)
updateTime()
registerDataChange()
initialize()
registerKeyEvent()
val durationTime = System.currentTimeMillis() - startTime
Timber.d("启动时间:$durationTime")
startFaceTask()
}
/**
* 监听扫描枪扫描事件
*/
protected fun registerKeyEvent() {
keyEventHelper =
ScanGunKeyEventHelper(context, object : ScanGunKeyEventHelper.OnScanSuccessListener {
override fun onScanSuccess(barcode: String?) {
Timber.d("onScanSuccess barcode = $barcode")
if (barcode == null) return
handleScanKeyInfo(barcode)
}
})
}
/**
* 处理扫描枪数据
*/
protected open fun handleScanKeyInfo(scanInfo: String) {
settingViewModel.findByPlateNumber(plateNumber = scanInfo)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (keyEventHelper != null) {
if (keyEventHelper!!.isScanGunEvent(event)) {
keyEventHelper!!.analysisKeyEvent(event)
return true
}
}
return super.dispatchKeyEvent(event)
}
fun updateTime() {
headerBinding?.let {
headerBinding!!.tvRightTime.setClickListeners(
it.tvLeftDate.setClickListeners(
onDoubleClick = {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
onLeftDoubleClick()
}
)
it.tvRightTime.setClickListeners(
onDoubleClick = {
onRightDoubleClick()
}
)
val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow()
.collect { (date, time) ->
headerBinding!!.tvLeftDate.text = date
headerBinding!!.tvRightTime.text = time
it.tvLeftDate.text = date
it.tvRightTime.text = time
}
}
}
}
/**
* 左侧日期双击
*/
open fun onLeftDoubleClick() {
}
/**
* 右侧时间双击
*/
open fun onRightDoubleClick() {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// 统一处理权限拒绝情况
val helper = permissionHelpers[requestCode]
val allGranted = PermissionHelper.handlePermissionResult(
this,
requestCode,
permissions,
grantResults
) { permanentlyDeniedPermissions ->
// 权限被永久拒绝的统一处理
Timber.e("用户永久拒绝权限 $permanentlyDeniedPermissions")
grantResults,
permissionHelper = helper
)
if (allGranted) {
permissionHelpers.remove(requestCode)
}
}
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
override fun onResume() {
super.onResume()
// 关键点2:防止某些场景下系统栏恢复
enforceImmersiveMode()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
disableSystemUICompletely()
}
}
private fun disableSystemUICompletely() {
Timber.d("disableSystemUICompletely")
// 禁用系统手势(Android 10+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
// 全屏+隐藏导航栏(所有版本通用)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
// 禁止窗口扩展至系统栏区域(彻底锁定)
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
private fun enforceImmersiveMode() {
Timber.d("enforceImmersiveMode")
// 持续强制隐藏系统栏(防止手势触发)
window.decorView.postDelayed({
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}, 100) // 延迟100ms确保覆盖手势触发
}
/**
* 显示等待提示框
*/
@@ -85,7 +241,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).setText(tip)
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting = CustomLoadingDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
@@ -105,8 +261,213 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected abstract fun initialize()
/**
* 注册数据监听
*/
protected open fun registerDataChange() {
Timber.d("registerDataChange")
lifecycleScope.launch {
viewModel.showLoading.collect {
Timber.d("registerDataChange 用户 showLoading = $it")
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
lifecycleScope.launch {
viewModel.currentUserInfo.collect {
if (it == null) return@collect
it.showBalanceNotEnoughDialog = true
handleLoginSuccess(it, false)
}
}
lifecycleScope.launch {
settingViewModel.showLoading.collect {
Timber.d("registerDataChange 管理员 showLoading = $it")
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
lifecycleScope.launch {
settingViewModel.searchUserInfo.collect {
Timber.d("registerDataChange 管理员 currentUserInfo = $it")
if (it == null) return@collect
handleLoginSuccess(it, true)
}
}
// lifecycleScope.launch {
// settingViewModel.equipmentList.collect {
// loadEquipmentList(it)
// }
// }
}
private fun loadEquipmentList(items: List<EquipmentUserInfo>) {
if (items.isEmpty()) return
val unbindList = items.filter { !it.isBound() }
if (unbindList.isEmpty()) {
MainActivity.start(context = context, pageType = PageType.PLATE_CABINET_FULL)
} else {
val firstInfo = unbindList[0]
val currentUserInfo = settingViewModel.searchUserInfo.value
firstInfo.plateNumber = currentUserInfo?.plateNumber ?: ""
MainActivity.start(
context = context,
pageType = PageType.BIND_PLATE,
equipmentUserInfo = firstInfo
)
}
}
/**
* 处理登录成功操作
* @param isAdmin true 管理员 执行绑盘此操作 false 用户,提示错误
*/
open fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
Timber.d("handleLoginSuccess isAdmin = $isAdmin")
equipmentUserInfo.isIntercept = false
val cardBalance = equipmentUserInfo.cardBalance ?: 0.toDouble()
val balanceIsNotEnough = cardBalance <= 0.toDouble()
if (balanceIsNotEnough && equipmentUserInfo.showBalanceNotEnoughDialog) {
//提示余额不足弹窗
equipmentUserInfo.isIntercept = true
BalanceNotEnoughDialog(this).show()
return
}
viewModel.resetUserInfo()
// if (equipmentUserInfo.isOtherEquipment()) {
// MainActivity.start(
// context,
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = equipmentUserInfo,
// isAdmin
// )
// return
// }
if (equipmentUserInfo.equipmentBoxCode?.isNotEmpty() == true) {
SerialApi.openPlate(
equipmentUserInfo.equipmentBoxCode!!.toInt(),
object : SerialPortManager.SendCallback {
override fun onSuccess() {
MainActivity.start(
context,
pageType = PageType.PLATE_TIP,
isAdmin = isAdmin
)
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("柜门打开失败")
}
})
} else {
if (isAdmin) {
settingViewModel.getEquipmentList {
loadEquipmentList(it)
}
} else {
Timber.d("无餐盘信息")
MainActivity.start(
context,
pageType = PageType.PLATE_TIP,
equipmentUserInfo = equipmentUserInfo,
isAdmin = isAdmin
)
}
}
}
override fun onDestroy() {
timeJob?.cancel()
keyEventHelper?.onDestroy()
super.onDestroy()
}
private val intervalExecutor by lazy { IntervalExecutor() }
private var faceTaskJob: Job? = null
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 1 * 60 * 1000L
private val dealyMillis = 1 * 60 * 1000L
private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
getFaceIncrementList()
}
}
private var faceTimestamp = 0L
private fun getFaceIncrementList() {
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return
}
settingViewModel.getFaceIncrementList(
pageNo = taskPageNo,
timestamp = timestamp
) { list ->
runOnUiThread {
if (taskPageNo == 1 && list.isEmpty()) {
//未查询到增量数据
return@runOnUiThread
}
updateFaceData(list)
if (list.size >= settingViewModel.PAGE_SIZE) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
taskPageNo++
getFaceIncrementList()
return@runOnUiThread
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
}
SpTool.setLastFaceTimestamp(faceTimestamp)
}
}
}
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
val faceList = mutableListOf<FaceEntity>()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
} else {
//保存数据
val faceEntity = FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
it.userType = "1"
}
faceList.add(faceEntity)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList()
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
}
@@ -0,0 +1,91 @@
package com.sw.platecabinet.activity
import android.content.Intent
import android.util.Log
import androidx.activity.viewModels
import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.databinding.ActivityDeviceInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.gone
import com.sw.platecabinet.ext.invisible
import com.sw.platecabinet.viewmodel.SettingViewModel
/**
* 设备初始化界面
*/
class DeviceInitActivity : BaseActivity<ActivityDeviceInitBinding>() {
override fun inflateViewBinding(): ActivityDeviceInitBinding {
return ActivityDeviceInitBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return null
}
override fun initialize() {
// val isSuccess = deviceViewModel.checkEquipmentInfo()
// if (isSuccess) {
// goLoginActivity()
// return
// }
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.btnInit.setOnClickListener {
// deviceViewModel.getDeviceToken()
// }
// binding.btnInit.gone()
// binding.ivQrCode.invisible()
// binding.root.postDelayed({
// goLoginActivity()
// }, 1000)
showWaitingDialog("加载中……")
viewModel.getDeviceConfig { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
hideWaitingDialog()
ToastUtils.showToast("获取设备配置数据失败")
hideWaitingDialog()
goLoginActivity()
return@runOnUiThread
}
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
binding.root.postDelayed({
hideWaitingDialog()
goLoginActivity()
}, 500)
}
}
// goLoginActivity()
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// deviceViewModel.deviceInfoResult.collect { it ->
// if (it == true) {
// goLoginActivity()
// }
// }
// }
}
private fun goLoginActivity() {
val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent)
finish()
}
override fun onLeftDoubleClick() {
}
}
@@ -0,0 +1,42 @@
package com.sw.platecabinet.activity
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.databinding.ActivityInitBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* 待机界面
*/
class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun inflateViewBinding(): ActivityInitBinding {
return ActivityInitBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return binding.includeHeader
}
override fun initialize() {
binding.main.setOnClickListener {
finish()
}
// binding.takeButton.setOnClickListener {
//
// lifecycleScope.launch(Dispatchers.IO) {
// FaceApi().deleteByUserName("288")
// }
//
// finish()
// }
}
override fun onLeftDoubleClick() {
finish()
}
}
@@ -2,30 +2,58 @@ package com.sw.platecabinet.activity
import android.Manifest
import android.content.Intent
import android.graphics.Outline
import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.os.CountDownTimer
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewOutlineProvider
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import com.arcsoft.face.ErrorInfo
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
import com.sw.plate.utils.arcface.PreviewConfig
import com.sw.plate.utils.arcface.camera.CameraListener
import com.sw.plate.utils.arcface.camera.DualCameraHelper
import com.sw.plate.utils.arcface.face.constants.LivenessType
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.network.task.TaskManager
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.viewmodel.UserViewModel
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
/**
* 人脸识别
*/
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private val viewModel by viewModels<UserViewModel>()
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener {
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private var countDownTimer: CountDownTimer? = null
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater)
@@ -35,64 +63,386 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
return binding.includeHeader
}
companion object {
private var instance: LoginByFaceActivity? = null
fun goInitActivity() {
instance?.goInitActivity()
}
}
override fun initialize() {
// 请求权限
checkCameraPermission()
initView()
viewModel.generateToken()
instance = this
viewModel.activeEngine()
initCountTime()
initArcViewModel()
initArcView()
openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true)
//viewModel.generateToken()
// viewModel.activeEngine()
viewModel.getUserFaceCache()
binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java)
startActivity(intent)
}
// TODO: 测试请求
TaskManager.startTask()
EventBus.getDefault().register(this)
}
private fun checkCameraPermission() {
Timber.i("checkCameraPermission")
val permissionHelper =
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
Timber.d("checkCameraPermission 权限已允许")
// 权限已授予
startCamera()
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
}
.checkAndRequest()
}
private fun initView() {
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
outline.setOval(0, 0, view.width, view.height)
}
}
binding.previewView.clipToOutline = true
}
private fun startCamera() {
Timber.d("startCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 创建预览用例
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
registerPermissionHelper(permissionHelper)
permissionHelper.checkAndRequest()
}
// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
@Subscribe(threadMode = ThreadMode.MAIN)
fun getFaceEntity(insertEntity: FaceEntity) {
Timber.tag("performSync").e("-time=%s", insertEntity.registerTime)
recognizeViewModel.addFace(insertEntity)
Timber.tag("performSync over").e("-time=%s", insertEntity.registerTime)
}
try {
// 解绑所有用例
cameraProvider.unbindAll()
override fun onLeftDoubleClick() {
finish()
}
// 绑定到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
private fun initArcViewModel() {
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
)
} catch (exc: Exception) {
Timber.e(exc, "Use case binding failed")
Timber.e("ftInitCode observe = $error")
ToastUtils.showToast(error)
}
}, ContextCompat.getMainExecutor(this))
})
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(this, Observer { userId: String? ->
Timber.i("recognizeUserId observe userId = $userId")
viewModel.getUserInfoById(memberId = userId)
})
recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
Timber.i("drawRectInfoText observe info = $info")
})
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
override fun onDestroy() {
Timber.d("onDestroy")
instance = null
if (rgbCameraHelper != null) {
rgbCameraHelper!!.release()
rgbCameraHelper = null
}
recognizeViewModel.destroy()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
/**
* 调整View的宽高,使预览显示正常且采集框固定为380x380
*
* @param rgbPreview RGB预览View
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view (380x380)
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @param scale 缩放比例
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
val layoutParams = previewView.layoutParams
val measuredWidth = previewView.measuredWidth
val measuredHeight = previewView.measuredHeight
var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
if (ratio > 1) {
ratio = 1 / ratio
}
if (displayOrientation % 180 == 0) {
layoutParams.width = measuredWidth
layoutParams.height = (measuredWidth * ratio).toInt()
} else {
layoutParams.height = measuredHeight
layoutParams.width = (measuredHeight * ratio).toInt()
}
if (scale < 1f) {
val rgbParam = rgbPreview.getLayoutParams()
layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt()
} else {
layoutParams.width = (layoutParams.width * scale).toInt()
layoutParams.height = (layoutParams.height * scale).toInt()
}
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
if (layoutParams.width >= metrics.widthPixels) {
val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
if (layoutParams.height >= metrics.heightPixels) {
val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
Timber.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 0.6f
)
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
Timber.d(
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
ConfigUtil.isDrawRgbRectHorizontalMirror(
context
)
}, isDrawRgbRectVerticalMirror = ${
ConfigUtil.isDrawRgbRectVerticalMirror(
context
)
}"
)
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
layoutParams.width,
layoutParams.height,
90,
cameraId,
isMirror,
true,
true
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
if (!isRecognition) {
return
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Timber.i("initRgbCamera onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.i("initRgbCamera onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
}
}
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewRgb.measuredWidth,
binding.dualCameraTexturePreviewRgb.measuredHeight
)
)
.rotation(windowManager.defaultDisplay.rotation)
.additionalRotation(0) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper!!.init()
rgbCameraHelper!!.start()
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
}
fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
}
override fun onGlobalLayout() {
Timber.d("onGlobalLayout")
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
// 请求权限
checkCameraPermission()
}
private fun resumeCamera() {
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
}
}
override fun onResume() {
super.onResume()
resumeCamera()
viewModel.resetUserInfo()
countDownTimer?.let {
it.cancel()
it.start()
}
}
protected override fun onPause() {
pauseCamera()
super.onPause()
countDownTimer?.cancel()
}
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// goInitActivity()
}
fun initCountTime() {
val totalTimeInMillis = 30L * 1000
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
Timber.d("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}")
binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}"
}
override fun onFinish() {
goInitActivity()
}
}.start()
}
fun goInitActivity() {
val intent = Intent(context, InitActivity::class.java)
startActivity(intent)
}
}
@@ -1,10 +1,19 @@
package com.sw.platecabinet.activity
import android.content.Intent
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.dialog.BalanceNotEnoughDialog
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.KeyboardUtils
/**
* 密码登录
*/
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater)
}
@@ -15,7 +24,19 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun initialize() {
binding.btnLogin.setOnClickListener {
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
val phone = binding.etPhone.text.toString()
val pwd = binding.etPwd.text.toString()
if (phone.isEmpty() || pwd.isEmpty()) {
ToastUtils.showToast("手机或校验码不能为空")
return@setOnClickListener
}
val loginParam = LoginParam(
//equipmentId = equipmentId,
phone = phone,
password = pwd
)
viewModel.loginWithPwd(loginParam)
KeyboardUtils.hideKeyboard(this)
}
binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java)
@@ -23,4 +44,19 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
}
}
override fun onResume() {
super.onResume()
viewModel.resetUserInfo()
}
override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) {
super.handleLoginSuccess(equipmentUserInfo, isAdmin)
// val cardBalance = equipmentUserInfo.cardBalance?:0.toDouble()
// val balanceIsNotEnough = cardBalance <= 0.toDouble()
if (equipmentUserInfo.isIntercept) {
return
}
LoginByFaceActivity.goInitActivity()
finish()
}
}
@@ -13,8 +13,15 @@ import com.sw.platecabinet.fragment.UnBindPlateFragment
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.FragmentHelper
typealias Callback = (String) -> Unit
/**
* 主界面,内部嵌套framgnet使用
*/
class MainActivity : BaseActivity<ActivityMainBinding>() {
private lateinit var fragmentHelper: FragmentHelper
private var pageType = PageType.SETTING_LIST
private var callback: Callback? = null
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
@@ -28,42 +35,71 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
companion object {
private const val PARAM_PAGE_TYPE = "pageType"
private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
private const val PARAM_IS_ADMIN = "isAdmin"
fun start(
context: Context,
pageType: PageType,
equipmentUserInfo: EquipmentUserInfo? = null
equipmentUserInfo: EquipmentUserInfo? = null,
isAdmin: Boolean = false
) {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo)
intent.putExtra(PARAM_IS_ADMIN, isAdmin)
context.startActivity(intent)
}
}
override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
val isAdmin = intent.getBooleanExtra(PARAM_IS_ADMIN, false)
val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
val pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) {
PageType.SETTING_LIST -> SettingListFragment()
PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_OPEN -> PlateOpenFragment()
PageType.PLATE_TIP -> PlateOpenFragment.newInstance(equipmentUserInfo, isAdmin)
PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment()
}
fragmentHelper.addFragment(page)
}
override fun onLeftDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
finish()
}
}
override fun onRightDoubleClick() {
}
override fun handleScanKeyInfo(scanInfo: String) {
if (callback != null) {
callback!!.invoke(scanInfo)
}
}
fun registerKeyEventInfo(callback: Callback) {
this.callback = callback
}
override fun registerDataChange() {
super.registerDataChange()
}
}
/**
* 界面类型
*/
enum class PageType {
/**
* 餐盘柜打开
* 提示
*/
PLATE_OPEN,
PLATE_TIP,
/**
* 设置列表
@@ -0,0 +1,57 @@
package com.sw.platecabinet.activity
import android.graphics.Color
import android.view.Gravity
import android.view.ViewGroup
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.databinding.ActivityUnbindDialogBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.dp
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 解绑确认弹窗界面
*/
class UnBindDialogActivity : BaseActivity<ActivityUnbindDialogBinding>() {
private var equipmentUserInfo: EquipmentUserInfo? = null
override fun inflateViewBinding(): ActivityUnbindDialogBinding {
return ActivityUnbindDialogBinding.inflate(layoutInflater)
}
override fun inflateTitleBinding(): ItemTitleTimeBinding? {
return null
}
override fun initialize() {
window?.apply {
WindowCompat.setDecorFitsSystemWindows(this, false)
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
attributes = attributes.apply {
y = 314.dp
width = (context.resources.displayMetrics.widthPixels * 0.8).toInt()
height = ViewGroup.LayoutParams.MATCH_PARENT
gravity = Gravity.CENTER
}
}
equipmentUserInfo =
intent.getParcelableExtra<EquipmentUserInfo>(GlobalKey.PARAM_EQUIPMENT_INFO)
if (equipmentUserInfo == null) return
binding.tvTipInfo.text = "即将解除【${equipmentUserInfo!!.name}】的餐盘"
binding.tvCancel.setOnClickListener {
finish()
}
binding.tvConfirm.setOnClickListener {
setResult(RESULT_OK)
finish()
}
}
override fun finish() {
super.finish()
overridePendingTransition(0, 0) // 确保退出也无动画
}
}
@@ -25,7 +25,18 @@ class GenericPageAdapter<T, VB : ViewBinding>(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context).inflate(pageLayoutId, parent, false)
return PageViewHolder(view)
val holder = PageViewHolder(view)
// 在创建ViewHolder时设置ItemDecoration,只设置一次
holder.childRecyclerView.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = view.context.dpToPx(12),
includeEdge = true // 包含边缘间距
)
)
return holder
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
@@ -35,22 +46,15 @@ class GenericPageAdapter<T, VB : ViewBinding>(
override fun getItemCount(): Int = pages.size
inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
init {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
}
fun bind(items: List<T>) {
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)
GenericItemAdapter(items, itemBindingInflater, itemBindCallback)
}
}
}
@@ -58,11 +62,11 @@ class GenericPageAdapter<T, VB : ViewBinding>(
/**
* 通用子项适配器(支持ViewBinding
*/
class GenericChildAdapter<T, VB : ViewBinding>(
private val items: List<T>,
class GenericItemAdapter<T, VB : ViewBinding>(
private var items: List<T>,
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val bindCallback: VB.(item: T, position: Int) -> Unit,
) : RecyclerView.Adapter<GenericChildAdapter<T, VB>.ViewHolder>() {
) : RecyclerView.Adapter<GenericItemAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T, position: Int) {
@@ -70,6 +74,11 @@ class GenericChildAdapter<T, VB : ViewBinding>(
}
}
fun updateData(newItem: List<T>) {
this.items = newItem
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = bindingInflater(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
@@ -77,7 +86,6 @@ class GenericChildAdapter<T, VB : ViewBinding>(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
@@ -0,0 +1,36 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Window
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogBalanceNotEnoughBinding
import com.sw.platecabinet.ext.dp
open class BalanceNotEnoughDialog(
private var activity: FragmentActivity
) : Dialog(activity) {
private lateinit var binding: DialogBalanceNotEnoughBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogBalanceNotEnoughBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
addListener()
}
private fun addListener() {
binding.tvConfirm.setOnClickListener { dismiss() }
}
}
@@ -0,0 +1,38 @@
package com.sw.platecabinet.dialog
import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Window
import androidx.fragment.app.FragmentActivity
import com.sw.platecabinet.databinding.DialogUserBindRemindBinding
import com.sw.platecabinet.ext.dp
open class UserBindRemindDialog(
private var activity: FragmentActivity,
var content:String
) : Dialog(activity) {
private lateinit var binding: DialogUserBindRemindBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogUserBindRemindBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
attributes = attributes.apply {
width = 480.dp
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
binding.tvBindContent.text = content
addListener()
}
private fun addListener() {
binding.tvConfirm.setOnClickListener { dismiss() }
}
}
@@ -2,6 +2,7 @@ package com.sw.platecabinet.ext
import android.content.res.Resources
import android.util.TypedValue
import android.view.View
import androidx.annotation.Dimension
/**
@@ -51,3 +52,15 @@ val Float.sp: Float
fun formatNumber(num: String): String {
return num.padStart(2, '0')
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
@@ -0,0 +1,37 @@
package com.sw.platecabinet.ext
/**
* 姓名脱敏处理
* 2个字:张*
* 多于2个字:张*某
*/
fun String?.maskName(): String {
if (this == null || this.isEmpty()) return ""
return when {
length == 2 -> "${this[0]}*"
length > 2 -> "${this[0]}*${this[length - 1]}"
else -> this // 1个字的情况原样返回
}
}
/**
* 手机号脱敏处理
* 将第4-7位替换为*
* 例如:138****1234
*/
fun String?.maskPhone(): String {
if (this == null || this.isEmpty()) return ""
if (this.length < 11) {
return this
}
val start = 3 // 第4位(索引从0开始)
val end = 6 // 第7位
val sb = StringBuilder(this)
for (i in start..end) {
sb.setCharAt(i, '*')
}
return sb.toString()
}
@@ -1,19 +1,28 @@
package com.sw.platecabinet.fragment
import android.app.Dialog
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
import com.sw.platecabinet.view.CustomLoadingDialog
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch
abstract class BaseFragment<VB : ViewBinding>(
private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB
) : Fragment() {
private var _binding: VB? = null
protected val binding get() = _binding!!
private var mDialogWaiting: CustomLoadingDialog? = null
protected val viewModel by viewModels<SettingViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
@@ -22,16 +31,46 @@ abstract class BaseFragment<VB : ViewBinding>(
): View? {
_binding = bindingInflater(inflater, container, false)
initialize()
registerDataChange()
return binding.root
}
/**
* 注册数据变化监听
*/
open fun registerDataChange() {
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
abstract fun initialize()
class ViewModelFactory<T : ViewModel>(private val creator: () -> T) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return creator() as T
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(requireContext(), R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomLoadingDialog(requireContext(), view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
override fun onDestroyView() {
@@ -1,56 +1,255 @@
package com.sw.platecabinet.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.inputmethod.EditorInfo
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.R
import com.sw.platecabinet.adapter.GenericChildAdapter
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.databinding.FragmentBindPlateBinding
import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
import com.sw.platecabinet.dialog.UserBindRemindDialog
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.KeyboardUtils
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 餐盘柜绑定
*/
class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
FragmentBindPlateBinding::inflate
) {
private lateinit var adapter: GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding>
internal val ARG_PARAM1 = "param1"
private lateinit var adapter: GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding>
internal val KEY_ACTION_TYPE = "actionType"
private var info: EquipmentUserInfo? = null
private var checkedItem: SearchResult.Member? = null
private var lastText = ""
private val debouncer = Debouncer(2000)
private val userViewModel by viewModels<UserViewModel>()
companion object {
@JvmStatic
fun newInstance(param1: EquipmentUserInfo?) =
fun newInstance(userInfo: EquipmentUserInfo?) =
BindPlateFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_PARAM1, param1)
putParcelable(KEY_ACTION_TYPE, userInfo)
}
}
}
@SuppressLint("SetTextI18n")
override fun initialize() {
arguments?.let {
info = it.getParcelable(ARG_PARAM1)
info?.let { binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}" }
arguments?.let { args ->
info = args.getParcelable(KEY_ACTION_TYPE)
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
if (!TextUtils.isEmpty(it.plateNumber)) {
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it.plateNumber)
binding.etCode.apply {
setText(it.plateNumber)
setSelection(length())
}
}
}
}
initView()
initListener()
}
private fun initView() {
adapter = createAdapter()
registerDateChange()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(6),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItems = layoutManager.itemCount
if (!viewModel.isLoading && viewModel.canLoadMore
&& lastVisibleItem >= totalItems - 3
) {
viewModel.getSearchMemberList(
param = lastText,
pageNum = viewModel.currentPage
)
}
}
})
(activity as MainActivity).registerKeyEventInfo {
// binding.etCode.text.clear()
// binding.etCode.postDelayed({
// binding.etCode.text.clear()
// binding.etCode.text = Editable.Factory.getInstance().newEditable(it)
activity?.runOnUiThread {
if (binding.etUserInfo.isFocused) {
return@runOnUiThread
}
binding.etCode.apply {
if (it.length > 6) {
setText(it.substring(0, 6))
} else {
setText(it)
}
if (length() > 0) {
setSelection(length())
}
}
binding.etUserInfo.requestFocus()
}
// }, 50)
}
}
private fun initListener() {
binding.etCode.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
Timber.d("beforeTextChanged s = ${s.toString()}, start = $start, count = $count, after = $after")
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
Timber.d("onTextChanged s = ${s.toString()}, start = $start, count = $count, before = $before")
}
override fun afterTextChanged(s: Editable?) {
Timber.d("afterTextChanged s = ${s.toString()}")
}
})
binding.etUserInfo.setOnEditorActionListener { _, actionId, keyEvent ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val currentText = binding.etUserInfo.text.toString()
val containsNewlines = currentText.contains('\n') || currentText.contains('\r')
if (containsNewlines) {
Timber.d("非用户主动搜索")
true
}
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(param = currentText, pageNum = 1)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
true // 表示已处理该事件
} else {
false // 未处理其他动作
}
}
binding.ivSearch.setOnClickListener {
debouncer.debounce {
// 执行搜索操作
viewModel.getSearchMemberList(
param = binding.etUserInfo.text.toString(),
pageNum = 1
)
// 隐藏键盘
KeyboardUtils.hideKeyboard(requireActivity())
}
}
binding.llBind.setOnClickListener {
if (equipmentBoxCode.isNullOrBlank().not()) {
showBindDialog()
return@setOnClickListener
}
if (info == null || info!!.equipmentCode?.isEmpty() == true || info!!.equipmentBoxCode?.isEmpty() == true) {
ToastUtils.showToast("传入的设备信息异常")
return@setOnClickListener
}
val plateNumber = binding.etCode.text.toString()
if (plateNumber.isEmpty()) {
ToastUtils.showToast("请输入餐盘号")
return@setOnClickListener
}
if (checkedItem == null) {
ToastUtils.showToast("请选择要绑定的会员")
return@setOnClickListener
}
if (checkedItem!!.id == null) {
ToastUtils.showToast("选中的会员信息异常")
return@setOnClickListener
}
val bindParam = BindParam(
equipmentId = info?.equipmentId,
equipmentCode = info?.equipmentCode,
equipmentBoxCode = info?.equipmentBoxCode,
faceId = checkedItem?.faceId,
plateNumber = plateNumber
)
viewModel.bindPlate(bindParam) { pair ->
updateBindState(pair)
}
KeyboardUtils.hideKeyboard(requireActivity())
}
binding.tvBack.setOnClickListener { activity?.finish() }
}
private fun registerDateChange() {
/**
* 监听数据变化
*/
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchMemberList.collect {
adapter.updateData(it)
}
}
// lifecycleScope.launch {
// viewModel.bindStateChange.collect {
// updateBindState(it)
// }
// }
}
private fun createAdapter(): GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding> {
return GenericChildAdapter(
private fun createAdapter(): GenericItemAdapter<SearchResult.Member, ItemSearchUserInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchUserInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.name
this.tvPhone.text = item.phone
if (item.isBound()) {
this.tvName.text = item.name.maskName()
this.tvPhone.text = item.phone.maskPhone()
if (item.id != checkedItem?.id) {
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
@@ -58,8 +257,61 @@ class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.name}, position = $position")
checkedItem = item
adapter.notifyDataSetChanged()
//TODO 调用接口查询用户是否已绑盘
this@BindPlateFragment.equipmentBoxCode = null
(activity as? MainActivity)?.showWaitingDialog("查询中,请稍后……")
userViewModel.getUserInfoById(memberId = item.faceId) {
(activity as? MainActivity)?.hideWaitingDialog()
this@BindPlateFragment.equipmentBoxCode = it?.equipmentBoxCode
}
}
}
)
}
private var equipmentBoxCode:String? = null
private fun showBindDialog() {
UserBindRemindDialog(
activity = requireActivity(),
content = "您已绑定编号为${equipmentBoxCode}的柜子,请解绑后重试"
)
.show()
}
private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
if (pair.first == null) return // 解绑状态过滤
val errorInfo = pair.second
if (errorInfo.isSuccess()) {
if (errorInfo.equipmentUserInfo == null) {
ToastUtils.showToast("数据异常")
return
}
// if (errorInfo.equipmentUserInfo.isOtherEquipment()) {
// MainActivity.start(
// requireContext(),
// pageType = PageType.PLATE_TIP,
// equipmentUserInfo = errorInfo.equipmentUserInfo,
// isAdmin = true
// )
// } else {
SerialApi.openPlate(
errorInfo.equipmentUserInfo.equipmentBoxCode!!.toInt(),
object : SerialPortManager.SendCallback {
override fun onSuccess() {
ToastUtils.showToast("绑定成功")
activity?.finish()
}
override fun onFail(e: Exception?) {
ToastUtils.showToast("绑定失败")
}
})
// }
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
@@ -1,23 +1,26 @@
package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
import timber.log.Timber
/**
* 餐盘柜已满
*/
class PlateCabinetFullFragment :
BaseFragment<FragmentPlateCabinetFullBinding>(FragmentPlateCabinetFullBinding::inflate) {
private var totalTimeInMillis: Long = 5 * 1000
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
override fun initialize() {
Timber.d("initialize")
initCountTime()
}
fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = "${(millisUntilFinished / 1000).toInt()}秒后返回主屏"
binding.tvAutoClose.text = "${(millisUntilFinished / 1000).toInt() + 1}秒后返回主屏"
}
override fun onFinish() {
@@ -1,27 +1,75 @@
package com.sw.platecabinet.fragment
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import com.sw.platecabinet.Constants
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.InitActivity
import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 餐盘柜打开界面
* 餐盘柜提示界面
*/
class PlateOpenFragment :
BaseFragment<FragmentPlateOpenBinding>(FragmentPlateOpenBinding::inflate) {
private var totalTimeInMillis: Long = 5 * 1000
private var totalTimeInMillis: Long = Constants.AUTO_CLOSE_TIME * 1000
internal val KEY_ACTION_TYPE = "actionType"
internal val PARAM_IS_ADMIN = "isAdmin"
private var info: EquipmentUserInfo? = null
private var isAdmin: Boolean = false
companion object {
@JvmStatic
fun newInstance(userInfo: EquipmentUserInfo?, isAdmin: Boolean) =
PlateOpenFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_ACTION_TYPE, userInfo)
putBoolean(PARAM_IS_ADMIN, isAdmin)
}
}
}
override fun initialize() {
arguments?.let {
info = it.getParcelable(KEY_ACTION_TYPE)
isAdmin = it.getBoolean(PARAM_IS_ADMIN)
info?.let {
if (!it.hasEquipmentBox()) {
binding.ivType.setImageResource(R.drawable.ic_tip_warn)
binding.tvTitle.text = "暂无餐盘信息"
binding.tvSubTitle.text = "请联系管理员"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
} else if (it.isOtherEquipment()) {
binding.ivType.setImageResource(R.drawable.ic_tip_warn)
binding.tvTitle.text = "暂无餐盘信息"
binding.tvSubTitle.text = "您的餐盘绑定在${it.equipmentName ?: " "}号柜上"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_fail))
} else {
binding.ivType.setImageResource(R.drawable.ic_tip_success)
binding.tvTitle.text = "柜门开启"
binding.tvSubTitle.text = "餐盘取出后请关闭柜门"
binding.tvTitle.setTextColor(requireContext().getColor(R.color.tip_title_success))
}
}
}
initCountTime()
}
fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = (millisUntilFinished / 1000).toInt().toString()
binding.tvAutoClose.text = ((millisUntilFinished / 1000).toInt() + 1).toString()
}
override fun onFinish() {
activity?.finish()
if (!isAdmin) {
val intent = Intent(context, InitActivity::class.java)
startActivity(intent)
}
}
}.start()
}
@@ -1,68 +1,125 @@
package com.sw.platecabinet.fragment
import android.view.View
import androidx.fragment.app.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.comn.SerialApi
import com.sw.plate.utils.comn.SerialPortManager
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericItemAdapter
import com.sw.platecabinet.adapter.GenericPageAdapter
import com.sw.platecabinet.adapter.GridSpacingItemDecoration
import com.sw.platecabinet.adapter.dpToPx
import com.sw.platecabinet.databinding.FragmentSettingListBinding
import com.sw.platecabinet.databinding.ItemBindViewBinding
import com.sw.platecabinet.ext.formatNumber
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.viewmodel.SettingViewModel
import com.sw.platecabinet.utils.ListArrangementUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import kotlin.math.ceil
/**
* 餐盘柜绑定的用户列表界面
*/
class SettingListFragment :
BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
private val viewModel: SettingViewModel by viewModels<SettingViewModel>()
/**
* 需要横向滚动的adapter
*/
private var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
private lateinit var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>
private val itemsPerPage = 22 // 每行2个,每列11个,共22个
/**
* 垂直滚动的adapter
*/
private var itemAdapter: GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding>? = null
private fun registerDateChange() {
lifecycleScope.launch {
// 使用 repeatOnLifecycle 确保只在特定生命周期状态收集
// repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.equipmentList.collect { it ->
Timber.d("registerDateChange updateAdapter it = $it")
updateAdapter(it)
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.equipmentList.collect { it ->
// Timber.d("registerDateChange updateAdapter it = $it")
// updateAdapter(it)
// }
// }
}
}
override fun initialize() {
binding.mainRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
pageAdapter = createAdapter()
if (GlobalData.arrayCross > 2) {
layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
pageAdapter = createPageAdapter()
adapter = pageAdapter
} else {
layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(12),
includeEdge = true // 包含边缘间距
)
)
itemAdapter = createItemAdapter()
adapter = itemAdapter
}
registerDateChange()
viewModel.getEquipmentList(EquipmentParam(equipmentCode = "202501171634"))
}
private fun createAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
}
override fun onResume() {
super.onResume()
viewModel.getEquipmentList{
updateAdapter(it)
}
}
private fun createItemAdapter(): GenericItemAdapter<EquipmentUserInfo, ItemBindViewBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemBindViewBinding::inflate,
bindCallback = { item, position ->
setItemInfo(item, position)
}
)
}
private fun createPageAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
return GenericPageAdapter(
pages = emptyList(),
pageLayoutId = R.layout.page_item_layout,
itemBindingInflater = ItemBindViewBinding::inflate,
itemBindCallback = { item, position ->
setItemInfo(item, position)
}
)
}
private fun ItemBindViewBinding.setItemInfo(
item: EquipmentUserInfo,
position: Int
) {
if (item.isPlaceholder() == true) {
this.llRoot.isVisible = false
return
}
this.llRoot.isVisible = true
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)
val isOldTime = DateTimeUtils.isMoreThanHoursFromNow(timeInMillis)
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
@@ -72,15 +129,15 @@ class SettingListFragment :
)
)
this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
this.tvOpen.setTextColor(resources.getColor(R.color.bind_F0C8B4))
this.tvOpen.setTextColor(resources.getColor(R.color.bind_FFCC99))
this.llBindInfo.visibility = View.VISIBLE
this.tvUnbind.visibility = View.GONE
this.tvName.text = item.name
this.tvName.text = item.name.maskName()
this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_F0C8B4))
this.tvNum.setTextColor(resources.getColor(R.color.bind_FFCC99))
this.llOpen.setBackgroundResource(R.drawable.grid_button_unbind)
this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
this.llBindInfo.visibility = View.GONE
@@ -103,41 +160,78 @@ class SettingListFragment :
}
}
this.llOpen.setOnClickListener {
Timber.d("itemClick llOpen ${item.equipmentCode}")
MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN)
}
Timber.d("itemClick llOpen equipmentBoxCode = ${item.equipmentBoxCode}")
val equipmentBoxCode: Int = item.equipmentBoxCode?.toInt() ?: -1
if (equipmentBoxCode < 0) {
ToastUtils.showToast("餐盘柜编号错误")
return@setOnClickListener
}
showWaitingDialog("请稍等")
SerialApi.openPlate(equipmentBoxCode, object : SerialPortManager.SendCallback {
override fun onSuccess() {
hideWaitingDialog()
MainActivity.start(
requireContext(),
pageType = PageType.PLATE_TIP,
isAdmin = true
)
}
override fun onFail(e: Exception?) {
hideWaitingDialog()
ToastUtils.showToast("柜门打开失败")
}
})
}
}
fun updateAdapter(items: List<EquipmentUserInfo>) {
val pages = if (items.size > itemsPerPage) {
items.chunked(itemsPerPage) {
convertToColumnFirst(it, 11)
val itemsPerPage = 2 * GlobalData.arrayVertical // 每行2个,每列11个,共22个
if (GlobalData.arrayCross > 2) {
lifecycleScope.launch(Dispatchers.Default) {
// 多recyclerview
var pages = if (items.size > itemsPerPage) {
if (GlobalData.arrayMode == 0) {
val pageList = items.chunked(itemsPerPage)
pageList.mapIndexed { index, it ->
var itemList: MutableList<EquipmentUserInfo> = it.toMutableList()
if (index == pageList.size - 1) {
// 补全剩余item
itemList = (itemList + List(itemsPerPage - itemList.size) {
EquipmentUserInfo()
}) as MutableList<EquipmentUserInfo>
}
ListArrangementUtil.convertToColumnFirst(
itemList,
GlobalData.arrayVertical
)
}
} else {
val newItems =
ListArrangementUtil.horizontalSortPageItem(items, GlobalData.arrayCross)
newItems.chunked(itemsPerPage)
}
} else {
listOf(
// 根据条件判断是否需要重新排列
// items
convertToColumnFirst(items, 11)
items
)
}
pageAdapter.updatePages(pages)
withContext(Dispatchers.Main) {
pageAdapter?.updatePages(pages)
}
/**
* 重新排列数组
*/
private fun convertToColumnFirst(
original: List<EquipmentUserInfo>,
cols: Int
): List<EquipmentUserInfo> {
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]
}
} else {
// 单 recyclerview
lifecycleScope.launch(Dispatchers.Default) {
var pages = items
if (GlobalData.arrayMode == 0) {
pages = ListArrangementUtil.verticalSortItem(pages)
}
withContext(Dispatchers.Main) {
itemAdapter?.updateData(pages)
}
}
}
binding.mainRecyclerView.scrollToPosition(0)
}
}
@@ -1,9 +1,20 @@
package com.sw.platecabinet.fragment
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.activity.UnBindDialogActivity
import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
import com.sw.platecabinet.ext.maskName
import com.sw.platecabinet.ext.maskPhone
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlinx.coroutines.launch
/**
* 餐盘柜绑定
@@ -12,7 +23,6 @@ class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPla
FragmentUnbindPlateBinding::inflate
) {
internal val ARG_PARAM1 = "param1"
private var param1: EquipmentUserInfo? = null
private var info: EquipmentUserInfo? = null
companion object {
@@ -25,21 +35,61 @@ class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPla
}
}
private val dialogLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
viewModel.unbindPlate(info?.id) { pair ->
updateBindState(pair)
}
}
}
override fun initialize() {
arguments?.let {
info = it.getParcelable(ARG_PARAM1)
arguments?.let { args ->
info = args.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
binding.tvNameValue.text = it.name.maskName()
binding.tvPhoneValue.text = it.phone.maskPhone()
val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
binding.tvUpdateTimeValue.text = timeAgo
}
}
binding.llUnbind.setOnClickListener {
if (info == null) return@setOnClickListener
val intent = Intent(requireContext(), UnBindDialogActivity::class.java).apply {
putExtra(GlobalKey.PARAM_EQUIPMENT_INFO, info)
}
dialogLauncher.launch(intent)
requireActivity().overridePendingTransition(0, 0)
}
binding.tvBack.setOnClickListener {
activity?.finish()
}
}
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.bindStateChange.collect {
// updateBindState(it)
// }
// }
}
private fun updateBindState(pair: Pair<Boolean?, ErrorInfo>){
if (pair.first == null) return // 绑定状态过滤
val errorInfo = pair.second
if (errorInfo.isSuccess()) {
ToastUtils.showToast("解绑成功")
// activity?.finish()
// 关闭宿主 Activity
requireActivity().finish()
requireActivity().overridePendingTransition(0, 0)
} else {
ToastUtils.showToast(errorInfo.msg)
}
}
}
@@ -0,0 +1,27 @@
package com.sw.platecabinet.model
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 错误信息 code == 200 成功
*/
data class ErrorInfo(
val code: String = "00000",
val msg: String = "",
val equipmentUserInfo: EquipmentUserInfo? = null
) {
fun isSuccess(): Boolean {
return code == "00000"
}
}
data class DeviceConfig(
var arcsoftAppId: String? = null,
var arcsoftSdkKey: String? = null,
var arcsoftActiveKey: String? = null
)
data class CodeMsg(
val code: String? = "",
val msg: String? = ""
)
@@ -12,24 +12,25 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class BindParam(
/**
*设备盒子编号
* 设备Id
*/
@SerializedName("equipmentBoxCode")
val equipmentBoxCode: String? = "",
val equipmentId: String? = null,
/**
* 设备编号
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
var equipmentCode: String? = "",
/**
*设备盒子编号
*/
val equipmentBoxCode: String? = null,
/**
*
* 会员Id
*/
@SerializedName("memberId")
val memberId: Int? = 0,
val memberId: String? = null,
val faceId: String? = null,
/**
*餐盘编号
*/
@SerializedName("plateNumber")
val plateNumber: String? = ""
val plateNumber: String? = null
) : Parcelable
@@ -1,6 +1,7 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
@@ -8,8 +9,13 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class EquipmentParam(
/**
* app版本号
*/
var appVersion: String = "",
/**
* 设备ID
*/
var equipmentCode: String = ""
@SerializedName("equipmentCode")
var equipmentCode: String = "",
) : Parcelable
@@ -11,23 +11,20 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class LoginParam(
/**
* 设备ID
* 设备Id
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
val equipmentId: Int? = null,
/**
* 会员信息
*/
@SerializedName("memberId")
val memberId: String? = "",
// val memberId: String? = null,
val faceId: String? = null,
/**
* 密码
*/
@SerializedName("password")
val password: String? = "",
val password: String? = null,
/**
* 手机号
*/
@SerializedName("phone")
val phone: String? = ""
val phone: String? = null
) : Parcelable
@@ -10,16 +10,27 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class SearchParam(
@SerializedName("consumptionTime")
val consumptionTime: List<String?>? = listOf(),
@SerializedName("createTime")
val createTime: List<String?>? = listOf(),
@SerializedName("memberFrom")
val memberFrom: Int? = 0,
@SerializedName("pageNum")
val pageNum: Int? = 0,
@SerializedName("pageSize")
val pageSize: Int? = 0,
@SerializedName("param")
val `param`: String? = ""
// /**
// * app版本号
// */
// var appVersion: String = "",
// /**
// * 设备ID
// */
// @SerializedName("equipmentCode")
// var equipmentCode: String = "",
// @SerializedName("consumptionTime")
// val consumptionTime: List<String?>? = listOf(),
// @SerializedName("createTime")
// val createTime: List<String?>? = listOf(),
// @SerializedName("memberFrom")
// val memberFrom: Int? = 0,
// @SerializedName("pageNum")
var pageNum: Int? = 0,
// @SerializedName("pageSize")
var pageSize: Int? = 0,
// @SerializedName("param")
// val `param`: String? = "",
var name:String?=null,
var phone:String?=null
) : Parcelable
@@ -1,10 +1,12 @@
package com.sw.platecabinet.model.response
data class ApiResponse<T>(
val code: Int,
val success: Boolean? = false,
val code: String,
// val success: Boolean? = false,
val msg: String? = "",
val data: T? = null
val data: T? = null,
// val result: T? = null,
) {
fun isSuccess(): Boolean = code == 200
// fun isSuccess(): Boolean = code == 200
fun isSuccess(): Boolean = code == "00000"
}
@@ -0,0 +1,72 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class EquipmentInfo(
@SerializedName("appPackageLocalUrl")
val appPackageLocalUrl: String? = "",
@SerializedName("appPackageUrl")
val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "",
@SerializedName("arrayCross")
val arrayCross: Int? = 0,
@SerializedName("arrayMode")
val arrayMode: String? = "",
@SerializedName("arrayVertical")
val arrayVertical: Int? = 0,
@SerializedName("canteenId")
val canteenId: String? = "",
@SerializedName("canteenName")
val canteenName: String? = "",
@SerializedName("clientServerIp")
val clientServerIp: String? = "",
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("customerName")
val customerName: String? = "",
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
@SerializedName("equipmentName")
val equipmentName: String? = "",
@SerializedName("equipmentName_dictText")
val equipmentNameDictText: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: Int? = 0,
@SerializedName("mqName")
val mqName: String? = "",
@SerializedName("mqPassword")
val mqPassword: String? = "",
@SerializedName("orgCode")
val orgCode: String? = "",
@SerializedName("owningTrack")
val owningTrack: Int? = 0,
@SerializedName("owningTrackOrder")
val owningTrackOrder: Int? = 0,
@SerializedName("screenHtml")
val screenHtml: String? = "",
@SerializedName("serviceRequestAddress")
val serviceRequestAddress: String? = "",
@SerializedName("status")
val status: Int? = 0,
@SerializedName("sysOrgCode")
val sysOrgCode: String? = "",
@SerializedName("updateBy")
val updateBy: String? = "",
@SerializedName("updateTime")
val updateTime: String? = "",
@SerializedName("zhstServerIp")
val zhstServerIp: String? = ""
) : Parcelable
@@ -1,8 +1,9 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import android.text.TextUtils
//import com.google.gson.annotations.SerializedName
import com.sw.platecabinet.GlobalData
import kotlinx.parcelize.Parcelize
/**
@@ -11,55 +12,79 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class EquipmentUserInfo(
/**
* 设备盒子编号
* 主键
*/
@SerializedName("equipmentBoxCode")
val equipmentBoxCode: String? = "",
/**
* 设备编号
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
val id: Long? = 0,
/**
*
* 设备Id
*/
@SerializedName("equipmentId")
val equipmentId: String? = "",
/**
* 设备名称
* 设备编号
*/
@SerializedName("equipmentName")
val equipmentName: String? = "",
var equipmentCode: String? = "",
/**
* 主键
* 设备盒子编号
*/
@SerializedName("id")
val id: Int? = 0,
var equipmentBoxCode: String? = "",
/**
* 会员Id
*/
@SerializedName("memberId")
val memberId: Int? = 0,
@SerializedName("name")
val name: String? = "",
@SerializedName("phone")
val phone: String? = "",
val faceId: String? = "",
/**
* 餐盘编号
*/
@SerializedName("plateNumber")
val plateNumber: String? = "",
var plateNumber: String? = "",
/**
* 设备名称
*/
val equipmentName: String? = "",
val orderNo: String? = "",
/**
*
* 更新时间
*/
@SerializedName("updateTime")
val updateTime: String? = ""
val updateTime: String? = "",
val name: String? = "",
val phone: String? = "",
val faceUrl: String? = "",
val mealTime: String? = "",
val mealTimeInterval: String? = "",
val openTime: String? = "",
val openTimeInterval: String? = "",
val eatCount: Int? = 0,
var cardBalance: Double? = 0.toDouble(),
/**
* 用于cardBalance后续拦截判断
*/
var showBalanceNotEnoughDialog: Boolean = false,
var isIntercept: Boolean = false
) : Parcelable {
/**
* 是否已绑定
*/
fun isBound(): Boolean {
return memberId != null
return faceId != null
}
/**
* 是否是其他设备
*/
fun isOtherEquipment(): Boolean {
return equipmentCode != null && equipmentCode != "" && GlobalData.globalEquipmentCode != equipmentCode
}
fun hasEquipmentBox(): Boolean {
return !TextUtils.isEmpty(equipmentBoxCode)
}
fun isPlaceholder(): Boolean {
return equipmentBoxCode == null || equipmentBoxCode == ""
}
}
@@ -5,6 +5,9 @@ import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 搜索会员信息结果
*/
@Parcelize
data class SearchResult(
@SerializedName("endRow")
@@ -46,33 +49,37 @@ data class SearchResult(
) : 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")
// @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? = ""
val id: String? = "",
val faceId: String? = "",
val name: String? = "",
@SerializedName("phone")
val phone: String? = "",
@SerializedName("rewardBalance")
val rewardBalance: String? = "",
@SerializedName("topUpBalance")
val topUpBalance: String? = ""
) : Parcelable
}
@@ -10,14 +10,21 @@ 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? = ""
// @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? = "",
// @SerializedName("updateTimeStamp")
// val updateTimeStamp: Long = 0,
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp:Long?=null,
val faceDeleted: Boolean?=false
) : Parcelable
@@ -1,5 +1,6 @@
package com.sw.platecabinet.network
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.interceptor.RequestInterceptor
@@ -11,12 +12,13 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "https://vip.shuziweidao.com/shuwei-zhct/"
//private const val BASE_URL = "https://vip.shuziweidao.com"
// private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889/"
private var DEVICE_BASE_URL = GlobalData.appBaseUrl
// private const val BASE_URL = "http://192.168.1.8:9092"
private const val TIME_OUT = 30L // 超时时间(秒)
private val okHttpClient = OkHttpClient.Builder()
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
@@ -33,7 +35,7 @@ object ApiClient {
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.baseUrl(DEVICE_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
@@ -1,54 +1,189 @@
package com.sw.platecabinet.network.api
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfig
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.EquipmentInfo
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.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.Url
interface ApiService {
/**
* 生成token
*/
@GET("scales/generateToken")
suspend fun generateToken(@Query("deviceId") deviceId: String): ApiResponse<String>
// /**
// * device获取token
// */
// @GET("sys/getEquipmentToken")
// suspend fun getDeviceToken(
// @Query("qrcodeId") qrcodeId: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion
// ): ApiResponse<String>
//
// /**
// *获取配置信息
// */
// @GET("equipment/stEquipment/queryByEquipmentCode")
// suspend fun getDeviceInfo(
// @Query("equipmentCode") equipmentCode: String,
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
// @Header("X-Access-Token") token: String
// ): ApiResponse<EquipmentInfo>
//
// /**
// * 生成token
// */
// @GET//("/shuwei-zhct/scales/generateToken")
// suspend fun generateToken(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/generateToken",
// @Query("deviceId") deviceId: String,
// @Query("appVersion") appVersion: String,
// @Query("equipmentCode") equipmentCode: String
// ): ApiResponse<String>
/**
* 获取人脸数据
*/
@GET("scales/getUserFaceCache")
suspend fun getUserFaceCache(): ApiResponse<List<UserFaceModel>>
// @GET//("/shuwei-zhct/scales/getUserFaceCache")
// suspend fun getUserFaceCache(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/getUserFaceCache",
// @Query("appVersion") appVersion: String,
// @Query("equipmentCode") equipmentCode: String
// ): ApiResponse<List<UserFaceModel>>
@POST
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/list",
@Body param: Map<String, Int>
): ApiResponse<List<UserFaceModel>>
// /**
// * 餐盘用户信息获取
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
// suspend fun equipmentBoxLogin(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin",
// @Body param: LoginParam
// ): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息获取
*/
@POST("swEquipmentRelUser/equipmentBoxLogin")
suspend fun equipmentBoxLogin(@Body param: LoginParam): ApiResponse<EquipmentUserInfo>
@POST//("/shuwei-zhct/swEquipmentRelUser/equipmentBoxLogin")
suspend fun equipmentBoxLogin(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getPlateBoxUserInfo",
@Body param: LoginParam
): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息列表查询
*/
@POST("swEquipmentRelUser/list")
suspend fun getEquipmentList(@Body param: EquipmentParam): ApiResponse<List<EquipmentUserInfo>>
// @POST//("/shuwei-zhct/swEquipmentRelUser/list")
// suspend fun getEquipmentList(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/list",
// @Body param: EquipmentParam
// ): ApiResponse<List<EquipmentUserInfo>>
@GET
suspend fun getEquipmentList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getYxMemberRefPlateByEquipmentCode",
): ApiResponse<List<EquipmentUserInfo>>
// /**
// * 餐盘用户信息绑定解绑
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/addOrEdit")
// suspend fun bindEquipment(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/addOrEdit",
// @Body param: BindParam
// ): ApiResponse<EquipmentUserInfo?>
/**
* 餐盘用户信息绑定解
* 餐盘绑
*/
@POST("swEquipmentRelUser/addOrEdit")
suspend fun bindEquipment(@Body param: BindParam): ApiResponse<EquipmentUserInfo?>
@POST
suspend fun plateBind(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateBinding",
@Body param: BindParam
): ApiResponse<EquipmentUserInfo?>
/**
* 餐盘解绑
*/
@POST
@FormUrlEncoded
suspend fun plateUnbind(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/plateUnbind",
@Field("id") id: Long?
): ApiResponse<Any?>
/**
* 用户信息模糊搜索
*/
@POST("swclientUserInfoShop/selectList")
suspend fun searchUser(@Body param: SearchParam): ApiResponse<SearchResult>
@POST//("/shuwei-user/swclientUserInfoShop/selectList")
suspend fun searchUser(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-user/swclientUserInfoShop/selectList",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getUserInfoByNameOrPhone",
@Body param: SearchParam
): ApiResponse<List<SearchResult.Member>?>
// /**
// * 通过餐盘号获取信息
// */
// @POST//("/shuwei-zhct/swEquipmentRelUser/findByPlateNumber")
// suspend fun findByPlateNumber(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/swEquipmentRelUser/findByPlateNumber",
// @Body param: BindParam
// ): ApiResponse<EquipmentUserInfo>
/**
* 通过餐盘号获取信息
*/
@GET
suspend fun findByPlateNumber(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/sideboard/app/getMemberRefPlateByPlateNumber",
@Query("plateNumber") plateNumber: String
): ApiResponse<EquipmentUserInfo>
/**
* 获取人脸增量数据
*
* @param timeDate 时间戳(毫秒级)
*/
// @GET//("/shuwei-zhct/scales/getHeartbeatInterface")
// suspend fun getHeartbeatInterface(
// @Url url: String = "${GlobalData.appBaseUrl}/shuwei-zhct/scales/getHeartbeatInterface",
// @Query("timeDate") timeDate: Long
// ): ApiResponse<List<UserFaceModel>>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param:Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?>
}
@@ -2,6 +2,7 @@ package com.sw.platecabinet.network.interceptor
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalKey
import okhttp3.Interceptor
import okhttp3.Response
@@ -16,7 +17,10 @@ class RequestInterceptor : Interceptor {
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
.header("Authorization", getToken())
// .header("Authorization", getToken())
.header("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiJiZTE1NDgzMS0zNDY2LTNiYTItYTJlYS01NzY1MmM5MTlmZWQiLCJ0eXBlIjoiNCIsInVzZXJJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDEifQ.sN40cOC-O5WQFrF4IDUs8fFlkNdUKLbJt_rHyTsgYYM")
.header("X-DEVICE-CODE", GlobalData.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
val newRequest = requestBuilder.build()
@@ -0,0 +1,104 @@
package com.sw.platecabinet.network.task
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.arcsoft.face.FaceEngine
import com.sw.inbound.utils.GsonUtils
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.PrefUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.plate.utils.arcface.faceserver.FaceServer
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
class HeartBeatTask(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
companion object {
private const val TAG = "HeartBeatTask"
}
override suspend fun doWork(): Result {
return try {
// 执行后台任务逻辑
performSync()
TaskManager.startTask()
Result.success()
} catch (e: Exception) {
e.printStackTrace()
Result.retry()
}
}
private val repository: RemoteRepository by lazy {
RemoteRepository(ApiClient.apiService)
}
private val faceApi: FaceApi = FaceApi()
private suspend fun performSync() {
// withContext(Dispatchers.IO) {
// val faceUpdateTimeStamp = PrefUtils.getLong(App.getContext(), "faceUpdateTimeStamp", 0)
// if (faceUpdateTimeStamp == 0L) {
// val timestamp = System.currentTimeMillis()
// PrefUtils.setLong(App.getContext(), "faceUpdateTimeStamp", timestamp)
// }
// val resp = repository.getHeartbeatInterface(
// PrefUtils.getLong(
// App.getContext(),
// "faceUpdateTimeStamp",
// 0
// )
// )
// Timber.tag(TAG).d("performSync: ${GsonUtils.toJson(resp)}")
// val list = resp.data
// if (list.isNullOrEmpty()) {
// return@withContext
// }
// val faceServer = FaceServer.getInstance()
// val faceEngine: FaceEngine? = faceServer.faceEngine
//
// list.forEachIndexed { index, it ->
// try {
// if (index == list.size - 1) {
// PrefUtils.setLong(
// App.getContext(),
// "faceUpdateTimeStamp",
// it.updateTimeStamp
// )
// }
// if (it.faceType == "1") {
// if (it.userId.isNullOrBlank().not()) {
// //userId非空,根据userId删除人脸数据
// faceApi.deleteByUserName(it.userId)
// faceEngine?.removeFaceFeature(it.userId.toInt())
// }
// } else if (it.faceType == "0") {
// //新增人脸数据
// val featureData = Base64.decode(it.faceFeature)
// val insertEntity = FaceEntity(it.userId, null, featureData)
// val faceId = faceApi.insert(insertEntity)
// insertEntity.faceId = faceId
//// faceServer.registerFaceFeatureInfoFromDb(insertEntity, faceEngine)
//
// EventBus.getDefault().post(insertEntity)
//// callback(insertEntity)
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// }
}
}
@@ -0,0 +1,40 @@
package com.sw.platecabinet.network.task
import androidx.lifecycle.LifecycleOwner
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.sw.plate.App
import java.util.concurrent.TimeUnit
object TaskManager {
private const val TASK_NAME = "SmartPlateCabinetTask"
// private var owner: LifecycleOwner?=null
// fun setOwner(owner: LifecycleOwner) {
// this.owner = owner
// }
fun startTask() {
val nextRequest = OneTimeWorkRequestBuilder<HeartBeatTask>()
.setInitialDelay(1, TimeUnit.MINUTES)
// .setInputData(inputData)
.build()
WorkManager.getInstance(App.getContext())
// .apply{
// getWorkInfoByIdLiveData(nextRequest.id)
// .observe(owner) { workInfo ->
// if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
// val result = workInfo.outputData.getString("RESULT_KEY")
// // 使用返回的结果
// }
// }
// }
.enqueueUniqueWork(TASK_NAME, ExistingWorkPolicy.REPLACE, nextRequest)
}
fun cancelTask() {
WorkManager.getInstance(App.getContext())
.cancelUniqueWork(TASK_NAME)
}
}
@@ -0,0 +1,21 @@
package com.sw.platecabinet.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.sw.platecabinet.activity.DeviceInitActivity
import com.sw.platecabinet.activity.LoginByFaceActivity
import timber.log.Timber
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
Timber.d("设备启动完成,开始执行自启动逻辑")
// val intent = Intent(context, LoginByFaceActivity::class.java)
val intent = Intent(context, DeviceInitActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}
@@ -1,6 +1,8 @@
package com.sw.platecabinet.repository
import com.google.gson.JsonParseException
import com.sw.inbound.utils.GsonUtils
import com.sw.platecabinet.model.CodeMsg
import com.sw.platecabinet.model.response.ApiResponse
import retrofit2.HttpException
import timber.log.Timber
@@ -18,31 +20,37 @@ abstract class BaseRepository {
when (e) {
is HttpException -> {
ApiResponse(code = e.code(), msg = e.message())
val jsonData = e.response()?.errorBody()?.string()
val codeMsg = GsonUtils.fromJson(jsonData, CodeMsg::class.java)
if (codeMsg?.msg.isNullOrBlank()) {
ApiResponse(code = "" + e.code(), msg = e.message())
} else {
ApiResponse(code = codeMsg?.code?:"", msg = codeMsg?.msg?:"")
}
}
is SocketTimeoutException -> {
ApiResponse(code = -2, msg = "请求超时: ${e.message}")
ApiResponse(code = "-2", msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = -3, msg = "连接失败: ${e.message}")
ApiResponse(code = "-3", msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = -4, msg = "SSL握手失败: ${e.message}")
ApiResponse(code = "-4", msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = -5, msg = "JSON解析错误: ${e.message}")
ApiResponse(code = "-5", msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = -6, msg = "网络IO错误: ${e.message}")
ApiResponse(code = "-6", msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = -1, msg = "未知错误: ${e.message ?: "无错误信息"}")
ApiResponse(code = "-1", msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
@@ -1,15 +1,17 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.DeviceConfig
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.EquipmentInfo
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
/**
* 远程数据处理
@@ -17,46 +19,129 @@ import retrofit2.http.POST
class RemoteRepository constructor(
private val apiService: ApiService
) : BaseRepository() {
/**
* 生成token
*/
suspend fun generateToken(deviceId: String): ApiResponse<String> {
return safeApiCall { apiService.generateToken(deviceId) }
}
// /**
// * 生成token
// */
// suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.getDeviceToken(
// qrcodeId
// )
// }
// }
//
// /**
// * 获取设备信息
// */
// suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
// return safeApiCall {
// apiService.getDeviceInfo(
// equipmentCode,
// token = token
// )
// }
// }
//
// /**
// * 生成token
// */
// suspend fun generateToken(deviceId: String): ApiResponse<String> {
// return safeApiCall {
// apiService.generateToken(
// deviceId = deviceId,
// appVersion = GlobalData.appVersion,
// equipmentCode = GlobalData.globalEquipmentCode
// )
// }
// }
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(): ApiResponse<List<UserFaceModel>> {
return safeApiCall { apiService.getUserFaceCache() }
suspend fun getUserFaceCache(
pageNum: Int,
pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>> {
return safeApiCall {
apiService.getUserFaceCache(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
/**
* 登录
*/
suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.equipmentBoxLogin(param) }
return safeApiCall { apiService.equipmentBoxLogin(param = param) }
}
/**
* 获取设备绑定用户列表
*/
suspend fun getEquipmentList(param: EquipmentParam): ApiResponse<List<EquipmentUserInfo>> {
return safeApiCall { apiService.getEquipmentList(param) }
suspend fun getEquipmentList(): ApiResponse<List<EquipmentUserInfo>> {
return safeApiCall { apiService.getEquipmentList() }
}
/**
* 餐盘用户信息绑定解绑
*/
suspend fun bindEquipment(param: BindParam): ApiResponse<EquipmentUserInfo?> {
return safeApiCall { apiService.bindEquipment(param) }
// /**
// * 餐盘用户信息绑定解绑
// */
// suspend fun bindEquipment(param: BindParam): ApiResponse<EquipmentUserInfo?> {
// return safeApiCall { apiService.bindEquipment(param = param) }
// }
suspend fun plateBind(param: BindParam): ApiResponse<EquipmentUserInfo?> {
return safeApiCall { apiService.plateBind(param = param) }
}
suspend fun plateUnbind(id: Long?): ApiResponse<Any?> {
return safeApiCall { apiService.plateUnbind(id = id) }
}
/**
* 用户信息模糊搜索
*/
@POST("/swclientUserInfoShop/selectList")
suspend fun searchUser(param: SearchParam): ApiResponse<SearchResult> {
return safeApiCall { apiService.searchUser(param) }
suspend fun searchUser(param: SearchParam): ApiResponse<List<SearchResult.Member>?> {
return safeApiCall { apiService.searchUser(param = param) }
}
/**
* 通过餐盘编号获取绑定信息
*/
suspend fun findByPlateNumber(plateNumber: String): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.findByPlateNumber(plateNumber = plateNumber) }
}
/**
* 获取人脸增量数据
*
* @param timeDate 时间戳(毫秒级)
*/
// suspend fun getHeartbeatInterface(timeDate: Long): ApiResponse<List<UserFaceModel>> {
// return safeApiCall { apiService.getHeartbeatInterface(timeDate = timeDate) }
// }
suspend fun getFaceIncrementList(
pageNum: Long,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
}
@@ -0,0 +1,237 @@
package com.sw.platecabinet.utils
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import com.sw.platecabinet.activity.InitActivity
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
/**
* 崩溃处理
*/
class CrashHandler private constructor(private val context: Context) :
Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_REPORTS_DIR = "crash_reports"
private const val LOG_LINES = 500 // 收集最近500行日志
@Volatile
private var instance: CrashHandler? = null
fun init(context: Context) {
if (instance == null) {
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
}
}
}
}
fun getCrashReportFiles(context: Context): Array<File> {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
return if (crashDir.exists() && crashDir.isDirectory) {
crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
} else {
emptyArray()
}
}
fun clearCrashReports(context: Context) {
getCrashReportFiles(context).forEach { it.delete() }
}
}
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, ex: Throwable) {
handleException(thread, ex)
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
// 否则自己结束程序
defaultHandler?.uncaughtException(thread, ex) ?: run {
Process.killProcess(Process.myPid())
exitProcess(1)
}
}
/**
* 自动重启app
*/
private fun restartApp() {
// 延迟1秒后重启应用
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(context, InitActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
Process.killProcess(Process.myPid())
exitProcess(1)
}, 1000)
}
private fun handleException(thread: Thread, ex: Throwable) {
// 收集设备信息和异常信息
val crashInfo = collectCrashInfo(thread, ex)
// 保存日志文件
saveCrashInfoToFile(crashInfo)
// 这里可以添加其他处理逻辑,比如上传到服务器等
}
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
return buildString {
// 收集设备信息
collectDeviceInfo(this)
// 收集应用日志
append("\n\n").append(collectLogs())
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
append("Thread: ${thread.name}\n")
append("Stack Trace:\n")
val sw = StringWriter()
val pw = PrintWriter(sw)
ex.printStackTrace(pw)
var cause: Throwable? = ex.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
append(sw.toString())
}
}
private fun collectDeviceInfo(sb: StringBuilder) {
sb.append("========== Device Info ==========\n")
try {
// 应用信息
val pm = context.packageManager
val pi = pm.getPackageInfo(context.packageName, 0)
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
// Android 版本信息
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
sb.append(
"Current Time: ${
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
}\n"
)
// 内存信息
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
} catch (e: Exception) {
Timber.e(e, "Error while collecting device info")
sb.append("Error while collecting device info: ${e.message}\n")
}
}
private fun collectLogs(): String {
return buildString {
append("========== Application Logs ==========\n")
try {
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
val reader = process.inputStream.bufferedReader()
val logLines = reader.readLines()
val start = kotlin.comparisons.maxOf(0, logLines.size - LOG_LINES)
logLines.subList(start, logLines.size).forEach {
append(it).append("\n")
}
} catch (e: IOException) {
Timber.e(e, "Error collecting logs")
append("Error collecting logs: ${e.message}\n")
}
}
}
private fun saveCrashInfoToFile(crashInfo: String) {
try {
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val fileName = "crash_$time.log"
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() && !crashDir.mkdirs()) {
Timber.tag(TAG).e("Failed to create crash report directory")
return
}
val crashFile = File(crashDir, fileName)
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error saving crash info to file")
}
}
/**
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
}
}
@@ -2,6 +2,7 @@ package com.sw.inbound.utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -19,6 +20,13 @@ object DateTimeUtils {
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
}
/**
* 获取标准时间格式
*/
fun getDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(date)
}
/**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/
@@ -60,21 +68,22 @@ object DateTimeUtils {
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
format.parse(timeString)
} catch (e: Exception) {
e.printStackTrace()
// e.printStackTrace()
Timber.e(e.message)
null
}
}
/**
* 判断给定时间是否距离当前时间超过36小时
* 判断给定时间是否距离当前时间超过72小时
* @param timeInMillis 时间戳(毫秒)
* @return true 表示超过36小时,false 表示未超过
* @return true 表示超过72小时,false 表示未超过
*/
fun isMoreThan36HoursFromNow(timeInMillis: Long): Boolean {
fun isMoreThanHoursFromNow(timeInMillis: Long): Boolean {
val currentTime = System.currentTimeMillis()
val timeDifference = currentTime - timeInMillis
val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
return hoursDifference >= 36
return hoursDifference >= 72
}
/**
@@ -102,8 +111,6 @@ object DateTimeUtils {
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
// hours < 48 -> "昨天"
// hours < 72 -> "前天"
else -> {
val days = hours / 24
"${days}天前"
@@ -0,0 +1,25 @@
package com.sw.platecabinet.utils
class Debouncer(private val delayMillis: Long) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
action()
return true
}
return false
}
// 重置防抖计时
fun reset() {
lastActionTime = 0L
}
}
@@ -0,0 +1,66 @@
package com.sw.platecabinet.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class IntervalExecutor {
/**
* 启动定时任务
* @param delayMillis 延迟时间(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
action()
delay(delayMillis)
}
}
}
/**
* 启动定时任务(带初始延迟)
* @param initialDelay 初始延迟时间(毫秒)
* @param delayMillis 后续执行间隔(毫秒)
* @param action 要执行的方法
* @return Job 可用于取消任务
*/
fun startIntervalTaskWithInitialDelay(
initialDelay: Long,
delayMillis: Long,
action: suspend () -> Unit
): Job {
return CoroutineScope(Dispatchers.Default).launch {
delay(initialDelay)
while (isActive) {
action()
delay(delayMillis)
}
}
}
}
// 使用示例
fun main() = runBlocking {
val executor = IntervalExecutor()
// 示例1:每隔10秒执行一次
val job1 = executor.startIntervalTask(10000) {
println("定时任务执行: ${System.currentTimeMillis()}")
// 这里可以执行你的业务逻辑
}
// 示例2:先延迟5秒,然后每隔3秒执行一次
val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) {
println("带初始延迟的定时任务: ${System.currentTimeMillis()}")
}
// 运行30秒后取消任务
delay(30000)
job1.cancel()
job2.cancel()
println("所有定时任务已取消")
}
@@ -0,0 +1,29 @@
package com.sw.platecabinet.utils
import android.app.Activity
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
/**
* 键盘工具类
*/
object KeyboardUtils {
/**
* 显示键盘
*/
fun showKeyboard(view: View) {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
/**
* 隐藏键盘
*/
fun hideKeyboard(activity: Activity) {
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val view = activity.currentFocus ?: View(activity)
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}
@@ -0,0 +1,88 @@
package com.sw.platecabinet.utils
import com.sw.platecabinet.model.response.EquipmentUserInfo
import timber.log.Timber
import kotlin.math.ceil
/**
* 列表排序工具类
*/
object ListArrangementUtil {
/**
* page item水平排序
*/
fun horizontalSortPageItem(
list: List<EquipmentUserInfo>,
columns: Int,
defaultValue: EquipmentUserInfo = EquipmentUserInfo()
): List<EquipmentUserInfo> {
Timber.d("horizontalSortPageItem")
require(columns > 0) { "Columns must be positive" }
val filledList = list.padToMultiple(columns, defaultValue)
val chunks = filledList.chunked(columns)
return (0 until columns)
.windowed(2, 2, partialWindows = true)
.flatMap { group ->
when (group.size) {
2 -> chunks.flatMap { listOf(it[group[0]], it[group[1]]) }
1 -> chunks.flatMap {
listOf(it[group[0]], EquipmentUserInfo(equipmentBoxCode = null))
}.dropLast(1)
else -> error("Unexpected group size")
}
}
}
/**
* item 垂直排序
*/
fun verticalSortItem(list: List<EquipmentUserInfo>): List<EquipmentUserInfo> {
Timber.d("verticalSortItem")
val users = list.padToEvenSize()
val half = users.size / 2
return users.take(half)
.zip(users.drop(half)) { a, b -> listOf(a, b) }
.flatten()
}
/**
* page item垂直排序
*/
fun convertToColumnFirst(
original: List<EquipmentUserInfo>,
cols: Int
): List<EquipmentUserInfo> {
Timber.d("convertToColumnFirst")
require(cols > 0) { "Columns must be positive" }
val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos ->
original.getOrNull(pos % rows * cols + pos / rows) ?: EquipmentUserInfo()
}
}
// 提取的扩展函数
private fun List<EquipmentUserInfo>.padToMultiple(
multiple: Int,
defaultValue: EquipmentUserInfo
): List<EquipmentUserInfo> {
return if (size % multiple != 0) {
this + List(multiple - (size % multiple)) { defaultValue }
} else {
this
}
}
private fun List<EquipmentUserInfo>.padToEvenSize(): List<EquipmentUserInfo> {
return if (size % 2 != 0) {
Timber.d("列表长度必须是偶数")
this + EquipmentUserInfo()
} else {
this
}
}
}
@@ -9,21 +9,19 @@ import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
class PermissionHelper private constructor(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int,
private val rationale: String? = null
internal val requestCode: Int,
private val rationale: String? = null,
private val onGranted: (() -> Unit)?
) {
private var onGranted: (() -> Unit)? = null
companion object {
/**
* 创建权限请求构建器
* @param context 上下文(Activity或Fragment
* @param permissions 需要请求的权限数组
* @param requestCode 请求码
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
@@ -32,7 +30,7 @@ class PermissionHelper private constructor(
/**
* 检查是否已授予所有权限
*/
fun areAllPermissionsGranted(context: Context, permissions: Array<String>): Boolean {
fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
return permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
@@ -43,15 +41,17 @@ class PermissionHelper private constructor(
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
* @return 是否所有权限都已授予
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
onPermissionPermanentlyDenied: (List<String>) -> Unit
permissionHelper: PermissionHelper? = null
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionHelper?.onGranted?.invoke()
return true
}
@@ -67,10 +67,6 @@ class PermissionHelper private constructor(
}
}
}
if (permanentlyDeniedPermissions.isNotEmpty()) {
onPermissionPermanentlyDenied(permanentlyDeniedPermissions)
}
return false
}
@@ -83,6 +79,8 @@ class PermissionHelper private constructor(
}
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
@@ -93,9 +91,10 @@ class PermissionHelper private constructor(
private val requestCode: Int
) {
private var rationale: String? = null
private var onGranted: (() -> Unit)? = null
/**
* 设置权限说明(当需要向用户解释权限用途时显示)
* 设置权限说明
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
@@ -105,10 +104,16 @@ class PermissionHelper private constructor(
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): PermissionHelper {
val helper = PermissionHelper(context, permissions, requestCode, rationale)
helper.onGranted = callback
return helper
fun onGranted(callback: () -> Unit): Builder {
this.onGranted = callback
return this
}
/**
* 构建PermissionHelper实例
*/
fun build(): PermissionHelper {
return PermissionHelper(context, permissions, requestCode, rationale, onGranted)
}
}
@@ -116,7 +121,7 @@ class PermissionHelper private constructor(
* 检查并请求权限
*/
fun checkAndRequest() {
if (areAllPermissionsGranted(context, permissions)) {
if (checkPermissions(context, permissions)) {
onGranted?.invoke()
} else {
requestPermissions()
@@ -126,13 +131,11 @@ class PermissionHelper private constructor(
private fun requestPermissions() {
val activity = context as? Activity ?: return
// 检查是否需要显示权限说明
val shouldShowRationale = permissions.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale && rationale != null) {
// 显示解释对话框
AlertDialog.Builder(activity)
.setTitle("权限说明")
.setMessage(rationale)
@@ -142,7 +145,6 @@ class PermissionHelper private constructor(
.setNegativeButton("取消", null)
.show()
} else {
// 直接请求权限
doRequestPermissions(activity)
}
}
@@ -0,0 +1,103 @@
package com.sw.platecabinet.utils
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlin.apply
import kotlin.ranges.until
import kotlin.text.isEmpty
/**
* 二维码生成工具类
*/
object QRCodeUtil {
/**
* 生成二维码(默认大小)
* @param content 二维码内容
* @return 生成的二维码Bitmap
*/
@JvmOverloads
fun generateQRCode(content: String, size: Int = 500): Bitmap? {
return generateQRCode(content, size, Color.BLACK, Color.WHITE)
}
/**
* 生成二维码(自定义颜色)
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param colorCode 二维码颜色
* @param backgroundColor 背景颜色
* @return 生成的二维码Bitmap
*/
fun generateQRCode(
content: String,
size: Int,
colorCode: Int,
backgroundColor: Int
): Bitmap? {
if (content.isEmpty()) {
return null
}
return try {
val hints = mutableMapOf<EncodeHintType, Any>().apply {
put(EncodeHintType.CHARACTER_SET, "UTF-8")
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H) // 纠错级别
put(EncodeHintType.MARGIN, 1) // 边距
}
val bitMatrix = QRCodeWriter().encode(
content,
BarcodeFormat.QR_CODE,
size,
size,
hints
)
val pixels = IntArray(size * size).apply {
for (y in 0 until size) {
for (x in 0 until size) {
this[y * size + x] = if (bitMatrix.get(x, y)) colorCode else backgroundColor
}
}
}
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
setPixels(pixels, 0, size, 0, 0, size, size)
}
} catch (e: WriterException) {
e.printStackTrace()
null
}
}
/**
* 生成带Logo的二维码
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param logo Logo Bitmap
* @return 带Logo的二维码Bitmap
*/
fun generateQRCodeWithLogo(content: String, size: Int, logo: Bitmap?): Bitmap? {
val qrCode = generateQRCode(content, size) ?: return null
logo ?: return qrCode
val logoSize = size / 5 // Logo大小约为二维码的1/5
val scaledLogo = Bitmap.createScaledBitmap(logo, logoSize, logoSize, false)
val offset = (size - logoSize) / 2
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
val canvas = Canvas(this)
canvas.drawBitmap(qrCode, 0f, 0f, null)
canvas.drawBitmap(scaledLogo, offset.toFloat(), offset.toFloat(), null)
}
}
}
@@ -0,0 +1,20 @@
package com.sw.platecabinet.utils;
import com.sw.inbound.utils.SPUtil;
import com.sw.platecabinet.MyApp;
public class SpTool {
public static final String LAST_FACE_TIMESTAMP = "faceTimestamp";
public static long getLastFaceTimestamp() {
return SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").get(LAST_FACE_TIMESTAMP, 0L);
}
public static void setLastFaceTimestamp(long timestamp) {
SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").put(LAST_FACE_TIMESTAMP, timestamp);
}
}
@@ -10,13 +10,16 @@ import android.view.View;
import android.view.Window;
import android.view.WindowManager;
public class CustomDialog extends Dialog {
/**
* 加载进度弹窗
*/
public class CustomLoadingDialog extends Dialog {
/**
* 宽高由布局文件中指定但是最底层的宽度无效可以多嵌套一层解决
*/
public CustomDialog(Context context, View layout, int style) {
public CustomLoadingDialog(Context context, View layout, int style) {
super(context, style);
@@ -35,7 +38,7 @@ public class CustomDialog extends Dialog {
/**
* 宽高由该方法的参数设置
*/
public CustomDialog(Context context, int width, int height, View layout,
public CustomLoadingDialog(Context context, int width, int height, View layout,
int style) {
super(context, style);
// 设置内容
@@ -3,7 +3,9 @@ package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentInfo
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.flow.MutableStateFlow
@@ -62,4 +64,13 @@ abstract class BaseViewModel() : ViewModel() {
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
}
fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
// GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
// GlobalData.appBaseUrl="https://vip.shuziweidao.com"
// GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
// GlobalData.appId = equipmentInfo.arcsoftAppId!!
// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
// GlobalData.restId = equipmentInfo.canteenId!!
}
}
@@ -0,0 +1,68 @@
//package com.sw.platecabinet.viewmodel
//
//import com.sw.inbound.utils.GsonUtils
//import com.sw.inbound.utils.SPUtil
//import com.sw.platecabinet.GlobalData
//import com.sw.platecabinet.GlobalKey
//import com.sw.platecabinet.model.response.EquipmentInfo
//import kotlinx.coroutines.flow.MutableStateFlow
//import kotlinx.coroutines.flow.StateFlow
//import timber.log.Timber
//import kotlin.jvm.java
//
///**
// * 初始化的viewmodel
// */
//class DeviceViewModel : BaseViewModel() {
// private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
// val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
//
// /**
// * 获取token
// */
// fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
// launchWithLoading {
// val response = repository.getDeviceToken(deviceId)
// if (parseResponse(response)) {
// val response1 = repository.getDeviceInfo(deviceId, response.result!!)
//
// if (parseResponse(response1)) {
// val equipmentInfo = response1.result
// if (equipmentInfo == null) return@launchWithLoading
// try {
// parseEquipmentInfo(equipmentInfo)
// SPUtil.getInstance()
// .put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
// _deviceInfoResult.value = true
// } catch (e: Exception) {
// Timber.e(e)
// }
// }
// }
// }
// }
//
// /**
// * 检查缓存数据
// */
// fun checkEquipmentInfo(): Boolean {
// val equipmentInfoStr = SPUtil.getInstance().get(GlobalKey.KEY_EQUIPMENT_INFO, "")
// if (equipmentInfoStr == null) {
// Timber.e("获取缓存设备信息失败")
// return false
// }
// val equipmentInfo =
// GsonUtils.fromJson<EquipmentInfo>(equipmentInfoStr, EquipmentInfo::class.java)
// if (equipmentInfo == null) {
// Timber.e("解析缓存设备信息失败")
// return false
// }
// try {
// parseEquipmentInfo(equipmentInfo)
// return true
// } catch (e: Exception) {
// Timber.e(e)
// return false
// }
// }
//}
@@ -1,21 +1,237 @@
package com.sw.platecabinet.viewmodel
import android.text.TextUtils
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.model.ErrorInfo
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* 管理员操作viewmodel
*/
class SettingViewModel : BaseViewModel() {
// 设备绑定列表
private val _equipmentList = MutableStateFlow<List<EquipmentUserInfo>>(emptyList())
val equipmentList: StateFlow<List<EquipmentUserInfo>> = _equipmentList
// private val _equipmentList = MutableStateFlow<List<EquipmentUserInfo>>(emptyList())
// val equipmentList: StateFlow<List<EquipmentUserInfo>> = _equipmentList
fun getEquipmentList(param: EquipmentParam) {
private val _searchMemberList = MutableStateFlow<List<SearchResult.Member>>(emptyList())
val searchMemberList: StateFlow<List<SearchResult.Member>> = _searchMemberList
private val _searchUserInfo = MutableStateFlow<EquipmentUserInfo?>(null)
val searchUserInfo: StateFlow<EquipmentUserInfo?> = _searchUserInfo
var currentPage = 1
var isLoading = false
var canLoadMore = true
/**
* 绑定/解绑状态 <绑定/解绑, 错误信息,>
*/
// private val _bindStateChange = MutableStateFlow<Pair<Boolean?, ErrorInfo>>(null to ErrorInfo())
// val bindStateChange: StateFlow<Pair<Boolean?, ErrorInfo>> = _bindStateChange
/**
* 获取设备绑定用户信息列表
*/
// fun getEquipmentList(equipmentCode: String = globalEquipmentCode) {
// launchWithLoading {
// val param =
// EquipmentParam(appVersion = GlobalData.appVersion, equipmentCode = equipmentCode)
// val response = repository.getEquipmentList(param)
// if (parseResponse(response)) {
// _equipmentList.value = response.data ?: emptyList()
// }
// }
//// val list = mutableListOf<EquipmentUserInfo>()
//// for (i in 1 .. 30){
//// list.add(EquipmentUserInfo(id = i, equipmentBoxCode = i.toString()))
//// }
//// _equipmentList.value = list
// }
fun getEquipmentList(block: (List<EquipmentUserInfo>) -> Unit) {
launchWithLoading {
val response = repository.getEquipmentList(param)
val response = repository.getEquipmentList()
if (parseResponse(response)) {
_equipmentList.value = response.data ?: emptyList()
val list = response.data ?: emptyList()
block(list)
}
}
}
/**
* 搜索会员信息列表
*/
fun getSearchMemberList(param:String, pageNum: Int = 1, pageSize: Int = 50) {
if (TextUtils.isEmpty(param)) return
currentPage = pageNum
var name =""
var phone =""
val num = param.toIntOrNull()
if (num != null) {
phone = param
} else {
name = param
}
launch {
val searchParam = SearchParam(
// appVersion = GlobalData.appVersion,
name = name,
phone = phone,
pageNum = pageNum,
pageSize = pageSize
)
isLoading = true
val response = repository.searchUser(searchParam)
isLoading = false
if (parseResponse(response)) {
response.data?.let { list ->
if (pageNum == 1) {
_searchMemberList.value = list
} else {
_searchMemberList.value = _searchMemberList.value.plus(list)
}
if (list.size >= pageSize) currentPage++
}
}
}
}
// fun bindPlate(
// equipmentBoxCode: String,
// equipmentCode: String = globalEquipmentCode,
// memberId: Int,
// plateNumber: String
// ) {
// _bindStateChange.value = null to ErrorInfo()
// launchWithLoading {
// val bindParam = BindParam(
// appVersion = GlobalData.appVersion,
// equipmentBoxCode = equipmentBoxCode,
// equipmentCode = equipmentCode,
// memberId = memberId,
// plateNumber = plateNumber
// )
// val response = repository.bindEquipment(bindParam)
// if (parseResponse(response)) {
// var userInfo = response.data
// if (userInfo == null) {
// userInfo = EquipmentUserInfo()
// userInfo.equipmentBoxCode = equipmentBoxCode
// userInfo.plateNumber = plateNumber
// userInfo.equipmentCode = equipmentCode
// }
// _bindStateChange.value = true to ErrorInfo(equipmentUserInfo = userInfo)
// } else {
// _bindStateChange.value = true to ErrorInfo(response.code, response.msg.toString())
// }
// }
// }
// fun unbindPlate(
// equipmentBoxCode: String,
// equipmentCode: String = globalEquipmentCode
// ) {
// launchWithLoading {
// val bindParam = BindParam(
// appVersion = GlobalData.appVersion,
// equipmentBoxCode = equipmentBoxCode,
// equipmentCode = equipmentCode
// )
// val response = repository.bindEquipment(bindParam)
// if (parseResponse(response)) {
// _bindStateChange.value = false to ErrorInfo()
// } else {
// _bindStateChange.value = false to ErrorInfo(response.code, response.msg.toString())
// }
// }
// }
fun unbindPlate(id: Long?, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
block(null to ErrorInfo())
launchWithLoading {
val response = repository.plateUnbind(id)
if (parseResponse(response)) {
block(false to ErrorInfo())
} else {
block(false to ErrorInfo(response.code, response.msg.toString()))
}
}
}
fun bindPlate(bindParam: BindParam, block:(Pair<Boolean?, ErrorInfo>)-> Unit) {
launchWithLoading {
val response = repository.plateBind(bindParam)
if (parseResponse(response)) {
var userInfo = response.data
if (userInfo == null) {
userInfo = EquipmentUserInfo()
userInfo.equipmentBoxCode = bindParam.equipmentBoxCode
userInfo.plateNumber = bindParam.plateNumber
userInfo.equipmentCode = bindParam.equipmentCode
}
block(true to ErrorInfo(equipmentUserInfo = userInfo))
} else {
block(true to ErrorInfo(response.code, response.msg.toString()))
}
}
}
fun findByPlateNumber(
plateNumber: String,
equipmentCode: String = globalEquipmentCode
) {
_searchUserInfo.value = null
launchWithLoading {
// val bindParam = BindParam(plateNumber = plateNumber)
val response = repository.findByPlateNumber(plateNumber)
if (response.code == "00000") {
_searchUserInfo.value = response.data
} else {
_searchUserInfo.value = EquipmentUserInfo(
plateNumber = plateNumber,
equipmentCode = equipmentCode,
updateTime = DateTimeUtils.getDateTimeString()
)
}
}
}
val PAGE_SIZE = 100
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
block: (List<UserFaceModel>) -> Unit
) {
launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
block(list)
}
}
}
}
}
@@ -1,43 +1,181 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import com.sw.platecabinet.GlobalData
import com.sw.platecabinet.GlobalData.globalEquipmentCode
import com.sw.platecabinet.GlobalKey
import com.sw.platecabinet.model.DeviceConfig
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.SpTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* 用户viewmodel
*/
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()
}
}
}
companion object {
const val PAGE_SIZE = 100
}
fun getUserFaceCache() {
// 通过人脸获取到的用户信息
private val _currentUserInfo = MutableStateFlow<EquipmentUserInfo?>(null)
val currentUserInfo: StateFlow<EquipmentUserInfo?> = _currentUserInfo
private val faceApi: FaceApi = FaceApi()
/**
* 获取token
*/
// fun generateToken(deviceId: String = "SWSN:88:12:AC:4E:D9:CC") {
// launchWithLoading {
// val response = repository.generateToken(deviceId)
// 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(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) {
var currentPageNo = pageNo
launchWithLoading {
val response = repository.getUserFaceCache()
val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize)
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo==1&&list.isEmpty()) {
return@launchWithLoading
}
val faceEntityList = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureStr))
}
withContext(Dispatchers.Default) {
faceApi.updateFaceData(pageNo, faceEntityList)
if (list.size >= pageSize) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
currentPageNo++
getUserFaceCache(currentPageNo)
return@withContext
}
if (list.isNotEmpty()) {
faceTimestamp = list.last().faceUpdateTimestamp?:0
}
SpTool.setLastFaceTimestamp(faceTimestamp)
//activeEngine()
}
}
}
}
private var faceTimestamp = 0L
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
// ThreadUtils.launch {
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
/**
* 校验码登录
*/
fun loginWithPwd(loginParam: LoginParam) {
launchWithLoading {
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
}
}
}
/**
* 激活 arcsoft 人脸
* 通过用户id获取用户信息
*/
fun activeEngine(
appId: String = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj",
sdkKey: String = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k",
activeKey: String = "085F-118G-Q391-53YL"
) {
// val runtimeABI: RuntimeABI? = FaceEngine.getRuntimeABI()
fun getUserInfoById(memberId: String?, action:(EquipmentUserInfo?)->Unit={}) {
_currentUserInfo.value = null
launchWithLoading {
val loginParam = LoginParam(faceId = memberId)
val response = repository.equipmentBoxLogin(loginParam)
if (parseResponse(response)) {
_currentUserInfo.value = response.data
action(response.data)
}
}
}
/**
* 重置用户信息
*/
fun resetUserInfo() {
Timber.d("resetUserInfo")
_currentUserInfo.value = null
}
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
// Timber.tag(TAG).d("getDeviceConfig")
launch {
val response = repository.getDeviceConfig()
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#000000" /> <!-- 整体黑色背景 -->
<stroke
android:width="2dp"
android:color="#333333" /> <!-- 边框颜色和宽度 -->
<corners android:radius="12dp" /> <!-- 圆角半径 -->
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="#ff08C6DC" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="600dp"
android:height="1024dp">
<bitmap android:src="@drawable/bg"/>
</item>
<item
android:width="337dp"
android:height="322dp"
android:gravity="top|center_horizontal"
android:top="112dp">
<bitmap android:src="@drawable/img_init" android:scaleType="fitCenter"/>
</item>
</layer-list>
@@ -4,5 +4,5 @@
<corners android:radius="12dp" />
<stroke
android:width="2dp"
android:color="#887872" />
android:color="#FFCC99" />
</shape>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="#F0C8B4" />
<solid android:color="#FFCC99" />
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

+1 -4
View File
@@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp" />
<!-- <solid android:color="#F3D2BD"/>-->
<gradient
android:endColor="#E5B79F"
android:startColor="#F9DDC9" />
<solid android:color="#FFCC99" />
</shape>
+1 -1
View File
@@ -3,5 +3,5 @@
<corners android:radius="12dp" />
<stroke
android:width="2dp"
android:color="#F0C8B4" />
android:color="#FFCC99" />
</shape>
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="337dp"
android:layout_height="322dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="112dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<TextView
android:id="@+id/btnInit"
android:layout_width="225dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="75dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold"
android:visibility="invisible"
tools:ignore="HardcodedText" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="37dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512"
android:visibility="invisible"/>
</LinearLayout>
+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg"
android:orientation="vertical"
tools:context=".activity.InitActivity">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="142dp"
android:src="@drawable/ic_init_text"
android:adjustViewBounds="true"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="345dp"
android:layout_marginTop="90dp"
android:src="@drawable/ic_init_img"
android:adjustViewBounds="true" />
<!-- <ImageView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="20dp"-->
<!-- android:src="@drawable/ic_init_press" />-->
<TextView android:id="@+id/takeButton"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="点击取盘"
android:textColor="#FFCC99"
android:textSize="24sp"
android:paddingHorizontal="45dp"
android:gravity="center"
android:background="@drawable/btn_outline"
tools:ignore="HardcodedText" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
@@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#14141E"
android:background="@drawable/bg"
android:orientation="vertical">
<include
@@ -21,7 +21,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="手机后四位"
android:textColor="#F0C8B4"
android:textColor="#FFCC99"
android:textSize="26sp"
android:textStyle="bold" />
@@ -33,18 +33,20 @@
android:gravity="center_vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="55dp"
android:src="@mipmap/ic_phone" />
android:src="@drawable/ic_phone" />
<EditText
android:id="@+id/et_phone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
android:background="@android:color/transparent"
android:hint="输入手机后四位"
android:imeOptions="actionNext"
android:inputType="number"
android:maxLength="4"
android:maxLines="1"
@@ -60,7 +62,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:text="校验码"
android:textColor="#F0C8B4"
android:textColor="#FFCC99"
android:textSize="26sp"
android:textStyle="bold" />
@@ -72,22 +74,24 @@
android:gravity="center_vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="55dp"
android:src="@mipmap/ic_password" />
android:src="@drawable/ic_pwd" />
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
android:background="@android:color/transparent"
android:hint="输入校验码"
android:inputType="textPassword"
android:inputType="numberPassword"
android:maxLines="1"
android:textColor="#F3D2BD"
android:textColorHint="#7B6D6A"
android:imeOptions="actionDone"
android:textSize="26sp"
android:textStyle="bold" />
@@ -111,7 +115,7 @@
android:layout_marginTop="40dp"
android:padding="8dp"
android:text="人脸识别"
android:textColor="#887872"
android:textColor="#FFCC99"
android:textSize="26sp"
android:textStyle="bold" />
</LinearLayout>
+55 -31
View File
@@ -4,7 +4,8 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#14141E"
android:background="@drawable/bg"
android:fitsSystemWindows="false"
android:orientation="vertical"
tools:context=".activity.LoginByFaceActivity">
@@ -18,46 +19,57 @@
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:visibility="visible">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="0dp"
android:paddingTop="1dp">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="0dp"
android:background="@drawable/bg_face"
android:visibility="visible" />
<TextView
android:id="@+id/tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="159dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="129dp"
android:text="请正视屏幕"
android:textColor="#fff0c8b4"
android:textStyle="bold"
android:textColor="#FFCC99"
android:textSize="36sp" />
<FrameLayout
android:layout_width="380dp"
android:layout_height="380dp"
android:layout_marginTop="119dp"
android:padding="10dp"
android:visibility="visible">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_margin="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="0dp"
android:background="@drawable/circle_dashed_border" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/bg_face" />
</FrameLayout>
<LinearLayout
android:id="@+id/ll_to_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"
android:background="@drawable/btn_outline_887872"
android:layout_marginStart="208dp"
android:layout_marginTop="730dp"
android:background="@drawable/btn_outline"
android:gravity="center"
android:paddingHorizontal="45dp"
android:paddingVertical="18dp">
@@ -66,9 +78,21 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="密码登录"
android:textColor="#ff887872"
android:textColor="#FFCC99"
android:textSize="24sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_to_init"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="30"
android:padding="12dp"
android:gravity="center"
android:layout_marginTop="185dp"
android:textColor="#FFCC99"
android:textSize="36sp" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
+1 -1
View File
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#14141E"
android:background="@drawable/bg"
tools:context=".activity.MainActivity">
<include
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="1024dp"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/dialogContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="0dp"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="30dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tv_tip_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
tools:text="即将解除【曹德磊】的餐盘"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="解绑后该用户将无法使用餐盘柜"
android:textColor="#ff999999"
android:textSize="24sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_cancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="取消"
android:textColor="#ff999999"
android:textSize="30sp"
android:textStyle="bold" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#333333" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="确定解绑"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dialogContainer"
android:layout_width="480dp"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="0dp"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical"
tools:ignore="HardcodedText">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tv_tip_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="余额不足"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="请通过小程序或到柜台充值后使用"
android:textColor="#ff999999"
android:textSize="24sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="match_parent"
android:layout_height="90dp"
android:gravity="center"
android:text="知道了"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dialogContainer"
android:layout_width="480dp"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="0dp"
android:background="@drawable/bg_dialog"
android:gravity="center"
android:orientation="vertical"
tools:ignore="HardcodedText">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:src="@drawable/ic_tip_warn_unbind" />
<TextView
android:id="@+id/tv_tip_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="温馨提示"
android:textColor="#ffffffff"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvBindContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="请通过小程序或到柜台充值后使用"
android:textColor="#ff999999"
android:textSize="24sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="30dp"
android:background="#ff333333" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="match_parent"
android:layout_height="90dp"
android:gravity="center"
android:text="知道了"
android:textColor="#ffffcc99"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
@@ -15,6 +15,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/white"
android:text="请稍等"
android:textSize="22sp" />
<ProgressBar
+48 -29
View File
@@ -1,48 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingHorizontal="60dp"
tools:context=".fragment.BindPlateFragment">
tools:context=".fragment.BindPlateFragment"
tools:background="@color/black">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:layout_marginTop="60dp"
android:text="柜体编号"
android:textColor="#ff7c6d6a"
android:textColor="#999999"
android:textSize="20sp" />
<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
tools:text="1-12"
android:textColor="#fff0c8b4"
android:layout_marginTop="20dp"
android:textColor="#FFCC99"
android:textSize="36sp"
android:textStyle="bold" />
android:textStyle="bold"
tools:text="1-12" />
<ImageView
android:layout_width="280dp"
android:layout_height="24dp"
android:layout_marginTop="24dp"
android:src="@mipmap/ic_bind_img" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:src="@drawable/ic_bind_img" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="54dp"
android:layout_marginTop="48dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="餐盘码"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="26sp" />
<Space
@@ -54,14 +56,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="确认餐盘码是否有误"
android:textColor="#ff7c6d6a"
android:textColor="#666666"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:layout_marginTop="21dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
@@ -71,23 +73,27 @@
android:layout_height="match_parent"
android:background="@color/transparent"
android:gravity="center"
android:textColor="#fff0c8b4"
android:hint="请录入餐盘码"
android:inputType="number"
android:maxLines="1"
android:textColor="#FFCC99"
android:textColorHint="#999999"
android:textSize="26sp"
android:textStyle="bold"
tools:text="100888" />
tools:text="" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_marginTop="22dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="会员"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="26sp" />
<Space
@@ -99,40 +105,53 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="仅限VIP会员绑定,非VIP暂不支持"
android:textColor="#ff7c6d6a"
android:textColor="#666666"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:layout_marginTop="22dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
<EditText
android:id="@+id/et_user_info"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@color/transparent"
android:gravity="center"
android:hint="请输入会员姓名或手机号"
android:textColor="#F0C8B4"
android:textColorHint="#ff7c6d6a"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:textColor="#FFCC99"
android:textColorHint="#999999"
android:textSize="26sp" />
<ImageView
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
android:src="@drawable/ic_search" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="188dp"
android:layout_marginTop="24dp" />
android:layout_marginTop="24dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2" />
<LinearLayout
android:id="@+id/ll_bind"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="28dp"
android:layout_marginTop="24dp"
android:background="@drawable/shape_btn_bg"
android:gravity="center">
@@ -140,7 +159,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="绑定开门"
android:textColor="#ff11111c"
android:textColor="#11111C"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
@@ -149,9 +168,9 @@
android:id="@+id/tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginTop="46dp"
android:paddingHorizontal="20dp"
android:text="返回"
android:textColor="#ff887872"
android:textColor="#FFCC99"
android:textSize="26sp" />
</LinearLayout>
@@ -5,19 +5,20 @@
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="80dp"
tools:context=".fragment.PlateCabinetFullFragment">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_full" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_full" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="餐盘柜已满"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="36sp" />
<TextView
@@ -26,7 +27,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="3秒后返回主屏"
android:textColor="#ff887872"
android:textColor="#FFCC99"
android:textSize="20sp" />
@@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingBottom="80dp"
android:orientation="vertical">
<FrameLayout
@@ -27,11 +28,13 @@
android:orientation="vertical">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_plate_open" />
android:id="@+id/iv_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_tip_success" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
@@ -40,11 +43,15 @@
android:textSize="36sp" />
<TextView
android:id="@+id/tv_sub_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
android:text="餐盘取出后请关闭柜门"
android:textColor="#ff887872"
android:layout_marginHorizontal="20dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#FFCC99"
android:textSize="24sp" />
<TextView
@@ -53,7 +60,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="34dp"
android:text="3"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="36sp" />
</LinearLayout>
</FrameLayout>
@@ -7,12 +7,9 @@
android:orientation="vertical"
tools:context=".fragment.SettingListFragment">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainRecyclerView"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@@ -10,17 +10,18 @@
<ImageView
android:layout_width="280dp"
android:layout_height="24dp"
android:layout_marginTop="92dp"
android:src="@mipmap/ic_unbind_img" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:rotation="180"
android:src="@drawable/ic_bind_img" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:text="柜体编号"
android:textColor="#ff7c6d6a"
android:textColor="#999999"
android:textSize="20sp" />
<TextView
@@ -30,7 +31,7 @@
android:layout_marginTop="23dp"
tools:text="1-12"
android:textStyle="bold"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="36sp" />
@@ -53,7 +54,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="餐盘码"
android:textColor="#ff7c6d6a"
android:textColor="#999999"
android:textSize="24sp" />
<Space
@@ -66,7 +67,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="100888"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
@@ -88,7 +89,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="姓名"
android:textColor="#ff7c6d6a"
android:textColor="#999999"
android:textSize="24sp" />
<Space
@@ -101,7 +102,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="曹*磊"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
@@ -123,7 +124,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="手机号"
android:textColor="#ff7c6d6a"
android:textColor="#999999"
android:textSize="24sp" />
<Space
@@ -136,7 +137,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="138****8000"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
@@ -157,7 +158,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最近使用"
android:textColor="#ff7c6d6a"
android:textColor="#999999"
android:textSize="24sp" />
<Space
@@ -170,7 +171,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="14天前使用"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
@@ -179,7 +180,7 @@
<LinearLayout
android:id="@+id/ll_unbind"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_height="70dp"
android:layout_marginTop="187dp"
android:background="@drawable/shape_btn_bg"
android:gravity="center">
@@ -188,7 +189,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="解除绑定"
android:textColor="#ff11111c"
android:textColor="#11111C"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
@@ -200,6 +201,6 @@
android:layout_marginTop="48dp"
android:paddingHorizontal="20dp"
android:text="返回"
android:textColor="#ff887872"
android:textColor="#FFCC99"
android:textSize="26sp" />
</LinearLayout>
+8 -8
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="75dp"
android:id="@+id/ll_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_unbind"
android:gravity="center_vertical"
android:orientation="horizontal"
@@ -16,7 +16,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="02"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="24sp"
android:textStyle="bold" />
@@ -27,10 +27,10 @@
android:layout_marginStart="24dp"
android:layout_weight="1"
android:text="未绑定"
android:visibility="gone"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="18sp"
android:textStyle="bold" />
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_bind_info"
@@ -55,10 +55,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="10dp"
android:text="9小时前使用"
android:textColor="#585868"
android:textSize="18sp"
android:textStyle="bold" />
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_bind"
android:padding="18dp"
android:padding="12dp"
android:id="@+id/ll_root"
android:orientation="vertical">
@@ -12,7 +12,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="马*飞(8000)"
android:textColor="#fff0c8b4"
android:textColor="#FFCC99"
android:textSize="26sp"
android:textStyle="bold" />
@@ -22,7 +22,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="138****8000"
android:textColor="#ff7c6d6a"
android:textColor="#999999"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
+6 -5
View File
@@ -3,18 +3,18 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
android:layout_height="40dp"
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/tvLeftDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="#F0C8B4"
android:textColor="#FFCC99"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="2025年6月18日 星期三" />
<TextView
@@ -22,10 +22,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="#F0C8B4"
android:textColor="#FFCC99"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvLeftDate"
app:layout_constraintBottom_toBottomOf="@id/tvLeftDate"
tools:text="14:02:39" />
</androidx.constraintlayout.widget.ConstraintLayout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Some files were not shown because too many files have changed in this diff Show More