Compare commits
9
Commits
0648f458a7
...
3b1dc0a03f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b1dc0a03f | ||
|
|
f4d383dfe1 | ||
|
|
a14072af00 | ||
|
|
436de88d73 | ||
|
|
a0137d6979 | ||
|
|
c46264a4cf | ||
|
|
979b93c53d | ||
|
|
9880f2a5a8 | ||
|
|
19b5d721a7 |
@@ -1,6 +1,7 @@
|
||||
package com.shuwei.dish.match.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -12,6 +13,7 @@ import com.shuwei.dish.match.entity.GoodsItem
|
||||
import com.shuwei.dish.match.utils.ext.gone
|
||||
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
|
||||
import com.shuwei.dish.match.utils.ext.visible
|
||||
import androidx.core.graphics.toColorInt
|
||||
|
||||
class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
|
||||
BaseQuickAdapter<GoodsItem, FoodMaterialAdapter.VH>(list) {
|
||||
@@ -54,6 +56,14 @@ class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
|
||||
if (data.isOriginalData) R.drawable.ic_dish_clear
|
||||
else R.drawable.ic_delete
|
||||
)
|
||||
// isOriginalData 时根据是否有重量信息控制图标颜色:无重量染灰色,有重量恢复原色
|
||||
if (data.isOriginalData) {
|
||||
val hasWeight = data.useWeight != null && data.useWeight != 0.0
|
||||
ivClearIcon.imageTintList = if (hasWeight) null
|
||||
else ColorStateList.valueOf("#BFBFBF".toColorInt())
|
||||
} else {
|
||||
ivClearIcon.imageTintList = null
|
||||
}
|
||||
ivOperateIcon.setImageResource(
|
||||
if (data.isSetFinished) R.drawable.ic_dish_selected
|
||||
else R.drawable.ic_dish_unselected
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.shuwei.dish.match.net.NetViewModel
|
||||
import com.shuwei.dish.match.db.DbViewModel
|
||||
import com.shuwei.dish.match.ui.InitActivity
|
||||
import com.shuwei.dish.match.utils.ActivityManager
|
||||
import com.shuwei.dish.match.utils.KeyboardUtil
|
||||
import com.shuwei.dish.match.utils.ext.dp
|
||||
import com.shuwei.dish.match.utils.ext.gone
|
||||
import com.shuwei.dish.match.utils.ext.invisible
|
||||
@@ -65,6 +66,7 @@ open class BaseActivity : AppCompatActivity() {
|
||||
binding.tvRightTime.setPadding(0, verticalPadding, 0, verticalPadding)
|
||||
binding.llTitleBar.gone()
|
||||
binding.ivBack.setOnClickListener { finish() }
|
||||
binding.root.setOnClickListener { hideKeyboard() }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -264,4 +266,8 @@ open class BaseActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
KeyboardUtil.hideKeyboard(binding.root)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -222,6 +222,17 @@ class DbRepository {
|
||||
suspend fun countSlotAll() = withContext(Dispatchers.IO) {
|
||||
db.seasoningSlotDao().countSlotAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理清空全部 4 张表的测试数据
|
||||
* 清除顺序:先删子表(食材),再删主表(菜品),最后删配置表(调料、槽位)
|
||||
*/
|
||||
suspend fun clearAllData() = withContext(Dispatchers.IO) {
|
||||
db.cookFoodGoodsDao().deleteAll()
|
||||
db.cookFoodDao().deleteAll()
|
||||
db.seasoningDao().deleteAll()
|
||||
db.seasoningSlotDao().deleteAll()
|
||||
}
|
||||
}
|
||||
//
|
||||
//class SeasoningRepository(val seasoningDao: SeasoningDao) {
|
||||
|
||||
@@ -239,6 +239,17 @@ class DbViewModel : ViewModel() {
|
||||
|
||||
// ---- 数据库调试查看 ----
|
||||
|
||||
/**
|
||||
* 物理清空全部 4 张表的测试数据,完成后通过回调通知 UI
|
||||
* @param onDone 清除完成后在主线程执行的回调
|
||||
*/
|
||||
fun clearAllData(onDone: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
rep.clearAllData()
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** 每页加载条数 */
|
||||
const val DB_INSPECT_PAGE_SIZE = 50
|
||||
|
||||
@@ -40,4 +40,8 @@ interface CookFoodDao {
|
||||
/** 统计菜品记录总数,showDel=true 时包含已删除记录 */
|
||||
@Query("SELECT COUNT(*) FROM dm_cook_food WHERE (:showDel = 1 OR isDel = 0)")
|
||||
suspend fun countCookFoodAll(showDel: Int): Int
|
||||
|
||||
/** 物理清空全表,用于清除测试数据 */
|
||||
@Query("DELETE FROM dm_cook_food")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
||||
@@ -40,4 +40,8 @@ interface CookFoodGoodsDao {
|
||||
/** 统计食材记录总数,showDel=true 时包含已删除记录 */
|
||||
@Query("SELECT COUNT(*) FROM dm_cook_food_goods WHERE (:showDel = 1 OR isDel = 0)")
|
||||
suspend fun countCookFoodGoodsAll(showDel: Int): Int
|
||||
|
||||
/** 物理清空全表,用于清除测试数据 */
|
||||
@Query("DELETE FROM dm_cook_food_goods")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
||||
@@ -53,4 +53,8 @@ interface SeasoningDao {
|
||||
/** 统计调料记录总数,showDel=true 时包含已删除记录 */
|
||||
@Query("SELECT COUNT(*) FROM dm_seasoning WHERE (:showDel = 1 OR isDel = 0)")
|
||||
suspend fun countSeasoningAll(showDel: Int): Int
|
||||
|
||||
/** 物理清空全表,用于清除测试数据(比 clearAllSeasoning 的逻辑删除更彻底) */
|
||||
@Query("DELETE FROM dm_seasoning")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
||||
@@ -60,4 +60,8 @@ interface SeasoningSlotDao {
|
||||
*/
|
||||
@Query("DELETE FROM dm_seasoning_slot WHERE deviceId = :deviceId")
|
||||
suspend fun deleteAllByDeviceId(deviceId: String)
|
||||
|
||||
/** 物理清空全表,用于清除测试数据 */
|
||||
@Query("DELETE FROM dm_seasoning_slot")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
||||
@@ -186,12 +186,25 @@ class SeasoningSelectDialog(
|
||||
* 请求调料列表
|
||||
*/
|
||||
private fun getGoodsList() {
|
||||
activity.netViewModel.queryGoodsList(
|
||||
goodsType = "1",
|
||||
pageNum = pageNo,
|
||||
pageSize = PAGE_SIZE,
|
||||
goodsName = goodsName.takeUnless { it.isNullOrBlank() }
|
||||
// activity.netViewModel.queryGoodsList(
|
||||
// goodsType = "1",
|
||||
// pageNum = pageNo,
|
||||
// pageSize = PAGE_SIZE,
|
||||
// goodsName = goodsName.takeUnless { it.isNullOrBlank() }
|
||||
// )
|
||||
|
||||
// TODO: 联调时注释掉下方模拟数据,改为真实接口调用
|
||||
val nameList = listOf(
|
||||
"盐", "白砂糖","冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香",
|
||||
"料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "葱", "蒜", "姜", "香菜", "油"
|
||||
)
|
||||
|
||||
val mockData = mutableListOf<GoodsItem>()
|
||||
nameList.forEachIndexed { index, name ->
|
||||
val padString = "${index + 1}".padStart(2, '0')
|
||||
mockData.add(GoodsItem(goodsId = "1000$padString", goodsName = name))
|
||||
}
|
||||
loadGoodsList(mockData)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,6 +51,10 @@ class NetViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun resetFoodDetailState() {
|
||||
_foodDetailState.value = UiState.Idle
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索菜品列表的 UI 状态流,UI 层通过 collect 监听
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,9 @@ object ScaleDeviceConfig {
|
||||
/** 18个秤的显示顺序(按物理位置排列) */
|
||||
val SCALE_ORDER_18 = listOf(6, 5, 4, 3, 2, 1, 12, 11, 10, 9, 8, 7, 18, 17, 16, 15, 14, 13)
|
||||
|
||||
/** 1个秤的地址列表 */
|
||||
val SCALE_ORDER_1 = listOf(1)
|
||||
|
||||
/** 设备在列表中的显示顺序 */
|
||||
val DEVICE_ORDER = listOf(DEVICE_ID_2, DEVICE_ID_22, DEVICE_ID_18, DEVICE_ID_1)
|
||||
}
|
||||
|
||||
@@ -34,5 +34,7 @@ data class ScaleEvent(
|
||||
const val TYPE_SEASONING_ADDED = "seasoning_added"
|
||||
/** 调料槽位配置同步事件:主设备配置变更后广播给子设备 */
|
||||
const val TYPE_SEASONING_CONFIG = "seasoning_config"
|
||||
/** 清除数据事件:主设备通知所有子设备清除本机测试数据 */
|
||||
const val TYPE_CLEAR_DATA = "clear_data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,14 @@ object ScaleServiceManager {
|
||||
get() = wsServer?.onSeasoningConfig
|
||||
set(value) { wsServer?.onSeasoningConfig = value }
|
||||
|
||||
/**
|
||||
* 收到主设备下发的清除数据事件时的回调,仅子设备有效
|
||||
* 在子线程中调用,需自行切换到主线程更新 UI
|
||||
*/
|
||||
var onClearData: ((ScaleEvent) -> Unit)?
|
||||
get() = wsServer?.onClearData
|
||||
set(value) { wsServer?.onClearData = value }
|
||||
|
||||
/**
|
||||
* 向所有已连接子设备广播调料配置(主设备调用)
|
||||
* @param slots 全量槽位配置列表
|
||||
@@ -79,6 +87,16 @@ object ScaleServiceManager {
|
||||
wsClient?.sendToAllSlaves(buildConfigEvent(slots))
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有已连接子设备清除本机数据(主设备调用)
|
||||
* 子设备收到后执行与主设备相同的清除逻辑
|
||||
*/
|
||||
fun sendClearData() {
|
||||
wsClient?.sendToAllSlaves(
|
||||
ScaleEvent(type = ScaleEvent.TYPE_CLEAR_DATA, deviceId = GlobalData.deviceId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 向单台刚连接的子设备推送调料配置(连接时按需调用)
|
||||
* @param deviceId 目标子设备 ID
|
||||
|
||||
@@ -43,6 +43,11 @@ class ScaleWebSocketClient {
|
||||
private val reconnectDelays = ConcurrentHashMap<String, Long>()
|
||||
/** 已主动移除的设备集合,不对其触发重连 */
|
||||
private val removedDevices = ConcurrentHashMap.newKeySet<String>()
|
||||
/**
|
||||
* 各设备的连接版本号,每次调用 connect() 时递增
|
||||
* doConnect 执行前校验版本,版本不匹配说明已有更新的连接请求,直接放弃本次连接
|
||||
*/
|
||||
private val connectVersions = ConcurrentHashMap<String, Int>()
|
||||
|
||||
private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
@@ -66,13 +71,21 @@ class ScaleWebSocketClient {
|
||||
*/
|
||||
fun connect(deviceId: String, host: String, port: Int) {
|
||||
removedDevices.remove(deviceId)
|
||||
// 取消旧 IP 的重连任务,防止 IP 变化时新旧任务并发竞争
|
||||
// 取消旧重连任务,防止 IP 变化时新旧任务并发竞争
|
||||
reconnectTasks.remove(deviceId)?.cancel(false)
|
||||
reconnectDelays[deviceId] = RECONNECT_BASE_MS
|
||||
doConnect(deviceId, host, port)
|
||||
// 递增版本号:doConnect 执行前会校验版本,旧任务版本不匹配时直接放弃
|
||||
val version = (connectVersions[deviceId] ?: 0) + 1
|
||||
connectVersions[deviceId] = version
|
||||
doConnect(deviceId, host, port, version)
|
||||
}
|
||||
|
||||
private fun doConnect(deviceId: String, host: String, port: Int) {
|
||||
private fun doConnect(deviceId: String, host: String, port: Int, version: Int = connectVersions[deviceId] ?: 0) {
|
||||
// 版本校验:若当前版本已被更新的 connect() 调用覆盖,放弃本次连接
|
||||
if (connectVersions[deviceId] != version) {
|
||||
Log.d(TAG, "放弃过期连接任务: $deviceId, version=$version")
|
||||
return
|
||||
}
|
||||
val url = "ws://$host:$port"
|
||||
val request = Request.Builder().url(url).build()
|
||||
|
||||
@@ -103,17 +116,24 @@ class ScaleWebSocketClient {
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
Log.w(TAG, "连接子设备失败: $deviceId, ${t.message}")
|
||||
connections.remove(deviceId)
|
||||
// 使用 remove(key, value) 确保只移除自己的实例
|
||||
// 避免旧连接超时触发时误删新连接引用,导致 aggregator 错误清空新连接数据
|
||||
val removed = connections.remove(deviceId, webSocket)
|
||||
if (removed) {
|
||||
onDeviceDisconnected?.invoke(deviceId)
|
||||
scheduleReconnect(deviceId, host, port)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
Log.d(TAG, "子设备连接已关闭: $deviceId, reason=$reason")
|
||||
connections.remove(deviceId)
|
||||
// 同上:只有移除的是自己,才触发断线回调和重连
|
||||
val removed = connections.remove(deviceId, webSocket)
|
||||
if (removed) {
|
||||
onDeviceDisconnected?.invoke(deviceId)
|
||||
scheduleReconnect(deviceId, host, port)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
connections[deviceId] = ws
|
||||
@@ -129,10 +149,12 @@ class ScaleWebSocketClient {
|
||||
if (scheduler.isShutdown) return
|
||||
val delay = reconnectDelays[deviceId] ?: RECONNECT_BASE_MS
|
||||
Log.d(TAG, "将在 ${delay}ms 后重连子设备: $deviceId")
|
||||
// 记录当前版本,延迟任务执行时再次校验,防止期间有新的 connect() 调用
|
||||
val version = connectVersions[deviceId] ?: 0
|
||||
|
||||
val future = scheduler.schedule({
|
||||
if (!removedDevices.contains(deviceId)) {
|
||||
doConnect(deviceId, host, port)
|
||||
doConnect(deviceId, host, port, version)
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS)
|
||||
|
||||
@@ -188,6 +210,7 @@ class ScaleWebSocketClient {
|
||||
reconnectTasks.remove(deviceId)?.cancel(false)
|
||||
connections.remove(deviceId)?.close(1000, "设备离线")
|
||||
reconnectDelays.remove(deviceId)
|
||||
connectVersions.remove(deviceId)
|
||||
Log.d(TAG, "已断开子设备: $deviceId")
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.java_websocket.WebSocket
|
||||
import org.java_websocket.handshake.ClientHandshake
|
||||
import org.java_websocket.server.WebSocketServer
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* 秤数据 WebSocket 服务端(主设备和子设备均运行)
|
||||
@@ -20,10 +22,14 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
private const val TAG = "ScaleWebSocketServer"
|
||||
/** 节流间隔:两次推送之间的最小间隔(毫秒) */
|
||||
private const val THROTTLE_MS = 100L
|
||||
/** latestData 为空时延迟补推的时间(毫秒),给 WeightUtil 时间产生第一次读数 */
|
||||
private const val RETRY_PUSH_DELAY_MS = 3_000L
|
||||
}
|
||||
|
||||
private val gson = Gson()
|
||||
private var server: InternalServer? = null
|
||||
/** 延迟补推任务的调度器 */
|
||||
private val scheduler = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
/** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */
|
||||
var onConnectionChanged: ((connected: Boolean) -> Unit)? = null
|
||||
@@ -31,6 +37,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
/** 收到主设备下发的调料配置同步事件时的回调,在子线程调用 */
|
||||
var onSeasoningConfig: ((event: ScaleEvent) -> Unit)? = null
|
||||
|
||||
/** 收到主设备下发的清除数据事件时的回调,在子线程调用 */
|
||||
var onClearData: ((event: ScaleEvent) -> Unit)? = null
|
||||
|
||||
/** 当前是否有主设备连接 */
|
||||
val isConnected: Boolean
|
||||
get() = server?.connections?.isNotEmpty() == true
|
||||
@@ -77,6 +86,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
WeightUtil.removeWeightListener(TAG)
|
||||
onConnectionChanged = null
|
||||
onSeasoningConfig = null
|
||||
onClearData = null
|
||||
scheduler.shutdownNow()
|
||||
try {
|
||||
server?.stop(1000)
|
||||
} catch (e: Exception) {
|
||||
@@ -124,7 +135,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
connectionCount++
|
||||
Log.d(TAG, "主设备已连接: ${conn.remoteSocketAddress}")
|
||||
onConnectionChanged?.invoke(true)
|
||||
|
||||
// 新客户端连接后,立即推送所有秤的最新数据
|
||||
if (latestData.isNotEmpty()) {
|
||||
latestData.values.forEach { data ->
|
||||
try {
|
||||
conn.send(gson.toJson(data))
|
||||
@@ -132,6 +145,23 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
Log.w(TAG, "初始推送失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// latestData 为空说明刚重启,WeightUtil 还未产生读数
|
||||
// 延迟 3 秒后补推一次,确保主设备能看到本机秤
|
||||
Log.d(TAG, "latestData 为空,${RETRY_PUSH_DELAY_MS}ms 后补推")
|
||||
scheduler.schedule({
|
||||
if (conn.isOpen && latestData.isNotEmpty()) {
|
||||
latestData.values.forEach { data ->
|
||||
try {
|
||||
conn.send(gson.toJson(data))
|
||||
Log.d(TAG, "补推数据: address=${data.address}, weight=${data.weight}")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "补推失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}, RETRY_PUSH_DELAY_MS, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) {
|
||||
@@ -151,6 +181,7 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
val event = gson.fromJson(message, ScaleEvent::class.java)
|
||||
when (event.type) {
|
||||
ScaleEvent.TYPE_SEASONING_CONFIG -> onSeasoningConfig?.invoke(event)
|
||||
ScaleEvent.TYPE_CLEAR_DATA -> onClearData?.invoke(event)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ class CollectedFoodActivity : BaseActivity() {
|
||||
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
|
||||
showDeleteDialog(foodList[position].foodName)
|
||||
}
|
||||
setOnItemClickListener { _, view, _ ->
|
||||
KeyboardUtil.hideKeyboard(view)
|
||||
setOnItemClickListener { _, _, _ ->
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,12 +84,12 @@ class CollectedFoodActivity : BaseActivity() {
|
||||
setContentView(binding.root)
|
||||
setHeaderBackground()
|
||||
binding.root.setOnClickListener { v ->
|
||||
KeyboardUtil.hideKeyboard(v)
|
||||
hideKeyboard()
|
||||
}
|
||||
setTitleBar(titleBarAction = {
|
||||
it.visible()
|
||||
it.setOnClickListener { v ->
|
||||
KeyboardUtil.hideKeyboard(v)
|
||||
hideKeyboard()
|
||||
}
|
||||
}, titleAction = {
|
||||
it.text = "已采集食材"
|
||||
@@ -156,7 +156,7 @@ class CollectedFoodActivity : BaseActivity() {
|
||||
performSearch {
|
||||
toast(binding.editSearch.hint.toString())
|
||||
}
|
||||
KeyboardUtil.hideKeyboard(v)
|
||||
hideKeyboard()
|
||||
}
|
||||
binding.editSearch.run {
|
||||
addTextChangedListener {
|
||||
@@ -169,7 +169,7 @@ class CollectedFoodActivity : BaseActivity() {
|
||||
performSearch {
|
||||
toast(binding.editSearch.hint.toString())
|
||||
}
|
||||
KeyboardUtil.hideKeyboard(v)
|
||||
hideKeyboard()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -117,11 +117,10 @@ class CookingModeActivity : BaseActivity() {
|
||||
return
|
||||
}
|
||||
startActivity<FoodSearchActivity> {
|
||||
putExtra(FoodSearchActivity.PAGE_TYPE, 1)
|
||||
putExtra(FoodSearchActivity.DINNER_TYPE, dinnerType)
|
||||
putExtra(FoodSearchActivity.FOOD_NAME, searchText)
|
||||
}
|
||||
KeyboardUtil.hideKeyboard(binding.etInputDish)
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
private fun loadFragment() {
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
|
||||
import com.shuwei.dish.match.entity.FoodRecord
|
||||
import com.shuwei.dish.match.net.UiState
|
||||
import com.shuwei.dish.match.utils.KeyboardUtil
|
||||
import com.shuwei.dish.match.utils.SpTool
|
||||
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
|
||||
import com.shuwei.dish.match.utils.ext.gone
|
||||
import com.shuwei.dish.match.utils.ext.startActivity
|
||||
@@ -32,7 +33,7 @@ class FoodSearchActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FoodSearchActivity"
|
||||
const val PAGE_TYPE = "pageType"
|
||||
const val FOOD_ITEM = "foodItem"
|
||||
const val FOOD_ID = "foodId"
|
||||
const val FOOD_NAME = "foodName"
|
||||
const val DINNER_TYPE = "dinnerType"
|
||||
@@ -41,13 +42,12 @@ class FoodSearchActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityFoodSearchBinding
|
||||
|
||||
private var list: MutableList<FoodRecord> = mutableListOf()
|
||||
private var pageType = 0
|
||||
private var dinnerType = "1"
|
||||
private val recordAdapter by lazy {
|
||||
Food2Adapter(list).apply {
|
||||
isStateViewEnable = true
|
||||
setOnItemClickListener { adapter, view, position ->
|
||||
if (pageType == 1) {
|
||||
if (SpTool.cookMode == 0) {
|
||||
val jumpItem = list[position].also { it.dinnerType = dinnerType }
|
||||
startActivity<PrepareFoodActivity> {
|
||||
putExtra(PrepareFoodActivity.FOOD_ITEM, jumpItem as Serializable)
|
||||
@@ -55,8 +55,7 @@ class FoodSearchActivity : BaseActivity() {
|
||||
return@setOnItemClickListener
|
||||
}
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
putExtra(FOOD_ID, list[position].foodId)
|
||||
putExtra(FOOD_NAME, list[position].foodName)
|
||||
putExtra(FOOD_ITEM, list[position] as Serializable)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
@@ -69,7 +68,6 @@ class FoodSearchActivity : BaseActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityFoodSearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
pageType = intent.getIntExtra(PAGE_TYPE, 0)
|
||||
dinnerType = intent.getStringExtra(DINNER_TYPE) ?: "0"
|
||||
setHeaderBackground()
|
||||
foodName = intent.getStringExtra(FOOD_NAME)
|
||||
@@ -122,7 +120,7 @@ class FoodSearchActivity : BaseActivity() {
|
||||
binding.ivDishSearch.setOnClickListener { v ->
|
||||
pageNo = 1
|
||||
getInputAndSearch()
|
||||
KeyboardUtil.hideKeyboard(v)
|
||||
hideKeyboard()
|
||||
}
|
||||
binding.refreshLayout.setEnableRefresh(true)
|
||||
binding.refreshLayout.setEnableLoadMore(false)
|
||||
@@ -139,13 +137,13 @@ class FoodSearchActivity : BaseActivity() {
|
||||
queryListInfo(foodName!!)
|
||||
}
|
||||
binding.root.setOnClickListener { v ->
|
||||
KeyboardUtil.hideKeyboard(v)
|
||||
hideKeyboard()
|
||||
}
|
||||
binding.etInputDish.let { v ->
|
||||
v.addOnActionSearchListener {
|
||||
pageNo = 1
|
||||
getInputAndSearch()
|
||||
KeyboardUtil.hideKeyboard( v)
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,12 +42,14 @@ class HomeActivity : BaseActivity() {
|
||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setHeaderBackground(isHomePage = true)
|
||||
// 根据当前 canteenId 恢复 RadioButton 选中状态
|
||||
binding.radioGroup.check(
|
||||
if (BaseApp.canteenId == "1678234139391512577") R.id.radioSw else R.id.radioZk
|
||||
)
|
||||
binding.radioGroup.setOnCheckedChangeListener { group, checkedId ->
|
||||
if (checkedId == R.id.radioSw) {
|
||||
BaseApp.canteenId = "1678234139391512577"
|
||||
} else {
|
||||
BaseApp.canteenId = "0"
|
||||
}
|
||||
val id = if (checkedId == R.id.radioSw) "1678234139391512577" else "0"
|
||||
SpTool.canteenId = id
|
||||
BaseApp.canteenId = id
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this) {}
|
||||
// binding.radioGroup.check(R.id.radioZk)
|
||||
|
||||
@@ -80,7 +80,7 @@ class InitActivity : BaseActivity() {
|
||||
setHeaderBgVisible(false)
|
||||
BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString()
|
||||
|
||||
BaseApp.canteenId = "0"
|
||||
BaseApp.canteenId = SpTool.canteenId
|
||||
|
||||
WeightUtil.init()
|
||||
WeightUtil.getWeight()
|
||||
|
||||
@@ -5,13 +5,17 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.shuwei.dish.match.R
|
||||
import com.shuwei.dish.match.adapter.FoodMaterialAdapter
|
||||
import com.shuwei.dish.match.base.BaseActivity
|
||||
@@ -39,6 +43,7 @@ import com.shuwei.dish.match.utils.ext.toJsonString
|
||||
import com.shuwei.dish.match.utils.ext.toast
|
||||
import com.shuwei.dish.match.utils.ext.visible
|
||||
import com.yanzhenjie.recyclerview.SwipeMenuItem
|
||||
import com.yanzhenjie.recyclerview.SwipeRecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -53,11 +58,13 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
companion object {
|
||||
const val TAG = "CookActivity"
|
||||
const val FOOD_ITEM = "foodItem"
|
||||
const val FOOD_NAME = "foodName"
|
||||
const val PAGE_FROM = "pageFrom"
|
||||
const val HOME = "home"
|
||||
const val WEIGHT_CHANGE_VALUE = 15
|
||||
const val WEIGHT_RECOGNIZE_VALUE = 10
|
||||
|
||||
/** 主材数量上限 */
|
||||
const val MATERIAL_COUNT = 3
|
||||
}
|
||||
|
||||
private val binding by lazy { ActivityPrepareFoodBinding.inflate(layoutInflater) }
|
||||
@@ -66,9 +73,6 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
|
||||
private var food: FoodRecord? = null
|
||||
|
||||
/** 采集模式下通过搜索页回填的 foodId */
|
||||
private var foodId: String? = null
|
||||
|
||||
/** 来源页面标识,HOME 时返回跳转 SamplingListActivity */
|
||||
private var pageFrom: String? = null
|
||||
|
||||
@@ -78,8 +82,8 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
setContentView(binding.root)
|
||||
setHeaderBackground()
|
||||
pageFrom = intent.getStringExtra(PAGE_FROM)
|
||||
food = intent.extras?.getSerializable(FOOD_ITEM) as FoodRecord?
|
||||
|
||||
//food = intent.extras?.getSerializable(FOOD_ITEM) as FoodRecord?
|
||||
food = IntentCompat.getSerializableExtra(intent, FOOD_ITEM, FoodRecord::class.java)
|
||||
val isSamplingMode = SpTool.cookMode == 1
|
||||
val titleText = if (isSamplingMode) "菜品采集" else "菜品制作"
|
||||
setTitleBar(titleBarAction = {
|
||||
@@ -93,16 +97,21 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
})
|
||||
|
||||
food?.let {
|
||||
binding.etInputDish.run {
|
||||
setText(it.foodName)
|
||||
tag = it.foodName
|
||||
}
|
||||
}
|
||||
// 根据 cookMode 设置搜索栏可编辑性
|
||||
if (isSamplingMode) {
|
||||
// 采集模式:可编辑,支持搜索
|
||||
val foodName = intent.getStringExtra(FOOD_NAME)
|
||||
binding.etInputDish.setText(foodName ?: food?.foodName ?: "")
|
||||
binding.ivDishSearch.visible()
|
||||
binding.etInputDish.let {
|
||||
it.setSelection(it.length())
|
||||
}
|
||||
} else {
|
||||
// 制作模式:不可编辑,仅展示菜名
|
||||
binding.etInputDish.setText(food?.foodName ?: "")
|
||||
binding.etInputDish.isFocusable = false
|
||||
binding.etInputDish.isFocusableInTouchMode = false
|
||||
binding.ivDishSearch.gone()
|
||||
@@ -114,8 +123,8 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
initRecyclerView()
|
||||
initObserver()
|
||||
|
||||
if (!isSamplingMode) {
|
||||
getDishDetail()
|
||||
if (food?.foodId.isNullOrBlank().not()) {
|
||||
getDishDetail(food!!.foodId!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +134,7 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
WeightUtil.tareTwo(AddressUtil.ONE)
|
||||
delayDismissLoading()
|
||||
}
|
||||
//binding.root.setOnClickListener { hideKeyboard() }
|
||||
WeightUtil.addWeightListener(
|
||||
weightKey = TAG,
|
||||
getWeight = { address, state, weight ->
|
||||
@@ -143,8 +153,8 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
|
||||
if (SpTool.cookMode == 1) {
|
||||
// 采集模式:绑定搜索栏交互
|
||||
binding.ivDishSearch.setOnClickListener { v -> jumpSearch(v) }
|
||||
binding.etInputDish.addOnActionSearchListener { jumpSearch(binding.etInputDish) }
|
||||
binding.ivDishSearch.setOnClickListener { jumpSearch() }
|
||||
binding.etInputDish.addOnActionSearchListener { jumpSearch() }
|
||||
}
|
||||
|
||||
binding.btnCook.clickWithDebounce {
|
||||
@@ -159,10 +169,11 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
toast("菜品构成信息未设置")
|
||||
return@clickWithDebounce
|
||||
}
|
||||
val foodRecord = FoodRecord()
|
||||
val foodRecord = food ?: FoodRecord()
|
||||
binding.etInputDish.let {
|
||||
if (it.tag != null && it.tag.toString() == it.text.toString()) {
|
||||
foodRecord.foodId = foodId
|
||||
if (it.tag == null || it.tag.toString() != it.text.toString()) {
|
||||
// 输入框内容与回填名称不一致,说明用户手动修改了菜名,清除 foodId
|
||||
foodRecord.foodId = null
|
||||
}
|
||||
}
|
||||
foodRecord.foodName = showFoodName
|
||||
@@ -187,11 +198,13 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private val isTakingPhoto = AtomicBoolean(false)
|
||||
|
||||
// 识别成功并查询到数据打开识别页面
|
||||
private var showRecognizePage = false
|
||||
|
||||
// 页面可见时允许识别,onPause 后禁止,onResume 后恢复
|
||||
private var pageVisible = true
|
||||
|
||||
// 手动取消标识
|
||||
private var manualCancelFlag = false
|
||||
private var lastPhotoUri: Uri? = null
|
||||
@@ -211,8 +224,14 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
FoodRecognizeActivity.close()
|
||||
return
|
||||
}
|
||||
if (!pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE) {
|
||||
Log.d(TAG, "recognizeFood, lastWeight:$lastWeight, weight:$weight, pageVisible:$pageVisible, manualCancelFlag:$manualCancelFlag, isTakingPhoto:${isTakingPhoto.get()}")
|
||||
if (!pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage || abs(
|
||||
lastWeight - weight
|
||||
) <= WEIGHT_CHANGE_VALUE
|
||||
) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"recognizeFood, lastWeight:$lastWeight, weight:$weight, pageVisible:$pageVisible, manualCancelFlag:$manualCancelFlag, isTakingPhoto:${isTakingPhoto.get()}"
|
||||
)
|
||||
return
|
||||
}
|
||||
showLoading("识别中……")
|
||||
@@ -239,9 +258,40 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
it.isNewDishType = true
|
||||
it.isSetFinished = true
|
||||
})
|
||||
|
||||
binding.rvMaterialList.smoothScrollToPosition(list.size - 1)
|
||||
}
|
||||
updateMaterialTypes()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算 list 中非接口数据(isOriginalData=false)的 materialType
|
||||
*
|
||||
* 规则:
|
||||
* - 接口数据主材数量 n = list 中 isOriginalData=true 且 materialType=1 的数量
|
||||
* - 有接口数据且 MATERIAL_COUNT - n <= 0:所有非接口数据均为辅材(2)
|
||||
* - 无接口数据,或 MATERIAL_COUNT - n > 0:m = MATERIAL_COUNT - n,
|
||||
* 非接口数据按 useWeight 从大到小排序,前 m 个为主材(1),其余为辅材(2)
|
||||
*/
|
||||
private fun updateMaterialTypes() {
|
||||
val hasOriginalData = list.any { it.isOriginalData }
|
||||
// 接口数据中主材数量
|
||||
val n = list.count { it.isOriginalData && it.materialType == 1 }
|
||||
// 待分配的非接口数据
|
||||
val nonOriginalList = list.filter { !it.isOriginalData }
|
||||
|
||||
if (hasOriginalData && MATERIAL_COUNT - n <= 0) {
|
||||
// 主材已满,所有非接口数据均为辅材
|
||||
nonOriginalList.forEach { it.materialType = 2 }
|
||||
} else {
|
||||
// 剩余可分配主材名额
|
||||
val m = MATERIAL_COUNT - n
|
||||
// 按 useWeight 从大到小排序
|
||||
val sorted = nonOriginalList.sortedByDescending { it.useWeight ?: 0.0 }
|
||||
sorted.forEachIndexed { index, item ->
|
||||
item.materialType = if (index < m) 1 else 2
|
||||
}
|
||||
}
|
||||
materialAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun openSubmitPage() {
|
||||
@@ -282,31 +332,25 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
|
||||
private fun initObserver() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
netViewModel.foodDetailState.collect { state ->
|
||||
when (state) {
|
||||
is UiState.Success -> {
|
||||
// 防止 StateFlow 重放导致 list 被重复清空
|
||||
if (isDetailLoaded) return@collect
|
||||
val detail = state.data
|
||||
if (detail == null) {
|
||||
toast("查询菜品信息为空")
|
||||
return@collect
|
||||
}
|
||||
isDetailLoaded = true
|
||||
loadDishDetail(detail)
|
||||
}
|
||||
|
||||
is UiState.Error -> toast(state.msg)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDishDetail() {
|
||||
netViewModel.getFoodDetail(food?.foodId ?: "")
|
||||
private fun getDishDetail(foodId: String) {
|
||||
netViewModel.getFoodDetail(foodId)
|
||||
}
|
||||
|
||||
|
||||
@@ -319,36 +363,48 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
// 筛选出主材和辅材,转换为 GoodsItem(useWeight 置零)
|
||||
val tempData = voList.filter { it.materialType == 1 || it.materialType == 2 }
|
||||
.map { it.toGoodsItem().also { item -> item.useWeight = 0.0 } }
|
||||
// 直接替换 adapter.items,确保 adapter 数据源被完全替换
|
||||
// 同时同步外部 list,保证后续 updateMaterialTypes/removeFood 等操作正确
|
||||
list.clear()
|
||||
firstReqSize = list.size
|
||||
list.addAll(tempData)
|
||||
materialAdapter.items = list
|
||||
materialAdapter.notifyDataSetChanged()
|
||||
firstReqSize = list.size
|
||||
}
|
||||
|
||||
private var firstReqSize = 0
|
||||
private var isDetailLoaded = false
|
||||
private val list = mutableListOf<GoodsItem>()
|
||||
private val materialAdapter by lazy {
|
||||
FoodMaterialAdapter(list = list).apply {
|
||||
val adapter = this
|
||||
FoodMaterialAdapter(list).apply {
|
||||
onItemClick = { positon ->
|
||||
list.forEachIndexed { index, entity ->
|
||||
entity.isItemClicked = index == positon
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
addOnItemChildClickListener(R.id.ivClearIcon) { _, _, positon ->
|
||||
Log.d(TAG, "onFoodItemClick: ${list[positon].toJsonString()}")
|
||||
if (list[positon].isOriginalData) {
|
||||
list[positon].run {
|
||||
addOnItemChildClickListener(R.id.ivClearIcon) { _, _, position ->
|
||||
Log.d(TAG, "onFoodItemClick: ${list[position].toJsonString()}")
|
||||
val item = list[position]
|
||||
if (item.isOriginalData) {
|
||||
// 无重量信息时转为 item 整体点击(触发选中效果)
|
||||
if (item.useWeight == null || item.useWeight == 0.0) {
|
||||
onItemClick?.invoke(position)
|
||||
return@addOnItemChildClickListener
|
||||
}
|
||||
deleteRemindDialog("清除确认", "确定清除食材「${item.goodsName}」添加的重量吗?") {
|
||||
item.run {
|
||||
isNewDishType = false
|
||||
useWeight = 0.0
|
||||
isSetFinished = false
|
||||
}
|
||||
notifyItemChanged(positon)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
return@addOnItemChildClickListener
|
||||
}
|
||||
adapter.removeAt(positon)
|
||||
|
||||
deleteRemindDialog("删除确认", "确定删除食材「${item.goodsName}」吗?") {
|
||||
removeFood(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,37 +412,55 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
/**
|
||||
* RecyclerView初始化
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun initRecyclerView() {
|
||||
binding.rvMaterialList.run {
|
||||
layoutManager =
|
||||
LinearLayoutManager(this@PrepareFoodActivity, LinearLayoutManager.VERTICAL, false)
|
||||
binding.rvMaterialList.let {
|
||||
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
// 仅 isNewDishType=true 的 item 显示侧滑删除菜单(必须在 setAdapter 之前调用)
|
||||
setSwipeMenuCreator { _, rightMenu, position ->
|
||||
it.setSwipeMenuCreator { _, rightMenu, position ->
|
||||
if (list.getOrNull(position)?.isNewDishType == true) {
|
||||
rightMenu.addMenuItem(buildDeleteMenuItem())
|
||||
}
|
||||
}
|
||||
// 点击侧滑菜单项:先关闭菜单,再弹窗确认删除
|
||||
setOnItemMenuClickListener { menuBridge, position ->
|
||||
it.setOnItemMenuClickListener { menuBridge, position ->
|
||||
menuBridge.closeMenu()
|
||||
val item = list.getOrNull(position) ?: return@setOnItemMenuClickListener
|
||||
CommonDialog(this@PrepareFoodActivity)
|
||||
.setTitle("删除确认")
|
||||
.setContent("确定删除食材「${item.goodsName}」吗?")
|
||||
.setNegativeButton("取消")
|
||||
.setPositiveButton("删除") {
|
||||
materialAdapter.removeAt(position)
|
||||
toast("已删除")
|
||||
}.show()
|
||||
deleteRemindDialog("删除确认", "确定删除食材「${item.goodsName}」吗?") {
|
||||
removeFood(position)
|
||||
}
|
||||
}
|
||||
// item 点击事件
|
||||
setOnItemClickListener { _, position ->
|
||||
it.setOnItemClickListener { _, position ->
|
||||
materialAdapter.onItemClick?.invoke(position)
|
||||
}
|
||||
adapter = materialAdapter
|
||||
handleRvTouchHideKeyboard()
|
||||
it.adapter = materialAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除提醒
|
||||
*/
|
||||
private fun deleteRemindDialog(title: String, content: String, action: () -> Unit) {
|
||||
CommonDialog(this@PrepareFoodActivity)
|
||||
.setTitle(title)
|
||||
.setContent(content)
|
||||
.setNegativeButton("取消")
|
||||
.setPositiveButton("确认") {
|
||||
action()
|
||||
}.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除菜品
|
||||
*/
|
||||
private fun removeFood(position: Int) {
|
||||
materialAdapter.removeAt(position)
|
||||
toast("已删除")
|
||||
updateMaterialTypes()
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建侧滑删除菜单项
|
||||
*/
|
||||
@@ -526,7 +600,10 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
currentWeight = currentWeight,
|
||||
nameScoreList = ArrayList(nameScoreList),
|
||||
finishCallback = { isManualCancel, entity ->
|
||||
Log.d(TAG, "queryFood: isManualCancel=$isManualCancel, entity=${entity.toJsonString()}")
|
||||
Log.d(
|
||||
TAG,
|
||||
"queryFood: isManualCancel=$isManualCancel, entity=${entity.toJsonString()}"
|
||||
)
|
||||
manualCancelFlag = isManualCancel
|
||||
if (isManualCancel) {
|
||||
return@start
|
||||
@@ -542,24 +619,67 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
|
||||
/**
|
||||
* 跳转食物搜索页(采集模式专用)
|
||||
* @param v 触发搜索的 View,用于收起键盘
|
||||
*/
|
||||
private fun jumpSearch(v: View) {
|
||||
val intent = Intent(this, FoodSearchActivity::class.java).apply {
|
||||
putExtra(FoodSearchActivity.FOOD_NAME, binding.etInputDish.text.toString().trim())
|
||||
private fun jumpSearch() {
|
||||
val searchContent = binding.etInputDish.text.toString().trim()
|
||||
if (searchContent.isBlank()) {
|
||||
toast(binding.etInputDish.hint.toString())
|
||||
return
|
||||
}
|
||||
startActivity(intent) {
|
||||
foodId = it?.getStringExtra(FoodSearchActivity.FOOD_ID)
|
||||
val name = it?.getStringExtra(FoodSearchActivity.FOOD_NAME)
|
||||
Log.d(TAG, "jumpSearch: foodId=$foodId, name=$name")
|
||||
if (name.isNullOrBlank()) return@startActivity
|
||||
val launchIntent = Intent(this, FoodSearchActivity::class.java).apply {
|
||||
putExtra(FoodSearchActivity.FOOD_NAME, searchContent)
|
||||
}
|
||||
startActivity(launchIntent) { resultIntent ->
|
||||
if (resultIntent == null) return@startActivity
|
||||
food = IntentCompat.getSerializableExtra(resultIntent, FOOD_ITEM, FoodRecord::class.java)
|
||||
Log.d(TAG, "jumpSearch: record=$food")
|
||||
if (food == null) return@startActivity
|
||||
// 将搜索结果写入 food,保留 cookMode
|
||||
food!!.cookMode = 1
|
||||
binding.etInputDish.run {
|
||||
setText(name)
|
||||
tag = name
|
||||
setText(food!!.foodName)
|
||||
tag = food!!.foodName
|
||||
setSelection(text.length)
|
||||
}
|
||||
// 有 foodId 时请求菜品构成
|
||||
if (!food!!.foodId.isNullOrBlank()) {
|
||||
getDishDetail(food!!.foodId!!)
|
||||
}
|
||||
KeyboardUtil.hideKeyboard(v)
|
||||
}
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
private fun handleRvTouchHideKeyboard() {
|
||||
val gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
hideKeyboard()
|
||||
// 返回 false,不消费事件,不影响 item 点击和滚动
|
||||
return false
|
||||
}
|
||||
})
|
||||
binding.rvMaterialList.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
|
||||
private var startX = 0f
|
||||
private var startY = 0f
|
||||
|
||||
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
|
||||
when (e.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = e.x
|
||||
startY = e.y
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val dx = abs(e.x - startX)
|
||||
val dy = abs(e.y - startY)
|
||||
// 水平滑动超过阈值时(侧滑手势),关闭键盘
|
||||
if (dx > dy && dx > 10) {
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
gestureDetector.onTouchEvent(e)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -186,7 +186,7 @@ class SamplingModeActivity : BaseActivity() {
|
||||
return
|
||||
}
|
||||
startActivity<PrepareFoodActivity> {
|
||||
putExtra(PrepareFoodActivity.FOOD_NAME, item.foodName)
|
||||
putExtra(PrepareFoodActivity.FOOD_ITEM, item as Serializable)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ import androidx.core.content.ContextCompat
|
||||
import com.shuwei.dish.match.base.BaseActivity
|
||||
import com.shuwei.dish.match.databinding.ActivitySettingBinding
|
||||
import com.shuwei.dish.match.dialog.CommonDialog
|
||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
||||
import com.shuwei.dish.match.utils.SpTool
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -46,7 +47,9 @@ class SettingActivity : BaseActivity() {
|
||||
}, rightIconActon = {
|
||||
it.visible()
|
||||
it.clickWithDebounce {
|
||||
startActivity<MasterScaleActivity>()
|
||||
binding.llDbInspect.visible()
|
||||
binding.llScaleObserve.visible()
|
||||
binding.llClearData.visible()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -79,6 +82,34 @@ class SettingActivity : BaseActivity() {
|
||||
binding.llDbInspect.clickWithDebounce {
|
||||
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.DB_INSPECT)
|
||||
}
|
||||
binding.llScaleObserve.clickWithDebounce {
|
||||
startActivity<MasterScaleActivity>()
|
||||
}
|
||||
binding.llClearData.clickWithDebounce {
|
||||
showClearDataDialog()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出清除测试数据的二次确认弹窗
|
||||
*/
|
||||
private fun showClearDataDialog() {
|
||||
CommonDialog(this)
|
||||
.setTitle("数据清除")
|
||||
.setContent("将物理删除全部 4 张表数据(菜品、食材、调料、槽位),此操作不可恢复,同时重置菜品模式和食堂id,确认继续?")
|
||||
.setNegativeButton("取消")
|
||||
.setPositiveButton("确认清除") {
|
||||
appViewModel.clearAllData {
|
||||
SpTool.cookMode = -1
|
||||
SpTool.canteenId = "0"
|
||||
// 通知所有已连接子设备同步清除数据
|
||||
ScaleServiceManager.sendClearData()
|
||||
startActivity<HomeActivity> {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun openCollectPage() {
|
||||
|
||||
@@ -18,7 +18,6 @@ import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
|
||||
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
|
||||
import com.shuwei.dish.match.adapter.ScaleRowAdapter
|
||||
import com.shuwei.dish.match.base.BaseActivity
|
||||
import com.shuwei.dish.match.base.BaseApp
|
||||
import com.shuwei.dish.match.base.GlobalData
|
||||
import com.shuwei.dish.match.databinding.ActivitySlaveBinding
|
||||
import com.shuwei.dish.match.dialog.CommonDialog
|
||||
@@ -28,6 +27,7 @@ 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.SpTool
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import com.shuwei.dish.match.utils.ext.dp
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -77,6 +77,7 @@ class SlaveActivity : BaseActivity() {
|
||||
listenLocalScales()
|
||||
listenMasterConnection()
|
||||
listenSeasoningConfig()
|
||||
listenClearData()
|
||||
addBackKeyListener()
|
||||
}
|
||||
|
||||
@@ -109,6 +110,8 @@ class SlaveActivity : BaseActivity() {
|
||||
it.onItemClick = { scale, _ -> showTareDialog(scale.address) }
|
||||
}
|
||||
binding.rvScaleList.adapter = scale22Adapter
|
||||
// 启动时用空哨兵预填全部格子,不依赖 WeightUtil 回调才显示结构
|
||||
scale22Adapter!!.updateByAddress(emptyList())
|
||||
}
|
||||
ScaleDeviceConfig.DEVICE_ID_18 -> {
|
||||
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = 0 }
|
||||
@@ -121,12 +124,18 @@ class SlaveActivity : BaseActivity() {
|
||||
it.onItemClick = { scale, _ -> showTareDialog(scale.address) }
|
||||
}
|
||||
binding.rvScaleList.adapter = scale18Adapter
|
||||
// 启动时用空哨兵预填全部格子,不依赖 WeightUtil 回调才显示结构
|
||||
scale18Adapter!!.updateByAddress(emptyList())
|
||||
}
|
||||
else -> {
|
||||
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = 20.dp }
|
||||
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
|
||||
linearAdapter.onItemClick = { item -> showTareDialog(item.address) }
|
||||
binding.rvScaleList.adapter = linearAdapter
|
||||
// 启动时预填空占位,不依赖 WeightUtil 回调才显示列表结构
|
||||
ScaleDeviceConfig.SCALE_ORDER_1.forEach { addr ->
|
||||
linearAdapter.updateItem(ScaleRowAdapter.ScaleItem(addr, 0.0, WeightUtil.STATE_STABLE))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +186,23 @@ class SlaveActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听主设备下发的清除数据事件
|
||||
* 收到后清空本机全部 4 张表数据并重置 SP 状态,与主设备保持一致
|
||||
* 清除完成后刷新 UI,清空所有秤的调料名称显示
|
||||
*/
|
||||
private fun listenClearData() {
|
||||
ScaleServiceManager.onClearData = {
|
||||
lifecycleScope.launch {
|
||||
appViewModel.clearAllData {
|
||||
SpTool.cookMode = -1
|
||||
SpTool.canteenId = "0"
|
||||
runOnUiThread { applySlots(emptyList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将槽位配置应用到对应 adapter(按地址匹配格子位置)
|
||||
* 同时以传入列表为准全量刷新 slotNameMap:
|
||||
|
||||
@@ -78,7 +78,7 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
|
||||
}
|
||||
item.dinnerType = dinnerType
|
||||
activity.startActivity<PrepareFoodActivity> {
|
||||
putExtra(SubmitFoodActivity.FOOD_ITEM, item as Serializable)
|
||||
putExtra(PrepareFoodActivity.FOOD_ITEM, item as Serializable)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ object SpTool {
|
||||
const val DEVICE_ROLE = "deviceRole"
|
||||
/** 秤串口路径,默认 /dev/ttyS7,子设备可能需要配置为其他值 */
|
||||
const val SCALE_DEVICE_PORT = "scaleDevicePort"
|
||||
/** 食堂 ID,默认 "0" */
|
||||
const val CANTEEN_ID = "canteenId"
|
||||
|
||||
val pref = BaseApp.getSharedPref()!!
|
||||
|
||||
@@ -37,4 +39,11 @@ object SpTool {
|
||||
set(value) {
|
||||
put(LAUNCH_PAGE_TYPE, value)
|
||||
}
|
||||
|
||||
/** 食堂 ID,持久化存储,默认 "0" */
|
||||
var canteenId: String
|
||||
get() = getString(CANTEEN_ID, "0")
|
||||
set(value) {
|
||||
put(CANTEEN_ID, value)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ package com.shuwei.dish.match.utils;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.aithings.Weigher;
|
||||
import com.blankj.utilcode.util.Utils;
|
||||
import com.t507.System;
|
||||
import com.wabon.wbintelligenthardwaresdk.api.SensorScale;
|
||||
@@ -73,6 +75,7 @@ public class Weigher2 {
|
||||
|
||||
public static void unInit() {
|
||||
// $FF: Couldn't be decompiled
|
||||
Weigher.unInit();
|
||||
}
|
||||
|
||||
public static void config() {
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
<?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"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/bg_color"
|
||||
android:orientation="vertical">
|
||||
tools:ignore="HardcodedText,UnusedAttribute">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llCookMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:background="@drawable/shape_white_dc_15_corners"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clipToOutline="true">
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_weight="1"
|
||||
android:text="菜品模式"
|
||||
android:textColor="@color/black333"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginStart="32dp"
|
||||
android:textColor="@color/black333" />
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCurrentMode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="当前:制作模式"
|
||||
android:textSize="26sp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:textColor="@color/black999" />
|
||||
android:textColor="@color/black999"
|
||||
android:textSize="26sp"
|
||||
tools:text="当前:制作模式" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
@@ -51,23 +51,23 @@
|
||||
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="50dp"
|
||||
android:background="@drawable/shape_white_dc_15_corners"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clipToOutline="true">
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_weight="1"
|
||||
android:text="调料区设置"
|
||||
android:textColor="@color/black333"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginStart="32dp"
|
||||
android:textColor="@color/black333" />
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
@@ -82,23 +82,23 @@
|
||||
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="50dp"
|
||||
android:background="@drawable/shape_white_dc_15_corners"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clipToOutline="true">
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_weight="1"
|
||||
android:text="食材采集"
|
||||
android:textColor="@color/black333"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginStart="32dp"
|
||||
android:textColor="@color/black333" />
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
@@ -113,23 +113,88 @@
|
||||
android:id="@+id/llDbInspect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="50dp"
|
||||
android:background="@drawable/shape_white_dc_15_corners"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clipToOutline="true">
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_weight="1"
|
||||
android:text="数据库查看"
|
||||
android:textColor="@color/black333"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/ic_arrow_right3"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llScaleObserve"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="50dp"
|
||||
android:background="@drawable/shape_white_dc_15_corners"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:textColor="@color/black333" />
|
||||
android:layout_weight="1"
|
||||
android:text="秤数据监控"
|
||||
android:textColor="@color/black333"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/ic_arrow_right3"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llClearData"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="50dp"
|
||||
android:background="@drawable/shape_white_dc_15_corners"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_weight="1"
|
||||
android:text="数据清除"
|
||||
android:textColor="@color/black333"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="5dp"
|
||||
tools:background="@color/bg_color">
|
||||
tools:background="@color/bg_color"
|
||||
tools:ignore="HardcodedText,UnusedAttribute">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
tools:ignore="HardcodedText,UnusedAttribute">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 单个秤格子:秤地址 + 调料名 + 重量 + 状态 -->
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- 单个秤格子:秤地址 + 调料名 + 重量 + 状态 -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/shape_scale_cell"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:background="@drawable/shape_scale_cell"
|
||||
android:clipToOutline="true">
|
||||
tools:ignore="HardcodedText,UnusedAttribute">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAddress"
|
||||
@@ -26,7 +27,7 @@
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/black"
|
||||
android:textStyle="bold"/>
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvWeight"
|
||||
@@ -34,7 +35,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/home_title"/>
|
||||
android:textColor="@color/home_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvState"
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
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:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:clipToOutline="true">
|
||||
tools:ignore="HardcodedText,UnusedAttribute">
|
||||
|
||||
<!-- 秤编号 -->
|
||||
<TextView
|
||||
@@ -26,10 +27,10 @@
|
||||
android:id="@+id/tvName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="-"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold"
|
||||
android:text="-" />
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- 重量 -->
|
||||
<TextView
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 1个秤设备的卡片布局 -->
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- 1个秤设备的卡片布局 -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="10dp">
|
||||
android:paddingTop="10dp"
|
||||
tools:ignore="HardcodedText,UnusedAttribute">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDeviceIp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/home_title"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="IP:192.168.1.100" />
|
||||
|
||||
<TextView
|
||||
@@ -30,58 +30,58 @@
|
||||
android:id="@+id/llScaleContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="@drawable/shape_scale_cell"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:background="@drawable/shape_scale_cell">
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAddr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/home_sub_title"
|
||||
android:textSize="18sp"
|
||||
tools:text="秤 1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold"
|
||||
android:includeFontPadding="false"
|
||||
android:text="-"
|
||||
android:textColor="@color/black"
|
||||
android:text="-" />
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvWeight"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="22sp"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginVertical="5dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/home_title"
|
||||
android:textSize="22sp"
|
||||
tools:text="100.0 g" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/home_sub_title"
|
||||
android:textSize="18sp"
|
||||
tools:text="稳定" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- <com.google.android.material.divider.MaterialDivider-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="1dp"-->
|
||||
<!-- android:background="@color/gray_eb"-->
|
||||
<!-- android:layout_marginTop="10dp" />-->
|
||||
<!-- <com.google.android.material.divider.MaterialDivider-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="1dp"-->
|
||||
<!-- android:background="@color/gray_eb"-->
|
||||
<!-- android:layout_marginTop="10dp" />-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
+14
-2
@@ -63,6 +63,7 @@ scale 包负责多设备秤数据的采集、发现、传输与聚合,采用 *
|
||||
|---|---|---|
|
||||
| `seasoning_added` | 子设备 → 主设备 | 某秤检测到调料添加,携带 `delta`(重量变化量) |
|
||||
| `seasoning_config` | 主设备 → 子设备 | 调料槽位配置同步,携带 `List<SlotConfig>` |
|
||||
| `clear_data` | 主设备 → 子设备 | 通知所有子设备清除本机全部测试数据并重置 SP 状态 |
|
||||
|
||||
### ScaleCommand
|
||||
主设备向子设备发送的**控制指令**,子设备收到后校验 `deviceId` 是否匹配自身再执行。
|
||||
@@ -112,9 +113,10 @@ scale 包负责多设备秤数据的采集、发现、传输与聚合,采用 *
|
||||
|
||||
**职责:**
|
||||
- 监听本机 `WeightUtil` 回调,将秤数据实时推送给所有已连接客户端(节流 100ms)
|
||||
- 新客户端连接时,立即推送所有秤的最新快照(`latestData` 缓存)
|
||||
- 接收主设备下发的 `ScaleCommand`(清零)和 `ScaleEvent`(配置同步)
|
||||
- 新客户端连接时,立即推送所有秤的最新快照(`latestData` 缓存);若 `latestData` 为空(刚重启尚无读数),延迟 3 秒后补推一次
|
||||
- 接收主设备下发的 `ScaleCommand`(清零)和 `ScaleEvent`(配置同步、清除数据)
|
||||
- 通过 `connectionCount` 计数器跟踪连接数,避免 `connections` 集合竞态问题
|
||||
- 内置单线程 `scheduler`,用于延迟补推任务调度;`stop()` 时同步关闭
|
||||
|
||||
### ScaleWebSocketClient(仅主设备运行)
|
||||
管理主设备与多台子设备的 WebSocket 长连接。
|
||||
@@ -125,6 +127,10 @@ scale 包负责多设备秤数据的采集、发现、传输与聚合,采用 *
|
||||
- 向指定设备或全部设备发送指令/事件
|
||||
- 子设备首次连接成功时触发 `onDeviceConnected`,供主设备推送全量配置
|
||||
|
||||
**并发安全机制:**
|
||||
- **版本号防重复连接**:每次调用 `connect()` 时递增 `connectVersions[deviceId]`,`doConnect` 执行前校验版本号,版本不匹配(说明已有更新的连接请求)则直接放弃,避免 UDP 触发的新连接与指数退避重连任务并发建立两条连接
|
||||
- **原子移除防误删**:`onFailure`/`onClosed` 使用 `connections.remove(deviceId, webSocket)` 原子操作,只有移除的是自己的实例时才触发 `onDeviceDisconnected` 和重连,避免旧连接超时回调误删新连接引用
|
||||
|
||||
---
|
||||
|
||||
## 数据聚合(1个)
|
||||
@@ -172,12 +178,18 @@ ScaleServiceManager.sendTare(deviceId, address)
|
||||
// 主设备广播调料配置
|
||||
ScaleServiceManager.sendSeasoningConfig(slots)
|
||||
|
||||
// 主设备通知所有子设备清除数据
|
||||
ScaleServiceManager.sendClearData()
|
||||
|
||||
// 主设备监听子设备秤事件(如调料添加)
|
||||
ScaleServiceManager.onScaleEvent = { event -> ... }
|
||||
|
||||
// 子设备监听主设备下发的调料配置同步
|
||||
ScaleServiceManager.onSeasoningConfig = { event -> ... }
|
||||
|
||||
// 子设备监听主设备下发的清除数据指令
|
||||
ScaleServiceManager.onClearData = { ... }
|
||||
|
||||
// 子设备向主设备广播秤事件
|
||||
ScaleServiceManager.broadcastEvent(event)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user