fix(activity): 修复调料配置删除后子设备未同步问题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 11:28:59 +08:00
co-authored by Claude Sonnet 4.6
parent 617ccfb456
commit 594c551a98
5 changed files with 214 additions and 54 deletions
@@ -137,6 +137,23 @@ class DbRepository {
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) = 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) {
@@ -222,6 +222,21 @@ class DbViewModel : ViewModel() {
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) =
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<SeasoningEntity>) {
// viewModelScope.launch {
// list.forEach {
@@ -37,4 +37,19 @@ interface SeasoningSlotDao {
*/
@Query("SELECT * FROM dm_seasoning_slot")
suspend fun queryAll(): List<SeasoningSlotEntity>
/**
* 删除指定设备的指定槽位配置
* @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)
}
@@ -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<SeasoningSlotEntity>) {
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
}
/**
@@ -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<FragmentSeasoningConfigBinding>() {
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,80 +80,163 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
/** 绑定油盆格子点击,弹出调料选择弹窗并保存到 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 推送给所有已连接子设备
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() {