5 Commits
Author SHA1 Message Date
lvmeng a2f8fb903a refactor(http): 移除旧的HTTP客户端配置并整合到网络管理器中
- 删除了废弃的HttpClient.kt文件中的自定义HTTP客户端实现
- 将基础URL配置从UrlConfig迁移到GlobalData统一管理
- 更新HttpManager使用GlobalData.appBaseUrl作为基础URL
- 移除了HttpUtil.kt文件中的遗留HTTP工具类
- 简化了网络层架构,移除重复的配置类
2026-04-24 17:23:27 +08:00
lvmengandClaude Sonnet 4.6 13fc7c7e0f fix(ui): 修复22格高宽比调整后列表高度显示不全
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:14:19 +08:00
lvmeng fed161b088 style(adapter): 更新网格适配器高亮样式
- 将 setBackgroundColor 替换为 setBackgroundResource 使用形状资源
- 为 Seasoning18GridAdapter 和 Seasoning22GridAdapter 统一高亮样式
- 添加 shape_scale_cell_highlight.xml 定义圆角橙色背景
- 移除硬编码颜色值,提高样式可维护性
2026-04-24 16:23:15 +08:00
lvmengandClaude Sonnet 4.6 594c551a98 fix(activity): 修复调料配置删除后子设备未同步问题
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:28:59 +08:00
lvmeng 617ccfb456 feat(dialog): 调料选择对话框增加滑动冲突处理和重量传递功能
- 修改 dialog_seasoning_select.xml 布局高度为 match_parent 并添加标题文本
- 调整 SmartRefreshLayout 高度限制并移除 RecyclerView 内边距
- 在 FoodRecognizeActivity 中新增 currentWeight 参数传递机制
- 移除 WeightUtil 监听器相关代码并初始化当前重量值
- 在 PrepareCookActivity 中传递当前重量到识别页面
- 从 SeasoningConfigFragment 和 SeasoningSelectDialog 中移除无用参数
- 添加 Window.Callback 触摸事件拦截解决 RecyclerView 滑动冲突
- 实现触摸点位置检测动态控制 BottomSheet 拖拽行为
2026-04-24 10:07:11 +08:00
18 changed files with 339 additions and 426 deletions
@@ -93,7 +93,7 @@ class Seasoning18GridAdapter(
holder.b.tvState.text = stateStr(item.state)
}
if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell_highlight)
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddress.setTextColor(white)
holder.b.tvName.setTextColor(white)
@@ -15,21 +15,24 @@ 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.SizeTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
/**
* 22格网格 Adapter,同时服务于调料配置页和主设备秤总览页。
*
* @param largeSize 前4个大格子的尺寸px
* @param smallSize 其余小格子的尺寸px
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState
* false = 秤总览模式,三行全部显示
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
* @param largeSize 前4个大格子的宽度px
* @param smallSize 其余小格子的宽度px
* @param cellAspectRatio 格子高宽比(height / width
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState
* false = 秤总览模式,三行全部显示
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
*/
class Seasoning22GridAdapter(
private val largeSize: Int,
private val smallSize: Int,
private val cellAspectRatio: Float = SizeTool.DEFAULT_CELL_ASPECT_RATIO,
private val showNameOnly: Boolean = false,
private val showAddress: Boolean = false
) : BaseQuickAdapter<ScaleData, Seasoning22GridAdapter.VH>(mutableListOf()) {
@@ -51,13 +54,14 @@ 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
val cellWidth = if (viewType == 1) largeSize else smallSize
val cellHeight = (cellWidth * cellAspectRatio).toInt()
b.root.updateLayoutParams<FlexboxLayoutManager.LayoutParams> {
width = cellSize
height = cellSize
width = cellWidth
height = cellHeight
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
val baseSp = (cellWidth / parent.resources.displayMetrics.density).toInt()
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)
@@ -95,7 +99,7 @@ class Seasoning22GridAdapter(
}
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell_highlight)
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddress.setTextColor(white)
holder.b.tvName.setTextColor(white)
@@ -137,6 +137,23 @@ class DbRepository {
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().upsertAll(slots)
}
/**
* 删除指定设备的指定槽位配置
* @param deviceId 目标设备 ID
* @param address 秤硬件地址
*/
suspend fun deleteSeasoningSlot(deviceId: String, address: Int) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().deleteByDeviceIdAndAddress(deviceId, address)
}
/**
* 删除指定设备的全部槽位配置
* @param deviceId 目标设备 ID
*/
suspend fun deleteAllSlotsByDeviceId(deviceId: String) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().deleteAllByDeviceId(deviceId)
}
}
//
//class SeasoningRepository(val seasoningDao: SeasoningDao) {
@@ -222,6 +222,21 @@ class DbViewModel : ViewModel() {
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) =
rep.upsertAllSeasoningSlots(slots)
/**
* 删除指定设备的指定槽位配置
* @param deviceId 目标设备 ID
* @param address 秤硬件地址
*/
suspend fun deleteSeasoningSlot(deviceId: String, address: Int) =
rep.deleteSeasoningSlot(deviceId, address)
/**
* 删除指定设备的全部槽位配置(子设备接收全量广播时先清空再写入)
* @param deviceId 目标设备 ID
*/
suspend fun deleteAllSlotsByDeviceId(deviceId: String) =
rep.deleteAllSlotsByDeviceId(deviceId)
// fun updateAll(list: MutableList<SeasoningEntity>) {
// viewModelScope.launch {
// list.forEach {
@@ -37,4 +37,19 @@ interface SeasoningSlotDao {
*/
@Query("SELECT * FROM dm_seasoning_slot")
suspend fun queryAll(): List<SeasoningSlotEntity>
/**
* 删除指定设备的指定槽位配置
* @param deviceId 目标设备 ID
* @param address 秤硬件地址
*/
@Query("DELETE FROM dm_seasoning_slot WHERE deviceId = :deviceId AND address = :address")
suspend fun deleteByDeviceIdAndAddress(deviceId: String, address: Int)
/**
* 删除指定设备的全部槽位配置(子设备接收全量广播时先清空再写入)
* @param deviceId 目标设备 ID
*/
@Query("DELETE FROM dm_seasoning_slot WHERE deviceId = :deviceId")
suspend fun deleteAllByDeviceId(deviceId: String)
}
@@ -1,17 +1,18 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.graphics.Rect
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.core.view.isEmpty
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.SeasoningSearchAdapter
@@ -30,7 +31,6 @@ import com.shuwei.dish.match.utils.ext.toast
*/
class SeasoningSelectDialog(
private val activity: BaseActivity,
private val clickIndex: Int,
private val onItemSelected: (item: SeasoningEntity) -> Unit
) : BottomSheetDialog(activity, R.style.BottomSheet) {
@@ -84,18 +84,10 @@ class SeasoningSelectDialog(
addOnActionSearchListener { searchGoods(this) }
}
// RecyclerView 初始化及滑动冲突处理
// RecyclerView 初始化
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 3, GridLayoutManager.VERTICAL, false)
adapter = this@SeasoningSelectDialog.adapter
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(rv, dx, dy)
// 解决 RecyclerView 与 SmartRefreshLayout 滑动冲突
val topRowVerticalPosition = if (rv.isEmpty()) 0 else rv.getChildAt(0).top
binding.refreshLayout.setNestedScrollingEnabled(topRowVerticalPosition >= 0)
}
})
}
binding.refreshLayout.run {
@@ -109,11 +101,32 @@ class SeasoningSelectDialog(
binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) }
}
/** 弹窗显示时自动加载默认调料列表 */
/** 弹窗显示时自动加载默认调料列表,并在 Window.Callback 层提前拦截触摸以解决滑动冲突 */
override fun show() {
super.show()
pageNo = 1
getGoodsList()
// Window.Callback.dispatchTouchEvent 在整个 View 树的 onInterceptTouchEvent 之前执行,
// 是最早能感知触摸的时机。在此处判断触摸点位置来控制 behavior.isDraggable
// 从而确保 BottomSheetBehavior 在 CoordinatorLayout.onInterceptTouchEvent 时
// 已经是禁用状态,不会拦截列表区域的滑动手势。
val originalCallback = window?.callback ?: return
window?.callback = object : Window.Callback by originalCallback {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val rect = Rect()
binding.refreshLayout.getGlobalVisibleRect(rect)
// 触摸点落在列表区域内则禁止 BottomSheet 拖拽
behavior.isDraggable = !rect.contains(event.rawX.toInt(), event.rawY.toInt())
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
behavior.isDraggable = true
}
}
return originalCallback.dispatchTouchEvent(event)
}
}
}
/**
@@ -1,70 +0,0 @@
//package com.shuwei.dish.match.http
//
//import android.annotation.SuppressLint
//import android.util.Log
//import okhttp3.Call
//import okhttp3.Interceptor
//import okhttp3.OkHttpClient
//import okhttp3.Request
//import okhttp3.Response
//import java.util.concurrent.TimeUnit
//
//import javax.net.ssl.*
//import java.security.SecureRandom
//import java.security.cert.X509Certificate
//
//class HttpClient private constructor() {
// private val client: OkHttpClient by lazy {
// OkHttpClient.Builder()
// .apply {
// connectTimeout(15, TimeUnit.SECONDS)
// readTimeout(30, TimeUnit.SECONDS)
// writeTimeout(15, TimeUnit.SECONDS)
// sslSocketFactory(createSSLSocketFactory(), TrustAllCerts())
// hostnameVerifier { _, _ -> true }
// //if (BuildConfig.Debug) {
// addInterceptor(LoggingInterceptor())
// // }
// }
// .build()
// }
//
// companion object {
// val instance by lazy { HttpClient() }
// }
//
// fun newCall(request: Request): Call = client.newCall(request)
//
// inner class LoggingInterceptor : Interceptor {
// override fun intercept(chain: Interceptor.Chain): Response {
// val request = chain.request()
// // 打印请求日志
// Log.d("OkHttp", "--> ${request.method} ${request.url}")
//
// val response = chain.proceed(request)
// // 打印响应日志
// Log.d("OkHttp", "<-- ${response.code} ${response.request.url}")
//
// return response
// }
// }
//
// // 信任所有证书的TrustManager实现
// @SuppressLint("CustomX509TrustManager")
// class TrustAllCerts : X509TrustManager {
// @SuppressLint("TrustAllX509TrustManager")
// override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
// @SuppressLint("TrustAllX509TrustManager")
// override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
// override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
// }
//
// // 创建信任所有证书的SSLSocketFactory
// fun createSSLSocketFactory(): SSLSocketFactory {
// val sslContext = SSLContext.getInstance("TLS").apply {
// init(null, arrayOf(TrustAllCerts()), SecureRandom())
// }
// return sslContext.socketFactory
// }
//
//}
@@ -1,199 +0,0 @@
//package com.shuwei.dish.match.http
//
//import android.os.Handler
//import android.os.Looper
//import android.util.Log
//import com.google.gson.reflect.TypeToken
//import com.shuwei.dish.match.base.BaseApp
//import com.shuwei.dish.match.base.BaseReq
//import com.shuwei.dish.match.utils.SpTool
//import com.shuwei.dish.match.utils.ext.toType
//import okhttp3.Call
//import okhttp3.Callback
//import okhttp3.MediaType.Companion.toMediaTypeOrNull
//import okhttp3.Request
//import okhttp3.RequestBody.Companion.toRequestBody
//import okhttp3.Response
//import java.io.IOException
//import java.util.concurrent.Executors
//import java.util.concurrent.TimeUnit
//
//
//object HttpUtil {
//
// var loopGetToken = true
//
// fun runMainThread(action: () -> Unit) {
// Handler(Looper.getMainLooper()).post {
// action()
// }
// }
//
// // GET请求(HTTPS
// fun get(
// url: String,
// header: Map<String, String>?=null,
// doSuccess: (data: Any) -> Unit,
// doFailure: (code: Int?, msg: String?) -> Unit
// ) {
// val request = Request.Builder()
// .url(if (url.isHttpApi()) url else UrlConfig.BASE_URL + url)
// .apply {
// if (header != null) {
// header.forEach {
// addHeader(it.key, it.value)
// }
// } else {
// val token = getToken()
// if (token.isNotBlank()) {
// addHeader("X-Access-Token", token)
// }
// }
// }
// .get()
// .build()
// HttpClient.instance.newCall(request).enqueue(CallbackImpl(doSuccess, doFailure))
// }
////
//// fun get(
//// isHttps: Boolean = false,
//// host: String = "vip.shuziweidao.com",
//// pathSegmentList: List<String>,
//// queryParams: Map<String, String>,
//// doSuccess: (data: Any) -> Unit,
//// doFailure: (code: Int?, msg: String?) -> Unit
//// ) {
//// val token = getToken()
//// val url = HttpUrl.Builder()
//// .scheme(if (isHttps) "https" else "http")
//// .host(host)
//// .apply {
//// pathSegmentList.forEach { addPathSegment(it) }
//// queryParams.forEach { (key, value) -> addQueryParameter(key, value) }
//// }
//// .build()
//// val request = Request.Builder()
//// .url(url)
//// .apply {
//// if (token.isNotBlank()) {
//// addHeader("Authorization", token)
//// }
//// }
//// .get()
//// .build()
//// HttpClient.instance.newCall(request).enqueue(CallbackImpl(doSuccess, doFailure))
//// }
//
// // POST表单(HTTPS
//// fun postForm(url: String, params: Map<String, String>, callback: Callback) {
//// val formBody = FormBody.Builder().apply {
//// params.forEach { (k, v) -> add(k, v) }
//// }.build()
////
//// Request.Builder()
//// .url(url)
//// .post(formBody)
//// .build().let { HttpClient.instance.newCall(it).enqueue(callback) }
//// }
//
// // POST JSONHTTPS
// fun postJson(
// url: String,
// json: String,
// doSuccess: (data: Any) -> Unit,
// doFailure: (code: Int?, msg: String?) -> Unit
// ) {
// val body = json
// .toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
// val token = getToken()
// Request.Builder()
// .url(if (url.isHttpApi()) url else UrlConfig.BASE_URL + url)
// .apply {
// if (token.isNotBlank()) {
// addHeader("X-Access-Token", token)
// }
// }
// .post(body)
// .build()
// .let { HttpClient.instance.newCall(it).enqueue(CallbackImpl(doSuccess, doFailure)) }
// }
//
//
// val executor = Executors.newSingleThreadScheduledExecutor()
// var getTokenIsRunning = false
// private fun exeTokenThread() {
// if (getTokenIsRunning.not()) {
// executor.scheduleWithFixedDelay(
// task, 0, 1, TimeUnit.MINUTES
// )
// getTokenIsRunning = true
// }
// }
//
// val task = Runnable {
// if (BaseApp.token.isNullOrBlank()) {
// getAppToken()
// } else {
// executor.shutdown()
// }
// }
//
// fun getToken() = SpTool.getString(SpTool.TOKEN)
////BaseApp.instance?.token ?:""
////BaseApp.getSharedPref()?.getString("token", "") ?: ""
//
// fun getAppToken() {
// val url = "${UrlConfig.GET_TOKEN}?qrcodeId=${SpTool.getString(SpTool.DEVICE_ID)}"
//// Log.d(TAG, "getToken: url = $url")
// get(url = url, doSuccess = {
// SpTool.put(SpTool.TOKEN, it)
// BaseApp.token = it.toString()
// }) { code, msg ->
// //toast(msg)
// }
// }
//
// class CallbackImpl(
// private val doSuccess: (data: Any) -> Unit,
// private val doFailure: (code: Int?, msg: String?) -> Unit
// ) :
// Callback {
// override fun onFailure(call: Call, e: IOException) {
// runMainThread {
// e.printStackTrace()
// doFailure(-1, "服务异常,${e.message}")
// }
// }
//
// override fun onResponse(call: Call, response: Response) {
// val respData = response.body?.string()
// runMainThread {
// Log.d("HttpUtil", respData ?: "")
// runCatching {
// val typeToken = object : TypeToken<BaseReq<Any>>() {}
// val baseReq = respData?.toType<BaseReq<Any>>(typeToken = typeToken)
// if (baseReq == null) {
// doFailure(-1, "查询数据失败")
// return@runCatching
// }
// if (baseReq.code != 200) {
// doFailure(baseReq.code, baseReq.message)
// //baseReq.code == 500 ||
// if (baseReq.code == 401 && loopGetToken) {
// exeTokenThread()
// }
// return@runCatching
// }
// doSuccess(baseReq.result ?: "")
// }.onFailure {
// it.printStackTrace()
// doFailure(-1, "解析异常")
// }
// }
// }
//
// }
//
//}
//
//fun String.isHttpApi() = this.startsWith("http://") || this.startsWith("https://")
@@ -1,37 +0,0 @@
package com.shuwei.dish.match.http
//import com.shuwei.dish.match.base.BaseApp
object UrlConfig {
// private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889"
// /**
// * device获取token
// */
// const val DEVICE_TOKEN = "${DEVICE_BASE_URL}/sys/getEquipmentToken"
// /**
// *获取配置信息
// */
// const val DEVICE_CONFIG = "${DEVICE_BASE_URL}/equipment/stEquipment/queryByEquipmentCode"
// const val BASE_URL = "http://vip.shuziweidao.com/shuwei-zhct"
// const val DISH_DETAIL = "$BASE_URL/scales/goodsUseList?foodId={foodId}&foodWeight={foodWeight}"
// const val DISH_LIST = "$BASE_URL/scales/getRestInfoFoods?eaId=99&type=0&foodName"
// const val BASE_URL = "http://192.168.1.207:9102"
// const val BASE_URL = "https://yyjk.shuziweidao.com/gateway"
const val BASE_URL = "https://yyjk.shuziweidao.com/gateway/"
// const val DISH_LIST = "$BASE_URL/scales/getRestInfoFoods?eaId=99&type=0&foodName"
// var DISH_DETAIL = "${BaseApp.configUrl}/food/stFoodInfoMatching/queryById"
//
// // "$BASE_URL/scales/generateToken?deviceId=1111111111111111111111111111"
//
// var GET_TOKEN = "${BaseApp.configUrl}/restaurant/equipment/stEquipment/getEquipmentToken"
// var SUBMIT_DISH = "${BaseApp.configUrl}/food/stFoodInfoMatching/saveoredit"
//
// var QUERY_GOODS_LIST = "${BaseApp.configUrl}/food/stFoodInfoMatching/queryGoodsInfoList"
// var QUERY_FOOD_LIST = "${BaseApp.configUrl}/food/stFoodInfoMatching/list"
// var SAMPLING_LIST = "${BaseApp.configUrl}/food/stFoodInfoMatching/queryHistorical/goodsInfoList"
}
@@ -2,7 +2,7 @@ package com.shuwei.dish.match.net
import android.annotation.SuppressLint
import android.util.Log
import com.shuwei.dish.match.http.UrlConfig
import com.shuwei.dish.match.base.GlobalData
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
@@ -16,7 +16,7 @@ import kotlin.apply
import kotlin.jvm.java
val apiService: ApiService = Retrofit.Builder()
.baseUrl(UrlConfig.BASE_URL)
.baseUrl(GlobalData.appBaseUrl)
// .baseUrl(UrlConfig.DEVICE_BASE_URL)
.client(HttpManager.instance.client)
.addConverterFactory(GsonConverterFactory.create())
@@ -12,7 +12,6 @@ import com.shuwei.dish.match.databinding.ActivityFoodRecognizeBinding
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.utils.ActivityManager
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
@@ -28,6 +27,7 @@ class FoodRecognizeActivity : BaseActivity() {
companion object {
private const val TAG = "FoodRecognizeActivity"
private const val EXTRA_IMAGE_URI = "extra_image_uri"
private const val EXTRA_CURRENT_WEIGHT = "extra_current_weight"
const val EXTRA_SELECTED_ITEM = "extra_selected_item"
const val IS_MANUAL_CANCEL = "isManualCancel"
@@ -44,12 +44,14 @@ class FoodRecognizeActivity : BaseActivity() {
fun start(
activity: BaseActivity,
imageUri: String? = null,
currentWeight: Double = 0.0,
goodsList: ArrayList<CookFoodGoodsEntity> = arrayListOf(),
finishCallback: (Boolean, CookFoodGoodsEntity?) -> Unit = { _, _ -> }
) {
pendingGoodsList = goodsList
val launchIntent = Intent(activity, FoodRecognizeActivity::class.java).apply {
putExtra(EXTRA_IMAGE_URI, imageUri)
putExtra(EXTRA_CURRENT_WEIGHT, currentWeight)
}
activity.startActivity(launchIntent) { resultIntent ->
val isManualCancel = resultIntent?.getBooleanExtra(IS_MANUAL_CANCEL, false) ?: false
@@ -145,6 +147,8 @@ class FoodRecognizeActivity : BaseActivity() {
if (!imageUri.isNullOrBlank()) {
binding.ivFoodPhoto.setImageURI(imageUri.toUri())
}
currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0)
binding.tvWeight.text = "${currentWeight}g"
// 取出内存缓存的食材列表,取用后立即清空
val goodsList = pendingGoodsList?.also { pendingGoodsList = null }
@@ -153,6 +157,7 @@ class FoodRecognizeActivity : BaseActivity() {
// 默认选中第一项
selectedPosition = 0
list[0].isClicked = true
list[0].useWeight = currentWeight
binding.tvSelectedFood.text = list[0].goodsName ?: "-"
}
}
@@ -222,40 +227,40 @@ class FoodRecognizeActivity : BaseActivity() {
* 低于阈值说明食材已被取走,保留上次有效值等待页面关闭
*/
private fun registerWeightListener() {
WeightUtil.addWeightListener(TAG) { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread {
currentWeight = weight
binding.tvWeight.run {
if (tag != weight) {
text = "$weight g"
tag = weight
}
}
if (weight >= PrepareCookActivity.WEIGHT_RECOGNIZE_VALUE) {
adapter.items.firstOrNull { it.isClicked }?.let { item ->
if (pendingWeight != weight) {
// 无论增减,均延迟更新,避免手拿物品时的瞬时增重干扰
cancelPendingWeightUpdate()
pendingWeight = weight
pendingWeightRunnable = Runnable {
item.useWeight = weight
pendingWeightRunnable = null
}.also { handler.postDelayed(it, 3000) }
}
}
}
}
}
}
// WeightUtil.addWeightListener(TAG) { address, state, weight ->
// if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
// runOnUiThread {
// currentWeight = weight
// binding.tvWeight.run {
// if (tag != weight) {
// text = "$weight g"
// tag = weight
// }
// }
// if (weight >= PrepareCookActivity.WEIGHT_RECOGNIZE_VALUE) {
// adapter.items.firstOrNull { it.isClicked }?.let { item ->
// if (pendingWeight != weight) {
// // 无论增减,均延迟更新,避免手拿物品时的瞬时增重干扰
// cancelPendingWeightUpdate()
// pendingWeight = weight
// pendingWeightRunnable = Runnable {
// item.useWeight = weight
// pendingWeightRunnable = null
// }.also { handler.postDelayed(it, 3000) }
// }
// }
// }
// }
// }
// }
}
/**
* 取消待执行的延迟重量更新任务
*/
private fun cancelPendingWeightUpdate() {
pendingWeightRunnable?.let { handler.removeCallbacks(it) }
pendingWeightRunnable = null
// pendingWeightRunnable?.let { handler.removeCallbacks(it) }
// pendingWeightRunnable = null
}
/**
@@ -196,6 +196,7 @@ class PrepareCookActivity : BaseActivity() {
if (weight < WEIGHT_RECOGNIZE_VALUE) {
showRecognizePage = false
manualCancelFlag = false
lastWeight = weight
FoodRecognizeActivity.close()
return
}
@@ -515,9 +516,11 @@ class PrepareCookActivity : BaseActivity() {
}
)
}
lastWeight = currentWeight
FoodRecognizeActivity.start(
activity = this,
imageUri = lastPhotoUri?.toString(),
currentWeight = currentWeight,
goodsList = ArrayList(foodList),
finishCallback = { isManualCancel, entity ->
Log.d(TAG, "queryFood: isManualCancel=$isManualCancel, entity=${entity.toJsonString()}")
@@ -100,7 +100,11 @@ class SlaveActivity : BaseActivity() {
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.FLEX_START
}
scale22Adapter = Seasoning22GridAdapter(size.largeSize, size.smallSize, showNameOnly = false).also {
scale22Adapter = Seasoning22GridAdapter(
size.largeSize,
size.smallSize,
showNameOnly = false
).also {
it.onItemClick = { scale, _ -> showTareDialog(scale.address) }
}
binding.rvScaleList.adapter = scale22Adapter
@@ -158,7 +162,7 @@ class SlaveActivity : BaseActivity() {
/**
* 监听主设备下发的调料配置同步事件
* 收到后更新 adapter 并写入本地 Room
* 收到后以全量替换方式更新本机 DB(先删后插),再刷新 UI
*/
private fun listenSeasoningConfig() {
ScaleServiceManager.onSeasoningConfig = onSeasoningConfig@{ event ->
@@ -167,6 +171,8 @@ class SlaveActivity : BaseActivity() {
?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) }
?: return@onSeasoningConfig
lifecycleScope.launch {
// 全量替换:先删除本机所有旧槽位,再写入最新配置
appViewModel.deleteAllSlotsByDeviceId(GlobalData.deviceId)
appViewModel.upsertAllSeasoningSlots(mySlots)
applySlots(mySlots)
}
@@ -175,9 +181,38 @@ class SlaveActivity : BaseActivity() {
/**
* 将槽位配置应用到对应 adapter(按地址匹配格子位置)
* 同时新 slotNameMap,确保后续重量回调构造 ScaleData 时携带正确名称
* 同时以传入列表为准全量刷新 slotNameMap
* - 新列表中存在的地址:更新名称
* - 新列表中不存在的地址:从 slotNameMap 移除,并清空对应格子名称
*/
private fun applySlots(slots: List<SeasoningSlotEntity>) {
val newAddresses = slots.map { it.address }.toSet()
// 清除已被删除的槽位:从 slotNameMap 移除并清空 adapter 对应格子
val removedAddresses = slotNameMap.keys.filter { it !in newAddresses }
removedAddresses.forEach { addr ->
slotNameMap.remove(addr)
when {
scale22Adapter != null -> {
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(addr)
if (pos >= 0) scale22Adapter!!.updateItemName(pos, "")
}
scale18Adapter != null -> {
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(addr)
if (pos >= 0) scale18Adapter!!.updateItemName(pos, "")
}
else -> {
val list = linearAdapter.items.toMutableList()
val idx = list.indexOfFirst { it.address == addr }
if (idx >= 0) {
list[idx] = list[idx].copy(name = "")
linearAdapter.submitList(list)
}
}
}
}
// 更新新列表中存在的槽位
slots.forEach { slot ->
slotNameMap[slot.address] = slot.goodsName
when {
@@ -190,7 +225,6 @@ class SlaveActivity : BaseActivity() {
if (pos >= 0) scale18Adapter!!.updateItemName(pos, slot.goodsName)
}
else -> {
// 线性列表模式(子设备1):刷新已显示条目的名称
val list = linearAdapter.items.toMutableList()
val idx = list.indexOfFirst { it.address == slot.address }
if (idx >= 0) {
@@ -290,7 +324,7 @@ class SlaveActivity : BaseActivity() {
/** 重量增加阈值(克),超过此值才触发高亮 */
private const val WEIGHT_DELTA_THRESHOLD = 10.0
/** 高亮自动恢复时间(毫秒) */
private const val HIGHLIGHT_DURATION_MS = 15_000L
private const val HIGHLIGHT_DURATION_MS = 5_000L
}
/**
@@ -17,8 +17,8 @@ import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.SeasoningSelectDialog
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.SeasoningSlotEntity
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleEvent
@@ -39,7 +39,7 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
private const val WEIGHT_DELTA_THRESHOLD = 10.0
/** 高亮自动恢复时间(毫秒) */
private const val HIGHLIGHT_DURATION_MS = 15_000L
private const val HIGHLIGHT_DURATION_MS = 5_000L
}
override fun inflateBinding(
@@ -80,82 +80,164 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
/** 绑定油盆格子点击,弹出调料选择弹窗并保存到 DEVICE_ID_1 */
private fun bindOilSlotClick() {
binding.tvOilSlot.setOnClickListener {
openSeasoningSelectDialog(0) { item ->
binding.tvOilSlot.text = item.goodsName ?: "-"
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_1,
address = 1,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
)
}
handleSlotClick(
currentName = binding.tvOilSlot.text.toString().takeIf { it != "-" },
onSelect = {
selectAndSave(ScaleDeviceConfig.DEVICE_ID_1, 1) { name ->
binding.tvOilSlot.text = name.ifBlank { "-" }
}
},
onClear = {
binding.tvOilSlot.text = "-"
deleteSlotAndBroadcast(ScaleDeviceConfig.DEVICE_ID_1, 1)
}
)
}
}
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
private fun bindAdapterClicks() {
adapter22.onItemClick = { scaleData, position ->
openSeasoningSelectDialog(position) { item ->
adapter22.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_22,
address = scaleData.address,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
bindSlotAdapterClick(
adapter = adapter22,
deviceId = ScaleDeviceConfig.DEVICE_ID_22
)
bindSlotAdapterClick(
adapter = adapter18,
deviceId = ScaleDeviceConfig.DEVICE_ID_18
)
}
/** 为指定 Adapter 绑定格子点击逻辑(已配置弹操作弹窗,未配置直接选择) */
private fun bindSlotAdapterClick(
adapter: Any,
deviceId: String
) {
when (adapter) {
is Seasoning22GridAdapter -> adapter.onItemClick = { scaleData, position ->
handleSlotClick(
currentName = scaleData.name,
onSelect = {
selectAndSave(deviceId, scaleData.address) { name ->
adapter.updateItemName(position, name)
}
},
onClear = {
adapter.updateItemName(position, "")
deleteSlotAndBroadcast(deviceId, scaleData.address)
}
)
}
is Seasoning18GridAdapter -> adapter.onItemClick = { scaleData, position ->
handleSlotClick(
currentName = scaleData.name,
onSelect = {
selectAndSave(deviceId, scaleData.address) { name ->
adapter.updateItemName(position, name)
}
},
onClear = {
adapter.updateItemName(position, "")
deleteSlotAndBroadcast(deviceId, scaleData.address)
}
)
}
}
adapter18.onItemClick = { scaleData, position ->
openSeasoningSelectDialog(position) { item ->
adapter18.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_18,
address = scaleData.address,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
)
}
}
/**
* 格子点击分发:已配置则弹操作弹窗,未配置直接执行选择
* @param currentName 格子当前调料名称
* @param onSelect 执行选择(新增或重新选择)
* @param onClear 执行清除
*/
private fun handleSlotClick(
currentName: String?,
onSelect: () -> Unit,
onClear: () -> Unit
) {
if (!currentName.isNullOrBlank()) {
showSlotActionDialog(goodsName = currentName, onReselect = onSelect, onClear = onClear)
} else {
onSelect()
}
}
/**
* 打开调料选择弹窗
*
* @param position 格子位置
* @param callback 调料选择回调
*/
private fun openSeasoningSelectDialog(
position: Int,
callback: (item: SeasoningEntity) -> Unit
/**
* 打开选择弹窗,选中后更新 UI 并保存槽位到 Room 广播
* @param deviceId 目标设备 ID
* @param address 秤硬件地址
* @param onUpdateName 选中后更新格子显示名称的回调
*/
private fun selectAndSave(
deviceId: String,
address: Int,
onUpdateName: (String) -> Unit
) {
SeasoningSelectDialog(
activity = currentActivity,
clickIndex = position,
onItemSelected = callback
onItemSelected = { item ->
val name = item.goodsName ?: ""
onUpdateName(name)
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = deviceId,
address = address,
goodsId = item.goodsId,
goodsName = name
)
)
}
).show()
}
/**
* 将单个槽位写入 Room,再读取全量配置广播给子设备
* 仅主设备执行广播,子设备不调用此方法
*/
/** 将单个槽位写入 Room,再广播全量配置给子设备 */
private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) {
lifecycleScope.launch {
currentActivity.appViewModel.upsertSeasoningSlot(slot)
// 读取全量配置,通过 wsClient 推送给所有已连接子设备
val allSlots = currentActivity.appViewModel.loadSeasoningSlot().map {
SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName)
}
ScaleServiceManager.sendSeasoningConfig(allSlots)
broadcastAllSlots()
}
}
/**
* 弹出已配置格子的操作选择弹窗(3按钮)
* @param goodsName 当前格子已配置的调料名称
* @param onReselect 用户点击「重新选择」后的回调
* @param onClear 用户点击「清除配置」后的回调
*/
private fun showSlotActionDialog(
goodsName: String,
onReselect: () -> Unit,
onClear: () -> Unit
) {
CommonDialog(currentActivity)
.setTitle("调料操作")
.setContent("当前配置:$goodsName")
.setNegativeButton("取消")
.setNeutralButton("重新选择") { onReselect() }
.setPositiveButton("清除配置") { onClear() }
.show()
}
/** 从 Room 删除指定槽位,再广播全量配置给子设备 */
private fun deleteSlotAndBroadcast(deviceId: String, address: Int) {
lifecycleScope.launch {
currentActivity.appViewModel.deleteSeasoningSlot(deviceId, address)
broadcastAllSlots()
}
}
/** 读取全量槽位配置并通过 wsClient 推送给所有已连接子设备 */
private suspend fun broadcastAllSlots() {
val allSlots = currentActivity.appViewModel.loadSeasoningSlot().map {
SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName)
}
ScaleServiceManager.sendSeasoningConfig(allSlots)
}
// /** 初始化 ViewModel */
// private fun initViewModel() {
// val factory = AppFactory(DbRepository(BaseApp.instance!!.database.appDao()))
@@ -2,6 +2,7 @@ package com.shuwei.dish.match.utils
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.utils.ext.dp
import kotlin.math.max
data class FlexLayoutSize(
val smallSize: Int,
@@ -11,14 +12,28 @@ data class FlexLayoutSize(
object SizeTool {
fun getFlexLayoutSize(leftRightSpace: Int = 10.dp): FlexLayoutSize {
const val DEFAULT_CELL_ASPECT_RATIO = 1.26f
fun getFlexLayoutSize(
leftRightSpace: Int,
cellAspectRatio: Float = DEFAULT_CELL_ASPECT_RATIO,
cellMargin: Int = 2.dp
): FlexLayoutSize {
// 上层:FlexboxLayoutManager,仿 MasterScaleActivity 中 Scale22VH 的布局方式
// rvTop 高度 = 小格子尺寸 * 3行 + 3 * 间距,与 Scale22VH 保持一致
// rvTop 高度需覆盖:
// 1) 小格列(3行)总高度
// 2) 大格列(2行)总高度(考虑四舍五入误差)
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
val smallHeight = (smallSize * cellAspectRatio).toInt()
val largeHeight = (largeSize * cellAspectRatio).toInt()
val smallColumnHeight = smallHeight * 3 + cellMargin * 6
val largeColumnHeight = largeHeight * 2 + cellMargin * 4
val rvHeight = max(smallColumnHeight, largeColumnHeight)
return FlexLayoutSize(smallSize, largeSize, rvHeight)
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 秤格子背景:白色底 + 灰色描边 + 圆角,与页面蓝色背景形成对比 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FA9523" />
<corners android:radius="8dp" />
</shape>
@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:gravity="center_horizontal"
tools:background="@color/white"
android:orientation="vertical">
@@ -15,6 +15,16 @@
android:layout_marginTop="30dp"
android:background="@drawable/shape_gray_dc_5" />
<TextView
android:id="@+id/tvSheetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textColor="@color/black333"
android:textSize="36sp"
android:textStyle="bold"
android:text="调料检索" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="100dp"
@@ -57,8 +67,10 @@
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="700dp"
android:layout_marginTop="10dp"
android:layout_marginStart="45dp"
android:layout_marginEnd="45dp"
app:srlEnableOverScrollDrag="false">
<com.scwang.smart.refresh.header.ClassicsHeader
@@ -69,11 +81,8 @@
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="45dp"
android:layout_marginEnd="45dp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:minHeight="380dp"
android:nestedScrollingEnabled="true"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+1 -1
View File
@@ -29,7 +29,7 @@
<LinearLayout
android:id="@+id/llScaleContainer"
android:layout_width="match_parent"
android:layout_height="260dp"
android:layout_height="200dp"
android:orientation="vertical"
android:gravity="center"
android:layout_marginTop="5dp"