6 Commits
Author SHA1 Message Date
lvmeng ef13aa8307 feat(fragment): 新增净材种类选择功能及相关数据结构支持
- 新增 rawMaterialsType 字段至 GoodsItem、CookFoodGoodsEntity、SeasoningEntity,DB 升级至 v12
- VectorCollectionFragment 重构食材搜索,点击食材弹出净材种类单选弹窗
- CommonDialog 新增 addContentView 和可拦截确认按钮(setPositiveButtonInterceptable)能力
- Food3Adapter 和 list_item_food3.xml 支持显示已选净材种类副标签
2026-05-09 18:10:07 +08:00
lvmeng a9663a21f0 style(ui): 调整食物识别页面布局间距和移除测试代码
- 调整 activity_food_recognize.xml 中多个组件的 margin 间距
- 移除 FoodRecognizeActivity.kt 中的测试数据相关注释代码
- 为 list_item_food2.xml 添加可点击背景效果
- 清理 PrepareFoodActivity.kt 中的无用导入和调整常量值
- 优化 PrepareFoodActivity.kt 中的条件判断换行格式
2026-05-09 14:45:18 +08:00
lvmeng d060305417 fix(food): 修复Food类的equals和hashCode方法
- 在equals方法中添加version字段比较
- 在hashCode方法中添加version字段哈希计算
- 确保对象比较的一致性
2026-05-09 13:53:00 +08:00
lvmeng bb319ee4ee feat(network): 新增菜品向量采集数据上传接口及调用链路
- ApiService/RemoteRepository/NetViewModel 新增 uploadFoodVectorData 三层实现,新增 uploadFoodVectorDataBatch 供分批循环调用直接 await 结果
- VectorCollectionFragment 启用 upload() 链路:调用 uploadFoodVectorDataBatch 分批上传,去掉 runBlocking/runOnUiThread,统一在协程主线程更新 UI
- ImageUploader 修正包路径并将 onProgress 改为 suspend lambda
- Food 实体新增 version 字段,同步 ObjectBox model
- SeasoningSelectDialog 切换为真实接口,FoodSearchDialog 注释模拟数据
- 调整删除按钮 drawable 为圆形并对列表 ItemView 增加 tint
2026-05-09 13:51:15 +08:00
lvmeng bb1ecbde43 feat: 新增 goodsCode 字段并同步迁移 DB v11,修复 IPv6 WebSocket 地址格式,重构 VectorCollectionFragment 相机回调为协程 2026-05-09 10:51:43 +08:00
lvmeng 7a03ab8108 refactor(activity): 统一 cook/submit 中调料数据处理逻辑,先清除 materialType==3 再从 adapter 覆盖写入 2026-05-09 10:51:04 +08:00
27 changed files with 645 additions and 287 deletions
+6 -1
View File
@@ -5,7 +5,7 @@
"entities": [ "entities": [
{ {
"id": "1:6465153867413579585", "id": "1:6465153867413579585",
"lastPropertyId": "4:3660727688686055762", "lastPropertyId": "5:7592685238899911344",
"name": "Food", "name": "Food",
"properties": [ "properties": [
{ {
@@ -30,6 +30,11 @@
"indexId": "1:5497952887792054610", "indexId": "1:5497952887792054610",
"type": 28, "type": 28,
"flags": 8 "flags": 8
},
{
"id": "5:7592685238899911344",
"name": "version",
"type": 9
} }
], ],
"relations": [] "relations": []
@@ -2,6 +2,7 @@ package com.shuwei.dish.match.adapter
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
@@ -24,6 +25,15 @@ class Food3Adapter(list: MutableList<GoodsItem>) :
text = item.goodsName text = item.goodsName
isChecked = item.isClicked isChecked = item.isClicked
} }
// 显示当前选中的净材种类,无数据时隐藏
holder.binding.tvRawMaterialsType.run {
if (item.rawMaterialsType.isNullOrEmpty()) {
visibility = View.GONE
} else {
visibility = View.VISIBLE
text = item.rawMaterialsType
}
}
} }
override fun onCreateViewHolder( override fun onCreateViewHolder(
@@ -25,7 +25,7 @@ import com.shuwei.dish.match.entity.SeasoningSlotEntity
CookFoodGoodsEntity::class, CookFoodGoodsEntity::class,
SeasoningSlotEntity::class, SeasoningSlotEntity::class,
], ],
version = 10, version = 12,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -232,6 +232,22 @@ val MIGRATION_9_10 = MigrationImpl(9, 10) { db ->
db.execSQL("DROP INDEX IF EXISTS dm_cook_food_foodId_IDX") db.execSQL("DROP INDEX IF EXISTS dm_cook_food_foodId_IDX")
} }
/**
* 版本10→11:为 dm_cook_food_goods 和 dm_seasoning 表新增 goodsCode(物料编码)字段
*/
val MIGRATION_10_11 = MigrationImpl(10, 11) { db ->
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN goodsCode TEXT DEFAULT ''")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN goodsCode TEXT DEFAULT ''")
}
/**
* 版本11→12:为 dm_cook_food_goods 和 dm_seasoning 表新增 rawMaterialsType(净材种类)字段
*/
val MIGRATION_11_12 = MigrationImpl(11, 12) { db ->
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN rawMaterialsType TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN rawMaterialsType TEXT DEFAULT NULL")
}
class DatabaseProvider(private val context: Context) { class DatabaseProvider(private val context: Context) {
// 单例模式确保全局唯一实例‌ // 单例模式确保全局唯一实例‌
val instance: AppDatabase by lazy { val instance: AppDatabase by lazy {
@@ -253,6 +269,8 @@ class DatabaseProvider(private val context: Context) {
.addMigrations(MIGRATION_7_8) .addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9) .addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10) .addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.addMigrations(MIGRATION_11_12)
.build() .build()
} }
} }
@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import com.shuwei.dish.match.base.DeviceRole import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
@@ -26,9 +27,14 @@ open class CommonDialog(
private var positiveText = "确认" private var positiveText = "确认"
private var neutralText: String? = null private var neutralText: String? = null
private var negativeClick: (() -> Unit)? = null private var negativeClick: (() -> Unit)? = null
private var positiveClick: (() -> Unit)? = null // 返回 true 时关闭弹窗,返回 false 时保持弹窗显示(用于校验不通过的场景)
private var positiveClick: (() -> Boolean)? = null
private var neutralClick: (() -> Unit)? = null private var neutralClick: (() -> Unit)? = null
private var dismissCallback: (() -> Unit)? = null private var dismissCallback: (() -> Unit)? = null
// 待添加到 llContent 的自定义 View,在 onCreate 后挂载
private var pendingContentView: View? = null
// pendingContentView 非空时对 tvContent 的灵活控制回调(可调整可见性、间距等)
private var tvContentConfigurator: ((TextView) -> Unit)? = null
/** 设置标题,为空时隐藏 */ /** 设置标题,为空时隐藏 */
fun setTitle(text: String): CommonDialog = apply { titleText = text } fun setTitle(text: String): CommonDialog = apply { titleText = text }
@@ -42,8 +48,17 @@ open class CommonDialog(
negativeClick = onClick negativeClick = onClick
} }
/** 设置右侧确认按钮文字及点击回调(3按钮模式下对应底部按钮) */ /** 设置右侧确认按钮文字及点击回调(3按钮模式下对应底部按钮),点击后自动关闭弹窗 */
fun setPositiveButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply { fun setPositiveButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply {
positiveText = text
positiveClick = onClick?.let { { it(); true } }
}
/**
* 设置右侧确认按钮文字及可拦截的点击回调
* 回调返回 true 时关闭弹窗,返回 false 时保持弹窗显示(用于校验不通过的场景)
*/
fun setPositiveButtonInterceptable(text: String, onClick: () -> Boolean): CommonDialog = apply {
positiveText = text positiveText = text
positiveClick = onClick positiveClick = onClick
} }
@@ -59,6 +74,16 @@ open class CommonDialog(
dismissCallback = callback dismissCallback = callback
} }
/**
* 向弹窗自定义内容区域添加 View,调用后 llContent 自动显示
* @param view 要嵌入的自定义布局(如单选列表)
* @param tvContentConfig 可选回调,用于在 pendingContentView 非空时灵活控制 tvContent 的可见性及间距等属性
*/
fun addContentView(view: View, tvContentConfig: ((TextView) -> Unit)? = null): CommonDialog = apply {
pendingContentView = view
tvContentConfigurator = tvContentConfig
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE) requestWindowFeature(Window.FEATURE_NO_TITLE)
@@ -88,8 +113,8 @@ open class CommonDialog(
dismiss() dismiss()
} }
binding.btnRight.setOnClickListener { binding.btnRight.setOnClickListener {
positiveClick?.invoke() // 回调返回 false 时阻止关闭,null 或 true 时正常关闭
dismiss() if (positiveClick?.invoke() != false) dismiss()
} }
// 3按钮点击事件(negativeClick/positiveClick 与2按钮模式共用) // 3按钮点击事件(negativeClick/positiveClick 与2按钮模式共用)
@@ -102,11 +127,17 @@ open class CommonDialog(
dismiss() dismiss()
} }
binding.btnBottom.setOnClickListener { binding.btnBottom.setOnClickListener {
positiveClick?.invoke() if (positiveClick?.invoke() != false) dismiss()
dismiss()
} }
setOnDismissListener { dismissCallback?.invoke() } setOnDismissListener { dismissCallback?.invoke() }
// 挂载自定义内容 View,并执行 tvContent 的灵活控制回调
pendingContentView?.let {
binding.llContent.addView(it)
binding.llContent.visibility = View.VISIBLE
tvContentConfigurator?.invoke(binding.tvContent)
}
} }
override fun show() { override fun show() {
@@ -50,27 +50,27 @@ class FoodSearchDialog(
private val binding = DialogFoodSearchBinding.inflate(LayoutInflater.from(activity)) private val binding = DialogFoodSearchBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<GoodsItem>() private val list = mutableListOf<GoodsItem>()
.apply { // .apply {
add(GoodsItem(goodsId = "200001", goodsName = "土豆丝")) // add(GoodsItem(goodsId = "200001", goodsName = "土豆丝"))
add(GoodsItem(goodsId = "200002", goodsName = "土豆片")) // add(GoodsItem(goodsId = "200002", goodsName = "土豆片"))
add(GoodsItem(goodsId = "200003", goodsName = "土豆丁")) // add(GoodsItem(goodsId = "200003", goodsName = "土豆丁"))
add(GoodsItem(goodsId = "200004", goodsName = "胡萝卜丝")) // add(GoodsItem(goodsId = "200004", goodsName = "胡萝卜丝"))
add(GoodsItem(goodsId = "200005", goodsName = "胡萝卜片")) // add(GoodsItem(goodsId = "200005", goodsName = "胡萝卜片"))
add(GoodsItem(goodsId = "200006", goodsName = "胡萝卜丁")) // add(GoodsItem(goodsId = "200006", goodsName = "胡萝卜丁"))
add(GoodsItem(goodsId = "200007", goodsName = "黄瓜丝")) // add(GoodsItem(goodsId = "200007", goodsName = "黄瓜丝"))
add(GoodsItem(goodsId = "200008", goodsName = "黄瓜片")) // add(GoodsItem(goodsId = "200008", goodsName = "黄瓜片"))
add(GoodsItem(goodsId = "200009", goodsName = "黄瓜丁")) // add(GoodsItem(goodsId = "200009", goodsName = "黄瓜丁"))
add(GoodsItem(goodsId = "200010", goodsName = "洋葱丝")) // add(GoodsItem(goodsId = "200010", goodsName = "洋葱丝"))
add(GoodsItem(goodsId = "200011", goodsName = "洋葱丁")) // add(GoodsItem(goodsId = "200011", goodsName = "洋葱丁"))
add(GoodsItem(goodsId = "200012", goodsName = "A")) // add(GoodsItem(goodsId = "200012", goodsName = "A"))
add(GoodsItem(goodsId = "200013", goodsName = "B")) // add(GoodsItem(goodsId = "200013", goodsName = "B"))
add(GoodsItem(goodsId = "200014", goodsName = "C")) // add(GoodsItem(goodsId = "200014", goodsName = "C"))
add(GoodsItem(goodsId = "200015", goodsName = "D")) // add(GoodsItem(goodsId = "200015", goodsName = "D"))
add(GoodsItem(goodsId = "200016", goodsName = "E")) // add(GoodsItem(goodsId = "200016", goodsName = "E"))
add(GoodsItem(goodsId = "200017", goodsName = "F")) // add(GoodsItem(goodsId = "200017", goodsName = "F"))
add(GoodsItem(goodsId = "200018", goodsName = "G")) // add(GoodsItem(goodsId = "200018", goodsName = "G"))
add(GoodsItem(goodsId = "200019", goodsName = "H")) // add(GoodsItem(goodsId = "200019", goodsName = "H"))
} // }
private val adapter = FoodAdapter(list).apply { private val adapter = FoodAdapter(list).apply {
isStateViewEnable = true isStateViewEnable = true
setOnItemClickListener { _, _, position -> setOnItemClickListener { _, _, position ->
@@ -135,13 +135,13 @@ class FoodSearchDialog(
initObserver() initObserver()
// // 若有默认食材名称,自动填充并触发搜索 // 若有默认食材名称,自动填充并触发搜索
// //if (!defGoodsName.isNullOrBlank()) { //if (!defGoodsName.isNullOrBlank()) {
//binding.etSheetInput.setText(defGoodsName) //binding.etSheetInput.setText(defGoodsName)
// pageNo = 1 pageNo = 1
// getGoodsList() getGoodsList()
// KeyboardUtil.hideKeyboard(binding.root) KeyboardUtil.hideKeyboard(binding.root)
// //} //}
} }
/** 弹窗显示时在 Window.Callback 层提前拦截触摸,解决 BottomSheet 拖拽与列表滑动的手势冲突 */ /** 弹窗显示时在 Window.Callback 层提前拦截触摸,解决 BottomSheet 拖拽与列表滑动的手势冲突 */
@@ -186,25 +186,25 @@ class SeasoningSelectDialog(
* 请求调料列表 * 请求调料列表
*/ */
private fun getGoodsList() { private fun getGoodsList() {
// activity.netViewModel.queryGoodsList( activity.netViewModel.queryGoodsList(
// goodsType = "1", goodsType = "1",
// pageNum = pageNo, pageNum = pageNo,
// pageSize = PAGE_SIZE, pageSize = PAGE_SIZE,
// goodsName = goodsName.takeUnless { it.isNullOrBlank() } goodsName = goodsName.takeUnless { it.isNullOrBlank() }
// )
// TODO: 联调时注释掉下方模拟数据,改为真实接口调用
val nameList = listOf(
"", "白砂糖","冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香",
"料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "", "", "", "香菜", ""
) )
val mockData = mutableListOf<GoodsItem>() // // TODO: 联调时注释掉下方模拟数据,改为真实接口调用
nameList.forEachIndexed { index, name -> // val nameList = listOf(
val padString = "${index + 1}".padStart(2, '0') // "盐", "白砂糖","冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香",
mockData.add(GoodsItem(goodsId = "1000$padString", goodsName = name)) // "料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "葱", "蒜", "姜", "香菜", "油"
} // )
loadGoodsList(mockData) //
// 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)
} }
/** /**
@@ -66,6 +66,12 @@ class CookFoodGoodsEntity(
var materId: String? = "", var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var isDel: Int = 0, var isDel: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now()) var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now())
) : BaseEntity, Serializable { ) : BaseEntity, Serializable {
@@ -80,10 +86,12 @@ class CookFoodGoodsEntity(
popularName = popularName, popularName = popularName,
zjmCode = zjmCode, zjmCode = zjmCode,
materId = materId, materId = materId,
materCode = goodsCode,
materialType = materialType, materialType = materialType,
useWeight = useWeight, useWeight = useWeight,
relateionType = relateionType, relateionType = relateionType,
allEdible = allEdible, allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
) )
} }
@@ -13,6 +13,7 @@ data class GoodsItem(
val popularName: String? = null, val popularName: String? = null,
val zjmCode: String? = null, val zjmCode: String? = null,
val materId: String? = null, val materId: String? = null,
val materCode: String? = null,
// 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用) // 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用)
var materialType: Int = 0, var materialType: Int = 0,
@@ -30,9 +31,18 @@ data class GoodsItem(
// true-原始数据,false-非原始数据(识图新增) // true-原始数据,false-非原始数据(识图新增)
var isOriginalData: Boolean = true, var isOriginalData: Boolean = true,
// 识图匹配分数,-1 表示不显示 // 识图匹配分数,-1 表示不显示
var foodScore: Int = -1 var foodScore: Int = -1,
// 净材种类(当前选中值)
var rawMaterialsType: String? = null,
// 净材种类候选列表(临时字段,逗号分隔,不持久化)
var rawMaterialsTypes: String? = null
) : Serializable { ) : Serializable {
/**
* 物料编码,与查询接口返回的 materCode 同值,统一对外暴露为 goodsCode 供下游使用
*/
val goodsCode: String? get() = materCode
/** /**
* 转换为 CookFoodGoodsEntity,用于写入数据库或传给 SubmitFoodActivity * 转换为 CookFoodGoodsEntity,用于写入数据库或传给 SubmitFoodActivity
*/ */
@@ -42,11 +52,13 @@ data class GoodsItem(
popularName = popularName, popularName = popularName,
zjmCode = zjmCode, zjmCode = zjmCode,
materId = materId, materId = materId,
goodsCode = goodsCode,
materialType = materialType, materialType = materialType,
useWeight = useWeight, useWeight = useWeight,
relateionType = relateionType, relateionType = relateionType,
allEdible = allEdible, allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
) )
/** /**
@@ -58,9 +70,11 @@ data class GoodsItem(
popularName = popularName, popularName = popularName,
zjmCode = zjmCode, zjmCode = zjmCode,
materId = materId, materId = materId,
goodsCode = goodsCode,
materialType = materialType, materialType = materialType,
relateionType = relateionType, relateionType = relateionType,
allEdible = allEdible, allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
) )
} }
@@ -58,6 +58,12 @@ data class SeasoningEntity(
var materId: String? = "", var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var sort: Int = 9999, var sort: Int = 9999,
/** /**
* isDel表示数据是否已删除,1-已删除,为无效数据,0-正常使用,有效数据 * isDel表示数据是否已删除,1-已删除,为无效数据,0-正常使用,有效数据
@@ -4,9 +4,14 @@ import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.GoodsItem import com.shuwei.dish.match.entity.GoodsItem
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query import retrofit2.http.Query
import retrofit2.http.Url import retrofit2.http.Url
@@ -58,4 +63,14 @@ interface ApiService {
@Body param: MutableMap<String, Any> @Body param: MutableMap<String, Any>
): ApiResponse<MutableList<GoodsItem>?> ): ApiResponse<MutableList<GoodsItem>?>
/**
* 提交采集图片数据
*/
@Multipart
@POST
suspend fun uploadFoodVectorData(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/addWeighingCollectionData",
@PartMap params: Map<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?>
} }
@@ -10,6 +10,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
/** /**
* 网络请求 ViewModel,所有网络操作通过 RemoteRepository 发起,不直接依赖 apiService * 网络请求 ViewModel,所有网络操作通过 RemoteRepository 发起,不直接依赖 apiService
@@ -126,4 +131,44 @@ class NetViewModel(
_goodsListState.value = repository.queryGoodsList(param) _goodsListState.value = repository.queryGoodsList(param)
} }
} }
/**
* 上传采集菜品信息 UI 状态流,UI 层通过 collect 监听
*/
private val _uploadCollectState = MutableStateFlow<UiState<List<String>?>>(UiState.Idle)
val uploadCollectState: StateFlow<UiState<List<String>?>> = _uploadCollectState.asStateFlow()
/**
* 上传采集菜品信息(触发 StateFlow,适合单次上传场景)
*/
fun uploadFoodVectorData(
fileList: List<File>,
params: MutableMap<String, RequestBody>
) {
val fileParts = fileList.map { file ->
file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
.let { MultipartBody.Part.createFormData("foodPics", file.name, it) }
}
viewModelScope.launch {
_uploadCollectState.value = UiState.Loading
_uploadCollectState.value = repository.uploadFoodVectorData(params, fileParts)
}
}
/**
* 上传采集菜品信息(直接返回结果,适合分批循环上传场景)
* @param fileList 图片文件列表
* @param params 附加参数
* @return UiState 包装的结果
*/
suspend fun uploadFoodVectorDataBatch(
fileList: List<File>,
params: MutableMap<String, RequestBody>
): UiState<List<String>?> {
val fileParts = fileList.map { file ->
file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
.let { MultipartBody.Part.createFormData("foodPics", file.name, it) }
}
return repository.uploadFoodVectorData(params, fileParts)
}
} }
@@ -3,6 +3,9 @@ package com.shuwei.dish.match.net
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.GoodsItem import com.shuwei.dish.match.entity.GoodsItem
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.File
/** /**
* 网络数据仓库,封装所有 ApiService 调用 * 网络数据仓库,封装所有 ApiService 调用
@@ -90,4 +93,24 @@ class RemoteRepository {
} }
} }
/**
* 上次采集数据
* @param params 参数
* @param foodPics 图片
* @return UiState 包装的结果,Success 携带列表数据,Error 携带错误信息
*/
suspend fun uploadFoodVectorData(
params: Map<String, RequestBody>,
foodPics: List<MultipartBody.Part>
): UiState<List<String>?> {
return try {
val resp = apiService.uploadFoodVectorData(params = params, foodPics = foodPics)
if (resp.isSuccess()) UiState.Success(resp.data)
else UiState.Error(resp.code, resp.msg ?: "")
} catch (e: Exception) {
val ex = getApiException(e)
UiState.Error("-1", ex.errorMsg)
}
}
} }
@@ -11,7 +11,8 @@ data class Food(
var name: String? = null, var name: String? = null,
var foodIdx: Int = 0, var foodIdx: Int = 0,
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT) @HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
var foodVector: FloatArray? = null var foodVector: FloatArray? = null,
var version: String? = null
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -23,6 +24,7 @@ data class Food(
if (foodIdx != other.foodIdx) return false if (foodIdx != other.foodIdx) return false
if (name != other.name) return false if (name != other.name) return false
if (!foodVector.contentEquals(other.foodVector)) return false if (!foodVector.contentEquals(other.foodVector)) return false
if (version != other.version) return false
return true return true
} }
@@ -32,6 +34,8 @@ data class Food(
result = 31 * result + foodIdx result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0) result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0) result = 31 * result + (foodVector?.contentHashCode() ?: 0)
result = 31 * result + (version?.hashCode() ?: 0)
return result return result
} }
} }
@@ -86,7 +86,9 @@ class ScaleWebSocketClient {
Log.d(TAG, "放弃过期连接任务: $deviceId, version=$version") Log.d(TAG, "放弃过期连接任务: $deviceId, version=$version")
return return
} }
val url = "ws://$host:$port" // IPv6 地址在 URL 中需用方括号包裹,否则 OkHttp 会将冒号后内容误解析为端口
val formattedHost = if (host.contains(':')) "[$host]" else host
val url = "ws://$formattedHost:$port"
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
val ws = httpClient.newWebSocket(request, object : WebSocketListener() { val ws = httpClient.newWebSocket(request, object : WebSocketListener() {
@@ -176,26 +176,26 @@ class FoodRecognizeActivity : BaseActivity() {
} }
currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0) currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0)
binding.tvWeight.text = "${currentWeight}g" binding.tvWeight.text = "${currentWeight}g"
val tempList = arrayListOf<GoodsItem>() // val tempList = arrayListOf<GoodsItem>()
recognizeNameList?.apply { recognizeNameList?.apply {
forEach { forEach {
val goodsName = it.name.split("WP").first() val goodsName = it.name.split("WP").first()
goodsNameList.add(goodsName) goodsNameList.add(goodsName)
nameScoreMap.put(goodsName, it) nameScoreMap.put(goodsName, it)
// TODO: 测试数据,临时使用------------------------- // // TODO: 测试数据,临时使用-------------------------
val testData = buildTestData(it) // val testData = buildTestData(it)
tempList.add(testData) // tempList.add(testData)
// TODO: 测试数据,临时使用------------------------- // // TODO: 测试数据,临时使用-------------------------
} }
// TODO: 测试数据,临时使用------------------------- // // TODO: 测试数据,临时使用-------------------------
loadRecognizeList(tempList) // loadRecognizeList(tempList)
// TODO: 测试数据,临时使用------------------------- // // TODO: 测试数据,临时使用-------------------------
recognizeNameList = null recognizeNameList = null
} }
//TODO: 注释掉等接口正常再调用,临时使用测试数据 //TODO: 注释掉等接口正常再调用,临时使用测试数据
//netViewModel.queryGoodsList(goodsType = "0", goodsNames = goodsNameList) netViewModel.queryGoodsList(goodsType = "0", goodsNames = goodsNameList)
} }
private fun buildTestData(item: FoodModule.IdNameScore): GoodsItem { private fun buildTestData(item: FoodModule.IdNameScore): GoodsItem {
@@ -7,13 +7,10 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
@@ -31,7 +28,6 @@ import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AddressUtil import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.CameraUtils import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.ImageUtil import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.SpTool import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
@@ -43,7 +39,6 @@ import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import com.yanzhenjie.recyclerview.SwipeMenuItem import com.yanzhenjie.recyclerview.SwipeMenuItem
import com.yanzhenjie.recyclerview.SwipeRecyclerView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -56,11 +51,11 @@ import kotlin.math.abs
class PrepareFoodActivity : BaseActivity() { class PrepareFoodActivity : BaseActivity() {
companion object { companion object {
const val TAG = "CookActivity" const val TAG = "PrepareFoodActivity"
const val FOOD_ITEM = "foodItem" const val FOOD_ITEM = "foodItem"
const val PAGE_FROM = "pageFrom" const val PAGE_FROM = "pageFrom"
const val HOME = "home" const val HOME = "home"
const val WEIGHT_CHANGE_VALUE = 15 const val WEIGHT_CHANGE_VALUE = 5
const val WEIGHT_RECOGNIZE_VALUE = 10 const val WEIGHT_RECOGNIZE_VALUE = 10
/** 主材数量上限 */ /** 主材数量上限 */
@@ -224,9 +219,9 @@ class PrepareFoodActivity : BaseActivity() {
FoodRecognizeActivity.close() FoodRecognizeActivity.close()
return return
} }
if (!pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage || abs( if (
lastWeight - weight !pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage ||
) <= WEIGHT_CHANGE_VALUE abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE
) { ) {
Log.d( Log.d(
TAG, TAG,
@@ -98,7 +98,7 @@ class SettingActivity : BaseActivity() {
.setTitle("数据清除") .setTitle("数据清除")
.setContent("将物理删除全部 4 张表数据(菜品、食材、调料、槽位),此操作不可恢复,同时重置菜品模式和食堂id,确认继续?") .setContent("将物理删除全部 4 张表数据(菜品、食材、调料、槽位),此操作不可恢复,同时重置菜品模式和食堂id,确认继续?")
.setNegativeButton("取消") .setNegativeButton("取消")
.setPositiveButton("确认清除") { .setPositiveButton("确认") {
appViewModel.clearAllData { appViewModel.clearAllData {
SpTool.cookMode = -1 SpTool.cookMode = -1
SpTool.canteenId = "0" SpTool.canteenId = "0"
@@ -293,11 +293,6 @@ class SubmitFoodActivity : BaseActivity() {
toast("未获取到菜品或构成信息") toast("未获取到菜品或构成信息")
return return
} }
val seasoningData = seasoningAdapter.items
// if (seasoningData.isEmpty()) {
// toast("未获取到调料信息")
// return
// }
showLoading() showLoading()
val isSamplingData = food!!.cookMode == 1 val isSamplingData = food!!.cookMode == 1
@@ -316,23 +311,11 @@ class SubmitFoodActivity : BaseActivity() {
else -> it.dinnerType else -> it.dinnerType
} }
} }
// 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList // 先移除 goodsList 中已有的调料数据(materialType==3),再以 adapter 中最新调料覆盖
val tempSeasoningList = seasoningData.map { item -> goodsList?.removeAll { it.materialType == 3 }
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId) seasoningAdapter.items
CookFoodGoodsEntity().also { entity -> .filter { it.useWeight > 0.0 }
entity.goodsId = item.goodsId .forEach { goodsList?.add(buildSeasoningEntity(it)) }
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
entity.popularName = seasoning?.popularName
entity.zjmCode = seasoning?.zjmCode
entity.materId = seasoning?.materId
entity.relateionType = seasoning?.relateionType ?: 0
entity.allEdible = seasoning?.allEdible ?: true
entity.goodsOrRelationCode = seasoning?.goodsOrRelationCode
}
}
goodsList?.addAll(tempSeasoningList)
appViewModel.saveCookFoodAndGoods( appViewModel.saveCookFoodAndGoods(
cookMode = food!!.cookMode, cookMode = food!!.cookMode,
entity = cookFoodEntity, entity = cookFoodEntity,
@@ -348,28 +331,14 @@ class SubmitFoodActivity : BaseActivity() {
toast("未获取到菜品或构成信息") toast("未获取到菜品或构成信息")
return return
} }
val seasoningData = seasoningAdapter.items
showLoading() showLoading()
lifecycleScope.launch { lifecycleScope.launch {
// 将重量大于0且不重复的调料追加到 goodsList // 先移除 goodsList 中已有的调料数据(materialType==3),再以 adapter 中最新调料覆盖
seasoningData.forEach { item -> goodsList?.removeAll { it.materialType == 3 }
if (item.useWeight > 0.0 && goodsList?.none { it.goodsId == item.goodsId } == true) { seasoningAdapter.items
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId) .filter { it.useWeight > 0.0 }
goodsList?.add(CookFoodGoodsEntity().also { entity -> .forEach { goodsList?.add(buildSeasoningEntity(it)) }
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
entity.popularName = seasoning?.popularName
entity.zjmCode = seasoning?.zjmCode
entity.materId = seasoning?.materId
entity.relateionType = seasoning?.relateionType ?: 0
entity.allEdible = seasoning?.allEdible ?: true
entity.goodsOrRelationCode = seasoning?.goodsOrRelationCode
})
}
}
cookFoodEntity.let { cookFoodEntity.let {
it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList
@@ -392,6 +361,27 @@ class SubmitFoodActivity : BaseActivity() {
} }
} }
/**
* 将 adapter 中的调料 item 转换为 CookFoodGoodsEntity
* 从本地 dm_seasoning 表补充完整字段(popularName、zjmCode、materId、goodsCode 等)
*/
private suspend fun buildSeasoningEntity(item: SeasoningWeightAdapter.Item): CookFoodGoodsEntity {
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId)
return CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
entity.popularName = seasoning?.popularName
entity.zjmCode = seasoning?.zjmCode
entity.materId = seasoning?.materId
entity.goodsCode = seasoning?.goodsCode
entity.relateionType = seasoning?.relateionType ?: 0
entity.allEdible = seasoning?.allEdible ?: true
entity.goodsOrRelationCode = seasoning?.goodsOrRelationCode
}
}
private fun submitSuccess(isSamplingData: Boolean) { private fun submitSuccess(isSamplingData: Boolean) {
if (food?.foodId.isNullOrBlank()) { if (food?.foodId.isNullOrBlank()) {
dismissLoading() dismissLoading()
@@ -11,30 +11,46 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.view.Gravity
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.VectorCollectionAdapter import com.shuwei.dish.match.adapter.VectorCollectionAdapter
import com.shuwei.dish.match.adapter.Food3Adapter import com.shuwei.dish.match.adapter.Food3Adapter
import com.shuwei.dish.match.base.BaseFragment import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentVectorCollectionBinding import com.shuwei.dish.match.databinding.FragmentVectorCollectionBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.Loading import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.entity.FoodCollectionBean import com.shuwei.dish.match.entity.FoodCollectionBean
import com.shuwei.dish.match.entity.GoodsItem import com.shuwei.dish.match.entity.GoodsItem
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.objbox.Food
import com.shuwei.dish.match.objbox.FoodModule import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.objbox.ObjectBox
import com.shuwei.dish.match.ui.CollectedFoodActivity import com.shuwei.dish.match.ui.CollectedFoodActivity
import com.shuwei.dish.match.ui.SingleFragmentActivity import com.shuwei.dish.match.ui.SingleFragmentActivity
import com.shuwei.dish.match.utils.BitmapSaver import com.shuwei.dish.match.utils.BitmapSaver
import com.shuwei.dish.match.utils.CameraUtils import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.Debouncer import com.shuwei.dish.match.utils.Debouncer
import com.shuwei.dish.match.utils.ImageUploader
import com.shuwei.dish.match.utils.ImageUtil import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.LogSaveUtil import com.shuwei.dish.match.utils.LogSaveUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.toast
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import androidx.core.content.withStyledAttributes
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() { class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() {
@@ -53,30 +69,15 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
} }
} }
private val searchList = mutableListOf<GoodsItem>().apply { private val searchList = mutableListOf<GoodsItem>()
add(GoodsItem(goodsId = "200001", goodsName = "土豆丝"))
add(GoodsItem(goodsId = "200002", goodsName = "土豆片"))
add(GoodsItem(goodsId = "200003", goodsName = "土豆丁"))
add(GoodsItem(goodsId = "200004", goodsName = "胡萝卜丝"))
add(GoodsItem(goodsId = "200005", goodsName = "胡萝卜片"))
add(GoodsItem(goodsId = "200006", goodsName = "胡萝卜丁"))
add(GoodsItem(goodsId = "200007", goodsName = "黄瓜丝"))
add(GoodsItem(goodsId = "200008", goodsName = "黄瓜片"))
add(GoodsItem(goodsId = "200009", goodsName = "黄瓜丁"))
add(GoodsItem(goodsId = "200010", goodsName = "洋葱丝"))
add(GoodsItem(goodsId = "200011", goodsName = "洋葱丁"))
}
private var checkedItem: GoodsItem? = null private var checkedItem: GoodsItem? = null
private val searchAdapter by lazy { private val searchAdapter by lazy {
Food3Adapter(searchList).apply { Food3Adapter(searchList).apply {
setOnItemClickListener { adapter, view, position -> setOnItemClickListener { _, _, position ->
searchList.forEachIndexed { index, item -> item.isClicked = index == position } val item = searchList[position]
checkedItem = searchList[position] val types = item.rawMaterialsTypes
notifyDataSetChanged() // 弹出净材种类单选弹窗
checkedItem?.let { showRawMaterialsDialog(item, position, types)
selectedFoodId = it.goodsId
selectedFoodName = it.goodsName
}
} }
} }
} }
@@ -111,15 +112,16 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
//typealias CameraCallback = (Uri) -> Unit //typealias CameraCallback = (Uri) -> Unit
private val cameraCallback: (Uri) -> Unit = cameraCallback@{ uri -> private val cameraCallback: (Uri) -> Unit = { uri ->
lifecycleScope.launch {
try { try {
val index = vectorList.indexOfFirst { it.imageFile == null } val index = vectorList.indexOfFirst { it.imageFile == null }
if (index == -1) { if (index == -1) {
toast("每次只允许保存${MAX_COUNT}条数据") toast("每次只允许保存${MAX_COUNT}条数据")
hideWaitingDialog() hideWaitingDialog()
return@cameraCallback return@launch
} }
activity?.runOnUiThread { // 主线程更新 UI
vectorList[index].let { vectorList[index].let {
it.imageVector = null it.imageVector = null
it.bitmap = null it.bitmap = null
@@ -128,13 +130,11 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
it.imageUri = uri it.imageUri = uri
} }
vectorAdapter.notifyItemChanged(index) vectorAdapter.notifyItemChanged(index)
// 切到 IO 线程执行耗时的 Bitmap 解码
val bitmap = withContext(Dispatchers.IO) {
ImageUtil.uriToBitmap(requireActivity(), uri)
} }
// hideWaitingDialog() bitmap?.let { getImageVector(index, it) }
Thread {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap)
}
}.start()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
hideWaitingDialog() hideWaitingDialog()
@@ -142,28 +142,30 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
log("程序异常${e.message}") log("程序异常${e.message}")
} }
} }
private fun getImageVector(index: Int, bitmap: Bitmap) {
// val bitmap = BitmapCropper.cropCenter(
// original = srcBmp,
// targetWidth = 900, targetHeight = 900,
//// offsetX = 30, offsetY = 100
// )
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) {
e.printStackTrace()
toast("操作失败")
log("操作失败:${e.message}")
hideWaitingDialog()
return
} }
private suspend fun getImageVector(index: Int, bitmap: Bitmap) {
// 切到 IO 线程执行耗时的向量计算和文件保存
val (imageVector, file) = withContext(Dispatchers.IO) {
try {
val vector = FoodModule.bitmap2FloatArray(bitmap, false)
val file = BitmapSaver.saveToAppFilesDir( val file = BitmapSaver.saveToAppFilesDir(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg" bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
) )
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
vector to file
activity?.runOnUiThread { } catch (e: Exception) {
e.printStackTrace()
log("操作失败:${e.message}")
null to null
}
}
// 回到主线程处理结果
if (imageVector == null) {
toast("操作失败")
hideWaitingDialog()
return
}
vectorList[index].let { vectorList[index].let {
it.imageVector = imageVector it.imageVector = imageVector
it.bitmap = null it.bitmap = null
@@ -171,10 +173,7 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
it.imageFile = file it.imageFile = file
} }
vectorAdapter.notifyItemChanged(index) vectorAdapter.notifyItemChanged(index)
} if (bitmap.isRecycled.not()) bitmap.recycle()
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
hideWaitingDialog() hideWaitingDialog()
} }
@@ -235,16 +234,16 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
} }
binding.btnSave.setOnClickListener { binding.btnSave.setOnClickListener {
// if (checkedItem == null || checkedItem!!.isChecked.not()) { if (checkedItem == null || checkedItem!!.isClicked.not()) {
// toast("请选择菜品名称") toast("请选择菜品名称")
// return@setOnClickListener return@setOnClickListener
// } }
// val count = foodCollectionList.count { it.imageFile != null } val count = vectorList.count { it.imageFile != null }
// if (count == 0) { if (count == 0) {
// toast("请拍摄菜品照片") toast("请拍摄菜品照片")
// return@setOnClickListener return@setOnClickListener
// } }
// upload() upload()
} }
binding.editFoodName.setOnEditorActionListener { v, actionId, event -> binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
@@ -279,79 +278,196 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
searchFood() searchFood()
} }
@SuppressLint("NotifyDataSetChanged")
private fun upload() { private fun upload() {
// lifecycleScope.launch { lifecycleScope.launch {
// val totalFileCount = foodCollectionList.count { it.imageFile != null } val totalFileCount = vectorList.count { it.imageFile != null }
// showWaitingDialog2("图片上传中0/$totalFileCount") showWaitingDialog("图片上传中0/$totalFileCount")
// val params = HashMap<String, RequestBody>() val params = mutableMapOf<String, RequestBody>()
// //params["placeId"] = restId.toRequestBody() params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
// params["foodId"] = checkedItem!!.foodId.toRequestBody() params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
// params["foodName"] = checkedItem!!.foodName!!.toRequestBody() //净材种类,逗号分隔
// params["version"] = GlobalData.foodModelVersion.toRequestBody() // params["rawMaterialsTypes"] = "".toRequestBody()
// //params["version"] = "1.0.0".toRequestBody()
// ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
// val files = batch.map { it.imageFile } ImageUploader(totalList = vectorList, uploadImage = { batch ->
// val foodVectorList = batch.filter { it.imageVector != null }.map { val files = batch.mapNotNull { it.imageFile }
// it.imageVector!!.joinToString( val foodVectorList = batch.filter { it.imageVector != null }.map {
// separator = ",", prefix = "[", postfix = "]" it.imageVector!!.joinToString(
// ) separator = ",", prefix = "[", postfix = "]"
// } )
// val foodVectorJson = }
// foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]") val foodVectorJson =
// log("json=$foodVectorJson") foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
// params["foodVector"] = foodVectorJson.toRequestBody() log("json=$foodVectorJson")
// viewModel?.uploadCollectFoodPics(files, params) params["foodVector"] = foodVectorJson.toRequestBody()
// }, onProgress = { count, batch, idList -> // 直接 await 单批上传结果,成功返回 id 列表,失败返回 null 触发 onError
// //batch.forEach { val result = currentActivity.netViewModel.uploadFoodVectorDataBatch(files, params)
// // it.uploadSuccess = true if (result is UiState.Success) result.data else null
// //} }, onProgress = { count, batch, idList ->
// runBlocking { showWaitingDialog("图片上传中$count/$totalFileCount")
// activity?.runOnUiThread { val foodList = batch.mapIndexed { index, it ->
// showWaitingDialog("图片上传中$count/$totalFileCount") Food(
// } name = checkedItem!!.goodsName,
// val foodList = batch.mapIndexed { index, it -> foodVector = it.imageVector,
// Food( )
// collectId = if (index < idList.size) idList[index] else null, }
// foodId = checkedItem!!.foodId, ObjectBox.putAll(foodList)
// foodName = checkedItem!!.foodName, batch.forEach { it.isFinish = true }
// foodVector = it.imageVector, vectorAdapter.notifyDataSetChanged()
// version = GlobalData.foodModelVersion }, onError = {
// ) binding.root.postDelayed({
// } hideWaitingDialog()
// ObjectBox.putAll(foodList) toast("上传失败,请稍后重试")
// activity?.runOnUiThread { }, 1000)
// batch.forEach { it.isFinish = true } }, onComplete = {
// collectionAdapter.notifyDataSetChanged() binding.root.postDelayed({
// } hideWaitingDialog()
// } toast("上传成功")
// }, onError = { }, 1000)
// activity?.runOnUiThread { }).processUploads()
// binding.root.postDelayed({ }
// hideWaitingDialog()
// toast("上传失败,请稍后重试")
// }, 1000)
// }
// }, onComplete = {
// //vectorThread()
// binding.root.postDelayed({
// hideWaitingDialog()
// toast("上传成功")
// }, 1000)
// }).processUploads()
// }
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun searchFood() { fun searchFood() {
debouncer.debounce { debouncer.debounce {
searchByFoodName(binding.editFoodName.text.toString()) { val keyword = binding.editFoodName.text.toString().trim()
// searchFoodList.clear() // 通过 NetViewModel 查询净材数据(goodsType=0 食材)
// searchFoodList.addAll(it) currentActivity.netViewModel.queryGoodsList(
// searchFoodAdapter.notifyDataSetChanged() goodsType = "0",
goodsName = keyword.ifEmpty { null }
)
// 监听查询结果(一次性收集,避免重复订阅)
lifecycleScope.launch {
currentActivity.netViewModel.goodsListState.collect { state ->
when (state) {
is UiState.Success -> {
var list: List<GoodsItem> = state.data ?: emptyList()
// TODO: 接口暂无数据时使用假数据进行效果测试,联调完成后删除
if (list.isEmpty()) {
list = mutableListOf(
GoodsItem(goodsId = "1", goodsName = "土豆", rawMaterialsType = "土豆丝,土豆条,土豆片,土豆丁"),
GoodsItem(goodsId = "2", goodsName = "胡萝卜", rawMaterialsType = "胡萝卜丝,胡萝卜片,胡萝卜丁"),
GoodsItem(goodsId = "3", goodsName = "黄瓜", rawMaterialsType = "黄瓜丝,黄瓜片,黄瓜丁,黄瓜块"),
GoodsItem(goodsId = "4", goodsName = "洋葱", rawMaterialsType = "洋葱丝,洋葱丁,洋葱圈"),
GoodsItem(goodsId = "5", goodsName = "白菜", rawMaterialsType = "白菜丝,白菜块,白菜叶"),
GoodsItem(goodsId = "6", goodsName = "豆腐", rawMaterialsType = "豆腐块,豆腐丁"),
GoodsItem(goodsId = "7", goodsName = "猪肉", rawMaterialsType = "猪肉丝,猪肉片,猪肉丁,猪肉块"),
GoodsItem(goodsId = "8", goodsName = "鸡胸肉", rawMaterialsType = "鸡胸肉丝,鸡胸肉片,鸡胸肉丁"),
GoodsItem(goodsId = "9", goodsName = "牛肉", rawMaterialsType = "牛肉丝,牛肉片,牛肉块"),
GoodsItem(goodsId = "10", goodsName = "虾仁", rawMaterialsType = "整虾仁,切段虾仁"),
GoodsItem(goodsId = "11", goodsName = "茄子", rawMaterialsType = "茄子丝,茄子片,茄子块,茄子条"),
GoodsItem(goodsId = "12", goodsName = "青椒", rawMaterialsType = "青椒丝,青椒片,青椒块"),
GoodsItem(goodsId = "13", goodsName = "西红柿", rawMaterialsType = "西红柿片,西红柿块,西红柿丁"),
GoodsItem(goodsId = "14", goodsName = "芹菜", rawMaterialsType = "芹菜段,芹菜丝"),
GoodsItem(goodsId = "15", goodsName = "莲藕", rawMaterialsType = "莲藕片,莲藕丁,莲藕丝"),
// 以下5条 rawMaterialsType 为空,测试无净材数据场景
GoodsItem(goodsId = "16", goodsName = "菠菜"),
GoodsItem(goodsId = "17", goodsName = "生菜"),
GoodsItem(goodsId = "18", goodsName = "香菇"),
GoodsItem(goodsId = "19", goodsName = "木耳"),
GoodsItem(goodsId = "20", goodsName = "豆芽"),
)
}
// 将 rawMaterialsType 转存到 rawMaterialsTypes,并清空 rawMaterialsType
list.forEach { item ->
item.rawMaterialsTypes = item.rawMaterialsType
item.rawMaterialsType = null
}
searchList.clear()
searchList.addAll(list)
searchAdapter.notifyDataSetChanged()
}
else -> Unit
} }
} }
} }
}
}
/**
* 弹出净材种类单选弹窗
* @param item 当前点击的食材项
* @param position 在 searchList 中的位置
* @param types 净材种类字符串(逗号分隔),为空时提示无数据
*/
@SuppressLint("NotifyDataSetChanged")
private fun showRawMaterialsDialog(item: GoodsItem, position: Int, types: String?) {
val typeList = types?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList()
val dialog = CommonDialog(requireContext())
.setTitle("选择净材种类")
.setNegativeButton("取消")
if (typeList.isEmpty()) {
// 无净材数据时仅展示提示文字
dialog.setContent("无净材数据")
.setPositiveButton("确认") {}
} else {
// 构建单选 RadioGroup 作为自定义内容
val radioGroup = RadioGroup(requireContext()).apply {
orientation = RadioGroup.VERTICAL
// RadioGroup 本身居中,子项 wrap_content 宽度,整体图标+文字自然居中
gravity = Gravity.CENTER_HORIZONTAL
typeList.forEachIndexed { index, typeName ->
val rb = RadioButton(requireContext()).apply {
id = index
text = typeName
textSize = 26f
layoutParams = RadioGroup.LayoutParams(
RadioGroup.LayoutParams.WRAP_CONTENT,
RadioGroup.LayoutParams.WRAP_CONTENT
)
setPadding(30.dp, 15.dp, 30.dp, 15.dp)
// 移除默认点击水波纹背景,改用 foreground 实现 selectableItemBackground 效果
setBackgroundResource(0)
val attrs = intArrayOf(android.R.attr.selectableItemBackground)
context.withStyledAttributes(null, attrs) {
foreground = getDrawable(0)
}
// 若当前项已有选中值则默认勾选对应选项
if (typeName == item.rawMaterialsType) isChecked = true
// 通过 InsetDrawable 在 buttonDrawable 右侧追加 80dp 间距,实现图标与文字的间距控制
buttonDrawable?.let { orig ->
buttonDrawable = android.graphics.drawable.InsetDrawable(
orig, 30.dp, 0, 30.dp, 0
)
}
}
addView(rb)
}
}
dialog.addContentView(radioGroup) { tvContent ->
tvContent.gone()
}
dialog.setPositiveButtonInterceptable("确认") {
val checkedId = radioGroup.checkedRadioButtonId
if (checkedId == -1) {
toast("请选择净材种类")
// 返回 false 阻止弹窗关闭
return@setPositiveButtonInterceptable false
}
val selectedType = typeList[checkedId]
// 清空其他已选项的状态
searchList.forEachIndexed { index, it ->
if (index != position) {
it.isClicked = false
it.rawMaterialsType = null
}
}
// 设置当前选中项
item.isClicked = true
item.rawMaterialsType = selectedType
checkedItem = item
selectedFoodId = item.goodsId
selectedFoodName = item.goodsName
searchAdapter.notifyDataSetChanged()
true
}
}
dialog.show()
}
var clickIndex = -1 var clickIndex = -1
@@ -396,7 +512,6 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
LogSaveUtil.saveLogFile(msg) LogSaveUtil.saveLogFile(msg)
} }
private fun searchByFoodName(name: String, block: (String) -> Unit) {}
private fun hideWaitingDialog() { private fun hideWaitingDialog() {
currentActivity.dismissLoading() currentActivity.dismissLoading()
} }
@@ -0,0 +1,38 @@
package com.shuwei.dish.match.utils
import com.shuwei.dish.match.entity.FoodCollectionBean
import java.util.concurrent.atomic.AtomicInteger
class ImageUploader(
private val totalList: List<FoodCollectionBean>,
private val uploadImage: suspend (List<FoodCollectionBean>) -> List<String>?,
private val onProgress: suspend (Int, List<FoodCollectionBean>, List<String>) -> Unit,
private val onError: (List<FoodCollectionBean>) -> Unit,
private val onComplete: () -> Unit
) {
companion object {
private const val BATCH_SIZE = 3
}
private val uploadedCount = AtomicInteger(0)
suspend fun processUploads() {
val batches = totalList.filter { it.imageVector!=null }.chunked(BATCH_SIZE)
for (batch in batches) {
val idList = uploadImage(batch)
if (idList.isNullOrEmpty()) {
//println("上传失败,终止流程")
onError(batch)
return
}
val fileCount = batch.count { it.imageFile!=null }
uploadedCount.addAndGet(fileCount)
onProgress(uploadedCount.get(), batch, idList)
//println("已上传 ${uploadedCount.get()}/$totalImages")
}
onComplete()
//println("流程完成,总计上传 ${uploadedCount.get()} 张图片")
}
}
@@ -1,12 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 绿色圆背景 + 白色垃圾桶图标,替代红色的 ic_delete_red.png --> <!-- 绿色圆背景 + 白色垃圾桶图标,替代红色的 ic_delete_red.png -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 绿色圆角背景 --> <!-- 绿色圆角背景 -->
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="oval">
<solid android:color="#00BC71" /> <solid android:color="#00BC71" />
<corners android:radius="10dp" /> <padding
android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp" />
</shape> </shape>
</item> </item>
@@ -32,7 +32,7 @@
android:id="@+id/flListContainer" android:id="@+id/flListContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="260dp" android:layout_height="260dp"
android:layout_marginTop="12dp"> android:layout_marginTop="20dp">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRecognizeList" android:id="@+id/rvRecognizeList"
@@ -85,7 +85,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="20dp"
android:background="@drawable/shape_white_12_corners" android:background="@drawable/shape_white_12_corners"
android:orientation="vertical"> android:orientation="vertical">
@@ -119,7 +119,7 @@
android:id="@+id/btnChangeFood" android:id="@+id/btnChangeFood"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="90dp" android:layout_height="90dp"
android:layout_marginTop="30dp" android:layout_marginTop="40dp"
android:layout_marginHorizontal="15dp" android:layout_marginHorizontal="15dp"
android:background="@drawable/shape_green_bg" android:background="@drawable/shape_green_bg"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
@@ -134,7 +134,7 @@
android:id="@+id/btnCancel" android:id="@+id/btnCancel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="90dp" android:layout_height="90dp"
android:layout_marginVertical="30dp" android:layout_marginVertical="40dp"
android:layout_marginHorizontal="15dp" android:layout_marginHorizontal="15dp"
android:background="@drawable/shape_white_12_corners" android:background="@drawable/shape_white_12_corners"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
+10
View File
@@ -37,6 +37,16 @@
android:textSize="26sp" android:textSize="26sp"
tools:text="这是弹窗的内容信息,请确认"/> tools:text="这是弹窗的内容信息,请确认"/>
<!-- 自定义内容区域,默认隐藏,通过 CommonDialog.addContentView 动态填充 -->
<LinearLayout
android:id="@+id/llContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:orientation="vertical"
android:visibility="gone" />
<!-- 分割线 --> <!-- 分割线 -->
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -18,6 +18,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:text="点击分格配置调料" android:text="点击分格配置调料"
android:includeFontPadding="false" android:includeFontPadding="false"
android:textStyle="bold"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" /> android:textSize="20sp" />
</FrameLayout> </FrameLayout>
@@ -17,6 +17,7 @@
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:gravity="center" android:gravity="center"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/shape_white_f6_10_corners" android:background="@drawable/shape_white_f6_10_corners"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="32sp" android:textSize="32sp"
+26 -6
View File
@@ -1,18 +1,38 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tvFoodName"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_marginHorizontal="14dp"
android:layout_marginVertical="14dp"
android:background="@drawable/bg_item_search" android:background="@drawable/bg_item_search"
android:paddingHorizontal="15dp"
android:paddingVertical="10dp">
<!-- 食材名称,选中状态通过 CheckedTextView 的 checked 驱动背景/文字色变化 -->
<CheckedTextView
android:id="@+id/tvFoodName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:layout_marginHorizontal="14dp"
android:layout_marginVertical="14dp"
android:paddingHorizontal="15dp"
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/color_item_search" android:textColor="@color/color_item_search"
android:textSize="30sp" android:textSize="28sp"
android:textStyle="bold" android:textStyle="bold"
tools:text="土豆丝" /> tools:text="土豆丝" />
<!-- 当前选中的净材种类,小字显示,无数据时隐藏 -->
<TextView
android:id="@+id/tvRawMaterialsType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center|bottom"
android:textColor="@color/black999"
android:textSize="18sp"
android:visibility="gone"
tools:text="切丝" />
</FrameLayout>
@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="212dp" android:layout_width="212dp"
android:layout_height="159dp" android:layout_height="159dp"
android:layout_margin="16dp" android:layout_margin="16dp"
android:background="@drawable/bg_gray2"> android:background="@drawable/bg_gray2"
tools:ignore="ContentDescription">
<ImageView <ImageView
android:id="@+id/imageView" android:id="@+id/imageView"
@@ -27,5 +29,6 @@
android:layout_height="36dp" android:layout_height="36dp"
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
android:padding="5dp" android:padding="5dp"
android:src="@drawable/ic_finish" /> android:src="@drawable/ic_finish"
app:tint="@color/dish_green" />
</FrameLayout> </FrameLayout>