9 Commits
Author SHA1 Message Date
lvmengandClaude Sonnet 4.6 75830b9204 refactor(network): 引入 RemoteRepository 隔离网络层,AppRepository 支持调料槽位查询
- 新增 RemoteRepository,将 NetViewModel 中直接调用 apiService 的逻辑下沉至仓库层
- NetViewModel 改为依赖注入 RemoteRepository,不再直接接触 apiService
- AppRepository 构造函数增加 SeasoningSlotDao 参数,新增 hasSeasoningSlotConfig() 方法
- AppViewModel 新增 hasSeasoningSlotConfig() 方法,明确区分"调料数据"与"槽位配置"语义
- 同步更新 6 个 Activity 的 AppFactory 构造调用,传入 seasoningSlotDao

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 18:31:38 +08:00
lvmeng 0498501b33 style(ui): 统一界面组件样式并优化设置页面交互
- 为所有列表项和设置面板应用统一的白色圆角背景样式
- 为可点击区域添加波纹效果提升用户体验
- 修复布局文件中ID命名不规范的问题
- 在设置页面添加右上角功能按钮并实现跳转逻辑
- 导入页面跳转扩展函数支持快速导航到主秤配置页面
- 优化布局参数确保界面元素正确显示和交互响应
2026-04-22 17:35:26 +08:00
lvmeng 8c709a3f5f fix(fragment): 修复 CollectFragment 首次打开相机预览不显示的问题 2026-04-22 17:09:33 +08:00
lvmeng 9ba6443f4f refactor(setting): 重构设置页面界面和功能结构
- 将底部导航菜单替换为三个独立的功能卡片布局
- 添加餐品模式、调料区设置和食材采样功能入口
- 移除原有的Fragment容器和标签页切换逻辑
- 更新CollectFragment以使用新的SingleFragmentActivity架构
- 修改相机权限检查逻辑并添加权限拒绝提示对话框
- 简化DeviceConfigFragment和SeasoningConfigFragment的依赖导入
- 在设置页面添加当前模式显示和跳转功能
- 重构调料配置页面的日志记录和高亮逻辑
2026-04-22 14:55:09 +08:00
lvmeng 04e4efe1fc feat(activity): 新增 SingleFragmentActivity 通用 Fragment 容器页 2026-04-22 13:39:52 +08:00
lvmengandClaude Sonnet 4.6 bce9675b0f feat(activity): SubmitFoodActivity 支持制作中菜品调料历史数据加载与新增叠加
- SeasoningWeightAdapter 改继承 BaseQuickAdapter
- item_seasoning_weight 左右间距改为 32dp
- isCooking=true 时从数据库查询 goodsList 并预填充调料历史数据
- refreshAdapterByName 叠加历史用量,修复新增用量覆盖历史数据问题
- 返回键拦截,弹窗提示保存数据

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 11:47:13 +08:00
lvmeng f25108360b feat(ui): CommonDialog 支持垂直3按钮布局(iOS风格) 2026-04-22 11:39:37 +08:00
lvmeng 4ec02ac2bc feat(MasterScaleActivity): 添加秤名称显示功能
- 在UI界面上增加tvName文本视图用于显示秤的名称
- 当秤名称不为空时显示实际名称,否则显示默认值"-"
- 优化了秤信息展示的完整性,补充了名称字段的显示逻辑
2026-04-22 09:34:04 +08:00
lvmeng 97b357850c refactor(layout): 优化布局尺寸计算逻辑并提取公共工具类
- 添加 SizeTool 工具类统一管理 Flexbox 布局尺寸计算
- 在 MasterScaleActivity 中使用 SizeTool 替代硬编码计算
- 在 SeasoningConfigFragment 中使用 SizeTool 重构尺寸计算
- 在 SlaveActivity 中应用统一的尺寸计算方法
- 更新 Seasoning18GridAdapter 和 Seasoning22GridAdapter 使用现代化布局参数设置
- 调整 fragment_seasoning_config.xml 中的边距和内边距数值
- 为布局文件添加 tools:itemCount 属性支持预览功能
2026-04-22 09:28:07 +08:00
38 changed files with 1124 additions and 493 deletions
+4
View File
@@ -106,6 +106,10 @@
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.SingleFragmentActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.FoodRecognizeActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
@@ -25,7 +26,8 @@ import com.shuwei.dish.match.utils.ext.dp
*/
class Seasoning18GridAdapter(
private val showNameOnly: Boolean = false,
private val showAddress: Boolean = false
private val showAddress: Boolean = false,
private val leftRightSpace: Int = 10.dp
) : BaseQuickAdapter<ScaleData, Seasoning18GridAdapter.VH>(mutableListOf()) {
/** 空位哨兵,address=0 表示无效 */
@@ -47,12 +49,12 @@ class Seasoning18GridAdapter(
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
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 as? ViewGroup.MarginLayoutParams)?.also {
it.width = cellSize
it.height = cellSize
if (viewType == 0) it.setMargins(margin, 0, margin, 2.dp)
else it.setMargins(margin, margin, margin, margin)
val cellSize = (screenWidth - 6 * margin * 2 - leftRightSpace) / 6
b.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
width = cellSize
height = cellSize
if (viewType == 0) setMargins(margin, 0, margin, 2.dp)
else setMargins(margin, margin, margin, margin)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddress.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
@@ -51,7 +52,7 @@ class Seasoning22GridAdapter(
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 {
b.root.updateLayoutParams<FlexboxLayoutManager.LayoutParams> {
width = cellSize
height = cellSize
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
@@ -1,8 +1,10 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ItemSeasoningWeightBinding
/**
@@ -10,7 +12,7 @@ import com.shuwei.dish.match.databinding.ItemSeasoningWeightBinding
* 用于 SubmitFoodActivity 展示各调料槽位的实时用量
* 同名调料已在外部聚合,adapter 按 goodsName 作为唯一标识
*/
class SeasoningWeightAdapter : RecyclerView.Adapter<SeasoningWeightAdapter.VH>() {
class SeasoningWeightAdapter : BaseQuickAdapter<SeasoningWeightAdapter.Item, SeasoningWeightAdapter.VH>() {
/** 调料展示数据项,key = goodsName(同名调料已聚合) */
data class Item(
@@ -20,19 +22,15 @@ class SeasoningWeightAdapter : RecyclerView.Adapter<SeasoningWeightAdapter.VH>()
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)
set(idx, item)
} else {
items.add(item)
notifyItemInserted(items.lastIndex)
add(item)
}
}
@@ -41,29 +39,20 @@ class SeasoningWeightAdapter : RecyclerView.Adapter<SeasoningWeightAdapter.VH>()
*/
fun removeItem(goodsName: String) {
val idx = items.indexOfFirst { it.goodsName == goodsName }
if (idx >= 0) {
items.removeAt(idx)
notifyItemRemoved(idx)
}
if (idx >= 0) removeAt(idx)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val binding = ItemSeasoningWeightBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ItemSeasoningWeightBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(items[position])
override fun onBindViewHolder(holder: VH, position: Int, item: Item?) {
item ?: return
holder.binding.tvSeasoningName.text = item.goodsName
holder.binding.tvSeasoningWeight.text = "${item.useWeight}g"
}
override fun getItemCount() = items.size
inner class VH(var binding: ItemSeasoningWeightBinding) : QuickViewHolder(binding.root)
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"
}
}
}
@@ -126,6 +126,9 @@ open class BaseActivity : AppCompatActivity() {
)
}
/** 返回 Fragment 容器的 View ID,供子类加载 Fragment 使用 */
protected fun getFragmentContainerId(): Int = R.id.flContainer
override fun setContentView(view: View?) {
if (view == binding.root) {
super.setContentView(view)
@@ -2,6 +2,7 @@ package com.shuwei.dish.match.db
import com.shuwei.dish.match.base.BaseApp
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.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.SeasoningEntity
@@ -9,7 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AppRepository(val appDao: AppDao) {
class AppRepository(val appDao: AppDao, val seasoningSlotDao: SeasoningSlotDao) {
suspend fun insertCookFood(item: CookFoodEntity) = withContext(Dispatchers.IO) {
appDao.insertCookFood(item)
@@ -105,6 +106,15 @@ class AppRepository(val appDao: AppDao) {
suspend fun insertSeasoning(item: SeasoningEntity) = withContext(Dispatchers.IO) {
appDao.insertSeasoning(item)
}
/**
* 查询指定设备是否已配置调料槽位
* @param deviceId 目标设备 ID
* @return true 表示已有配置,false 表示未配置
*/
suspend fun hasSeasoningSlotConfig(deviceId: String) = withContext(Dispatchers.IO) {
seasoningSlotDao.queryByDeviceId(deviceId).isNotEmpty()
}
}
//
//class SeasoningRepository(val seasoningDao: SeasoningDao) {
@@ -26,8 +26,10 @@ open class CommonDialog(
private var contentText: String? = null
private var negativeText = "取消"
private var positiveText = "确认"
private var neutralText: String? = null
private var negativeClick: (() -> Unit)? = null
private var positiveClick: (() -> Unit)? = null
private var neutralClick: (() -> Unit)? = null
private var dismissCallback: (() -> Unit)? = null
/** 设置标题,为空时隐藏 */
@@ -36,18 +38,24 @@ open class CommonDialog(
/** 设置内容,为空时隐藏 */
fun setContent(text: String): CommonDialog = apply { contentText = text }
/** 设置左侧取消按钮文字及点击回调 */
/** 设置左侧取消按钮文字及点击回调3按钮模式下对应顶部按钮) */
fun setNegativeButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply {
negativeText = text
negativeClick = onClick
}
/** 设置右侧确认按钮文字及点击回调 */
/** 设置右侧确认按钮文字及点击回调3按钮模式下对应底部按钮) */
fun setPositiveButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply {
positiveText = text
positiveClick = onClick
}
/** 设置中间第三个按钮文字及点击回调,调用后自动切换为垂直3按钮布局 */
fun setNeutralButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply {
neutralText = text
neutralClick = onClick
}
/** 设置弹窗消失回调 */
fun setOnDismissCallback(callback: () -> Unit): CommonDialog = apply {
dismissCallback = callback
@@ -70,6 +78,8 @@ open class CommonDialog(
val verticalMargin = if (GlobalData.deviceRole == DeviceRole.MASTER) 35.dp else 20.dp
binding.tvTitle.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = verticalMargin }
binding.tvContent.updateLayoutParams<LinearLayout.LayoutParams> { bottomMargin = verticalMargin }
// 2按钮点击事件
binding.btnLeft.setOnClickListener {
negativeClick?.invoke()
dismiss()
@@ -78,6 +88,21 @@ open class CommonDialog(
positiveClick?.invoke()
dismiss()
}
// 3按钮点击事件(negativeClick/positiveClick 与2按钮模式共用)
binding.btnTop.setOnClickListener {
negativeClick?.invoke()
dismiss()
}
binding.btnMiddle.setOnClickListener {
neutralClick?.invoke()
dismiss()
}
binding.btnBottom.setOnClickListener {
positiveClick?.invoke()
dismiss()
}
setOnDismissListener { dismissCallback?.invoke() }
}
@@ -87,10 +112,23 @@ open class CommonDialog(
binding.tvContent.text = contentText
binding.tvTitle.visibility = if (titleText.isNullOrEmpty()) View.GONE else View.VISIBLE
binding.tvContent.visibility = if (contentText.isNullOrEmpty()) View.GONE else View.VISIBLE
val hasThreeButtons = !neutralText.isNullOrEmpty()
binding.layoutButtonsHorizontal.visibility = if (hasThreeButtons) View.GONE else View.VISIBLE
binding.layoutButtonsVertical.visibility = if (hasThreeButtons) View.VISIBLE else View.GONE
if (hasThreeButtons) {
// 3按钮模式:顶部=取消,中间=中立,底部=确认
binding.btnTop.text = negativeText
binding.btnMiddle.text = neutralText
binding.btnBottom.text = positiveText
} else {
// 2按钮模式
binding.btnLeft.text = negativeText
binding.btnRight.text = positiveText
}
}
}
open class SafeDialog(activity: Activity, @StyleRes themeResId: Int) :
Dialog(activity, themeResId) {
@@ -47,6 +47,7 @@ import java.net.UnknownHostException
* @param onSuccess 成功回调
* @param onFailure 自定义错误回调(可选,不传则用默认提示)
*/
@Suppress("UNCHECKED_CAST")
suspend fun <T> request(
onRequest: suspend () -> ApiResponse<T>,
onSuccess: (T) -> Unit,
@@ -73,6 +74,7 @@ data class CodeMsg(
var msg: String? = null
)
@Suppress("DEPRECATION")
fun getApiException(e: Exception): ApiException {
// 捕获各类网络异常并转换为自定义异常
return when (e) {
@@ -116,7 +118,7 @@ fun httpException2ApiException(e: HttpException): ApiException {
val body = e.response()?.errorBody()?.string()
val codeMsg: CodeMsg? = try {
body?.toObject<CodeMsg>()
} catch (ex: Exception) {
} catch (_: Exception) {
null
}
return ApiException(
@@ -8,104 +8,89 @@ import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.SeasoningEntity
import kotlinx.coroutines.launch
class NetViewModel : ViewModel() {
/**
* 网络请求 ViewModel,所有网络操作通过 RemoteRepository 发起,不直接依赖 apiService
* @param repository 网络数据仓库,默认使用单例实例
*/
class NetViewModel(
private val repository: RemoteRepository = RemoteRepository()
) : ViewModel() {
/**
* 提交制作菜品
*/
fun submitCookFood(
entity: CookFoodEntity,
onSuccess: (Any?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.submitCookFood(param = entity)
},
onSuccess = onSuccess,
onFailure = onFailure
)
repository.submitCookFood(entity, onSuccess, onFailure)
}
}
/**
* 查询菜品详情
*/
fun getFoodDetail(
foodId: String,
onSuccess: (CookFoodEntity?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.getFoodDetail(foodId = foodId)
},
onSuccess = onSuccess,
onFailure = onFailure
)
repository.getFoodDetail(foodId, onSuccess, onFailure)
}
}
/**
* 搜索菜品列表
*/
fun searchFoodList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<FoodRecord>?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.searchFoodList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
repository.searchFoodList(param, onSuccess, onFailure)
}
}
/**
* 查询采样数据列表
*/
fun getSamplingList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<FoodRecord>?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.getSamplingList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
repository.getSamplingList(param, onSuccess, onFailure)
}
}
/**
* 查询物品信息列表
*/
fun queryGoodsList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<CookFoodGoodsEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.queryGoodsList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
repository.queryGoodsList(param, onSuccess, onFailure)
}
}
/**
* 查询调料信息列表
*/
fun querySeasoningList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<SeasoningEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.querySeasoningList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
repository.querySeasoningList(param, onSuccess, onFailure)
}
}
}
@@ -0,0 +1,109 @@
package com.shuwei.dish.match.net
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.SeasoningEntity
/**
* 网络数据仓库,封装所有 ApiService 调用
* ViewModel 不直接接触 apiService,统一通过此类发起网络请求
*/
class RemoteRepository {
/**
* 提交制作菜品
* @param entity 菜品实体
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
*/
suspend fun submitCookFood(
entity: CookFoodEntity,
onSuccess: (Any?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.submitCookFood(param = entity) },
onSuccess = onSuccess,
onFailure = onFailure
)
/**
* 查询菜品详情
* @param foodId 菜品 ID
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
*/
suspend fun getFoodDetail(
foodId: String,
onSuccess: (CookFoodEntity?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.getFoodDetail(foodId = foodId) },
onSuccess = onSuccess,
onFailure = onFailure
)
/**
* 搜索菜品列表
* @param param 查询参数
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
*/
suspend fun searchFoodList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<FoodRecord>?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.searchFoodList(param = param) },
onSuccess = onSuccess,
onFailure = onFailure
)
/**
* 查询采样数据列表
* @param param 查询参数
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
*/
suspend fun getSamplingList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<FoodRecord>?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.getSamplingList(param = param) },
onSuccess = onSuccess,
onFailure = onFailure
)
/**
* 查询物品信息列表
* @param param 查询参数
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
*/
suspend fun queryGoodsList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<CookFoodGoodsEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.queryGoodsList(param = param) },
onSuccess = onSuccess,
onFailure = onFailure
)
/**
* 查询调料信息列表
* @param param 查询参数
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
*/
suspend fun querySeasoningList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<SeasoningEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.querySeasoningList(param = param) },
onSuccess = onSuccess,
onFailure = onFailure
)
}
@@ -102,7 +102,7 @@ class DishSamplingActivity : BaseActivity() {
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory =
AppFactory(AppRepository(db.appDao()))
AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao()))
appViewModel =
ViewModelProvider(this, factory)[AppViewModel::class.java]
}
@@ -138,7 +138,7 @@ class HomeActivity : BaseActivity() {
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
val factory = AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
@@ -70,7 +70,7 @@ class InitActivity : BaseActivity() {
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
val factory = AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
@@ -33,6 +33,7 @@ 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.SizeTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.toast
@@ -51,7 +52,7 @@ private const val VIEW_TYPE_SCALE1 = 3
class MasterScaleActivity : BaseActivity() {
private lateinit var binding: ActivityMasterScaleBinding
private val size by lazy { SizeTool.getFlexLayoutSize(10.dp) }
private data class DeviceGroup(
val deviceId: String,
val ip: String,
@@ -357,12 +358,7 @@ class MasterScaleActivity : BaseActivity() {
b.tvDeviceLabel.text = "设备:${group.deviceId}"
if (innerAdapter == null) {
val screenWidth = resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
val rvHeight = smallSize * 3 + 3 * 4.dp
b.rvScales.layoutParams = b.rvScales.layoutParams.also { it.height = rvHeight }
b.rvScales.layoutParams = b.rvScales.layoutParams.also { it.height = size.rvHeight }
b.rvScales.layoutManager = FlexboxLayoutManager(b.root.context).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
@@ -370,8 +366,8 @@ class MasterScaleActivity : BaseActivity() {
}
b.rvScales.itemAnimator = null
innerAdapter = Seasoning22GridAdapter(
largeSize,
smallSize,
size.largeSize,
size.smallSize,
showNameOnly = false,
showAddress = true
).apply {
@@ -455,6 +451,7 @@ class MasterScaleActivity : BaseActivity() {
val scale = scales.firstOrNull()
b.llScaleContainer.tag = scale
b.tvAddr.text = scale?.let { "${it.address}" } ?: ""
b.tvName.text = scale?.name?.takeIf { it.isNotBlank() } ?: "-"
b.tvWeight.text = scale?.let { "${it.weight} g" } ?: ""
b.tvState.text = stateStr(scale?.state)
}
@@ -74,7 +74,7 @@ class SamplingListActivity : BaseActivity() {
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
val factory = AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
@@ -68,7 +68,7 @@ class SelectDishActivity : BaseActivity() {
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
val factory = AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
@@ -2,33 +2,20 @@ package com.shuwei.dish.match.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatButton
import android.provider.Settings
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import androidx.fragment.app.Fragment
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import com.shuwei.dish.match.R
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivitySettingBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.ui.fragment.CollectFragment
import com.shuwei.dish.match.ui.fragment.DeviceConfigFragment
import com.shuwei.dish.match.ui.fragment.SeasoningConfigFragment
import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
/**
@@ -42,10 +29,10 @@ class SettingActivity : BaseActivity() {
}
private lateinit var binding: ActivitySettingBinding
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this)
}
private lateinit var previewView: PreviewView
// private val cameraUtils: CameraUtils by lazy {
// CameraUtils(this)
// }
// private lateinit var previewView: PreviewView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -55,104 +42,54 @@ class SettingActivity : BaseActivity() {
setTitleBar(titleBarAction = {
it.visible()
}, titleAction = {
it.text = "设置"
it.text = "备设"
}, rightIconActon = {
it.gone()
})
// 默认显示设备配置Fragment
if (savedInstanceState == null) {
showDeviceConfigFragment()
}
initUI()
}
/**
* 初始化UI和事件监听
*/
private fun initUI() {
// 设备配置按钮点击事件
binding.btnDeviceConfig.setOnClickListener {
showDeviceConfigFragment()
loadTabStyle(button = binding.btnDeviceConfig, isSelected = true)
loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
}
// 调料配置按钮点击事件
binding.btnSeasoningConfig.setOnClickListener {
showSeasoningConfigFragment()
loadTabStyle(button = binding.btnDeviceConfig, isSelected = false)
loadTabStyle(button = binding.btnSeasoningConfig, isSelected = true)
loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
}
// 菜品采集按钮点击事件
binding.btnFoodCollect.setOnClickListener {
checkCameraPermissionAndShowFragment()
loadTabStyle(button = binding.btnDeviceConfig, isSelected = false)
loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
loadTabStyle(button = binding.btnFoodCollect, isSelected = true)
}
// 主设备显示「秤数据监控」入口
if (GlobalData.deviceRole == DeviceRole.MASTER) {
binding.btnScaleMonitor.visible()
binding.btnScaleMonitor.setOnClickListener {
it.visible()
it.clickWithDebounce {
startActivity<MasterScaleActivity>()
}
}
})
// 初始化按钮颜色(默认设备配置为选中状态)
initView()
loadTabStyle(button = binding.btnDeviceConfig, isSelected = true)
loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
}
// /** 刷新角色按钮文字 */
// private fun updateRoleButtonText() {
// val roleLabel = if (GlobalData.deviceRole == DeviceRole.MASTER) "主设备" else "子设备"
// binding.btnDeviceRole.text = "设备角色:$roleLabel"
// // 默认显示设备配置Fragment
// if (savedInstanceState == null) {
// showDeviceConfigFragment()
// }
//
// initUI()
}
/**
* 加载顶部Tab按钮样式
*
* @param button 按钮
* @param isSelected 是否选中
*/
private fun loadTabStyle(button: AppCompatButton, isSelected: Boolean) {
button.apply {
if (isSelected){
// 选中
setTextColor(Color.WHITE)
textSize = 28f
setTypeface(null, android.graphics.Typeface.BOLD)
background = ContextCompat.getDrawable(this@SettingActivity, R.drawable.bg_btn_underline)
} else {
// 未选中
setTextColor("#5E7585".toColorInt())
textSize = 26f
setTypeface(null, android.graphics.Typeface.NORMAL)
background = null
private fun initView() {
binding.tvCurrentMode.text = when (SpTool.cookMode) {
0 -> "当前:制作模式"
1 -> "当前:采样模式"
else -> "当前:未知模式"
}
binding.llCookMode.clickWithDebounce {
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.COOK_MODE)
}
binding.llSeasoningConfig.clickWithDebounce {
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.SEASONING_CONFIG)
}
binding.llFoodCollect.clickWithDebounce {
checkCameraPermission()
}
}
private fun openCollectPage() {
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.FOOD_COLLECT)
}
/**
* 检查相机权限并显示 CollectFragment
*/
@SuppressLint("ObsoleteSdkInt")
private fun checkCameraPermissionAndShowFragment() {
val collectFragment = fragmentList[2]
private fun checkCameraPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Android 6.0 以下,直接显示 Fragment
showFragment(collectFragment)
// Android 6.0 以下,直接打开
openCollectPage()
return
}
if (ContextCompat.checkSelfPermission(
@@ -160,109 +97,228 @@ class SettingActivity : BaseActivity() {
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
// 权限已授予,直接显示 Fragment
showFragment(collectFragment)
// 权限已授予,直接打开
openCollectPage()
} else {
// 权限未授予,申请权限
requestPermission(Manifest.permission.CAMERA) { isGranted ->
if (isGranted) {
// 权限已授予,显示 CollectFragment
showFragment(collectFragment)
// 权限已授予,直接打开
openCollectPage()
} else {
// 权限被拒绝
toast("暂无相机权限,无法使用菜品采集功能")
showNotGrantedCameraPermissionDialog()
}
}
}
}
/**
* 初始化相机
* @param container 相机预览容器
*/
fun initCamera(container: ViewGroup) {
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, container)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 456.dp
height = 342.dp
private fun showNotGrantedCameraPermissionDialog() {
CommonDialog(this)
.setTitle("温馨提示")
.setContent("暂无相机权限,无法使用菜品采集功能,请确认是否同意开始权限?")
.setNegativeButton("不同意")
.setPositiveButton("同意") {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent) {
checkCameraPermission()
}
}
cameraUtils.setPreviewController(previewView)
.show()
}
/**
* Activity 恢复时绑定相机
*/
override fun onResume() {
super.onResume()
cameraUtils.bind()
}
/**
* Activity 暂停时解绑相机
*/
override fun onPause() {
super.onPause()
cameraUtils.unbind()
}
/**
* 提供给 Fragment 调用的拍照方法
*/
fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = {}) {
cameraUtils.takePhoto(succCallback = succCallback, failCallback = failCallback)
}
/**
* 显示设备切换Fragment
*/
private fun showDeviceConfigFragment() {
showFragment(fragmentList[0])
}
/**
* 显示设调料配置Fragment
*/
private fun showSeasoningConfigFragment() {
showFragment(fragmentList[1])
}
private val fragmentList = mutableListOf<Fragment>().apply {
add(DeviceConfigFragment())
add(SeasoningConfigFragment())
add(CollectFragment())
}
/**
* 显示指定的 Fragment
* 使用 add/show/hide 方式管理 Fragment,避免重复创建
* 已添加的 Fragment 直接显示,未添加的 Fragment 先添加再显示
*/
private fun showFragment(fragment: Fragment) {
runCatching {
supportFragmentManager.beginTransaction().apply {
// 如果 Fragment 还未添加到容器中,则添加
if (!fragment.isAdded) {
add(binding.fragmentContainer.id, fragment)
} else {
// 已添加过,直接显示
show(fragment)
}
// 隐藏其他 Fragment
fragmentList.forEach { otherFragment ->
if (otherFragment != fragment && otherFragment.isAdded) {
hide(otherFragment)
}
}
commit()
}
}.onFailure {
it.printStackTrace()
}
}
//
// /**
// * 初始化UI和事件监听
// */
// private fun initUI() {
// // 设备配置按钮点击事件
// binding.btnDeviceConfig.setOnClickListener {
// showDeviceConfigFragment()
//
// loadTabStyle(button = binding.btnDeviceConfig, isSelected = true)
// loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
// loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
// }
//
// // 调料配置按钮点击事件
// binding.btnSeasoningConfig.setOnClickListener {
// showSeasoningConfigFragment()
//
// loadTabStyle(button = binding.btnDeviceConfig, isSelected = false)
// loadTabStyle(button = binding.btnSeasoningConfig, isSelected = true)
// loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
// }
//
// // 菜品采集按钮点击事件
// binding.btnFoodCollect.setOnClickListener {
// checkCameraPermissionAndShowFragment()
//
// loadTabStyle(button = binding.btnDeviceConfig, isSelected = false)
// loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
// loadTabStyle(button = binding.btnFoodCollect, isSelected = true)
// }
//
// // 主设备显示「秤数据监控」入口
// if (GlobalData.deviceRole == DeviceRole.MASTER) {
// binding.btnScaleMonitor.visible()
// binding.btnScaleMonitor.setOnClickListener {
// startActivity<MasterScaleActivity>()
// }
// }
//
// // 初始化按钮颜色(默认设备配置为选中状态)
//
// loadTabStyle(button = binding.btnDeviceConfig, isSelected = true)
// loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
// loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
// }
//
// /**
// * 加载顶部Tab按钮样式
// *
// * @param button 按钮
// * @param isSelected 是否选中
// */
// private fun loadTabStyle(button: AppCompatButton, isSelected: Boolean) {
// button.apply {
// if (isSelected){
// // 选中
// setTextColor(Color.WHITE)
// textSize = 28f
// setTypeface(null, android.graphics.Typeface.BOLD)
// background = ContextCompat.getDrawable(this@SettingActivity, R.drawable.bg_btn_underline)
// } else {
// // 未选中
// setTextColor("#5E7585".toColorInt())
// textSize = 26f
// setTypeface(null, android.graphics.Typeface.NORMAL)
// background = null
// }
// }
// }
//
// /**
// * 检查相机权限并显示 CollectFragment
// */
// @SuppressLint("ObsoleteSdkInt")
// private fun checkCameraPermissionAndShowFragment() {
// val collectFragment = fragmentList[2]
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// // Android 6.0 以下,直接显示 Fragment
// showFragment(collectFragment)
// return
// }
// if (ContextCompat.checkSelfPermission(
// this,
// Manifest.permission.CAMERA
// ) == PackageManager.PERMISSION_GRANTED
// ) {
// // 权限已授予,直接显示 Fragment
// showFragment(collectFragment)
// } else {
// // 权限未授予,申请权限
// requestPermission(Manifest.permission.CAMERA) { isGranted ->
// if (isGranted) {
// // 权限已授予,显示 CollectFragment
// showFragment(collectFragment)
// } else {
// // 权限被拒绝
// toast("暂无相机权限,无法使用菜品采集功能")
// }
// }
// }
// }
//
// /**
// * 初始化相机
// * @param container 相机预览容器
// */
// fun initCamera(container: ViewGroup) {
// cameraUtils.initCamera()
// val previewBinding =
// LayoutCameraPreviewBinding.inflate(layoutInflater, container)
// previewView = previewBinding.previewView.also {
// it.updateLayoutParams {
// width = 456.dp
// height = 342.dp
// }
// }
// cameraUtils.setPreviewController(previewView)
// }
//
// /**
// * Activity 恢复时绑定相机
// */
// override fun onResume() {
// super.onResume()
// cameraUtils.bind()
// }
//
// /**
// * Activity 暂停时解绑相机
// */
// override fun onPause() {
// super.onPause()
// cameraUtils.unbind()
// }
//
// /**
// * 提供给 Fragment 调用的拍照方法
// */
// fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = {}) {
// cameraUtils.takePhoto(succCallback = succCallback, failCallback = failCallback)
// }
//
// /**
// * 显示设备切换Fragment
// */
// private fun showDeviceConfigFragment() {
// showFragment(fragmentList[0])
// }
//
// /**
// * 显示设调料配置Fragment
// */
// private fun showSeasoningConfigFragment() {
// showFragment(fragmentList[1])
// }
//
// private val fragmentList = mutableListOf<Fragment>().apply {
// add(DeviceConfigFragment())
// add(SeasoningConfigFragment())
// add(CollectFragment())
// }
//
// /**
// * 显示指定的 Fragment
// * 使用 add/show/hide 方式管理 Fragment,避免重复创建
// * 已添加的 Fragment 直接显示,未添加的 Fragment 先添加再显示
// */
// private fun showFragment(fragment: Fragment) {
// runCatching {
// supportFragmentManager.beginTransaction().apply {
// // 如果 Fragment 还未添加到容器中,则添加
// if (!fragment.isAdded) {
// add(binding.fragmentContainer.id, fragment)
// } else {
// // 已添加过,直接显示
// show(fragment)
// }
//
// // 隐藏其他 Fragment
// fragmentList.forEach { otherFragment ->
// if (otherFragment != fragment && otherFragment.isAdded) {
// hide(otherFragment)
// }
// }
//
// commit()
// }
// }.onFailure {
// it.printStackTrace()
// }
// }
}
@@ -0,0 +1,119 @@
package com.shuwei.dish.match.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.ui.fragment.CollectFragment
import com.shuwei.dish.match.ui.fragment.DeviceConfigFragment
import com.shuwei.dish.match.ui.fragment.SeasoningConfigFragment
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
/**
* 通用单 Fragment 容器 Activity
* 通过 [PageType] 决定加载哪个 Fragment,避免为每个配置/功能页面单独创建 Activity
*/
class SingleFragmentActivity : BaseActivity() {
interface LifecycleCallback {
fun onActivityResume()
fun onActivityPause()
}
/**
* 支持的页面类型枚举
* 新增页面时在此追加枚举值,并在 [createFragment] 中补充对应分支
*/
enum class PageType {
/** 菜品采集页 */
FOOD_COLLECT,
/** 设备配置页 */
COOK_MODE,
/** 调料配置页 */
SEASONING_CONFIG,
}
companion object {
private const val EXTRA_PAGE_TYPE = "extra_page_type"
/**
* 启动 SingleFragmentActivity
* @param context 调用方 Context
* @param pageType 要显示的页面类型
*/
fun start(context: Context, pageType: PageType) {
Intent(context, SingleFragmentActivity::class.java).apply {
putExtra(EXTRA_PAGE_TYPE, pageType.name)
context.startActivity(this)
}
}
}
private var lifecycleCallback: LifecycleCallback? = null
fun setLifecycleCallback(callback: LifecycleCallback) {
lifecycleCallback = callback
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHeaderBackground()
// 解析页面类型,非法值直接关闭
val typeName = intent.getStringExtra(EXTRA_PAGE_TYPE)
val pageType = typeName?.let {
runCatching { PageType.valueOf(it) }.getOrNull()
}
if (pageType == null) {
finish()
return
}
// 配置标题栏
setTitleBar(
titleBarAction = { it.visible() },
titleAction = { it.text = getTitleForPage(pageType) },
rightIconActon = { it.gone() }
)
// 首次创建时加载 Fragment,避免屏幕旋转重复添加
if (savedInstanceState == null) {
val fragment = createFragment(pageType)
supportFragmentManager.beginTransaction()
.replace(getFragmentContainerId(), fragment)
.commit()
}
}
/**
* 根据页面类型返回对应标题文本
*/
private fun getTitleForPage(pageType: PageType): String = when (pageType) {
PageType.FOOD_COLLECT -> "食材采样"
PageType.COOK_MODE -> "餐品模式"
PageType.SEASONING_CONFIG -> "调料区设置"
}
/**
* 根据页面类型创建对应 Fragment 实例
*/
private fun createFragment(pageType: PageType): Fragment = when (pageType) {
PageType.FOOD_COLLECT -> CollectFragment()
PageType.COOK_MODE -> DeviceConfigFragment()
PageType.SEASONING_CONFIG -> SeasoningConfigFragment()
}
override fun onResume() {
super.onResume()
lifecycleCallback?.onActivityResume()
}
override fun onPause() {
super.onPause()
lifecycleCallback?.onActivityPause()
}
}
@@ -27,6 +27,7 @@ import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.SizeTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
@@ -61,6 +62,7 @@ class SlaveActivity : BaseActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
/** 当前高亮清除任务,新高亮触发时取消旧任务 */
private var clearHighlightRunnable: Runnable? = null
private val size by lazy { SizeTool.getFlexLayoutSize(10.dp) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -90,13 +92,8 @@ class SlaveActivity : BaseActivity() {
binding.rvScaleList.itemAnimator = null
when (GlobalData.deviceId) {
ScaleDeviceConfig.DEVICE_ID_22 -> {
val screenWidth = resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
val rvHeight = smallSize * 3 + 3 * 4.dp
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> {
height = rvHeight
height = size.rvHeight
weight = 0f
topMargin = 20.dp
}
@@ -105,7 +102,7 @@ class SlaveActivity : BaseActivity() {
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.FLEX_START
}
scale22Adapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false).also {
scale22Adapter = Seasoning22GridAdapter(size.largeSize, size.smallSize, showNameOnly = false).also {
it.onItemClick = { scale, _ -> showTareDialog(scale.address) }
}
binding.rvScaleList.adapter = scale22Adapter
@@ -3,6 +3,7 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import androidx.activity.addCallback
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
@@ -13,6 +14,7 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySubmitFoodBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
@@ -23,6 +25,7 @@ import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toJsonString
@@ -83,6 +86,12 @@ class SubmitFoodActivity : BaseActivity() {
*/
private val rawWeightMap = mutableMapOf<String, Triple<String, String, Double>>()
/**
* isCooking=true 时从 goodsList 预填充的历史用量,key = goodsNamevalue = goodsId to useWeight
* 用于 refreshAdapterByName 时叠加到秤新增用量上
*/
private val baseSeasoningMap = mutableMapOf<String, Pair<String, Double>>()
@Suppress("unchecked_cast", "DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -93,8 +102,11 @@ class SubmitFoodActivity : BaseActivity() {
intent.extras?.apply {
food = getSerializable(FOOD_ITEM) as FoodRecord?
Log.d(TAG, "onCreate: cookMode=${food?.cookMode}")
// 非烹饪中,从菜品主辅材配置页面带入数据
// isCooking=false:从前一页面带入主辅材数据;isCooking=true:从数据库查询
if (food?.isCooking == false) {
goodsList = getSerializable(GOODS_LIST) as MutableList<CookFoodGoodsEntity>?
Log.d(TAG, "onCreate: goodsList=${goodsList.toJsonString()}")
}
}
setTitleBar(titleBarAction = {
it.visible()
@@ -102,11 +114,17 @@ class SubmitFoodActivity : BaseActivity() {
it.text = food?.foodName
}, rightIconActon = {
it.gone()
it.setImageResource(R.drawable.ic_setting)
}, backAction = {
it.setOnClickListener {
remindSaveDataDialog()
}
})
setupRecyclerView()
addViewListener()
loadSlotsThenObserveScales()
onBackPressedDispatcher.addCallback(this) {
remindSaveDataDialog()
}
}
/** 初始化 RecyclerView2列 GridLayoutManager */
@@ -118,7 +136,7 @@ class SubmitFoodActivity : BaseActivity() {
/**
* 从 Room 加载槽位配置后,开始订阅子设备秤数据
* 确保 slotMap 就绪后再处理秤数据,避免名称为空
* isCooking=true 时先从数据库查询 goodsList,筛选调料预填充 adapter,再开始观测
*/
private fun loadSlotsThenObserveScales() {
lifecycleScope.launch(Dispatchers.IO) {
@@ -127,8 +145,30 @@ class SubmitFoodActivity : BaseActivity() {
slots.forEach { slot ->
slotMap["${slot.deviceId}#${slot.address}"] = slot.goodsId to slot.goodsName
}
if (food?.isCooking == true) {
// 制作中:从数据库查询 goodsList,筛选调料数据预填充 adapter
val foodId = food?.foodId ?: return@withContext
val cookMode = food?.cookMode ?: return@withContext
appViewModel.getCookFoodGoodsList(foodId, cookMode) { list ->
goodsList = list
list.filter { it.materialType == 3 }.forEach { item ->
val name = item.goodsName ?: ""
val weight = item.useWeight ?: 0.0
baseSeasoningMap[name] = (item.goodsId) to weight
seasoningAdapter.updateItem(
SeasoningWeightAdapter.Item(
goodsId = item.goodsId,
goodsName = name,
useWeight = weight
)
)
}
observeScaleData()
}
} else {
observeScaleData()
}
}
}
}
@@ -174,33 +214,34 @@ class SubmitFoodActivity : BaseActivity() {
}
/**
* 按 goodsName 聚合 rawWeightMap将同名调料的用量累加后推给 adapter
* 聚合后总量 < 0.5g 则从 adapter 移除
* 按 goodsName 聚合 rawWeightMap叠加历史用量后推给 adapter
* isCooking=true 时只新增,不移除(历史调料数据不可减少)
*/
private fun refreshAdapterByName(goodsName: String) {
val grouped = rawWeightMap.values.filter { it.second == goodsName }
if (grouped.isEmpty()) {
val newWeight = grouped.sumOf { it.third }.roundedOneDecimalPlace()
val (baseGoodsId, baseWeight) = baseSeasoningMap[goodsName] ?: ("" to 0.0)
val totalWeight = (newWeight + baseWeight).roundedOneDecimalPlace()
if (totalWeight < 0.5) {
// 制作中模式:已有历史数据,不允许移除
if (food?.isCooking != true) {
seasoningAdapter.removeItem(goodsName)
}
return
}
val totalWeight = grouped.sumOf { it.third }.roundedOneDecimalPlace()
if (totalWeight < 0.5) {
seasoningAdapter.removeItem(goodsName)
} else {
val goodsId = grouped.firstOrNull()?.first?.takeIf { it.isNotEmpty() } ?: baseGoodsId
seasoningAdapter.updateItem(
SeasoningWeightAdapter.Item(
goodsId = grouped.first().first,
goodsId = goodsId,
goodsName = goodsName,
useWeight = totalWeight
)
)
}
}
private fun addViewListener() {
binding.btnCook.clickWithDebounce { cook() }
binding.btnWeightClear.clickWithDebounce { WeightUtil.tareTwo(2) }
binding.btnSubmit.clickWithDebounce {
if (cookFoodEntity.foodWeight <= 0.toDouble()) {
toast("未识别到菜品熟重")
@@ -212,8 +253,14 @@ class SubmitFoodActivity : BaseActivity() {
// 监听主设备 2格秤的熟重
WeightUtil.addWeightListener(TAG) { address, _, weight ->
if (address == AddressUtil.TWO) {
cookFoodEntity.foodWeight = weight.toDouble()
binding.tvTotalWeight.text = "${weight / 1000f}"
cookFoodEntity.foodWeight = weight
if (weight > 1000) {
binding.tvTotalWeight.text = "${(weight / 1000f).roundedDecimalPlace(3)}"
binding.tvWeightUnit.text = "千克"
} else {
binding.tvTotalWeight.text = "$weight"
binding.tvWeightUnit.text = ""
}
}
}
}
@@ -231,7 +278,7 @@ class SubmitFoodActivity : BaseActivity() {
showLoading()
// 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList
val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
// val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
val tempSeasoningList = seasoningData.map { item ->
CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
@@ -348,7 +395,7 @@ class SubmitFoodActivity : BaseActivity() {
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
val factory = AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
@@ -356,4 +403,18 @@ class SubmitFoodActivity : BaseActivity() {
WeightUtil.removeWeightListener(TAG)
super.onDestroy()
}
/**
* 未保存数据提醒dialog
*/
private fun remindSaveDataDialog() {
CommonDialog(this)
.setTitle("温馨提示")
.setContent("请确认是否存在未保存的数据?")
.setNegativeButton("取消,留在页面")
.setNeutralButton("返回上页,不保存") { finish() }
.setPositiveButton("保存为烹饪中菜品") { cook() }
.show()
}
}
@@ -10,20 +10,26 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.camera.view.PreviewView
import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FoodCollectionAdapter
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentCollectBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.objbox.FoodCollectionBean
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.ui.SettingActivity
import com.shuwei.dish.match.ui.SingleFragmentActivity
import com.shuwei.dish.match.utils.BitmapSaver
import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.Debouncer
import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.LogSaveUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.toast
@SuppressLint("NotifyDataSetChanged")
@@ -31,7 +37,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
companion object {
private const val TAG = "CollectFragment"
const val MAX_COUNT = 9
const val MAX_COUNT = 18
}
private var selectedFoodId: String? = ""
@@ -58,7 +64,8 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
// }
// }
private val debouncer = Debouncer(2000)
private lateinit var settingActivity: SettingActivity
// private lateinit var settingActivity: SettingActivity
private lateinit var currentActivity: SingleFragmentActivity
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(foodCollectionList).apply {
@@ -168,7 +175,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
return
}
showWaitingDialog("采集中……")
settingActivity.takePhoto(succCallback = cameraCallback, failCallback = { errMsg ->
cameraUtils.takePhoto(succCallback = cameraCallback, failCallback = { errMsg ->
hideWaitingDialog()
// toast("拍照异常,请重新操作")
// if (cameraErrorCount >= 10) {
@@ -183,9 +190,22 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
}
override fun initialize() {
settingActivity = activity as SettingActivity
currentActivity = activity as SingleFragmentActivity
// 首次显示 CollectFragment 时初始化相机
settingActivity.initCamera(binding.flCameraPreview)
initCamera(binding.flCameraPreview)
currentActivity.setLifecycleCallback(object : SingleFragmentActivity.LifecycleCallback {
override fun onActivityResume() {
// 首次进入时 previewView 尚未 attach,由 doOnLayout 负责首次 bind
// 从后台切回时 previewView 已 attach,直接 bind 恢复预览
if (::previewView.isInitialized && previewView.isAttachedToWindow) {
cameraUtils.bind()
}
}
override fun onActivityPause() {
cameraUtils.unbind()
}
})
binding.rvFoodList.let {
it.layoutManager =
@@ -340,7 +360,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
override fun onResume() {
super.onResume()
if (isVisible.not()) return
// 不使用 isVisible 判断,首次打开时 View 尚未 attach 到 WindowisVisible 会返回 false 导致遮罩永不隐藏
binding.llCameraFlag.run {
visibility = View.VISIBLE
postDelayed({
@@ -361,7 +381,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
private fun searchByFoodName(name: String, block: (String) -> Unit) {}
private fun hideWaitingDialog() {
settingActivity.dismissLoading()
currentActivity.dismissLoading()
}
private fun showWaitingDialog(msg: String) {
@@ -369,6 +389,34 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
Loading.updateContent(msg)
return
}
settingActivity.showLoading(msg)
currentActivity.showLoading(msg)
}
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(requireActivity())
}
/**
* 初始化相机
* @param container 相机预览容器
*/
fun initCamera(container: ViewGroup) {
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, container)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 456.dp
height = 342.dp
}
}
cameraUtils.setPreviewController(previewView)
// doOnLayout 确保 previewView 测量完成(有有效尺寸和 Surface)后再 bind,解决首次打开无预览问题
previewView.doOnLayout {
cameraUtils.bind()
}
}
}
@@ -1,46 +1,22 @@
package com.shuwei.dish.match.ui.fragment
import android.graphics.Typeface
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.LineHeightSpan
import android.text.style.StyleSpan
import android.util.Log
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.graphics.toColorInt
import androidx.core.view.forEach
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentDeviceConfigBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.SeasoningSearchDialog
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.ResetReasoningRecord
import com.shuwei.dish.match.adapter.TextCellAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.JsonAssetsLoader
import com.shuwei.dish.match.utils.MultiClickDetector
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import com.shuwei.dish.match.ui.SamplingListActivity
import com.shuwei.dish.match.ui.SelectDishActivity
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.startActivity
/**
@@ -65,10 +41,10 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
// private lateinit var appViewModel: AppViewModel
private val weightArray = SparseArray<Double>()
private val addressArray = AddressUtil.getWeighAddressArray()
private var seasoningItems = mutableListOf<SeasoningEntity>()
private val detector = MultiClickDetector(targetCount = 10, intervalMs = 800)
// private val weightArray = SparseArray<Double>()
// private val addressArray = AddressUtil.getWeighAddressArray()
// private var seasoningItems = mutableListOf<SeasoningEntity>()
// private val detector = MultiClickDetector(targetCount = 10, intervalMs = 800)
override fun inflateBinding(
inflater: LayoutInflater,
@@ -140,7 +116,8 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
loadCookSpan()
loadSamplingSpan(isClicked = true)
activity?.startActivity<SamplingListActivity> {
putExtra(SamplingListActivity.IS_CONFIG_PAGE, true) }
putExtra(SamplingListActivity.IS_CONFIG_PAGE, true)
}
activity?.finish()
}
}
@@ -19,12 +19,14 @@ import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
import com.shuwei.dish.match.dialog.SeasoningSelectDialog
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.SeasoningSlotEntity
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleEvent
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.SingleFragmentActivity
import com.shuwei.dish.match.utils.SizeTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
@@ -35,8 +37,10 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
companion object {
private const val TAG = "SeasoningConfigFragment"
/** 重量增加阈值(克),超过此值才触发高亮 */
private const val WEIGHT_DELTA_THRESHOLD = 10.0
/** 高亮自动恢复时间(毫秒) */
private const val HIGHLIGHT_DURATION_MS = 15_000L
}
@@ -46,21 +50,28 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
container: ViewGroup?
) = FragmentSeasoningConfigBinding.inflate(inflater, container, false)
private lateinit var settingActivity: SettingActivity
private lateinit var currentActivity: SingleFragmentActivity
// private lateinit var appViewModel: AppViewModel
private val adapter22 by lazy { buildAdapter22() }
private val adapter18 by lazy { Seasoning18GridAdapter(showNameOnly = true) }
private val adapter18 by lazy {
Seasoning18GridAdapter(
showNameOnly = true,
leftRightSpace = 20.dp
)
}
/** 各秤的上一次重量快照,key 格式 "{deviceId}#{address}",用于计算 delta */
private val weightSnapshot = mutableMapOf<String, Double>()
/** 主线程 Handler,用于延迟清除高亮 */
private val mainHandler = Handler(Looper.getMainLooper())
/** 当前高亮清除任务,新高亮触发时取消旧任务 */
private var clearHighlightRunnable: Runnable? = null
override fun initialize() {
settingActivity = activity as SettingActivity
currentActivity = activity as SingleFragmentActivity
// initViewModel()
setupRecyclerViews()
bindAdapterClicks()
@@ -72,10 +83,7 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
/** 绑定油盆格子点击,弹出调料选择弹窗并保存到 DEVICE_ID_1 */
private fun bindOilSlotClick() {
binding.tvOilSlot.setOnClickListener {
SeasoningSelectDialog(
activity = settingActivity,
clickIndex = 0,
onItemSelected = { item ->
openSeasoningSelectDialog(0) { item ->
binding.tvOilSlot.text = item.goodsName ?: "-"
saveSlotAndBroadcast(
SeasoningSlotEntity(
@@ -86,17 +94,13 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
)
)
}
).show()
}
}
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
private fun bindAdapterClicks() {
adapter22.onItemClick = { scaleData, position ->
SeasoningSelectDialog(
activity = settingActivity,
clickIndex = position,
onItemSelected = { item ->
openSeasoningSelectDialog(position) { item ->
adapter22.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
@@ -107,13 +111,9 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
)
)
}
).show()
}
adapter18.onItemClick = { scaleData, position ->
SeasoningSelectDialog(
activity = settingActivity,
clickIndex = position,
onItemSelected = { item ->
openSeasoningSelectDialog(position) { item ->
adapter18.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
@@ -124,10 +124,26 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
)
)
}
).show()
}
}
/**
* 打开调料选择弹窗
*
* @param position 格子位置
* @param callback 调料选择回调
*/
private fun openSeasoningSelectDialog(
position: Int,
callback: (item: SeasoningEntity) -> Unit
) {
SeasoningSelectDialog(
activity = currentActivity,
clickIndex = position,
onItemSelected = callback
).show()
}
/**
* 将单个槽位写入 Room,再读取全量配置广播给子设备
* 仅主设备执行广播,子设备不调用此方法
@@ -150,24 +166,24 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
// appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
// }
private val size by lazy { SizeTool.getFlexLayoutSize(20.dp) }
/** 构建上层(22格)Adapter,需要先计算格子尺寸 */
private fun buildAdapter22(): Seasoning22GridAdapter {
val usable = resources.displayMetrics.widthPixels - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
return Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = true)
return Seasoning22GridAdapter(
largeSize = size.largeSize,
smallSize = size.smallSize,
showNameOnly = true
)
}
/** 配置两个 RecyclerView */
private fun setupRecyclerViews() {
// 上层:FlexboxLayoutManager,仿 MasterScaleActivity 中 Scale22VH 的布局方式
// rvTop 高度 = 小格子尺寸 * 3行 + 3 * 间距,与 Scale22VH 保持一致
val screenWidth = resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val rvTopHeight = smallSize * 3 + 3 * 4.dp
binding.rvTop.apply {
layoutParams = layoutParams.also { it.height = rvTopHeight }
layoutParams = layoutParams.also { it.height = size.rvHeight }
layoutManager = FlexboxLayoutManager(requireContext()).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
@@ -205,7 +221,8 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
}
// 加载油盆格子名称(address=1
val oilSlot = slotsOil.firstOrNull { it.address == 1 }
binding.tvOilSlot.text = if (oilSlot != null && oilSlot.goodsName.isNotEmpty()) oilSlot.goodsName else "-"
binding.tvOilSlot.text =
if (oilSlot != null && oilSlot.goodsName.isNotEmpty()) oilSlot.goodsName else "-"
}
}
}
@@ -223,7 +240,10 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
* - STATE_STABLE:无条件更新基准(覆盖取走调料/归零等场景)
*/
private fun addWeightListener() {
Log.d(TAG, "addWeightListener: 注册监听, deviceId=${GlobalData.deviceId}, role=${GlobalData.deviceRole}")
Log.d(
TAG,
"addWeightListener: 注册监听, deviceId=${GlobalData.deviceId}, role=${GlobalData.deviceRole}"
)
if (GlobalData.deviceRole == DeviceRole.MASTER) {
// 主设备:通过 allScales StateFlow 观测所有秤(含远端子设备秤)
@@ -243,14 +263,21 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
return@forEach
}
val delta = data.weight - baseline
Log.d(TAG, "allScales: key=$key address=${data.address} state=${data.state} weight=${data.weight} baseline=$baseline delta=$delta")
Log.d(
TAG,
"allScales: key=$key address=${data.address} state=${data.state} weight=${data.weight} baseline=$baseline delta=$delta"
)
when {
// 仅在秤稳定后判断增量,避免取走物品时手部冲力造成误触发
data.state == WeightUtil.STATE_STABLE && delta > WEIGHT_DELTA_THRESHOLD -> {
weightSnapshot[snapshotKey] = data.weight
Log.d(TAG, "allScales: 触发高亮 key=$key address=${data.address} delta=$delta")
Log.d(
TAG,
"allScales: 触发高亮 key=$key address=${data.address} delta=$delta"
)
triggerHighlight(data.deviceId, data.address, delta)
}
data.state == WeightUtil.STATE_STABLE -> {
weightSnapshot[snapshotKey] = data.weight
}
@@ -263,7 +290,10 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
WeightUtil.addWeightListener(TAG) { address, state, weight ->
val snapshotKey = "${GlobalData.deviceId}#$address"
val baseline = weightSnapshot[snapshotKey]
Log.d(TAG, "onWeight: address=$address state=$state weight=$weight baseline=$baseline")
Log.d(
TAG,
"onWeight: address=$address state=$state weight=$weight baseline=$baseline"
)
if (baseline == null) {
weightSnapshot[snapshotKey] = weight
@@ -271,7 +301,10 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
}
val delta = weight - baseline
Log.d(TAG, "onWeight: address=$address state=$state weight=$weight baseline=$baseline delta=$delta")
Log.d(
TAG,
"onWeight: address=$address state=$state weight=$weight baseline=$baseline delta=$delta"
)
when {
// 仅在秤稳定后判断增量,避免取走物品时手部冲力造成误触发
@@ -280,6 +313,7 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
Log.d(TAG, "onWeight: 触发高亮 address=$address delta=$delta")
triggerHighlight(GlobalData.deviceId, address, delta)
}
state == WeightUtil.STATE_STABLE -> {
weightSnapshot[snapshotKey] = weight
}
@@ -302,7 +336,10 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
val pos18 = if (sourceDeviceId == ScaleDeviceConfig.DEVICE_ID_18)
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"
)
// 两个列表都不包含该地址,忽略
if (pos22 < 0 && pos18 < 0) {
@@ -318,6 +355,7 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
adapter22.highlightPosition(pos22)
adapter18.clearHighlight()
}
else -> {
adapter18.highlightPosition(pos18)
adapter22.clearHighlight()
@@ -64,7 +64,7 @@ class CameraUtils(private var activity: ComponentActivity) {
)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
}
bind()
// bind() 由外部在 previewView attach 到窗口后调用,避免首次无预览
}
// if (isCameraReady.not()) {
@@ -0,0 +1,25 @@
package com.shuwei.dish.match.utils
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.utils.ext.dp
data class FlexLayoutSize(
val smallSize: Int,
val largeSize: Int,
val rvHeight: Int
)
object SizeTool {
fun getFlexLayoutSize(leftRightSpace: Int = 10.dp): FlexLayoutSize {
// 上层:FlexboxLayoutManager,仿 MasterScaleActivity 中 Scale22VH 的布局方式
// rvTop 高度 = 小格子尺寸 * 3行 + 3 * 间距,与 Scale22VH 保持一致
val screenWidth = BaseApp.instance!!.resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - leftRightSpace
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
val rvHeight = smallSize * 3 + 3 * 4.dp
return FlexLayoutSize(smallSize, largeSize, rvHeight)
}
}
@@ -173,6 +173,17 @@ class AppViewModel(private val rep: AppRepository) : ViewModel() {
}
}
/**
* 查询指定设备是否已配置调料槽位
* @param deviceId 目标设备 ID
* @param action 回调,true 表示已配置,false 表示未配置
*/
fun hasSeasoningSlotConfig(deviceId: String, action: (Boolean) -> Unit) {
viewModelScope.launch {
action(rep.hasSeasoningSlotConfig(deviceId))
}
}
// fun updateAll(list: MutableList<SeasoningEntity>) {
// viewModelScope.launch {
// list.forEach {
@@ -3,6 +3,7 @@ package com.shuwei.dish.match.viewmodel.factory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.db.dao.SeasoningSlotDao
import com.shuwei.dish.match.viewmodel.AppViewModel
@Suppress("UNCHECKED_CAST")
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -59,6 +59,7 @@
app:layout_constraintTop_toBottomOf="@id/tvConnectionStatus"
app:layout_constraintBottom_toBottomOf="parent"
android:overScrollMode="never"
tools:itemCount="4"
tools:listitem="@layout/list_type_scale22"/>
</androidx.constraintlayout.widget.ConstraintLayout>
+141 -46
View File
@@ -1,77 +1,172 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical">
<!-- 底部导航菜单 -->
<LinearLayout
android:id="@+id/bottomMenu"
android:id="@+id/llCookMode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="120dp"
android:orientation="horizontal"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:elevation="8dp">
android:layout_marginHorizontal="32dp"
android:background="@drawable/shape_white_e6_12_corners"
android:foreground="?android:attr/selectableItemBackground"
android:clipToOutline="true">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnDeviceConfig"
<TextView
android:layout_width="0dp"
android:layout_height="70dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="设备切换"
android:textSize="28sp"
android:text="餐品模式"
android:textSize="32sp"
android:textStyle="bold"
android:background="@android:color/transparent" />
android:layout_marginStart="32dp"
android:textColor="@color/black333" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnSeasoningConfig"
android:layout_width="0dp"
android:layout_height="70dp"
android:layout_weight="1"
android:text="调料配置"
<TextView
android:id="@+id/tvCurrentMode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="当前:制作模式"
android:textSize="26sp"
android:background="@android:color/transparent" />
android:layout_marginStart="32dp"
android:textColor="@color/black999" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnFoodCollect"
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="20dp"
android:adjustViewBounds="true"
android:src="@drawable/ic_arrow_right2"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:id="@+id/llSeasoningConfig"
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="32dp"
android:background="@drawable/shape_white_e6_12_corners"
android:foreground="?android:attr/selectableItemBackground"
android:clipToOutline="true">
<TextView
android:layout_width="0dp"
android:layout_height="70dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="采集食材"
android:textSize="26sp"
android:background="@android:color/transparent" />
android:text="调料区设置"
android:textSize="32sp"
android:textStyle="bold"
android:layout_marginStart="32dp"
android:textColor="@color/black333" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="20dp"
android:adjustViewBounds="true"
android:src="@drawable/ic_arrow_right2"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:id="@+id/llFoodCollect"
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="32dp"
android:background="@drawable/shape_white_e6_12_corners"
android:foreground="?android:attr/selectableItemBackground"
android:clipToOutline="true">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="食材采样"
android:textSize="32sp"
android:textStyle="bold"
android:layout_marginStart="32dp"
android:textColor="@color/black333" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="20dp"
android:adjustViewBounds="true"
android:src="@drawable/ic_arrow_right2"
tools:ignore="ContentDescription" />
</LinearLayout>
<!-- &lt;!&ndash; 底部导航菜单 &ndash;&gt;-->
<!-- <LinearLayout-->
<!-- android:id="@+id/bottomMenu"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="horizontal"-->
<!-- android:background="@android:color/transparent"-->
<!-- android:layout_marginTop="10dp"-->
<!-- android:layout_marginBottom="10dp"-->
<!-- android:layout_marginStart="15dp"-->
<!-- android:layout_marginEnd="15dp"-->
<!-- android:elevation="8dp">-->
<!-- <androidx.appcompat.widget.AppCompatButton-->
<!-- android:id="@+id/btnDeviceRole"-->
<!-- android:id="@+id/btnDeviceConfig"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="70dp"-->
<!-- android:layout_weight="1"-->
<!-- android:text="设备角色:子设备"-->
<!-- android:textSize="22sp"-->
<!-- android:text="设备切换"-->
<!-- android:textSize="28sp"-->
<!-- android:textStyle="bold"-->
<!-- android:background="@android:color/transparent" />-->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnScaleMonitor"
android:layout_width="0dp"
android:layout_height="70dp"
android:layout_weight="1"
android:text="秤数据监控"
android:textSize="26sp"
android:visibility="visible"
android:background="@android:color/transparent" />
<!-- <androidx.appcompat.widget.AppCompatButton-->
<!-- android:id="@+id/btnSeasoningConfig"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="70dp"-->
<!-- android:layout_weight="1"-->
<!-- android:text="调料配置"-->
<!-- android:textSize="26sp"-->
<!-- android:background="@android:color/transparent" />-->
</LinearLayout>
<!-- <androidx.appcompat.widget.AppCompatButton-->
<!-- android:id="@+id/btnFoodCollect"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="70dp"-->
<!-- android:layout_weight="1"-->
<!-- android:text="采集食材"-->
<!-- android:textSize="26sp"-->
<!-- android:background="@android:color/transparent" />-->
<!-- Fragment 容器 -->
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- <androidx.appcompat.widget.AppCompatButton-->
<!-- android:id="@+id/btnScaleMonitor"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="70dp"-->
<!-- android:layout_weight="1"-->
<!-- android:text="秤数据监控"-->
<!-- android:textSize="26sp"-->
<!-- android:visibility="visible"-->
<!-- android:background="@android:color/transparent" />-->
<!-- </LinearLayout>-->
<!-- &lt;!&ndash; Fragment 容器 &ndash;&gt;-->
<!-- <FrameLayout-->
<!-- android:id="@+id/fragmentContainer"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="0dp"-->
<!-- android:layout_weight="1" />-->
</LinearLayout>
@@ -32,6 +32,8 @@
android:layout_width="match_parent"
android:layout_height="660dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="12dp"
android:overScrollMode="never" />
</LinearLayout>
@@ -98,7 +100,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:includeFontPadding="false"
android:text="千克"
tools:text="千克"
android:textColor="@color/black999"
android:textSize="32sp" />
</LinearLayout>
+49
View File
@@ -36,7 +36,9 @@
android:layout_height="1dp"
android:background="@color/gray_eb" />
<!-- 2个按钮时显示(水平排列) -->
<LinearLayout
android:id="@+id/layoutButtonsHorizontal"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center_vertical"
@@ -69,4 +71,51 @@
android:textStyle="bold" />
</LinearLayout>
<!-- 3个按钮时显示(垂直排列,iOS风格) -->
<LinearLayout
android:id="@+id/layoutButtonsVertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/btnTop"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:background="@drawable/ripple_effect_light2"
android:textColor="@color/dish_green"
android:textSize="30sp" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/gray_eb" />
<TextView
android:id="@+id/btnMiddle"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:background="@drawable/ripple_effect_light2"
android:textColor="@color/dish_green"
android:textSize="30sp" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/gray_eb" />
<TextView
android:id="@+id/btnBottom"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:background="@drawable/ripple_effect_light2"
android:textColor="@color/dish_green"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
@@ -8,9 +8,9 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:paddingBottom="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:paddingBottom="10dp"
android:layout_marginTop="10dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
@@ -94,6 +94,8 @@
android:background="@drawable/shape_scale_cell"
android:gravity="center"
android:text="-"
android:foreground="?android:attr/selectableItemBackground"
android:clipToOutline="true"
android:textColor="@color/home_title"
android:textSize="28sp" />
@@ -4,8 +4,10 @@
android:layout_height="60dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
android:background="@drawable/shape_scale_cell"
android:paddingHorizontal="16dp"
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="4dp">
<TextView
android:id="@+id/tvSeasoningName"
@@ -6,7 +6,9 @@
android:gravity="center"
android:orientation="vertical"
android:padding="4dp"
android:background="@drawable/shape_scale_cell">
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/shape_scale_cell"
android:clipToOutline="true">
<TextView
android:id="@+id/tvAddress"
@@ -3,13 +3,15 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="340dp"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/shape_scale_cell"
android:gravity="center"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
android:paddingBottom="8dp"
android:clipToOutline="true">
<!-- 秤编号 -->
<TextView
@@ -34,6 +34,8 @@
android:gravity="center"
android:layout_marginTop="5dp"
android:layout_marginBottom="2dp"
android:clipToOutline="true"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/shape_scale_cell">
<TextView
@@ -29,6 +29,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal"
android:layout_marginTop="5dp">