13 Commits
Author SHA1 Message Date
lvmengandClaude Sonnet 4.6 b6b5088107 feat(ui): 新增油盆配置区,子设备1调料名改为动态配置
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 18:18:22 +08:00
lvmeng bbdebc0c9c feat(activity): 新增同名调料用量聚合合并展示逻辑
- SubmitFoodActivity 新增 rawWeightMap 记录各秤槽位原始用量
- 每次秤数据更新后按 goodsName 聚合求和,同名调料只展示一条
- SeasoningWeightAdapter 改为按 goodsName 作为唯一标识匹配更新/移除
- SlaveActivity 简化高亮触发条件判断
2026-04-21 17:50:02 +08:00
lvmeng 0a90a8b368 feat(activity): 重构 SubmitFoodActivity 调料展示,改用子设备实时秤数据 2026-04-21 17:08:34 +08:00
lvmeng 07eb626e12 feat(layout): 更新秤数据列表项布局样式
- 将布局高度从120dp增加到340dp
- 调整布局方向为垂直排列并居中对齐
- 添加商品名称显示文本框
- 调整字体大小和颜色配置
- 修改权重布局为包裹内容模式
- 更新内边距设置以优化显示效果
2026-04-21 16:57:55 +08:00
lvmeng 736e4820dd fix(fragment): 修复 triggerHighlight 因设备判断用 != 导致主设备秤误触发调料格子高亮 2026-04-21 16:52:23 +08:00
lvmengandClaude Sonnet 4.6 3fc54d5f52 refactor(ui): 重构 Scale2VH 使用 XML 布局,优化秤总览界面样式
- list_type_scale2.xml 替换动态格子为 include list_item_scale22_row,Scale2VH 删除 makeScaleCell/updateScaleCell/makeCellText
- list_type_scale1.xml 布局调整为垂直居中,新增 tvName 展示调料名称
- list_type_scale18/22.xml 移除分割线
- SeasoningSelectDialog 搜索列表改为3列,扩充模拟调料数据

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 15:54:04 +08:00
lvmengandClaude Sonnet 4.6 b16d7d19cd feat(scale): 实现调料槽位配置持久化及主从设备数据同步
- 新增 SeasoningSlotEntity / SeasoningSlotDao,独立存储格子调料配置
- AppDatabase 升级至版本 8,添加 MIGRATION_7_8
- ScaleEvent 新增 TYPE_SEASONING_CONFIG 和 SlotConfig,用于主从同步
- SeasoningConfigFragment 选择调料后写 Room 并通过 wsClient 推送给子设备
- ScaleWebSocketClient 新增 sendToAllSlaves / sendToDevice / onDeviceConnected
- ScaleWebSocketServer 扩展 onMessage 支持接收主设备下发的配置事件
- ScaleServiceManager 暴露 sendSeasoningConfig / onSeasoningConfig;子设备连接时自动推送全量配置
- MasterScaleActivity 启动时从 Room 加载 slotNameMap,注入 ScaleData.name 展示调料名
- SlaveActivity 新增 slotNameMap 防止重量回调覆盖调料名;监听 onSeasoningConfig 写 Room 并刷新 adapter
- Adapter 新增 tvAddress 和 showAddress 参数,仅 MasterScaleActivity 显示秤地址行;tvName 空值统一显示"-"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 15:27:28 +08:00
lvmengandClaude Sonnet 4.6 a81e914b63 refactor(adapter): 合并 Scale18/22GridAdapter 为 Seasoning18/22GridAdapter,统一支持调料配置和秤总览双模式
- ScaleData 新增可空 name 字段,用于承载调料名称
- Seasoning18/22GridAdapter 改用 ScaleData,新增 showNameOnly 参数区分页面模式
- showNameOnly=true(调料配置):仅显示名称,无名称显示"-"
- showNameOnly=false(秤总览):显示名称+重量+状态,无名称显示秤地址编号
- update() 恢复差量更新逻辑,避免 submitList 动画期间点击失效
- MasterScaleActivity、SlaveActivity 改用新 Adapter,删除旧 Scale18/22GridAdapter
- 删除 Scale22GridActivity 及其布局和 Manifest 注册
- list_item_scale22_row.xml 中 tvAddr 重命名为 tvName

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 11:59:59 +08:00
lvmeng ffdf364ee8 style(ui): 调料配置界面布局优化
- 添加了整体容器的圆角背景样式
- 新增了顶部标题区域,包含主标题和操作提示文字
- 统一调整了上下层调料区标题的文字大小为28sp
- 优化了RecyclerView的边距设置
- 改善了整体界面的视觉层次和用户体验
2026-04-21 10:51:34 +08:00
lvmengandClaude Sonnet 4.6 6d3bb96d61 fix(activity): 修复 SlaveActivity 负值清零后误触发高亮的问题
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 10:41:42 +08:00
lvmengandClaude Sonnet 4.6 da712ed25a fix(activity): 修复 FoodRecognizeActivity 称重更新导致 useWeight 异常的问题
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 10:11:10 +08:00
lvmeng 6b312ec689 fix(prepareCook): 修复食材选择和数据处理逻辑
- 修复了食材选择回调中的变量命名问题,统一使用 entity 替代 item
- 改进了食材名称解析逻辑,正确提取 WP 标识符和食材名称
- 为食材 ID 提取添加了边界情况处理,避免数组越界异常
- 添加了调试日志以便追踪食材查询结果
- 优化了食材列表数据结构的构建过程
2026-04-21 09:46:15 +08:00
lvmeng e175893790 refactor(FoodRecognizeActivity): 重构食材识别页面返回逻辑和数据传递机制
- 移除废弃的 Uri 导入并添加 Activity 兼容性支持
- 新增手动取消标识常量 IS_MANUAL_CANCEL 用于区分正常选择和手动取消
- 将启动方法参数 onResult 重命名为 finishCallback 并支持取消状态回调
- 使用 IntentCompat 替代已弃用的 getSerializableExtra 方法
- 在标题栏和返回键添加手动返回功能,统一处理页面关闭逻辑
- 修改 finishWithResult 方法支持传递取消状态,仅在非取消状态下传递选中数据
- 更新 PrepareCookActivity 中的重量阈值从 5 改为 15
- 重构识别控制变量,使用 showRecognizePage 和 manualCancelFlag 替代原有变量
- 调整页面生命周期中的识别启用逻辑,确保正确的状态管理
- 修改食材识别启动回调,集成手动取消状态处理机制
2026-04-21 09:29:26 +08:00
33 changed files with 1297 additions and 1115 deletions
-5
View File
@@ -106,11 +106,6 @@
android:theme="@style/Theme.DishMatch.NoSplash" android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait" android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" /> tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.Scale22GridActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity <activity
android:name="com.shuwei.dish.match.ui.FoodRecognizeActivity" android:name="com.shuwei.dish.match.ui.FoodRecognizeActivity"
android:theme="@style/Theme.DishMatch.NoSplash" android:theme="@style/Theme.DishMatch.NoSplash"
@@ -1,139 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.annotation.SuppressLint
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
/**
* 18个秤的网格 Adapter
* 使用 GridLayoutManager6列)排列,第1行格子 margin 较大(视觉更小),第2-3行 margin 较小
* 点击格子触发清零回调
*/
class Scale18GridAdapter : BaseQuickAdapter<ScaleData, Scale18GridAdapter.VH>(mutableListOf()) {
inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
/** 用 address=0 的哨兵表示空位 */
private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1
/** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */
var onItemClick: ((ScaleData) -> Unit)? = null
/**
* 按 SCALE_ORDER_18 顺序更新数据,空位用哨兵填充
* 只更新显示字段有变化的格子,避免全量刷新
*/
fun update(newList: List<ScaleData?>) {
val filled = newList.map { it ?: empty }
if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
val cur = items.getOrNull(i)
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state) {
set(i, scale)
}
}
}
}
/**
* 将 ScaleData 列表按 SCALE_ORDER_18 重排后调用 update
*/
fun updateByAddress(scales: List<ScaleData>) {
val byAddress = scales.associateBy { it.address }
update(ScaleDeviceConfig.SCALE_ORDER_18.map { byAddress[it] })
}
/** 前6个位置为第1行(viewType=0margin大),其余为第2-3行(viewType=1margin小) */
override fun getItemViewType(position: Int, list: List<ScaleData>) = if (position < 6) 0 else 1
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
// 第1行 margin=12dp(格子视觉更小),第2-3行 margin=2dp
val margin = if (viewType == 0) 12.dp else 2.dp
val screenWidth = parent.resources.displayMetrics.widthPixels
val cellSize = (screenWidth - 6 * margin * 2 - 10.dp) / 6
b.root.layoutParams = (b.root.layoutParams as ViewGroup.MarginLayoutParams).also {
it.width = cellSize
it.height = cellSize
// 第1行:顶部间距为0,底部间距2dp,左右保持原有 margin
if (viewType == 0) it.setMargins(margin, 0, margin, 2.dp)
else it.setMargins(margin, margin, margin, margin)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
val vh = VH(b)
// 在 ViewHolder 创建时绑定点击,只执行一次,不受 notifyItemChanged 重绑定影响
b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.takeIf { it.address != 0 }?.let { onItemClick?.invoke(it) }
}
}
return vh
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0
holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddr.setTextColor(white)
holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white)
} else {
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
holder.b.tvAddr.setTextColor(normal)
holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(normal)
}
}
/**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/
fun highlightPosition(position: Int) {
val prev = highlightedPosition
highlightedPosition = position
if (prev >= 0) notifyItemChanged(prev)
if (position >= 0) notifyItemChanged(position)
}
/**
* 清除所有高亮,恢复默认样式
*/
fun clearHighlight() {
highlightPosition(-1)
}
private fun stateStr(state: Int) = when (state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "$state"
}
}
@@ -1,141 +0,0 @@
package com.shuwei.dish.match.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
/**
* 22个秤的网格 Adapter
* 使用 FlexboxLayoutManager 按列排列,前4个格子较大(对应物理上的大秤),其余较小
* 点击格子触发清零回调
*
* @param largeSize 前4个大格子的尺寸(px
* @param smallSize 其余小格子的尺寸(px
*/
class Scale22GridAdapter(
private val largeSize: Int,
private val smallSize: Int
) : BaseQuickAdapter<ScaleData, Scale22GridAdapter.VH>(mutableListOf()) {
inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
/** 用 address=0 的哨兵表示空位 */
private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1
/** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */
var onItemClick: ((ScaleData) -> Unit)? = null
/**
* 按 SCALE_ORDER_22 顺序更新数据,空位用哨兵填充
* 只更新显示字段有变化的格子,避免全量刷新
*/
fun update(newList: List<ScaleData?>) {
val filled = newList.map { it ?: empty }
if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
val cur = items.getOrNull(i)
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state) {
set(i, scale)
}
}
}
}
/**
* 将 ScaleData 列表按 SCALE_ORDER_22 重排后调用 update
*/
fun updateByAddress(scales: List<ScaleData>) {
val byAddress = scales.associateBy { it.address }
update(ScaleDeviceConfig.SCALE_ORDER_22.map { byAddress[it] })
}
/** 前4个位置为大格子(viewType=1),其余为小格子(viewType=0 */
override fun getItemViewType(position: Int, list: List<ScaleData>) = if (position < 4) 1 else 0
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
val cellSize = if (viewType == 1) largeSize else smallSize
(b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply {
width = cellSize
height = cellSize
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
val vh = VH(b)
// 在 ViewHolder 创建时绑定点击,只执行一次,不受 notifyItemChanged 重绑定影响
b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.takeIf { it.address != 0 }?.let { onItemClick?.invoke(it) }
}
}
return vh
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0
holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddr.setTextColor(white)
holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white)
} else {
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
holder.b.tvAddr.setTextColor(normal)
holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(normal)
}
}
/**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/
fun highlightPosition(position: Int) {
val prev = highlightedPosition
highlightedPosition = position
if (prev >= 0) notifyItemChanged(prev)
if (position >= 0) notifyItemChanged(position)
}
/**
* 清除所有高亮,恢复默认样式
*/
fun clearHighlight() {
highlightPosition(-1)
}
private fun stateStr(state: Int) = when (state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "$state"
}
}
@@ -19,8 +19,9 @@ class ScaleRowAdapter : BaseQuickAdapter<ScaleRowAdapter.ScaleItem, ScaleRowAdap
* @param address 秤地址 * @param address 秤地址
* @param weight 重量(克) * @param weight 重量(克)
* @param state 状态码 * @param state 状态码
* @param name 调料名称,未配置时为空
*/ */
data class ScaleItem(val address: Int, val weight: Double, val state: Int) data class ScaleItem(val address: Int, val weight: Double, val state: Int, val name: String = "")
inner class VH(val b: ListItemScaleRowBinding) : QuickViewHolder(b.root) inner class VH(val b: ListItemScaleRowBinding) : QuickViewHolder(b.root)
@@ -31,6 +32,7 @@ class ScaleRowAdapter : BaseQuickAdapter<ScaleRowAdapter.ScaleItem, ScaleRowAdap
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleItem?) { override fun onBindViewHolder(holder: VH, position: Int, item: ScaleItem?) {
item ?: return item ?: return
holder.b.tvScaleLabel.text = "${item.address}" holder.b.tvScaleLabel.text = "${item.address}"
holder.b.tvName.text = item.name.ifEmpty { "-" }
holder.b.tvWeight.text = "${item.weight} g" holder.b.tvWeight.text = "${item.weight} g"
holder.b.tvState.text = when (item.state) { holder.b.tvState.text = when (item.state) {
WeightUtil.STATE_STABLE -> "稳定" WeightUtil.STATE_STABLE -> "稳定"
@@ -1,80 +1,115 @@
package com.shuwei.dish.match.adapter package com.shuwei.dish.match.adapter
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemSeasoningCellBinding import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.entity.SeasoningEntity import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
/** /**
* 调料配置-下层(18格网格 Adapter * 18格网格 Adapter,同时服务于调料配置页和主设备秤总览页。
* 使用 GridLayoutManager6列)排列,第1行格子 margin 较大,第2-3行 margin 较小 *
* 实体类为 SeasoningEntity,格子显示调料名称 goodsName * @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState
* false = 秤总览模式,三行全部显示
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
*/ */
class Seasoning18GridAdapter : BaseQuickAdapter<SeasoningEntity, Seasoning18GridAdapter.VH>(mutableListOf()) { class Seasoning18GridAdapter(
private val showNameOnly: Boolean = false,
private val showAddress: Boolean = false
) : BaseQuickAdapter<ScaleData, Seasoning18GridAdapter.VH>(mutableListOf()) {
/** 空位哨兵,sort=-1 表示无效 */ /** 空位哨兵,address=0 表示无效 */
private val empty = SeasoningEntity(goodsId = "").also { it.sort = -1 } private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 当前高亮的格子位置,-1 表示无高亮 */ /** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1 private var highlightedPosition: Int = -1
/** 点击回调,预留,暂不使用 */ /** 点击回调 */
var onItemClick: ((SeasoningEntity, Int) -> Unit)? = null var onItemClick: ((ScaleData, Int) -> Unit)? = null
inner class VH(val b: ListItemSeasoningCellBinding) : QuickViewHolder(b.root) inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
/** 前6个位置为第1行(viewType=0margin大),其余为第2-3行(viewType=1margin小) */ /** 前6个位置为第1行(viewType=0margin大),其余为第2-3行(viewType=1margin小) */
override fun getItemViewType(position: Int, list: List<SeasoningEntity>) = override fun getItemViewType(position: Int, list: List<ScaleData>) =
if (position < 6) 0 else 1 if (position < 6) 0 else 1
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemSeasoningCellBinding.inflate(LayoutInflater.from(context), parent, false) val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
val margin = if (viewType == 0) 12.dp else 2.dp val margin = if (viewType == 0) 12.dp else 2.dp
val screenWidth = parent.resources.displayMetrics.widthPixels val screenWidth = parent.resources.displayMetrics.widthPixels
val cellSize = (screenWidth - 6 * margin * 2 - 10.dp) / 6 val cellSize = (screenWidth - 6 * margin * 2 - 10.dp) / 6
(b.root.layoutParams as? ViewGroup.MarginLayoutParams)?.also { (b.root.layoutParams as? ViewGroup.MarginLayoutParams)?.also {
it.width = cellSize it.width = cellSize
it.height = cellSize it.height = cellSize
// 第1行:顶部间距为0,底部间距2dp,左右保持原有 margin
if (viewType == 0) it.setMargins(margin, 0, margin, 2.dp) if (viewType == 0) it.setMargins(margin, 0, margin, 2.dp)
else it.setMargins(margin, margin, margin, margin) else it.setMargins(margin, margin, margin, margin)
} }
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt() val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvGoodsName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f) b.tvAddress.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
// 秤地址行:仅 MasterScaleActivity 显示
b.tvAddress.visibility = if (showAddress) View.VISIBLE else View.GONE
// 调料配置模式隐藏重量和状态行
if (showNameOnly) {
b.tvWeight.visibility = View.GONE
b.tvState.visibility = View.GONE
}
val vh = VH(b) val vh = VH(b)
// 点击事件预留,暂不处理
b.root.setOnClickListener { b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) { if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.let { item -> onItemClick?.invoke(item, pos) } items.getOrNull(pos)?.takeIf { it.address != 0 }?.let { item -> onItemClick?.invoke(item, pos) }
} }
} }
return vh return vh
} }
override fun onBindViewHolder(holder: VH, position: Int, item: SeasoningEntity?) { @SuppressLint("SetTextI18n")
holder.b.tvGoodsName.text = item?.goodsName?.takeIf { it.isNotBlank() } ?: "-" override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
// 高亮格子:红底白字;其余恢复默认 val valid = (item?.address ?: 0) != 0
if (position == highlightedPosition) { if (!valid) {
holder.b.tvGoodsName.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444)) holder.b.tvAddress.text = ""
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.white)) holder.b.tvName.text = "-"
holder.b.tvWeight.text = ""
holder.b.tvState.text = ""
} else { } else {
holder.b.tvGoodsName.setBackgroundResource(R.drawable.shape_scale_cell) holder.b.tvAddress.text = "${item!!.address}"
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.home_title)) holder.b.tvName.text = if (!item.name.isNullOrBlank()) item.name else "-"
holder.b.tvWeight.text = "${item.weight}g"
holder.b.tvState.text = stateStr(item.state)
}
if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddress.setTextColor(white)
holder.b.tvName.setTextColor(white)
holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white)
} else {
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
val sub = ContextCompat.getColor(holder.b.root.context, R.color.home_sub_title)
holder.b.tvAddress.setTextColor(sub)
holder.b.tvName.setTextColor(sub)
holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(sub)
} }
} }
/** /**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮 * 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/ */
fun highlightPosition(position: Int) { fun highlightPosition(position: Int) {
val prev = highlightedPosition val prev = highlightedPosition
@@ -91,23 +126,46 @@ class Seasoning18GridAdapter : BaseQuickAdapter<SeasoningEntity, Seasoning18Grid
} }
/** /**
* 更新指定位置的调料名称并刷新对应格子 * 更新指定位置的名称并刷新对应格子
* @param position 格子索引
* @param name 新的调料名称
*/ */
fun updateItemName(position: Int, name: String) { fun updateItemName(position: Int, name: String) {
items.getOrNull(position)?.goodsName = name items.getOrNull(position)?.let {
notifyItemChanged(position) set(position, it.copy(name = name))
}
} }
/** /**
* 按 SCALE_ORDER_18 的 sort 顺序更新数据,空位用哨兵填充 * 按 SCALE_ORDER_18 的 sort 顺序更新数据,空位用哨兵填充
* 只更新显示字段有变化的格子,避免全量刷新导致 ViewHolder 重建影响点击监听
*/ */
fun update(seasoningList: List<SeasoningEntity>) { fun update(list: List<ScaleData>) {
val bySort = seasoningList.associateBy { it.sort } val byAddress = list.associateBy { it.address }
val filled = ScaleDeviceConfig.SCALE_ORDER_18.map { sort -> val filled = ScaleDeviceConfig.SCALE_ORDER_18.map { addr ->
bySort[sort] ?: empty.copy().also { it.sort = sort } byAddress[addr] ?: empty.copy(address = addr)
} }
submitList(filled) if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
val cur = items.getOrNull(i)
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state || cur.name != scale.name) {
set(i, scale)
}
}
}
}
/**
* 将 ScaleData 列表按 SCALE_ORDER_18 重排后调用 update
*/
fun updateByAddress(scales: List<ScaleData>) {
update(scales)
}
private fun stateStr(state: Int) = when (state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> ""
} }
} }
@@ -1,7 +1,9 @@
package com.shuwei.dish.match.adapter package com.shuwei.dish.match.adapter
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -9,41 +11,45 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemSeasoningCellBinding import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.entity.SeasoningEntity import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
/** /**
* 调料配置-上层(22格网格 Adapter * 22格网格 Adapter,同时服务于调料配置页和主设备秤总览页。
* 使用 FlexboxLayoutManager 按列排列,前4个格子较大,其余较小
* 实体类为 SeasoningEntity,格子显示调料名称 goodsName
* *
* @param largeSize 前4个大格子的尺寸(px * @param largeSize 前4个大格子的尺寸(px
* @param smallSize 其余小格子的尺寸(px * @param smallSize 其余小格子的尺寸(px
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState
* false = 秤总览模式,三行全部显示
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
*/ */
class Seasoning22GridAdapter( class Seasoning22GridAdapter(
private val largeSize: Int, private val largeSize: Int,
private val smallSize: Int private val smallSize: Int,
) : BaseQuickAdapter<SeasoningEntity, Seasoning22GridAdapter.VH>(mutableListOf()) { private val showNameOnly: Boolean = false,
private val showAddress: Boolean = false
) : BaseQuickAdapter<ScaleData, Seasoning22GridAdapter.VH>(mutableListOf()) {
/** 空位哨兵,sort=-1 表示无效 */ /** 空位哨兵,address=0 表示无效 */
private val empty = SeasoningEntity(goodsId = "").also { it.sort = -1 } private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 当前高亮的格子位置,-1 表示无高亮 */ /** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1 private var highlightedPosition: Int = -1
/** 点击回调,预留,暂不使用 */ /** 点击回调 */
var onItemClick: ((SeasoningEntity, Int) -> Unit)? = null var onItemClick: ((ScaleData, Int) -> Unit)? = null
inner class VH(val b: ListItemSeasoningCellBinding) : QuickViewHolder(b.root) inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
/** 前4个位置为大格子(viewType=1),其余为小格子(viewType=0 */ /** 前4个位置为大格子(viewType=1),其余为小格子(viewType=0 */
override fun getItemViewType(position: Int, list: List<SeasoningEntity>) = override fun getItemViewType(position: Int, list: List<ScaleData>) =
if (position < 4) 1 else 0 if (position < 4) 1 else 0
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemSeasoningCellBinding.inflate(LayoutInflater.from(context), parent, false) val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
val cellSize = if (viewType == 1) largeSize else smallSize val cellSize = if (viewType == 1) largeSize else smallSize
(b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply { (b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply {
width = cellSize width = cellSize
@@ -51,33 +57,62 @@ class Seasoning22GridAdapter(
setMargins(2.dp, 2.dp, 2.dp, 2.dp) setMargins(2.dp, 2.dp, 2.dp, 2.dp)
} }
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt() val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvGoodsName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f) b.tvAddress.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
// 秤地址行:仅 MasterScaleActivity 显示
b.tvAddress.visibility = if (showAddress) View.VISIBLE else View.GONE
// 调料配置模式隐藏重量和状态行
if (showNameOnly) {
b.tvWeight.visibility = View.GONE
b.tvState.visibility = View.GONE
}
val vh = VH(b) val vh = VH(b)
// 点击事件预留,暂不处理
b.root.setOnClickListener { b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) { if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.let { item -> onItemClick?.invoke(item, pos) } items.getOrNull(pos)?.takeIf { it.address != 0 }?.let { item -> onItemClick?.invoke(item, pos) }
} }
} }
return vh return vh
} }
override fun onBindViewHolder(holder: VH, position: Int, item: SeasoningEntity?) { @SuppressLint("SetTextI18n")
holder.b.tvGoodsName.text = item?.goodsName?.takeIf { it.isNotBlank() } ?: "-" override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0
if (!valid) {
holder.b.tvAddress.text = ""
holder.b.tvName.text = "-"
holder.b.tvWeight.text = ""
holder.b.tvState.text = ""
} else {
holder.b.tvAddress.text = "${item!!.address}"
holder.b.tvName.text = if (!item.name.isNullOrBlank()) item.name else "-"
holder.b.tvWeight.text = "${item.weight}g"
holder.b.tvState.text = stateStr(item.state)
}
// 高亮格子:红底白字;其余恢复默认 // 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) { if (position == highlightedPosition) {
holder.b.tvGoodsName.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444)) holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.white)) val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddress.setTextColor(white)
holder.b.tvName.setTextColor(white)
holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white)
} else { } else {
holder.b.tvGoodsName.setBackgroundResource(R.drawable.shape_scale_cell) holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.home_title)) val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
val sub = ContextCompat.getColor(holder.b.root.context, R.color.home_sub_title)
holder.b.tvAddress.setTextColor(sub)
holder.b.tvName.setTextColor(sub)
holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(sub)
} }
} }
/** /**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮 * 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/ */
fun highlightPosition(position: Int) { fun highlightPosition(position: Int) {
val prev = highlightedPosition val prev = highlightedPosition
@@ -94,23 +129,46 @@ class Seasoning22GridAdapter(
} }
/** /**
* 更新指定位置的调料名称并刷新对应格子 * 更新指定位置的名称并刷新对应格子
* @param position 格子索引
* @param name 新的调料名称
*/ */
fun updateItemName(position: Int, name: String) { fun updateItemName(position: Int, name: String) {
items.getOrNull(position)?.goodsName = name items.getOrNull(position)?.let {
notifyItemChanged(position) set(position, it.copy(name = name))
}
} }
/** /**
* 按 SCALE_ORDER_22 的 sort 顺序更新数据,空位用哨兵填充 * 按 SCALE_ORDER_22 的 sort 顺序更新数据,空位用哨兵填充
* 只更新显示字段有变化的格子,避免全量刷新导致 ViewHolder 重建影响点击监听
*/ */
fun update(seasoningList: List<SeasoningEntity>) { fun update(list: List<ScaleData>) {
val bySort = seasoningList.associateBy { it.sort } val byAddress = list.associateBy { it.address }
val filled = ScaleDeviceConfig.SCALE_ORDER_22.map { sort -> val filled = ScaleDeviceConfig.SCALE_ORDER_22.map { addr ->
bySort[sort] ?: empty.copy().also { it.sort = sort } byAddress[addr] ?: empty.copy(address = addr)
} }
submitList(filled) if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
val cur = items.getOrNull(i)
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state || cur.name != scale.name) {
set(i, scale)
}
}
}
}
/**
* 将 ScaleData 列表按 SCALE_ORDER_22 重排后调用 update
*/
fun updateByAddress(scales: List<ScaleData>) {
update(scales)
}
private fun stateStr(state: Int) = when (state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> ""
} }
} }
@@ -0,0 +1,69 @@
package com.shuwei.dish.match.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.databinding.ItemSeasoningWeightBinding
/**
* 调料使用重量列表 Adapter
* 用于 SubmitFoodActivity 展示各调料槽位的实时用量
* 同名调料已在外部聚合,adapter 按 goodsName 作为唯一标识
*/
class SeasoningWeightAdapter : RecyclerView.Adapter<SeasoningWeightAdapter.VH>() {
/** 调料展示数据项,key = goodsName(同名调料已聚合) */
data class Item(
val goodsId: String,
val goodsName: String,
/** 使用重量(克),同名调料已累加 */
val useWeight: Double
)
val items = mutableListOf<Item>()
/**
* 更新单个 item,按 goodsName 匹配;不存在则追加
*/
fun updateItem(item: Item) {
val idx = items.indexOfFirst { it.goodsName == item.goodsName }
if (idx >= 0) {
items[idx] = item
notifyItemChanged(idx)
} else {
items.add(item)
notifyItemInserted(items.lastIndex)
}
}
/**
* 移除指定 goodsName 的 item(聚合后用量不足阈值时调用)
*/
fun removeItem(goodsName: String) {
val idx = items.indexOfFirst { it.goodsName == goodsName }
if (idx >= 0) {
items.removeAt(idx)
notifyItemRemoved(idx)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val binding = ItemSeasoningWeightBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
class VH(private val b: ItemSeasoningWeightBinding) : RecyclerView.ViewHolder(b.root) {
fun bind(item: Item) {
b.tvSeasoningName.text = item.goodsName
b.tvSeasoningWeight.text = "${item.useWeight}g"
}
}
}
@@ -7,9 +7,11 @@ import androidx.room.RoomDatabase
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.shuwei.dish.match.db.dao.AppDao import com.shuwei.dish.match.db.dao.AppDao
import com.shuwei.dish.match.db.dao.SeasoningSlotDao
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.SeasoningEntity import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.SeasoningSlotEntity
// 步骤1:更新版本号 // 步骤1:更新版本号
@@ -19,8 +21,9 @@ import com.shuwei.dish.match.entity.SeasoningEntity
SeasoningEntity::class, SeasoningEntity::class,
CookFoodEntity::class, CookFoodEntity::class,
CookFoodGoodsEntity::class, CookFoodGoodsEntity::class,
SeasoningSlotEntity::class,
], ],
version = 7, version = 8,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -30,6 +33,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun appDao(): AppDao abstract fun appDao(): AppDao
abstract fun seasoningSlotDao(): SeasoningSlotDao
} }
// 步骤2:创建Migration对象 // 步骤2:创建Migration对象
@@ -140,6 +145,20 @@ val MIGRATION_5_6 = MigrationImpl(5, 6) { db ->
db.endTransaction() db.endTransaction()
} }
val MIGRATION_7_8 = MigrationImpl(7, 8) { db ->
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `dm_seasoning_slot` (
`deviceId` TEXT NOT NULL,
`address` INTEGER NOT NULL,
`goodsId` TEXT NOT NULL,
`goodsName` TEXT NOT NULL,
PRIMARY KEY(`deviceId`, `address`)
)
""".trimIndent()
)
}
val MIGRATION_6_7 = MigrationImpl(6, 7) {db -> val MIGRATION_6_7 = MigrationImpl(6, 7) {db ->
runCatching { runCatching {
@@ -210,6 +229,7 @@ class DatabaseProvider(private val context: Context) {
// .addMigrations(MIGRATION_4_5) // .addMigrations(MIGRATION_4_5)
// .addMigrations(MIGRATION_5_6) // .addMigrations(MIGRATION_5_6)
// .addMigrations(MIGRATION_6_7) // .addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.build() .build()
} }
} }
@@ -0,0 +1,40 @@
package com.shuwei.dish.match.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.shuwei.dish.match.entity.SeasoningSlotEntity
/**
* 调料槽位配置 DAO
* 以 (deviceId, address) 为主键做 upsert,保证同一槽位只保留最新配置
*/
@Dao
interface SeasoningSlotDao {
/**
* 插入或更新单个槽位配置(主键冲突时覆盖)
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(slot: SeasoningSlotEntity)
/**
* 批量插入或更新槽位配置(主设备广播全量时使用)
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(slots: List<SeasoningSlotEntity>)
/**
* 查询指定设备的全部槽位配置
* @param deviceId 目标设备 ID
*/
@Query("SELECT * FROM dm_seasoning_slot WHERE deviceId = :deviceId")
suspend fun queryByDeviceId(deviceId: String): List<SeasoningSlotEntity>
/**
* 查询所有设备的全部槽位配置(主设备广播时使用)
*/
@Query("SELECT * FROM dm_seasoning_slot")
suspend fun queryAll(): List<SeasoningSlotEntity>
}
@@ -86,7 +86,7 @@ class SeasoningSelectDialog(
// RecyclerView 初始化及滑动冲突处理 // RecyclerView 初始化及滑动冲突处理
binding.recyclerView.run { binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false) layoutManager = GridLayoutManager(activity, 3, GridLayoutManager.VERTICAL, false)
adapter = this@SeasoningSelectDialog.adapter adapter = this@SeasoningSelectDialog.adapter
addOnScrollListener(object : RecyclerView.OnScrollListener() { addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
@@ -134,19 +134,15 @@ class SeasoningSelectDialog(
*/ */
private fun getGoodsList() { private fun getGoodsList() {
// TODO: 联调时注释掉下方模拟数据,取消注释真实接口调用 // TODO: 联调时注释掉下方模拟数据,取消注释真实接口调用
val mockData = mutableListOf( val nameList = listOf(
SeasoningEntity(goodsId = "1", goodsName = "精盐"), "", "白砂糖","冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香",
SeasoningEntity(goodsId = "2", goodsName = "鸡精"), "料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "", "", "", "香菜", ""
SeasoningEntity(goodsId = "3", goodsName = "陈醋"),
SeasoningEntity(goodsId = "4", goodsName = "老抽"),
SeasoningEntity(goodsId = "5", goodsName = "生抽"),
SeasoningEntity(goodsId = "6", goodsName = "花生油"),
SeasoningEntity(goodsId = "7", goodsName = "菜籽油"),
SeasoningEntity(goodsId = "8", goodsName = "胡椒粉"),
SeasoningEntity(goodsId = "9", goodsName = "十三香"),
SeasoningEntity(goodsId = "10", goodsName = "料酒"),
SeasoningEntity(goodsId = "11", goodsName = "白醋")
) )
val mockData = mutableListOf<SeasoningEntity>()
nameList.forEachIndexed { index, name ->
val padString = "${index + 1}".padStart(2, '0')
mockData.add(SeasoningEntity(goodsId = "1000$padString", goodsName = name))
}
loadGoodsList(mockData) loadGoodsList(mockData)
// val param = mutableMapOf<String, Any>( // val param = mutableMapOf<String, Any>(
@@ -0,0 +1,23 @@
package com.shuwei.dish.match.entity
import androidx.room.Entity
/**
* 调料槽位配置表,记录每个格子对应哪种调料
* 主键为 (deviceId, address) 组合,唯一标识一个物理秤槽位
*
* @param deviceId 所属设备 ID,用于区分 22格/18格 设备
* @param address 秤硬件地址,对应 ScaleDeviceConfig.SCALE_ORDER_22/18 中的值
* @param goodsId 调料 ID
* @param goodsName 调料名称,用于 adapter 展示
*/
@Entity(
tableName = "dm_seasoning_slot",
primaryKeys = ["deviceId", "address"]
)
data class SeasoningSlotEntity(
val deviceId: String,
val address: Int,
val goodsId: String,
val goodsName: String
)
@@ -15,5 +15,6 @@ data class ScaleData(
val weight: Double, val weight: Double,
val state: Int, val state: Int,
val ts: Long, val ts: Long,
val ip: String = "" val ip: String = "",
val name: String? = null
) )
@@ -1,20 +1,38 @@
package com.shuwei.dish.match.scale package com.shuwei.dish.match.scale
/** /**
* 秤事件数据类,用于设备向主设备发送非重量类通知 * 秤事件数据类,用于设备和子设备之间的非重量类通知
* @param type 事件类型,参见 companion object 中的常量 * @param type 事件类型,参见 companion object 中的常量
* @param deviceId 发送方设备 ID * @param deviceId 发送方设备 ID
* @param address 触发事件的秤地址 * @param address 触发事件的秤地址TYPE_SEASONING_CONFIG 时为 0(无意义)
* @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义 * @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义
* @param slots 调料槽位配置列表,仅 TYPE_SEASONING_CONFIG 时有效
*/ */
data class ScaleEvent( data class ScaleEvent(
val type: String, val type: String,
val deviceId: String, val deviceId: String,
val address: Int, val address: Int = 0,
val delta: Double = 0.0 val delta: Double = 0.0,
val slots: List<SlotConfig>? = null
) { ) {
/**
* 单个槽位的配置信息,随 TYPE_SEASONING_CONFIG 事件一起传输
* @param deviceId 所属设备 ID
* @param address 秤硬件地址
* @param goodsId 调料 ID
* @param goodsName 调料名称
*/
data class SlotConfig(
val deviceId: String,
val address: Int,
val goodsId: String,
val goodsName: String
)
companion object { companion object {
/** 调料添加事件:某秤重量增加超过阈值 */ /** 调料添加事件:某秤重量增加超过阈值 */
const val TYPE_SEASONING_ADDED = "seasoning_added" const val TYPE_SEASONING_ADDED = "seasoning_added"
/** 调料槽位配置同步事件:主设备配置变更后广播给子设备 */
const val TYPE_SEASONING_CONFIG = "seasoning_config"
} }
} }
@@ -2,11 +2,15 @@ package com.shuwei.dish.match.scale
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.shuwei.dish.match.base.BaseApp
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.utils.NetworkUtil import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/** /**
* 秤服务统一门面(单例) * 秤服务统一门面(单例)
@@ -59,6 +63,36 @@ object ScaleServiceManager {
wsClient?.onScaleEvent = value wsClient?.onScaleEvent = value
} }
/**
* 收到主设备下发的调料配置同步事件时的回调,仅子设备有效
* 在子线程中调用,需自行切换到主线程更新 UI
*/
var onSeasoningConfig: ((ScaleEvent) -> Unit)?
get() = wsServer?.onSeasoningConfig
set(value) { wsServer?.onSeasoningConfig = value }
/**
* 向所有已连接子设备广播调料配置(主设备调用)
* @param slots 全量槽位配置列表
*/
fun sendSeasoningConfig(slots: List<ScaleEvent.SlotConfig>) {
wsClient?.sendToAllSlaves(buildConfigEvent(slots))
}
/**
* 向单台刚连接的子设备推送调料配置(连接时按需调用)
* @param deviceId 目标子设备 ID
*/
private fun sendSeasoningConfigTo(deviceId: String, slots: List<ScaleEvent.SlotConfig>) {
wsClient?.sendToDevice(deviceId, buildConfigEvent(slots))
}
private fun buildConfigEvent(slots: List<ScaleEvent.SlotConfig>) = ScaleEvent(
type = ScaleEvent.TYPE_SEASONING_CONFIG,
deviceId = GlobalData.deviceId,
slots = slots
)
/** /**
* 向主设备广播秤事件(子设备调用) * 向主设备广播秤事件(子设备调用)
* @param event 要广播的事件 * @param event 要广播的事件
@@ -123,6 +157,19 @@ object ScaleServiceManager {
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) } it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) } it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) }
it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) } it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) }
it.onDeviceConnected = { remoteId ->
// 子设备连接成功时,从 Room 读取全量配置并单独推送给该设备
CoroutineScope(Dispatchers.IO).launch {
val slots = BaseApp.instance!!.database.seasoningSlotDao()
.queryAll()
.map { slot ->
ScaleEvent.SlotConfig(
slot.deviceId, slot.address, slot.goodsId, slot.goodsName
)
}
if (slots.isNotEmpty()) sendSeasoningConfigTo(remoteId, slots)
}
}
} }
wsClient = client wsClient = client
@@ -55,6 +55,9 @@ class ScaleWebSocketClient {
/** 设备断线时的回调(连接失败或关闭),在子线程调用 */ /** 设备断线时的回调(连接失败或关闭),在子线程调用 */
var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null
/** 子设备首次连接成功时的回调,在子线程调用;可用于主设备主动推送全量配置 */
var onDeviceConnected: ((deviceId: String) -> Unit)? = null
/** /**
* 连接到指定子设备 * 连接到指定子设备
* @param deviceId 子设备 ID * @param deviceId 子设备 ID
@@ -79,6 +82,7 @@ class ScaleWebSocketClient {
connections[deviceId] = webSocket connections[deviceId] = webSocket
reconnectDelays[deviceId] = RECONNECT_BASE_MS reconnectDelays[deviceId] = RECONNECT_BASE_MS
reconnectTasks.remove(deviceId)?.cancel(false) reconnectTasks.remove(deviceId)?.cancel(false)
onDeviceConnected?.invoke(deviceId)
} }
override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
@@ -148,6 +152,31 @@ class ScaleWebSocketClient {
return ws.send(gson.toJson(command)) return ws.send(gson.toJson(command))
} }
/**
* 向所有已连接的子设备广播事件(主设备调用,用于配置同步等场景)
* @param event 要广播的事件
*/
fun sendToAllSlaves(event: ScaleEvent) {
val json = gson.toJson(event)
connections.values.forEach { ws ->
try { ws.send(json) } catch (e: Exception) {
Log.w(TAG, "sendToAllSlaves 失败: ${e.message}")
}
}
}
/**
* 向指定子设备发送事件(用于连接时单独推送配置)
* @param deviceId 目标子设备 ID
* @param event 要发送的事件
*/
fun sendToDevice(deviceId: String, event: ScaleEvent) {
val ws = connections[deviceId] ?: return
try { ws.send(gson.toJson(event)) } catch (e: Exception) {
Log.w(TAG, "sendToDevice $deviceId 失败: ${e.message}")
}
}
/** /**
* 断开指定子设备连接(不再重连) * 断开指定子设备连接(不再重连)
* @param deviceId 子设备 ID * @param deviceId 子设备 ID
@@ -28,6 +28,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
/** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */ /** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */
var onConnectionChanged: ((connected: Boolean) -> Unit)? = null var onConnectionChanged: ((connected: Boolean) -> Unit)? = null
/** 收到主设备下发的调料配置同步事件时的回调,在子线程调用 */
var onSeasoningConfig: ((event: ScaleEvent) -> Unit)? = null
/** 当前是否有主设备连接 */ /** 当前是否有主设备连接 */
val isConnected: Boolean val isConnected: Boolean
get() = server?.connections?.isNotEmpty() == true get() = server?.connections?.isNotEmpty() == true
@@ -72,6 +75,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
*/ */
fun stop() { fun stop() {
WeightUtil.removeWeightListener(TAG) WeightUtil.removeWeightListener(TAG)
onConnectionChanged = null
onSeasoningConfig = null
try { try {
server?.stop(1000) server?.stop(1000)
} catch (e: Exception) { } catch (e: Exception) {
@@ -135,6 +140,15 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
override fun onMessage(conn: WebSocket, message: String) { override fun onMessage(conn: WebSocket, message: String) {
try { try {
val raw = gson.fromJson(message, Map::class.java)
if (raw.containsKey("type")) {
// 主设备下发的事件(如调料配置同步)
val event = gson.fromJson(message, ScaleEvent::class.java)
when (event.type) {
ScaleEvent.TYPE_SEASONING_CONFIG -> onSeasoningConfig?.invoke(event)
}
return
}
val cmd = gson.fromJson(message, ScaleCommand::class.java) val cmd = gson.fromJson(message, ScaleCommand::class.java)
// 校验指令目标设备是否为本机 // 校验指令目标设备是否为本机
if (cmd.deviceId != deviceId) { if (cmd.deviceId != deviceId) {
@@ -148,7 +162,7 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "收到清零指令, 解析指令失败: ${e.message}") Log.w(TAG, "收到消息, 解析失败: ${e.message}")
} }
} }
@@ -2,8 +2,9 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.addCallback
import androidx.core.content.IntentCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
@@ -28,6 +29,7 @@ class FoodRecognizeActivity : BaseActivity() {
private const val TAG = "FoodRecognizeActivity" private const val TAG = "FoodRecognizeActivity"
private const val EXTRA_IMAGE_URI = "extra_image_uri" private const val EXTRA_IMAGE_URI = "extra_image_uri"
const val EXTRA_SELECTED_ITEM = "extra_selected_item" const val EXTRA_SELECTED_ITEM = "extra_selected_item"
const val IS_MANUAL_CANCEL = "isManualCancel"
/** 内存缓存食材列表,避免通过 Intent 序列化传递大数据 */ /** 内存缓存食材列表,避免通过 Intent 序列化传递大数据 */
private var pendingGoodsList: ArrayList<CookFoodGoodsEntity>? = null private var pendingGoodsList: ArrayList<CookFoodGoodsEntity>? = null
@@ -37,22 +39,27 @@ class FoodRecognizeActivity : BaseActivity() {
* @param activity 宿主 Activity * @param activity 宿主 Activity
* @param imageUri 食材照片路径(可为空) * @param imageUri 食材照片路径(可为空)
* @param goodsList 识别到的食材列表 * @param goodsList 识别到的食材列表
* @param onResult 页面关闭后回调,携带用户选中的食材(未选中时为 null) * @param finishCallback 页面关闭后回调,携带用户选中的食材(未选中时为 null)
*/ */
fun start( fun start(
activity: BaseActivity, activity: BaseActivity,
imageUri: String? = null, imageUri: String? = null,
goodsList: ArrayList<CookFoodGoodsEntity> = arrayListOf(), goodsList: ArrayList<CookFoodGoodsEntity> = arrayListOf(),
onResult: (CookFoodGoodsEntity?) -> Unit = {} finishCallback: (Boolean, CookFoodGoodsEntity?) -> Unit = { _, _ -> }
) { ) {
pendingGoodsList = goodsList pendingGoodsList = goodsList
val intent = Intent(activity, FoodRecognizeActivity::class.java).apply { val launchIntent = Intent(activity, FoodRecognizeActivity::class.java).apply {
putExtra(EXTRA_IMAGE_URI, imageUri) putExtra(EXTRA_IMAGE_URI, imageUri)
} }
activity.startActivity(intent) { resultIntent -> activity.startActivity(launchIntent) { resultIntent ->
@Suppress("DEPRECATION") val isManualCancel = resultIntent?.getBooleanExtra(IS_MANUAL_CANCEL, false) ?: false
val item = resultIntent?.getSerializableExtra(EXTRA_SELECTED_ITEM) as? CookFoodGoodsEntity val entity = resultIntent?.let {
onResult(item) IntentCompat.getSerializableExtra(
it,
EXTRA_SELECTED_ITEM,
CookFoodGoodsEntity::class.java)
}
finishCallback(isManualCancel, entity)
} }
} }
@@ -64,7 +71,7 @@ class FoodRecognizeActivity : BaseActivity() {
ActivityManager.getActivityStack() ActivityManager.getActivityStack()
.filterIsInstance<FoodRecognizeActivity>() .filterIsInstance<FoodRecognizeActivity>()
.lastOrNull() .lastOrNull()
?.finishWithResult() ?.finishWithResult(false)
} }
} }
@@ -103,11 +110,30 @@ class FoodRecognizeActivity : BaseActivity() {
setHeaderBackground() setHeaderBackground()
// 显示标题栏 // 显示标题栏
setTitleBar(titleBarAction = { it.visible() }, titleAction = { it.text = "食材识别" }) setTitleBar(
titleBarAction = { it.visible() },
titleAction = { it.text = "食材识别" },
backAction = {
it.setOnClickListener {
manualBack()
}
}
)
initData() initData()
initRecyclerView() initRecyclerView()
initClickListeners() initClickListeners()
onBackPressedDispatcher.addCallback(this) {
manualBack()
}
}
/**
* 手动返回
*/
private fun manualBack() {
finishWithResult(true)
} }
/** /**
@@ -168,7 +194,7 @@ class FoodRecognizeActivity : BaseActivity() {
} }
// 取消:关闭页面 // 取消:关闭页面
binding.btnCancel.setOnClickListener { finish() } binding.btnCancel.setOnClickListener { manualBack() }
} }
/** /**
@@ -208,12 +234,8 @@ class FoodRecognizeActivity : BaseActivity() {
} }
if (weight >= PrepareCookActivity.WEIGHT_RECOGNIZE_VALUE) { if (weight >= PrepareCookActivity.WEIGHT_RECOGNIZE_VALUE) {
adapter.items.firstOrNull { it.isClicked }?.let { item -> adapter.items.firstOrNull { it.isClicked }?.let { item ->
if (weight > (item.useWeight ?: 0.0)) { if (pendingWeight != weight) {
// 重量增加:立即更新 // 无论增减,均延迟更新,避免手拿物品时的瞬时增重干扰
cancelPendingWeightUpdate()
item.useWeight = currentWeight
} else if (pendingWeight != weight) {
// 重量减少且目标值变化:重置延迟任务,3 秒无新变化后更新
cancelPendingWeightUpdate() cancelPendingWeightUpdate()
pendingWeight = weight pendingWeight = weight
pendingWeightRunnable = Runnable { pendingWeightRunnable = Runnable {
@@ -239,10 +261,20 @@ class FoodRecognizeActivity : BaseActivity() {
/** /**
* 携带当前选中食材关闭页面 * 携带当前选中食材关闭页面
*/ */
private fun finishWithResult() { private fun finishWithResult(isManualCancel: Boolean) {
val selected = list.getOrNull(selectedPosition) cancelPendingWeightUpdate()
val intent = Intent().apply { val intent = Intent().apply {
selected?.let { putExtra(EXTRA_SELECTED_ITEM, it) } putExtra(IS_MANUAL_CANCEL, isManualCancel)
if (isManualCancel.not()) {
list.getOrNull(selectedPosition)?.let {
// 兜底:若 useWeight 仍为空或0,使用当前实时重量
if ((it.useWeight ?: 0.0) <= 0.0) {
it.useWeight = currentWeight
}
putExtra(EXTRA_SELECTED_ITEM, it)
}
}
} }
setResult(RESULT_OK, intent) setResult(RESULT_OK, intent)
finish() finish()
@@ -251,6 +283,6 @@ class FoodRecognizeActivity : BaseActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
cancelPendingWeightUpdate() cancelPendingWeightUpdate()
// WeightUtil.removeWeightListener(TAG) WeightUtil.removeWeightListener(TAG)
} }
} }
@@ -3,11 +3,9 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import androidx.core.view.updateLayoutParams
import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -18,8 +16,8 @@ import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.Scale18GridAdapter import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
import com.shuwei.dish.match.adapter.Scale22GridAdapter import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding
@@ -27,9 +25,13 @@ import com.shuwei.dish.match.databinding.ListTypeScale1Binding
import com.shuwei.dish.match.databinding.ListTypeScale18Binding import com.shuwei.dish.match.databinding.ListTypeScale18Binding
import com.shuwei.dish.match.databinding.ListTypeScale2Binding import com.shuwei.dish.match.databinding.ListTypeScale2Binding
import com.shuwei.dish.match.databinding.ListTypeScale22Binding import com.shuwei.dish.match.databinding.ListTypeScale22Binding
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.shuwei.dish.match.utils.NetworkUtil import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
@@ -37,10 +39,10 @@ import com.shuwei.dish.match.utils.ext.toast
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val VIEW_TYPE_SCALE2 = 0 private const val VIEW_TYPE_SCALE2 = 0
private const val VIEW_TYPE_SCALE22 = 1 private const val VIEW_TYPE_SCALE22 = 1
private const val VIEW_TYPE_SCALE18 = 2 private const val VIEW_TYPE_SCALE18 = 2
private const val VIEW_TYPE_SCALE1 = 3 private const val VIEW_TYPE_SCALE1 = 3
/** /**
* 主设备秤数据总览页面 * 主设备秤数据总览页面
@@ -59,6 +61,9 @@ class MasterScaleActivity : BaseActivity() {
private val groupList = mutableListOf<DeviceGroup>() private val groupList = mutableListOf<DeviceGroup>()
private val adapter = DeviceGroupAdapter(groupList) private val adapter = DeviceGroupAdapter(groupList)
/** key = "$deviceId#$address"value = 调料名称,用于注入 ScaleData.name */
private val slotNameMap = mutableMapOf<String, String>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMasterScaleBinding.inflate(layoutInflater) binding = ActivityMasterScaleBinding.inflate(layoutInflater)
@@ -70,7 +75,26 @@ class MasterScaleActivity : BaseActivity() {
binding.rvScaleList.itemAnimator = null binding.rvScaleList.itemAnimator = null
binding.rvScaleList.adapter = adapter binding.rvScaleList.adapter = adapter
observeScaleData() loadSlotNames()
}
/** 从 Room 加载槽位配置到 slotNameMap,加载完成后启动数据观测 */
private fun loadSlotNames() {
lifecycleScope.launch(Dispatchers.IO) {
val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll()
withContext(Dispatchers.Main) {
slots.forEach { slot ->
slotNameMap["${slot.deviceId}#${slot.address}"] = slot.goodsName
}
observeScaleData()
}
}
}
/** 用 slotNameMap 为 ScaleData 注入名称 */
private fun ScaleData.withSlotName(): ScaleData {
val name = slotNameMap["$deviceId#$address"]
return if (name != null) copy(name = name) else this
} }
private fun observeScaleData() { private fun observeScaleData() {
@@ -83,24 +107,30 @@ class MasterScaleActivity : BaseActivity() {
.groupBy { it.deviceId } .groupBy { it.deviceId }
.entries .entries
.sortedWith(compareBy { entry -> .sortedWith(compareBy { entry ->
ScaleDeviceConfig.DEVICE_ORDER.indexOf(entry.key).let { if (it == -1) Int.MAX_VALUE else it } ScaleDeviceConfig.DEVICE_ORDER.indexOf(entry.key)
.let { if (it == -1) Int.MAX_VALUE else it }
}) })
.map { (deviceId, scales) -> .map { (deviceId, scales) ->
DeviceGroup( DeviceGroup(
deviceId = deviceId, deviceId = deviceId,
ip = if (deviceId == localId) localIp ip = if (deviceId == localId) localIp
else scales.firstOrNull { it.ip.isNotEmpty() }?.ip ?: "", else scales.firstOrNull { it.ip.isNotEmpty() }?.ip ?: "",
scales = scales.sortedBy { it.address } scales = scales.map { it.withSlotName() }.sortedBy { it.address }
) )
} }
val structureChanged = groups.size != groupList.size || val structureChanged = groups.size != groupList.size ||
groups.indices.any { groups[it].deviceId != groupList[it].deviceId } groups.indices.any { groups[it].deviceId != groupList[it].deviceId }
if (structureChanged) { if (structureChanged) {
updateList(groups) updateList(groups)
} else { } else {
groups.forEach { group -> adapter.updateScalesForDevice(group.deviceId, group.scales) } groups.forEach { group ->
adapter.updateScalesForDevice(
group.deviceId,
group.scales
)
}
} }
updateStatusBar(scaleMap) updateStatusBar(scaleMap)
@@ -142,8 +172,8 @@ class MasterScaleActivity : BaseActivity() {
private inner class DeviceGroupAdapter(data: MutableList<DeviceGroup>) : private inner class DeviceGroupAdapter(data: MutableList<DeviceGroup>) :
BaseMultiItemAdapter<DeviceGroup>(data) { BaseMultiItemAdapter<DeviceGroup>(data) {
private val scale1VHMap = mutableMapOf<String, Scale1VH>() private val scale1VHMap = mutableMapOf<String, Scale1VH>()
private val scale2VHMap = mutableMapOf<String, Scale2VH>() private val scale2VHMap = mutableMapOf<String, Scale2VH>()
private val scale22VHMap = mutableMapOf<String, Scale22VH>() private val scale22VHMap = mutableMapOf<String, Scale22VH>()
private val scale18VHMap = mutableMapOf<String, Scale18VH>() private val scale18VHMap = mutableMapOf<String, Scale18VH>()
@@ -155,101 +185,171 @@ class MasterScaleActivity : BaseActivity() {
} }
init { init {
addItemType(VIEW_TYPE_SCALE2, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> { addItemType(
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) = VIEW_TYPE_SCALE2,
QuickViewHolder(ListTypeScale2Binding.inflate(LayoutInflater.from(context), parent, false).root) object :
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) { BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
item ?: return override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
val b = ListTypeScale2Binding.bind(holder.itemView) QuickViewHolder(
val vh = Scale2VH(b) ListTypeScale2Binding.inflate(
scale2VHMap[item.deviceId] = vh LayoutInflater.from(context),
vh.bind(item) parent,
false
).root
)
override fun onBind(
holder: QuickViewHolder,
position: Int,
item: DeviceGroup?
) {
item ?: return
val b = ListTypeScale2Binding.bind(holder.itemView)
val vh = Scale2VH(b)
scale2VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(
VIEW_TYPE_SCALE22,
object :
BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
QuickViewHolder(
ListTypeScale22Binding.inflate(
LayoutInflater.from(context),
parent,
false
).root
)
override fun onBind(
holder: QuickViewHolder,
position: Int,
item: DeviceGroup?
) {
item ?: return
val b = ListTypeScale22Binding.bind(holder.itemView)
// 每次 onBind 时注册/更新 VH 缓存,后续数据更新直接走 VH,不再触发 notify
val vh = Scale22VH(b)
scale22VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(
VIEW_TYPE_SCALE18,
object :
BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
QuickViewHolder(
ListTypeScale18Binding.inflate(
LayoutInflater.from(context),
parent,
false
).root
)
override fun onBind(
holder: QuickViewHolder,
position: Int,
item: DeviceGroup?
) {
item ?: return
val b = ListTypeScale18Binding.bind(holder.itemView)
val vh = Scale18VH(b)
scale18VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(
VIEW_TYPE_SCALE1,
object :
BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
QuickViewHolder(
ListTypeScale1Binding.inflate(
LayoutInflater.from(context),
parent,
false
).root
)
override fun onBind(
holder: QuickViewHolder,
position: Int,
item: DeviceGroup?
) {
item ?: return
val b = ListTypeScale1Binding.bind(holder.itemView)
val vh = Scale1VH(b)
scale1VHMap[item.deviceId] = vh
vh.bind(item)
}
})
onItemViewType { position, list ->
when (list.getOrNull(position)?.deviceId) {
ScaleDeviceConfig.DEVICE_ID_2 -> VIEW_TYPE_SCALE2
ScaleDeviceConfig.DEVICE_ID_22 -> VIEW_TYPE_SCALE22
ScaleDeviceConfig.DEVICE_ID_18 -> VIEW_TYPE_SCALE18
else -> VIEW_TYPE_SCALE1
} }
}) }
addItemType(VIEW_TYPE_SCALE22, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
QuickViewHolder(ListTypeScale22Binding.inflate(LayoutInflater.from(context), parent, false).root)
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) {
item ?: return
val b = ListTypeScale22Binding.bind(holder.itemView)
// 每次 onBind 时注册/更新 VH 缓存,后续数据更新直接走 VH,不再触发 notify
val vh = Scale22VH(b)
scale22VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(VIEW_TYPE_SCALE18, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
QuickViewHolder(ListTypeScale18Binding.inflate(LayoutInflater.from(context), parent, false).root)
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) {
item ?: return
val b = ListTypeScale18Binding.bind(holder.itemView)
val vh = Scale18VH(b)
scale18VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(VIEW_TYPE_SCALE1, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
QuickViewHolder(ListTypeScale1Binding.inflate(LayoutInflater.from(context), parent, false).root)
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) {
item ?: return
val b = ListTypeScale1Binding.bind(holder.itemView)
val vh = Scale1VH(b)
scale1VHMap[item.deviceId] = vh
vh.bind(item)
}
})
onItemViewType { position, list -> when (list.getOrNull(position)?.deviceId) {
ScaleDeviceConfig.DEVICE_ID_2 -> VIEW_TYPE_SCALE2
ScaleDeviceConfig.DEVICE_ID_22 -> VIEW_TYPE_SCALE22
ScaleDeviceConfig.DEVICE_ID_18 -> VIEW_TYPE_SCALE18
else -> VIEW_TYPE_SCALE1
}}
} }
} }
// ── ViewHolder: 2个秤 ────────────────────────────────────────────────── // ── ViewHolder: 2个秤 ──────────────────────────────────────────────────
private inner class Scale2VH(val b: ListTypeScale2Binding) : RecyclerView.ViewHolder(b.root) { private inner class Scale2VH(val b: ListTypeScale2Binding) : RecyclerView.ViewHolder(b.root) {
private lateinit var cells: List<LinearLayout>
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) { fun bind(group: DeviceGroup) {
b.tvDeviceIp.text = "IP${group.ip.ifEmpty { "未知" }}" b.tvDeviceIp.text = "IP${group.ip.ifEmpty { "未知" }}"
b.tvDeviceLabel.text = "设备:${group.deviceId}" b.tvDeviceLabel.text = "设备:${group.deviceId}"
// 两个格子显示秤地址行,隐藏重量/状态(showAddress 通过 visibility 控制)
if (!::cells.isInitialized) { listOf(b.cell1, b.cell2).forEach { cell ->
val cellSize = (resources.displayMetrics.widthPixels - 4 * 4.dp - 10.dp) / 2 cell.tvAddress.visibility = android.view.View.VISIBLE
cells = group.scales.take(2).map { scale -> cell.tvWeight.visibility = android.view.View.VISIBLE
makeScaleCell(scale, cellSize).also { cell -> cell.tvState.visibility = android.view.View.VISIBLE
cell.setOnClickListener { cell.root.setOnClickListener {
val s = cell.tag as? ScaleData ?: return@setOnClickListener val s = cell.root.tag as? ScaleData ?: return@setOnClickListener
showTareDialog(deviceId = s.deviceId, ip = s.ip, address = s.address) showTareDialog(deviceId = s.deviceId, ip = s.ip, address = s.address)
}
}
} }
b.llScaleContainer.removeAllViews()
cells.forEach { b.llScaleContainer.addView(it) }
} }
updateScales(group.scales) updateScales(group.scales)
} }
fun updateScales(scales: List<ScaleData>) { fun updateScales(scales: List<ScaleData>) {
if (!::cells.isInitialized) return bindCell(b.cell1, scales.getOrNull(0))
scales.take(2).forEachIndexed { i, scale -> bindCell(b.cell2, scales.getOrNull(1))
cells.getOrNull(i)?.let { cell -> }
cell.tag = scale
updateScaleCell(cell, scale) @SuppressLint("SetTextI18n")
} private fun bindCell(cell: ListItemScale22RowBinding, scale: ScaleData?) {
cell.root.tag = scale
cell.tvAddress.run {
text = scale?.let { "${it.address}" } ?: ""
textSize = 18f
} }
cell.tvName.run {
text = if (scale?.address == 1) "净材" else "熟食"
textSize = 22f
}
cell.tvWeight.run {
text = scale?.let { "${it.weight}g" } ?: ""
textSize = 22f
}
cell.tvState.run {
text = stateStr(scale?.state)
textSize = 22f
}
cell.root.updateLayoutParams { height = 400.dp }
} }
} }
// ── ViewHolder: 22个秤 ───────────────────────────────────────────────── // ── ViewHolder: 22个秤 ─────────────────────────────────────────────────
private inner class Scale22VH(val b: ListTypeScale22Binding) : RecyclerView.ViewHolder(b.root) { private inner class Scale22VH(val b: ListTypeScale22Binding) : RecyclerView.ViewHolder(b.root) {
private var innerAdapter: Scale22GridAdapter? = null private var innerAdapter: Seasoning22GridAdapter? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) { fun bind(group: DeviceGroup) {
@@ -269,9 +369,18 @@ class MasterScaleActivity : BaseActivity() {
alignItems = AlignItems.FLEX_START alignItems = AlignItems.FLEX_START
} }
b.rvScales.itemAnimator = null b.rvScales.itemAnimator = null
innerAdapter = Scale22GridAdapter(largeSize, smallSize).apply { innerAdapter = Seasoning22GridAdapter(
onItemClick = { scale -> largeSize,
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address) smallSize,
showNameOnly = false,
showAddress = true
).apply {
onItemClick = { scale, _ ->
showTareDialog(
deviceId = scale.deviceId,
ip = scale.ip,
address = scale.address
)
} }
} }
b.rvScales.adapter = innerAdapter b.rvScales.adapter = innerAdapter
@@ -288,7 +397,7 @@ class MasterScaleActivity : BaseActivity() {
// ── ViewHolder: 18个秤 ───────────────────────────────────────────────── // ── ViewHolder: 18个秤 ─────────────────────────────────────────────────
private inner class Scale18VH(val b: ListTypeScale18Binding) : RecyclerView.ViewHolder(b.root) { private inner class Scale18VH(val b: ListTypeScale18Binding) : RecyclerView.ViewHolder(b.root) {
private var innerAdapter: Scale18GridAdapter? = null private var innerAdapter: Seasoning18GridAdapter? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) { fun bind(group: DeviceGroup) {
@@ -303,11 +412,16 @@ class MasterScaleActivity : BaseActivity() {
} }
b.rvScales.layoutManager = gridLayoutManager b.rvScales.layoutManager = gridLayoutManager
b.rvScales.itemAnimator = null b.rvScales.itemAnimator = null
innerAdapter = Scale18GridAdapter().apply { innerAdapter =
onItemClick = { scale -> Seasoning18GridAdapter(showNameOnly = false, showAddress = true).apply {
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address) onItemClick = { scale, _ ->
showTareDialog(
deviceId = scale.deviceId,
ip = scale.ip,
address = scale.address
)
}
} }
}
b.rvScales.adapter = innerAdapter b.rvScales.adapter = innerAdapter
} }
updateScales(group.scales) updateScales(group.scales)
@@ -349,53 +463,13 @@ class MasterScaleActivity : BaseActivity() {
// ── 工具方法 ─────────────────────────────────────────────────────────── // ── 工具方法 ───────────────────────────────────────────────────────────
private fun stateStr(state: Int?) = when (state) { private fun stateStr(state: Int?) = when (state) {
WeightUtil.STATE_STABLE -> "稳定" WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定" WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量" WeightUtil.STATE_OVER_WEIGHT -> "超量"
null -> "" null -> ""
else -> "$state" else -> "$state"
} }
/** 创建正方形秤格子(用于 Scale2VH 初始化) */
@SuppressLint("SetTextI18n")
private fun makeScaleCell(scale: ScaleData, cellSize: Int): LinearLayout {
val margin = 2.dp
return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(0, cellSize - margin * 2, 1f)
.also { it.setMargins(margin, margin, margin, margin) }
setPadding(4.dp, 2.dp, 4.dp, 2.dp)
setBackgroundResource(R.drawable.shape_scale_cell)
val baseSize = ((cellSize - margin * 2) / resources.displayMetrics.density).toInt()
addView(makeCellText("${scale.address}", (baseSize * 0.16f).coerceIn(9f, 14f).toInt()))
addView(makeCellText("${scale.weight}g", (baseSize * 0.22f).coerceIn(12f, 20f).toInt(), bold = true))
addView(makeCellText(stateStr(scale.state), (baseSize * 0.14f).coerceIn(8f, 12f).toInt()))
tag = scale
}
}
/** 仅更新格子内文字,不重建 view(用于 Scale2VH 数据刷新) */
@SuppressLint("SetTextI18n")
private fun updateScaleCell(cell: LinearLayout, scale: ScaleData) {
(cell.getChildAt(0) as? TextView)?.text = "${scale.address}"
(cell.getChildAt(1) as? TextView)?.text = "${scale.weight}g"
(cell.getChildAt(2) as? TextView)?.text = stateStr(scale.state)
}
private fun makeCellText(text: String, spSize: Int, bold: Boolean = false): TextView =
TextView(this).apply {
this.text = text
textSize = spSize.toFloat()
if (bold) setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(if (bold) getColor(R.color.home_title) else getColor(R.color.home_sub_title))
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
)
gravity = Gravity.CENTER
includeFontPadding = false
}
/** /**
* 弹出清零确认对话框 * 弹出清零确认对话框
* @param deviceId 目标设备 ID * @param deviceId 目标设备 ID
@@ -17,7 +17,6 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.entity.FoodRecord
@@ -32,7 +31,6 @@ import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toJsonString import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.toast
@@ -53,7 +51,7 @@ class PrepareCookActivity : BaseActivity() {
const val FOOD_NAME = "foodName" const val FOOD_NAME = "foodName"
const val PAGE_FROM = "pageFrom" const val PAGE_FROM = "pageFrom"
const val HOME = "home" const val HOME = "home"
const val WEIGHT_CHANGE_VALUE = 5 const val WEIGHT_CHANGE_VALUE = 15
const val WEIGHT_RECOGNIZE_VALUE = 10 const val WEIGHT_RECOGNIZE_VALUE = 10
} }
@@ -173,10 +171,13 @@ class PrepareCookActivity : BaseActivity() {
} }
private val isTakingPhoto = AtomicBoolean(false) private val isTakingPhoto = AtomicBoolean(false)
private var isRecognizeOpen = false // 识别成功并查询到数据打开识别页面
private var showRecognizePage = false
// 页面可见时允许识别,onPause 后禁止,onResume 后恢复 // 页面可见时允许识别,onPause 后禁止,onResume 后恢复
private var isRecognizeEnabled = true private var pageVisible = true
// 手动取消标识
private var manualCancelFlag = false
private var lastPhotoUri: Uri? = null private var lastPhotoUri: Uri? = null
private var lastWeight = 0.0 private var lastWeight = 0.0
private var currentWeight = 0.0 private var currentWeight = 0.0
@@ -188,11 +189,14 @@ class PrepareCookActivity : BaseActivity() {
private fun recognizeFood(weight: Double) { private fun recognizeFood(weight: Double) {
this.currentWeight = weight this.currentWeight = weight
if (weight < WEIGHT_RECOGNIZE_VALUE) { if (weight < WEIGHT_RECOGNIZE_VALUE) {
isRecognizeOpen = false showRecognizePage = false
manualCancelFlag = false
FoodRecognizeActivity.close() FoodRecognizeActivity.close()
return return
} }
if (!isRecognizeEnabled || isTakingPhoto.get() || isRecognizeOpen || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE) return if (!pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE) {
return
}
isTakingPhoto.set(true) isTakingPhoto.set(true)
cameraUtils.takePhoto( cameraUtils.takePhoto(
succCallback = cameraSuccessCallback, succCallback = cameraSuccessCallback,
@@ -205,14 +209,14 @@ class PrepareCookActivity : BaseActivity() {
* - list 中不含该 goodsId:直接新增,isNewDishType = true * - list 中不含该 goodsId:直接新增,isNewDishType = true
* - list 中已含该 goodsId:累加 useWeight * - list 中已含该 goodsId:累加 useWeight
*/ */
private val foodSelectCallback: (CookFoodGoodsEntity) -> Unit = { item -> private val foodSelectCallback: (CookFoodGoodsEntity) -> Unit = { entity ->
val existing = list.firstOrNull { it.goodsId == item.goodsId } val existing = list.firstOrNull { it.goodsId == entity.goodsId }
if (existing != null) { if (existing != null) {
existing.useWeight = (existing.useWeight ?: 0.0) + (item.useWeight ?: 0.0) existing.useWeight = (existing.useWeight ?: 0.0) + (entity.useWeight ?: 0.0)
existing.isSetFinished = true existing.isSetFinished = true
dishPartAdapter.notifyItemChanged(list.indexOf(existing)) dishPartAdapter.notifyItemChanged(list.indexOf(existing))
} else { } else {
list.add(item.also { list.add(entity.also {
it.isNewDishType = true it.isNewDishType = true
it.isSetFinished = true it.isSetFinished = true
}) })
@@ -363,14 +367,14 @@ class PrepareCookActivity : BaseActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
isRecognizeEnabled = true pageVisible = true
isRecognizeOpen = false showRecognizePage = false
cameraUtils.bind() cameraUtils.bind()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
isRecognizeEnabled = false pageVisible = false
cameraUtils.unbind() cameraUtils.unbind()
} }
@@ -473,10 +477,19 @@ class PrepareCookActivity : BaseActivity() {
//调用接口成功,返回食材列表设置isTakingPhoto.set(false),暂时写死数据 //调用接口成功,返回食材列表设置isTakingPhoto.set(false),暂时写死数据
val foodList = mutableListOf<CookFoodGoodsEntity>() val foodList = mutableListOf<CookFoodGoodsEntity>()
list.forEachIndexed { index, food -> list.forEachIndexed { index, food ->
val foodName = food.name.split("WP").first() var foodName = ""
var foodId = ""
food.name.split("WP").let {
foodName = it[0]
foodId = if (it.size > 1) {
"WP${it[1]}"
} else {
food.name
}
}
foodList.add( foodList.add(
CookFoodGoodsEntity( CookFoodGoodsEntity(
goodsId = "${food.id}", goodsId = foodId,
goodsName = foodName, goodsName = foodName,
foodId = index.toString() foodId = index.toString()
).also { ).also {
@@ -489,9 +502,18 @@ class PrepareCookActivity : BaseActivity() {
activity = this, activity = this,
imageUri = lastPhotoUri?.toString(), imageUri = lastPhotoUri?.toString(),
goodsList = ArrayList(foodList), goodsList = ArrayList(foodList),
onResult = { item -> item?.let { foodSelectCallback(it) } } finishCallback = { isManualCancel, entity ->
Log.d(TAG, "queryFood: isManualCancel=$isManualCancel, entity=${entity.toJsonString()}")
manualCancelFlag = isManualCancel
if (isManualCancel) {
return@start
}
entity?.let {
foodSelectCallback(it)
}
}
) )
isRecognizeOpen = true showRecognizePage = true
isTakingPhoto.set(false) isTakingPhoto.set(false)
} }
@@ -1,142 +0,0 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityScale22GridBinding
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/** 22格设备的 deviceId */
private const val DEVICE_ID_22 = "a46fa55c-113c-3511-bb1f-41e5eff77c4b"
/**
* 列优先顺序:大列1(20,22) → 大列2(19,21) → 小列1~6
* FlexboxLayoutManager COLUMN 方向按列填充,顺序天然对应
*/
private val SCALE_ORDER_22 = listOf(20, 22, 19, 21, 6, 12, 18, 5, 11, 17, 4, 10, 16, 3, 9, 15, 2, 8, 14, 1, 7, 13)
/** 前4个 item(大列1+大列2,各2行)为大格子 */
private fun isLargeCell(position: Int) = position < 4
/**
* 22格秤混合网格布局页
* FlexboxLayoutManager(COLUMN) 竖向排列:
* - 大格子 flexBasisPercent=1.5/9,每列2个,自动换列
* - 小格子 flexBasisPercent=1.0/9,每列3个,自动换列
*/
class Scale22GridActivity : BaseActivity() {
private lateinit var binding: ActivityScale22GridBinding
private val scaleList = mutableListOf<ScaleData?>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityScale22GridBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
binding.ivBack.setOnClickListener { finish() }
// 格子尺寸计算:
// 屏幕宽 - 8列×左右margin(2dp×2) = 可用宽,共9份(大1.5+小1.0×6)
val screenWidth = resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - 4.dp // 减去8列格子margin + RecyclerView左右margin各2dp
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2 // 剩余宽度平分给2列大格子
// 高度以小格子3行为准(3行×格子高 + 3行×上下margin各2dp
val rvHeight = smallSize * 3 + 3 * 4.dp
binding.rvScale22.layoutParams = binding.rvScale22.layoutParams.also {
it.height = rvHeight
}
binding.rvScale22.layoutManager = FlexboxLayoutManager(this).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.FLEX_START
}
binding.rvScale22.itemAnimator = null
binding.rvScale22.adapter = Scale22Adapter(scaleList, largeSize, smallSize)
observeScaleData()
}
private fun observeScaleData() {
val flow = ScaleServiceManager.allScales ?: return
lifecycleScope.launch {
flow.collectLatest { scaleMap ->
val scaleByAddress = scaleMap.values
.filter { it.deviceId == DEVICE_ID_22 }
.associateBy { it.address }
val newList = SCALE_ORDER_22.map { scaleByAddress[it] }
if (scaleList.isEmpty()) {
scaleList.addAll(newList)
binding.rvScale22.adapter?.notifyItemRangeInserted(0, scaleList.size)
} else {
newList.forEachIndexed { i, scale ->
if (scaleList[i] != scale) {
scaleList[i] = scale
binding.rvScale22.adapter?.notifyItemChanged(i)
}
}
}
}
}
}
private inner class Scale22Adapter(
private val data: List<ScaleData?>,
private val largeSize: Int, // 大格子宽高(px
private val smallSize: Int // 小格子宽高(px
) : RecyclerView.Adapter<Scale22Adapter.VH>() {
inner class VH(val b: ListItemScale22RowBinding) : RecyclerView.ViewHolder(b.root)
override fun getItemViewType(position: Int) = if (isLargeCell(position)) 1 else 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val cellSize = if (viewType == 1) largeSize else smallSize
// 固定宽高实现正方形,FlexboxLayoutManager COLUMN 方向按高度装满后自动换列
(b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply {
width = cellSize
height = cellSize
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
return VH(b)
}
override fun getItemCount() = data.size
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int) {
val scale = data[position]
holder.b.tvAddr.text = scale?.let { "${it.address}" } ?: ""
holder.b.tvWeight.text = scale?.let { "${it.weight}g" } ?: ""
holder.b.tvState.text = when (scale?.state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
null -> ""
else -> "${scale.state}"
}
}
}
}
@@ -9,23 +9,29 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.lifecycle.lifecycleScope
import com.google.android.flexbox.AlignItems import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.adapter.Scale18GridAdapter import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
import com.shuwei.dish.match.adapter.Scale22GridAdapter import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
import com.shuwei.dish.match.adapter.ScaleRowAdapter import com.shuwei.dish.match.adapter.ScaleRowAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivitySlaveBinding import com.shuwei.dish.match.databinding.ActivitySlaveBinding
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.SeasoningSlotEntity
import com.shuwei.dish.match.scale.ScaleData import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** /**
* 子设备专属页面(小屏幕设备使用) * 子设备专属页面(小屏幕设备使用)
@@ -40,15 +46,17 @@ class SlaveActivity : BaseActivity() {
private val linearAdapter = ScaleRowAdapter() private val linearAdapter = ScaleRowAdapter()
/** 22个秤网格 adapter */ /** 22个秤网格 adapter */
private var scale22Adapter: Scale22GridAdapter? = null private var scale22Adapter: Seasoning22GridAdapter? = null
/** 18个秤网格 adapter */ /** 18个秤网格 adapter */
private var scale18Adapter: Scale18GridAdapter? = null private var scale18Adapter: Seasoning18GridAdapter? = null
/** 已连接的主设备数量(有连接即代表主设备在线) */ /** 已连接的主设备数量(有连接即代表主设备在线) */
private var masterConnected = false private var masterConnected = false
/** 各秤地址的上一次重量快照,用于计算 delta */ /** 各秤地址的上一次重量快照,用于计算 delta */
private val weightSnapshot = mutableMapOf<Int, Double>() private val weightSnapshot = mutableMapOf<Int, Double>()
/** key = addressvalue = 调料名称,用于注入 ScaleData.name,避免重量更新时覆盖名称 */
private val slotNameMap = mutableMapOf<Int, String>()
/** 主线程 Handler,用于延迟清除高亮 */ /** 主线程 Handler,用于延迟清除高亮 */
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
/** 当前高亮清除任务,新高亮触发时取消旧任务 */ /** 当前高亮清除任务,新高亮触发时取消旧任务 */
@@ -65,8 +73,10 @@ class SlaveActivity : BaseActivity() {
setupScaleList() setupScaleList()
updateMasterStatus(ScaleServiceManager.isMasterConnected) updateMasterStatus(ScaleServiceManager.isMasterConnected)
loadSlotsFromDb()
listenLocalScales() listenLocalScales()
listenMasterConnection() listenMasterConnection()
listenSeasoningConfig()
addBackKeyListener() addBackKeyListener()
} }
@@ -95,8 +105,8 @@ class SlaveActivity : BaseActivity() {
flexWrap = FlexWrap.WRAP flexWrap = FlexWrap.WRAP
alignItems = AlignItems.FLEX_START alignItems = AlignItems.FLEX_START
} }
scale22Adapter = Scale22GridAdapter(largeSize, smallSize).also { scale22Adapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false).also {
it.onItemClick = { scale -> showTareDialog(scale.address) } it.onItemClick = { scale, _ -> showTareDialog(scale.address) }
} }
binding.rvScaleList.adapter = scale22Adapter binding.rvScaleList.adapter = scale22Adapter
} }
@@ -107,8 +117,8 @@ class SlaveActivity : BaseActivity() {
override fun getSpanSize(position: Int) = 1 override fun getSpanSize(position: Int) = 1
} }
} }
scale18Adapter = Scale18GridAdapter().also { scale18Adapter = Seasoning18GridAdapter(showNameOnly = false).also {
it.onItemClick = { scale -> showTareDialog(scale.address) } it.onItemClick = { scale, _ -> showTareDialog(scale.address) }
} }
binding.rvScaleList.adapter = scale18Adapter binding.rvScaleList.adapter = scale18Adapter
} }
@@ -140,6 +150,64 @@ class SlaveActivity : BaseActivity() {
} }
} }
/**
* 从本地 Room 加载本机设备的槽位配置并更新 adapter
* 主设备未开机时也能正常展示上次配置
*/
private fun loadSlotsFromDb() {
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
val slots = dao.queryByDeviceId(GlobalData.deviceId)
withContext(Dispatchers.Main) { applySlots(slots) }
}
}
/**
* 监听主设备下发的调料配置同步事件
* 收到后更新 adapter 并写入本地 Room
*/
private fun listenSeasoningConfig() {
ScaleServiceManager.onSeasoningConfig = onSeasoningConfig@{ event ->
val mySlots = event.slots
?.filter { it.deviceId == GlobalData.deviceId }
?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) }
?: return@onSeasoningConfig
lifecycleScope.launch(Dispatchers.IO) {
BaseApp.instance!!.database.seasoningSlotDao().upsertAll(mySlots)
withContext(Dispatchers.Main) { applySlots(mySlots) }
}
}
}
/**
* 将槽位配置应用到对应 adapter(按地址匹配格子位置)
* 同时更新 slotNameMap,确保后续重量回调构造 ScaleData 时携带正确名称
*/
private fun applySlots(slots: List<SeasoningSlotEntity>) {
slots.forEach { slot ->
slotNameMap[slot.address] = slot.goodsName
when {
scale22Adapter != null -> {
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
if (pos >= 0) scale22Adapter!!.updateItemName(pos, slot.goodsName)
}
scale18Adapter != null -> {
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
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) {
list[idx] = list[idx].copy(name = slot.goodsName)
linearAdapter.submitList(list)
}
}
}
}
}
/** /**
* 监听本机 WeightUtil 回调,实时刷新秤列表 * 监听本机 WeightUtil 回调,实时刷新秤列表
* 22/18个秤走网格 adapter,其余走线性列表 * 22/18个秤走网格 adapter,其余走线性列表
@@ -162,19 +230,19 @@ class SlaveActivity : BaseActivity() {
// 用最新数据更新缓存后整体刷新网格 // 用最新数据更新缓存后整体刷新网格
val existing = scale22Adapter!!.items.toMutableList() val existing = scale22Adapter!!.items.toMutableList()
val idx = existing.indexOfFirst { it.address == address } val idx = existing.indexOfFirst { it.address == address }
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis()) val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis(), name = slotNameMap[address])
if (idx >= 0) existing[idx] = newData else existing.add(newData) if (idx >= 0) existing[idx] = newData else existing.add(newData)
scale22Adapter!!.updateByAddress(existing) scale22Adapter!!.updateByAddress(existing)
} }
scale18Adapter != null -> { scale18Adapter != null -> {
val existing = scale18Adapter!!.items.toMutableList() val existing = scale18Adapter!!.items.toMutableList()
val idx = existing.indexOfFirst { it.address == address } val idx = existing.indexOfFirst { it.address == address }
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis()) val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis(), name = slotNameMap[address])
if (idx >= 0) existing[idx] = newData else existing.add(newData) if (idx >= 0) existing[idx] = newData else existing.add(newData)
scale18Adapter!!.updateByAddress(existing) scale18Adapter!!.updateByAddress(existing)
} }
else -> { else -> {
val item = ScaleRowAdapter.ScaleItem(address, weight, state) val item = ScaleRowAdapter.ScaleItem(address, weight, state, slotNameMap[address] ?: "")
val list = linearAdapter.items.toMutableList() val list = linearAdapter.items.toMutableList()
val idx = list.indexOfFirst { it.address == address } val idx = list.indexOfFirst { it.address == address }
if (idx >= 0) { if (idx >= 0) {
@@ -217,6 +285,7 @@ class SlaveActivity : BaseActivity() {
override fun onDestroy() { override fun onDestroy() {
ScaleServiceManager.onMasterConnectionChanged = null ScaleServiceManager.onMasterConnectionChanged = null
ScaleServiceManager.onSeasoningConfig = null
WeightUtil.removeWeightListener(TAG) WeightUtil.removeWeightListener(TAG)
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) } clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
super.onDestroy() super.onDestroy()
@@ -3,25 +3,22 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.util.SparseArray
import android.util.SparseIntArray
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.core.util.forEach
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.TextCellAdapter import com.shuwei.dish.match.adapter.SeasoningWeightAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySubmitFoodBinding import com.shuwei.dish.match.databinding.ActivitySubmitFoodBinding
import com.shuwei.dish.match.db.AppRepository import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.net.NetViewModel import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.AddressUtil import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
@@ -34,7 +31,10 @@ import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory import com.shuwei.dish.match.viewmodel.factory.AppFactory
import kotlin.getValue import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@SuppressLint("UseSparseArrays") @SuppressLint("UseSparseArrays")
class SubmitFoodActivity : BaseActivity() { class SubmitFoodActivity : BaseActivity() {
@@ -50,8 +50,6 @@ class SubmitFoodActivity : BaseActivity() {
private var food: FoodRecord? = null private var food: FoodRecord? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null private var goodsList: MutableList<CookFoodGoodsEntity>? = null
private var isCooking = false
private val cookFoodEntity by lazy { private val cookFoodEntity by lazy {
CookFoodEntity().apply { CookFoodEntity().apply {
food?.let { food?.let {
@@ -64,6 +62,27 @@ class SubmitFoodActivity : BaseActivity() {
} }
} }
/** 调料列表 adapter */
private val seasoningAdapter = SeasoningWeightAdapter()
/**
* 各秤的初始重量快照,key = "deviceId#address"
* 首次收到稳定数据时记录,之后用于计算用量差值
*/
private val baselineMap = mutableMapOf<String, Double>()
/**
* 槽位配置:key = "deviceId#address"value = goodsId to goodsName
* 从 Room dm_seasoning_slot 加载
*/
private val slotMap = mutableMapOf<String, Pair<String, String>>()
/**
* 各秤原始用量,key = "deviceId#address"value = Triple(goodsId, goodsName, useWeight)
* 每次秤数据变化后更新,再按 goodsName 聚合推给 adapter
*/
private val rawWeightMap = mutableMapOf<String, Triple<String, String, Double>>()
@Suppress("unchecked_cast", "DEPRECATION") @Suppress("unchecked_cast", "DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -74,14 +93,8 @@ class SubmitFoodActivity : BaseActivity() {
intent.extras?.apply { intent.extras?.apply {
food = getSerializable(FOOD_ITEM) as FoodRecord? food = getSerializable(FOOD_ITEM) as FoodRecord?
Log.d(TAG, "onCreate: cookMode=${food?.cookMode}") Log.d(TAG, "onCreate: cookMode=${food?.cookMode}")
isCooking = food?.isCooking ?: false // 非烹饪中,从菜品主辅材配置页面带入数据
if (isCooking) { goodsList = getSerializable(GOODS_LIST) as MutableList<CookFoodGoodsEntity>?
//烹饪中,从本地查询数据
loadSeasoningFromLocal()
} else {
//非烹饪中,从菜品主辅材配置页面带入数据
goodsList = getSerializable(GOODS_LIST) as MutableList<CookFoodGoodsEntity>?
}
} }
setTitleBar(titleBarAction = { setTitleBar(titleBarAction = {
it.visible() it.visible()
@@ -90,80 +103,103 @@ class SubmitFoodActivity : BaseActivity() {
}, rightIconActon = { }, rightIconActon = {
it.gone() it.gone()
it.setImageResource(R.drawable.ic_setting) it.setImageResource(R.drawable.ic_setting)
it.setOnClickListener {
//startActivity<DeviceSettingActivity>()
}
}) })
setupRecyclerView()
addViewListener() addViewListener()
loadSlotsThenObserveScales()
} }
private fun loadSeasoningFromLocal() { /** 初始化 RecyclerView2列 GridLayoutManager */
appViewModel.getCookFoodGoodsList( private fun setupRecyclerView() {
foodId = food!!.foodId!!, binding.rvSeasoning.itemAnimator = null
cookMode = food!!.cookMode binding.rvSeasoning.layoutManager = GridLayoutManager(this, 2)
) { it -> binding.rvSeasoning.adapter = seasoningAdapter
goodsList = mutableListOf() }
val goodsIdList = mutableListOf<String>()
//设置主辅材数据
val tempList = it.filter { goods -> goods.materialType != 3 }
tempList.forEach {
if (!goodsIdList.contains(it.goodsId)) {
goodsList?.add(it)
goodsIdList.add(it.goodsId)
}
}
//获取调料数据并展示 /**
val filterList: List<SeasoningEntity>? = it.filter { goods -> goods.materialType == 3 } * 从 Room 加载槽位配置后,开始订阅子设备秤数据
.map { entity -> * 确保 slotMap 就绪后再处理秤数据,避免名称为空
SeasoningEntity().also { se -> */
se.goodsId = entity.goodsId private fun loadSlotsThenObserveScales() {
se.goodsName = entity.goodsName lifecycleScope.launch(Dispatchers.IO) {
se.goodsOrRelationCode = entity.goodsOrRelationCode val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll()
se.relateionType = entity.relateionType withContext(Dispatchers.Main) {
se.materialType = entity.materialType slots.forEach { slot ->
se.allEdible = entity.allEdible slotMap["${slot.deviceId}#${slot.address}"] = slot.goodsId to slot.goodsName
se.useWeight = entity.useWeight
se.popularName = entity.popularName
se.canteenId = entity.canteenId
se.relateionType_dictText = entity.relateionType_dictText
se.foodId = entity.foodId
se.sort = entity.sort
se.pageType = 0
}
} }
filterList?.toMutableList()?.let { cellData -> observeScaleData()
loadTextCell(cellData)
} }
} }
} }
/**
* 订阅 ScaleServiceManager.allScales,过滤子设备数据并更新 adapter
* 排除主设备(DEVICE_ID_2),只处理 22格、18格、1格子设备
*/
private fun observeScaleData() {
val flow = ScaleServiceManager.allScales ?: return
lifecycleScope.launch {
flow.collectLatest { scaleMap ->
scaleMap.forEach { (key, data) ->
// 排除主设备 2格秤
if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_2) return@forEach
// private lateinit var seasoningList: List<SeasoningEntity> // 从槽位配置读取调料名,未配置则跳过
// private var isFirstSave = true val (goodsId, goodsName) = slotMap[key] ?: return@forEach
// fun updateSeasoningList(list: MutableList<SeasoningEntity>) {
// if (isFirstSave) {
// val json = list.toJsonString()
// val typeToken = object : TypeToken<List<SeasoningEntity>>() {}
// seasoningList = json.toType(typeToken = typeToken)
// isFirstSave = false
// }
// }
private val seasoningArray by lazy { // 首次收到稳定数据时记录基准重量
SparseArray<Double>() val baseline = baselineMap[key]
if (baseline == null) {
if (data.state == WeightUtil.STATE_STABLE) {
baselineMap[key] = data.weight
}
return@forEach
}
// 用量 = 初始重量 - 当前重量,负值归零
val useWeight = (baseline - data.weight)
.coerceAtLeast(0.0)
.roundedOneDecimalPlace()
// 更新原始用量 map,再按 goodsName 聚合刷新 adapter
if (useWeight < 0.5) {
rawWeightMap.remove(key)
} else {
rawWeightMap[key] = Triple(goodsId, goodsName, useWeight)
}
refreshAdapterByName(goodsName)
}
}
}
} }
/**
private val firstGoodsArray = SparseArray<Double>() * 按 goodsName 聚合 rawWeightMap,将同名调料的用量累加后推给 adapter
* 聚合后总量 < 0.5g 则从 adapter 移除
*/
private fun refreshAdapterByName(goodsName: String) {
val grouped = rawWeightMap.values.filter { it.second == goodsName }
if (grouped.isEmpty()) {
seasoningAdapter.removeItem(goodsName)
return
}
val totalWeight = grouped.sumOf { it.third }.roundedOneDecimalPlace()
if (totalWeight < 0.5) {
seasoningAdapter.removeItem(goodsName)
} else {
seasoningAdapter.updateItem(
SeasoningWeightAdapter.Item(
goodsId = grouped.first().first,
goodsName = goodsName,
useWeight = totalWeight
)
)
}
}
private fun addViewListener() { private fun addViewListener() {
addWeightListener()
binding.btnWeightClear.setOnClickListener { WeightUtil.tareTwo(AddressUtil.TWO) }
binding.btnCook.clickWithDebounce { binding.btnCook.clickWithDebounce { cook() }
cook()
}
binding.btnSubmit.clickWithDebounce { binding.btnSubmit.clickWithDebounce {
if (cookFoodEntity.foodWeight <= 0.toDouble()) { if (cookFoodEntity.foodWeight <= 0.toDouble()) {
@@ -172,114 +208,40 @@ class SubmitFoodActivity : BaseActivity() {
} }
submit() submit()
} }
}
private fun addWeightListener() { // 监听主设备 2格秤的熟重
WeightUtil.addWeightListener( WeightUtil.addWeightListener(TAG) { address, _, weight ->
weightKey = TAG, if (address == AddressUtil.TWO) {
getWeight = { address, state, weight -> cookFoodEntity.foodWeight = weight.toDouble()
if (address == AddressUtil.TWO) { binding.tvTotalWeight.text = "${weight / 1000f}"
cookFoodEntity.foodWeight = weight.toDouble()
binding.tvTotalWeight.text = "${weight / 1000f}"
}
val firstWeight = firstGoodsArray.get(address, null)
if (firstWeight == null) {
//未设置数据
if (state != WeightUtil.STATE_STABLE) {
//首次记录数据需要稳定数据
return@addWeightListener
}
firstGoodsArray.put(address, weight)
return@addWeightListener
}
var realUseWeight = firstWeight - weight
realUseWeight = if (realUseWeight > 0) realUseWeight else 0.0
seasoningArray.put(address, realUseWeight.toDouble())
// refreshSeasoningWeight()
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
item?.let {
val lastWeight = seasoningArray.get(address) ?: 0.toDouble()
it.useWeight =
(lastWeight + getCookingSeasoning(address)).roundedOneDecimalPlace()
updateGridData(it)
}
})
}
// private fun refreshSeasoningWeight() {
// if (seasoningItems.isEmpty()) {
// return
// }
// seasoningItems.let { items ->
// addressArray.forEach { key, value ->
// items[key].useWeight =
// (seasoningArray.get(value) ?: 0.toDouble()) + getCookingSeasoning(value)
// }
// }
// setGridData(seasoningItems)
// }
private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
private fun getCookingSeasoning(address: Int): Double {
if (isCooking.not()) return 0.toDouble()
if (seasoningItems.isEmpty()) return 0.toDouble()
val weight =
seasoningCookingItems?.get(weightRelateArray2.get(address))?.useWeight ?: 0.toDouble()
return weight
}
private val weightRelateArray2 by lazy {
SparseIntArray().apply {
addressArray.forEach { key, value ->
put(value, key)
} }
} }
} }
private val addressArray by lazy {
AddressUtil.getWeighAddressArray()
}
// private fun prepareData(acton: () -> Unit) {
//
// //只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题
// val seasoningJson = realSeasoningData.toJsonString()
// val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
// val tempSeasoningList = seasoningJson.toType(typeToken = typeToken)
// goodsList?.addAll(tempSeasoningList)
// acton()
//
// }
private fun cook() { private fun cook() {
if (food == null || goodsList.isNullOrEmpty()) { if (food == null || goodsList.isNullOrEmpty()) {
toast("未获取到菜品或构成信息") toast("未获取到菜品或构成信息")
return return
} }
val realSeasoningData = seasoningItems val seasoningData = seasoningAdapter.items
//(binding.rvSeasoning.adapter as TextCellAdapter).list if (seasoningData.isEmpty()) {
if (realSeasoningData.isEmpty() && !isCooking) {
toast("未获取到调料信息") toast("未获取到调料信息")
return return
} }
if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) {
toast("还未设置调料信息,请去设置页面操作")
return
}
showLoading() showLoading()
//只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题
val seasoningJson = realSeasoningData.toJsonString() // 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList
val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {} val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
val tempSeasoningList = seasoningJson.toType(typeToken = typeToken) val tempSeasoningList = seasoningData.map { item ->
CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
}
}
goodsList?.addAll(tempSeasoningList) goodsList?.addAll(tempSeasoningList)
cookFoodEntity.let { cookFoodEntity.let {
it.dinnerType = when (it.dinnerType) { it.dinnerType = when (it.dinnerType) {
"早餐" -> "1" "早餐" -> "1"
@@ -288,17 +250,14 @@ class SubmitFoodActivity : BaseActivity() {
else -> it.dinnerType else -> it.dinnerType
} }
} }
val isSamplingData = food!!.cookMode == 1 val isSamplingData = food!!.cookMode == 1
if (isSamplingData) { if (isSamplingData) {
//生成临时foodId,解决采样模式没有foodId,无法关联数据的问题
val tempFoodId = cookFoodEntity.foodId.ifBlank { "${System.currentTimeMillis()}" } val tempFoodId = cookFoodEntity.foodId.ifBlank { "${System.currentTimeMillis()}" }
cookFoodEntity.foodId = tempFoodId cookFoodEntity.foodId = tempFoodId
} }
//全部设置同一foodId goodsList?.forEach { it.foodId = cookFoodEntity.foodId }
goodsList?.forEach {
it.foodId = cookFoodEntity.foodId
}
//保存菜品-保存主辅料调料
appViewModel.saveCookFoodAndGoods( appViewModel.saveCookFoodAndGoods(
cookMode = food!!.cookMode, cookMode = food!!.cookMode,
entity = cookFoodEntity, entity = cookFoodEntity,
@@ -316,31 +275,22 @@ class SubmitFoodActivity : BaseActivity() {
toast("未获取到菜品或构成信息") toast("未获取到菜品或构成信息")
return return
} }
val realSeasoningData = seasoningItems val seasoningData = seasoningAdapter.items
if (realSeasoningData.isEmpty() && !isCooking) {
toast("未获取到调料信息")
return
}
if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) {
toast("还未设置调料信息,请去设置页面操作")
return
}
showLoading() showLoading()
//只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题
val seasoningJson = realSeasoningData.toJsonString() // 将重量大于0且不重复的调料追加到 goodsList
val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {} seasoningData.forEach { item ->
val tempSeasoningList = seasoningJson.toType(typeToken = typeToken) if (item.useWeight > 0.0 && goodsList?.none { it.goodsId == item.goodsId } == true) {
tempSeasoningList.forEach { entity -> goodsList?.add(CookFoodGoodsEntity().also { entity ->
val realWeight = entity.useWeight ?: 0.toDouble() entity.goodsId = item.goodsId
val filterData = goodsList?.firstOrNull { it.goodsId == entity.goodsId } entity.goodsName = item.goodsName
//确保重量大于0,且无重复数据 entity.useWeight = item.useWeight
if (realWeight > 0.toDouble() && filterData == null) { entity.materialType = 3
goodsList?.add(entity) })
} }
} }
cookFoodEntity.let { cookFoodEntity.let {
// it.stFoodInfoConstituteList = this@SubmitFoodActivity.goodsList
it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList
it.dinnerType = when (it.dinnerType) { it.dinnerType = when (it.dinnerType) {
"1" -> "早餐" "1" -> "早餐"
@@ -352,30 +302,15 @@ class SubmitFoodActivity : BaseActivity() {
val isSamplingData = food!!.cookMode == 1 val isSamplingData = food!!.cookMode == 1
if (isSamplingData) { if (isSamplingData) {
//采样模式没有foodId
cookFoodEntity.foodId = "" cookFoodEntity.foodId = ""
goodsList?.forEach { it.foodId = "" } goodsList?.forEach { it.foodId = "" }
} }
val json = cookFoodEntity.toJsonString()
Log.d(TAG, "submit: json=$json")
// HttpUtil.postJson(
// url = UrlConfig.SUBMIT_DISH,
// json = json,
// doSuccess = {
// Log.d(TAG, "submit: postJson=$it")
// submitSuccess(isSamplingData)
// }, doFailure = { code, msg ->
// Log.d(TAG, "submit: postJson:code=$code,msg=$msg")
// toast(msg)
// dismissLoading()
// })
Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}")
netViewModel.submitCookFood( netViewModel.submitCookFood(
entity = cookFoodEntity, entity = cookFoodEntity,
onSuccess = { onSuccess = { submitSuccess(isSamplingData) },
submitSuccess(isSamplingData) onFailure = { _, msg ->
},
onFailure = { code, msg ->
toast(msg) toast(msg)
dismissLoading() dismissLoading()
}) })
@@ -383,12 +318,10 @@ class SubmitFoodActivity : BaseActivity() {
private fun submitSuccess(isSamplingData: Boolean) { private fun submitSuccess(isSamplingData: Boolean) {
if (food?.foodId.isNullOrBlank()) { if (food?.foodId.isNullOrBlank()) {
//本地无删除数据直接跳转页面
dismissLoading() dismissLoading()
jumpPage(isSamplingData, false) jumpPage(isSamplingData, false)
return return
} }
//删除本地数据
appViewModel.deleteCookFoodAndGoods( appViewModel.deleteCookFoodAndGoods(
cookMode = food!!.cookMode, cookMode = food!!.cookMode,
foodId = food!!.foodId!! foodId = food!!.foodId!!
@@ -415,57 +348,12 @@ class SubmitFoodActivity : BaseActivity() {
private fun initViewModel() { private fun initViewModel() {
val db = BaseApp.instance!!.database val db = BaseApp.instance!!.database
val factory = val factory = AppFactory(AppRepository(db.appDao()))
AppFactory(AppRepository(db.appDao())) appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
appViewModel =
ViewModelProvider(this, factory)[AppViewModel::class.java]
} }
override fun onResume() { override fun onDestroy() {
super.onResume() WeightUtil.removeWeightListener(TAG)
if (isCooking.not()) { super.onDestroy()
//烹饪中使用本地数据,未烹饪则使用默认的调料配置数据
appViewModel.loadSeasoning {
Log.d(TAG, "onResume: loadSeasoning:${it.toJsonString()}")
loadTextCell(it)
}
}
}
private var seasoningItems = mutableListOf<SeasoningEntity>()
private var seasoningCookingItems: List<SeasoningEntity>? = null
private fun loadTextCell(list: MutableList<SeasoningEntity>) {
seasoningItems.clear()
seasoningItems.addAll(list)
initConfigData()
// setGridData(list)
list.forEach { entity ->
//烹饪中重量数据从本地获取,否则显示0
if (isCooking.not()) {
entity.useWeight = 0.0
}
updateGridData(entity)
}
seasoningCookingItems = seasoningItems.map { it.copy() }.toList()
}
private fun initConfigData() {
repeat(12) { num ->
val firstOne = seasoningItems.firstOrNull { it.sort == num }
if (firstOne == null) {
seasoningItems.add(SeasoningEntity().also { it.sort = num })
}
}
}
private fun setGridData(list: MutableList<SeasoningEntity>) {
val gridLayout = binding.include.root
list.forEach { entity ->
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
} }
} }
@@ -13,18 +13,23 @@ import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.adapter.Seasoning18GridAdapter import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.BaseFragment 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.SeasoningSelectDialog import com.shuwei.dish.match.dialog.SeasoningSelectDialog
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
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.scale.ScaleEvent.SlotConfig
import com.shuwei.dish.match.ui.SettingActivity import com.shuwei.dish.match.ui.SettingActivity
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() { class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
@@ -45,7 +50,7 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
// private lateinit var appViewModel: AppViewModel // private lateinit var appViewModel: AppViewModel
private val adapter22 by lazy { buildAdapter22() } private val adapter22 by lazy { buildAdapter22() }
private val adapter18 by lazy { Seasoning18GridAdapter() } private val adapter18 by lazy { Seasoning18GridAdapter(showNameOnly = true) }
/** 各秤的上一次重量快照,key 格式 "{deviceId}#{address}",用于计算 delta */ /** 各秤的上一次重量快照,key 格式 "{deviceId}#{address}",用于计算 delta */
private val weightSnapshot = mutableMapOf<String, Double>() private val weightSnapshot = mutableMapOf<String, Double>()
@@ -59,32 +64,86 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
// initViewModel() // initViewModel()
setupRecyclerViews() setupRecyclerViews()
bindAdapterClicks() bindAdapterClicks()
bindOilSlotClick()
addWeightListener() addWeightListener()
loadData() loadData()
} }
/** 绑定油盆格子点击,弹出调料选择弹窗并保存到 DEVICE_ID_1 */
private fun bindOilSlotClick() {
binding.tvOilSlot.setOnClickListener {
SeasoningSelectDialog(
activity = settingActivity,
clickIndex = 0,
onItemSelected = { item ->
binding.tvOilSlot.text = item.goodsName ?: "-"
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_1,
address = 1,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
)
}
).show()
}
}
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */ /** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
private fun bindAdapterClicks() { private fun bindAdapterClicks() {
adapter22.onItemClick = { _, position -> adapter22.onItemClick = { scaleData, position ->
SeasoningSelectDialog( SeasoningSelectDialog(
activity = settingActivity, activity = settingActivity,
clickIndex = position, clickIndex = position,
onItemSelected = { item -> onItemSelected = { item ->
adapter22.updateItemName(position, item.goodsName ?: "") adapter22.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_22,
address = scaleData.address,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
)
} }
).show() ).show()
} }
adapter18.onItemClick = { _, position -> adapter18.onItemClick = { scaleData, position ->
SeasoningSelectDialog( SeasoningSelectDialog(
activity = settingActivity, activity = settingActivity,
clickIndex = position, clickIndex = position,
onItemSelected = { item -> onItemSelected = { item ->
adapter18.updateItemName(position, item.goodsName ?: "") adapter18.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_18,
address = scaleData.address,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
)
} }
).show() ).show()
} }
} }
/**
* 将单个槽位写入 Room,再读取全量配置广播给子设备
* 仅主设备执行广播,子设备不调用此方法
*/
private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) {
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
dao.upsert(slot)
// 读取全量配置,通过 wsClient 推送给所有已连接子设备
val allSlots = dao.queryAll().map {
SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName)
}
ScaleServiceManager.sendSeasoningConfig(allSlots)
}
}
// /** 初始化 ViewModel */ // /** 初始化 ViewModel */
// private fun initViewModel() { // private fun initViewModel() {
// val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao())) // val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao()))
@@ -96,7 +155,7 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
val usable = resources.displayMetrics.widthPixels - 8 * 4.dp - 10.dp val usable = resources.displayMetrics.widthPixels - 8 * 4.dp - 10.dp
val smallSize = usable / 9 val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2 val largeSize = (usable - smallSize * 6) / 2
return Seasoning22GridAdapter(largeSize, smallSize) return Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = true)
} }
/** 配置两个 RecyclerView */ /** 配置两个 RecyclerView */
@@ -126,14 +185,29 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
} }
} }
/** 加载默认空数据,使两个列表完整展示所有格子 */ /** 从 Room 加载已配置的槽位名称,填充到对应格子 */
private fun loadData() { private fun loadData() {
adapter22.update(emptyList()) adapter22.update(emptyList())
adapter18.update(emptyList()) adapter18.update(emptyList())
// appViewModel.loadSeasoning { list -> lifecycleScope.launch(Dispatchers.IO) {
// adapter22.update(list ?: emptyList()) val dao = BaseApp.instance!!.database.seasoningSlotDao()
// adapter18.update(list ?: emptyList()) val slots22 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_22)
// } val slots18 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_18)
val slotsOil = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_1)
withContext(Dispatchers.Main) {
slots22.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
if (pos >= 0) adapter22.updateItemName(pos, slot.goodsName)
}
slots18.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
if (pos >= 0) adapter18.updateItemName(pos, slot.goodsName)
}
// 加载油盆格子名称(address=1
val oilSlot = slotsOil.firstOrNull { it.address == 1 }
binding.tvOilSlot.text = if (oilSlot != null && oilSlot.goodsName.isNotEmpty()) oilSlot.goodsName else "-"
}
}
} }
/** /**
@@ -222,10 +296,10 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
* @param delta 重量变化量(克) * @param delta 重量变化量(克)
*/ */
private fun triggerHighlight(sourceDeviceId: String, address: Int, delta: Double) { private fun triggerHighlight(sourceDeviceId: String, address: Int, delta: Double) {
// 根据来源设备决定查哪个列表,而非查本机设备ID // 只有对应设备的秤才查对应列表,避免主设备(DEVICE_ID_2)等其他设备误触发
val pos22 = if (sourceDeviceId != ScaleDeviceConfig.DEVICE_ID_18) val pos22 = if (sourceDeviceId == ScaleDeviceConfig.DEVICE_ID_22)
ScaleDeviceConfig.SCALE_ORDER_22.indexOf(address) else -1 ScaleDeviceConfig.SCALE_ORDER_22.indexOf(address) else -1
val pos18 = if (sourceDeviceId != ScaleDeviceConfig.DEVICE_ID_22) val pos18 = if (sourceDeviceId == ScaleDeviceConfig.DEVICE_ID_18)
ScaleDeviceConfig.SCALE_ORDER_18.indexOf(address) else -1 ScaleDeviceConfig.SCALE_ORDER_18.indexOf(address) else -1
Log.d(TAG, "triggerHighlight: sourceDeviceId=$sourceDeviceId address=$address pos22=$pos22 pos18=$pos18") Log.d(TAG, "triggerHighlight: sourceDeviceId=$sourceDeviceId address=$address pos22=$pos22 pos18=$pos18")
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="@color/bg_color">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="22格秤数据"
android:textColor="@color/home_title"
android:textSize="36sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivBack"
android:layout_width="70dp"
android:layout_height="60dp"
android:paddingStart="30dp"
android:paddingEnd="10dp"
android:src="@drawable/ic_back"
app:layout_constraintBottom_toBottomOf="@id/tvTitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvTitle"
tools:ignore="ContentDescription" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvScale22"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:overScrollMode="never"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle"
tools:listitem="@layout/list_item_scale22_row" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -27,11 +27,12 @@
android:textColor="@color/black666" android:textColor="@color/black666"
android:textSize="28sp" /> android:textSize="28sp" />
<include <androidx.recyclerview.widget.RecyclerView
android:id="@+id/include" android:id="@+id/rvSeasoning"
layout="@layout/layout_grid"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="660dp"
android:layout_marginTop="10dp"
android:overScrollMode="never" />
</LinearLayout> </LinearLayout>
@@ -2,41 +2,100 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:background="@color/bg_color"
android:background="@color/bg_color"> android:orientation="vertical">
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="上层" android:layout_marginStart="0dp"
android:textSize="18sp" android:layout_marginEnd="0dp"
android:textStyle="bold" android:paddingBottom="20dp"
android:textColor="@color/home_title"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginBottom="6dp" /> android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView <FrameLayout
android:id="@+id/rvTop" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="106dp"
android:layout_height="wrap_content" android:layout_marginStart="6dp"
android:overScrollMode="never" /> android:layout_marginEnd="6dp"
android:background="@drawable/bg_bottom_line">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="下层" android:layout_gravity="start|center_vertical"
android:textSize="18sp" android:text="调料区设置"
android:textStyle="bold" android:textColor="@color/black"
android:textColor="@color/home_title" android:textSize="28sp"
android:layout_marginStart="10dp" android:textStyle="bold" />
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp" />
<androidx.recyclerview.widget.RecyclerView <TextView
android:id="@+id/rvBottom" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_gravity="end|center_vertical"
android:overScrollMode="never" /> android:text="点击分格配置调料"
android:textColor="@color/gray_b4"
android:textSize="28sp" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="上层调料区"
android:textColor="@color/home_title"
android:textSize="28sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="下层调料区"
android:textColor="@color/home_title"
android:textSize="28sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvBottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="油盆配置区"
android:textColor="@color/home_title"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvOilSlot"
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_marginHorizontal="10dp"
android:layout_marginBottom="10dp"
android:background="@drawable/shape_scale_cell"
android:gravity="center"
android:text="-"
android:textColor="@color/home_title"
android:textSize="28sp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/tvSeasoningName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black666"
android:textSize="24sp" />
<TextView
android:id="@+id/tvSeasoningWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="@color/black"
android:textSize="24sp" />
</LinearLayout>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 单个秤格子:地址 + 重量 + 状态 --> <!-- 单个秤格子:地址 + 调料名 + 重量 + 状态 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -9,7 +9,16 @@
android:background="@drawable/shape_scale_cell"> android:background="@drawable/shape_scale_cell">
<TextView <TextView
android:id="@+id/tvAddr" android:id="@+id/tvAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@color/home_sub_title"
android:visibility="gone" />
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
+21 -17
View File
@@ -1,48 +1,52 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!-- 单行秤数据:秤编号 + 重量 + 状态 -->
<!-- 单行秤数据:秤编号 + 重量 + 状态 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="120dp" android:layout_height="340dp"
android:orientation="horizontal" android:background="@drawable/shape_scale_cell"
android:gravity="center_vertical" android:gravity="center"
android:orientation="vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp" android:paddingEnd="16dp"
android:background="@drawable/shape_scale_cell"> android:paddingBottom="8dp">
<!-- 秤编号 --> <!-- 秤编号 -->
<TextView <TextView
android:id="@+id/tvScaleLabel" android:id="@+id/tvScaleLabel"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="22sp"
android:textColor="@color/home_sub_title" android:textColor="@color/home_sub_title"
android:textSize="18sp"
tools:text="秤 1" /> tools:text="秤 1" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/home_sub_title"
android:textSize="22sp"
android:text="-" />
<!-- 重量 --> <!-- 重量 -->
<TextView <TextView
android:id="@+id/tvWeight" android:id="@+id/tvWeight"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="center" android:gravity="center"
android:textColor="@color/home_title"
android:textSize="26sp" android:textSize="26sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/home_title"
tools:text="235.5 g" /> tools:text="235.5 g" />
<!-- 状态 --> <!-- 状态 -->
<TextView <TextView
android:id="@+id/tvState" android:id="@+id/tvState"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end" android:gravity="end"
android:textSize="18sp"
android:textColor="@color/home_sub_title" android:textColor="@color/home_sub_title"
android:textSize="18sp"
tools:text="稳定" /> tools:text="稳定" />
</LinearLayout> </LinearLayout>
+25 -16
View File
@@ -29,48 +29,57 @@
<LinearLayout <LinearLayout
android:id="@+id/llScaleContainer" android:id="@+id/llScaleContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="120dp" android:layout_height="260dp"
android:orientation="horizontal" android:orientation="vertical"
android:gravity="center_vertical" android:gravity="center"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:paddingStart="30dp"
android:background="@drawable/shape_scale_cell"> android:background="@drawable/shape_scale_cell">
<TextView <TextView
android:id="@+id/tvAddr" android:id="@+id/tvAddr"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:textSize="18sp"
android:textSize="22sp" android:includeFontPadding="false"
android:textColor="@color/home_sub_title" android:textColor="@color/home_sub_title"
tools:text="秤 1" /> tools:text="秤 1" />
<TextView <TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:includeFontPadding="false"
android:textColor="@color/home_sub_title"
android:text="-" />
<TextView
android:id="@+id/tvWeight" android:id="@+id/tvWeight"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="26sp" android:textSize="26sp"
android:textStyle="bold" android:textStyle="bold"
android:includeFontPadding="false"
android:layout_marginVertical="5dp"
android:textColor="@color/home_title" android:textColor="@color/home_title"
tools:text="100.0 g" /> tools:text="100.0 g" />
<TextView <TextView
android:id="@+id/tvState" android:id="@+id/tvState"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="18sp" android:textSize="18sp"
android:includeFontPadding="false"
android:textColor="@color/home_sub_title" android:textColor="@color/home_sub_title"
tools:text="稳定" /> tools:text="稳定" />
</LinearLayout> </LinearLayout>
<com.google.android.material.divider.MaterialDivider <!-- <com.google.android.material.divider.MaterialDivider-->
android:layout_width="match_parent" <!-- android:layout_width="match_parent"-->
android:layout_height="1dp" <!-- android:layout_height="1dp"-->
android:background="@color/gray_eb" <!-- android:background="@color/gray_eb"-->
android:layout_marginTop="10dp" /> <!-- android:layout_marginTop="10dp" />-->
</LinearLayout> </LinearLayout>
@@ -32,10 +32,10 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:overScrollMode="never" /> android:overScrollMode="never" />
<com.google.android.material.divider.MaterialDivider <!-- <com.google.android.material.divider.MaterialDivider-->
android:layout_width="match_parent" <!-- android:layout_width="match_parent"-->
android:layout_height="1dp" <!-- android:layout_height="1dp"-->
android:background="@color/gray_eb" <!-- android:background="@color/gray_eb"-->
android:layout_marginTop="10dp" /> <!-- android:layout_marginTop="10dp" />-->
</LinearLayout> </LinearLayout>
+20 -9
View File
@@ -25,18 +25,29 @@
android:textSize="14sp" android:textSize="14sp"
tools:text="设备:device_01" /> tools:text="设备:device_01" />
<!-- 水平排列2个秤格子,高度固定 400dp --> <!-- 水平排列2个秤格子 -->
<LinearLayout <LinearLayout
android:id="@+id/llScaleContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="400dp" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginTop="5dp" /> android:layout_marginTop="5dp">
<com.google.android.material.divider.MaterialDivider <include
android:layout_width="match_parent" android:id="@+id/cell1"
android:layout_height="1dp" layout="@layout/list_item_scale22_row"
android:background="@color/gray_eb" android:layout_width="0dp"
android:layout_marginTop="10dp" /> android:layout_height="match_parent"
android:layout_weight="1"
android:layout_margin="2dp" />
<include
android:id="@+id/cell2"
layout="@layout/list_item_scale22_row"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_margin="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
@@ -32,10 +32,10 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:overScrollMode="never" /> android:overScrollMode="never" />
<com.google.android.material.divider.MaterialDivider <!-- <com.google.android.material.divider.MaterialDivider-->
android:layout_width="match_parent" <!-- android:layout_width="match_parent"-->
android:layout_height="1dp" <!-- android:layout_height="1dp"-->
android:background="@color/gray_eb" <!-- android:background="@color/gray_eb"-->
android:layout_marginTop="10dp" /> <!-- android:layout_marginTop="10dp" />-->
</LinearLayout> </LinearLayout>