Compare commits
12
Commits
ef13aa8307
...
7e72ce413b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e72ce413b | ||
|
|
5745791041 | ||
|
|
d412e03c6e | ||
|
|
b6389be898 | ||
|
|
919e43afba | ||
|
|
0b9b3e8988 | ||
|
|
803ae75e40 | ||
|
|
ec4c3f1232 | ||
|
|
63911fe432 | ||
|
|
8f15c0efb7 | ||
|
|
879af8426f | ||
|
|
dc947df6e3 |
@@ -45,6 +45,8 @@
|
||||
<activity
|
||||
android:name=".ui.InitActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|keyboard|navigation|uiMode|locale|layoutDirection|fontScale|density"
|
||||
tools:ignore="DiscouragedApi,LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -25,12 +25,14 @@ class FoodAdapter(list: MutableList<GoodsItem>) :
|
||||
item: GoodsItem?
|
||||
) {
|
||||
item ?: return
|
||||
holder.binding.tvGoodsInfo.run {
|
||||
text = item.goodsName
|
||||
holder.binding.root.run {
|
||||
setBackgroundResource(
|
||||
if (item.isClicked) R.drawable.shape_green_stroke
|
||||
else R.drawable.shape_white_12_corners
|
||||
)
|
||||
}
|
||||
holder.binding.tvGoodsInfo.run {
|
||||
text = item.goodsName
|
||||
setTextColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
@@ -46,6 +48,15 @@ class FoodAdapter(list: MutableList<GoodsItem>) :
|
||||
it.visible()
|
||||
val foodScoreText = "${(item.foodScore / 100.0).roundedDecimalPlace(2)}%"
|
||||
it.text = foodScoreText
|
||||
|
||||
}
|
||||
}
|
||||
if (item.rawMaterialsType.isNullOrBlank()) {
|
||||
holder.binding.tvRawMaterialsType.gone()
|
||||
} else {
|
||||
holder.binding.tvRawMaterialsType.let {
|
||||
it.visible()
|
||||
it.text = item.rawMaterialsType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,11 @@ class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
|
||||
clBlock.setOnClickListener {
|
||||
onItemClick?.invoke(holder.bindingAdapterPosition)
|
||||
}
|
||||
|
||||
tvDishName.text = data.goodsName
|
||||
if (data.isOriginalData) {
|
||||
tvDishName.text = data.goodsName
|
||||
} else {
|
||||
tvDishName.text = data.goodsName + "-${data.rawMaterialsType}"
|
||||
}
|
||||
tvDishType.text = if (data.materialType == 1) "主辅材:主材" else if (data.materialType == 2) "主辅材:辅材" else ""
|
||||
tvDishWeight.text =
|
||||
if (data.useWeight == null || data.useWeight == 0.toDouble()) "" else "${data.useWeight!!.roundedOneDecimalPlace()}克"
|
||||
@@ -69,7 +72,7 @@ class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
|
||||
else R.drawable.ic_dish_unselected
|
||||
)
|
||||
clBlock.setBackgroundResource(
|
||||
if (data.isItemClicked) R.drawable.shape_item_cook_dish
|
||||
if (data.isClicked) R.drawable.shape_item_cook_dish
|
||||
else R.drawable.shape_white_fb_15_corners
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,14 +6,25 @@ import android.view.ViewGroup
|
||||
import com.chad.library.adapter4.BaseQuickAdapter
|
||||
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||
import com.shuwei.dish.match.databinding.ItemSeasoningWeightBinding
|
||||
import com.shuwei.dish.match.databinding.ItemSeasoningWeightListBinding
|
||||
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
|
||||
|
||||
/**
|
||||
* 调料使用重量列表 Adapter
|
||||
* 用于 SubmitFoodActivity 展示各调料槽位的实时用量
|
||||
* 同名调料已在外部聚合,adapter 按 goodsName 作为唯一标识
|
||||
* 支持两种布局模式:
|
||||
* - VIEW_TYPE_GRID(默认):4 列卡片,名称与重量垂直排列
|
||||
* - VIEW_TYPE_LIST:2 列,名称与重量水平排列,名称占剩余空间
|
||||
*/
|
||||
class SeasoningWeightAdapter : BaseQuickAdapter<SeasoningWeightAdapter.Item, SeasoningWeightAdapter.VH>() {
|
||||
class SeasoningWeightAdapter : BaseQuickAdapter<SeasoningWeightAdapter.Item, QuickViewHolder>() {
|
||||
|
||||
companion object {
|
||||
/** 4 列卡片模式 */
|
||||
const val VIEW_TYPE_GRID = 0
|
||||
/** 2 列水平模式 */
|
||||
const val VIEW_TYPE_LIST = 1
|
||||
}
|
||||
|
||||
/** 调料展示数据项,key = goodsName(同名调料已聚合) */
|
||||
data class Item(
|
||||
@@ -23,6 +34,13 @@ class SeasoningWeightAdapter : BaseQuickAdapter<SeasoningWeightAdapter.Item, Sea
|
||||
val useWeight: Double
|
||||
)
|
||||
|
||||
/** 当前布局模式,由外部切换后调用 notifyDataSetChanged 生效 */
|
||||
var isListMode: Boolean = false
|
||||
|
||||
override fun getItemViewType(position: Int, list: List<Item>): Int {
|
||||
return if (isListMode) VIEW_TYPE_LIST else VIEW_TYPE_GRID
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个 item,按 goodsName 匹配;不存在则追加
|
||||
*/
|
||||
@@ -43,18 +61,21 @@ class SeasoningWeightAdapter : BaseQuickAdapter<SeasoningWeightAdapter.Item, Sea
|
||||
if (idx >= 0) removeAt(idx)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
|
||||
val binding = ItemSeasoningWeightBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
return VH(binding)
|
||||
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): QuickViewHolder {
|
||||
return if (viewType == VIEW_TYPE_LIST) {
|
||||
val binding = ItemSeasoningWeightListBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
QuickViewHolder(binding.root)
|
||||
} else {
|
||||
val binding = ItemSeasoningWeightBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
QuickViewHolder(binding.root)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, item: Item?) {
|
||||
override fun onBindViewHolder(holder: QuickViewHolder, position: Int, item: Item?) {
|
||||
item ?: return
|
||||
holder.binding.tvSeasoningName.text = item.goodsName
|
||||
val showWeight = "${item.useWeight.roundedDecimalPlace(1)}g"
|
||||
holder.binding.tvSeasoningWeight.text = showWeight
|
||||
holder.setText(com.shuwei.dish.match.R.id.tvSeasoningName, item.goodsName)
|
||||
holder.setText(com.shuwei.dish.match.R.id.tvSeasoningWeight, showWeight)
|
||||
}
|
||||
|
||||
inner class VH(var binding: ItemSeasoningWeightBinding) : QuickViewHolder(binding.root)
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import com.shuwei.dish.match.entity.SeasoningSlotEntity
|
||||
CookFoodGoodsEntity::class,
|
||||
SeasoningSlotEntity::class,
|
||||
],
|
||||
version = 12,
|
||||
version = 13,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -248,6 +248,14 @@ val MIGRATION_11_12 = MigrationImpl(11, 12) { db ->
|
||||
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN rawMaterialsType TEXT DEFAULT NULL")
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本12→13:为 dm_cook_food 表新增 submitState(接口提交状态)字段
|
||||
* 0-未提交(本地草稿/手动删除),1-接口提交成功后删除
|
||||
*/
|
||||
val MIGRATION_12_13 = MigrationImpl(12, 13) { db ->
|
||||
db.execSQL("ALTER TABLE dm_cook_food ADD COLUMN submitState INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
|
||||
class DatabaseProvider(private val context: Context) {
|
||||
// 单例模式确保全局唯一实例
|
||||
val instance: AppDatabase by lazy {
|
||||
@@ -271,6 +279,7 @@ class DatabaseProvider(private val context: Context) {
|
||||
.addMigrations(MIGRATION_9_10)
|
||||
.addMigrations(MIGRATION_10_11)
|
||||
.addMigrations(MIGRATION_11_12)
|
||||
.addMigrations(MIGRATION_12_13)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ class DbRepository {
|
||||
db.cookFoodDao().updateCookFood(item)
|
||||
}
|
||||
|
||||
suspend fun getCookFoodById(foodId: String, cookMode: Int) = withContext(Dispatchers.IO) {
|
||||
db.cookFoodDao().getCookFoodById(BaseApp.canteenId, foodId, cookMode)
|
||||
suspend fun getCookFoodById(foodId: String, cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
|
||||
db.cookFoodDao().getCookFoodById(BaseApp.canteenId, foodId, cookMode, dinnerType)
|
||||
}
|
||||
|
||||
suspend fun getCookFoodList(cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
|
||||
@@ -33,13 +33,23 @@ class DbRepository {
|
||||
db.cookFoodDao().countCookFood(BaseApp.canteenId, cookMode)
|
||||
}
|
||||
|
||||
suspend fun deleteCookFood(foodId: String, cookMode: Int) = withContext(Dispatchers.IO) {
|
||||
db.cookFoodDao().deleteCookFood(BaseApp.canteenId, foodId, cookMode)
|
||||
suspend fun deleteCookFood(foodId: String, cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
|
||||
db.cookFoodDao().deleteCookFood(BaseApp.canteenId, foodId, cookMode, dinnerType)
|
||||
}
|
||||
|
||||
/** 接口提交成功后逻辑删除菜品,同时把 submitState 置 1 */
|
||||
suspend fun markSubmittedAndDelete(foodId: String, cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
|
||||
db.cookFoodDao().markSubmittedAndDelete(BaseApp.canteenId, foodId, cookMode, dinnerType)
|
||||
}
|
||||
|
||||
suspend fun deleteCookFoodGoodsList(pid: Long, foodId: String) = withContext(Dispatchers.IO) {
|
||||
db.cookFoodGoodsDao().deleteCookFoodGoodsList(pid, foodId)
|
||||
}
|
||||
|
||||
/** 仅逻辑删除调料行(materialType = 3),主辅材不动 */
|
||||
suspend fun deleteSeasoningOnly(pid: Long, foodId: String) = withContext(Dispatchers.IO) {
|
||||
db.cookFoodGoodsDao().deleteSeasoningOnly(pid, foodId)
|
||||
}
|
||||
//--------------------------------------------------------------------------------------------
|
||||
|
||||
suspend fun insertGoodsList(items: MutableList<CookFoodGoodsEntity>) =
|
||||
|
||||
@@ -28,9 +28,9 @@ class DbViewModel : ViewModel() {
|
||||
private val _cookFoodDetailState = MutableStateFlow<CookFoodEntity?>(null)
|
||||
val cookFoodDetailState: StateFlow<CookFoodEntity?> = _cookFoodDetailState.asStateFlow()
|
||||
|
||||
fun getCookFoodById(foodId: String, cookMode: Int) {
|
||||
fun getCookFoodById(foodId: String, cookMode: Int, dinnerType: String) {
|
||||
viewModelScope.launch {
|
||||
_cookFoodDetailState.value = rep.getCookFoodById(foodId, cookMode)
|
||||
_cookFoodDetailState.value = rep.getCookFoodById(foodId, cookMode, dinnerType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,28 +68,49 @@ class DbViewModel : ViewModel() {
|
||||
entity: CookFoodEntity,
|
||||
list: MutableList<CookFoodGoodsEntity>?
|
||||
) {
|
||||
val data = rep.getCookFoodById(entity.foodId, cookMode)
|
||||
if (data != null) {
|
||||
// 存在数据先删除
|
||||
rep.deleteCookFood(entity.foodId, cookMode)
|
||||
rep.deleteCookFoodGoodsList(data.id, entity.foodId)
|
||||
}
|
||||
// 重新保存数据
|
||||
rep.insertCookFood(entity)
|
||||
val newData = rep.getCookFoodById(entity.foodId, cookMode)
|
||||
list?.let { it ->
|
||||
it.forEach { goods ->
|
||||
goods.id = 0
|
||||
goods.pid = newData?.id ?: 0
|
||||
val data = rep.getCookFoodById(entity.foodId, cookMode, entity.dinnerType ?: "0")
|
||||
if (data == null) {
|
||||
// 首次保存:主表 + 主辅材 + 调料 全量 insert
|
||||
rep.insertCookFood(entity)
|
||||
val newData = rep.getCookFoodById(entity.foodId, cookMode, entity.dinnerType ?: "0")
|
||||
list?.let {
|
||||
it.forEach { goods ->
|
||||
goods.id = 0
|
||||
goods.pid = newData?.id ?: 0
|
||||
}
|
||||
rep.insertGoodsList(it)
|
||||
}
|
||||
} else {
|
||||
// 已存在:主表与主辅材保持不变,仅重建调料部分,避免主表/主辅材累积逻辑删除脏数据
|
||||
rep.deleteSeasoningOnly(data.id, entity.foodId)
|
||||
val seasoningList = list?.filter { it.materialType == 3 }?.toMutableList()
|
||||
if (!seasoningList.isNullOrEmpty()) {
|
||||
seasoningList.forEach { goods ->
|
||||
goods.id = 0
|
||||
goods.pid = data.id
|
||||
goods.foodId = entity.foodId
|
||||
}
|
||||
rep.insertGoodsList(seasoningList)
|
||||
}
|
||||
rep.insertGoodsList(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteCookFoodAndGoods(cookMode: Int, foodId: String) {
|
||||
val data = rep.getCookFoodById(foodId, cookMode)
|
||||
suspend fun deleteCookFoodAndGoods(cookMode: Int, foodId: String, dinnerType: String) {
|
||||
val data = rep.getCookFoodById(foodId, cookMode, dinnerType)
|
||||
if (data != null) {
|
||||
rep.deleteCookFood(foodId = foodId, cookMode = cookMode)
|
||||
rep.deleteCookFood(foodId = foodId, cookMode = cookMode, dinnerType = dinnerType)
|
||||
rep.deleteCookFoodGoodsList(pid = data.id, foodId = foodId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口提交成功后删除本地菜品草稿
|
||||
* 与 [deleteCookFoodAndGoods] 的区别:主表 submitState 会被置 1,便于审计区分手动删除与接口删除
|
||||
*/
|
||||
suspend fun markSubmittedAndDelete(cookMode: Int, foodId: String, dinnerType: String) {
|
||||
val data = rep.getCookFoodById(foodId, cookMode, dinnerType)
|
||||
if (data != null) {
|
||||
rep.markSubmittedAndDelete(foodId = foodId, cookMode = cookMode, dinnerType = dinnerType)
|
||||
rep.deleteCookFoodGoodsList(pid = data.id, foodId = foodId)
|
||||
}
|
||||
}
|
||||
@@ -98,9 +119,10 @@ class DbViewModel : ViewModel() {
|
||||
|
||||
suspend fun getCookFoodGoodsList(
|
||||
foodId: String,
|
||||
cookMode: Int
|
||||
cookMode: Int,
|
||||
dinnerType: String
|
||||
): MutableList<CookFoodGoodsEntity> {
|
||||
val cookFood = rep.getCookFoodById(foodId, cookMode)
|
||||
val cookFood = rep.getCookFoodById(foodId, cookMode, dinnerType)
|
||||
return if (cookFood != null) rep.getCookFoodGoodsList(pid = cookFood.id, foodId = cookFood.foodId)
|
||||
else mutableListOf()
|
||||
}
|
||||
@@ -343,7 +365,8 @@ class DbViewModel : ViewModel() {
|
||||
fields = listOf(
|
||||
"id" to "${e.id}", "foodId" to "${e.foodId}", "foodName" to "${e.foodName}",
|
||||
"canteenId" to "${e.canteenId}", "foodWeight" to "${e.foodWeight}", "cookMode" to "${e.cookMode}",
|
||||
"dinnerType" to "${e.dinnerType}", "isDel" to "${e.isDel}", "createTime" to "${e.createTime}"
|
||||
"dinnerType" to "${e.dinnerType}", "isDel" to "${e.isDel}", "submitState" to "${e.submitState}",
|
||||
"createTime" to "${e.createTime}"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ interface CookFoodDao {
|
||||
@Update
|
||||
suspend fun updateCookFood(item: CookFoodEntity)
|
||||
|
||||
@Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
|
||||
suspend fun getCookFoodById(canteenId: String, foodId: String, cookMode: Int): CookFoodEntity?
|
||||
@Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode AND dinnerType = :dinnerType")
|
||||
suspend fun getCookFoodById(canteenId: String, foodId: String, cookMode: Int, dinnerType: String): CookFoodEntity?
|
||||
|
||||
@Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND cookMode = :cookMode AND dinnerType = :dinnerType ORDER BY createTime DESC")
|
||||
suspend fun getCookFoodList(canteenId: String, cookMode: Int, dinnerType: String): MutableList<CookFoodEntity>?
|
||||
@@ -26,8 +26,12 @@ interface CookFoodDao {
|
||||
suspend fun countCookFood(canteenId: String, cookMode: Int): Int
|
||||
|
||||
// @Query("DELETE FROM dm_cook_food WHERE canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
|
||||
@Query("UPDATE dm_cook_food SET isDel = 1 WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
|
||||
suspend fun deleteCookFood(canteenId: String, foodId: String, cookMode: Int)
|
||||
@Query("UPDATE dm_cook_food SET isDel = 1 WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode AND dinnerType = :dinnerType")
|
||||
suspend fun deleteCookFood(canteenId: String, foodId: String, cookMode: Int, dinnerType: String)
|
||||
|
||||
/** 接口提交成功后逻辑删除菜品,同时把 submitState 置 1 以便在数据库审计中区分手动删除 */
|
||||
@Query("UPDATE dm_cook_food SET isDel = 1, submitState = 1 WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode AND dinnerType = :dinnerType")
|
||||
suspend fun markSubmittedAndDelete(canteenId: String, foodId: String, cookMode: Int, dinnerType: String)
|
||||
|
||||
/** 查询全部菜品记录(含已删除),用于数据库调试查看 */
|
||||
@Query("SELECT * FROM dm_cook_food ORDER BY createTime DESC")
|
||||
|
||||
@@ -29,6 +29,10 @@ interface CookFoodGoodsDao {
|
||||
@Query("UPDATE dm_cook_food_goods SET isDel = 1 WHERE isDel = 0 AND pid = :pid AND foodId = :foodId")
|
||||
suspend fun deleteCookFoodGoodsList(pid: Long, foodId: String)
|
||||
|
||||
/** 仅逻辑删除调料行(materialType = 3),主辅材不动,用于 cook 时只重建调料部分 */
|
||||
@Query("UPDATE dm_cook_food_goods SET isDel = 1 WHERE isDel = 0 AND pid = :pid AND foodId = :foodId AND materialType = 3")
|
||||
suspend fun deleteSeasoningOnly(pid: Long, foodId: String)
|
||||
|
||||
/** 查询全部食材记录(含已删除),用于数据库调试查看 */
|
||||
@Query("SELECT * FROM dm_cook_food_goods ORDER BY createTime DESC")
|
||||
suspend fun getAllCookFoodGoods(): MutableList<CookFoodGoodsEntity>
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.chad.library.adapter4.util.setOnDebouncedItemClick
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.shuwei.dish.match.R
|
||||
import com.shuwei.dish.match.adapter.FoodAdapter
|
||||
@@ -28,6 +29,8 @@ import com.shuwei.dish.match.dialog.SeasoningSelectDialog.Companion.PAGE_SIZE
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
import com.shuwei.dish.match.net.UiState
|
||||
import com.shuwei.dish.match.utils.KeyboardUtil
|
||||
import com.shuwei.dish.match.utils.buildMockGoodsList
|
||||
import com.shuwei.dish.match.utils.showRawMaterialsDialog
|
||||
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
|
||||
import com.shuwei.dish.match.utils.ext.toast
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -50,36 +53,25 @@ 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"))
|
||||
// }
|
||||
|
||||
private val adapter = FoodAdapter(list).apply {
|
||||
isStateViewEnable = true
|
||||
setOnItemClickListener { _, _, position ->
|
||||
list[position].isClicked = true
|
||||
notifyItemChanged(position)
|
||||
binding.root.postDelayed({
|
||||
onItemSelected(list[position])
|
||||
dismiss()
|
||||
}, 300)
|
||||
setOnDebouncedItemClick { _, _, position ->
|
||||
val item = list[position]
|
||||
// 弹出净材种类单选弹窗,确认后再回调并关闭弹窗
|
||||
showRawMaterialsDialog(
|
||||
context = activity,
|
||||
types = item.rawMaterialsTypes,
|
||||
checkedType = item.rawMaterialsType
|
||||
) { selectedType ->
|
||||
item.rawMaterialsType = selectedType
|
||||
item.isClicked = true
|
||||
notifyItemChanged(position)
|
||||
binding.root.postDelayed({
|
||||
onItemSelected(item)
|
||||
dismiss()
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +116,7 @@ class FoodSearchDialog(
|
||||
}
|
||||
|
||||
binding.refreshLayout.run {
|
||||
setEnableRefresh(false)
|
||||
setEnableRefresh(true)
|
||||
setEnableLoadMore(false)
|
||||
setOnRefreshListener { pageNo = 1; getGoodsList() }
|
||||
setOnLoadMoreListener { getGoodsList() }
|
||||
@@ -183,12 +175,13 @@ class FoodSearchDialog(
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集 goodsListState,统一处理 Loading / Success / Error 状态
|
||||
* 收集 foodSearchGoodsListState,统一处理 Loading / Success / Error 状态
|
||||
* 使用独立的 State,避免与 FoodRecognizeActivity 等页面共用 goodsListState 产生干扰
|
||||
*/
|
||||
private fun initObserver() {
|
||||
activity.lifecycleScope.launch {
|
||||
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
activity.netViewModel.goodsListState.collect { state ->
|
||||
activity.netViewModel.foodSearchGoodsListState.collect { state ->
|
||||
when (state) {
|
||||
is UiState.Success -> loadGoodsList(state.data)
|
||||
is UiState.Error -> {
|
||||
@@ -216,10 +209,10 @@ class FoodSearchDialog(
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求食材列表
|
||||
* 请求食材列表,使用弹窗专用接口,结果写入 foodSearchGoodsListState
|
||||
*/
|
||||
private fun getGoodsList() {
|
||||
activity.netViewModel.queryGoodsList(
|
||||
activity.netViewModel.queryFoodSearchGoodsList(
|
||||
goodsType = "0",
|
||||
pageNum = pageNo,
|
||||
pageSize = PAGE_SIZE,
|
||||
@@ -241,6 +234,11 @@ class FoodSearchDialog(
|
||||
}
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
|
||||
if (pageNo == 1) list.clear()
|
||||
// 将接口返回的 rawMaterialsType(全部种类)移存到 rawMaterialsTypes,供弹窗使用
|
||||
records.forEach {
|
||||
it.rawMaterialsTypes = it.rawMaterialsType
|
||||
it.rawMaterialsType = null
|
||||
}
|
||||
list.addAll(records)
|
||||
adapter.notifyDataSetChanged()
|
||||
val isLoadMoreEnable = records.size >= PAGE_SIZE
|
||||
@@ -269,16 +267,19 @@ class FoodSearchDialog(
|
||||
false
|
||||
)
|
||||
}
|
||||
emptyViewBinding!!.tvContent.text = "暂无数据"
|
||||
emptyViewBinding!!.tvSubContent.text = "未查询到食材信息,请联系管理员添加"
|
||||
emptyViewBinding!!.root.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recyclerView.post {
|
||||
emptyViewBinding!!.root.minimumHeight = binding.recyclerView.height
|
||||
adapter.stateView = emptyViewBinding!!.root
|
||||
emptyViewBinding?.let {
|
||||
it.tvContent.text = "暂无数据"
|
||||
it.tvSubContent.text = "未查询到食材信息,请联系管理员添加"
|
||||
it.root.setOnClickListener { loadGoodsList(buildMockGoodsList().toMutableList()) }
|
||||
it.root.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recyclerView.post {
|
||||
it.root.minimumHeight = binding.recyclerView.height
|
||||
adapter.stateView = it.root
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -102,7 +102,7 @@ class SeasoningSelectDialog(
|
||||
}
|
||||
|
||||
binding.refreshLayout.run {
|
||||
setEnableRefresh(false)
|
||||
setEnableRefresh(true)
|
||||
setEnableLoadMore(false)
|
||||
setOnRefreshListener { pageNo = 1; getGoodsList() }
|
||||
setOnLoadMoreListener { getGoodsList() }
|
||||
@@ -117,7 +117,7 @@ class SeasoningSelectDialog(
|
||||
super.show()
|
||||
// 启动 StateFlow 收集,弹窗关闭时由 setOnDismissListener 取消
|
||||
collectJob = activity.lifecycleScope.launch {
|
||||
activity.netViewModel.goodsListState.collect { state ->
|
||||
activity.netViewModel.foodSearchGoodsListState.collect { state ->
|
||||
when (state) {
|
||||
is UiState.Loading -> {}
|
||||
is UiState.Success -> loadGoodsList(state.data)
|
||||
@@ -186,25 +186,29 @@ class SeasoningSelectDialog(
|
||||
* 请求调料列表
|
||||
*/
|
||||
private fun getGoodsList() {
|
||||
activity.netViewModel.queryGoodsList(
|
||||
activity.netViewModel.queryFoodSearchGoodsList(
|
||||
goodsType = "1",
|
||||
pageNum = pageNo,
|
||||
pageSize = PAGE_SIZE,
|
||||
goodsName = goodsName.takeUnless { it.isNullOrBlank() }
|
||||
)
|
||||
}
|
||||
|
||||
// // TODO: 联调时注释掉下方模拟数据,改为真实接口调用
|
||||
// val nameList = listOf(
|
||||
// "盐", "白砂糖","冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香",
|
||||
// "料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "葱", "蒜", "姜", "香菜", "油"
|
||||
// )
|
||||
//
|
||||
// val mockData = mutableListOf<GoodsItem>()
|
||||
// nameList.forEachIndexed { index, name ->
|
||||
// val padString = "${index + 1}".padStart(2, '0')
|
||||
// mockData.add(GoodsItem(goodsId = "1000$padString", goodsName = name))
|
||||
// }
|
||||
// loadGoodsList(mockData)
|
||||
/**
|
||||
* 加载本地模拟数据,仅用于联调时点击空布局快速验证 UI
|
||||
*/
|
||||
private fun loadMockData() {
|
||||
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))
|
||||
}
|
||||
pageNo = 1
|
||||
loadGoodsList(mockData)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,16 +255,20 @@ class SeasoningSelectDialog(
|
||||
false
|
||||
)
|
||||
}
|
||||
emptyViewBinding!!.tvContent.text = "暂无数据"
|
||||
emptyViewBinding!!.tvSubContent.text = "未查询到调料信息,请联系管理员添加"
|
||||
emptyViewBinding!!.root.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recyclerView.post {
|
||||
emptyViewBinding!!.root.minimumHeight = binding.recyclerView.height
|
||||
adapter.stateView = emptyViewBinding!!.root
|
||||
emptyViewBinding?.let {
|
||||
it.tvContent.text = "暂无数据"
|
||||
it.tvSubContent.text = "未查询到调料信息,请联系管理员添加"
|
||||
// 点击空布局加载本地模拟数据,方便联调时验证 UI
|
||||
it.root.setOnClickListener { loadMockData() }
|
||||
it.root.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recyclerView.post {
|
||||
it.root.minimumHeight = binding.recyclerView.height
|
||||
adapter.stateView = it.root
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.shuwei.dish.match.entity
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* 菜品详情接口传输对象(DTO),不依赖 Room
|
||||
* 对应接口:getConstituteByFoodId / saveConstitute
|
||||
*/
|
||||
data class CookFoodDTO(
|
||||
var foodId: String = "",
|
||||
var foodName: String? = null,
|
||||
var canteenId: String? = null,
|
||||
var foodWeight: Double = 0.0,
|
||||
// 0-制作模式,1-采样模式,2-品控模式
|
||||
var cookMode: Int = 0,
|
||||
var dinnerType: String? = "0",
|
||||
// 接口返回的食材构成列表
|
||||
var matchingConstituteInfoList: MutableList<CookFoodGoodsDTO>? = null,
|
||||
var foodConstituteList: MutableList<CookFoodGoodsDTO>? = null
|
||||
) : Serializable {
|
||||
|
||||
/**
|
||||
* 转换为 Room 实体,用于写入本地数据库
|
||||
* id/isDel/createTime 由 Room 自行管理,不从 DTO 携带
|
||||
*/
|
||||
fun toEntity(): CookFoodEntity = CookFoodEntity(
|
||||
foodId = foodId,
|
||||
foodName = foodName,
|
||||
canteenId = canteenId,
|
||||
foodWeight = foodWeight,
|
||||
cookMode = cookMode,
|
||||
dinnerType = dinnerType
|
||||
).also { entity ->
|
||||
entity.matchingConstituteInfoList = matchingConstituteInfoList
|
||||
?.map { it.toEntity() }
|
||||
?.toMutableList()
|
||||
entity.foodConstituteList = foodConstituteList
|
||||
?.map { it.toEntity() }
|
||||
?.toMutableList()
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ data class CookFoodEntity(
|
||||
//0-制作模式,1-采样模式,2-品控模式
|
||||
var cookMode:Int = 0,
|
||||
var isDel: Int = 0,
|
||||
/** 接口提交状态:0-未提交(本地草稿或手动删除),1-接口提交成功后删除 */
|
||||
var submitState: Int = 0,
|
||||
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now()),
|
||||
var dinnerType: String? = "0",
|
||||
) : BaseEntity {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.shuwei.dish.match.entity
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* 菜品构成(主辅料/调料)接口传输对象(DTO),不依赖 Room
|
||||
* 对应 CookFoodDTO 中 foodConstituteList、matchingConstituteInfoList 的元素
|
||||
*/
|
||||
data class CookFoodGoodsDTO(
|
||||
var goodsId: String = "",
|
||||
var goodsName: String? = null,
|
||||
var foodId: String? = "",
|
||||
var goodsOrRelationCode: String? = "",
|
||||
// 调料数据顺序
|
||||
var sort: Int = 0,
|
||||
// 食材原材料:1,预制品:2
|
||||
var relateionType: Int = 0,
|
||||
// 物品类型:1主料 2辅料 3调料
|
||||
var materialType: Int = 0,
|
||||
// 物品 是否全部可食:true-全部可食,false-部分可食
|
||||
var allEdible: Boolean = true,
|
||||
// 物品 用料重量
|
||||
var useWeight: Double? = 0.0,
|
||||
var popularName: String? = "",
|
||||
var canteenId: String? = "",
|
||||
var relateionType_dictText: String? = "",
|
||||
var zjmCode: String? = "",
|
||||
var materId: String? = "",
|
||||
// 物料编码
|
||||
var goodsCode: String? = "",
|
||||
// 净材种类
|
||||
var rawMaterialsType: String? = null
|
||||
) : Serializable {
|
||||
|
||||
/**
|
||||
* 转换为 GoodsItem,用于 UI 层
|
||||
*/
|
||||
fun toGoodsItem(): GoodsItem = GoodsItem(
|
||||
goodsId = goodsId,
|
||||
goodsName = goodsName,
|
||||
popularName = popularName,
|
||||
zjmCode = zjmCode,
|
||||
materId = materId,
|
||||
materCode = goodsCode,
|
||||
materialType = materialType,
|
||||
useWeight = useWeight,
|
||||
relateionType = relateionType,
|
||||
allEdible = allEdible,
|
||||
goodsOrRelationCode = goodsOrRelationCode,
|
||||
rawMaterialsType = rawMaterialsType
|
||||
)
|
||||
|
||||
/**
|
||||
* 转换为 Room 实体,用于写入本地数据库
|
||||
* id/pid/isDel/createTime 由 Room 自行管理,不从 DTO 携带
|
||||
*/
|
||||
fun toEntity(): CookFoodGoodsEntity = CookFoodGoodsEntity(
|
||||
goodsId = goodsId,
|
||||
goodsName = goodsName,
|
||||
foodId = foodId,
|
||||
goodsOrRelationCode = goodsOrRelationCode,
|
||||
sort = sort,
|
||||
relateionType = relateionType,
|
||||
materialType = materialType,
|
||||
allEdible = allEdible,
|
||||
useWeight = useWeight,
|
||||
popularName = popularName,
|
||||
canteenId = canteenId,
|
||||
relateionType_dictText = relateionType_dictText,
|
||||
zjmCode = zjmCode,
|
||||
materId = materId,
|
||||
goodsCode = goodsCode,
|
||||
rawMaterialsType = rawMaterialsType
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Room 实体 → DTO 反向映射,用于将本地数据转为接口提交格式
|
||||
* id/pid/isDel/createTime 为本地数据库字段,不参与提交
|
||||
*/
|
||||
fun CookFoodGoodsEntity.toDTO(): CookFoodGoodsDTO = CookFoodGoodsDTO(
|
||||
goodsId = goodsId,
|
||||
goodsName = goodsName,
|
||||
foodId = foodId,
|
||||
goodsOrRelationCode = goodsOrRelationCode,
|
||||
sort = sort,
|
||||
relateionType = relateionType,
|
||||
materialType = materialType,
|
||||
allEdible = allEdible,
|
||||
useWeight = useWeight,
|
||||
popularName = popularName,
|
||||
canteenId = canteenId,
|
||||
relateionType_dictText = relateionType_dictText,
|
||||
zjmCode = zjmCode,
|
||||
materId = materId,
|
||||
goodsCode = goodsCode,
|
||||
rawMaterialsType = rawMaterialsType
|
||||
)
|
||||
|
||||
/**
|
||||
* Room 实体 → DTO 反向映射,用于将本地数据转为接口提交格式
|
||||
* id/isDel/createTime 为本地数据库字段,不参与提交
|
||||
*/
|
||||
fun CookFoodEntity.toDTO(): CookFoodDTO = CookFoodDTO(
|
||||
foodId = foodId,
|
||||
foodName = foodName,
|
||||
canteenId = canteenId,
|
||||
foodWeight = foodWeight,
|
||||
cookMode = cookMode,
|
||||
dinnerType = dinnerType,
|
||||
matchingConstituteInfoList = matchingConstituteInfoList
|
||||
?.map { it.toDTO() }
|
||||
?.toMutableList(),
|
||||
foodConstituteList = foodConstituteList
|
||||
?.map { it.toDTO() }
|
||||
?.toMutableList()
|
||||
)
|
||||
@@ -24,7 +24,7 @@ data class GoodsItem(
|
||||
|
||||
// 从 CookFoodGoodsEntity @Ignore 迁移的 UI 状态字段
|
||||
var isSetFinished: Boolean = false,
|
||||
var isItemClicked: Boolean = false,
|
||||
// var isItemClicked: Boolean = false,
|
||||
var isSamplingPage: Boolean = false,
|
||||
var isNewDishType: Boolean = false,
|
||||
var isClicked: Boolean = false,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.shuwei.dish.match.entity
|
||||
|
||||
/**
|
||||
* 物品名称批量查询入参 DTO
|
||||
* 用于 queryGoodsInfoList 接口 goodsNames 字段的元素类型
|
||||
* @param goodsName 物品名称
|
||||
* @param rawMaterialsType 净材种类
|
||||
*/
|
||||
data class GoodsNameQueryDTO(
|
||||
val goodsName: String,
|
||||
val rawMaterialsType: String
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.shuwei.dish.match.net
|
||||
|
||||
import com.shuwei.dish.match.base.GlobalData
|
||||
import com.shuwei.dish.match.entity.CookFoodEntity
|
||||
import com.shuwei.dish.match.entity.CookFoodDTO
|
||||
import com.shuwei.dish.match.entity.FoodRecord
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
import okhttp3.MultipartBody
|
||||
@@ -24,7 +24,7 @@ interface ApiService {
|
||||
suspend fun getFoodDetail(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/getConstituteByFoodId",
|
||||
@Query("foodId") foodId: String
|
||||
): ApiResponse<CookFoodEntity?>
|
||||
): ApiResponse<CookFoodDTO?>
|
||||
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ interface ApiService {
|
||||
@POST
|
||||
suspend fun submitCookFood(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/saveConstitute",
|
||||
@Body param: CookFoodEntity
|
||||
@Body param: CookFoodDTO
|
||||
): ApiResponse<Any?>
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ interface ApiService {
|
||||
@POST
|
||||
suspend fun uploadFoodVectorData(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/addWeighingCollectionData",
|
||||
@PartMap params: Map<String, RequestBody>,
|
||||
@PartMap params: MutableMap<String, RequestBody>,
|
||||
@Part foodPics: List<MultipartBody.Part>
|
||||
): ApiResponse<List<String>?>
|
||||
}
|
||||
@@ -3,9 +3,10 @@ package com.shuwei.dish.match.net
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.shuwei.dish.match.base.BaseApp
|
||||
import com.shuwei.dish.match.entity.CookFoodEntity
|
||||
import com.shuwei.dish.match.entity.CookFoodDTO
|
||||
import com.shuwei.dish.match.entity.FoodRecord
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
import com.shuwei.dish.match.entity.GoodsNameQueryDTO
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -33,7 +34,7 @@ class NetViewModel(
|
||||
/**
|
||||
* 提交制作菜品
|
||||
*/
|
||||
fun submitCookFood(entity: CookFoodEntity) {
|
||||
fun submitCookFood(entity: CookFoodDTO) {
|
||||
viewModelScope.launch {
|
||||
_submitCookFoodState.value = UiState.Loading
|
||||
_submitCookFoodState.value = repository.submitCookFood(entity)
|
||||
@@ -43,8 +44,8 @@ class NetViewModel(
|
||||
/**
|
||||
* 查询菜品详情的 UI 状态流,UI 层通过 collect 监听
|
||||
*/
|
||||
private val _foodDetailState = MutableStateFlow<UiState<CookFoodEntity?>>(UiState.Idle)
|
||||
val foodDetailState: StateFlow<UiState<CookFoodEntity?>> = _foodDetailState.asStateFlow()
|
||||
private val _foodDetailState = MutableStateFlow<UiState<CookFoodDTO?>>(UiState.Idle)
|
||||
val foodDetailState: StateFlow<UiState<CookFoodDTO?>> = _foodDetailState.asStateFlow()
|
||||
|
||||
/**
|
||||
* 查询菜品详情
|
||||
@@ -101,6 +102,12 @@ class NetViewModel(
|
||||
private val _goodsListState = MutableStateFlow<UiState<MutableList<GoodsItem>?>>(UiState.Idle)
|
||||
val goodsListState: StateFlow<UiState<MutableList<GoodsItem>?>> = _goodsListState.asStateFlow()
|
||||
|
||||
/**
|
||||
* 食材搜索弹窗专用的 UI 状态流,与 goodsListState 隔离,避免弹窗请求影响其他页面
|
||||
*/
|
||||
private val _foodSearchGoodsListState = MutableStateFlow<UiState<MutableList<GoodsItem>?>>(UiState.Idle)
|
||||
val foodSearchGoodsListState: StateFlow<UiState<MutableList<GoodsItem>?>> = _foodSearchGoodsListState.asStateFlow()
|
||||
|
||||
/**
|
||||
* 查询物品信息列表(食材 goodsType=0,调料 goodsType=1 共用)
|
||||
* @param goodsType 物品类型:0=食材,1=调料
|
||||
@@ -108,7 +115,7 @@ class NetViewModel(
|
||||
* @param pageNum 页码,默认第 1 页
|
||||
* @param pageSize 每页条数,默认 50
|
||||
* @param goodsName 按名称模糊搜索,为 null 时不传该字段
|
||||
* @param goodsNames 按名称列表批量查询,为 null 时不传该字段
|
||||
* @param goodsNames 按名称+净材种类列表批量查询,为 null 时不传该字段
|
||||
*/
|
||||
fun queryGoodsList(
|
||||
goodsType: String,
|
||||
@@ -116,7 +123,7 @@ class NetViewModel(
|
||||
pageNum: Int = 1,
|
||||
pageSize: Int = 50,
|
||||
goodsName: String? = null,
|
||||
goodsNames: List<String>? = null
|
||||
goodsNames: List<GoodsNameQueryDTO>? = null
|
||||
) {
|
||||
val param = mutableMapOf<String, Any>(
|
||||
"goodsType" to goodsType,
|
||||
@@ -132,6 +139,37 @@ class NetViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 食材搜索弹窗专用的查询方法,结果写入 foodSearchGoodsListState,不影响 goodsListState
|
||||
* @param goodsType 物品类型:0=食材,1=调料
|
||||
* @param placeId 场所 ID,默认取当前食堂 ID
|
||||
* @param pageNum 页码,默认第 1 页
|
||||
* @param pageSize 每页条数,默认 50
|
||||
* @param goodsName 按名称模糊搜索,为 null 时不传该字段
|
||||
* @param goodsNames 按名称+净材种类列表批量查询,为 null 时不传该字段
|
||||
*/
|
||||
fun queryFoodSearchGoodsList(
|
||||
goodsType: String,
|
||||
placeId: String = BaseApp.canteenId,
|
||||
pageNum: Int = 1,
|
||||
pageSize: Int = 50,
|
||||
goodsName: String? = null,
|
||||
goodsNames: List<GoodsNameQueryDTO>? = null
|
||||
) {
|
||||
val param = mutableMapOf<String, Any>(
|
||||
"goodsType" to goodsType,
|
||||
"placeId" to placeId,
|
||||
"pageNum" to pageNum,
|
||||
"pageSize" to pageSize
|
||||
)
|
||||
goodsName?.let { param["goodsName"] = it }
|
||||
goodsNames?.let { param["goodsNames"] = it }
|
||||
viewModelScope.launch {
|
||||
_foodSearchGoodsListState.value = UiState.Loading
|
||||
_foodSearchGoodsListState.value = repository.queryGoodsList(param)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传采集菜品信息 UI 状态流,UI 层通过 collect 监听
|
||||
*/
|
||||
@@ -146,7 +184,7 @@ class NetViewModel(
|
||||
params: MutableMap<String, RequestBody>
|
||||
) {
|
||||
val fileParts = fileList.map { file ->
|
||||
file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
|
||||
file.asRequestBody("image/jpeg".toMediaTypeOrNull())
|
||||
.let { MultipartBody.Part.createFormData("foodPics", file.name, it) }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
@@ -166,6 +204,7 @@ class NetViewModel(
|
||||
params: MutableMap<String, RequestBody>
|
||||
): UiState<List<String>?> {
|
||||
val fileParts = fileList.map { file ->
|
||||
//multipart/form-data image/jpeg
|
||||
file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
|
||||
.let { MultipartBody.Part.createFormData("foodPics", file.name, it) }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.shuwei.dish.match.net
|
||||
|
||||
import com.shuwei.dish.match.entity.CookFoodEntity
|
||||
import com.shuwei.dish.match.entity.CookFoodDTO
|
||||
import com.shuwei.dish.match.entity.FoodRecord
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
import okhttp3.MultipartBody
|
||||
@@ -18,7 +18,7 @@ class RemoteRepository {
|
||||
* @param entity 菜品实体
|
||||
* @return UiState 包装的结果,Success 表示提交成功,Error 携带错误信息
|
||||
*/
|
||||
suspend fun submitCookFood(entity: CookFoodEntity): UiState<Any?> {
|
||||
suspend fun submitCookFood(entity: CookFoodDTO): UiState<Any?> {
|
||||
return try {
|
||||
val resp = apiService.submitCookFood(param = entity)
|
||||
if (resp.isSuccess()) UiState.Success(resp.data)
|
||||
@@ -34,7 +34,7 @@ class RemoteRepository {
|
||||
* @param foodId 菜品 ID
|
||||
* @return UiState 包装的结果,Success 携带详情数据,Error 携带错误信息
|
||||
*/
|
||||
suspend fun getFoodDetail(foodId: String): UiState<CookFoodEntity?> {
|
||||
suspend fun getFoodDetail(foodId: String): UiState<CookFoodDTO?> {
|
||||
return try {
|
||||
val resp = apiService.getFoodDetail(foodId = foodId)
|
||||
if (resp.isSuccess()) UiState.Success(resp.data)
|
||||
@@ -100,7 +100,7 @@ class RemoteRepository {
|
||||
* @return UiState 包装的结果,Success 携带列表数据,Error 携带错误信息
|
||||
*/
|
||||
suspend fun uploadFoodVectorData(
|
||||
params: Map<String, RequestBody>,
|
||||
params: MutableMap<String, RequestBody>,
|
||||
foodPics: List<MultipartBody.Part>
|
||||
): UiState<List<String>?> {
|
||||
return try {
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.io.InputStream
|
||||
|
||||
object FoodModule {
|
||||
|
||||
private lateinit var module_mobile: Module
|
||||
private lateinit var module: Module
|
||||
|
||||
private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
|
||||
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
||||
@@ -37,8 +37,8 @@ object FoodModule {
|
||||
|
||||
suspend fun init(context: Context) {
|
||||
withContext(Dispatchers.IO) {
|
||||
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
|
||||
initDefFoodData(context)
|
||||
module = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
|
||||
// initDefFoodData(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ object FoodModule {
|
||||
// }
|
||||
|
||||
fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
|
||||
// 模型未初始化时直接返回 null,避免 UninitializedPropertyAccessException
|
||||
if (!::module.isInitialized) {
|
||||
logInfo("bitmap2FloatArray: module 尚未初始化,跳过推理")
|
||||
return null
|
||||
}
|
||||
var rgb565Bitmap: Bitmap? = null
|
||||
try {
|
||||
val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
|
||||
@@ -64,7 +69,7 @@ object FoodModule {
|
||||
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
|
||||
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
|
||||
)
|
||||
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
|
||||
val outputTensor = module.forward(IValue.from(inputTensor)).toTensor()
|
||||
return outputTensor.dataAsFloatArray
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printStackTrace()
|
||||
@@ -124,7 +129,12 @@ object FoodModule {
|
||||
val nameScoreList = mutableListOf<IdNameScore>()
|
||||
idScoreList.forEach {
|
||||
val name = foodMap[it.id]?.name ?: ""
|
||||
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
|
||||
val arr = name.split("&&")
|
||||
var rawMaterialsType = ""
|
||||
if (arr.size > 1) {
|
||||
rawMaterialsType = arr[1]
|
||||
}
|
||||
nameScoreList.add(IdNameScore(id = it.id, name = arr[0], score = it.score, rawMaterialsType = rawMaterialsType))
|
||||
}
|
||||
logInfo("queryFood数据:${nameScoreList.toJsonString()}")
|
||||
return nameScoreList
|
||||
@@ -176,7 +186,8 @@ object FoodModule {
|
||||
data class IdNameScore(
|
||||
val id: Long,
|
||||
var name: String,
|
||||
val score: Double
|
||||
val score: Double,
|
||||
val rawMaterialsType: String
|
||||
)
|
||||
|
||||
suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.java_websocket.handshake.ClientHandshake
|
||||
import org.java_websocket.server.WebSocketServer
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -50,6 +51,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
/** 缓存各秤最新数据,新客户端连接时立即同步当前状态 */
|
||||
private val latestData = mutableMapOf<Int, ScaleData>()
|
||||
|
||||
/** 节流窗口内的延迟推送任务,key=address;保证窗口结束时最新数据被推送 */
|
||||
private val pendingPushTasks = mutableMapOf<Int, ScheduledFuture<*>>()
|
||||
|
||||
/**
|
||||
* 启动 WebSocket 服务器,并注册 WeightUtil 回调
|
||||
*/
|
||||
@@ -61,11 +65,12 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
|
||||
// 注册本机秤数据监听
|
||||
WeightUtil.addWeightListener(TAG) { address, state, weight ->
|
||||
Log.d(TAG, "WebSocket 设备${deviceId}推送数据, address=$address, state=$state, weight=$weight")
|
||||
val now = System.currentTimeMillis()
|
||||
// 节流:同一地址 100ms 内只推送一次
|
||||
if (now - (lastPushTime[address] ?: 0L) < THROTTLE_MS) return@addWeightListener
|
||||
// 方案三:数据未变化时直接跳过,避免无效推送
|
||||
val last = latestData[address]
|
||||
if (last?.weight == weight && last.state == state) return@addWeightListener
|
||||
|
||||
lastPushTime[address] = now
|
||||
val data = ScaleData(
|
||||
deviceId = deviceId,
|
||||
address = address,
|
||||
@@ -74,8 +79,28 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
ts = now,
|
||||
ip = NetworkUtil.getLocalIpAddress(context)
|
||||
)
|
||||
// 始终更新 latestData,保证最新一帧不丢失
|
||||
latestData[address] = data
|
||||
broadcast(gson.toJson(data))
|
||||
|
||||
val lastPush = lastPushTime[address] ?: 0L
|
||||
if (now - lastPush >= THROTTLE_MS) {
|
||||
// 窗口已过:立即推送,并取消可能存在的延迟任务
|
||||
lastPushTime[address] = now
|
||||
pendingPushTasks.remove(address)?.cancel(false)
|
||||
broadcast(gson.toJson(data))
|
||||
} else if (pendingPushTasks[address] == null) {
|
||||
// 方案一:窗口内首次触发时安排一个 trailing 推送,保证窗口末尾推最新值
|
||||
val delay = THROTTLE_MS - (now - lastPush)
|
||||
if (!scheduler.isShutdown) {
|
||||
pendingPushTasks[address] = scheduler.schedule({
|
||||
val latest = latestData[address] ?: return@schedule
|
||||
lastPushTime[address] = System.currentTimeMillis()
|
||||
pendingPushTasks.remove(address)
|
||||
broadcast(gson.toJson(latest))
|
||||
}, delay, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
// 窗口内且已安排过 trailing 任务:仅更新 latestData,等 trailing 触发时推送
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +121,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
server = null
|
||||
latestData.clear()
|
||||
lastPushTime.clear()
|
||||
pendingPushTasks.values.forEach { it.cancel(false) }
|
||||
pendingPushTasks.clear()
|
||||
Log.d(TAG, "WebSocket 服务端已停止")
|
||||
}
|
||||
|
||||
|
||||
@@ -140,9 +140,9 @@ class CookingModeActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCookFoodAndGoods(foodId: String, action: () -> Unit) {
|
||||
fun deleteCookFoodAndGoods(foodId: String, dinnerType: String, action: () -> Unit) {
|
||||
lifecycleScope.launch {
|
||||
appViewModel.deleteCookFoodAndGoods(cookMode = 0, foodId = foodId)
|
||||
appViewModel.deleteCookFoodAndGoods(cookMode = 0, foodId = foodId, dinnerType = dinnerType)
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.shuwei.dish.match.base.BaseActivity
|
||||
import com.shuwei.dish.match.databinding.ActivityFoodRecognizeBinding
|
||||
import com.shuwei.dish.match.dialog.FoodSearchDialog
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
import com.shuwei.dish.match.entity.GoodsNameQueryDTO
|
||||
import com.shuwei.dish.match.utils.ActivityManager
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import com.shuwei.dish.match.utils.ext.clickWithDebounce
|
||||
@@ -24,6 +25,7 @@ import com.shuwei.dish.match.net.UiState
|
||||
import com.shuwei.dish.match.objbox.FoodModule
|
||||
import com.shuwei.dish.match.utils.ext.dp
|
||||
import com.shuwei.dish.match.utils.ext.toast
|
||||
import com.shuwei.dish.match.utils.showRawMaterialsDialog
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -110,8 +112,8 @@ class FoodRecognizeActivity : BaseActivity() {
|
||||
/** 通过name查询IdNameScore */
|
||||
private val nameScoreMap = mutableMapOf<String, FoodModule.IdNameScore>()
|
||||
|
||||
/** 识别物品名称列表 */
|
||||
private val goodsNameList = mutableListOf<String>()
|
||||
/** 识别物品名称列表(名称 + 净材种类,用于批量查询接口) */
|
||||
private val goodsNameList = mutableListOf<GoodsNameQueryDTO>()
|
||||
|
||||
private val scrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
@@ -121,12 +123,32 @@ class FoodRecognizeActivity : BaseActivity() {
|
||||
|
||||
private val adapter = FoodAdapter(list).apply {
|
||||
setOnItemClickListener { _, _, position ->
|
||||
list.forEachIndexed { i, item -> item.isClicked = (i == position) }
|
||||
// rawMaterialsRemind(position)
|
||||
val item = list[position]
|
||||
list.forEachIndexed { i, it -> it.isClicked = (i == position) }
|
||||
// 点击选择时记录当前秤重量
|
||||
list[position].useWeight = currentWeight
|
||||
item.useWeight = currentWeight
|
||||
notifyDataSetChanged()
|
||||
selectedPosition = position
|
||||
binding.tvSelectedFood.text = list[position].goodsName ?: "-"
|
||||
binding.tvSelectedFood.text = item.goodsName ?: "-"
|
||||
}
|
||||
}
|
||||
|
||||
private fun rawMaterialsRemind(position: Int) {
|
||||
val item = list[position]
|
||||
// 弹出净材种类单选弹窗,确认后再选中该食材
|
||||
showRawMaterialsDialog(
|
||||
context = this@FoodRecognizeActivity,
|
||||
types = item.rawMaterialsTypes,
|
||||
checkedType = item.rawMaterialsType
|
||||
) { selectedType ->
|
||||
item.rawMaterialsType = selectedType
|
||||
list.forEachIndexed { i, it -> it.isClicked = (i == position) }
|
||||
// 点击选择时记录当前秤重量
|
||||
item.useWeight = currentWeight
|
||||
adapter.notifyDataSetChanged()
|
||||
selectedPosition = position
|
||||
binding.tvSelectedFood.text = item.goodsName ?: "-"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,9 +189,11 @@ class FoodRecognizeActivity : BaseActivity() {
|
||||
|
||||
/**
|
||||
* 从 Intent 读取数据并初始化页面
|
||||
* Reads data from Intent and initializes the page
|
||||
*/
|
||||
private fun initData() {
|
||||
// 加载食材照片
|
||||
// Load food photo
|
||||
val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI)
|
||||
if (!imageUri.isNullOrBlank()) {
|
||||
binding.ivFoodPhoto.setImageURI(imageUri.toUri())
|
||||
@@ -177,10 +201,15 @@ class FoodRecognizeActivity : BaseActivity() {
|
||||
currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0)
|
||||
binding.tvWeight.text = "${currentWeight}g"
|
||||
// val tempList = arrayListOf<GoodsItem>()
|
||||
// Process recognized name list if available
|
||||
recognizeNameList?.apply {
|
||||
// Iterate through each recognized item
|
||||
forEach {
|
||||
// Extract goods name by splitting "WP" and taking the first part
|
||||
val goodsName = it.name.split("WP").first()
|
||||
goodsNameList.add(goodsName)
|
||||
val rawMaterialsType = it.rawMaterialsType
|
||||
// Add to goods name list and name-score mapping
|
||||
goodsNameList.add(GoodsNameQueryDTO(goodsName, rawMaterialsType))
|
||||
nameScoreMap.put(goodsName, it)
|
||||
|
||||
// // TODO: 测试数据,临时使用-------------------------
|
||||
@@ -191,11 +220,16 @@ class FoodRecognizeActivity : BaseActivity() {
|
||||
// // TODO: 测试数据,临时使用-------------------------
|
||||
// loadRecognizeList(tempList)
|
||||
// // TODO: 测试数据,临时使用-------------------------
|
||||
// Clear the recognize name list after processing
|
||||
recognizeNameList = null
|
||||
}
|
||||
|
||||
//TODO: 注释掉等接口正常再调用,临时使用测试数据
|
||||
// If goods name list is not empty, query goods list from network
|
||||
// if (goodsNameList.isNotEmpty()){
|
||||
netViewModel.queryGoodsList(goodsType = "0", goodsNames = goodsNameList)
|
||||
// } else {
|
||||
// loadRecognizeList(emptyList())
|
||||
// }
|
||||
}
|
||||
|
||||
private fun buildTestData(item: FoodModule.IdNameScore): GoodsItem {
|
||||
@@ -256,8 +290,13 @@ class FoodRecognizeActivity : BaseActivity() {
|
||||
binding.tvNoData.gone()
|
||||
list.clear()
|
||||
goodsList.forEach {
|
||||
val food = nameScoreMap[it.goodsName]
|
||||
val foodScore = ((1 - (food?.score ?: 0.0)) * 10000).toInt()
|
||||
var foodScore = -1
|
||||
nameScoreMap[it.goodsName]?.let { food ->
|
||||
foodScore = ((1 - food.score) * 10000).toInt()
|
||||
}
|
||||
// // 将接口返回的 rawMaterialsType(全部种类)移存到 rawMaterialsTypes,供弹窗使用
|
||||
// it.rawMaterialsTypes = it.rawMaterialsType
|
||||
// it.rawMaterialsType = null
|
||||
list.add(it.also { it.foodScore = foodScore })
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
@@ -143,7 +143,8 @@ class InitActivity : BaseActivity() {
|
||||
override fun onDestroy() {
|
||||
countdownHandler.removeCallbacksAndMessages(null)
|
||||
WeightUtil.stopContinuousRead()
|
||||
ScaleServiceManager.stop()
|
||||
// 秤服务伴随进程生命周期,无需在 Activity 销毁时停止;进程退出时 OS 会自动回收所有资源
|
||||
// ScaleServiceManager.stop()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.shuwei.dish.match.base.BaseActivity
|
||||
import com.shuwei.dish.match.databinding.ActivityPrepareFoodBinding
|
||||
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
|
||||
import com.shuwei.dish.match.dialog.CommonDialog
|
||||
import com.shuwei.dish.match.entity.CookFoodEntity
|
||||
import com.shuwei.dish.match.entity.CookFoodDTO
|
||||
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
|
||||
import com.shuwei.dish.match.entity.FoodRecord
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
@@ -129,6 +129,16 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
WeightUtil.tareTwo(AddressUtil.ONE)
|
||||
delayDismissLoading()
|
||||
}
|
||||
// TODO: 测试功能-------------------------------
|
||||
binding.btnTestConfirm.clickWithDebounce {
|
||||
if (currentWeight <= 0.0) return@clickWithDebounce
|
||||
val (index, item) = list.withIndex().find { (_, it) -> it.isOriginalData && it.isClicked && (it.useWeight?:0.0) <= 0.0 } ?: return@clickWithDebounce
|
||||
item.useWeight = currentWeight
|
||||
item.isSetFinished = true
|
||||
list.forEachIndexed { i, it -> it.isClicked = i == index }
|
||||
materialAdapter.notifyDataSetChanged()
|
||||
}
|
||||
// TODO: 测试功能-------------------------------
|
||||
//binding.root.setOnClickListener { hideKeyboard() }
|
||||
WeightUtil.addWeightListener(
|
||||
weightKey = TAG,
|
||||
@@ -221,6 +231,7 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
}
|
||||
if (
|
||||
!pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage ||
|
||||
notRecognizeDialog?.isShowing == true ||
|
||||
abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE
|
||||
) {
|
||||
Log.d(
|
||||
@@ -242,16 +253,25 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
* - list 中不含该 goodsId:直接新增,isNewDishType = true
|
||||
* - list 中已含该 goodsId:累加 useWeight
|
||||
*/
|
||||
private val foodSelectCallback: (GoodsItem) -> Unit = { entity ->
|
||||
val existing = list.firstOrNull { it.goodsId == entity.goodsId }
|
||||
if (existing != null) {
|
||||
existing.useWeight = (existing.useWeight ?: 0.0) + (entity.useWeight ?: 0.0)
|
||||
existing.isSetFinished = true
|
||||
materialAdapter.notifyItemChanged(list.indexOf(existing))
|
||||
private val foodSelectCallback: (GoodsItem) -> Unit = foodSelectCallback@{ entity ->
|
||||
val filterValue = list.withIndex().find { (_, it) -> it.goodsId == entity.goodsId }
|
||||
if (filterValue != null) {
|
||||
val index = filterValue.index
|
||||
val item = filterValue.value
|
||||
item.useWeight = (item.useWeight ?: 0.0) + (entity.useWeight ?: 0.0)
|
||||
item.isSetFinished = true
|
||||
list.forEachIndexed { i, it ->
|
||||
item.isClicked = i == index
|
||||
}
|
||||
} else {
|
||||
materialAdapter.add(entity.also {
|
||||
list.forEach { it.isClicked = false }
|
||||
list.add(entity.also {
|
||||
it.isNewDishType = true
|
||||
it.isSetFinished = true
|
||||
// 标记为非接口数据,否则 updateMaterialTypes 不会为其计算 materialType,
|
||||
// 导致列表中不显示"主辅材:主材/辅材"
|
||||
it.isOriginalData = false
|
||||
it.isClicked = true
|
||||
})
|
||||
binding.rvMaterialList.smoothScrollToPosition(list.size - 1)
|
||||
}
|
||||
@@ -349,7 +369,7 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
|
||||
private fun loadDishDetail(detail: CookFoodEntity) {
|
||||
private fun loadDishDetail(detail: CookFoodDTO) {
|
||||
// val voList = detail.stFoodInfoConstituteList
|
||||
val voList = detail.foodConstituteList
|
||||
if (voList.isNullOrEmpty()) {
|
||||
@@ -373,7 +393,7 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
FoodMaterialAdapter(list).apply {
|
||||
onItemClick = { positon ->
|
||||
list.forEachIndexed { index, entity ->
|
||||
entity.isItemClicked = index == positon
|
||||
entity.isClicked = index == positon
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@@ -555,6 +575,9 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
dismissLoading()
|
||||
Log.d(TAG, "takePhoto foodScoreList is empty")
|
||||
isTakingPhoto.set(false)
|
||||
withContext(Dispatchers.Main) {
|
||||
showNotRecognizeDialog()
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -567,6 +590,19 @@ class PrepareFoodActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
private var notRecognizeDialog: CommonDialog? = null
|
||||
private fun showNotRecognizeDialog() {
|
||||
notRecognizeDialog = CommonDialog(this)
|
||||
.setTitle("提示")
|
||||
.setContent("未识别到食材,请稍后重试")
|
||||
.setNegativeButton("取消")
|
||||
.setPositiveButton("手动设置"){
|
||||
loadRecognizeResultPage(emptyList())
|
||||
}
|
||||
.also {
|
||||
it.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拍照失败回调
|
||||
|
||||
@@ -75,6 +75,7 @@ class SamplingModeActivity : BaseActivity() {
|
||||
foodName = entity.foodName
|
||||
cookMode = 1
|
||||
isCooking = true
|
||||
dinnerType = entity.dinnerType ?: "0"
|
||||
})
|
||||
}
|
||||
list.clear()
|
||||
@@ -146,9 +147,9 @@ class SamplingModeActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCookFoodAndGoods(foodId: String, action: () -> Unit) {
|
||||
fun deleteCookFoodAndGoods(foodId: String, dinnerType: String, action: () -> Unit) {
|
||||
lifecycleScope.launch {
|
||||
appViewModel.deleteCookFoodAndGoods(cookMode = 1, foodId = foodId)
|
||||
appViewModel.deleteCookFoodAndGoods(cookMode = 1, foodId = foodId, dinnerType = dinnerType)
|
||||
action()
|
||||
}
|
||||
}
|
||||
@@ -210,7 +211,7 @@ class SamplingModeActivity : BaseActivity() {
|
||||
.setContent("确定要删除「${item.foodName}」吗?")
|
||||
.setNegativeButton("取消")
|
||||
.setPositiveButton("删除") {
|
||||
deleteCookFoodAndGoods(foodId = item.foodId ?: "") {
|
||||
deleteCookFoodAndGoods(foodId = item.foodId ?: "", dinnerType = item.dinnerType) {
|
||||
if (list.size > 1) {
|
||||
dishAdapter.removeAt(position)
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.shuwei.dish.match.dialog.CommonDialog
|
||||
import com.shuwei.dish.match.entity.CookFoodEntity
|
||||
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
|
||||
import com.shuwei.dish.match.entity.FoodRecord
|
||||
import com.shuwei.dish.match.entity.toDTO
|
||||
import com.shuwei.dish.match.net.UiState
|
||||
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
||||
@@ -45,6 +46,9 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
const val TAG = "SubmitFoodActivity"
|
||||
const val FOOD_ITEM = "foodItem"
|
||||
const val GOODS_LIST = "goodsList"
|
||||
|
||||
/** 本地临时 foodId 前缀;以此开头说明菜品尚未在服务器登记,提交时需清空让服务器分配真实 ID */
|
||||
const val LOCAL_ID_PREFIX = "localId_"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivitySubmitFoodBinding
|
||||
@@ -91,6 +95,11 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
*/
|
||||
private val baseSeasoningMap = mutableMapOf<String, Pair<String, Double>>()
|
||||
|
||||
/**
|
||||
* 各秤上一次处理过的 ts,用于跳过 StateFlow 因其他秤变化而重发的相同帧
|
||||
*/
|
||||
private val lastTsMap = mutableMapOf<String, Long>()
|
||||
|
||||
@Suppress("unchecked_cast", "DEPRECATION")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -147,7 +156,7 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 RecyclerView,24列 GridLayoutManager */
|
||||
/** 初始化 RecyclerView,默认 4 列 GridLayoutManager */
|
||||
private fun setupRecyclerView() {
|
||||
binding.rvSeasoning.let {
|
||||
it.itemAnimator = null
|
||||
@@ -158,7 +167,8 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
|
||||
/**
|
||||
* 从 Room 加载槽位配置后,开始订阅子设备秤数据
|
||||
* isCooking=true 时先从数据库查询 goodsList,筛选调料预填充 adapter,再开始观测
|
||||
* isCooking=true 时从数据库查询 goodsList 并预填充调料 adapter;
|
||||
* isCooking=false 时从前一页面带入的 goodsList 中预填充调料 adapter,再开始观测
|
||||
*/
|
||||
private fun loadSlotsThenObserveScales() {
|
||||
lifecycleScope.launch {
|
||||
@@ -170,24 +180,34 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
// 制作中:从数据库查询 goodsList,筛选调料数据预填充 adapter
|
||||
val foodId = food?.foodId ?: return@launch
|
||||
val cookMode = food?.cookMode ?: return@launch
|
||||
val list = appViewModel.getCookFoodGoodsList(foodId, cookMode)
|
||||
val list =
|
||||
appViewModel.getCookFoodGoodsList(foodId, cookMode, food?.dinnerType ?: "0")
|
||||
goodsList = list
|
||||
list.filter { it.materialType == 3 }.forEach { item ->
|
||||
val name = item.goodsName ?: ""
|
||||
val weight = item.useWeight ?: 0.0
|
||||
baseSeasoningMap[name] = (item.goodsId) to weight
|
||||
seasoningAdapter.updateItem(
|
||||
SeasoningWeightAdapter.Item(
|
||||
goodsId = item.goodsId,
|
||||
goodsName = name,
|
||||
useWeight = weight
|
||||
)
|
||||
)
|
||||
}
|
||||
observeScaleData()
|
||||
prefillSeasoningFromGoodsList(list)
|
||||
} else {
|
||||
observeScaleData()
|
||||
// 非制作中:从前一页面带入的 goodsList 中筛选调料数据预填充 adapter
|
||||
goodsList?.let { prefillSeasoningFromGoodsList(it) }
|
||||
}
|
||||
observeScaleData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 goodsList 中筛选 materialType==3 的调料数据,
|
||||
* 写入 baseSeasoningMap 作为历史用量基准,并预填充到 adapter
|
||||
*/
|
||||
private fun prefillSeasoningFromGoodsList(list: List<CookFoodGoodsEntity>) {
|
||||
list.filter { it.materialType == 3 }.forEach { item ->
|
||||
val name = item.goodsName ?: ""
|
||||
val weight = item.useWeight ?: 0.0
|
||||
baseSeasoningMap[name] = item.goodsId to weight
|
||||
seasoningAdapter.updateItem(
|
||||
SeasoningWeightAdapter.Item(
|
||||
goodsId = item.goodsId,
|
||||
goodsName = name,
|
||||
useWeight = weight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,13 +219,20 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
val flow = ScaleServiceManager.allScales ?: return
|
||||
lifecycleScope.launch {
|
||||
flow.collectLatest { scaleMap ->
|
||||
//Log.d(TAG, "observeScaleData,scaleMap=${scaleMap.toJsonString()}")
|
||||
scaleMap.forEach { (key, data) ->
|
||||
// ts 未变化说明是其他秤更新触发的 StateFlow 重发,跳过
|
||||
if (lastTsMap[key] == data.ts) return@forEach
|
||||
lastTsMap[key] = data.ts
|
||||
// 排除主设备 2格秤
|
||||
if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_2) return@forEach
|
||||
|
||||
// 从槽位配置读取调料名,未配置则跳过
|
||||
val (goodsId, goodsName) = slotMap[key] ?: return@forEach
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"observeScaleData,1key=$key, goodsId=$goodsId, goodsName=$goodsName, data=$data"
|
||||
)
|
||||
// 首次收到稳定数据时记录基准重量
|
||||
val baseline = baselineMap[key]
|
||||
if (baseline == null) {
|
||||
@@ -214,19 +241,28 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
|
||||
Log.d(TAG, "observeScaleData,2key=$key, baseline=$baseline, data.weight=${data.weight}")
|
||||
// 用量 = 初始重量 - 当前重量,负值归零
|
||||
val useWeight = (baseline - data.weight)
|
||||
.coerceAtLeast(0.0)
|
||||
.roundedOneDecimalPlace()
|
||||
|
||||
// 更新原始用量 map,再按 goodsName 聚合刷新 adapter
|
||||
if (useWeight < 0.5) {
|
||||
rawWeightMap.remove(key)
|
||||
// 放回场景:必须稳定后才移除,避免拿取过程中误删
|
||||
if (data.state != WeightUtil.STATE_STABLE) return@forEach
|
||||
if (rawWeightMap.remove(key) != null) {
|
||||
Log.d(TAG, "observeScaleData,移除key=${key}, goodsName=$goodsName")
|
||||
refreshAdapterByName(goodsName)
|
||||
}
|
||||
} else {
|
||||
rawWeightMap[key] = Triple(goodsId, goodsName, useWeight)
|
||||
// 拿取场景:实时响应,无需等待稳定
|
||||
val newTriple = Triple(goodsId, goodsName, useWeight)
|
||||
if (rawWeightMap[key] != newTriple) {
|
||||
Log.d(TAG, "observeScaleData,新增:$newTriple")
|
||||
rawWeightMap[key] = newTriple
|
||||
refreshAdapterByName(goodsName)
|
||||
}
|
||||
}
|
||||
refreshAdapterByName(goodsName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,30 +271,45 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
/**
|
||||
* 按 goodsName 聚合 rawWeightMap,叠加历史用量后推给 adapter
|
||||
* isCooking=true 时只新增,不移除(历史调料数据不可减少)
|
||||
* 移除前检查 item 是否存在,更新前比较数据是否变化,避免无效操作
|
||||
*/
|
||||
private fun refreshAdapterByName(goodsName: String) {
|
||||
val grouped = rawWeightMap.values.filter { it.second == goodsName }
|
||||
val newWeight = grouped.sumOf { it.third }.roundedOneDecimalPlace()
|
||||
val (baseGoodsId, baseWeight) = baseSeasoningMap[goodsName] ?: ("" to 0.0)
|
||||
val totalWeight = (newWeight + baseWeight).roundedOneDecimalPlace()
|
||||
Log.d(TAG, "refreshAdapterByName, goodsName=$goodsName, newWeight=$newWeight, baseWeight=$baseWeight, totalWeight=$totalWeight, isCooking=${food?.isCooking}")
|
||||
if (totalWeight < 0.5) {
|
||||
// 制作中模式:已有历史数据,不允许移除
|
||||
if (food?.isCooking != true) {
|
||||
seasoningAdapter.removeItem(goodsName)
|
||||
Log.d(TAG, "refreshAdapterByName,重量小于0.5g, isCooking=${food?.isCooking}, goodsName=$goodsName, totalWeight=$totalWeight")
|
||||
// 仅保护 baseSeasoningMap 中的历史调料;本次会话新加的调料可以移除
|
||||
if (!baseSeasoningMap.containsKey(goodsName)) {
|
||||
// 只有 item 确实存在时才调用 remove,避免无效遍历
|
||||
if (seasoningAdapter.items.any { it.goodsName == goodsName }) {
|
||||
seasoningAdapter.removeItem(goodsName)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
val goodsId = grouped.firstOrNull()?.first?.takeIf { it.isNotEmpty() } ?: baseGoodsId
|
||||
seasoningAdapter.updateItem(
|
||||
SeasoningWeightAdapter.Item(
|
||||
goodsId = goodsId,
|
||||
goodsName = goodsName,
|
||||
useWeight = totalWeight
|
||||
)
|
||||
// 数据未变化时跳过,避免触发不必要的 RecyclerView 刷新
|
||||
if (seasoningAdapter.items.any { it.goodsName == goodsName && it.useWeight == totalWeight && it.goodsId == goodsId }) return
|
||||
val item = SeasoningWeightAdapter.Item(
|
||||
goodsId = goodsId,
|
||||
goodsName = goodsName,
|
||||
useWeight = totalWeight
|
||||
)
|
||||
Log.d(TAG, "refreshAdapterByName,更新数据item=${item.toJsonString()}")
|
||||
seasoningAdapter.updateItem(item)
|
||||
}
|
||||
|
||||
private fun addViewListener() {
|
||||
// 切换调料列表布局:4 列卡片 ↔ 2 列水平
|
||||
binding.ivSeasoningLayoutToggle.setOnClickListener {
|
||||
seasoningAdapter.isListMode = !seasoningAdapter.isListMode
|
||||
val spanCount = if (seasoningAdapter.isListMode) 2 else 4
|
||||
(binding.rvSeasoning.layoutManager as? GridLayoutManager)?.spanCount = spanCount
|
||||
seasoningAdapter.notifyDataSetChanged()
|
||||
}
|
||||
binding.btnCook.clickWithDebounce {
|
||||
val content = getRemindSpannable("开始制作")
|
||||
showRemindDialog(content) { cook() }
|
||||
@@ -297,9 +348,11 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
|
||||
val isSamplingData = food!!.cookMode == 1
|
||||
if (isSamplingData) {
|
||||
val tempFoodId = cookFoodEntity.foodId.ifBlank { "${System.currentTimeMillis()}" }
|
||||
val tempFoodId = cookFoodEntity.foodId
|
||||
.ifBlank { "$LOCAL_ID_PREFIX${System.currentTimeMillis()}" }
|
||||
cookFoodEntity.foodId = tempFoodId
|
||||
}
|
||||
// 主辅材统一在此赋 foodId;新加的调料行 foodId 由 buildSeasoningEntity 内部设置
|
||||
goodsList?.forEach { it.foodId = cookFoodEntity.foodId }
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -336,6 +389,8 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
lifecycleScope.launch {
|
||||
// 先移除 goodsList 中已有的调料数据(materialType==3),再以 adapter 中最新调料覆盖
|
||||
goodsList?.removeAll { it.materialType == 3 }
|
||||
// 主辅材 foodId 统一以菜品 foodId 为准;新加调料行的 foodId 由 buildSeasoningEntity 内部设置
|
||||
goodsList?.forEach { it.foodId = cookFoodEntity.foodId }
|
||||
seasoningAdapter.items
|
||||
.filter { it.useWeight > 0.0 }
|
||||
.forEach { goodsList?.add(buildSeasoningEntity(it)) }
|
||||
@@ -350,14 +405,15 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val isSamplingData = food!!.cookMode == 1
|
||||
if (isSamplingData) {
|
||||
// 仅本地生成的临时 foodId 需清空,由服务器分配真实 ID;服务器下发的真实 foodId 直接保留
|
||||
if (cookFoodEntity.foodId.startsWith(LOCAL_ID_PREFIX)) {
|
||||
cookFoodEntity.foodId = ""
|
||||
goodsList?.forEach { it.foodId = "" }
|
||||
}
|
||||
|
||||
Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}")
|
||||
netViewModel.submitCookFood(entity = cookFoodEntity)
|
||||
// 接口提交使用 DTO,与本地 Room 实体解耦
|
||||
netViewModel.submitCookFood(entity = cookFoodEntity.toDTO())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +424,7 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
private suspend fun buildSeasoningEntity(item: SeasoningWeightAdapter.Item): CookFoodGoodsEntity {
|
||||
val seasoning = appViewModel.getSeasoningByGoodsId(item.goodsId)
|
||||
return CookFoodGoodsEntity().also { entity ->
|
||||
entity.foodId = cookFoodEntity.foodId
|
||||
entity.goodsId = item.goodsId
|
||||
entity.goodsName = item.goodsName
|
||||
entity.useWeight = item.useWeight
|
||||
@@ -389,9 +446,10 @@ class SubmitFoodActivity : BaseActivity() {
|
||||
return
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
appViewModel.deleteCookFoodAndGoods(
|
||||
appViewModel.markSubmittedAndDelete(
|
||||
cookMode = food!!.cookMode,
|
||||
foodId = food!!.foodId!!
|
||||
foodId = food!!.foodId!!,
|
||||
dinnerType = food!!.dinnerType
|
||||
)
|
||||
dismissLoading()
|
||||
jumpPage(isSamplingData, false)
|
||||
|
||||
@@ -109,7 +109,7 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
|
||||
.setContent("确定要删除「${item.foodName}」吗?")
|
||||
.setNegativeButton("取消")
|
||||
.setPositiveButton("删除") {
|
||||
activity.deleteCookFoodAndGoods(foodId = item.foodId ?: "") {
|
||||
activity.deleteCookFoodAndGoods(foodId = item.foodId ?: "", dinnerType = item.dinnerType) {
|
||||
activity.toast("已删除")
|
||||
pageNo = 1
|
||||
getDishList()
|
||||
|
||||
@@ -11,14 +11,16 @@ 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.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.chad.library.adapter4.util.setOnDebouncedItemClick
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -28,7 +30,7 @@ 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.databinding.LayoutEmptyViewBinding
|
||||
import com.shuwei.dish.match.dialog.Loading
|
||||
import com.shuwei.dish.match.entity.FoodCollectionBean
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
@@ -44,13 +46,16 @@ 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.buildSingleChoiceChipGroup
|
||||
import com.shuwei.dish.match.utils.buildMockGoodsList
|
||||
import com.shuwei.dish.match.utils.showRawMaterialsDialog
|
||||
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
|
||||
import com.shuwei.dish.match.utils.ext.toJsonString
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() {
|
||||
@@ -73,11 +78,31 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
private var checkedItem: GoodsItem? = null
|
||||
private val searchAdapter by lazy {
|
||||
Food3Adapter(searchList).apply {
|
||||
setOnItemClickListener { _, _, position ->
|
||||
// 开启空状态视图能力,否则 stateView 赋值不会生效
|
||||
isStateViewEnable = true
|
||||
setOnDebouncedItemClick { _, _, position ->
|
||||
val item = searchList[position]
|
||||
val types = item.rawMaterialsTypes
|
||||
// 弹出净材种类单选弹窗
|
||||
showRawMaterialsDialog(item, position, types)
|
||||
showRawMaterialsDialog(
|
||||
context = requireContext(),
|
||||
types = item.rawMaterialsTypes,
|
||||
checkedType = item.rawMaterialsType
|
||||
) { selectedType ->
|
||||
// 清空其他已选项的状态
|
||||
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
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,15 +271,23 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
upload()
|
||||
}
|
||||
|
||||
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
searchFood()
|
||||
val imm =
|
||||
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
binding.editFoodName.let {
|
||||
it.addTextChangedListener { editable ->
|
||||
if (editable?.isEmpty() == true) {
|
||||
searchFood()
|
||||
return@addTextChangedListener
|
||||
}
|
||||
}
|
||||
it.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
searchFood()
|
||||
val imm =
|
||||
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +308,22 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
it.adapter = searchAdapter
|
||||
}
|
||||
|
||||
// 统一订阅一次,避免每次 searchFood() 重复创建 collect 协程导致多次回调
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
currentActivity.netViewModel.goodsListState.collect { state ->
|
||||
when (state) {
|
||||
is UiState.Success -> {
|
||||
val list = state.data ?: emptyList()
|
||||
if (list.isEmpty()) loadEmptyView() else fillSearchList(list)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchFood()
|
||||
}
|
||||
|
||||
@@ -285,8 +334,8 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
val params = mutableMapOf<String, RequestBody>()
|
||||
params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
|
||||
params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
|
||||
//净材种类,逗号分隔
|
||||
// params["rawMaterialsTypes"] = "".toRequestBody()
|
||||
//净材种类
|
||||
params["rawMaterialsTypes"] = checkedItem!!.rawMaterialsType!!.toRequestBody()
|
||||
//params["version"] = "1.0.0".toRequestBody()
|
||||
|
||||
ImageUploader(totalList = vectorList, uploadImage = { batch ->
|
||||
@@ -302,12 +351,14 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
params["foodVector"] = foodVectorJson.toRequestBody()
|
||||
// 直接 await 单批上传结果,成功返回 id 列表,失败返回 null 触发 onError
|
||||
val result = currentActivity.netViewModel.uploadFoodVectorDataBatch(files, params)
|
||||
log("uploadImage返回:result=${result.toJsonString()}")
|
||||
if (result is UiState.Success) result.data else null
|
||||
}, onProgress = { count, batch, idList ->
|
||||
showWaitingDialog("图片上传中$count/$totalFileCount")
|
||||
val foodList = batch.mapIndexed { index, it ->
|
||||
val name = checkedItem!!.goodsName + "&&" + checkedItem!!.rawMaterialsType
|
||||
Food(
|
||||
name = checkedItem!!.goodsName,
|
||||
name = name,
|
||||
foodVector = it.imageVector,
|
||||
)
|
||||
}
|
||||
@@ -332,141 +383,73 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
fun searchFood() {
|
||||
debouncer.debounce {
|
||||
val keyword = binding.editFoodName.text.toString().trim()
|
||||
// 通过 NetViewModel 查询净材数据(goodsType=0 食材)
|
||||
// 通过 NetViewModel 查询净材数据(goodsType=0 食材),结果由 initialize() 中的 observer 统一处理
|
||||
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 净材种类字符串(逗号分隔),为空时提示无数据
|
||||
* 将数据列表填充到 searchAdapter 并切换为网格布局
|
||||
* @param list 查询到的食材列表
|
||||
*/
|
||||
@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
|
||||
}
|
||||
private fun fillSearchList(list: List<GoodsItem>) {
|
||||
// 将 rawMaterialsType 转存到 rawMaterialsTypes,并清空 rawMaterialsType
|
||||
list.forEach { item ->
|
||||
item.rawMaterialsTypes = item.rawMaterialsType
|
||||
item.rawMaterialsType = null
|
||||
}
|
||||
// 空布局切过 LinearLayoutManager,此处恢复为 2 列 GridLayoutManager
|
||||
if (binding.rvSearchFood.layoutManager !is GridLayoutManager) {
|
||||
binding.rvSearchFood.layoutManager = GridLayoutManager(context, 2)
|
||||
}
|
||||
// 清空 stateView 否则空视图会一直占位导致无法显示列表项
|
||||
searchAdapter.stateView = null
|
||||
searchList.clear()
|
||||
searchList.addAll(list)
|
||||
searchAdapter.notifyDataSetChanged()
|
||||
Log.d(TAG, "fillSearchList,${searchList.toJsonString()}")
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
private var emptyViewBinding: LayoutEmptyViewBinding? = null
|
||||
|
||||
/**
|
||||
* 展示空布局
|
||||
* 默认 GridLayoutManager 会把 stateView 限制在单列宽度内,切换为 LinearLayoutManager 让空布局横向铺满;
|
||||
* 点击空布局时临时填充假数据用于效果测试,联调完成后可去除点击逻辑
|
||||
*/
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun loadEmptyView() {
|
||||
try {
|
||||
searchList.clear()
|
||||
searchAdapter.notifyDataSetChanged()
|
||||
// 切换为线性布局让空视图横向铺满,避免 Grid 模式下空布局只占一列
|
||||
binding.rvSearchFood.layoutManager = LinearLayoutManager(requireContext())
|
||||
if (emptyViewBinding == null) {
|
||||
emptyViewBinding = LayoutEmptyViewBinding.inflate(
|
||||
LayoutInflater.from(requireContext()),
|
||||
binding.rvSearchFood,
|
||||
false
|
||||
)
|
||||
}
|
||||
emptyViewBinding?.let {
|
||||
it.tvContent.text = "暂无数据"
|
||||
it.tvSubContent.text = "点击此处重新查询"
|
||||
// 点击空布局填充本地假数据,便于 UI 效果测试
|
||||
it.root.setOnClickListener {
|
||||
fillSearchList(buildMockGoodsList())
|
||||
}
|
||||
binding.rvSearchFood.post {
|
||||
it.root.minimumHeight = binding.rvSearchFood.height
|
||||
searchAdapter.stateView = it.root
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
var clickIndex = -1
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.shuwei.dish.match.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.shuwei.dish.match.R
|
||||
import com.shuwei.dish.match.dialog.CommonDialog
|
||||
import com.shuwei.dish.match.entity.GoodsItem
|
||||
import com.shuwei.dish.match.utils.ext.dp
|
||||
import com.shuwei.dish.match.utils.ext.gone
|
||||
import com.shuwei.dish.match.utils.ext.toast
|
||||
|
||||
/**
|
||||
* 构造用于效果测试的食材假数据
|
||||
* TODO: 联调完成后删除
|
||||
*/
|
||||
fun buildMockGoodsList(): List<GoodsItem> = 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 = "豆芽"),
|
||||
)
|
||||
|
||||
/**
|
||||
* 弹出净材种类单选弹窗
|
||||
* 确认选中后通过 onConfirmed 回调返回选中的种类文本,业务逻辑由调用方自行处理;
|
||||
* 无净材数据时展示提示文字,点击确认直接关闭弹窗。
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param types 净材种类字符串(逗号分隔),为空时提示无数据
|
||||
* @param checkedType 弹窗打开时的默认选中项(与 types 中元素对比),null 表示无默认选中
|
||||
* @param onConfirmed 用户选中并确认后的回调,参数为选中的净材种类文本
|
||||
*/
|
||||
fun showRawMaterialsDialog(
|
||||
context: Context,
|
||||
types: String?,
|
||||
checkedType: String?,
|
||||
onConfirmed: (selectedType: String) -> Unit
|
||||
) {
|
||||
val typeList = types?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList()
|
||||
|
||||
val dialog = CommonDialog(context)
|
||||
.setTitle("选择净材种类")
|
||||
.setNegativeButton("取消")
|
||||
|
||||
if (typeList.isEmpty()) {
|
||||
// 无净材数据时仅展示提示文字
|
||||
dialog.setContent("无净材数据")
|
||||
.setPositiveButton("确认") {}
|
||||
} else {
|
||||
// 构建单选 ChipGroup 作为自定义内容
|
||||
val chipGroup = buildSingleChoiceChipGroup(context, typeList, checkedType)
|
||||
dialog.addContentView(chipGroup) { tvContent -> tvContent.gone() }
|
||||
dialog.setPositiveButtonInterceptable("确认") {
|
||||
val checkedId = chipGroup.checkedChipId
|
||||
if (checkedId == View.NO_ID) {
|
||||
context.toast("请选择净材种类")
|
||||
// 返回 false 阻止弹窗关闭
|
||||
return@setPositiveButtonInterceptable false
|
||||
}
|
||||
onConfirmed(typeList[checkedId])
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建单选样式的 ChipGroup
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param typeList 选项文本列表,每一项作为一个 Chip 的文字
|
||||
* @param checkedType 默认选中项的文本,与 typeList 中元素相等的 Chip 会被勾选;为空则无默认选中
|
||||
* @return 配置完成的 ChipGroup,每个 Chip 的 id 即为其在 typeList 中的下标
|
||||
*/
|
||||
fun buildSingleChoiceChipGroup(
|
||||
context: Context,
|
||||
typeList: List<String>,
|
||||
checkedType: String?
|
||||
): ChipGroup {
|
||||
return ChipGroup(context).apply {
|
||||
isSingleSelection = true
|
||||
isSelectionRequired = false
|
||||
setPadding(5.dp, 40.dp, 5.dp, 40.dp)
|
||||
chipSpacingVertical = 10.dp
|
||||
chipSpacingHorizontal = 20.dp
|
||||
typeList.forEachIndexed { index, typeName ->
|
||||
val chip = Chip(context).apply {
|
||||
id = index
|
||||
text = typeName
|
||||
textSize = 22f
|
||||
isCheckable = true
|
||||
chipStartPadding = 20.dp.toFloat()
|
||||
chipEndPadding = 20.dp.toFloat()
|
||||
chipMinHeight = (textSize + 15.dp * 2).toFloat()
|
||||
// 选中/未选中态背景色:主题绿 / 浅灰
|
||||
chipBackgroundColor = ColorStateList(
|
||||
arrayOf(
|
||||
intArrayOf(android.R.attr.state_checked),
|
||||
intArrayOf(-android.R.attr.state_checked)
|
||||
),
|
||||
intArrayOf(
|
||||
ContextCompat.getColor(context, R.color.dish_green),
|
||||
ContextCompat.getColor(context, R.color.gray_eb)
|
||||
)
|
||||
)
|
||||
// 选中/未选中态文字色:白 / 深灰
|
||||
setTextColor(
|
||||
ColorStateList(
|
||||
arrayOf(
|
||||
intArrayOf(android.R.attr.state_checked),
|
||||
intArrayOf(-android.R.attr.state_checked)
|
||||
),
|
||||
intArrayOf(
|
||||
ContextCompat.getColor(context, R.color.white),
|
||||
ContextCompat.getColor(context, R.color.black333)
|
||||
)
|
||||
)
|
||||
)
|
||||
// 隐藏默认的左侧勾选图标,让填充色作为唯一选中反馈
|
||||
isCheckedIconVisible = false
|
||||
// 去除描边
|
||||
chipStrokeWidth = 0f
|
||||
// 若当前项已有选中值则默认勾选对应选项
|
||||
isChecked = (typeName == checkedType)
|
||||
}
|
||||
addView(chip)
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -3,4 +3,5 @@
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFF4FAFF"/>
|
||||
<stroke android:width="2dp" android:color="@color/white"/>
|
||||
<corners android:radius="8dp"/>
|
||||
</shape>
|
||||
@@ -63,6 +63,7 @@
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnTestConfirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
|
||||
@@ -16,22 +16,39 @@
|
||||
android:background="@drawable/shape_white_30_corners"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginStart="22dp"
|
||||
android:layout_marginTop="40dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:text="@string/seasoning"
|
||||
android:textColor="@color/black666"
|
||||
android:textSize="28sp" />
|
||||
android:layout_marginEnd="22dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/seasoning"
|
||||
android:textColor="@color/black666"
|
||||
android:textSize="28sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivSeasoningLayoutToggle"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:contentDescription="切换布局"
|
||||
android:src="@drawable/ic_switch_box"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvSeasoning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="660dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
tools:itemCount="26"
|
||||
|
||||
@@ -69,8 +69,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="700dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="45dp"
|
||||
android:layout_marginEnd="45dp"
|
||||
app:srlEnableOverScrollDrag="false">
|
||||
|
||||
<com.scwang.smart.refresh.header.ClassicsHeader
|
||||
@@ -81,6 +79,8 @@
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="45dp"
|
||||
android:layout_marginEnd="45dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:nestedScrollingEnabled="true"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginHorizontal="5dp"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/shape_white_fb_15_corners2">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSeasoningName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="土豆丝" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSeasoningWeight"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="@color/dish_green"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="100g" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,35 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="100dp"
|
||||
android:layout_margin="15dp"
|
||||
tools:background="@drawable/shape_white_12_corners">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvGoodsInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textColor="@color/black666"
|
||||
android:textSize="30sp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:gravity="center"
|
||||
android:layout_margin="15dp"
|
||||
tools:text="椒盐"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="椒盐" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvRawMaterialsType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:background="@drawable/shape_white_12_corners"/>
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black999"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/tvFoodScore"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvGoodsInfo"
|
||||
tools:text="切丝" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFoodScore"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black666"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
tools:text="100%"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tvRawMaterialsType"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/tvRawMaterialsType"
|
||||
app:layout_constraintTop_toTopOf="@id/tvRawMaterialsType"
|
||||
tools:text="100%" />
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -4,10 +4,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_marginHorizontal="14dp"
|
||||
android:layout_marginVertical="14dp"
|
||||
android:background="@drawable/bg_item_search"
|
||||
android:paddingHorizontal="15dp"
|
||||
android:paddingVertical="10dp">
|
||||
android:layout_marginVertical="14dp">
|
||||
|
||||
<!-- 食材名称,选中状态通过 CheckedTextView 的 checked 驱动背景/文字色变化 -->
|
||||
<CheckedTextView
|
||||
@@ -21,6 +18,7 @@
|
||||
android:textColor="@color/color_item_search"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:background="@drawable/bg_item_search"
|
||||
tools:text="土豆丝" />
|
||||
|
||||
<!-- 当前选中的净材种类,小字显示,无数据时隐藏 -->
|
||||
@@ -30,6 +28,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center|bottom"
|
||||
android:layout_marginBottom="3dp"
|
||||
android:textColor="@color/black999"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone"
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/black"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user