diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1772d7e..a4a1aa7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,6 +45,8 @@
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
+
+
= mutableListOf()
+
+ // 当前选中货架下标
+ private var shelfIndex = 0
+
+ // 当前温度(摄氏)
+ private var showTemperatureC = "0"
+
+ // 当前湿度
+ private var showHumidity = "0"
+
+ // 仅特定 deviceId 的部署点需要发送开锁指令
+ private fun isNeedOpenLock() = App.deviceId == "4787e213-90ab-3e32-88e0-ac271a937751"
+
+ private var deviceName = ""
+
+ private lateinit var binding: ActivityHomeBinding
+
+ private val viewModel: NetViewModel by viewModels()
+ private val viewModelV2: NetViewModelV2 by viewModels()
+
+ // 串口协议解析器,通过回调通知本 Activity
+ private fun initScaleManager() {
+ lifecycleScope.launch {
+ val ok = ScaleManager
+ .init(activateCode = ProtocolConstants.ACTIVE_MAP[App.deviceId] ?: "")
+ .onHeartbeat { _, data ->
+ data.weights.forEach { weight ->
+ val shelfNo = weight.index
+ if (shelfNo in 1..list.size) {
+ // 用 slotNo 末尾数字与串口编号匹配
+ list.firstOrNull {
+ it.slotNo?.filter { c -> c.isDigit() }?.toIntOrNull() == shelfNo
+ }?.let { model ->
+ val pos = list.indexOf(model)
+ shelfAdapter.notifyItemChanged(pos)
+ log("getWeightInfo: slotNo=${model.slotNo},realWeight=${weight.grams}")
+ EventBus.getDefault().post(SendWeightEvent(shelfNo, weight.grams))
+ }
+ }
+ }
+ }
+ .onTempReport { _ ->
+ updateLeftStatus(deviceName)
+ }
+ .onLog { message -> log(message) }
+ .start { data ->
+ lastDataReceivedTime = System.currentTimeMillis()
+ noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
+ log("receiveSerialPortData: $data")
+ }
+ log("onCreate: openState=$ok")
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityHomeBinding.inflate(layoutInflater)
+ EventBus.getDefault().register(this)
+ setBackground()
+ setContentView(binding.root)
+ onBackPressedDispatcher.addCallback(this) {
+
+ }
+ updateLeftStatus("")
+ initRecyclerView()
+
+ // 初始化 ScaleManager:注册回调、打开串口、启动接收
+ initScaleManager()
+
+ // 网络状态监听,独立协程,与串口初始化并行执行
+ lifecycleScope.launch {
+ viewModelV2.initCabinetUiState.collect { state ->
+ when (state) {
+ is UiState.Loading -> showProgress()
+ is UiState.Success<*> -> updateUI(state.data)
+ is UiState.Error -> showError(state.msg)
+ else -> {}
+ }
+ }
+ }
+
+ binding.root.postDelayed({
+ sendCmd(START_TEMP_CTRL_CMD)
+ sendCmd(C_TEMP_CMD)
+ }, 5000)
+
+ saveGoodsTask()
+ overdueTask()
+
+ viewModelV2.initCabinet(cabinetId = App.deviceId)
+ }
+
+ private val defDeviceList = listOf(
+ //原410货柜
+ "7a991439-3a12-3ef7-809b-c0258b839473",
+ //原1楼餐厅货柜
+ "4787e213-90ab-3e32-88e0-ac271a937751"
+ )
+
+ private fun getTokenSuccess(data: RespData<*>) {
+ log("getTokenSuccess: $data")
+ data.data?.let {
+ App.accessToken = it.toString()
+ viewModelV2.initCabinet(cabinetId = App.deviceId)
+ }
+ }
+
+ private fun showProgress() {
+ Loading.show(this)
+ }
+ private var cabinetInitResult:CabinetInitResult?=null
+ @SuppressLint("NotifyDataSetChanged")
+ private fun updateUI(data: RespData<*>) {
+ binding.include?.root?.gone()
+ window?.decorView?.postDelayed({ Loading.dismiss() }, 500)
+ cabinetInitResult = data.data as? CabinetInitResult
+ if (cabinetInitResult == null) {
+ log("updateUI: 数据类型错误,期望 CabinetInitResult,实际类型:${data.data?.javaClass?.simpleName}")
+ loadEmptyView()
+ return
+ }
+ deviceName = cabinetInitResult!!.deviceName
+ updateLeftStatus(deviceName)
+ App.canteenId = cabinetInitResult!!.canteenId.toString()
+ var tempList = cabinetInitResult!!.containerGoodsList
+ if (tempList.isNullOrEmpty()) {
+ loadEmptyView()
+ return
+ }
+ list.clear()
+ tempList = GridLayoutTool.initListSortV2(tempList)
+ list.addAll(tempList)
+ shelfAdapter.notifyDataSetChanged()
+ }
+
+ private fun showError(message: String) {
+ Loading.dismiss()
+ toast(message)
+ loadEmptyView()
+ }
+
+ private var tipDialog: CommonDialog? = null
+
+ /** 最后一次成功收到串口数据的时间戳(毫秒) */
+ private var lastDataReceivedTime = System.currentTimeMillis()
+
+ /** 无数据超时提示弹窗 */
+ private var noDataWarningDialog: CommonDialog? = null
+
+ /**
+ * 无数据看门狗:每30秒检查一次,若超过5分钟未收到数据则弹出提示。
+ * 收到数据后会自动重置计时并关闭弹窗。
+ */
+ private val noDataWatchdogTask = object : Runnable {
+ override fun run() {
+ // 生命周期检查,防止 Activity 销毁后继续执行
+ if (isFinishing || isDestroyed) return
+
+ val elapsed = System.currentTimeMillis() - lastDataReceivedTime
+ if (elapsed >= 5 * 60 * 1000L) {
+ if (noDataWarningDialog == null) {
+ noDataWarningDialog = CommonDialog(this@HomeV2Activity).apply {
+ dialogTitle = "设备提示"
+ dialogContent =
+ "长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备"
+ }
+ }
+ if (noDataWarningDialog?.isShowing == false) {
+ noDataWarningDialog?.show()
+ }
+ }
+ handler.postDelayed(this, 30 * 1000L)
+ }
+ }
+
+ /**
+ * 用户超时无操作时关灯
+ */
+ override fun onUserInactive() {
+ switchLight(false)
+ }
+
+ override fun onDestroy() {
+ // 清理 Handler 所有回调,防止内存泄漏
+ handler.removeCallbacksAndMessages(null)
+
+ // 关闭并释放 Dialog 引用,防止内存泄漏
+ tipDialog?.dismiss()
+ tipDialog = null
+ noDataWarningDialog?.dismiss()
+ noDataWarningDialog = null
+
+ runBlocking { ScaleManager.release() }
+ saveTaskJob?.cancel()
+ overdueTaskJob?.cancel()
+ EventBus.getDefault().unregister(this)
+ super.onDestroy()
+ }
+
+ private val shelfAdapter by lazy {
+ ShelfV2Adapter(list).apply {
+ setOnItemClickListener { _, _, position ->
+ onShelfItemClicked(position)
+ }
+ }
+ }
+
+ /**
+ * 处理货架格子点击事件:按需开锁,然后跳转 ShelfV2Activity
+ */
+ private fun onShelfItemClicked(position: Int) {
+ shelfIndex = position
+ lifecycleScope.launch {
+ if (isNeedOpenLock()) {
+ // 解析 slotNo 末尾数字判断锁区(1-5 号格口走锁1,其余走锁2)
+ val slotNum = list[position].slotNo?.filter { it.isDigit() }?.toIntOrNull() ?: 0
+ val openCmd = if (slotNum in 1..5)
+ ScaleManager.buildOpenLock1Cmd()
+ else
+ ScaleManager.buildOpenLock2Cmd()
+ ScaleManager.sendCmd(openCmd)
+ }
+ launch(Intent(this@HomeV2Activity, ShelfV2Activity::class.java).also {
+ it.putExtra(ShelfV2Activity.SHELF_MODEL, list[position])
+ }) { result ->
+ result?.let { onShelfActivityResult(position, it) }
+ }
+ }
+ }
+
+ /**
+ * 处理 ShelfV2Activity 返回结果
+ * TODO: 待 ShelfV2Activity 迁移到 RecordItem 后重新实现字段更新逻辑
+ */
+ private fun onShelfActivityResult(position: Int, intent: Intent) {
+ @Suppress("DEPRECATION")
+ val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getSerializableExtra(ShelfV2Activity.SHELF_MODEL, ShelfModelV2::class.java)
+ } else {
+ intent.getSerializableExtra(ShelfV2Activity.SHELF_MODEL) as? ShelfModelV2
+ } ?: return
+ list[position].let {
+ it.itemType = model.itemType
+ it.itemName = model.itemName
+ it.traceCode = model.traceCode
+ it.spec = model.spec
+ it.foodId = model.foodId
+ it.storeTime = model.storeTime
+ }
+ shelfAdapter.notifyItemChanged(position)
+// syncShelfGoodsToServer()
+ }
+
+ private fun initRecyclerView() {
+ val layoutManager = GridLayoutTool.getLayoutManager(this@HomeV2Activity) ?: return
+ binding.rvShelf.let {
+ it.layoutManager = layoutManager
+ it.adapter = shelfAdapter
+ it.itemAnimator.let {
+ if (it is DefaultItemAnimator) {
+ it.supportsChangeAnimations = false
+ }
+ }
+ }
+ }
+
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun onResume() {
+ super.onResume()
+ hideStatusBar()
+ // 启动无数据看门狗(每30秒检查一次)
+ handler.postDelayed(noDataWatchdogTask, 30 * 1000L)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ // 停止无数据看门狗
+ handler.removeCallbacks(noDataWatchdogTask)
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun loadEmptyView() {
+ list.clear()
+ shelfAdapter.notifyDataSetChanged()
+ binding.include?.let {
+ it.root.visible()
+ it.root.setOnClickListener {
+ viewModelV2.initCabinet(cabinetId = App.deviceId)
+ }
+ it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white)
+ it.tvEmptyContent.setTextColor(Color.WHITE)
+ }
+
+ loadTestData()
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun loadTestData() {
+ binding.rvShelf.layoutManager = GridLayoutManager(this, 2)
+ binding.rvShelf.adapter = shelfAdapter
+ binding.include.root.gone()
+ list.clear()
+ var tempList = (1..10).map { i -> ShelfModelV2(slotNo = i.toString()) }
+ tempList = GridLayoutTool.initListSortV2(tempList)
+ list.addAll(tempList)
+ shelfAdapter.notifyDataSetChanged()
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun clearEmptyShelf(event: ClearShelfEvent) {
+ // 用 slotNo 末尾数字与串口编号匹配
+ val index = list.indexOfFirst {
+ it.slotNo?.filter { c -> c.isDigit() }?.toIntOrNull() == event.shelfNo
+ }
+ if (index < 0) return
+ list[index] = list[index].copy(itemName = "")
+ shelfAdapter.notifyItemChanged(index)
+ }
+
+ private val taskExecutor by lazy { IntervalExecutor() }
+ private var saveTaskJob: Job? = null
+
+ /**
+ * 保存定时任务
+ */
+ private fun saveGoodsTask() {
+ saveTaskJob =
+ taskExecutor.startIntervalTaskWithInitialDelay(1 * 60 * 1000L, 5 * 60 * 1000L) {
+ syncShelfGoodsToServer()
+ }
+ }
+
+ private var overdueTaskJob: Job? = null
+
+ /**
+ * 过期时间定时任务
+ */
+ private fun overdueTask() {
+ overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60 * 1000L) {
+ viewModelV2.initCabinet(cabinetId = App.deviceId)
+ }
+ }
+
+ /**
+ * 将接口返回的临期状态应用到本地列表,仅在状态变化时刷新对应 item
+ */
+ private fun applyOverdueState(items: List) {
+ if (items.size != list.size) return
+ items.forEachIndexed { index, newItem ->
+ val oldItem = list[index]
+ // slotStatus=2 表示临期预警
+ if (oldItem.slotStatus != newItem.slotStatus) {
+ list[index] = newItem
+ shelfAdapter.notifyItemChanged(index)
+ log("更新格口 ${newItem.slotNo} 物品:${newItem.itemName} 的状态:${newItem.slotStatus}")
+ }
+ }
+ }
+
+ /**
+ * 构建同步请求并提交到服务端
+ * TODO: 待 ShelfV2Activity 迁移到 RecordItem 后重新实现
+ */
+ private fun syncShelfGoodsToServer() {
+ if (cabinetInitResult == null) {
+ return
+ }
+ viewModelV2.syncCabinetData(SyncBody().apply {
+ cabinetId = list[0].cabinetId
+ temperature = null
+ humidity = null
+ goodsList = cabinetInitResult!!.containerGoodsList
+ })
+ }
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/activity/InitActivity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/activity/InitActivity.kt
index 5e6b3ae..8678100 100644
--- a/app/src/main/java/com/shuwei/intelligent/shelves/activity/InitActivity.kt
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/activity/InitActivity.kt
@@ -59,7 +59,6 @@ class InitActivity : BaseActivity() {
binding = ActivityInitBinding.inflate(layoutInflater)
setContentView(binding.root)
-
App.deviceId = AppUtil.getUDID( this)
// App.deviceId = "4787e213-90ab-3e32-88e0-ac271a937751"
Log.d(TAG, "onCreate: deviceId = ${App.deviceId}")
@@ -112,60 +111,60 @@ class InitActivity : BaseActivity() {
}
}
- private fun initConfig() {
- binding.ivQrCode.setImageBitmap(
- QRCodeUtil.generateQRCode(
- content = App.deviceId,
- size = 200.dp
- )
- )
- binding.btnInit.setOnClickListener {
- viewModel.getDeviceToken(App.deviceId)
- }
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- viewModel.getDeviceTokenUiState.collect { state ->
- when (state) {
- is UiState.Loading -> {}
- is UiState.Success<*> -> {
- state.data.data?.let { deviceToken ->
- Log.d(TAG, "initConfig: $deviceToken")
- viewModel.getDeviceConfig(
- deviceId = App.deviceId,
- deviceToken = deviceToken.toString()
- )
- }
- }
- is UiState.Error -> toast(state.msg)
- else -> {}
- }
- }
- }
- launch {
- viewModel.getDeviceConfigUiState.collect { state ->
- when (state) {
- is UiState.Loading -> {}
- is UiState.Success<*> -> {
- state.data.data?.let {
- if (it is DeviceConfigInfo) {
- SpTool.put(SpTool.DEVICE_CONFIG_CACHE, it.toJsonString())
- GlobalData.appBaseUrl = it.appPackageUrl?:""
- App.canteenId = it.canteenId?:""
-
- startActivity()
- finish()
- }
- }
- }
- is UiState.Error -> toast(message = state.msg)
- else -> {}
- }
- }
- }
- }
- }
- }
+// private fun initConfig() {
+// binding.ivQrCode.setImageBitmap(
+// QRCodeUtil.generateQRCode(
+// content = App.deviceId,
+// size = 200.dp
+// )
+// )
+//// binding.btnInit.setOnClickListener {
+//// viewModel.getDeviceToken(App.deviceId)
+//// }
+// lifecycleScope.launch {
+// repeatOnLifecycle(Lifecycle.State.CREATED) {
+//// launch {
+//// viewModel.getDeviceTokenUiState.collect { state ->
+//// when (state) {
+//// is UiState.Loading -> {}
+//// is UiState.Success<*> -> {
+//// state.data.data?.let { deviceToken ->
+//// Log.d(TAG, "initConfig: $deviceToken")
+//// viewModel.getDeviceConfig(
+//// deviceId = App.deviceId,
+//// deviceToken = deviceToken.toString()
+//// )
+//// }
+//// }
+//// is UiState.Error -> toast(state.msg)
+//// else -> {}
+//// }
+//// }
+//// }
+//// launch {
+//// viewModel.getDeviceConfigUiState.collect { state ->
+//// when (state) {
+//// is UiState.Loading -> {}
+//// is UiState.Success<*> -> {
+//// state.data.data?.let {
+//// if (it is DeviceConfigInfo) {
+//// SpTool.put(SpTool.DEVICE_CONFIG_CACHE, it.toJsonString())
+//// GlobalData.appBaseUrl = it.appPackageUrl?:""
+//// App.canteenId = it.canteenId?:""
+////
+//// startActivity()
+//// finish()
+//// }
+//// }
+//// }
+//// is UiState.Error -> toast(message = state.msg)
+//// else -> {}
+//// }
+//// }
+//// }
+// }
+// }
+// }
private fun checkConfigData(data: String): Boolean {
if (data.isBlank()) {
@@ -285,7 +284,8 @@ class InitActivity : BaseActivity() {
* 跳转到 HomeActivity
*/
private fun navigateToHome() {
- startActivity()
+// startActivity()
+ startActivity()
finish()
}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfV2Activity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfV2Activity.kt
new file mode 100644
index 0000000..6935d60
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfV2Activity.kt
@@ -0,0 +1,349 @@
+package com.shuwei.intelligent.shelves.activity
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.widget.LinearLayout
+import androidx.activity.viewModels
+import androidx.core.view.updateLayoutParams
+import androidx.core.widget.addTextChangedListener
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.GridLayoutManager
+import com.chad.library.adapter4.BaseQuickAdapter
+import com.chad.library.adapter4.util.setOnDebouncedItemClick
+import com.scwang.smart.refresh.layout.constant.RefreshState
+import com.shuwei.intelligent.shelves.R
+import com.shuwei.intelligent.shelves.adapter.CleanPackageAdapter
+import com.shuwei.intelligent.shelves.adapter.MealPackageAdapter
+import com.shuwei.intelligent.shelves.base.BaseActivity
+import com.shuwei.intelligent.shelves.databinding.ActivityShelfV2Binding
+import com.shuwei.intelligent.shelves.model.CleanPackageOption
+import com.shuwei.intelligent.shelves.model.MealPackageOption
+import com.shuwei.intelligent.shelves.model.ShelfModelV2
+import com.shuwei.intelligent.shelves.model.SendWeightEvent
+import com.shuwei.intelligent.shelves.net.Loading
+import com.shuwei.intelligent.shelves.net.NetViewModelV2
+import com.shuwei.intelligent.shelves.net.RespData
+import com.shuwei.intelligent.shelves.net.UiState
+import com.shuwei.intelligent.shelves.serial.ScaleManager
+import com.shuwei.intelligent.shelves.utils.KeyboardUtil
+import com.shuwei.intelligent.shelves.utils.ext.addOnActionSearchListener
+import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce
+import com.shuwei.intelligent.shelves.utils.ext.dp
+import com.shuwei.intelligent.shelves.utils.ext.gone
+import com.shuwei.intelligent.shelves.utils.ext.toast
+import com.shuwei.intelligent.shelves.utils.ext.visible
+import kotlinx.coroutines.launch
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+@SuppressLint("NotifyDataSetChanged")
+class ShelfV2Activity : BaseActivity() {
+
+ companion object {
+ const val SHELF_MODEL = "shelfModel"
+ const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
+
+ /** itemType=1:净菜包 */
+ private const val TYPE_CLEAN = 1
+ /** itemType=2:餐品净菜包 */
+ private const val TYPE_MEAL = 2
+ }
+
+ private lateinit var binding: ActivityShelfV2Binding
+
+ private var recordItem: ShelfModelV2? = null
+ private var realWeight: Int = 0
+ private var startTime = 0L
+
+ private val viewModel: NetViewModelV2 by viewModels()
+
+ // 当前选中类型,默认净菜包
+ private var currentType = TYPE_CLEAN
+
+ // 净菜包列表与 Adapter
+ private val cleanList: MutableList = mutableListOf()
+ private val cleanAdapter by lazy {
+ CleanPackageAdapter(cleanList).apply {
+ setOnDebouncedItemClick { _, _, position ->
+ cleanList.forEachIndexed { index, option -> option.isSelected = index == position }
+ notifyDataSetChanged()
+ binding.tvFoodName.text = cleanList[position].itemName
+ KeyboardUtil.hideKeyboard(this@ShelfV2Activity.window.decorView)
+ }
+ }
+ }
+
+ // 餐品净菜包列表与 Adapter
+ private val mealList: MutableList = mutableListOf()
+ private val mealAdapter by lazy {
+ MealPackageAdapter(mealList).apply {
+ setOnDebouncedItemClick { _, _, position ->
+ mealList.forEachIndexed { index, option -> option.isSelected = index == position }
+ notifyDataSetChanged()
+ binding.tvFoodName.text = mealList[position].itemName
+ KeyboardUtil.hideKeyboard(this@ShelfV2Activity.window.decorView)
+ }
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityShelfV2Binding.inflate(layoutInflater)
+ setBackground()
+ setContentView(binding.root)
+ EventBus.getDefault().register(this)
+
+ recordItem = intent.getSerializableExtra(SHELF_MODEL) as? ShelfModelV2
+ recordItem?.let {
+ binding.tvShelfName.text = it.slotNo
+ binding.tvFoodName.text = if (it.itemName.isNullOrBlank()) "-" else it.itemName
+ val weightG = it.totalWeightG?.toInt() ?: 0
+ realWeight = weightG
+ binding.tvFoodWeight.text = if (weightG < 1000) "${weightG}克"
+ else "%.3f千克".format(weightG / 1000.0)
+ startTime = System.currentTimeMillis()
+ }
+
+ initChipGroup()
+ initRecyclerView()
+ initSearchBox()
+ initButtons()
+ observeViewModel()
+
+ sendCmd(ScaleManager.buildGetStatusCmd())
+ loadList()
+ }
+
+ private fun initChipGroup() {
+ binding.chipGroupType.setOnCheckedStateChangeListener { _, checkedIds ->
+ currentType = when {
+ checkedIds.contains(R.id.chipClean) -> TYPE_CLEAN
+ checkedIds.contains(R.id.chipMeal) -> TYPE_MEAL
+ else -> currentType
+ }
+ // 切换时清空搜索框并重新加载列表
+ binding.etInputFood.setText("")
+ switchAdapter()
+ loadList()
+ }
+ }
+
+ private fun switchAdapter() {
+ val adapter: BaseQuickAdapter<*, *> = if (currentType == TYPE_CLEAN) cleanAdapter else mealAdapter
+ binding.rvSearch.adapter = adapter
+ }
+
+ private fun initRecyclerView() {
+ binding.rvSearch.run {
+ layoutManager = GridLayoutManager(this@ShelfV2Activity, 2)
+ adapter = cleanAdapter
+ }
+ binding.refreshLayout.run {
+ setEnableRefresh(true)
+ setEnableLoadMore(false)
+ setOnRefreshListener { loadList() }
+ }
+ }
+
+ private fun initSearchBox() {
+ binding.etInputFood.run {
+ addOnActionSearchListener {
+ loadList()
+ KeyboardUtil.hideKeyboard(context, this)
+ }
+ addTextChangedListener(afterTextChanged = { editable ->
+ if (editable.isNullOrBlank()) loadList()
+ })
+ }
+ binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) }
+ }
+
+ private fun initButtons() {
+ binding.ivBack.setOnClickListener { finish() }
+ binding.btnClearZero.setOnClickListener { clearZero() }
+ binding.btnClearEmpty.setOnClickListener {
+ clearZero()
+ binding.tvFoodName.text = "-"
+ }
+ binding.btnConfirm.clickWithDebounce {
+ var shelfModelV2: ShelfModelV2?=null
+ if (currentType == TYPE_CLEAN) {
+ val item = cleanList.firstOrNull { it.isSelected }
+ if (item == null) {
+ toast("请选择净菜包"); return@clickWithDebounce
+ }
+ shelfModelV2 = item.toShelfModelV2(recordItem)
+ } else {
+ val item = mealList.firstOrNull { it.isSelected }
+ if (item == null) {
+ toast("请选择餐品净菜包"); return@clickWithDebounce
+ }
+ shelfModelV2 = item.toShelfModelV2(recordItem)
+ }
+ // 将选中结果回传给上层 Activity
+ val result = Intent().apply {
+ putExtra(SHELF_MODEL, shelfModelV2)
+ }
+ setResult(RESULT_OK, result)
+ finish()
+ }
+ }
+
+ private fun observeViewModel() {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.cleanPackageOptionsUiState.collect { state ->
+ when (state) {
+ is UiState.Loading -> showProgress()
+ is UiState.Success<*> -> updateCleanList(state.data)
+ is UiState.Error -> showError(state.msg)
+ else -> {}
+ }
+ }
+ }
+ launch {
+ viewModel.mealPackageOptionsUiState.collect { state ->
+ when (state) {
+ is UiState.Loading -> showProgress()
+ is UiState.Success<*> -> updateMealList(state.data)
+ is UiState.Error -> showError(state.msg)
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun loadList() {
+ val keyword = binding.etInputFood.text.trim().toString().ifBlank { null }
+ if (currentType == TYPE_CLEAN) {
+ viewModel.getCleanPackageOptions(keyword = keyword)
+ } else {
+ viewModel.getMealPackageOptions(keyword = keyword)
+ }
+ }
+
+ private fun showProgress() {
+ Loading.show(this)
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun updateCleanList(data: RespData<*>) {
+ finishRefresh()
+ binding.include?.root?.gone()
+ binding.root.postDelayed({ Loading.dismiss() }, 200)
+ @Suppress("UNCHECKED_CAST")
+ val items = data.data as? List
+ if (items.isNullOrEmpty()) {
+ loadEmptyView(); return
+ }
+ cleanList.clear()
+ cleanList.addAll(items)
+ cleanAdapter.notifyDataSetChanged()
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun updateMealList(data: RespData<*>) {
+ finishRefresh()
+ binding.include?.root?.gone()
+ binding.root.postDelayed({ Loading.dismiss() }, 200)
+ @Suppress("UNCHECKED_CAST")
+ val items = data.data as? List
+ if (items.isNullOrEmpty()) {
+ loadEmptyView(); return
+ }
+ mealList.clear()
+ mealList.addAll(items)
+ mealAdapter.notifyDataSetChanged()
+ }
+
+ private fun showError(message: String) {
+ finishRefresh()
+ Loading.dismiss()
+ toast(message)
+ loadEmptyView()
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun loadEmptyView() {
+ cleanList.clear()
+ mealList.clear()
+ cleanAdapter.notifyDataSetChanged()
+ mealAdapter.notifyDataSetChanged()
+ binding.include?.let {
+ it.root.visible()
+ it.root.setOnClickListener { loadList() }
+ it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_gray)
+ it.ivEmptyIcon.updateLayoutParams {
+ val swDpValue = resources?.configuration?.smallestScreenWidthDp ?: 0
+ width = if (swDpValue > 600) 200.dp else 120.dp
+ }
+ }
+ binding.refreshLayout.run {
+ setEnableRefresh(false)
+ setEnableLoadMore(false)
+ }
+ }
+
+ private fun finishRefresh() {
+ binding.refreshLayout.let {
+ if (it.state == RefreshState.Refreshing) it.finishRefresh(500)
+ else if (it.state == RefreshState.Loading) it.finishLoadMore(500)
+ }
+ }
+
+ private fun clearZero() {
+ lifecycleScope.launch {
+ val slotNum = recordItem?.slotNo?.filter { it.isDigit() }?.toIntOrNull() ?: 0
+ val zeroClearingCmd = ScaleManager.buildScaleZeroCmd(slotNum)
+ log("格口${recordItem?.slotNo}执行清零指令:$zeroClearingCmd")
+ sendCmd(zeroClearingCmd)
+ realWeight = 0
+ sendCmd(ScaleManager.buildGetStatusCmd())
+ startTime = System.currentTimeMillis()
+ Loading.show(this@ShelfV2Activity)
+ window.decorView.postDelayed({ Loading.dismiss() }, 10000)
+ }
+ KeyboardUtil.hideKeyboard(window.decorView)
+ }
+
+ override fun onClockTick() {
+ val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
+ val arr = sdf.format(Date()).split("***")
+ updateLeftStatus(arr[0])
+ updateRightStatus(arr[1])
+ }
+
+ override fun onResume() {
+ super.onResume()
+ hideStatusBar()
+ }
+
+ override fun onDestroy() {
+ EventBus.getDefault().unregister(this)
+ super.onDestroy()
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun receiveWeightEvent(event: SendWeightEvent) {
+ val slotNum = recordItem?.slotNo?.filter { it.isDigit() }?.toIntOrNull() ?: return
+ if (event.shelfNo != slotNum) return
+ val intervalTime = System.currentTimeMillis() - startTime
+ val weight = event.weight
+ log("格口${recordItem?.slotNo}获取重量:${weight}克,间隔:${intervalTime}ms")
+ binding.tvFoodWeight.text = if (weight < 1000) "${weight}克"
+ else "%.3f千克".format(weight / 1000.0)
+ startTime = System.currentTimeMillis()
+ window.decorView.postDelayed({ Loading.dismiss() }, 1000)
+ }
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/adapter/CleanPackageAdapter.kt b/app/src/main/java/com/shuwei/intelligent/shelves/adapter/CleanPackageAdapter.kt
new file mode 100644
index 0000000..638a07a
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/adapter/CleanPackageAdapter.kt
@@ -0,0 +1,39 @@
+package com.shuwei.intelligent.shelves.adapter
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.toColorInt
+import com.chad.library.adapter4.BaseQuickAdapter
+import com.chad.library.adapter4.viewholder.QuickViewHolder
+import com.shuwei.intelligent.shelves.R
+import com.shuwei.intelligent.shelves.databinding.ListItemSearchBinding
+import com.shuwei.intelligent.shelves.model.CleanPackageOption
+
+class CleanPackageAdapter(list: MutableList) :
+ BaseQuickAdapter(list) {
+
+ inner class VH(val binding: ListItemSearchBinding) : QuickViewHolder(binding.root)
+
+ override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
+ val binding = ListItemSearchBinding.inflate(LayoutInflater.from(context), parent, false)
+ return VH(binding)
+ }
+
+ override fun onBindViewHolder(holder: VH, position: Int, item: CleanPackageOption?) {
+ item ?: return
+ val isSelected = item.isSelected
+ holder.binding.btnFoodName.run {
+ text = " ${item.itemName}"
+ setTextColor(getColor(if (isSelected) R.color.shelf_name_blue else R.color.black999))
+ strokeColor = ColorStateList.valueOf(
+ (if (isSelected) "#4969F5" else "#E6E6E6").toColorInt()
+ )
+ }
+ }
+
+ private fun getColor(@ColorRes id: Int) = ContextCompat.getColor(context, id)
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/adapter/MealPackageAdapter.kt b/app/src/main/java/com/shuwei/intelligent/shelves/adapter/MealPackageAdapter.kt
new file mode 100644
index 0000000..29cafe5
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/adapter/MealPackageAdapter.kt
@@ -0,0 +1,41 @@
+package com.shuwei.intelligent.shelves.adapter
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.toColorInt
+import com.chad.library.adapter4.BaseQuickAdapter
+import com.chad.library.adapter4.viewholder.QuickViewHolder
+import com.shuwei.intelligent.shelves.R
+import com.shuwei.intelligent.shelves.databinding.ListItemSearchBinding
+import com.shuwei.intelligent.shelves.model.MealPackageOption
+
+class MealPackageAdapter(list: MutableList) :
+ BaseQuickAdapter(list) {
+
+ inner class VH(val binding: ListItemSearchBinding) : QuickViewHolder(binding.root)
+
+ var selectedId: Long = -1L
+
+ override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
+ val binding = ListItemSearchBinding.inflate(LayoutInflater.from(context), parent, false)
+ return VH(binding)
+ }
+
+ override fun onBindViewHolder(holder: VH, position: Int, item: MealPackageOption?) {
+ item ?: return
+ val isSelected = item.isSelected
+ holder.binding.btnFoodName.run {
+ text = " ${item.itemName}"
+ setTextColor(getColor(if (isSelected) R.color.shelf_name_blue else R.color.black999))
+ strokeColor = ColorStateList.valueOf(
+ (if (isSelected) "#4969F5" else "#E6E6E6").toColorInt()
+ )
+ }
+ }
+
+ private fun getColor(@ColorRes id: Int) = ContextCompat.getColor(context, id)
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/adapter/ShelfV2Adapter.kt b/app/src/main/java/com/shuwei/intelligent/shelves/adapter/ShelfV2Adapter.kt
new file mode 100644
index 0000000..6ef4a5e
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/adapter/ShelfV2Adapter.kt
@@ -0,0 +1,78 @@
+package com.shuwei.intelligent.shelves.adapter
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+import com.chad.library.adapter4.BaseQuickAdapter
+import com.chad.library.adapter4.viewholder.QuickViewHolder
+import com.shuwei.intelligent.shelves.R
+import com.shuwei.intelligent.shelves.databinding.ListItemShelfBinding
+import com.shuwei.intelligent.shelves.model.ShelfModelV2
+import com.shuwei.intelligent.shelves.utils.ext.gone
+import com.shuwei.intelligent.shelves.utils.ext.visible
+import kotlin.math.abs
+
+class ShelfV2Adapter(list: MutableList) :
+ BaseQuickAdapter(list) {
+
+ inner class VH(val binding: ListItemShelfBinding) : QuickViewHolder(binding.root)
+
+ override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
+ val binding = ListItemShelfBinding.inflate(LayoutInflater.from(context), parent, false)
+ return VH(binding)
+ }
+
+ override fun onBindViewHolder(holder: VH, position: Int, item: ShelfModelV2?) {
+ item ?: return
+ val binding = holder.binding
+
+ // 格口编号
+ binding.tvShelfName.text = item.slotNo
+
+ // 临期预警(slotStatus=2)显示过期图标
+ if (item.slotStatus.toInt() == 2) binding.ivStaleFood.visible()
+ else binding.ivStaleFood.gone()
+
+ // 重量:itemType=2 用 totalWeightG(g),itemType=1 用 actualQty×1000(g)
+ val weightG: Double = when (item.itemType.toInt()) {
+ 2 -> item.totalWeightG?.toDouble() ?: 0.0
+ 1 -> (item.actualQty?.toDouble() ?: 0.0) * 1000.0
+ else -> 0.0
+ }
+ binding.tvFoodWeight.run {
+ setTextColor(getColor(R.color.food_weight_orange))
+ text = if (abs(weightG) < 1000) "${weightG.toInt()}克"
+ else "%.3f千克".format(weightG / 1000.0)
+ }
+
+ val isBlank = item.itemName.isNullOrBlank()
+ if (isBlank) {
+ binding.layoutCard.setCardBackgroundColor(getColor(R.color.bg_card_blue))
+ binding.tvShelfName.setTextColor(getColor(R.color.shelf_name_white))
+ binding.tvFoodName.run {
+ setTextColor(getColor(R.color.white))
+ text = "空"
+ }
+ binding.tvStoreDate.run {
+ setTextColor(getColor(R.color.food_weight_blue))
+ text = "-"
+ }
+ return
+ }
+
+ binding.layoutCard.setCardBackgroundColor(getColor(R.color.bg_card_white))
+ binding.tvShelfName.setTextColor(getColor(R.color.shelf_name_blue))
+ binding.tvFoodName.run {
+ setTextColor(getColor(R.color.food_name_black))
+ text = item.itemName
+ }
+ binding.tvStoreDate.run {
+ setTextColor(getColor(R.color.black999))
+ text = item.storeTime
+ }
+ }
+
+ private fun getColor(@ColorRes id: Int) = ContextCompat.getColor(context, id)
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/model/CabinetInitResult.kt b/app/src/main/java/com/shuwei/intelligent/shelves/model/CabinetInitResult.kt
new file mode 100644
index 0000000..3f2508b
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/model/CabinetInitResult.kt
@@ -0,0 +1,12 @@
+package com.shuwei.intelligent.shelves.model
+
+/**
+ * 货柜初始化接口(4.1)响应体
+ */
+data class CabinetInitResult(
+ val canteenId: Long = 0,
+ val canteenName: String = "",
+ val deviceName: String = "",
+ val area: String = "",
+ val containerGoodsList: List? = null,
+)
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/model/CleanPackageOption.kt b/app/src/main/java/com/shuwei/intelligent/shelves/model/CleanPackageOption.kt
new file mode 100644
index 0000000..f0d0cb2
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/model/CleanPackageOption.kt
@@ -0,0 +1,27 @@
+package com.shuwei.intelligent.shelves.model
+
+import com.shuwei.intelligent.shelves.utils.DateTimeUtil
+import java.util.Date
+
+/**
+ * 净菜包下拉选项,对应接口 4.2.1(itemType=1)
+ */
+data class CleanPackageOption(
+ val id: Long = 0,
+ val batchNo: String = "",
+ val itemName: String = "",
+ val packageSpec: String? = null,
+ val traceCode: String? = null,
+ val spec: String? = null,
+ val expiryDate: String? = null,
+ var isSelected: Boolean = false
+) {
+ /** 将净菜包选项转换为格口记录 */
+ fun toShelfModelV2(model: ShelfModelV2? = null) = (model ?: ShelfModelV2()).also {
+ it.itemType = 2
+ it.itemName = itemName
+ it.traceCode = traceCode
+ it.spec = packageSpec
+ it.storeTime = DateTimeUtil.formatDateTime(dateTime = Date())
+ }
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/model/MealPackageOption.kt b/app/src/main/java/com/shuwei/intelligent/shelves/model/MealPackageOption.kt
new file mode 100644
index 0000000..cde0e2e
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/model/MealPackageOption.kt
@@ -0,0 +1,30 @@
+package com.shuwei.intelligent.shelves.model
+
+import com.shuwei.intelligent.shelves.utils.DateTimeUtil
+import java.util.Date
+
+/**
+ * 餐品净菜包下拉选项,对应接口 4.2.2(itemType=2)
+ */
+data class MealPackageOption(
+ val id: Long = 0,
+ val batchNo: String = "",
+ val itemName: String = "",
+ val packageSpec: String? = null,
+ val traceCode: String? = null,
+ //格式 `yyyy-MM-dd`(包装完成后有值)
+ val expiryDate: String? = null,
+ /** 关联菜品 id */
+ val foodId: Long? = null,
+ var isSelected: Boolean = false
+) {
+ /** 将餐品净菜包选项转换为格口记录 */
+ fun toShelfModelV2(model: ShelfModelV2? = null) = (model ?: ShelfModelV2()).also {
+ it.itemType = 1
+ it.itemName = itemName
+ it.traceCode = traceCode
+ it.spec = packageSpec
+ it.foodId = foodId
+ it.storeTime = DateTimeUtil.formatDateTime(dateTime = Date())
+ }
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/model/ShelfModelV2.kt b/app/src/main/java/com/shuwei/intelligent/shelves/model/ShelfModelV2.kt
new file mode 100644
index 0000000..9e002c0
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/model/ShelfModelV2.kt
@@ -0,0 +1,54 @@
+package com.shuwei.intelligent.shelves.model
+
+import java.io.Serializable
+import java.math.BigDecimal
+
+/**
+ * 格口存放记录,对应接口 4.1 货柜初始化返回的 containerGoodsList 元素
+ */
+data class ShelfModelV2(
+ var id: Long = 0,
+ /** 类型:1-食材净菜 / 2-餐品净菜包 */
+ var itemType: Short = 0,
+ /** 格口编号 */
+ var slotNo: String? = "",
+ /** 格口所在柜子编号 */
+ var cabinetId: String? = "",
+ /** 智能柜名称 */
+ var cabinetName: String? = "",
+ var area: String = "",
+ /** 溯源码 */
+ var traceCode: String? = "",
+ /** 物料名称 */
+ var itemName: String? = "",
+ /** 订单量 kg(itemType=1) */
+ var orderQty: BigDecimal? = null,
+ /** 实收量 kg(itemType=1) */
+ var actualQty: BigDecimal? = null,
+ /** 差异量 kg(itemType=1,可为负) */
+ var diffQty: BigDecimal? = null,
+ /** 规格(itemType=2,如:标准份) */
+ var spec: String? = null,
+ var specLabel: String? = null,
+ /** 包数(itemType=2) */
+ var pkgCount: Int? = null,
+ /** 总重量 g(itemType=2) */
+ var totalWeightG: BigDecimal? = null,
+ /** 验收结果:1-合格 / 2-轻微差异 / 3-不合格 / 4-待验 */
+ var checkResult: Short? = null,
+ /** 存储温度(℃) */
+ var temperature: BigDecimal? = null,
+ /** 存储湿度(%) */
+ var humidity: BigDecimal? = null,
+ /** 剩余保质期(小时) */
+ var shelfLifeRemain: Int? = null,
+ var storeTime: String? = null,
+ /** 格口状态:1-存放中 / 2-临期预警 / 3-温度超标 / 4-空置 / 5-异常 */
+ var slotStatus: Short = 0,
+ /** 绑定菜品 id(itemType=2) */
+ var foodId: Long? = null,
+ var unit: String? = null,
+ var remark: String? = null,
+ var canteenId: Long? = null,
+ var isClicked: Boolean = false
+) : Serializable
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/model/SyncBody.kt b/app/src/main/java/com/shuwei/intelligent/shelves/model/SyncBody.kt
new file mode 100644
index 0000000..32e3468
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/model/SyncBody.kt
@@ -0,0 +1,14 @@
+package com.shuwei.intelligent.shelves.model
+
+import java.math.BigDecimal
+
+/**
+ * 全量同步整柜数据请求体,对应接口 4.3
+ * 注意:goodsList 为空或不传时,后端会软删除整柜所有格口数据
+ */
+data class SyncBody(
+ var cabinetId: String?=null,
+ var temperature: BigDecimal? = null,
+ var humidity: BigDecimal? = null,
+ var goodsList: List? = null,
+)
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/model/SyncItem.kt b/app/src/main/java/com/shuwei/intelligent/shelves/model/SyncItem.kt
new file mode 100644
index 0000000..2767732
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/model/SyncItem.kt
@@ -0,0 +1,34 @@
+//package com.shuwei.intelligent.shelves.model
+//
+//import java.math.BigDecimal
+//
+///**
+// * 全量同步单格口数据,对应接口 4.3 goodsList 元素
+// */
+//data class SyncItem(
+// val slotNo: String,
+// /** 类型:1-食材净菜 / 2-餐品净菜包 */
+// val itemType: Short? = null,
+// val itemName: String? = null,
+// val traceCode: String? = null,
+// /** 绑定菜品 id(itemType=2) */
+// val foodId: Long? = null,
+// /** 订单量 kg(itemType=1) */
+// val orderQty: BigDecimal? = null,
+// /** 实收量 kg(itemType=1,与 totalWeightG 二选一) */
+// val actualQty: BigDecimal? = null,
+// /** 总重量 g(itemType=2,后端自动换算为 kg 存储) */
+// val totalWeightG: BigDecimal? = null,
+// /** 规格:1-标准份 / 2-大份 / 3-小份(itemType=2) */
+// val spec: Int? = null,
+// val pkgCount: Int? = null,
+// /** 验收结果:1-合格 / 2-轻微差异 / 3-不合格 / 4-待验 */
+// val checkResult: Short? = null,
+// /** 格口状态:1-存放中 / 2-临期预警 / 3-温度超标 / 4-空置 / 5-异常 */
+// val slotStatus: Short? = null,
+// /** 剩余保质期(小时) */
+// val shelfLifeRemain: Int? = null,
+// /** 存入时间,仅新增时生效,不传则取当前时间 */
+// val storeTime: String? = null,
+// val remark: String? = null,
+//)
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/ApiServiceV2.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/ApiServiceV2.kt
new file mode 100644
index 0000000..7f99116
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/ApiServiceV2.kt
@@ -0,0 +1,47 @@
+package com.shuwei.intelligent.shelves.net
+
+import com.shuwei.intelligent.shelves.model.CabinetInitResult
+import com.shuwei.intelligent.shelves.model.CleanPackageOption
+import com.shuwei.intelligent.shelves.model.MealPackageOption
+import com.shuwei.intelligent.shelves.model.SyncBody
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Query
+
+interface ApiServiceV2 {
+
+ /**
+ * 4.1 货柜初始化
+ * 设备启动、Token 刷新后、首页重试时调用
+ */
+ @GET(UrlConfig.CABINET_INIT)
+ suspend fun initCabinet(
+ @Query("cabinetId") cabinetId: String
+ ): RespData
+
+ /**
+ * 4.2.1 净菜包下拉列表(itemType=1)
+ */
+ @GET(UrlConfig.CLEAN_PACKAGE_OPTIONS)
+ suspend fun getCleanPackageOptions(
+ @Query("keyword") keyword: String? = null
+ ): RespData>
+
+ /**
+ * 4.2.2 餐品净菜包下拉列表(itemType=2)
+ */
+ @GET(UrlConfig.MEAL_PACKAGE_OPTIONS)
+ suspend fun getMealPackageOptions(
+ @Query("keyword") keyword: String? = null
+ ): RespData>
+
+ /**
+ * 4.3 全量同步整柜数据(定时上报)
+ * 警告:body.goodsList 为 null 或空列表时,后端会软删除整柜所有格口数据
+ */
+ @POST(UrlConfig.CABINET_RECORD_SYNC)
+ suspend fun syncCabinetData(
+ @Body body: SyncBody
+ ): RespData
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/CabinetRequestInterceptor.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/CabinetRequestInterceptor.kt
new file mode 100644
index 0000000..c3b811a
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/CabinetRequestInterceptor.kt
@@ -0,0 +1,21 @@
+package com.shuwei.intelligent.shelves.net
+
+import com.shuwei.intelligent.shelves.App
+import okhttp3.Interceptor
+import okhttp3.Response
+
+/**
+ * v1.1 接口专用拦截器
+ * /nutrition/neglect/ 路径无需鉴权,仅注入基础请求头
+*/
+class CabinetRequestInterceptor : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request().newBuilder()
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json")
+ .header("X-DEVICE-CODE", App.deviceId)
+ .header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
+ .build()
+ return chain.proceed(request)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/HttpManager.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/HttpManager.kt
index b4baedc..577e486 100644
--- a/app/src/main/java/com/shuwei/intelligent/shelves/net/HttpManager.kt
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/HttpManager.kt
@@ -25,6 +25,14 @@ val apiService: ApiService = Retrofit.Builder()
.build()
.create(ApiService::class.java)
+/** v1.1 接口专用实例,使用轻量拦截器,不携带旧鉴权 Token */
+val apiServiceV2: ApiServiceV2 = Retrofit.Builder()
+ .baseUrl(GlobalData.appBaseUrl)
+ .client(HttpManager.instance.cabinetClient)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(ApiServiceV2::class.java)
+
class HttpManager private constructor() {
val client: OkHttpClient by lazy {
OkHttpClient.Builder()
@@ -56,6 +64,26 @@ class HttpManager private constructor() {
.build()
}
+ /** v1.1 接口专用 OkHttpClient,仅注入 Content-Type、Accept、X-DEVICE-CODE */
+ val cabinetClient: OkHttpClient by lazy {
+ OkHttpClient.Builder()
+ .apply {
+ connectTimeout(30, TimeUnit.SECONDS)
+ readTimeout(30, TimeUnit.SECONDS)
+ writeTimeout(30, TimeUnit.SECONDS)
+ sslSocketFactory(createSSLSocketFactory(), TrustAllCerts())
+ hostnameVerifier { _, _ -> true }
+ addNetworkInterceptor(HttpLoggingInterceptor(logger = {
+ Log.d("HttpManager", "v2 okhttp ==>${it}")
+ }).apply {
+ level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
+ else HttpLoggingInterceptor.Level.NONE
+ })
+ addInterceptor(CabinetRequestInterceptor())
+ }
+ .build()
+ }
+
companion object {
val instance by lazy { HttpManager() }
}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModelV2.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModelV2.kt
new file mode 100644
index 0000000..1e9adeb
--- /dev/null
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModelV2.kt
@@ -0,0 +1,99 @@
+package com.shuwei.intelligent.shelves.net
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.shuwei.intelligent.shelves.model.CabinetInitResult
+import com.shuwei.intelligent.shelves.model.CleanPackageOption
+import com.shuwei.intelligent.shelves.model.MealPackageOption
+import com.shuwei.intelligent.shelves.model.SyncBody
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+class NetViewModelV2 : ViewModel() {
+
+ private val _initCabinetUiState = MutableStateFlow(UiState.Initial)
+ val initCabinetUiState: StateFlow = _initCabinetUiState
+
+ private val _cleanPackageOptionsUiState = MutableStateFlow(UiState.Initial)
+ val cleanPackageOptionsUiState: StateFlow = _cleanPackageOptionsUiState
+
+ private val _mealPackageOptionsUiState = MutableStateFlow(UiState.Initial)
+ val mealPackageOptionsUiState: StateFlow = _mealPackageOptionsUiState
+
+ private val _syncCabinetUiState = MutableStateFlow(UiState.Initial)
+ val syncCabinetUiState: StateFlow = _syncCabinetUiState
+
+ /** 4.1 货柜初始化 */
+ fun initCabinet(cabinetId: String) {
+ viewModelScope.launch {
+ _initCabinetUiState.value = UiState.Loading
+ runCatching {
+ val response = apiServiceV2.initCabinet(cabinetId = cabinetId)
+ if (response.isSuccess()) {
+ _initCabinetUiState.value = UiState.Success(response)
+ } else {
+ _initCabinetUiState.value = UiState.Error(response.msg ?: "请求失败")
+ }
+ }.onFailure {
+ _initCabinetUiState.value = UiState.Error(it.message ?: "请求异常")
+ }
+ }
+ }
+
+ /** 4.2.1 净菜包下拉列表(itemType=1) */
+ fun getCleanPackageOptions(keyword: String? = null) {
+ viewModelScope.launch {
+ _cleanPackageOptionsUiState.value = UiState.Loading
+ runCatching {
+ val response = apiServiceV2.getCleanPackageOptions(keyword = keyword)
+ if (response.isSuccess()) {
+ _cleanPackageOptionsUiState.value = UiState.Success(response)
+ } else {
+ _cleanPackageOptionsUiState.value = UiState.Error(response.msg ?: "请求失败")
+ }
+ }.onFailure {
+ _cleanPackageOptionsUiState.value = UiState.Error(it.message ?: "请求异常")
+ }
+ }
+ }
+
+ /** 4.2.2 餐品净菜包下拉列表(itemType=2) */
+ fun getMealPackageOptions(keyword: String? = null) {
+ viewModelScope.launch {
+ _mealPackageOptionsUiState.value = UiState.Loading
+ runCatching {
+ val response = apiServiceV2.getMealPackageOptions(keyword = keyword)
+ if (response.isSuccess()) {
+ _mealPackageOptionsUiState.value = UiState.Success(response)
+ } else {
+ _mealPackageOptionsUiState.value = UiState.Error(response.msg ?: "请求失败")
+ }
+ }.onFailure {
+ _mealPackageOptionsUiState.value = UiState.Error(it.message ?: "请求异常")
+ }
+ }
+ }
+
+ /**
+ * 4.3 全量同步整柜数据
+ * 警告:body.goodsList 为 null 或空列表时,后端会软删除整柜所有格口数据
+ */
+ fun syncCabinetData(body: SyncBody) {
+ viewModelScope.launch {
+ _syncCabinetUiState.value = UiState.Loading
+ runCatching {
+ val response = apiServiceV2.syncCabinetData(body = body)
+ if (response.isSuccess()) {
+ _syncCabinetUiState.value = UiState.Success(response)
+ } else {
+ _syncCabinetUiState.value = UiState.Error(response.msg ?: "请求失败")
+ }
+ }.onFailure {
+ Log.e("NetViewModelV2", "syncCabinetData 异常:${it.message}")
+ _syncCabinetUiState.value = UiState.Error(it.message ?: "请求异常")
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/UrlConfig.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/UrlConfig.kt
index 29c865c..2d80812 100644
--- a/app/src/main/java/com/shuwei/intelligent/shelves/net/UrlConfig.kt
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/UrlConfig.kt
@@ -27,5 +27,11 @@ object UrlConfig {
var SAVE_SHELF_GOODS_LIST =
"${GlobalData.appBaseUrl}/terminal/neglect/smartShelves/app/smartShelves/app/saveShelvesGoodsList"
+ // v1.1 新接口
+ const val CABINET_INIT = "/nutrition/neglect/cabinet/init"
+ const val CLEAN_PACKAGE_OPTIONS = "/nutrition/neglect/cabinet/clean-package-options"
+ const val MEAL_PACKAGE_OPTIONS = "/nutrition/neglect/cabinet/meal-package-options"
+ const val CABINET_RECORD_SYNC = "/nutrition/neglect/cabinet/record/sync"
+
}
diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/utils/GridLayoutTool.kt b/app/src/main/java/com/shuwei/intelligent/shelves/utils/GridLayoutTool.kt
index 97ab9e5..135d73a 100644
--- a/app/src/main/java/com/shuwei/intelligent/shelves/utils/GridLayoutTool.kt
+++ b/app/src/main/java/com/shuwei/intelligent/shelves/utils/GridLayoutTool.kt
@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.model.ShelfModel
+import com.shuwei.intelligent.shelves.model.ShelfModelV2
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
object GridLayoutTool {
@@ -44,6 +45,22 @@ object GridLayoutTool {
}
}
+ fun initListSortV2(source: List): List {
+ val map = source.associateBy { it.slotNo?.toIntOrNull()?:0 }
+ // TODO: 测试 ------------
+ if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == App.deviceId) {
+ return ProtocolConstants.device2ColumnsOrder.mapNotNull { map[it] }
+ }
+ return if (device3Columns.contains(App.deviceId)) {
+ // 按全局定义的显示顺序取对应数据,找不到则跳过
+ ProtocolConstants.device3ColumnsOrder.mapNotNull { map[it] }
+ } else if (device2Columns.contains(App.deviceId)) {
+ ProtocolConstants.device2ColumnsOrder.mapNotNull { map[it] }
+ } else {
+ source
+ }
+ }
+
/**
* 提交接口list顺序
*/
diff --git a/app/src/main/res/layout/activity_shelf_v2.xml b/app/src/main/res/layout/activity_shelf_v2.xml
new file mode 100644
index 0000000..73311c8
--- /dev/null
+++ b/app/src/main/res/layout/activity_shelf_v2.xml
@@ -0,0 +1,311 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+