From b0739605b92f3f837c493254b48205ee39a7c12e Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Wed, 27 May 2026 19:54:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(v2):=20=E6=96=B0=E5=A2=9E=20V2=20=E8=B4=A7?= =?UTF-8?q?=E6=9F=9C=E6=8E=A5=E5=8F=A3=E5=B1=82=E3=80=81=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=8F=8A=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ApiServiceV2 / NetViewModelV2 承载 v1.1 接口(初始化、净菜包/餐品净菜包选项、数据同步) - 新增 CabinetRequestInterceptor 独立 Retrofit 实例 - 新增 HomeV2Activity + ShelfV2Activity 及对应布局 - 新增 ShelfV2Adapter / CleanPackageAdapter / MealPackageAdapter - 新增 ShelfModelV2 / CabinetInitResult / SyncBody / SyncItem / CleanPackageOption / MealPackageOption 数据模型 - HomeV2Activity 实现 syncShelfGoodsToServer 定时同步逻辑 - GridLayoutTool 新增 initListSortV2 支持 ShelfModelV2 排序 Co-Authored-By: Claude Sonnet 4.6 --- app/src/main/AndroidManifest.xml | 2 + .../shuwei/intelligent/shelves/GlobalData.kt | 3 +- .../shelves/activity/HomeV2Activity.kt | 434 ++++++++++++++++++ .../shelves/activity/InitActivity.kt | 112 ++--- .../shelves/activity/ShelfV2Activity.kt | 349 ++++++++++++++ .../shelves/adapter/CleanPackageAdapter.kt | 39 ++ .../shelves/adapter/MealPackageAdapter.kt | 41 ++ .../shelves/adapter/ShelfV2Adapter.kt | 78 ++++ .../shelves/model/CabinetInitResult.kt | 12 + .../shelves/model/CleanPackageOption.kt | 27 ++ .../shelves/model/MealPackageOption.kt | 30 ++ .../intelligent/shelves/model/ShelfModelV2.kt | 54 +++ .../intelligent/shelves/model/SyncBody.kt | 14 + .../intelligent/shelves/model/SyncItem.kt | 34 ++ .../intelligent/shelves/net/ApiServiceV2.kt | 47 ++ .../shelves/net/CabinetRequestInterceptor.kt | 21 + .../intelligent/shelves/net/HttpManager.kt | 28 ++ .../intelligent/shelves/net/NetViewModelV2.kt | 99 ++++ .../intelligent/shelves/net/UrlConfig.kt | 6 + .../shelves/utils/GridLayoutTool.kt | 17 + app/src/main/res/layout/activity_shelf_v2.xml | 311 +++++++++++++ 21 files changed, 1701 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeV2Activity.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfV2Activity.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/adapter/CleanPackageAdapter.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/adapter/MealPackageAdapter.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/adapter/ShelfV2Adapter.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/model/CabinetInitResult.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/model/CleanPackageOption.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/model/MealPackageOption.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/model/ShelfModelV2.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/model/SyncBody.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/model/SyncItem.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/net/ApiServiceV2.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/net/CabinetRequestInterceptor.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModelV2.kt create mode 100644 app/src/main/res/layout/activity_shelf_v2.xml 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +