fix(activity): 修复调料配置删除后子设备未同步问题
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,23 @@ class DbRepository {
|
|||||||
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) = withContext(Dispatchers.IO) {
|
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) = withContext(Dispatchers.IO) {
|
||||||
db.seasoningSlotDao().upsertAll(slots)
|
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) {
|
//class SeasoningRepository(val seasoningDao: SeasoningDao) {
|
||||||
|
|||||||
@@ -222,6 +222,21 @@ class DbViewModel : ViewModel() {
|
|||||||
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) =
|
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) =
|
||||||
rep.upsertAllSeasoningSlots(slots)
|
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>) {
|
// fun updateAll(list: MutableList<SeasoningEntity>) {
|
||||||
// viewModelScope.launch {
|
// viewModelScope.launch {
|
||||||
// list.forEach {
|
// list.forEach {
|
||||||
|
|||||||
@@ -37,4 +37,19 @@ interface SeasoningSlotDao {
|
|||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM dm_seasoning_slot")
|
@Query("SELECT * FROM dm_seasoning_slot")
|
||||||
suspend fun queryAll(): List<SeasoningSlotEntity>
|
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() {
|
private fun listenSeasoningConfig() {
|
||||||
ScaleServiceManager.onSeasoningConfig = onSeasoningConfig@{ event ->
|
ScaleServiceManager.onSeasoningConfig = onSeasoningConfig@{ event ->
|
||||||
@@ -167,6 +167,8 @@ class SlaveActivity : BaseActivity() {
|
|||||||
?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) }
|
?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) }
|
||||||
?: return@onSeasoningConfig
|
?: return@onSeasoningConfig
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
// 全量替换:先删除本机所有旧槽位,再写入最新配置
|
||||||
|
appViewModel.deleteAllSlotsByDeviceId(GlobalData.deviceId)
|
||||||
appViewModel.upsertAllSeasoningSlots(mySlots)
|
appViewModel.upsertAllSeasoningSlots(mySlots)
|
||||||
applySlots(mySlots)
|
applySlots(mySlots)
|
||||||
}
|
}
|
||||||
@@ -175,9 +177,38 @@ class SlaveActivity : BaseActivity() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 将槽位配置应用到对应 adapter(按地址匹配格子位置)
|
* 将槽位配置应用到对应 adapter(按地址匹配格子位置)
|
||||||
* 同时更新 slotNameMap,确保后续重量回调构造 ScaleData 时携带正确名称
|
* 同时以传入列表为准全量刷新 slotNameMap:
|
||||||
|
* - 新列表中存在的地址:更新名称
|
||||||
|
* - 新列表中不存在的地址:从 slotNameMap 移除,并清空对应格子名称
|
||||||
*/
|
*/
|
||||||
private fun applySlots(slots: List<SeasoningSlotEntity>) {
|
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 ->
|
slots.forEach { slot ->
|
||||||
slotNameMap[slot.address] = slot.goodsName
|
slotNameMap[slot.address] = slot.goodsName
|
||||||
when {
|
when {
|
||||||
@@ -190,7 +221,6 @@ class SlaveActivity : BaseActivity() {
|
|||||||
if (pos >= 0) scale18Adapter!!.updateItemName(pos, slot.goodsName)
|
if (pos >= 0) scale18Adapter!!.updateItemName(pos, slot.goodsName)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// 线性列表模式(子设备1):刷新已显示条目的名称
|
|
||||||
val list = linearAdapter.items.toMutableList()
|
val list = linearAdapter.items.toMutableList()
|
||||||
val idx = list.indexOfFirst { it.address == slot.address }
|
val idx = list.indexOfFirst { it.address == slot.address }
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
@@ -290,7 +320,7 @@ class SlaveActivity : BaseActivity() {
|
|||||||
/** 重量增加阈值(克),超过此值才触发高亮 */
|
/** 重量增加阈值(克),超过此值才触发高亮 */
|
||||||
private const val WEIGHT_DELTA_THRESHOLD = 10.0
|
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.DeviceRole
|
||||||
import com.shuwei.dish.match.base.GlobalData
|
import com.shuwei.dish.match.base.GlobalData
|
||||||
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
|
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.dialog.SeasoningSelectDialog
|
||||||
import com.shuwei.dish.match.entity.SeasoningEntity
|
|
||||||
import com.shuwei.dish.match.entity.SeasoningSlotEntity
|
import com.shuwei.dish.match.entity.SeasoningSlotEntity
|
||||||
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
||||||
import com.shuwei.dish.match.scale.ScaleEvent
|
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 WEIGHT_DELTA_THRESHOLD = 10.0
|
||||||
|
|
||||||
/** 高亮自动恢复时间(毫秒) */
|
/** 高亮自动恢复时间(毫秒) */
|
||||||
private const val HIGHLIGHT_DURATION_MS = 15_000L
|
private const val HIGHLIGHT_DURATION_MS = 8_000L
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inflateBinding(
|
override fun inflateBinding(
|
||||||
@@ -80,81 +80,164 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
|||||||
/** 绑定油盆格子点击,弹出调料选择弹窗并保存到 DEVICE_ID_1 */
|
/** 绑定油盆格子点击,弹出调料选择弹窗并保存到 DEVICE_ID_1 */
|
||||||
private fun bindOilSlotClick() {
|
private fun bindOilSlotClick() {
|
||||||
binding.tvOilSlot.setOnClickListener {
|
binding.tvOilSlot.setOnClickListener {
|
||||||
openSeasoningSelectDialog(0) { item ->
|
handleSlotClick(
|
||||||
binding.tvOilSlot.text = item.goodsName ?: "-"
|
currentName = binding.tvOilSlot.text.toString().takeIf { it != "-" },
|
||||||
saveSlotAndBroadcast(
|
onSelect = {
|
||||||
SeasoningSlotEntity(
|
selectAndSave(ScaleDeviceConfig.DEVICE_ID_1, 1) { name ->
|
||||||
deviceId = ScaleDeviceConfig.DEVICE_ID_1,
|
binding.tvOilSlot.text = name.ifBlank { "-" }
|
||||||
address = 1,
|
}
|
||||||
goodsId = item.goodsId,
|
},
|
||||||
goodsName = item.goodsName ?: ""
|
onClear = {
|
||||||
)
|
binding.tvOilSlot.text = "-"
|
||||||
)
|
deleteSlotAndBroadcast(ScaleDeviceConfig.DEVICE_ID_1, 1)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
|
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
|
||||||
private fun bindAdapterClicks() {
|
private fun bindAdapterClicks() {
|
||||||
adapter22.onItemClick = { scaleData, position ->
|
bindSlotAdapterClick(
|
||||||
openSeasoningSelectDialog(position) { item ->
|
adapter = adapter22,
|
||||||
adapter22.updateItemName(position, item.goodsName ?: "")
|
deviceId = ScaleDeviceConfig.DEVICE_ID_22
|
||||||
saveSlotAndBroadcast(
|
)
|
||||||
SeasoningSlotEntity(
|
bindSlotAdapterClick(
|
||||||
deviceId = ScaleDeviceConfig.DEVICE_ID_22,
|
adapter = adapter18,
|
||||||
address = scaleData.address,
|
deviceId = ScaleDeviceConfig.DEVICE_ID_18
|
||||||
goodsId = item.goodsId,
|
)
|
||||||
goodsName = item.goodsName ?: ""
|
}
|
||||||
)
|
|
||||||
|
/** 为指定 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(
|
* @param currentName 格子当前调料名称
|
||||||
deviceId = ScaleDeviceConfig.DEVICE_ID_18,
|
* @param onSelect 执行选择(新增或重新选择)
|
||||||
address = scaleData.address,
|
* @param onClear 执行清除
|
||||||
goodsId = item.goodsId,
|
*/
|
||||||
goodsName = item.goodsName ?: ""
|
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 调料选择回调
|
* @param callback 调料选择回调
|
||||||
*/
|
*/
|
||||||
private fun openSeasoningSelectDialog(
|
/**
|
||||||
position: Int,
|
* 打开选择弹窗,选中后更新 UI 并保存槽位到 Room 广播
|
||||||
callback: (item: SeasoningEntity) -> Unit
|
* @param deviceId 目标设备 ID
|
||||||
|
* @param address 秤硬件地址
|
||||||
|
* @param onUpdateName 选中后更新格子显示名称的回调
|
||||||
|
*/
|
||||||
|
private fun selectAndSave(
|
||||||
|
deviceId: String,
|
||||||
|
address: Int,
|
||||||
|
onUpdateName: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
SeasoningSelectDialog(
|
SeasoningSelectDialog(
|
||||||
activity = currentActivity,
|
activity = currentActivity,
|
||||||
onItemSelected = callback
|
onItemSelected = { item ->
|
||||||
|
val name = item.goodsName ?: ""
|
||||||
|
onUpdateName(name)
|
||||||
|
saveSlotAndBroadcast(
|
||||||
|
SeasoningSlotEntity(
|
||||||
|
deviceId = deviceId,
|
||||||
|
address = address,
|
||||||
|
goodsId = item.goodsId,
|
||||||
|
goodsName = name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 将单个槽位写入 Room,再广播全量配置给子设备 */
|
||||||
* 将单个槽位写入 Room,再读取全量配置广播给子设备
|
|
||||||
* 仅主设备执行广播,子设备不调用此方法
|
|
||||||
*/
|
|
||||||
private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) {
|
private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
currentActivity.appViewModel.upsertSeasoningSlot(slot)
|
currentActivity.appViewModel.upsertSeasoningSlot(slot)
|
||||||
// 读取全量配置,通过 wsClient 推送给所有已连接子设备
|
broadcastAllSlots()
|
||||||
val allSlots = currentActivity.appViewModel.loadSeasoningSlot().map {
|
|
||||||
SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName)
|
|
||||||
}
|
|
||||||
ScaleServiceManager.sendSeasoningConfig(allSlots)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹出已配置格子的操作选择弹窗(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 */
|
// /** 初始化 ViewModel */
|
||||||
// private fun initViewModel() {
|
// private fun initViewModel() {
|
||||||
// val factory = AppFactory(DbRepository(BaseApp.instance!!.database.appDao()))
|
// val factory = AppFactory(DbRepository(BaseApp.instance!!.database.appDao()))
|
||||||
|
|||||||
Reference in New Issue
Block a user