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": [
{
"id": "1:6465153867413579585",
"lastPropertyId": "4:3660727688686055762",
"lastPropertyId": "5:7592685238899911344",
"name": "Food",
"properties": [
{
@@ -30,6 +30,11 @@
"indexId": "1:5497952887792054610",
"type": 28,
"flags": 8
},
{
"id": "5:7592685238899911344",
"name": "version",
"type": 9
}
],
"relations": []
@@ -2,6 +2,7 @@ package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
@@ -24,6 +25,15 @@ class Food3Adapter(list: MutableList<GoodsItem>) :
text = item.goodsName
isChecked = item.isClicked
}
// 显示当前选中的净材种类,无数据时隐藏
holder.binding.tvRawMaterialsType.run {
if (item.rawMaterialsType.isNullOrEmpty()) {
visibility = View.GONE
} else {
visibility = View.VISIBLE
text = item.rawMaterialsType
}
}
}
override fun onCreateViewHolder(
@@ -25,7 +25,7 @@ import com.shuwei.dish.match.entity.SeasoningSlotEntity
CookFoodGoodsEntity::class,
SeasoningSlotEntity::class,
],
version = 10,
version = 12,
exportSchema = true
)
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")
}
/**
* 版本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) {
// 单例模式确保全局唯一实例‌
val instance: AppDatabase by lazy {
@@ -253,6 +269,8 @@ class DatabaseProvider(private val context: Context) {
.addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.addMigrations(MIGRATION_11_12)
.build()
}
}
@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.View
import android.view.Window
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
@@ -26,9 +27,14 @@ open class CommonDialog(
private var positiveText = "确认"
private var neutralText: String? = 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 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 }
@@ -42,8 +48,17 @@ open class CommonDialog(
negativeClick = onClick
}
/** 设置右侧确认按钮文字及点击回调(3按钮模式下对应底部按钮) */
/** 设置右侧确认按钮文字及点击回调(3按钮模式下对应底部按钮),点击后自动关闭弹窗 */
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
positiveClick = onClick
}
@@ -59,6 +74,16 @@ open class CommonDialog(
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?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
@@ -88,8 +113,8 @@ open class CommonDialog(
dismiss()
}
binding.btnRight.setOnClickListener {
positiveClick?.invoke()
dismiss()
// 回调返回 false 时阻止关闭,null 或 true 时正常关闭
if (positiveClick?.invoke() != false) dismiss()
}
// 3按钮点击事件(negativeClick/positiveClick 与2按钮模式共用)
@@ -102,11 +127,17 @@ open class CommonDialog(
dismiss()
}
binding.btnBottom.setOnClickListener {
positiveClick?.invoke()
dismiss()
if (positiveClick?.invoke() != false) dismiss()
}
setOnDismissListener { dismissCallback?.invoke() }
// 挂载自定义内容 View,并执行 tvContent 的灵活控制回调
pendingContentView?.let {
binding.llContent.addView(it)
binding.llContent.visibility = View.VISIBLE
tvContentConfigurator?.invoke(binding.tvContent)
}
}
override fun show() {
@@ -50,27 +50,27 @@ class FoodSearchDialog(
private val binding = DialogFoodSearchBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<GoodsItem>()
.apply {
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 = "洋葱丁"))
add(GoodsItem(goodsId = "200012", goodsName = "A"))
add(GoodsItem(goodsId = "200013", goodsName = "B"))
add(GoodsItem(goodsId = "200014", goodsName = "C"))
add(GoodsItem(goodsId = "200015", goodsName = "D"))
add(GoodsItem(goodsId = "200016", goodsName = "E"))
add(GoodsItem(goodsId = "200017", goodsName = "F"))
add(GoodsItem(goodsId = "200018", goodsName = "G"))
add(GoodsItem(goodsId = "200019", goodsName = "H"))
}
// .apply {
// 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 = "洋葱丁"))
// add(GoodsItem(goodsId = "200012", goodsName = "A"))
// add(GoodsItem(goodsId = "200013", goodsName = "B"))
// add(GoodsItem(goodsId = "200014", goodsName = "C"))
// add(GoodsItem(goodsId = "200015", goodsName = "D"))
// add(GoodsItem(goodsId = "200016", goodsName = "E"))
// add(GoodsItem(goodsId = "200017", goodsName = "F"))
// add(GoodsItem(goodsId = "200018", goodsName = "G"))
// add(GoodsItem(goodsId = "200019", goodsName = "H"))
// }
private val adapter = FoodAdapter(list).apply {
isStateViewEnable = true
setOnItemClickListener { _, _, position ->
@@ -135,13 +135,13 @@ class FoodSearchDialog(
initObserver()
// // 若有默认食材名称,自动填充并触发搜索
// //if (!defGoodsName.isNullOrBlank()) {
// binding.etSheetInput.setText(defGoodsName)
// pageNo = 1
// getGoodsList()
// KeyboardUtil.hideKeyboard(binding.root)
// //}
// 若有默认食材名称,自动填充并触发搜索
//if (!defGoodsName.isNullOrBlank()) {
//binding.etSheetInput.setText(defGoodsName)
pageNo = 1
getGoodsList()
KeyboardUtil.hideKeyboard(binding.root)
//}
}
/** 弹窗显示时在 Window.Callback 层提前拦截触摸,解决 BottomSheet 拖拽与列表滑动的手势冲突 */
@@ -186,25 +186,25 @@ class SeasoningSelectDialog(
* 请求调料列表
*/
private fun getGoodsList() {
// activity.netViewModel.queryGoodsList(
// goodsType = "1",
// pageNum = pageNo,
// pageSize = PAGE_SIZE,
// goodsName = goodsName.takeUnless { it.isNullOrBlank() }
// )
// TODO: 联调时注释掉下方模拟数据,改为真实接口调用
val nameList = listOf(
"", "白砂糖","冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香",
"料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "", "", "", "香菜", ""
activity.netViewModel.queryGoodsList(
goodsType = "1",
pageNum = pageNo,
pageSize = PAGE_SIZE,
goodsName = goodsName.takeUnless { it.isNullOrBlank() }
)
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)
// // 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)
}
/**
@@ -66,6 +66,12 @@ class CookFoodGoodsEntity(
var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var isDel: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now())
) : BaseEntity, Serializable {
@@ -80,10 +86,12 @@ class CookFoodGoodsEntity(
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
materCode = goodsCode,
materialType = materialType,
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
}
@@ -13,6 +13,7 @@ data class GoodsItem(
val popularName: String? = null,
val zjmCode: String? = null,
val materId: String? = null,
val materCode: String? = null,
// 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用)
var materialType: Int = 0,
@@ -30,9 +31,18 @@ data class GoodsItem(
// true-原始数据,false-非原始数据(识图新增)
var isOriginalData: Boolean = true,
// 识图匹配分数,-1 表示不显示
var foodScore: Int = -1
var foodScore: Int = -1,
// 净材种类(当前选中值)
var rawMaterialsType: String? = null,
// 净材种类候选列表(临时字段,逗号分隔,不持久化)
var rawMaterialsTypes: String? = null
) : Serializable {
/**
* 物料编码,与查询接口返回的 materCode 同值,统一对外暴露为 goodsCode 供下游使用
*/
val goodsCode: String? get() = materCode
/**
* 转换为 CookFoodGoodsEntity,用于写入数据库或传给 SubmitFoodActivity
*/
@@ -42,11 +52,13 @@ data class GoodsItem(
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
goodsCode = goodsCode,
materialType = materialType,
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
/**
@@ -58,9 +70,11 @@ data class GoodsItem(
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
goodsCode = goodsCode,
materialType = materialType,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
}
@@ -58,6 +58,12 @@ data class SeasoningEntity(
var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var sort: Int = 9999,
/**
* 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.FoodRecord
import com.shuwei.dish.match.entity.GoodsItem
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.Url
@@ -36,7 +41,7 @@ interface ApiService {
*/
@POST
suspend fun searchFoodList(
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryPageList",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryPageList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<FoodRecord>?>
@@ -45,7 +50,7 @@ interface ApiService {
*/
@POST
suspend fun getSamplingList(
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryHistoryGoodsInfoList",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryHistoryGoodsInfoList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<FoodRecord>?>
@@ -54,8 +59,18 @@ interface ApiService {
*/
@POST
suspend fun queryGoodsList(
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryGoodsInfoList",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryGoodsInfoList",
@Body param: MutableMap<String, Any>
): 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.asStateFlow
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
@@ -126,4 +131,44 @@ class NetViewModel(
_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.FoodRecord
import com.shuwei.dish.match.entity.GoodsItem
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.File
/**
* 网络数据仓库,封装所有 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 foodIdx: Int = 0,
@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 {
if (this === other) return true
@@ -23,6 +24,7 @@ data class Food(
if (foodIdx != other.foodIdx) return false
if (name != other.name) return false
if (!foodVector.contentEquals(other.foodVector)) return false
if (version != other.version) return false
return true
}
@@ -32,6 +34,8 @@ data class Food(
result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
result = 31 * result + (version?.hashCode() ?: 0)
return result
}
}
@@ -86,7 +86,9 @@ class ScaleWebSocketClient {
Log.d(TAG, "放弃过期连接任务: $deviceId, version=$version")
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 ws = httpClient.newWebSocket(request, object : WebSocketListener() {
@@ -176,26 +176,26 @@ class FoodRecognizeActivity : BaseActivity() {
}
currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0)
binding.tvWeight.text = "${currentWeight}g"
val tempList = arrayListOf<GoodsItem>()
// val tempList = arrayListOf<GoodsItem>()
recognizeNameList?.apply {
forEach {
val goodsName = it.name.split("WP").first()
goodsNameList.add(goodsName)
nameScoreMap.put(goodsName, it)
// TODO: 测试数据,临时使用-------------------------
val testData = buildTestData(it)
tempList.add(testData)
// TODO: 测试数据,临时使用-------------------------
// // TODO: 测试数据,临时使用-------------------------
// val testData = buildTestData(it)
// tempList.add(testData)
// // TODO: 测试数据,临时使用-------------------------
}
// TODO: 测试数据,临时使用-------------------------
loadRecognizeList(tempList)
// TODO: 测试数据,临时使用-------------------------
// // TODO: 测试数据,临时使用-------------------------
// loadRecognizeList(tempList)
// // TODO: 测试数据,临时使用-------------------------
recognizeNameList = null
}
// TODO: 注释掉等接口正常再调用,临时使用测试数据
//netViewModel.queryGoodsList(goodsType = "0", goodsNames = goodsNameList)
//TODO: 注释掉等接口正常再调用,临时使用测试数据
netViewModel.queryGoodsList(goodsType = "0", goodsNames = goodsNameList)
}
private fun buildTestData(item: FoodModule.IdNameScore): GoodsItem {
@@ -7,13 +7,10 @@ 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
@@ -31,7 +28,6 @@ import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.CameraUtils
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.WeightUtil
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.visible
import com.yanzhenjie.recyclerview.SwipeMenuItem
import com.yanzhenjie.recyclerview.SwipeRecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -56,11 +51,11 @@ import kotlin.math.abs
class PrepareFoodActivity : BaseActivity() {
companion object {
const val TAG = "CookActivity"
const val TAG = "PrepareFoodActivity"
const val FOOD_ITEM = "foodItem"
const val PAGE_FROM = "pageFrom"
const val HOME = "home"
const val WEIGHT_CHANGE_VALUE = 15
const val WEIGHT_CHANGE_VALUE = 5
const val WEIGHT_RECOGNIZE_VALUE = 10
/** 主材数量上限 */
@@ -224,9 +219,9 @@ class PrepareFoodActivity : BaseActivity() {
FoodRecognizeActivity.close()
return
}
if (!pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage || abs(
lastWeight - weight
) <= WEIGHT_CHANGE_VALUE
if (
!pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage ||
abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE
) {
Log.d(
TAG,
@@ -98,7 +98,7 @@ class SettingActivity : BaseActivity() {
.setTitle("数据清除")
.setContent("将物理删除全部 4 张表数据(菜品、食材、调料、槽位),此操作不可恢复,同时重置菜品模式和食堂id,确认继续?")
.setNegativeButton("取消")
.setPositiveButton("确认清除") {
.setPositiveButton("确认") {
appViewModel.clearAllData {
SpTool.cookMode = -1
SpTool.canteenId = "0"
@@ -293,11 +293,6 @@ class SubmitFoodActivity : BaseActivity() {
toast("未获取到菜品或构成信息")
return
}
val seasoningData = seasoningAdapter.items
// if (seasoningData.isEmpty()) {
// toast("未获取到调料信息")
// return
// }
showLoading()
val isSamplingData = food!!.cookMode == 1
@@ -316,23 +311,11 @@ class SubmitFoodActivity : BaseActivity() {
else -> it.dinnerType
}
}
// 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList
val tempSeasoningList = seasoningData.map { item ->
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId)
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.relateionType = seasoning?.relateionType ?: 0
entity.allEdible = seasoning?.allEdible ?: true
entity.goodsOrRelationCode = seasoning?.goodsOrRelationCode
}
}
goodsList?.addAll(tempSeasoningList)
// 先移除 goodsList 中已有的调料数据(materialType==3),再以 adapter 中最新调料覆盖
goodsList?.removeAll { it.materialType == 3 }
seasoningAdapter.items
.filter { it.useWeight > 0.0 }
.forEach { goodsList?.add(buildSeasoningEntity(it)) }
appViewModel.saveCookFoodAndGoods(
cookMode = food!!.cookMode,
entity = cookFoodEntity,
@@ -348,28 +331,14 @@ class SubmitFoodActivity : BaseActivity() {
toast("未获取到菜品或构成信息")
return
}
val seasoningData = seasoningAdapter.items
showLoading()
lifecycleScope.launch {
// 将重量大于0且不重复的调料追加到 goodsList
seasoningData.forEach { item ->
if (item.useWeight > 0.0 && goodsList?.none { it.goodsId == item.goodsId } == true) {
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId)
goodsList?.add(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.relateionType = seasoning?.relateionType ?: 0
entity.allEdible = seasoning?.allEdible ?: true
entity.goodsOrRelationCode = seasoning?.goodsOrRelationCode
})
}
}
// 先移除 goodsList 中已有的调料数据(materialType==3),再以 adapter 中最新调料覆盖
goodsList?.removeAll { it.materialType == 3 }
seasoningAdapter.items
.filter { it.useWeight > 0.0 }
.forEach { goodsList?.add(buildSeasoningEntity(it)) }
cookFoodEntity.let {
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) {
if (food?.foodId.isNullOrBlank()) {
dismissLoading()
@@ -11,30 +11,46 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.view.Gravity
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.camera.view.PreviewView
import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
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.adapter.VectorCollectionAdapter
import com.shuwei.dish.match.adapter.Food3Adapter
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentVectorCollectionBinding
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.entity.FoodCollectionBean
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.ObjectBox
import com.shuwei.dish.match.ui.CollectedFoodActivity
import com.shuwei.dish.match.ui.SingleFragmentActivity
import com.shuwei.dish.match.utils.BitmapSaver
import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.Debouncer
import com.shuwei.dish.match.utils.ImageUploader
import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.LogSaveUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.toast
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import androidx.core.content.withStyledAttributes
@SuppressLint("NotifyDataSetChanged")
class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() {
@@ -53,30 +69,15 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
}
}
private val searchList = mutableListOf<GoodsItem>().apply {
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 val searchList = mutableListOf<GoodsItem>()
private var checkedItem: GoodsItem? = null
private val searchAdapter by lazy {
Food3Adapter(searchList).apply {
setOnItemClickListener { adapter, view, position ->
searchList.forEachIndexed { index, item -> item.isClicked = index == position }
checkedItem = searchList[position]
notifyDataSetChanged()
checkedItem?.let {
selectedFoodId = it.goodsId
selectedFoodName = it.goodsName
}
setOnItemClickListener { _, _, position ->
val item = searchList[position]
val types = item.rawMaterialsTypes
// 弹出净材种类单选弹窗
showRawMaterialsDialog(item, position, types)
}
}
}
@@ -111,15 +112,16 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
//typealias CameraCallback = (Uri) -> Unit
private val cameraCallback: (Uri) -> Unit = cameraCallback@{ uri ->
try {
val index = vectorList.indexOfFirst { it.imageFile == null }
if (index == -1) {
toast("每次只允许保存${MAX_COUNT}条数据")
hideWaitingDialog()
return@cameraCallback
}
activity?.runOnUiThread {
private val cameraCallback: (Uri) -> Unit = { uri ->
lifecycleScope.launch {
try {
val index = vectorList.indexOfFirst { it.imageFile == null }
if (index == -1) {
toast("每次只允许保存${MAX_COUNT}条数据")
hideWaitingDialog()
return@launch
}
// 主线程更新 UI
vectorList[index].let {
it.imageVector = null
it.bitmap = null
@@ -128,53 +130,50 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
it.imageUri = uri
}
vectorAdapter.notifyItemChanged(index)
}
// hideWaitingDialog()
Thread {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap)
// 切到 IO 线程执行耗时的 Bitmap 解码
val bitmap = withContext(Dispatchers.IO) {
ImageUtil.uriToBitmap(requireActivity(), uri)
}
}.start()
} catch (e: Exception) {
e.printStackTrace()
hideWaitingDialog()
toast("程序异常${e.message}")
log("程序异常${e.message}")
bitmap?.let { getImageVector(index, it) }
} catch (e: Exception) {
e.printStackTrace()
hideWaitingDialog()
toast("程序异常${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()
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(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
vector to file
} catch (e: Exception) {
e.printStackTrace()
log("操作失败:${e.message}")
null to null
}
}
// 回到主线程处理结果
if (imageVector == null) {
toast("操作失败")
log("操作失败:${e.message}")
hideWaitingDialog()
return
}
val file = BitmapSaver.saveToAppFilesDir(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
activity?.runOnUiThread {
vectorList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
vectorAdapter.notifyItemChanged(index)
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
vectorList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
vectorAdapter.notifyItemChanged(index)
if (bitmap.isRecycled.not()) bitmap.recycle()
hideWaitingDialog()
}
@@ -235,16 +234,16 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
}
binding.btnSave.setOnClickListener {
// if (checkedItem == null || checkedItem!!.isChecked.not()) {
// toast("请选择菜品名称")
// return@setOnClickListener
// }
// val count = foodCollectionList.count { it.imageFile != null }
// if (count == 0) {
// toast("请拍摄菜品照片")
// return@setOnClickListener
// }
// upload()
if (checkedItem == null || checkedItem!!.isClicked.not()) {
toast("请选择菜品名称")
return@setOnClickListener
}
val count = vectorList.count { it.imageFile != null }
if (count == 0) {
toast("请拍摄菜品照片")
return@setOnClickListener
}
upload()
}
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
@@ -279,80 +278,197 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
searchFood()
}
@SuppressLint("NotifyDataSetChanged")
private fun upload() {
// lifecycleScope.launch {
// val totalFileCount = foodCollectionList.count { it.imageFile != null }
// showWaitingDialog2("图片上传中0/$totalFileCount")
// val params = HashMap<String, RequestBody>()
// //params["placeId"] = restId.toRequestBody()
// params["foodId"] = checkedItem!!.foodId.toRequestBody()
// params["foodName"] = checkedItem!!.foodName!!.toRequestBody()
// params["version"] = GlobalData.foodModelVersion.toRequestBody()
//
// ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
// val files = batch.map { it.imageFile }
// val foodVectorList = batch.filter { it.imageVector != null }.map {
// it.imageVector!!.joinToString(
// separator = ",", prefix = "[", postfix = "]"
// )
// }
// val foodVectorJson =
// foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
// log("json=$foodVectorJson")
// params["foodVector"] = foodVectorJson.toRequestBody()
// viewModel?.uploadCollectFoodPics(files, params)
// }, onProgress = { count, batch, idList ->
// //batch.forEach {
// // it.uploadSuccess = true
// //}
// runBlocking {
// activity?.runOnUiThread {
// showWaitingDialog("图片上传中$count/$totalFileCount")
// }
// val foodList = batch.mapIndexed { index, it ->
// Food(
// collectId = if (index < idList.size) idList[index] else null,
// foodId = checkedItem!!.foodId,
// foodName = checkedItem!!.foodName,
// foodVector = it.imageVector,
// version = GlobalData.foodModelVersion
// )
// }
// ObjectBox.putAll(foodList)
// activity?.runOnUiThread {
// batch.forEach { it.isFinish = true }
// collectionAdapter.notifyDataSetChanged()
// }
// }
// }, onError = {
// activity?.runOnUiThread {
// binding.root.postDelayed({
// hideWaitingDialog()
// toast("上传失败,请稍后重试")
// }, 1000)
// }
// }, onComplete = {
// //vectorThread()
// binding.root.postDelayed({
// hideWaitingDialog()
// toast("上传成功")
// }, 1000)
// }).processUploads()
// }
lifecycleScope.launch {
val totalFileCount = vectorList.count { it.imageFile != null }
showWaitingDialog("图片上传中0/$totalFileCount")
val params = mutableMapOf<String, RequestBody>()
params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
//净材种类,逗号分隔
// params["rawMaterialsTypes"] = "".toRequestBody()
//params["version"] = "1.0.0".toRequestBody()
ImageUploader(totalList = vectorList, uploadImage = { batch ->
val files = batch.mapNotNull { it.imageFile }
val foodVectorList = batch.filter { it.imageVector != null }.map {
it.imageVector!!.joinToString(
separator = ",", prefix = "[", postfix = "]"
)
}
val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
log("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody()
// 直接 await 单批上传结果,成功返回 id 列表,失败返回 null 触发 onError
val result = currentActivity.netViewModel.uploadFoodVectorDataBatch(files, params)
if (result is UiState.Success) result.data else null
}, onProgress = { count, batch, idList ->
showWaitingDialog("图片上传中$count/$totalFileCount")
val foodList = batch.mapIndexed { index, it ->
Food(
name = checkedItem!!.goodsName,
foodVector = it.imageVector,
)
}
ObjectBox.putAll(foodList)
batch.forEach { it.isFinish = true }
vectorAdapter.notifyDataSetChanged()
}, onError = {
binding.root.postDelayed({
hideWaitingDialog()
toast("上传失败,请稍后重试")
}, 1000)
}, onComplete = {
binding.root.postDelayed({
hideWaitingDialog()
toast("上传成功")
}, 1000)
}).processUploads()
}
}
@SuppressLint("NotifyDataSetChanged")
fun searchFood() {
debouncer.debounce {
searchByFoodName(binding.editFoodName.text.toString()) {
// searchFoodList.clear()
// searchFoodList.addAll(it)
// searchFoodAdapter.notifyDataSetChanged()
val keyword = binding.editFoodName.text.toString().trim()
// 通过 NetViewModel 查询净材数据(goodsType=0 食材)
currentActivity.netViewModel.queryGoodsList(
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
@SuppressLint("NotifyDataSetChanged")
@@ -396,7 +512,6 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
LogSaveUtil.saveLogFile(msg)
}
private fun searchByFoodName(name: String, block: (String) -> Unit) {}
private fun hideWaitingDialog() {
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"?>
<!-- 绿色圆背景 + 白色垃圾桶图标,替代红色的 ic_delete_red.png -->
<!-- 绿色圆背景 + 白色垃圾桶图标,替代红色的 ic_delete_red.png -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 绿色圆角背景 -->
<item>
<shape android:shape="rectangle">
<shape android:shape="oval">
<solid android:color="#00BC71" />
<corners android:radius="10dp" />
<padding
android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp" />
</shape>
</item>
@@ -32,7 +32,7 @@
android:id="@+id/flListContainer"
android:layout_width="match_parent"
android:layout_height="260dp"
android:layout_marginTop="12dp">
android:layout_marginTop="20dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRecognizeList"
@@ -85,7 +85,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginTop="20dp"
android:background="@drawable/shape_white_12_corners"
android:orientation="vertical">
@@ -119,7 +119,7 @@
android:id="@+id/btnChangeFood"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="30dp"
android:layout_marginTop="40dp"
android:layout_marginHorizontal="15dp"
android:background="@drawable/shape_green_bg"
android:foreground="?android:attr/selectableItemBackground"
@@ -134,7 +134,7 @@
android:id="@+id/btnCancel"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginVertical="30dp"
android:layout_marginVertical="40dp"
android:layout_marginHorizontal="15dp"
android:background="@drawable/shape_white_12_corners"
android:foreground="?android:attr/selectableItemBackground"
+10
View File
@@ -37,6 +37,16 @@
android:textSize="26sp"
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
android:layout_width="match_parent"
@@ -18,6 +18,7 @@
android:layout_gravity="center"
android:text="点击分格配置调料"
android:includeFontPadding="false"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="20sp" />
</FrameLayout>
@@ -17,6 +17,7 @@
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:gravity="center"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/shape_white_f6_10_corners"
android:textColor="@color/black"
android:textSize="32sp"
+31 -11
View File
@@ -1,18 +1,38 @@
<?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"
android:id="@+id/tvFoodName"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@drawable/bg_item_search"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:layout_marginHorizontal="14dp"
android:layout_marginVertical="14dp"
android:background="@drawable/bg_item_search"
android:paddingHorizontal="15dp"
android:textAlignment="center"
android:textColor="@color/color_item_search"
android:textSize="30sp"
android:textStyle="bold"
tools:text="土豆丝" />
android:paddingVertical="10dp">
<!-- 食材名称,选中状态通过 CheckedTextView 的 checked 驱动背景/文字色变化 -->
<CheckedTextView
android:id="@+id/tvFoodName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textAlignment="center"
android:textColor="@color/color_item_search"
android:textSize="28sp"
android:textStyle="bold"
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"?>
<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"
android:layout_width="212dp"
android:layout_height="159dp"
android:layout_margin="16dp"
android:background="@drawable/bg_gray2">
android:background="@drawable/bg_gray2"
tools:ignore="ContentDescription">
<ImageView
android:id="@+id/imageView"
@@ -27,5 +29,6 @@
android:layout_height="36dp"
android:layout_gravity="end|bottom"
android:padding="5dp"
android:src="@drawable/ic_finish" />
android:src="@drawable/ic_finish"
app:tint="@color/dish_green" />
</FrameLayout>