From 594c551a986253d2d4b958a0f105082267f0b7df Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Fri, 24 Apr 2026 11:28:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(activity):=20=E4=BF=AE=E5=A4=8D=E8=B0=83?= =?UTF-8?q?=E6=96=99=E9=85=8D=E7=BD=AE=E5=88=A0=E9=99=A4=E5=90=8E=E5=AD=90?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=9C=AA=E5=90=8C=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../com/shuwei/dish/match/db/DbRepository.kt | 17 ++ .../com/shuwei/dish/match/db/DbViewModel.kt | 15 ++ .../dish/match/db/dao/SeasoningSlotDao.kt | 15 ++ .../com/shuwei/dish/match/ui/SlaveActivity.kt | 38 +++- .../ui/fragment/SeasoningConfigFragment.kt | 183 +++++++++++++----- 5 files changed, 214 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt b/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt index 3d523c2..44593bb 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt @@ -137,6 +137,23 @@ class DbRepository { suspend fun upsertAllSeasoningSlots(slots: List) = withContext(Dispatchers.IO) { db.seasoningSlotDao().upsertAll(slots) } + + /** + * 删除指定设备的指定槽位配置 + * @param deviceId 目标设备 ID + * @param address 秤硬件地址 + */ + suspend fun deleteSeasoningSlot(deviceId: String, address: Int) = withContext(Dispatchers.IO) { + db.seasoningSlotDao().deleteByDeviceIdAndAddress(deviceId, address) + } + + /** + * 删除指定设备的全部槽位配置 + * @param deviceId 目标设备 ID + */ + suspend fun deleteAllSlotsByDeviceId(deviceId: String) = withContext(Dispatchers.IO) { + db.seasoningSlotDao().deleteAllByDeviceId(deviceId) + } } // //class SeasoningRepository(val seasoningDao: SeasoningDao) { diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt index 15bac25..ff0df1f 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt @@ -222,6 +222,21 @@ class DbViewModel : ViewModel() { suspend fun upsertAllSeasoningSlots(slots: List) = rep.upsertAllSeasoningSlots(slots) + /** + * 删除指定设备的指定槽位配置 + * @param deviceId 目标设备 ID + * @param address 秤硬件地址 + */ + suspend fun deleteSeasoningSlot(deviceId: String, address: Int) = + rep.deleteSeasoningSlot(deviceId, address) + + /** + * 删除指定设备的全部槽位配置(子设备接收全量广播时先清空再写入) + * @param deviceId 目标设备 ID + */ + suspend fun deleteAllSlotsByDeviceId(deviceId: String) = + rep.deleteAllSlotsByDeviceId(deviceId) + // fun updateAll(list: MutableList) { // viewModelScope.launch { // list.forEach { diff --git a/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt b/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt index 6107491..67b2216 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt @@ -37,4 +37,19 @@ interface SeasoningSlotDao { */ @Query("SELECT * FROM dm_seasoning_slot") suspend fun queryAll(): List + + /** + * 删除指定设备的指定槽位配置 + * @param deviceId 目标设备 ID + * @param address 秤硬件地址 + */ + @Query("DELETE FROM dm_seasoning_slot WHERE deviceId = :deviceId AND address = :address") + suspend fun deleteByDeviceIdAndAddress(deviceId: String, address: Int) + + /** + * 删除指定设备的全部槽位配置(子设备接收全量广播时先清空再写入) + * @param deviceId 目标设备 ID + */ + @Query("DELETE FROM dm_seasoning_slot WHERE deviceId = :deviceId") + suspend fun deleteAllByDeviceId(deviceId: String) } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt index 015bc8c..d28311c 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt @@ -158,7 +158,7 @@ class SlaveActivity : BaseActivity() { /** * 监听主设备下发的调料配置同步事件 - * 收到后更新 adapter 并写入本地 Room + * 收到后以全量替换方式更新本机 DB(先删后插),再刷新 UI */ private fun listenSeasoningConfig() { ScaleServiceManager.onSeasoningConfig = onSeasoningConfig@{ event -> @@ -167,6 +167,8 @@ class SlaveActivity : BaseActivity() { ?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) } ?: return@onSeasoningConfig lifecycleScope.launch { + // 全量替换:先删除本机所有旧槽位,再写入最新配置 + appViewModel.deleteAllSlotsByDeviceId(GlobalData.deviceId) appViewModel.upsertAllSeasoningSlots(mySlots) applySlots(mySlots) } @@ -175,9 +177,38 @@ class SlaveActivity : BaseActivity() { /** * 将槽位配置应用到对应 adapter(按地址匹配格子位置) - * 同时更新 slotNameMap,确保后续重量回调构造 ScaleData 时携带正确名称 + * 同时以传入列表为准全量刷新 slotNameMap: + * - 新列表中存在的地址:更新名称 + * - 新列表中不存在的地址:从 slotNameMap 移除,并清空对应格子名称 */ private fun applySlots(slots: List) { + val newAddresses = slots.map { it.address }.toSet() + + // 清除已被删除的槽位:从 slotNameMap 移除并清空 adapter 对应格子 + val removedAddresses = slotNameMap.keys.filter { it !in newAddresses } + removedAddresses.forEach { addr -> + slotNameMap.remove(addr) + when { + scale22Adapter != null -> { + val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(addr) + if (pos >= 0) scale22Adapter!!.updateItemName(pos, "") + } + scale18Adapter != null -> { + val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(addr) + if (pos >= 0) scale18Adapter!!.updateItemName(pos, "") + } + else -> { + val list = linearAdapter.items.toMutableList() + val idx = list.indexOfFirst { it.address == addr } + if (idx >= 0) { + list[idx] = list[idx].copy(name = "") + linearAdapter.submitList(list) + } + } + } + } + + // 更新新列表中存在的槽位 slots.forEach { slot -> slotNameMap[slot.address] = slot.goodsName when { @@ -190,7 +221,6 @@ class SlaveActivity : BaseActivity() { if (pos >= 0) scale18Adapter!!.updateItemName(pos, slot.goodsName) } else -> { - // 线性列表模式(子设备1):刷新已显示条目的名称 val list = linearAdapter.items.toMutableList() val idx = list.indexOfFirst { it.address == slot.address } if (idx >= 0) { @@ -290,7 +320,7 @@ class SlaveActivity : BaseActivity() { /** 重量增加阈值(克),超过此值才触发高亮 */ private const val WEIGHT_DELTA_THRESHOLD = 10.0 /** 高亮自动恢复时间(毫秒) */ - private const val HIGHLIGHT_DURATION_MS = 15_000L + private const val HIGHLIGHT_DURATION_MS = 8_000L } /** diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/SeasoningConfigFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/SeasoningConfigFragment.kt index 9c8dec4..00d78b1 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/SeasoningConfigFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/SeasoningConfigFragment.kt @@ -17,8 +17,8 @@ import com.shuwei.dish.match.base.BaseFragment import com.shuwei.dish.match.base.DeviceRole import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding +import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.SeasoningSelectDialog -import com.shuwei.dish.match.entity.SeasoningEntity import com.shuwei.dish.match.entity.SeasoningSlotEntity import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleEvent @@ -39,7 +39,7 @@ class SeasoningConfigFragment : BaseFragment() { private const val WEIGHT_DELTA_THRESHOLD = 10.0 /** 高亮自动恢复时间(毫秒) */ - private const val HIGHLIGHT_DURATION_MS = 15_000L + private const val HIGHLIGHT_DURATION_MS = 8_000L } override fun inflateBinding( @@ -80,81 +80,164 @@ class SeasoningConfigFragment : BaseFragment() { /** 绑定油盆格子点击,弹出调料选择弹窗并保存到 DEVICE_ID_1 */ private fun bindOilSlotClick() { binding.tvOilSlot.setOnClickListener { - openSeasoningSelectDialog(0) { item -> - binding.tvOilSlot.text = item.goodsName ?: "-" - saveSlotAndBroadcast( - SeasoningSlotEntity( - deviceId = ScaleDeviceConfig.DEVICE_ID_1, - address = 1, - goodsId = item.goodsId, - goodsName = item.goodsName ?: "" - ) - ) - } + handleSlotClick( + currentName = binding.tvOilSlot.text.toString().takeIf { it != "-" }, + onSelect = { + selectAndSave(ScaleDeviceConfig.DEVICE_ID_1, 1) { name -> + binding.tvOilSlot.text = name.ifBlank { "-" } + } + }, + onClear = { + binding.tvOilSlot.text = "-" + deleteSlotAndBroadcast(ScaleDeviceConfig.DEVICE_ID_1, 1) + } + ) } } /** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */ private fun bindAdapterClicks() { - adapter22.onItemClick = { scaleData, position -> - openSeasoningSelectDialog(position) { item -> - adapter22.updateItemName(position, item.goodsName ?: "") - saveSlotAndBroadcast( - SeasoningSlotEntity( - deviceId = ScaleDeviceConfig.DEVICE_ID_22, - address = scaleData.address, - goodsId = item.goodsId, - goodsName = item.goodsName ?: "" - ) + bindSlotAdapterClick( + adapter = adapter22, + deviceId = ScaleDeviceConfig.DEVICE_ID_22 + ) + bindSlotAdapterClick( + adapter = adapter18, + deviceId = ScaleDeviceConfig.DEVICE_ID_18 + ) + } + + /** 为指定 Adapter 绑定格子点击逻辑(已配置弹操作弹窗,未配置直接选择) */ + private fun bindSlotAdapterClick( + adapter: Any, + deviceId: String + ) { + when (adapter) { + is Seasoning22GridAdapter -> adapter.onItemClick = { scaleData, position -> + handleSlotClick( + currentName = scaleData.name, + onSelect = { + selectAndSave(deviceId, scaleData.address) { name -> + adapter.updateItemName(position, name) + } + }, + onClear = { + adapter.updateItemName(position, "") + deleteSlotAndBroadcast(deviceId, scaleData.address) + } + ) + } + is Seasoning18GridAdapter -> adapter.onItemClick = { scaleData, position -> + handleSlotClick( + currentName = scaleData.name, + onSelect = { + selectAndSave(deviceId, scaleData.address) { name -> + adapter.updateItemName(position, name) + } + }, + onClear = { + adapter.updateItemName(position, "") + deleteSlotAndBroadcast(deviceId, scaleData.address) + } ) } } - adapter18.onItemClick = { scaleData, position -> - openSeasoningSelectDialog(position) { item -> - adapter18.updateItemName(position, item.goodsName ?: "") - saveSlotAndBroadcast( - SeasoningSlotEntity( - deviceId = ScaleDeviceConfig.DEVICE_ID_18, - address = scaleData.address, - goodsId = item.goodsId, - goodsName = item.goodsName ?: "" - ) - ) - } + } + + /** + * 格子点击分发:已配置则弹操作弹窗,未配置直接执行选择 + * @param currentName 格子当前调料名称 + * @param onSelect 执行选择(新增或重新选择) + * @param onClear 执行清除 + */ + private fun handleSlotClick( + currentName: String?, + onSelect: () -> Unit, + onClear: () -> Unit + ) { + if (!currentName.isNullOrBlank()) { + showSlotActionDialog(goodsName = currentName, onReselect = onSelect, onClear = onClear) + } else { + onSelect() } } /** * 打开调料选择弹窗 - * - * @param position 格子位置 * @param callback 调料选择回调 */ - private fun openSeasoningSelectDialog( - position: Int, - callback: (item: SeasoningEntity) -> Unit + /** + * 打开选择弹窗,选中后更新 UI 并保存槽位到 Room 广播 + * @param deviceId 目标设备 ID + * @param address 秤硬件地址 + * @param onUpdateName 选中后更新格子显示名称的回调 + */ + private fun selectAndSave( + deviceId: String, + address: Int, + onUpdateName: (String) -> Unit ) { SeasoningSelectDialog( activity = currentActivity, - onItemSelected = callback + onItemSelected = { item -> + val name = item.goodsName ?: "" + onUpdateName(name) + saveSlotAndBroadcast( + SeasoningSlotEntity( + deviceId = deviceId, + address = address, + goodsId = item.goodsId, + goodsName = name + ) + ) + } ).show() } - /** - * 将单个槽位写入 Room,再读取全量配置广播给子设备 - * 仅主设备执行广播,子设备不调用此方法 - */ + /** 将单个槽位写入 Room,再广播全量配置给子设备 */ private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) { lifecycleScope.launch { currentActivity.appViewModel.upsertSeasoningSlot(slot) - // 读取全量配置,通过 wsClient 推送给所有已连接子设备 - val allSlots = currentActivity.appViewModel.loadSeasoningSlot().map { - SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName) - } - ScaleServiceManager.sendSeasoningConfig(allSlots) + broadcastAllSlots() } } + /** + * 弹出已配置格子的操作选择弹窗(3按钮) + * @param goodsName 当前格子已配置的调料名称 + * @param onReselect 用户点击「重新选择」后的回调 + * @param onClear 用户点击「清除配置」后的回调 + */ + private fun showSlotActionDialog( + goodsName: String, + onReselect: () -> Unit, + onClear: () -> Unit + ) { + CommonDialog(currentActivity) + .setTitle("调料操作") + .setContent("当前配置:$goodsName") + .setNegativeButton("取消") + .setNeutralButton("重新选择") { onReselect() } + .setPositiveButton("清除配置") { onClear() } + .show() + } + + /** 从 Room 删除指定槽位,再广播全量配置给子设备 */ + private fun deleteSlotAndBroadcast(deviceId: String, address: Int) { + lifecycleScope.launch { + currentActivity.appViewModel.deleteSeasoningSlot(deviceId, address) + broadcastAllSlots() + } + } + + /** 读取全量槽位配置并通过 wsClient 推送给所有已连接子设备 */ + private suspend fun broadcastAllSlots() { + val allSlots = currentActivity.appViewModel.loadSeasoningSlot().map { + SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName) + } + ScaleServiceManager.sendSeasoningConfig(allSlots) + } + // /** 初始化 ViewModel */ // private fun initViewModel() { // val factory = AppFactory(DbRepository(BaseApp.instance!!.database.appDao()))