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