Author SHA1 Message Date
mazengfei 7b66652c0b 厨配秤: 统一采样数据结构及接口
- 将提交构成接口改为提交采样,废弃制作模式,调整请求参数结构
- 修改采样历史接口为已提交采样列表,调整接口路径和请求参数
- 更新数据模型,替换旧的审核状态为实验状态,新增实验编号字段
- 修改列表适配器显示逻辑,展示实验状态及熟重信息,调整颜色映射
- 调整相关网络请求及回调函数,删除无用参数,简化调用
- 更新UI页面文本,适配新版采样列表数据格式与提示信息
- 删除相关无用的旧状态定义及属性,保持代码整洁
2026-09-11 15:34:37 +08:00
mazengfei 9e4b2bd009 厨配秤: 统一采样数据结构及接口
- 将提交构成接口改为提交采样,废弃制作模式,调整请求参数结构
- 修改采样历史接口为已提交采样列表,调整接口路径和请求参数
- 更新数据模型,替换旧的审核状态为实验状态,新增实验编号字段
- 修改列表适配器显示逻辑,展示实验状态及熟重信息,调整颜色映射
- 调整相关网络请求及回调函数,删除无用参数,简化调用
- 更新UI页面文本,适配新版采样列表数据格式与提示信息
- 删除相关无用的旧状态定义及属性,保持代码整洁
2026-09-11 15:34:06 +08:00
mazengfei 6d3c5490a4 feat(food-prepare): 增加食材信息录入卡及相关接口支持
- 新增手动录入食材信息的UI布局,包含食材名称搜索、主辅材选择及用量输入
- 整合采样模式下的食材搜索弹窗,支持食材模糊查询与净材种类选择
- 新增相关网络接口调用,实现食材搜索、菜品搜索、菜品构成查询及采样历史等功能
- 优化食材列表展示,支持采样历史审核状态和熟重显示,增加颜色区分
- 修改PrepareFoodActivity,采样模式启用手动新增食材功能,包含添加、重置操作
- 调整Adapter和ViewModel,兼容新接口数据模型,支持分页加载和状态回调
- 修正接口路径和网络请求地址,统一使用nutrition/neglect路径前缀
- 删除冗余代码和未使用的旧接口代码,简化搜索逻辑及界面响应机制
2026-09-10 13:41:35 +08:00
mazengfei 45543295e3 Merge remote-tracking branch 'origin/dev_260316_新设备' into dev_260316_新设备 2026-09-08 14:13:05 +08:00
mazengfei 049c724303 config(base): 更新生产环境配置地址和设备ID
- 将appBaseUrl从UAT环境切换到PROD生产环境
- 更新PROD_BASE_URL为正式平台API地址
- 更换SCALE设备的正式环境DEVICE_ID_2
- 移除测试设备ID注释,启用正式设备ID配置
2026-09-08 14:12:48 +08:00
mazengfei acd1dd4605 fix(base): 更新生产环境接口地址
- 将PROD_BASE_URL从"api.dm.yixiong-tech.com:8443"替换为"platform-api.uat.shuziweidao.com"
2026-08-07 20:08:35 +08:00
mazengfei 17470bf12c feat(match): 添加烹饪开始时间和调料首次使用时间功能
- 在CookCompleteRequest中新增cookStart字段记录烹饪开始时间
- 在CookCompleteItem中新增putTime字段记录调料首次使用时间
- SubmitFoodActivity中初始化cookStartTime为进入页面时刻
- 记录调料首次拿取时间,存入seasoningFirstUseMap
- 提交接口时按调料名称匹配首次使用时间填充putTime字段
2026-08-06 18:08:51 +08:00
mazengfei 0a3185fc9e feat(cookOrder): 集成新配比秤接口和制作模式功能
- 新增接口查询当日烹饪任务列表及菜品食材构成(主材+辅材,不含调料)
- 增加调料模糊搜索功能,支持关键字分页查询
- 实现烹饪完成接口,支持提交主辅材用量、熟重及调料数据
- 在 PrepareFoodActivity 中添加烹制单 ID 传递和接口调用,区分采样和制作模式
- 在 SubmitFoodActivity 中实现制作模式烹饪完成数据组装与提交新接口逻辑
- 适配 FoodListFragment 和 FoodSearchActivity 使用熟重及烹制单 ID 关联映射
- RemoteRepository 和 NetViewModel 中新增对应接口调用方法及 UI 状态更新
- 优化 SeasoningSelectDialog 适配新接口数据结构及刷新逻辑
- 调整响应数据 ApiResponse 支持新接口标准成功码和分页字段
- 调整摄像头绑定逻辑,采样模式启用,制作模式禁用
- 统一将熟重字段用于菜品总重量计算,优化数据一致性及 UI 展示
2026-07-30 18:36:32 +08:00
mazengfei 9e41841086 feat(api): 增加当日烹饪任务列表新接口及相关数据模型支持
- 新增 CookOrderPageRequest、CookOrderMealGroup、CookOrderItem 等数据模型定义
- 在 ApiService 添加 getCookOrderPage 新接口,支持按餐次分组查询烹饪任务
- RemoteRepository 新增对应接口调用封装 getCookOrderPage
- NetViewModel 新增 cookOrderPageState 状态流及接口调用方法
- FoodListFragment 使用新接口替换旧搜索菜品接口,调整数据合并与展示逻辑
- FoodSearchActivity 使用新接口查询烹饪任务,并适配结果展示
- 更新请求拦截器添加 X-DEVICE-TOKEN 请求头支持设备登录Token传递
- 修改 GlobalData 默认基础URL为预发布环境地址
- 修正 ScaleDeviceConfig 中测试设备ID常量值
- SettingActivity 恢复环境切换入口显示,方便环境切换测试
- 补充配比秤-制作菜品新API接口完整文档,详细描述接口请求响应及业务逻辑
2026-07-29 17:59:14 +08:00
90 changed files with 2761 additions and 6697 deletions
+4 -5
View File
@@ -1,4 +1,3 @@
plugins {
// alias(libs.plugins.android.application)
// alias(libs.plugins.kotlin.android)
@@ -16,7 +15,7 @@ android {
compileSdk = 36
defaultConfig {
applicationId = "com.shuwei.dish.match.new"
applicationId = "com.shuwei.dish.match"
minSdk = 29
targetSdk = 36
versionCode = 1
@@ -25,7 +24,7 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
//设置支持的SO库架构;x86_64 仅用于在 Windows 模拟器上调试(秤功能不可用)
abiFilters.addAll(listOf("armeabi-v7a", "x86_64"))
abiFilters.addAll(listOf("armeabi-v7a"))
}
// 设置输出APK文件名格式
@@ -139,8 +138,8 @@ dependencies {
implementation(libs.androidx.camera.extensions)
//pytorch
implementation (libs.pytorch.android)
implementation (libs.pytorch.android.torchvision)
implementation(libs.pytorch.android)
implementation(libs.pytorch.android.torchvision)
// objectbox
if (isDebug) {
-32
View File
@@ -46,7 +46,6 @@
android:name=".ui.InitActivity"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|keyboard|navigation|uiMode|locale|layoutDirection|fontScale|density"
tools:ignore="DiscouragedApi,LockedOrientationActivity">
<intent-filter>
@@ -60,36 +59,6 @@
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name=".ui.HomeV2Activity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name=".ui.CleanPackActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name=".ui.MealListActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name=".ui.PackActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name=".ui.TaskActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name=".ui.TaskDetailActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name=".ui.CookingModeActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
@@ -159,7 +128,6 @@
android:launchMode="singleTask"
android:screenOrientation="landscape"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name="com.shuwei.dish.match.ui.FoodSearchV2Activity" />
<receiver
android:name=".utils.BootReceiver"
@@ -1,31 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ListItemDropdownBinding
import com.shuwei.dish.match.model.DictType
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
class DropdownAdapter(var list: MutableList<DictType>) :
BaseQuickAdapter<DictType, DropdownAdapter.VH>(list) {
inner class VH(var binding: ListItemDropdownBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemDropdownBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: DictType?) {
holder.binding.tvDropdown.text = item?.value
holder.binding.divider.run {
if (position == list.size - 1) gone() else visible()
}
}
}
@@ -9,6 +9,7 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemFoodListBinding
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.SampleStatus
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
import java.text.DecimalFormat
@@ -88,6 +89,14 @@ class FoodListAdapter(
// 非烹饪中,采样模式
tvDishName.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.dish_green))
clBlock.setBackgroundResource(R.drawable.shape_white_fb_15_corners)
// 已提交采样列表(sample-list):展示实验状态 + 熟重(g)
val statusName = item.statusName
if (!statusName.isNullOrBlank()) {
tvDishCount.text = "熟重:${formatGram(foodWeight)}"
tvShowState.text = statusName
tvShowState.setTextColor(statusColor(holder, item.status))
return@run
}
val realTotalWeight = if (totalWeight > 0.0) totalWeight else foodWeight
val showTotalWeight = if (realTotalWeight == 0.0) "-" else df.format(realTotalWeight / 1000.0F)
tvDishCount.text = "累计统计:${showTotalWeight}kg(${item.count ?: "-"}次)"
@@ -96,5 +105,21 @@ class FoodListAdapter(
}
}
/** 熟重(g)格式化,整数不带小数,小数保留 2 位 */
private fun formatGram(weight: Double): String {
val gramDf = DecimalFormat("0.##")
return "${gramDf.format(weight)}g"
}
/** 实验状态着色:1=实验中(灰) / 2=已评测(绿) / 9=已转化(绿) */
private fun statusColor(holder: VH, status: Int?): Int {
val context = holder.itemView.context
return when (status) {
SampleStatus.EVALUATED, SampleStatus.CONVERTED ->
ContextCompat.getColor(context, R.color.dish_green)
else -> ContextCompat.getColor(context, R.color.black999)
}
}
private val df = DecimalFormat("0.000")
}
@@ -14,7 +14,6 @@ import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.visible
import androidx.core.graphics.toColorInt
import com.shuwei.dish.match.ui.MaterialTypeTool
class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
BaseQuickAdapter<GoodsItem, FoodMaterialAdapter.VH>(list) {
@@ -43,11 +42,7 @@ class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
//} else {
// tvDishName.text = data.goodsName + "-${data.rawMaterialsType}"
//}
val showGoodsCode = (data.materCode ?: "").ifBlank { "-" }
tvGoodsCode.text = "溯源码:$showGoodsCode"
val materialText = MaterialTypeTool.getMaterialText(data.materialType)
tvDishType.text = "主辅材:$materialText"
//if (data.materialType == 1) "主辅材:主材" else if (data.materialType == 2) "主辅材:辅材" else ""
tvDishType.text = if (data.materialType == 1) "主辅材:主材" else if (data.materialType == 2) "主辅材:辅材" else ""
tvDishWeight.text =
if (data.useWeight == null || data.useWeight == 0.toDouble()) "" else "${data.useWeight!!.roundedOneDecimalPlace()}"
tvDishWeight.setTextColor(
@@ -60,11 +55,11 @@ class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
ivOperateIcon.run {
if (data.isSamplingPage) gone() else visible()
}
// ivClearIcon.visible()
// ivClearIcon.setImageResource(
// if (data.isOriginalData) R.drawable.ic_dish_clear
// else R.drawable.ic_delete
// )
ivClearIcon.visible()
ivClearIcon.setImageResource(
if (data.isOriginalData) R.drawable.ic_dish_clear
else R.drawable.ic_delete
)
// isOriginalData 时根据是否有重量信息控制图标颜色:无重量染灰色,有重量恢复原色
if (data.isOriginalData) {
val hasWeight = data.useWeight != null && data.useWeight != 0.0
@@ -1,245 +0,0 @@
package com.shuwei.dish.match.adapter
import android.app.DatePickerDialog
import android.content.Context
import android.text.Editable
import android.text.InputType
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import androidx.recyclerview.widget.LinearLayoutManager
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemFormFieldBinding
import com.shuwei.dish.match.dialog.DropdownPopup
import com.shuwei.dish.match.model.FieldType
import com.shuwei.dish.match.model.FormField
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.hideKeyboard
import com.shuwei.dish.match.utils.ext.visible
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import kotlin.math.min
/**
* 动态表单字段 Adapter,配合 FlexboxLayoutManager 实现两列自动换行布局
* 单一 item 布局,通过 visibility 控制 TEXT/INTEGER/DECIMAL/FIXED/DROPDOWN/DATE_PICKER 六种类型渲染
*
* @param fields 表单字段列表(hidden=false 的字段,由外部过滤后传入)
*/
class FormFieldAdapter(
fields: MutableList<FormField>
) : BaseQuickAdapter<FormField, FormFieldAdapter.VH>(fields) {
private val isoFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
private val displayDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
inner class VH(val binding: ListItemFormFieldBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemFormFieldBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FormField?) {
item ?: return
val b = holder.binding
// 设置标签,必填项追加红色星号
b.tvLabel.text = buildLabelSpannable(item.label, item.required)
// 先全部隐藏,再按类型显示对应控件
b.flInputArea.gone()
b.etInput.gone()
b.flDropdown.gone()
b.flDatePicker.gone()
b.llChildren.gone()
// 移除旧的 TextWatcher,防止 RecyclerView 复用时错误触发
(b.etInput.tag as? TextWatcher)?.let { b.etInput.removeTextChangedListener(it) }
b.etInput.tag = null
when (item.type) {
FieldType.TEXT -> bindEditText(b, item, InputType.TYPE_CLASS_TEXT)
FieldType.INTEGER -> bindEditText(b, item, InputType.TYPE_CLASS_NUMBER)
FieldType.DECIMAL -> bindEditText(
b, item,
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED
)
FieldType.FIXED -> bindFixed(b, item)
FieldType.DROPDOWN -> bindDropdown(b, item)
FieldType.DATE_PICKER -> bindDatePicker(b, item)
FieldType.CHILDREN -> bindChildren(b, item)
}
if (item.type == FieldType.CHILDREN) {
b.flInputArea.gone()
b.llChildren.visible()
} else {
b.flInputArea.visible()
b.llChildren.gone()
}
}
/** 绑定可编辑文本框(TEXT / NUMBER */
private fun bindEditText(b: ListItemFormFieldBinding, item: FormField, inputType: Int) {
b.etInput.visible()
b.etInput.isEnabled = true
b.etInput.inputType = inputType
b.etInput.hint = item.hint.ifBlank { item.label }
b.etInput.setBackgroundResource(R.drawable.bg_white_radius10_stroke2)
b.etInput.setText(item.value)
b.etInput.setSelection(b.etInput.text.length)
val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
item.value = s?.toString() ?: ""
}
}
b.etInput.addTextChangedListener(watcher)
b.etInput.tag = watcher
}
/** 绑定固定值(不可编辑,灰色背景区分) */
private fun bindFixed(b: ListItemFormFieldBinding, item: FormField) {
b.etInput.visible()
b.etInput.isEnabled = false
b.etInput.inputType = InputType.TYPE_CLASS_TEXT
b.etInput.hint = ""
b.etInput.setBackgroundResource(R.drawable.bg_gray_radius10)
b.etInput.setText(item.value)
}
/** 绑定下拉选择框,点击时创建 DropdownPopup */
private fun bindDropdown(b: ListItemFormFieldBinding, item: FormField) {
b.flDropdown.visible()
b.tvDropdownValue.hint = item.hint.ifBlank { "请选择${item.label}" }
b.tvDropdownValue.text = item.value.ifBlank { null }
b.flDropdown.setOnClickListener { v ->
v.hideKeyboard()
val popHeight = min(item.options.size.coerceAtLeast(1), 4) * 71.dp
DropdownPopup(
context = context,
list = item.options.toMutableList(),
popWidth = v.width,
popHeight = popHeight
) { dictType ->
item.value = dictType.value ?: ""
b.tvDropdownValue.text = item.value
// 将 extraApiKeys 映射的 DictType 字段值写入 extraValues
item.extraValues.clear()
item.extraApiKeys.forEach { (apiKey, dictField) ->
item.extraValues[apiKey] = dictType[dictField]
}
// 触发选中回调
item.onDropdownSelected?.invoke(dictType)
}.also {
it.bgLayout = b.flDropdown
it.showAsDropDown(v)
}
}
}
/** 绑定日期选择器,点击时弹出 DatePickerDialog */
private fun bindDatePicker(b: ListItemFormFieldBinding, item: FormField) {
b.flDatePicker.visible()
b.tvDateValue.hint = item.hint.ifBlank { "请选择日期" }
b.tvDateValue.text = item.value.ifBlank { null }
b.flDatePicker.setOnClickListener { v ->
v.hideKeyboard()
val cal = Calendar.getInstance()
DatePickerDialog(
context,
{ _, year, month, dayOfMonth ->
val selected = Calendar.getInstance().apply {
set(year, month, dayOfMonth, 0, 0, 0)
set(Calendar.MILLISECOND, 0)
}
// value 存日期显示格式,供提交使用
item.value = displayDateFormat.format(selected.time)
b.tvDateValue.text = item.value
},
cal.get(Calendar.YEAR),
cal.get(Calendar.MONTH),
cal.get(Calendar.DAY_OF_MONTH)
).show()
}
}
/** 构建带红色星号的必填标签 */
private fun buildLabelSpannable(label: String, required: Boolean): SpannableStringBuilder {
return SpannableStringBuilder().apply {
append(label)
if (required) {
val start = length
append(" *")
setSpan(
ForegroundColorSpan("#FFCC0000".toColorInt()),
start, length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
/** 绑定 CHILDREN 类型:渲染子列表(表头 + 嵌套 RecyclerView + 添加按钮) */
private fun bindChildren(b: ListItemFormFieldBinding, item: FormField) {
b.llChildren.visible()
b.tvAddChild.text = item.childAddLabel ?: "+ 添加"
// 初始化嵌套 RecyclerView
if (b.rvChildren.adapter == null) {
b.rvChildren.layoutManager = LinearLayoutManager(context)
}
b.rvChildren.adapter = createRowAdapter(b, item)
// 添加按钮点击
b.tvAddChild.setOnClickListener {
item.children?.let { template ->
val newRow = template.map { field ->
FormField(
label = field.label,
type = field.type,
apiKey = field.apiKey,
extraApiKeys = field.extraApiKeys,
required = field.required,
hint = field.hint,
options = field.options
)
}.toMutableList()
item.childrenRows.add(newRow)
b.rvChildren.adapter?.notifyItemInserted(item.childrenRows.size - 1)
}
}
}
/** 创建子列表行 Adapter */
private fun createRowAdapter(b: ListItemFormFieldBinding, item: FormField): IngredientRowAdapter {
return IngredientRowAdapter(
rows = item.childrenRows,
onDelete = onDelete@{ position ->
val adapter = b.rvChildren.adapter as? IngredientRowAdapter ?: return@onDelete
item.childrenRows.removeAt(position)
adapter.notifyItemRemoved(position)
if (position < item.childrenRows.size) {
adapter.notifyItemRangeChanged(position, item.childrenRows.size - position)
}
},
onChildClick = { _, _, _ ->
// 子字段值已直接更新到 row 的 FormField 中,无需额外操作
}
)
}
}
@@ -1,40 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemInboundIngredientBinding
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.utils.ext.formatDecimal
/**
* 已入库净菜食材列表适配器
*/
class InboundIngredientAdapter(list: MutableList<NutCleanInboundIngredientVO>) :
BaseQuickAdapter<NutCleanInboundIngredientVO, InboundIngredientAdapter.VH>(list) {
inner class VH(var binding: ListItemInboundIngredientBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemInboundIngredientBinding.inflate(
LayoutInflater.from(context), parent, false
)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: NutCleanInboundIngredientVO?) {
item ?: return
holder.binding.run {
tvIngredientName.text = item.ingredientName ?: "-"
val traceCode = item.traceCode.takeUnless { it.isNullOrBlank() } ?: "-"
tvTraceCode.text = "溯源码:$traceCode"
val stock = item.currentStock.takeUnless { it.isNullOrBlank() } ?: "0"
tvStock.text = "库存:${stock.formatDecimal()}kg"
}
}
}
@@ -1,40 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ListItemInboundIngredient2Binding
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.utils.ext.formatDecimal
/**
* 已入库净菜食材列表适配器 V2
*/
class InboundIngredientV2Adapter(list: MutableList<NutCleanInboundIngredientVO>) :
BaseQuickAdapter<NutCleanInboundIngredientVO, InboundIngredientV2Adapter.VH>(list) {
inner class VH(var binding: ListItemInboundIngredient2Binding) :
QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemInboundIngredient2Binding.inflate(
LayoutInflater.from(context), parent, false
)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: NutCleanInboundIngredientVO?) {
item ?: return
holder.binding.run {
tvIngredientName.text = item.ingredientName ?: "-"
val traceCode = item.traceCode.takeUnless { it.isNullOrBlank() } ?: "-"
tvTraceCode.text = "溯源码:$traceCode"
val stock = item.currentStock.takeUnless { it.isNullOrBlank() } ?: "0"
tvStock.text = "库存:${stock.formatDecimal()}kg"
}
}
}
@@ -1,149 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemIngredientRowBinding
import com.shuwei.dish.match.dialog.DropdownPopup
import com.shuwei.dish.match.model.FormField
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.hideKeyboard
/**
* 食材行 Adapter,用于「包含食材」子列表的嵌套渲染
* @param context Activity 上下文
* @param rows 每行包含多个 FormField,对应溯源码、食材名称、分类、用量
* @param onDelete 删除行回调
* @param onChildClick 子字段点击回调(如下拉框点击)
*/
class IngredientRowAdapter(
rows: MutableList<MutableList<FormField>>,
private val onDelete: (Int) -> Unit,
private val onChildClick: (Int, Int, FormField) -> Unit
) : BaseQuickAdapter<MutableList<FormField>, IngredientRowAdapter.VH>(rows) {
inner class VH(val binding: ListItemIngredientRowBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemIngredientRowBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: MutableList<FormField>?) {
item ?: return
val b = holder.binding
// 假设 children 模板顺序:[溯源码(TEXT), 食材名称(DROPDOWN), 分类(DROPDOWN), 用量(TEXT)]
val traceCodeField = item.getOrNull(0)
val materNameField = item.getOrNull(1)
val classifyField = item.getOrNull(2)
val amountField = item.getOrNull(3)
// 溯源码 - TEXT
traceCodeField?.let { field ->
b.etTraceCode.setText(field.value)
b.etTraceCode.hint = field.hint.ifBlank { field.label }
(b.etTraceCode.tag as? TextWatcher)?.let { b.etTraceCode.removeTextChangedListener(it) }
val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
field.value = s?.toString() ?: ""
}
}
b.etTraceCode.addTextChangedListener(watcher)
b.etTraceCode.tag = watcher
}
// 食材名称
materNameField?.let { field ->
when (field.type) {
com.shuwei.dish.match.model.FieldType.FIXED -> {
b.flMaterName.setOnClickListener { v -> v.hideKeyboard() }
b.flMaterName.isClickable = false
b.tvMaterName.text = field.value
}
else -> {
b.tvMaterName.hint = field.hint.ifBlank { "请选择${field.label}" }
b.tvMaterName.text = field.value.ifBlank { null }
b.flMaterName.setOnClickListener { v ->
v.hideKeyboard()
showDropdown(b.flMaterName, field, b.tvMaterName, holder.getAdapterPosition(), 1)
}
}
}
}
// 分类 - DROPDOWN
classifyField?.let { field ->
b.tvClassify.hint = field.hint.ifBlank { "请选择${field.label}" }
b.tvClassify.text = field.value.ifBlank { null }
b.flClassify.setOnClickListener { v ->
v.hideKeyboard()
showDropdown(b.flClassify, field, b.tvClassify, holder.getAdapterPosition(), 2)
}
}
// 用量 - TEXT
amountField?.let { field ->
b.etAmount.setText(field.value)
b.etAmount.hint = field.hint.ifBlank { field.label }
(b.etAmount.tag as? TextWatcher)?.let { b.etAmount.removeTextChangedListener(it) }
val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
field.value = s?.toString() ?: ""
}
}
b.etAmount.addTextChangedListener(watcher)
b.etAmount.tag = watcher
}
// 删除按钮
b.tvRemove.setOnClickListener {
val pos = holder.layoutPosition
if (pos != RecyclerView.NO_POSITION) {
onDelete(pos)
}
}
}
/** 显示下拉弹出框 */
private fun showDropdown(
anchorView: ViewGroup,
field: FormField,
valueView: TextView,
rowPosition: Int,
childIndex: Int
) {
DropdownPopup(
context = context,
list = field.options.toMutableList(),
popWidth = anchorView.width,
popHeight = kotlin.math.min(field.options.size.coerceAtLeast(1), 4) * 50.dp
) { dictType ->
field.value = dictType.value ?: ""
valueView.text = field.value
// 将 extraApiKeys 映射值写入 extraValues
field.extraValues.clear()
field.extraApiKeys.forEach { (apiKey, dictField) ->
field.extraValues[apiKey] = dictType[dictField]
}
field.onDropdownSelected?.invoke(dictType)
// 回调通知外部更新
onChildClick(rowPosition, childIndex, field)
}.also {
it.bgLayout = anchorView
it.showAsDropDown(anchorView)
}
}
}
@@ -1,36 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ListItemFood2Binding
import com.shuwei.dish.match.model.NutFoodOptionVO
/**
* 餐品列表适配器
*/
class MealListAdapter(list: MutableList<NutFoodOptionVO>) :
BaseQuickAdapter<NutFoodOptionVO, MealListAdapter.VH>(list) {
inner class VH(var binding: ListItemFood2Binding) : QuickViewHolder(binding.root)
override fun onBindViewHolder(
holder: VH,
position: Int,
item: NutFoodOptionVO?
) {
holder.binding.tvDishInfo.text = item?.foodName
}
override fun onCreateViewHolder(
context: Context,
parent: ViewGroup,
viewType: Int
): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemFood2Binding.inflate(inflater, parent, false)
return VH(binding)
}
}
@@ -1,53 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemTaskBinding
import com.shuwei.dish.match.model.NutComboTaskVO
/**
* 组配任务列表适配器
*/
class TaskAdapter(list: MutableList<NutComboTaskVO>) :
BaseQuickAdapter<NutComboTaskVO, TaskAdapter.VH>(list) {
inner class VH(var binding: ListItemTaskBinding) : QuickViewHolder(binding.root)
override fun onBindViewHolder(
holder: VH,
position: Int,
item: NutComboTaskVO?
) {
holder.binding.apply {
tvTaskNo.text = "任务编号:${item?.taskNo}"
tvTargetDish.text = item?.targetDish
tvSpec.text = item?.spec
tvPlanDate.text = "计划日期:${item?.planDate}"
tvPortions.text = "总份数:${item?.portions}"
tvOwnerName.text = "负责人:${item?.ownerName}"
// 状态文本映射
val statusText = when (item?.comboStatus) {
0 -> "待组配"
1 -> "组配中"
2 -> "已完成"
else -> "未知"
}
tvStatus.text = statusText
}
}
override fun onCreateViewHolder(
context: Context,
parent: ViewGroup,
viewType: Int
): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemTaskBinding.inflate(inflater, parent, false)
return VH(binding)
}
}
@@ -1,96 +0,0 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemTaskDetailBinding
import com.shuwei.dish.match.model.NutComboTaskItemDetail
import com.shuwei.dish.match.ui.MaterialTypeTool
import com.shuwei.dish.match.utils.ext.formatDecimal
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
import java.math.BigDecimal
/**
* 组配任务详情食材列表适配器
* Adapter for displaying ingredient list in task details
*/
class TaskDetailAdapter(list: MutableList<NutComboTaskItemDetail>) :
BaseQuickAdapter<NutComboTaskItemDetail, TaskDetailAdapter.VH>(list) {
// ViewHolder class for holding the view bindings
inner class VH(var binding: ListItemTaskDetailBinding) : QuickViewHolder(binding.root)
/**
* Called by RecyclerView to display the item at the specified position.
* This method updates the ViewHolder's contents with the item's data.
*
* @param holder The ViewHolder to be updated
* @param position The position of the item in the adapter
* @param item The NutComboTaskItemDetail to be displayed
*/
override fun onBindViewHolder(
holder: VH,
position: Int,
item: NutComboTaskItemDetail?
) {
item ?: return
holder.binding.apply {
// Set ingredient name
tvIngredientName.text = item.ingredientName
// 食材类别
// val classText = when (item.ingredientClass) {
// 1 -> "主材"
// 2 -> "辅材"
// 3 -> "调料"
// else -> "-"
// }
tvIngredientClass.text = MaterialTypeTool.getMaterialText(item.ingredientClass ?: 0)
val unit = item.unit ?: "g"
tvPerPortionQty.text = "每份用量:${item.perPortionQty.formatDecimal()}${unit}"
tvActualQty.run {
if (item.actualQty.isNullOrBlank()) {
setTextColor("#999999".toColorInt())
text = "-"
} else {
setTextColor("#00BC71".toColorInt())
text = item.actualQty.formatDecimal() + unit
}
}
// tvTotalQty.text = "合计用量:${item?.totalQty.formatDecimal()}${unit}"
tvTraceCode.visible()
tvTraceCode.text = "溯源码:${(item.traceCode ?: "").ifBlank { "-" }}"
// val hasWeight = item.actualQty.isNullOrBlank().not()
// ivClearIcon.imageTintList = if (hasWeight) null else ColorStateList.valueOf("#BFBFBF".toColorInt())
ivOperateIcon.setImageResource(
if (item.isSetFinished == true) R.drawable.ic_dish_selected
else R.drawable.ic_dish_unselected
)
root.setBackgroundResource(
if (item.isClicked == true) R.drawable.shape_item_cook_dish
else R.drawable.shape_white_fb_15_corners
)
}
}
override fun onCreateViewHolder(
context: Context,
parent: ViewGroup,
viewType: Int
): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemTaskDetailBinding.inflate(inflater, parent, false)
return VH(binding)
}
}
@@ -51,10 +51,10 @@ open class BaseActivity : AppCompatActivity() {
// 提前隐藏系统栏,避免窗口创建时导航栏闪烁
hideSystemBars()
// 主设备竖屏,子设备横屏
// requestedOrientation = if (GlobalData.deviceRole == DeviceRole.MASTER)
// ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
// else
// ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
requestedOrientation = if (GlobalData.deviceRole == DeviceRole.MASTER)
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
ActivityManager.addActivity(this)
binding = ActivityBaseBinding.inflate(layoutInflater)
if (this !is InitActivity || GlobalData.deviceRole == DeviceRole.SLAVE) {
@@ -11,12 +11,15 @@ import android.util.Log
import com.shuwei.dish.match.BuildConfig
import com.shuwei.dish.match.R
import com.shuwei.dish.match.db.DatabaseProvider
import com.shuwei.dish.match.objbox.ObjectBox
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.AppUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.BootReceiver
import com.shuwei.dish.match.utils.CrashHandler
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.Weigher2
class BaseApp : Application() {
@@ -31,28 +34,25 @@ class BaseApp : Application() {
GlobalData.appBaseUrl = savedBaseUrl
}
CrashHandler.init(this)
val deviceId = if (BuildConfig.IS_TEST_DEVICE) ScaleDeviceConfig.DEVICE_ID_2 else AppUtil.getUDID(this)
Log.d(TAG, "onCreate: deviceId=$deviceId, isTestDevice=${BuildConfig.IS_TEST_DEVICE}")
GlobalData.deviceId = deviceId
GlobalData.deviceId = AppUtil.getUDID(this)
// 根据设备 ID 判断角色:指定 ID 为主设备,其余为子设备
// 主设备是/dev/ttyS7,子设备是/dev/ttyS4
if (deviceId == ScaleDeviceConfig.DEVICE_ID_2) {
GlobalData.deviceRole = DeviceRole.MASTER
Weigher2.setDevicePort("/dev/ttyS7")
} else {
GlobalData.deviceRole = DeviceRole.SLAVE
Weigher2.setDevicePort("/dev/ttyS4")
}
Log.d(TAG, "设备角色: ${GlobalData.deviceRole}")
// val deviceId = if (BuildConfig.IS_TEST_DEVICE) ScaleDeviceConfig.DEVICE_ID_2 else AppUtil.getUDID(this)
// Log.d(TAG, "onCreate: deviceId=$deviceId, isTestDevice=${BuildConfig.IS_TEST_DEVICE}")
// GlobalData.deviceId = deviceId
//
// // 根据设备 ID 判断角色:指定 ID 为主设备,其余为子设备
// // 主设备是/dev/ttyS7,子设备是/dev/ttyS4
// if (deviceId == ScaleDeviceConfig.DEVICE_ID_2) {
// GlobalData.deviceRole = DeviceRole.MASTER
// Weigher2.setDevicePort("/dev/ttyS7")
// } else {
// GlobalData.deviceRole = DeviceRole.SLAVE
// Weigher2.setDevicePort("/dev/ttyS4")
// }
// Log.d(TAG, "设备角色: ${GlobalData.deviceRole}")
//
// // 启动秤服务(主设备多启动发现+聚合,子设备只启动服务端+注册)
// ScaleServiceManager.start(this)
//
// // 初始化秤,进程级生命周期管理:最后一个 Activity 销毁时才停止读取
// 启动秤服务(主设备多启动发现+聚合,子设备只启动服务端+注册)
ScaleServiceManager.start(this)
// 初始化秤,进程级生命周期管理:最后一个 Activity 销毁时才停止读取
WeightUtil.init()
WeightUtil.getWeight()
WeightUtil.startContinuousRead()
@@ -74,14 +74,13 @@ class BaseApp : Application() {
// GlobalData.deviceId = "39a7abdd06b3c7ab"
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
registerReceiver(BootReceiver(), filter)
// ObjectBox.init(this)
ObjectBox.init(this)
}
companion object {
private val TAG = "BaseApp"
var canteenId = "0"
var appVersion: String = "1"
@Volatile
private var sharedPref: SharedPreferences? = null
@@ -8,17 +8,14 @@ package com.shuwei.dish.match.base
enum class DeviceRole { MASTER, SLAVE }
object GlobalData {
var appBaseUrl: String = LOCAL_BASE_URL
var appBaseUrl: String = PROD_BASE_URL
/**
* 具体业务 BaseUrl
*/
// const val LOCAL_BASE_URL = "http://192.168.1.201:14801"
const val LOCAL_BASE_URL = "http://192.168.10.101:24801"
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
// const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8083"
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443"
const val TEST_BASE_URL = "http://192.168.10.101:24801"
const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
/**
* 设备id
*/
@@ -28,8 +25,6 @@ object GlobalData {
* 当前设备角色,启动时从 SpTool 读取,默认为 SLAVE
*/
var deviceRole: DeviceRole = DeviceRole.SLAVE
var userName = "张三"
}
/**
@@ -1,78 +0,0 @@
package com.shuwei.dish.match.dialog
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupWindow
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.DropdownAdapter
import com.shuwei.dish.match.databinding.LayoutPopupDropdownBinding
import com.shuwei.dish.match.model.DictType
import com.shuwei.dish.match.utils.ext.dp
class DropdownPopup(
var context: Context,
var list: MutableList<DictType>,
var popWidth: Int = 280.dp,
var popHeight: Int = 284.dp,
var onPopupItemClick: (DictType) -> Unit
) : PopupWindow() {
init {
// 初始化PopupWindow
// val popupView = LayoutInflater.from(context).inflate(R.layout.layout_popup_dropdown, null)
val binding = LayoutPopupDropdownBinding.inflate(LayoutInflater.from(context))
contentView = binding.root
width = ViewGroup.LayoutParams.WRAP_CONTENT
height = ViewGroup.LayoutParams.WRAP_CONTENT
// 设置透明背景,避免覆盖圆角
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
isOutsideTouchable = true
isFocusable = true
// 设置动画样式
// animationStyle = R.style.PopupAnimation
// 初始化视图和事件
initViews(binding.rvWarehouse)
}
private fun initViews(rv: RecyclerView) {
// 获取关闭按钮并设置点击事件
rv.updateLayoutParams {
width = popWidth
height = popHeight
}
rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
rv.adapter = DropdownAdapter(list).setOnItemClickListener { adapter, view, position ->
onPopupItemClick(list[position])
dismiss()
}
}
var arrowImage: ImageView? = null
var bgLayout: ViewGroup? = null
var defLayoutBgResId = R.drawable.bg_white_radius10_stroke2
override fun showAsDropDown(anchor: View?) {
super.showAsDropDown(anchor)
bgLayout?.setBackgroundResource(R.drawable.shape_green_stroke)
arrowImage?.setImageResource(R.mipmap.ic_triangle_up)
}
override fun dismiss() {
super.dismiss()
bgLayout?.setBackgroundResource(defLayoutBgResId)
arrowImage?.setImageResource(R.mipmap.ic_triangle_down)
}
}
@@ -40,8 +40,8 @@ class EnvSwitchDialog(
return@setPositiveButtonInterceptable false
}
val newUrl = when (contentBinding.rgEnv.checkedRadioButtonId) {
contentBinding.rbTest.id -> GlobalData.LOCAL_BASE_URL
contentBinding.rbUat.id -> GlobalData.TEST_BASE_URL
contentBinding.rbTest.id -> GlobalData.TEST_BASE_URL
contentBinding.rbUat.id -> GlobalData.UAT_BASE_URL
contentBinding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setPositiveButtonInterceptable false
}
@@ -54,8 +54,8 @@ class EnvSwitchDialog(
init {
// 根据当前 appBaseUrl 预选对应 RadioButton
when (GlobalData.appBaseUrl) {
GlobalData.LOCAL_BASE_URL -> contentBinding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> contentBinding.rbUat.isChecked = true
GlobalData.TEST_BASE_URL -> contentBinding.rbTest.isChecked = true
GlobalData.UAT_BASE_URL -> contentBinding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> contentBinding.rbProd.isChecked = true
}
}
@@ -11,9 +11,6 @@ import android.view.View
import android.view.Window
import android.widget.FrameLayout
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.chad.library.adapter4.util.setOnDebouncedItemClick
@@ -24,6 +21,7 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.DialogFoodSearchBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.MaterSearchItem
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.showRawMaterialsDialog
@@ -54,15 +52,23 @@ class FoodSearchDialog(
isStateViewEnable = true
setOnDebouncedItemClick { _, _, position ->
val item = list.getOrNull(position) ?: return@setOnDebouncedItemClick
// 弹出净材种类单选弹窗,确认后再回调并关闭弹窗
item.isClicked = true
notifyItemChanged(position)
// mater-search 不返回净材种类,rawMaterialsTypes 为空时直接选中返回,避免卡在净材弹窗
if (item.rawMaterialsTypes.isNullOrBlank()) {
binding.root.postDelayed({
onItemSelected(item)
dismiss()
}, 300)
return@setOnDebouncedItemClick
}
// 有净材种类时弹出单选弹窗,确认后再回调并关闭弹窗
showRawMaterialsDialog(
context = activity,
types = item.rawMaterialsTypes,
checkedType = item.rawMaterialsType
) { selectedType ->
item.rawMaterialsType = selectedType
item.isClicked = true
notifyItemChanged(position)
binding.root.postDelayed({
onItemSelected(item)
dismiss()
@@ -121,8 +127,6 @@ class FoodSearchDialog(
binding.ivSearch.setOnClickListener { searchGoods(it) }
binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) }
initObserver()
// 若有默认食材名称,自动填充并触发搜索
//if (!defGoodsName.isNullOrBlank()) {
//binding.etSheetInput.setText(defGoodsName)
@@ -170,27 +174,6 @@ class FoodSearchDialog(
}
}
/**
* 收集 foodSearchGoodsListState,统一处理 Loading / Success / Error 状态
* 使用独立的 State,避免与 FoodRecognizeActivity 等页面共用 goodsListState 产生干扰
*/
private fun initObserver() {
activity.lifecycleScope.launch {
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
activity.netViewModel.foodSearchGoodsListState.collect { state ->
when (state) {
is UiState.Success -> loadGoodsList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
finishRefresh()
}
else -> {}
}
}
}
}
}
/**
* 触发搜索:校验输入、隐藏键盘、发起请求
*/
@@ -205,14 +188,24 @@ class FoodSearchDialog(
}
/**
* 请求食材列表,使用弹窗专用接口,结果写入 foodSearchGoodsListState
* 请求食材列表(采样模式新配比秤接口 mater-search,回调版本)
*/
private fun getGoodsList() {
activity.netViewModel.queryFoodSearchGoodsList(
goodsType = "0",
activity.netViewModel.materSearch(
keyword = goodsName.takeUnless { it.isNullOrBlank() },
pageNum = pageNo,
pageSize = PAGE_SIZE,
goodsName = goodsName.takeUnless { it.isNullOrBlank() }
onResult = { state ->
when (state) {
is UiState.Success -> loadGoodsList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
finishRefresh()
if (pageNo == 1) loadEmptyView()
}
else -> {}
}
}
)
}
@@ -220,7 +213,7 @@ class FoodSearchDialog(
* 将返回数据填充到列表
*/
@SuppressLint("NotifyDataSetChanged")
private fun loadGoodsList(records: MutableList<GoodsItem>?) {
private fun loadGoodsList(records: List<MaterSearchItem>?) {
finishRefresh()
if (records.isNullOrEmpty()) {
if (pageNo == 1) {
@@ -235,20 +228,27 @@ class FoodSearchDialog(
}
}
if (pageNo == 1) list.clear()
// 将接口返回的 rawMaterialsType(全部种类)移存到 rawMaterialsTypes,供弹窗使用
records.forEach {
it.rawMaterialsTypes = it.rawMaterialsType
it.rawMaterialsType = null
it.oldGoodsId = it.goodsId
it.goodsId = it.materId ?: ""
}
list.addAll(records)
// mater-search 不返回净材种类,rawMaterialsTypes 保持 null,点击时直接选中返回
val goodsItems = records.map { it.toGoodsItem() }
list.addAll(goodsItems)
adapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) pageNo++
}
/**
* MaterSearchItem → GoodsItem 映射
* id 即食材id,同时作为 goodsId 和 materId(提交 sample/save 时使用)
*/
private fun MaterSearchItem.toGoodsItem() = GoodsItem(
goodsId = id,
materId = id,
goodsName = materName,
materCode = materCode,
popularName = alias
)
private fun finishRefresh() {
if (pageNo == 1) binding.refreshLayout.finishRefresh()
else binding.refreshLayout.finishLoadMore()
@@ -1,214 +0,0 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.graphics.Rect
import android.text.Editable
import android.view.KeyboardShortcutGroup
import android.view.LayoutInflater
import android.view.Menu
import android.view.MotionEvent
import android.view.View
import android.view.Window
import android.widget.FrameLayout
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.chad.library.adapter4.util.setOnDebouncedItemClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.InboundIngredientAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.DialogInboundIngredientSearchBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.toast
/**
* 已入库净菜食材搜索弹窗
* @param activity 宿主 Activity
* @param defIngredientName 默认填充的食材名称(可选)
* @param onItemSelected 用户点击某项食材时的回调
*/
class InboundIngredientSearchDialog(
private val activity: BaseActivity,
private val viewModelV2: NetViewModelV2,
private val defIngredientName: String? = null,
private val onItemSelected: (item: NutCleanInboundIngredientVO) -> Unit
) : BottomSheetDialog(activity, R.style.BottomSheet) {
private val binding = DialogInboundIngredientSearchBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<NutCleanInboundIngredientVO>()
private val adapter = InboundIngredientAdapter(list).apply {
isStateViewEnable = true
setOnDebouncedItemClick { _, _, position ->
val item = list.getOrNull(position) ?: return@setOnDebouncedItemClick
onItemSelected(item)
dismiss()
}
}
private var keyword: String? = null
init {
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false
setOnDismissListener { activity.hideStatusBar() }
binding.tvSheetName.text = "已入库净菜检索"
// 输入框:清空时重置列表
binding.etSheetInput.run {
hint = "输入食材名称"
addTextChangedListener(
onTextChanged = { text, _, _, _ -> keyword = text.toString() },
afterTextChanged = { it: Editable? ->
if (it.isNullOrBlank()) {
list.clear()
adapter.notifyDataSetChanged()
}
}
)
addOnActionSearchListener { searchIngredient(this) }
}
// RecyclerView 初始化
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = this@InboundIngredientSearchDialog.adapter
}
binding.refreshLayout.run {
setEnableRefresh(true)
setEnableLoadMore(false)
setOnRefreshListener { getIngredientList() }
}
binding.ivSearch.setOnClickListener { searchIngredient(it) }
binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) }
// 若有默认食材名称,自动填充并触发搜索
if (!defIngredientName.isNullOrBlank()) {
binding.etSheetInput.setText(defIngredientName)
getIngredientList()
KeyboardUtil.hideKeyboard(binding.root)
}
}
/** 弹窗显示时在 Window.Callback 层提前拦截触摸,解决 BottomSheet 拖拽与列表滑动的手势冲突 */
override fun show() {
super.show()
val originalCallback = window?.callback ?: return
window?.callback = object : Window.Callback by originalCallback {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val rect = Rect()
binding.refreshLayout.getGlobalVisibleRect(rect)
behavior.isDraggable = !rect.contains(event.rawX.toInt(), event.rawY.toInt())
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
behavior.isDraggable = true
}
}
return originalCallback.dispatchTouchEvent(event)
}
override fun onPointerCaptureChanged(hasCapture: Boolean) {
originalCallback.onPointerCaptureChanged(hasCapture)
}
override fun onProvideKeyboardShortcuts(
data: List<KeyboardShortcutGroup?>?,
menu: Menu?,
deviceId: Int
) {
originalCallback.onProvideKeyboardShortcuts(data, menu, deviceId)
}
}
}
/**
* 触发搜索:校验输入、隐藏键盘、发起请求
*/
private fun searchIngredient(v: View) {
if (keyword.isNullOrBlank()) {
activity.toast("请输入食材名称")
return
}
getIngredientList()
KeyboardUtil.hideKeyboard(v)
}
/**
* 请求食材列表,使用回调方式发起
*/
private fun getIngredientList() {
viewModelV2.getInboundIngredientOptionsWithCallback(
keyword = keyword.takeUnless { it.isNullOrBlank() },
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> loadIngredientList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
binding.refreshLayout.finishRefresh()
}
else -> {}
}
}
)
}
/**
* 将返回数据填充到列表
*/
@SuppressLint("NotifyDataSetChanged")
private fun loadIngredientList(records: List<NutCleanInboundIngredientVO>?) {
binding.refreshLayout.finishRefresh()
if (records.isNullOrEmpty()) {
loadEmptyView()
return
}
binding.recyclerView.layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
list.clear()
list.addAll(records)
adapter.notifyDataSetChanged()
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
try {
list.clear()
adapter.notifyDataSetChanged()
if (emptyViewBinding == null) {
emptyViewBinding = LayoutEmptyViewBinding.inflate(
LayoutInflater.from(activity),
binding.recyclerView,
false
)
}
emptyViewBinding?.let {
it.tvContent.text = "暂无数据"
it.tvSubContent.text = "未查询到已入库净菜信息"
it.root.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
binding.recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
binding.recyclerView.post {
it.root.minimumHeight = binding.recyclerView.height
adapter.stateView = it.root
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@@ -15,7 +15,6 @@ import android.view.Window
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -25,13 +24,11 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.DialogSeasoningSelectBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.SeasoningSearchItem
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.toast
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
/**
* 调料选择弹窗,不含重量显示,继承 BottomSheetDialog 确保只初始化一次
* @param activity 宿主 Activity
@@ -64,7 +61,6 @@ class SeasoningSelectDialog(
private var goodsName: String? = null
private var pageNo = 1
private var collectJob: Job? = null
init {
setContentView(binding.root)
@@ -77,7 +73,6 @@ class SeasoningSelectDialog(
window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
setOnDismissListener {
collectJob?.cancel()
activity.hideStatusBar()
}
@@ -116,23 +111,6 @@ class SeasoningSelectDialog(
/** 弹窗显示时自动加载默认调料列表,并在 Window.Callback 层提前拦截触摸以解决滑动冲突 */
override fun show() {
super.show()
// 启动 StateFlow 收集,弹窗关闭时由 setOnDismissListener 取消
collectJob = activity.lifecycleScope.launch {
activity.netViewModel.foodSearchGoodsListState.collect { state ->
when (state) {
is UiState.Loading -> {}
is UiState.Success -> loadGoodsList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
finishRefresh()
if (pageNo == 1) {
loadEmptyView()
}
}
is UiState.Idle -> {}
}
}
}
pageNo = 1
getGoodsList()
// Window.Callback.dispatchTouchEvent 在整个 View 树的 onInterceptTouchEvent 之前执行,
@@ -184,14 +162,24 @@ class SeasoningSelectDialog(
}
/**
* 请求调料列表
* 请求调料列表(新配比秤接口,回调版本)
*/
private fun getGoodsList() {
activity.netViewModel.queryFoodSearchGoodsList(
goodsType = "1",
activity.netViewModel.querySeasoningSearch(
keyword = goodsName.takeUnless { it.isNullOrBlank() },
pageNum = pageNo,
pageSize = PAGE_SIZE,
goodsName = goodsName.takeUnless { it.isNullOrBlank() }
onResult = { state ->
when (state) {
is UiState.Success -> loadGoodsList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
finishRefresh()
if (pageNo == 1) loadEmptyView()
}
else -> {}
}
}
)
}
@@ -203,23 +191,22 @@ class SeasoningSelectDialog(
"", "白砂糖", "冰糖", "鸡精", "味精", "陈醋", "老抽", "生抽", "番茄酱", "胡椒粉", "孜然", "十三香",
"料酒", "白醋", "蚝油", "辣椒面", "辣椒酱", "豆瓣酱", "淀粉", "", "", "", "香菜", ""
)
val mockData = mutableListOf<GoodsItem>()
nameList.forEachIndexed { index, name ->
val mockData = nameList.mapIndexed { index, name ->
val padString = "${index + 1}".padStart(2, '0')
mockData.add(GoodsItem(goodsId = "1000$padString", goodsName = name))
SeasoningSearchItem(id = "1000$padString", materName = name)
}
pageNo = 1
loadGoodsList(mockData)
}
/**
* 将返回数据转换并填充到列表
* 将新接口返回的 SeasoningSearchItem 映射为 GoodsItem 并填充到列表
* 新接口的 id 即食材id,同时用作 goodsId 和 materId,无需再交换
*/
@SuppressLint("NotifyDataSetChanged")
private fun loadGoodsList(records: MutableList<GoodsItem>?) {
private fun loadGoodsList(records: List<SeasoningSearchItem>?) {
finishRefresh()
if (records.isNullOrEmpty()) {
//activity.toast("暂未搜索到调料信息")
if (pageNo == 1) {
loadEmptyView()
}
@@ -227,18 +214,26 @@ class SeasoningSelectDialog(
}
binding.recyclerView.layoutManager = GridLayoutManager(activity, 3, GridLayoutManager.VERTICAL, false)
if (pageNo == 1) list.clear()
list.addAll(records)
list.forEach {
it.oldGoodsId = it.goodsId
//materId才是提交数据需要的goodsId数据
it.goodsId = it.materId ?:""
}
val goodsItems = records.map { it.toGoodsItem() }
list.addAll(goodsItems)
adapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) pageNo++
}
/**
* SeasoningSearchItem → GoodsItem 映射
* id 既是食材id也是提交时的 goodsId,不需要 oldGoodsId 交换
*/
private fun SeasoningSearchItem.toGoodsItem() = GoodsItem(
goodsId = id,
materId = id,
goodsName = materName,
materCode = materCode,
popularName = alias
)
private fun finishRefresh() {
if (pageNo == 1) binding.refreshLayout.finishRefresh()
else binding.refreshLayout.finishLoadMore()
@@ -0,0 +1,146 @@
package com.shuwei.dish.match.model
/**
* 当日烹饪任务列表(按餐次分组)请求参数
* 对应接口: POST /neglect/ratio-scale/cook-orders/page
*/
data class CookOrderPageRequest(
/** 计划日期 yyyy-MM-dd,默认当天 */
val date: String? = null,
/** 餐次筛选: 1=早餐 / 2=午餐 / 3=晚餐 / 4=加餐,不传返回全部 */
val mealType: Int? = null,
/** 菜品名称模糊搜索 */
val keyword: String? = null,
/** 保留字段,当前不使用 */
val pageNum: Int? = null,
/** 保留字段,当前不使用 */
val pageSize: Int? = null
)
/**
* 餐次分组,包含该餐次下的烹制单列表
*/
data class CookOrderMealGroup(
/** 餐次: 1=早餐 / 2=午餐 / 3=晚餐 / 4=加餐 */
val mealType: Int = 0,
/** 餐次中文名称 */
val mealTypeName: String = "",
/** 该餐次下的烹制单列表 */
val cookOrders: List<CookOrderItem> = emptyList()
)
/**
* 单个烹制单,对应接口响应中 cookOrders 数组的条目
* 注意: Long 类型字段服务端序列化为 String
*/
data class CookOrderItem(
/** 烹制单idLong → String */
val cookOrderId: String = "",
/** 烹制单号 */
val cookNo: String = "",
/** 菜品名称 */
val dishName: String = "",
/** 菜品id(关联 nut_food */
val foodId: String = "",
/** 餐次: 1=早餐 / 2=午餐 / 3=晚餐 / 4=加餐 */
val mealType: Int = 0,
/** 餐次中文名称 */
val mealTypeName: String = "",
/** 烹制状态: 0=待烹制 / 1=烹制中 / 2=已完成 / 3=异常 */
val cookStatus: Int = 0,
/** 烹制状态中文 */
val cookStatusName: String = "",
/** 生重(kg),烹制完成后才有值 */
val rawWeight: Double? = null,
/** 熟重(kg),烹制完成后才有值 */
val cookedWeight: Double? = null,
/** 需求份数 */
val needPortions: Int = 0,
/** 烹制份数,烹制完成后才有值 */
val cookedPortions: Int? = null
)
/**
* 菜品食材构成(主材+辅材),对应接口响应中单个食材
* 对应接口: GET /neglect/ratio-scale/cook-orders/{cookOrderId}/composition
* 注意: 此接口仅返回主材(ingredientClass=1)和辅材(ingredientClass=2),不含调料
*/
data class CookOrderCompositionItem(
/** 食材id */
val materId: String = "",
/** 食材名称 */
val ingredientName: String = "",
/** 食材分类: 1=主材 / 2=辅材 */
val ingredientClass: Int = 0,
/** 食材分类中文名称 */
val ingredientClassName: String = "",
/** 标准用量(g) */
val useWeight: Double = 0.0,
/** 净菜类型id */
val vegTypeId: String = ""
)
// ==================== 调料模糊搜索 ====================
/**
* 调料模糊搜索请求参数
* 对应接口: POST /neglect/ratio-scale/seasoning-search
* 数据源: nut_mater_base 一级分类为 18(糖蜜饯)、19(油脂)、20(调味品)
*/
data class SeasoningSearchRequest(
/** 模糊匹配食材名称和别名,为空时返回全部 */
val keyword: String? = null,
/** 页码 */
val pageNum: Int = 1,
/** 每页条数 */
val pageSize: Int = 20
)
/**
* 调料模糊搜索结果条目
* 注意: Long 类型字段服务端序列化为 String
*/
data class SeasoningSearchItem(
/** 食材idLong → String */
val id: String = "",
/** 食材名称 */
val materName: String = "",
/** 食材编码 */
val materCode: String = "",
/** 别名 */
val alias: String = "",
/** 归属食堂idLong → String */
val canteenId: String = ""
)
// ==================== 烹饪完成 ====================
/**
* 烹饪完成 — 单个食材用量条目
* 对应接口: POST /neglect/ratio-scale/cook-orders/complete
*/
data class CookCompleteItem(
/** 食材id */
val materId: String = "",
/** 实际用量(g) */
val actualQty: Double = 0.0,
/** 调料首次使用时间 yyyy-MM-dd HH:mm:ss,仅调料需要,主辅材留空 */
val putTime: String? = null
)
/**
* 烹饪完成请求,一次性提交主辅材用量+熟重+调料
* 对应接口: POST /neglect/ratio-scale/cook-orders/complete
*/
data class CookCompleteRequest(
/** 烹制单id */
val cookOrderId: String = "",
/** 烹饪开始时间 yyyy-MM-dd HH:mm:ss,进入提交页面时刻 */
val cookStart: String? = null,
/** 主辅材实际用量列表,至少1条 */
val items: List<CookCompleteItem> = emptyList(),
/** 熟重(kg) */
val cookedWeight: Double = 0.0,
/** 调料用量列表,选填 */
val seasonings: List<CookCompleteItem>? = null
)
@@ -1,25 +0,0 @@
package com.shuwei.dish.match.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 字典类型 如仓库列表 物品类型 存储方式等
*/
@Parcelize
data class DictType(
val id: String? = null,
val value: String? = null,
val type: String = "",
var position: Int = 0
) : Parcelable {
/**
* 按字段名取值,供 extraApiKeys 映射直接使用
*/
operator fun get(fieldName: String): String = when (fieldName) {
"id" -> id ?: ""
"value" -> value ?: ""
else -> type
}
}
@@ -15,5 +15,12 @@ data class FoodRecord(
var dinnerType: String = "0",
//true-原始数据,false-非原始数据
var isOriginalData: Boolean = true,
var sort: Int = 0
var sort: Int = 0,
// ===== 已提交采样列表(sample-list)扩展字段,制作模式不涉及 =====
/** 实验编号(innovNo */
var innovNo: String? = null,
/** 实验状态: 1=实验中 / 2=已评测 / 9=已转化 */
var status: Int? = null,
/** 实验状态中文 */
var statusName: String? = null
) : Serializable
@@ -1,52 +0,0 @@
package com.shuwei.dish.match.model
/** 表单字段类型枚举 */
enum class FieldType {
/** 普通文本输入 */
TEXT,
/** 整数输入 */
INTEGER,
/** 小数输入 */
DECIMAL,
/** 下拉选择 */
DROPDOWN,
/** 日期选择弹窗 */
DATE_PICKER,
/** 固定值,不可编辑 */
FIXED,
/** 子列表容器,用于动态增减行(如"包含食材" */
CHILDREN
}
/**
* 动态表单字段数据模型
* @param label 字段标签名称
* @param type 字段类型,决定渲染哪种输入控件
* @param apiKey 非下拉字段对应的接口参数名,空字符串表示不提交
* @param extraApiKeys 下拉字段的多接口参数映射,key=接口参数名,value=DictType 字段名(id/value/type
* @param required 是否必填,提交时校验
* @param hidden 是否隐藏,hidden=true 时不渲染该字段
* @param hint 校验失败提示文案,同时作为输入框 hint
* @param options 下拉框选项列表(DROPDOWN 类型使用)
* @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式
* @param extraValues 下拉选中后按 extraApiKeys 映射存储的提交值,key=接口参数名
* @param children 子字段模板(CHILDREN 类型使用),每行按此模板创建一组字段
* @param childAddLabel 添加按钮文案(CHILDREN 类型使用),如 "+ 添加食材"
* @param childrenRows 子列表实际数据(CHILDREN 类型使用),每行是一个 List<FormField>
*/
data class FormField(
val label: String,
val type: FieldType,
val apiKey: String = "",
val extraApiKeys: Map<String, String> = emptyMap(),
val required: Boolean = false,
val hidden: Boolean = false,
val hint: String = "",
var options: MutableList<DictType> = mutableListOf(),
var value: String = "",
val extraValues: MutableMap<String, String> = mutableMapOf(),
val children: List<FormField>? = null,
val childAddLabel: String? = null,
val childrenRows: MutableList<MutableList<FormField>> = mutableListOf(),
var onDropdownSelected: ((DictType) -> Unit)? = null
)
@@ -11,11 +11,11 @@ import java.io.Serializable
data class GoodsItem(
// 接口返回字段
var goodsId: String = "",
var goodsName: String? = null,
val goodsName: String? = null,
val popularName: String? = null,
val zjmCode: String? = null,
var materId: String? = null,
var materCode: String? = null,
val materCode: String? = null,
// 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用)
var materialType: Int = 0,
@@ -32,6 +32,8 @@ data class GoodsItem(
var isClicked: Boolean = false,
// true-原始数据,false-非原始数据(识图新增)
var isOriginalData: Boolean = true,
// true-用户手动指定了主辅材,updateMaterialTypes 自动分配时跳过
var isMaterialTypeManual: Boolean = false,
// 识图匹配分数,-1 表示不显示
var foodScore: Int = -1,
// 净材种类(当前选中值)
@@ -1,264 +0,0 @@
package com.shuwei.dish.match.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 食堂VO - 食堂下拉列表响应
*/
@Parcelize
data class NutCanteenVO(
val id: Long?,
val canteenCode: String?,
val canteenName: String?,
val orgCode: String?,
val orgName: String?,
val leader: String?,
val leaderPhone: String?,
val useTerminal: Short?,
val address: String?,
val longitude: String?,
val latitude: String?,
val status: Short?
) : Parcelable
/**
* 字典项VO - 字典项下拉响应
*/
@Parcelize
data class NutDictItemVO(
val id: Long?,
val dictType: String?,
val dictCode: String?,
val dictLabel: String?,
val sortOrder: Int?,
val isBuiltin: Short?,
val status: Short?,
val remark: String?
) : Parcelable
/**
* 食材选项VO - 食材下拉响应
*/
@Parcelize
data class NutMaterOptionVO(
val id: Long?,
val materClassName: String?
) : Parcelable
/**
* 包装设备VO - 包装设备下拉响应
*/
@Parcelize
data class NutSupPkgDeviceVO(
val id: Long?,
val deviceNo: String?,
val deviceName: String?
) : Parcelable
/**
* 餐品选项VO - 餐品下拉搜索响应
*/
@Parcelize
data class NutFoodOptionVO(
val foodId: Long?,
val foodCode: String?,
val foodName: String?,
val compositions: List<NutFoodComposition>?
) : Parcelable
/**
* 餐品食材构成
*/
@Parcelize
data class NutFoodComposition(
var materId: Long?,
var ingredientName: String?,
var traceCode: String?,
var amount: Double?,
var isMain: Int?,
//1-主材、2-辅材、3-调料
var mainText: String?=null
) : Parcelable
/**
* 溯源码查食材信息VO
*/
@Parcelize
data class NutSupHygieneIngredientOptionVO(
val traceCode: String?,
val materId: Long?,
val materName: String?,
val vegTypes: String?,
val materClassId: Long?,
val materClassName: String?,
val testNo: String?
) : Parcelable
/**
* 组配任务视频监控VO
*/
@Parcelize
data class NutProdComboVideoVO(
val cameraCode: String?,
val streamUrl: String?,
val snapshotUrl: String?,
val onlineStatus: Int?,
val recordType: Int?,
val startTime: String?,
val endTime: String?
) : Parcelable
/**
* 组配任务DTO - 上报组配任务请求体
*/
data class NutProdComboTaskDTO(
val planDate: String,
val canteenId: Long,
val mealType: Short?,
val foodId: Long?,
val targetDish: String,
val spec: String,
val portions: Int,
val taskDeadline: String?,
val taskSource: Short?,
val ownerName: String?,
val items: List<NutProdComboTaskItem>?
)
/**
* 组配任务食材明细
*/
data class NutProdComboTaskItem(
val ingredientCode: String?,
val ingredientName: String,
val perPortionQty: String,
val unit: String? = "g",
val sortOrder: Int?
)
/**
* 单品净菜包装DTO - 上报单品净菜包装记录请求体
*/
data class NutSupCleanPackageDTO(
val traceCode: String,
val cleanInboundId: Long?,
val cleanName: String,
val packageMethod: String,
val spec: String,
val quantity: Int,
val packageDate: String,
val expiryDate: String,
val storageTemp: String?,
val relatedOrderNo: String?,
val operator: String?,
val deviceId: Long?,
val cameraCode: String?,
val videoUrl: String?
)
/**
* 餐品净菜包装DTO - 上报餐品净菜包装记录请求体
*/
data class NutSupMealPackageDTO(
val foodId: Long,
val mealName: String,
val spec: String,
val packageMethod: String,
val quantity: Int,
val packageDate: String,
val expiryDate: String,
val storageTemp: String?,
val relatedOrderNo: String?,
val operator: String?,
val deviceId: Long?,
val cameraCode: String?,
val videoUrl: String?,
val ingredients: List<NutSupMealPackageIngredient>
)
/**
* 餐品净菜包装食材明细
*/
data class NutSupMealPackageIngredient(
val materId: Long,
val ingredientName: String,
val amount: String?,
val traceCode: String
)
/**
* 组配任务VO - 未完成组配任务列表响应
*/
@Parcelize
data class NutComboTaskVO(
val id: Long?,
val taskNo: String?,
val planDate: String?,
val canteenId: Long?,
val mealType: Int?,
val foodId: Long?,
val targetDish: String?,
val spec: String?,
val portions: Int?,
var comboStatus: Int?,
val ownerName: String?,
val createTime: String?
) : Parcelable
/**
* 组配任务详情VO - 组配任务详情响应
*/
@Parcelize
data class NutComboTaskDetailVO(
var id: Long? = null,
var taskNo: String? = null,
var planDate: String? = null,
var canteenId: Long? = null,
var mealType: Int? = null,
var foodId: Long? = null,
var targetDish: String? = null,
var spec: String? = null,
var portions: Int? = null,
var comboStatus: Int? = null,
var ownerName: String? = null,
var deviceCode: String? = null,
var items: List<NutComboTaskItemDetail>? = null,
) : Parcelable
/**
* 组配任务食材明细
*/
@Parcelize
data class NutComboTaskItemDetail(
val id: Long?,
val ingredientCode: String?,
val ingredientName: String?,
val ingredientClass: Int?,
var traceCode: String?,
val perPortionQty: String?,
var actualQty: String?,
val totalQty: String?,
val unit: String?,
var isClicked: Boolean? = false,
var isSetFinished: Boolean? = false,
) : Parcelable
/**
* 已入库净菜食材VO - 按食材名称查询已入库有库存的净菜记录
*/
@Parcelize
data class NutCleanInboundIngredientVO(
val id: Long?,
val materId: Long?,
val ingredientName: String?,
val traceCode: String?,
val currentStock: String?
) : Parcelable
/**
* ID通用DTO - 用于开始/完成组配等需要id参数的接口
*/
data class IdDTO(
val id: Long
)
@@ -0,0 +1,251 @@
package com.shuwei.dish.match.model
/**
* 采样与制作模式相关模型,对应配比秤接口(Controller: NutRatioScaleController
* 路径前缀: /neglect/ratio-scale
* 序列化规则: Long 字段服务端返回字符串;请求体字段按后端约定(id 类字段用 String 与现有代码一致)
*/
/** 采样实验状态(味养创新 nut_innovation.status */
object SampleStatus {
/** 实验中 */
const val EXPERIMENTING = 1
/** 已评测 */
const val EVALUATED = 2
/** 已转化 */
const val CONVERTED = 9
}
// ==================== 一、食材模糊搜索 ====================
/**
* 食材模糊搜索请求参数
* 对应接口: POST /neglect/ratio-scale/mater-search
* 数据源: nut_mater_base,限定设备对应食堂
*/
data class MaterSearchRequest(
/** 模糊匹配食材名称和别名,为空时返回全部 */
val keyword: String? = null,
/** 页码 */
val pageNum: Int = 1,
/** 每页条数 */
val pageSize: Int = 20
)
/**
* 食材模糊搜索结果条目
* 注意: id / canteenId 为 Long 类型,服务端序列化为 String
*/
data class MaterSearchItem(
/** 食材idLong → String,提交构成 items.materId 使用) */
val id: String = "",
/** 食材名称 */
val materName: String = "",
/** 食材编码(一级+二级+自编码,全局唯一) */
val materCode: String = "",
/** 别名,可能为 null */
val alias: String? = null,
/** 食材一级分类 */
val materFirstClass: String? = null,
/** 食材二级分类 */
val materSecondClass: String? = null,
/** 食材图片url */
val materUrl: String? = null,
/** 归属食堂idLong → String */
val canteenId: String = ""
)
// ==================== 二、搜菜品 ====================
/**
* 搜菜品请求参数(制作模式选已有菜品)
* 对应接口: POST /neglect/ratio-scale/food-search
* 数据源: nut_foodstatus=1 上架)
*/
data class FoodSearchRequest(
/** 菜品名称或编号模糊搜索 */
val keyword: String? = null,
/** 页码 */
val pageNum: Int = 1,
/** 每页条数 */
val pageSize: Int = 20
)
/**
* 搜菜品结果条目
* 注意: foodId 为 Long 类型,服务端序列化为 String
*/
data class FoodSearchItem(
/** 菜品idLong → String */
val foodId: String = "",
/** 菜品编号 */
val foodCode: String = "",
/** 菜品名称 */
val foodName: String = ""
)
// ==================== 三、查菜品构成 ====================
/**
* 查菜品构成响应 — 单个食材
* 对应接口: GET /neglect/ratio-scale/food/{foodId}/composition
*/
data class FoodCompositionIngredient(
/** 食材idLong → String */
val materId: String = "",
/** 食材名称 */
val materName: String = "",
/** 用量(g) */
val useWeight: Double = 0.0,
/** 1=主材 / 2=辅材 / 3=调料 */
val isMain: Int = 0,
/** 分类中文名 */
val isMainName: String = ""
)
/**
* 查菜品构成响应(主辅料+调料 + 熟重)
* 对应接口: GET /neglect/ratio-scale/food/{foodId}/composition
*/
data class FoodCompositionResponse(
/** 菜品idLong → String */
val foodId: String = "",
/** 菜品名称 */
val foodName: String = "",
/** 熟重(g),从 nut_food_cook.output_weight 反查 */
val foodWeight: Double = 0.0,
/** 食材构成列表 */
val ingredients: List<FoodCompositionIngredient> = emptyList()
)
// ==================== 四、提交采样 ====================
/**
* 提交采样 — 单个食材构成条目
* 对应接口: POST /neglect/ratio-scale/constitute/save
*/
data class ConstituteSaveItem(
/** 食材idnut_mater_base.id */
val materId: String = "",
/** 食材称量用量(g),必须 > 0 */
val useWeight: Double = 0.0,
/** 食材分类: 1=主材 / 2=辅材 / 3=调料 */
val isMain: Int = 0
)
/**
* 提交采样请求(落库味养创新实验,status=1 实验中)
* 对应接口: POST /neglect/ratio-scale/constitute/save
* 制作模式已废弃本接口,统一为提交采样,不再传 cookMode/foodId
*/
data class ConstituteSaveRequest(
/** 菜品名称 */
val foodName: String? = null,
/** 熟重(g),必须 > 0 */
val foodWeight: Double = 0.0,
/** 食材构成列表,至少 1 条 */
val items: List<ConstituteSaveItem> = emptyList()
)
// ==================== 五、已提交采样列表 ====================
/**
* 已提交采样列表查询请求参数
* 对应接口: POST /neglect/ratio-scale/sample-list
* 数据源: nut_innovationsource=2 配比秤采样),按提交时间倒序,不分日期
*/
data class SampleHistoryRequest(
/** 页码 */
val pageNum: Int = 1,
/** 每页条数 */
val pageSize: Int = 10
)
/**
* 已提交采样列表结果条目
* 注意: innovId 为 Long 类型,服务端序列化为 String
*/
data class SampleHistoryItem(
/** 味养创新实验idLong → String */
val innovId: String = "",
/** 实验编号 */
val innovNo: String = "",
/** 餐品名称 */
val foodName: String = "",
/** 熟重(g) */
val foodWeight: Double = 0.0,
/** 状态: 1=实验中 / 2=已评测 / 9=已转化 */
val status: Int = 0,
/** 状态中文 */
val statusName: String = "",
/** 提交时间 */
val createTime: String = ""
)
// ==================== 六、制作历史 ====================
/**
* 制作历史查询请求参数
* 对应接口: POST /neglect/ratio-scale/make/history
* 数据源: nut_food_matching_use,按设备过滤
*/
data class MakeHistoryRequest(
/** 页码 */
val pageNum: Int = 1,
/** 每页条数 */
val pageSize: Int = 10,
/** 制作日期 yyyy-MM-dd,不传查全部 */
val day: String? = null
)
/**
* 制作历史结果条目
* 注意: id / foodId 为 Long 类型,服务端序列化为 String
*/
data class MakeHistoryItem(
/** 制作记录idLong → String */
val id: String = "",
/** 菜品idLong → String */
val foodId: String = "",
/** 菜品名称 */
val foodName: String = "",
/** 熟重(g) */
val foodWeight: Double = 0.0,
/** 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐 */
val mealType: Int = 0,
/** 餐次中文 */
val mealTypeName: String = "",
/** 制作日期 */
val day: String = "",
/** 制作时间 */
val createTime: String = ""
)
// ==================== 七、制作历史图表 ====================
/**
* 制作历史图表 — 横轴项
* 对应接口: GET /neglect/ratio-scale/make/history/{foodId}
*/
data class MakeHistoryChartAxis(
/** 食材id(时间/熟重项为空字符串) */
val goodId: String = "",
/** 项名称(时间/熟重/食材名称) */
val goodName: String = ""
)
/**
* 制作历史图表 — 纵轴值
* value 为 String(时间项) 或 Double(数值项)
*/
data class MakeHistoryChartValue(
val value: Any? = null
)
/**
* 制作历史图表 — 单条记录(xAxes 与 yAxes 一一对应)
*/
data class MakeHistoryChartRow(
val xAxes: List<MakeHistoryChartAxis> = emptyList(),
val yAxes: List<MakeHistoryChartValue> = emptyList()
)
@@ -11,25 +11,30 @@ package com.shuwei.dish.match.model
*/
data class SettingItem(
val type: Type,
val title: String = type.label,
val title: String,
val subtitle: String? = null,
val isHidden: Boolean = false,
val onClick: () -> Unit
) {
/** 设置菜单项类型枚举 */
enum class Type(val label: String) {
COOK_MODE("菜品模式"),
SEASONING_CONFIG("调料区设置"),
FOOD_COLLECT("食材采集"),
WEIGHT_CONFIG("减重配置"),
CLEAN_PACKAGE("净菜包"),
MEAL_PACKAGE("餐品净菜包"),
GROUP_TASK("组配任务"),
// MEAL_PACKAGE_TEST("餐品净菜包-测试"),
DB_INSPECT("数据库查看"),
SCALE_OBSERVE("秤数据监控"),
ENV_SWITCH("切换环境"),
CLEAR_DATA("数据清除"),
TEST_SEASONING("测试调料拿取"),
enum class Type {
/** 菜品模式切换 */
COOK_MODE,
/** 调料区设置 */
SEASONING_CONFIG,
/** 食材采集 */
FOOD_COLLECT,
/** 减重配置 */
WEIGHT_CONFIG,
/** 数据库查看(隐藏项) */
DB_INSPECT,
/** 秤数据监控(隐藏项) */
SCALE_OBSERVE,
/** 切换环境(隐藏项) */
ENV_SWITCH,
/** 数据清除(隐藏项) */
CLEAR_DATA,
/** 测试调料拿取(隐藏项) */
TEST_SEASONING,
}
}
@@ -2,8 +2,25 @@ package com.shuwei.dish.match.net
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.model.CookOrderCompositionItem
import com.shuwei.dish.match.model.CookOrderMealGroup
import com.shuwei.dish.match.model.CookOrderPageRequest
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.ConstituteSaveRequest
import com.shuwei.dish.match.model.CookCompleteRequest
import com.shuwei.dish.match.model.FoodCompositionResponse
import com.shuwei.dish.match.model.FoodSearchItem
import com.shuwei.dish.match.model.FoodSearchRequest
import com.shuwei.dish.match.model.MakeHistoryChartRow
import com.shuwei.dish.match.model.MakeHistoryItem
import com.shuwei.dish.match.model.MakeHistoryRequest
import com.shuwei.dish.match.model.MaterSearchItem
import com.shuwei.dish.match.model.MaterSearchRequest
import com.shuwei.dish.match.model.SampleHistoryItem
import com.shuwei.dish.match.model.SampleHistoryRequest
import com.shuwei.dish.match.model.SeasoningSearchItem
import com.shuwei.dish.match.model.SeasoningSearchRequest
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
@@ -12,6 +29,7 @@ import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Url
@@ -22,7 +40,7 @@ interface ApiService {
*/
@GET
suspend fun getFoodDetail(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/getConstituteByFoodId",
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/getConstituteByFoodId",
@Query("foodId") foodId: String
): ApiResponse<CookFoodDTO?>
@@ -32,16 +50,16 @@ interface ApiService {
*/
@POST
suspend fun submitCookFood(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/saveConstitute",
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/saveConstitute",
@Body param: CookFoodDTO
): ApiResponse<Any?>
/**
* 搜索菜品
* 搜索菜品(旧接口,采样模式仍在使用)
*/
@POST
suspend fun searchFoodList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryPageList",
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/queryPageList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<FoodRecord>?>
@@ -50,7 +68,7 @@ interface ApiService {
*/
@POST
suspend fun getSamplingList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryHistoryGoodsInfoList",
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/queryHistoryGoodsInfoList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<FoodRecord>?>
@@ -59,7 +77,7 @@ interface ApiService {
*/
@POST
suspend fun queryGoodsList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryGoodsInfoList",
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/queryGoodsInfoList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<GoodsItem>?>
@@ -69,8 +87,110 @@ interface ApiService {
@Multipart
@POST
suspend fun uploadFoodVectorData(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/addWeighingCollectionData",
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/matching/app/addWeighingCollectionData",
@PartMap params: MutableMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?>
/**
* 查询当日烹饪任务列表(按餐次分组),新配比秤接口
*/
@POST
suspend fun getCookOrderPage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/cook-orders/list",
@Body request: CookOrderPageRequest
): ApiResponse<List<CookOrderMealGroup>?>
/**
* 查询菜品食材构成(主材+辅材,不含调料),新配比秤接口
*/
@GET("nutrition/neglect/ratio-scale/cook-orders/{cookOrderId}/composition")
suspend fun getCookOrderComposition(
@Path("cookOrderId") cookOrderId: String
): ApiResponse<List<CookOrderCompositionItem>?>
/**
* 调料模糊搜索,新配比秤接口
* 数据源: nut_mater_base 一级分类为 18(糖蜜饯)、19(油脂)、20(调味品)
*/
@POST
suspend fun getSeasoningSearch(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/seasoning-search",
@Body request: SeasoningSearchRequest
): ApiResponse<List<SeasoningSearchItem>?>
/**
* 烹饪完成:提交主辅材用量+熟重+调料,一次性完成烹制
* 新配比秤接口,替代旧 saveConstitute
*/
@POST
suspend fun cookOrderComplete(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/cook-orders/complete",
@Body request: CookCompleteRequest
): ApiResponse<Any?>
/**
* 食材模糊搜索(采样/制作通用,称量前检索食材)
* 数据源: nut_mater_base,限定设备对应食堂,模糊匹配名称和别名
*/
@POST
suspend fun materSearch(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/mater-search",
@Body request: MaterSearchRequest
): ApiResponse<MutableList<MaterSearchItem>?>
/**
* 搜菜品(制作模式选已有上架菜品)
*/
@POST
suspend fun foodSearch(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/food-search",
@Body request: FoodSearchRequest
): ApiResponse<MutableList<FoodSearchItem>?>
/**
* 查菜品构成(制作模式查看已有菜品主辅料+调料 + 熟重)
*/
@GET("nutrition/neglect/ratio-scale/food/{foodId}/composition")
suspend fun getFoodComposition(
@Path("foodId") foodId: String
): ApiResponse<FoodCompositionResponse?>
/**
* 提交采样(落库味养创新实验,制作模式已废弃本接口)
*/
@POST
suspend fun constituteSave(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/constitute/save",
@Body request: ConstituteSaveRequest
): ApiResponse<Any?>
/**
* 已提交采样列表(本食堂 source=2,按提交时间倒序,不分日期)
*/
@POST
suspend fun sampleHistory(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/sample-list",
@Body request: SampleHistoryRequest
): ApiResponse<MutableList<SampleHistoryItem>?>
/**
* 制作历史(本设备,按日期可选过滤)
*/
@POST
suspend fun makeHistory(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ratio-scale/make/history",
@Body request: MakeHistoryRequest
): ApiResponse<MutableList<MakeHistoryItem>?>
/**
* 制作历史图表(按菜品id)
*/
@GET("nutrition/neglect/ratio-scale/make/history/{foodId}")
suspend fun makeHistoryChart(
@Path("foodId") foodId: String
): ApiResponse<MutableList<MakeHistoryChartRow>?>
}
@@ -1,173 +0,0 @@
package com.shuwei.dish.match.net
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.model.NutSupPkgDeviceVO
import com.shuwei.dish.match.model.NutCanteenVO
import com.shuwei.dish.match.model.NutDictItemVO
import com.shuwei.dish.match.model.NutMaterOptionVO
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO
import com.shuwei.dish.match.model.NutProdComboVideoVO
import com.shuwei.dish.match.model.NutProdComboTaskDTO
import com.shuwei.dish.match.model.IdDTO
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskVO
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.Url
interface ApiServiceV2 {
// ========== 一、下拉选项接口 ==========
/**
* 食堂下拉列表,返回启用状态的食堂列表,支持关键字模糊搜索
*/
@GET
suspend fun getCanteenOptions(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/canteen-options",
@Query("keyword") keyword: String? = null
): ApiResponse<List<NutCanteenVO>>
/**
* 字典项下拉,按 dictType 查询字典项
*/
@GET
suspend fun getDictItems(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/dict-items",
@Query("dictType") dictType: String
): ApiResponse<List<NutDictItemVO>>
/**
* 食材下拉列表,支持关键字模糊搜索
*/
@GET
suspend fun getMaterOptions(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/mater-options",
@Query("keyword") keyword: String? = null
): ApiResponse<List<NutMaterOptionVO>>
/**
* 包装设备下拉(仅在线),返回在线设备的 id、编号、名称
*/
@GET
suspend fun getDeviceOptions(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/device-options"
): ApiResponse<List<NutSupPkgDeviceVO>>
/**
* 餐品下拉搜索(含食材构成),按名称模糊搜索,分页返回
*/
@GET
suspend fun getFoodOptions(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/food-options",
@Query("keyword") keyword: String? = null,
@Query("pageNum") pageNum: Int = 1,
@Query("pageSize") pageSize: Int = 10
): ApiResponse<Page<NutFoodOptionVO>>
/**
* 按溯源码查食材信息
*/
@GET
suspend fun getIngredientByTrace(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/ingredient-by-trace",
@Query("traceCode") traceCode: String
): ApiResponse<List<NutSupHygieneIngredientOptionVO>>
/**
* 按食材名称查询已入库有库存的净菜记录
*/
@GET
suspend fun getInboundIngredientOptions(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/inbound-ingredient-options",
@Query("keyword") keyword: String? = null
): ApiResponse<List<NutCleanInboundIngredientVO>>
// ========== 二、净菜组配接口 ==========
/**
* 上报组配任务(含食材明细)
*/
@POST
suspend fun addComboTask(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/add",
@Body param: NutComboTaskDetailVO
): ApiResponse<Unit>
/**
* 开始组配,组配状态 0 -> 1
*/
@POST
suspend fun startComboTask(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/start",
@Body param: IdDTO
): ApiResponse<Unit>
/**
* 完成组配,组配状态 1 -> 2
*/
@POST
suspend fun finishComboTask(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/finish",
@Body param: IdDTO
): ApiResponse<Unit>
/**
* 获取组配任务视频监控地址
*/
@GET
suspend fun getComboTaskVideo(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/video",
@Query("taskNo") taskNo: String
): ApiResponse<NutProdComboVideoVO>
// ========== 三、单品净菜包装接口 ==========
/**
* 上报单品净菜包装记录
*/
@POST
suspend fun addCleanPackage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/clean-package/add",
@Body param: Map<String, @JvmSuppressWildcards Any?>
): ApiResponse<Unit>
// ========== 四、餐品净菜包装接口 ==========
/**
* 上报餐品净菜包装记录
*/
@POST
suspend fun addMealPackage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/meal-package/add",
@Body param: Map<String, @JvmSuppressWildcards Any?>
): ApiResponse<Unit>
// ========== 五、组配任务接口 ==========
/**
* 查询未完成组配任务(分页)
*/
@GET
suspend fun getIncompleteComboTasks(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/incomplete",
@Query("deviceCode") deviceCode: String? = null,
@Query("keyword") keyword: String? = null,
@Query("pageNum") pageNum: Int = 1,
@Query("pageSize") pageSize: Int = 10
): ApiResponse<Page<NutComboTaskVO>>
/**
* 获取组配任务详情
*/
@GET
suspend fun getComboTaskDetail(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/detail",
@Query("id") id: Long
): ApiResponse<NutComboTaskDetailVO>
}
@@ -4,9 +4,26 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.model.CookOrderCompositionItem
import com.shuwei.dish.match.model.CookOrderMealGroup
import com.shuwei.dish.match.model.CookOrderPageRequest
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.GoodsNameQueryDTO
import com.shuwei.dish.match.model.ConstituteSaveRequest
import com.shuwei.dish.match.model.CookCompleteRequest
import com.shuwei.dish.match.model.FoodCompositionResponse
import com.shuwei.dish.match.model.FoodSearchItem
import com.shuwei.dish.match.model.FoodSearchRequest
import com.shuwei.dish.match.model.MakeHistoryChartRow
import com.shuwei.dish.match.model.MakeHistoryItem
import com.shuwei.dish.match.model.MakeHistoryRequest
import com.shuwei.dish.match.model.MaterSearchItem
import com.shuwei.dish.match.model.MaterSearchRequest
import com.shuwei.dish.match.model.SampleHistoryItem
import com.shuwei.dish.match.model.SampleHistoryRequest
import com.shuwei.dish.match.model.SeasoningSearchItem
import com.shuwei.dish.match.model.SeasoningSearchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
@@ -64,6 +81,23 @@ class NetViewModel(
_foodDetailState.value = UiState.Idle
}
/**
* 查询菜品食材构成的 UI 状态流(新配比秤接口),UI 层通过 collect 监听
*/
private val _cookOrderCompositionState = MutableStateFlow<UiState<List<CookOrderCompositionItem>?>>(UiState.Idle)
val cookOrderCompositionState: StateFlow<UiState<List<CookOrderCompositionItem>?>> = _cookOrderCompositionState.asStateFlow()
/**
* 查询菜品食材构成(新配比秤接口,仅返回主材+辅材)
* @param cookOrderId 烹制单 ID
*/
fun getCookOrderComposition(cookOrderId: String) {
viewModelScope.launch {
_cookOrderCompositionState.value = UiState.Loading
_cookOrderCompositionState.value = repository.getCookOrderComposition(cookOrderId)
}
}
/**
* 搜索菜品列表的 UI 状态流。
* 使用 replay=1 保证 Fragment 重建后能收到最近一次结果;
@@ -104,6 +138,42 @@ class NetViewModel(
}
}
/**
* 当日烹饪任务列表的 UI 状态流(新配比秤接口),UI 层通过 collect 监听
* 使用 SharedFlow(replay=1) 保证 Fragment 重建后收到最近一次结果
*/
private val _cookOrderPageState = MutableSharedFlow<UiState<List<CookOrderMealGroup>?>>(
replay = 1,
extraBufferCapacity = 1
)
val cookOrderPageState: SharedFlow<UiState<List<CookOrderMealGroup>?>> = _cookOrderPageState.asSharedFlow()
/**
* 查询当日烹饪任务列表(新配比秤接口,StateFlow 版本)
* 适合 FoodSearchActivity 等通过 collect 监听结果的场景
*/
fun getCookOrderPage(request: CookOrderPageRequest) {
viewModelScope.launch {
_cookOrderPageState.emit(UiState.Loading)
_cookOrderPageState.emit(repository.getCookOrderPage(request))
}
}
/**
* 查询当日烹饪任务列表(新配比秤接口,回调版本)
* 适合 FoodListFragment 下拉刷新、Tab 切换等需要每次都能触发的场景
*/
fun getCookOrderPageWithCallback(
request: CookOrderPageRequest,
onLoading: () -> Unit = {},
onResult: (UiState<List<CookOrderMealGroup>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getCookOrderPage(request))
}
}
/**
*/
private val _samplingListState = MutableStateFlow<UiState<MutableList<FoodRecord>?>>(UiState.Idle)
@@ -215,6 +285,166 @@ class NetViewModel(
}
}
/**
* 调料模糊搜索(新配比秤接口,回调版本)
* 替代旧 queryGoodsList(goodsType=1) 调用
* @param keyword 模糊匹配食材名称和别名,为 null 时不传该字段
* @param pageNum 页码,默认第 1 页
* @param pageSize 每页条数,默认 50
* @param onLoading 请求发起前的回调
* @param onResult 结果回调,直接接收 UiState<List<SeasoningSearchItem>?>
*/
fun querySeasoningSearch(
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 50,
onLoading: () -> Unit = {},
onResult: (UiState<List<SeasoningSearchItem>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getSeasoningSearch(
SeasoningSearchRequest(keyword = keyword, pageNum = pageNum, pageSize = pageSize)
))
}
}
/**
* 烹饪完成(新配比秤接口,回调版本),替代旧 submitCookFood
* 仅在制作模式下 cookOrderId 可用的场景调用
*/
fun cookOrderComplete(
request: CookCompleteRequest,
onLoading: () -> Unit = {},
onResult: (UiState<Any?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.cookOrderComplete(request))
}
}
/**
* 食材模糊搜索(回调版本)
* @param keyword 模糊匹配食材名称和别名,为 null 时不传该字段
* @param pageNum 页码,默认第 1 页
* @param pageSize 每页条数,默认 20
*/
fun materSearch(
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 20,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<MaterSearchItem>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.materSearch(
MaterSearchRequest(keyword = keyword, pageNum = pageNum, pageSize = pageSize)
))
}
}
/**
* 搜菜品(回调版本,制作模式选已有上架菜品)
*/
fun foodSearch(
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 20,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<FoodSearchItem>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.foodSearch(
FoodSearchRequest(keyword = keyword, pageNum = pageNum, pageSize = pageSize)
))
}
}
/**
* 查菜品构成(回调版本,制作模式)
*/
fun getFoodComposition(
foodId: String,
onLoading: () -> Unit = {},
onResult: (UiState<FoodCompositionResponse?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getFoodComposition(foodId))
}
}
/**
* 提交采样(回调版本,落库味养创新实验,制作模式已废弃本接口)
*/
fun constituteSave(
request: ConstituteSaveRequest,
onLoading: () -> Unit = {},
onResult: (UiState<Any?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.constituteSave(request))
}
}
/**
* 已提交采样列表(回调版本,本食堂 source=2,不分日期)
* @param pageNum 页码,默认第 1 页
* @param pageSize 每页条数,默认 10
*/
fun sampleHistory(
pageNum: Int = 1,
pageSize: Int = 10,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<SampleHistoryItem>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.sampleHistory(
SampleHistoryRequest(
pageNum = pageNum,
pageSize = pageSize
)
))
}
}
/**
* 制作历史(回调版本,本设备按日期可选过滤)
*/
fun makeHistory(
pageNum: Int = 1,
pageSize: Int = 10,
day: String? = null,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<MakeHistoryItem>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.makeHistory(
MakeHistoryRequest(pageNum = pageNum, pageSize = pageSize, day = day)
))
}
}
/**
* 制作历史图表(回调版本,按菜品id)
*/
fun makeHistoryChart(
foodId: String,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<MakeHistoryChartRow>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.makeHistoryChart(foodId))
}
}
/**
* 上传采集菜品信息 UI 状态流,UI 层通过 collect 监听
*/
@@ -1,327 +0,0 @@
package com.shuwei.dish.match.net
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.dish.match.model.NutCanteenVO
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.model.NutDictItemVO
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.model.NutMaterOptionVO
import com.shuwei.dish.match.model.NutProdComboTaskDTO
import com.shuwei.dish.match.model.NutProdComboVideoVO
import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO
import com.shuwei.dish.match.model.NutSupPkgDeviceVO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
* 网络请求 ViewModel V2,所有网络操作通过 RemoteRepositoryV2 发起
*/
class NetViewModelV2(
private val repository: RemoteRepositoryV2 = RemoteRepositoryV2()
) : ViewModel() {
// ========== 下拉选项接口 ==========
// 食堂下拉列表
private val _canteenOptionsState = MutableStateFlow<UiState<List<NutCanteenVO>?>>(UiState.Idle)
val canteenOptionsState: StateFlow<UiState<List<NutCanteenVO>?>> =
_canteenOptionsState.asStateFlow()
fun getCanteenOptions(keyword: String? = null) {
viewModelScope.launch {
_canteenOptionsState.value = UiState.Loading
_canteenOptionsState.value = repository.getCanteenOptions(keyword)
}
}
// 字典项下拉
private val _dictItemsState = MutableStateFlow<UiState<List<NutDictItemVO>?>>(UiState.Idle)
val dictItemsState: StateFlow<UiState<List<NutDictItemVO>?>> = _dictItemsState.asStateFlow()
// fun getDictItems(dictType: String) {
// viewModelScope.launch {
// _dictItemsState.value = UiState.Loading
// _dictItemsState.value = repository.getDictItems(dictType)
// }
// }
// 食材下拉列表
private val _materOptionsState =
MutableStateFlow<UiState<List<NutMaterOptionVO>?>>(UiState.Idle)
val materOptionsState: StateFlow<UiState<List<NutMaterOptionVO>?>> =
_materOptionsState.asStateFlow()
fun getMaterOptions(keyword: String? = null) {
viewModelScope.launch {
_materOptionsState.value = UiState.Loading
_materOptionsState.value = repository.getMaterOptions(keyword)
}
}
// 包装设备下拉
private val _deviceOptionsState =
MutableStateFlow<UiState<List<NutSupPkgDeviceVO>?>>(UiState.Idle)
val deviceOptionsState: StateFlow<UiState<List<NutSupPkgDeviceVO>?>> =
_deviceOptionsState.asStateFlow()
fun getDeviceOptions() {
viewModelScope.launch {
_deviceOptionsState.value = UiState.Loading
_deviceOptionsState.value = repository.getDeviceOptions()
}
}
// 餐品下拉搜索
private val _foodOptionsState = MutableStateFlow<UiState<Page<NutFoodOptionVO>?>>(UiState.Idle)
val foodOptionsState: StateFlow<UiState<Page<NutFoodOptionVO>?>> =
_foodOptionsState.asStateFlow()
// fun getFoodOptions(keyword: String? = null, pageNum: Int = 1, pageSize: Int = 10) {
// viewModelScope.launch {
// _foodOptionsState.value = UiState.Loading
// _foodOptionsState.value = repository.getFoodOptions(keyword, pageNum, pageSize)
// }
// }
/**
* 字典项下拉(回调版本),不依赖 StateFlow,适合多次不同 dictType 的场景
*/
fun getDictItemsWithCallback(
dictType: String,
onLoading: () -> Unit = {},
onResult: (UiState<List<NutDictItemVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getDictItems(dictType))
}
}
/**
* 餐品下拉(回调版本),不依赖 StateFlow,适合多次调用的场景
*/
fun getFoodOptionsWithCallback(
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 10,
onLoading: () -> Unit = {},
onResult: (UiState<Page<NutFoodOptionVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
val state = repository.getFoodOptions(keyword, pageNum, pageSize)
onResult(state)
}
}
/**
* 设备下拉(回调版本),不依赖 StateFlow,适合多次调用的场景
*/
fun getDeviceOptionsWithCallback(
onLoading: () -> Unit = {},
onResult: (UiState<List<NutSupPkgDeviceVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getDeviceOptions())
}
}
// 按溯源码查食材信息
private val _ingredientByTraceState =
MutableStateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>>(UiState.Idle)
val ingredientByTraceState: StateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>> =
_ingredientByTraceState.asStateFlow()
fun getIngredientByTrace(traceCode: String) {
viewModelScope.launch {
_ingredientByTraceState.value = UiState.Loading
_ingredientByTraceState.value = repository.getIngredientByTrace(traceCode)
}
}
// ========== 净菜组配接口 ==========
// 上报组配任务
private val _addComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addComboTaskState: StateFlow<UiState<Unit?>> = _addComboTaskState.asStateFlow()
fun addComboTask(param: NutComboTaskDetailVO, block: (Boolean, String) -> Unit) {
viewModelScope.launch {
val uiState = repository.addComboTask(param)
when (uiState) {
is UiState.Success -> {
block(true, "")
}
is UiState.Error -> {
block(false, uiState.msg)
}
is UiState.Loading -> {}
is UiState.Idle -> {}
}
}
}
// fun addComboTask(param: NutProdComboTaskDTO) {
// viewModelScope.launch {
// _addComboTaskState.value = UiState.Loading
// _addComboTaskState.value = repository.addComboTask(param)
// }
// }
// 开始组配
private val _startComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val startComboTaskState: StateFlow<UiState<Unit?>> = _startComboTaskState.asStateFlow()
fun startComboTask(id: Long) {
viewModelScope.launch {
_startComboTaskState.value = UiState.Loading
_startComboTaskState.value = repository.startComboTask(id)
}
}
// 完成组配
private val _finishComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val finishComboTaskState: StateFlow<UiState<Unit?>> = _finishComboTaskState.asStateFlow()
fun finishComboTask(id: Long) {
viewModelScope.launch {
_finishComboTaskState.value = UiState.Loading
_finishComboTaskState.value = repository.finishComboTask(id)
}
}
// 获取组配任务视频监控地址
private val _comboTaskVideoState = MutableStateFlow<UiState<NutProdComboVideoVO?>>(UiState.Idle)
val comboTaskVideoState: StateFlow<UiState<NutProdComboVideoVO?>> =
_comboTaskVideoState.asStateFlow()
fun getComboTaskVideo(taskNo: String) {
viewModelScope.launch {
_comboTaskVideoState.value = UiState.Loading
_comboTaskVideoState.value = repository.getComboTaskVideo(taskNo)
}
}
// ========== 单品净菜包装接口 ==========
private val _addCleanPackageState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addCleanPackageState: StateFlow<UiState<Unit?>> = _addCleanPackageState.asStateFlow()
fun addCleanPackage(param: Map<String, Any?>, callback: (Boolean, String) -> Unit) {
viewModelScope.launch {
val uiState = repository.addCleanPackage(param)
when (uiState) {
is UiState.Success -> {
callback(true, "")
}
is UiState.Error -> {
callback(false, uiState.msg)
}
is UiState.Loading -> {}
is UiState.Idle -> {}
}
}
}
fun addCleanPackage(param: Map<String, Any?>) {
viewModelScope.launch {
_addCleanPackageState.value = UiState.Loading
_addCleanPackageState.value = repository.addCleanPackage(param)
}
}
// ========== 餐品净菜包装接口 ==========
private val _addMealPackageState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addMealPackageState: StateFlow<UiState<Unit?>> = _addMealPackageState.asStateFlow()
fun addMealPackage(param: Map<String, Any?>, callback: (Boolean, String) -> Unit) {
viewModelScope.launch {
val uiState = repository.addMealPackage(param)
when (uiState) {
is UiState.Success -> {
callback(true, "")
}
is UiState.Error -> {
callback(false, uiState.msg)
}
is UiState.Loading -> {}
is UiState.Idle -> {}
}
}
}
fun addMealPackage(param: Map<String, Any>) {
viewModelScope.launch {
_addMealPackageState.value = UiState.Loading
_addMealPackageState.value = repository.addMealPackage(param)
}
}
// ========== 组配任务接口 ==========
/**
* 查询未完成组配任务(回调版本)
*/
fun getIncompleteComboTasksWithCallback(
deviceCode: String? = null,
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 10,
onLoading: () -> Unit = {},
onResult: (UiState<Page<NutComboTaskVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(
repository.getIncompleteComboTasks(
deviceCode = deviceCode,
keyword = keyword,
pageNum = pageNum,
pageSize = pageSize
)
)
}
}
/**
* 获取组配任务详情(回调版本)
*/
fun getComboTaskDetailWithCallback(
id: Long,
onLoading: () -> Unit = {},
onResult: (UiState<NutComboTaskDetailVO?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getComboTaskDetail(id))
}
}
// ========== 已入库净菜食材接口 ==========
/**
* 按食材名称查询已入库有库存的净菜记录(回调版本)
*/
fun getInboundIngredientOptionsWithCallback(
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 20,
onLoading: () -> Unit = {},
onResult: (UiState<List<NutCleanInboundIngredientVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getInboundIngredientOptions(keyword))
}
}
}
@@ -1,10 +0,0 @@
package com.shuwei.dish.match.net
/** 通用分页响应包装 */
data class Page<T>(
val records: List<T>?,
val total: Long?,
val size: Int?,
val current: Int?,
val pages: Int?
)
@@ -1,8 +1,25 @@
package com.shuwei.dish.match.net
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.model.CookOrderCompositionItem
import com.shuwei.dish.match.model.CookOrderMealGroup
import com.shuwei.dish.match.model.CookOrderPageRequest
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.ConstituteSaveRequest
import com.shuwei.dish.match.model.CookCompleteRequest
import com.shuwei.dish.match.model.FoodCompositionResponse
import com.shuwei.dish.match.model.FoodSearchItem
import com.shuwei.dish.match.model.FoodSearchRequest
import com.shuwei.dish.match.model.MakeHistoryChartRow
import com.shuwei.dish.match.model.MakeHistoryItem
import com.shuwei.dish.match.model.MakeHistoryRequest
import com.shuwei.dish.match.model.MaterSearchItem
import com.shuwei.dish.match.model.MaterSearchRequest
import com.shuwei.dish.match.model.SampleHistoryItem
import com.shuwei.dish.match.model.SampleHistoryRequest
import com.shuwei.dish.match.model.SeasoningSearchItem
import com.shuwei.dish.match.model.SeasoningSearchRequest
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -31,6 +48,14 @@ class RemoteRepository {
suspend fun getFoodDetail(foodId: String): UiState<CookFoodDTO?> =
safeApiCall { apiService.getFoodDetail(foodId = foodId) }
/** 查询当日烹饪任务列表(按餐次分组),新配比秤接口 */
suspend fun getCookOrderPage(request: CookOrderPageRequest): UiState<List<CookOrderMealGroup>?> =
safeApiCall { apiService.getCookOrderPage(request = request) }
/** 查询菜品食材构成(主材+辅材),新配比秤接口 */
suspend fun getCookOrderComposition(cookOrderId: String): UiState<List<CookOrderCompositionItem>?> =
safeApiCall { apiService.getCookOrderComposition(cookOrderId = cookOrderId) }
/** 搜索菜品列表 */
suspend fun searchFoodList(param: MutableMap<String, Any>): UiState<MutableList<FoodRecord>?> =
safeApiCall { apiService.searchFoodList(param = param) }
@@ -43,6 +68,42 @@ class RemoteRepository {
suspend fun queryGoodsList(param: MutableMap<String, Any>): UiState<MutableList<GoodsItem>?> =
safeApiCall { apiService.queryGoodsList(param = param) }
/** 调料模糊搜索,新配比秤接口 */
suspend fun getSeasoningSearch(request: SeasoningSearchRequest): UiState<List<SeasoningSearchItem>?> =
safeApiCall { apiService.getSeasoningSearch(request = request) }
/** 烹饪完成,新配比秤接口,替代旧 submitCookFood */
suspend fun cookOrderComplete(request: CookCompleteRequest): UiState<Any?> =
safeApiCall { apiService.cookOrderComplete(request = request) }
/** 食材模糊搜索 */
suspend fun materSearch(request: MaterSearchRequest): UiState<MutableList<MaterSearchItem>?> =
safeApiCall { apiService.materSearch(request = request) }
/** 搜菜品(制作模式选已有上架菜品) */
suspend fun foodSearch(request: FoodSearchRequest): UiState<MutableList<FoodSearchItem>?> =
safeApiCall { apiService.foodSearch(request = request) }
/** 查菜品构成(制作模式) */
suspend fun getFoodComposition(foodId: String): UiState<FoodCompositionResponse?> =
safeApiCall { apiService.getFoodComposition(foodId = foodId) }
/** 提交采样(落库味养创新实验,制作模式已废弃本接口) */
suspend fun constituteSave(request: ConstituteSaveRequest): UiState<Any?> =
safeApiCall { apiService.constituteSave(request = request) }
/** 已提交采样列表(本食堂 source=2,按提交时间倒序,不分日期) */
suspend fun sampleHistory(request: SampleHistoryRequest): UiState<MutableList<SampleHistoryItem>?> =
safeApiCall { apiService.sampleHistory(request = request) }
/** 制作历史(本设备,按日期可选过滤) */
suspend fun makeHistory(request: MakeHistoryRequest): UiState<MutableList<MakeHistoryItem>?> =
safeApiCall { apiService.makeHistory(request = request) }
/** 制作历史图表(按菜品id) */
suspend fun makeHistoryChart(foodId: String): UiState<MutableList<MakeHistoryChartRow>?> =
safeApiCall { apiService.makeHistoryChart(foodId = foodId) }
/** 上传采集数据 */
suspend fun uploadFoodVectorData(
params: MutableMap<String, RequestBody>,
@@ -1,109 +0,0 @@
package com.shuwei.dish.match.net
import com.shuwei.dish.match.model.IdDTO
import com.shuwei.dish.match.model.NutCanteenVO
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.model.NutDictItemVO
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.model.NutMaterOptionVO
import com.shuwei.dish.match.model.NutProdComboTaskDTO
import com.shuwei.dish.match.model.NutProdComboVideoVO
import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO
import com.shuwei.dish.match.model.NutSupPkgDeviceVO
import com.shuwei.dish.match.base.GlobalData
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
/**
* 网络数据仓库 V2,封装所有 ApiServiceV2 调用
*/
class RemoteRepositoryV2 {
private val apiService: ApiServiceV2 by lazy {
Retrofit.Builder()
.baseUrl(GlobalData.appBaseUrl)
.client(HttpManager.instance.client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiServiceV2::class.java)
}
/** 统一封装 API 调用:处理响应映射和异常转换,返回 UiState */
private suspend fun <T> safeApiCall(call: suspend () -> ApiResponse<T>): UiState<T?> {
return try {
val resp = call()
if (resp.isSuccess()) UiState.Success(resp.data)
else UiState.Error(resp.code, resp.msg ?: "")
} catch (e: Exception) {
UiState.Error("-1", getApiException(e).errorMsg)
}
}
// ========== 下拉选项接口 ==========
suspend fun getCanteenOptions(keyword: String? = null): UiState<List<NutCanteenVO>?> =
safeApiCall { apiService.getCanteenOptions(keyword = keyword) }
suspend fun getDictItems(dictType: String): UiState<List<NutDictItemVO>?> =
safeApiCall { apiService.getDictItems(dictType = dictType) }
suspend fun getMaterOptions(keyword: String? = null): UiState<List<NutMaterOptionVO>?> =
safeApiCall { apiService.getMaterOptions(keyword = keyword) }
suspend fun getDeviceOptions(): UiState<List<NutSupPkgDeviceVO>?> =
safeApiCall { apiService.getDeviceOptions() }
suspend fun getFoodOptions(
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 10
): UiState<Page<NutFoodOptionVO>?> =
safeApiCall { apiService.getFoodOptions(keyword = keyword, pageNum = pageNum, pageSize = pageSize) }
suspend fun getIngredientByTrace(traceCode: String): UiState<List<NutSupHygieneIngredientOptionVO>?> =
safeApiCall { apiService.getIngredientByTrace(traceCode = traceCode) }
suspend fun getInboundIngredientOptions(keyword: String? = null): UiState<List<NutCleanInboundIngredientVO>?> =
safeApiCall { apiService.getInboundIngredientOptions(keyword = keyword) }
// ========== 净菜组配接口 ==========
suspend fun addComboTask(param: NutComboTaskDetailVO): UiState<Unit?> =
safeApiCall { apiService.addComboTask(param = param) }
suspend fun startComboTask(id: Long): UiState<Unit?> =
safeApiCall { apiService.startComboTask(param = IdDTO(id)) }
suspend fun finishComboTask(id: Long): UiState<Unit?> =
safeApiCall { apiService.finishComboTask(param = IdDTO(id)) }
suspend fun getComboTaskVideo(taskNo: String): UiState<NutProdComboVideoVO?> =
safeApiCall { apiService.getComboTaskVideo(taskNo = taskNo) }
// ========== 单品净菜包装接口 ==========
suspend fun addCleanPackage(param: Map<String, Any?>): UiState<Unit?> =
safeApiCall { apiService.addCleanPackage(param = param) }
// ========== 餐品净菜包装接口 ==========
suspend fun addMealPackage(param: Map<String, Any?>): UiState<Unit?> =
safeApiCall { apiService.addMealPackage(param = param) }
// ========== 组配任务接口 ==========
suspend fun getIncompleteComboTasks(
deviceCode: String? = null,
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 10
): UiState<Page<NutComboTaskVO>?> =
safeApiCall { apiService.getIncompleteComboTasks(deviceCode = deviceCode, keyword = keyword, pageNum = pageNum, pageSize = pageSize) }
suspend fun getComboTaskDetail(id: Long): UiState<NutComboTaskDetailVO?> =
safeApiCall { apiService.getComboTaskDetail(id = id) }
}
@@ -13,6 +13,7 @@ class RequestInterceptor : Interceptor {
val requestBuilder = originalRequest.newBuilder()
.header("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiJiZTE1NDgzMS0zNDY2LTNiYTItYTJlYS01NzY1MmM5MTlmZWQiLCJ0eXBlIjoiNCIsInVzZXJJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDEifQ.sN40cOC-O5WQFrF4IDUs8fFlkNdUKLbJt_rHyTsgYYM")
.header("X-DEVICE-CODE", GlobalData.deviceId)
.header("X-DEVICE-TOKEN", GlobalData.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
val newRequest = requestBuilder.build()
@@ -3,7 +3,10 @@ package com.shuwei.dish.match.net
data class ApiResponse<T>(
val code: String,
val msg: String? = "",
val data: T? = null
val data: T? = null,
/** 分页总条数,仅新配比秤接口返回(如 seasoning-search),旧接口为 null */
val total: Int? = null
) {
fun isSuccess() = code == "00000"
/** 兼容旧接口成功码 "00000" 和新配比秤接口成功码 "200" */
fun isSuccess() = code == "00000" || code == "200"
}
@@ -6,7 +6,7 @@ package com.shuwei.dish.match.scale
*/
object ScaleDeviceConfig {
const val DEVICE_ID_2 = "8fc2ab34-2137-3112-acca-f884ea8736d4"
const val DEVICE_ID_2 = "0bd74d78-c221-3182-b5ee-55d86dd79283"//测试0bd74d78-c221-3182-b5ee-55d86dd79283 正式8fc2ab34-2137-3112-acca-f884ea8736d4
const val DEVICE_ID_22 = "a46fa55c-113c-3511-bb1f-41e5eff77c4b"
const val DEVICE_ID_18 = "1038da9f-c6eb-326e-a1d9-d6d3af978b22"
const val DEVICE_ID_1 = "7cc0f6ea-f13d-3013-a867-fc998eb554ac"
@@ -1,716 +0,0 @@
package com.shuwei.dish.match.ui
import android.content.Intent
import android.os.Bundle
import androidx.core.content.IntentCompat
import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.dish.match.adapter.FormFieldAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivityCleanPackBinding
import com.shuwei.dish.match.model.DictType
import com.shuwei.dish.match.model.FieldType
import com.shuwei.dish.match.model.FormField
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.Job
/**
* 净菜包装表单页面,根据类型显示不同的表单字段
*/
class CleanPackActivity : BaseActivity() {
companion object {
private const val TAG = "CleanPackActivity"
/** 单品净菜包装 */
const val TYPE_CLEAN = 1
/** 餐品净菜包装 */
const val TYPE_MEAL = 2
const val EXTRA_TYPE = "extra_type"
}
private lateinit var binding: ActivityCleanPackBinding
private val fields = mutableListOf<FormField>()
private val formAdapter by lazy { FormFieldAdapter(fields) }
private val viewModelV2 by lazy { NetViewModelV2() }
private var packType: Int = TYPE_CLEAN
// 字段引用,用于跨方法访问
private var traceCodeField: FormField? = null
private var cleanNameField: FormField? = null
private var packageMethodField: FormField? = null
private var deviceField: FormField? = null
private var foodOptionsField: FormField? = null
// private var packagingSpecField: FormField? = null
private var ingredientsField: FormField? = null
private var traceCodeJob: Job? = null
// 缓存餐品选项完整数据,用于选择后获取 compositions
private val foodOptionsData = mutableListOf<NutFoodOptionVO>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
statusBarDarkFont(enable = true)
binding = ActivityCleanPackBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
packType = intent.getIntExtra(EXTRA_TYPE, TYPE_CLEAN)
// 根据类型设置标题
val title = when (packType) {
TYPE_CLEAN -> "净菜包"
TYPE_MEAL -> "餐品净菜包"
else -> return
}
setTitleBar(titleBarAction = {
it.visible()
it.setOnClickListener { v ->
hideKeyboard()
}
}, titleAction = {
it.text = title
}, rightIconAction = {
it.gone()
}, backAction = {
it.visible()
it.setOnClickListener {
finish()
}
})
addWeightListener()
// 初始化 RecyclerView
// val flexboxLayoutManager = FlexboxLayoutManager(this).apply {
// flexDirection = FlexDirection.ROW
// flexWrap = FlexWrap.WRAP
// alignItems = AlignItems.STRETCH
// }
val layoutManager = GridLayoutManager(this, 2)
binding.rvForm.layoutManager = layoutManager
binding.rvForm.adapter = formAdapter
// 根据类型初始化表单数据
when (packType) {
TYPE_CLEAN -> initCleanPackFields()
TYPE_MEAL -> initMealPackFields()
}
binding.btnSubmit.setOnClickListener {
hideKeyboard()
if (!validateFields()) return@setOnClickListener
// when (packType) {
// TYPE_CLEAN -> submitCleanPackage()
// TYPE_MEAL -> submitMealPackage()
// }
submitCleanPackage()
}
binding.ivClearWeight.clickWithDebounce {
initialWeight = 0.0
SpTool.configWeight = 0
WeightUtil.tareTwo(1)
}
binding.flSearchBar.clickWithDebounce {
val launchIntent = Intent(this, FoodSearchV2Activity::class.java)
startActivity(launchIntent) { data ->
data?:return@startActivity
IntentCompat.getParcelableExtra<NutCleanInboundIngredientVO>(
data,
FoodSearchV2Activity.INGREDIENT_ITEM,
NutCleanInboundIngredientVO::class.java
)?.let { food ->
this@CleanPackActivity.currentFood = food
binding.tvFoodName.text = food.ingredientName
}
}
}
}
private var currentFood: NutCleanInboundIngredientVO? = null
/**
* 初始化单品净菜包装表单字段
* 对应接口 NutSupCleanPackageDTO
*/
private fun initCleanPackFields() {
fields.clear()
// // 溯源码
// traceCodeField = FormField(
// label = "溯源码",
// type = FieldType.TEXT,
// apiKey = "traceCode",
// required = true
// )
// fields.add(traceCodeField!!)
// 净菜名称
// cleanNameField = FormField(
// label = "净菜名称",
// type = FieldType.DROPDOWN,
//// apiKey = "cleanName",
// extraApiKeys = mapOf("cleanName" to "value", "traceCode" to "type"),
// required = true
// )
// fields.add(cleanNameField!!)
// 包装方式(下拉)
packageMethodField = FormField(
label = "包装方式",
type = FieldType.DROPDOWN,
// extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
extraApiKeys = mapOf("packageMethod" to "type"),
required = true
)
fields.add(packageMethodField!!)
fields.add(
FormField(label = "包装规格", type = FieldType.TEXT, apiKey = "spec", required = false)
)
fields.add(
FormField(
label = "包装份数",
type = FieldType.INTEGER,
apiKey = "quantity",
required = true,
hint = "请输入整数"
)
)
fields.add(
FormField(
label = "包装日期",
type = FieldType.DATE_PICKER,
apiKey = "packageDate",
required = true
)
)
fields.add(
FormField(
label = "保质期至",
type = FieldType.DATE_PICKER,
apiKey = "expiryDate",
required = true
)
)
fields.add(
FormField(
label = "存放温度(℃)",
type = FieldType.DECIMAL,
apiKey = "storageTemp",
required = false,
hint = "请输入小数"
)
)
fields.add(
FormField(
label = "包装人",
type = FieldType.TEXT,
apiKey = "operator",
required = false
)
)
// 终端设备(下拉)
deviceField = FormField(
label = "终端设备",
type = FieldType.DROPDOWN,
extraApiKeys = mapOf("deviceId" to "id"),
required = false
)
fields.add(deviceField!!)
formAdapter.submitList(fields.filter { !it.hidden })
// 监听溯源码输入,自动填充净菜名称
// setupTraceCodeQuery()
// 加载下拉选项
// loadDictItems("sup_package_method", packageMethodField!!)
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
loadDeviceOptionsWithCallback(deviceField)
}
/**
* 初始化餐品净菜包装表单字段
* 对应接口 NutSupMealPackageDTO
*/
private fun initMealPackFields() {
fields.clear()
// 包装餐品(下拉)
foodOptionsField = FormField(
label = "包装餐品", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("foodId" to "id"),
required = true
)
fields.add(foodOptionsField!!)
// 包装规格(下拉)
// packagingSpecField = FormField(
// label = "包装规格", type = FieldType.DROPDOWN,
// extraApiKeys = mapOf("spec" to "value"),
// required = true
// )
// fields.add(packagingSpecField!!)
// 包装方式(下拉)
packageMethodField =
FormField(
label = "包装方式", type = FieldType.DROPDOWN,
// extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
extraApiKeys = mapOf("packageMethod" to "type"),
required = true
)
fields.add(packageMethodField!!)
// 包装份数
fields.add(
FormField(
label = "包装份数", type = FieldType.INTEGER,
apiKey = "quantity", required = true, hint = "请输入份数"
)
)
// 包含食材(子列表容器)
val ingredientTemplate = listOf(
FormField(
label = "食材溯源码",
type = FieldType.TEXT,
apiKey = "traceCode",
required = true,
hint = "溯源码"
),
FormField(
label = "食材名称",
type = FieldType.FIXED,
extraApiKeys = mapOf("materId" to "id", "ingredientName" to "value"),
required = true
),
FormField(
label = "分类", type = FieldType.FIXED,
// extraApiKeys = mapOf("classify" to "value"),
apiKey = "classify",
required = true,
// options = mutableListOf(
// DictType(id = "1", value = "蔬菜类", type = "蔬菜类"),
// DictType(id = "2", value = "肉禽类", type = "肉禽类"),
// DictType(id = "3", value = "水产类", type = "水产类"),
// )
),
FormField(
label = "每份用量",
type = FieldType.TEXT,
apiKey = "amount",
required = true,
hint = "如 120g"
),
)
ingredientsField = FormField(
label = "包含食材", type = FieldType.CHILDREN,
required = true,
children = ingredientTemplate,
childAddLabel = "+ 添加食材"
)
fields.add(ingredientsField!!)
// 餐品选中后,根据 position 从 compositions 加载包含食材
foodOptionsField!!.onDropdownSelected = { dictType ->
val position = dictType.position
if (position >= 0 && position < foodOptionsData.size) {
val compositions = foodOptionsData[position].compositions ?: emptyList()
val ingredients = ingredientsField!!
ingredients.childrenRows.clear()
for (comp in compositions) {
// 每行复制模板的4个字段,将 composition 数据填充进去
val newRow = ingredients.children?.map { template ->
// 根据 extraApiKeys 判断是否为食材名称字段
val hasMaterId = template.extraApiKeys.containsKey("materId")
val hasIngredientName = template.extraApiKeys.containsKey("ingredientName")
val fieldValue = if (hasIngredientName) comp.ingredientName ?: "" else ""
val fieldExtraValues = mutableMapOf<String, String>()
template.extraApiKeys.forEach { (apiKey, dictField) ->
fieldExtraValues[apiKey] = when (dictField) {
"id" -> comp.materId?.toString() ?: ""
"value" -> comp.ingredientName ?: ""
else -> ""
}
}
FormField(
label = template.label,
type = template.type,
apiKey = template.apiKey,
extraApiKeys = template.extraApiKeys,
required = template.required,
hint = template.hint,
options = template.options,
value = fieldValue,
extraValues = fieldExtraValues
)
}?.toMutableList() ?: mutableListOf()
ingredients.childrenRows.add(newRow)
}
val idx = fields.indexOf(ingredients)
if (idx >= 0) formAdapter.notifyItemChanged(idx)
}
}
// 包装日期
fields.add(
FormField(
label = "包装日期", type = FieldType.DATE_PICKER,
apiKey = "packageDate", required = true
)
)
// 保质期至
fields.add(
FormField(
label = "保质期至", type = FieldType.DATE_PICKER,
apiKey = "expiryDate", required = true
)
)
// 存放温度
fields.add(
FormField(
label = "存放温度(℃)", type = FieldType.DECIMAL,
apiKey = "storageTemp", required = false, hint = "如 3.8"
)
)
// 包装人
fields.add(
FormField(
label = "包装人",
type = FieldType.FIXED,
apiKey = "operator",
required = false,
hint = "请输入包装人",
value = GlobalData.userName
)
)
// 终端设备(下拉)
deviceField = FormField(
label = "终端设备", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("deviceId" to "id"),
required = false
)
fields.add(deviceField!!)
// 操作类型(暂时固定)
fields.add(
FormField(
label = "操作类型", type = FieldType.FIXED,
apiKey = "opType",
required = true,
value = "设备自动"
)
)
formAdapter.submitList(fields.filter { !it.hidden })
// 加载下拉选项
loadFoodOptionsWithKeyword()
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
// loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!)
loadDeviceOptionsWithCallback(deviceField)
}
/**
* 监听溯源码字段变化,输入后 800ms 无变化则触发查询
*/
private fun setupTraceCodeQuery() {
// val field = traceCodeField ?: return
// val cleanField = cleanNameField ?: return
//
// traceCodeJob = lifecycleScope.launch {
// var lastValue = ""
// while (true) {
// val current = field.value.trim()
// if (current != lastValue && current.isNotBlank()) {
// lastValue = current
// viewModelV2.getIngredientByTrace(current)
// }
// delay(800)
// }
// }
//
// lifecycleScope.launch {
// viewModelV2.ingredientByTraceState.collect { state ->
// when (state) {
// is UiState.Success -> {
// val data = state.data
// if (!data.isNullOrEmpty()) {
// val info = data[0]
// cleanField.value = info.materName ?: ""
// // 通知 adapter 刷新净菜名称字段(索引为 1)
// val idx = fields.indexOf(cleanField)
// if (idx >= 0) formAdapter.notifyItemChanged(idx)
// }
// }
//
// is UiState.Error -> {
// toast(state.msg)
// }
//
// else -> {}
// }
// }
// }
}
override fun onDestroy() {
super.onDestroy()
traceCodeJob?.cancel()
WeightUtil.removeWeightListener(TAG)
}
/**
* 加载餐品下拉选项(回调版本)
*/
private fun loadFoodOptionsWithKeyword() {
val field = foodOptionsField ?: return
viewModelV2.getFoodOptionsWithCallback(
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val records = state.data?.records ?: emptyList()
foodOptionsData.clear()
foodOptionsData.addAll(records)
val options = records.mapIndexed { index, vo ->
DictType(
id = vo.foodId?.toString() ?: "",
value = vo.foodName ?: "",
type = vo.foodCode ?: "",
position = index
)
}
field.options.clear()
field.options.addAll(options)
val index = fields.indexOf(field)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载餐品列表失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 加载字典项下拉(回调版本),适合多次不同 dictType 的场景
*/
private fun loadDictItemsWithCallback(dictType: String, targetField: FormField) {
viewModelV2.getDictItemsWithCallback(
dictType = dictType,
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.dictLabel ?: "",
type = vo.dictCode ?: ""
)
} ?: emptyList()
targetField.options.clear()
targetField.options.addAll(options)
val index = fields.indexOf(targetField)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载字典项失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 加载设备下拉(回调版本),用于餐品净菜包装
*/
private fun loadDeviceOptionsWithCallback(deviceField: FormField? = null) {
val field = deviceField ?: return
viewModelV2.getDeviceOptionsWithCallback(
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.deviceName ?: "",
type = vo.deviceNo ?: ""
)
} ?: emptyList()
field.options.clear()
field.options.addAll(options)
val index = fields.indexOf(field)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载设备列表失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 校验必填字段
*/
private fun validateFields(): Boolean {
for (field in fields) {
if (field.hidden) continue
if (field.required) {
if (field.type == FieldType.CHILDREN) {
if (field.childrenRows.isEmpty()) {
toast("请至少添加一条${field.label}")
return false
}
// 校验子行必填
for ((rowIdx, row) in field.childrenRows.withIndex()) {
for (child in row) {
if (child.required && child.value.isBlank()) {
toast("${rowIdx + 1}${child.label}不能为空")
return false
}
}
}
} else if (field.value.isBlank()) {
toast("${field.label}不能为空")
return false
}
}
}
return true
}
/**
* 收集字段提交值,根据 apiKey / extraApiKeys 规则汇总
* @return 提交参数 Mapkey 为接口参数名,value 为提交值
*/
private fun collectSubmitValues(): MutableMap<String, String?> {
val params = mutableMapOf<String, String?>()
for (field in fields) {
if (field.hidden) continue
// 跳过 CHILDREN 类型,暂不处理
if (field.type == FieldType.CHILDREN) continue
if (field.extraApiKeys.isNotEmpty()) {
// 下拉字段:直接读取 bindDropdown 已解析好的 extraValues
field.extraValues.forEach { (apiKey, value) ->
params[apiKey] = value
}
} else if (field.apiKey.isNotBlank()) {
// 普通字段
params[field.apiKey] = field.value
}
}
return params
}
/**
* 提交单品净菜包装
*/
private fun submitCleanPackage() {
val params = collectSubmitValues()
if (currentWeight <= 0.0) {
toast("未识别到重量")
return
}
if (currentFood == null || currentFood!!.ingredientName.isNullOrBlank() || currentFood!!.traceCode.isNullOrBlank()) {
toast("食材信息错误")
return
}
val food = currentFood!!
params["traceCode"] = food.traceCode
params["cleanInboundId"] = food.id.toString()
params["cleanName"] = food.ingredientName
params["weightPerUnit"] = currentWeight.toString()
viewModelV2.addCleanPackage(params) { state, msg ->
if (state.not()) {
toast("提交失败:$msg")
return@addCleanPackage
}
toast("提交成功")
finish()
}
}
/**
* 提交餐品净菜包装
*/
private fun submitMealPackage() {
val params = collectSubmitValues()
viewModelV2.addMealPackage(params) { state, msg ->
if (state.not()) {
toast("提交失败:$msg")
return@addMealPackage
}
toast("提交成功")
finish()
}
}
private var initialWeight = SpTool.configWeight.toDouble()
private var currentWeight = 0.0
private fun addWeightListener() {
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread {
currentWeight = weight - initialWeight
binding.tvShowWeight.run {
if (tag != currentWeight) {
val weightText = "${currentWeight.roundedOneDecimalPlace()}g"
text = weightText
tag = currentWeight
}
}
}
}
})
}
}
@@ -4,31 +4,26 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.Food2Adapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivityFoodSearchBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.FoodSearchItem
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import java.io.Serializable
import kotlinx.coroutines.launch
import kotlin.collections.getOrNull
/**
* 菜品搜索页面
* 菜品搜索页面(采样模式:搜索已有上架菜品作为称量模板)
* 数据源: POST /neglect/ratio-scale/food-search
*/
class FoodSearchActivity : BaseActivity() {
@@ -37,25 +32,16 @@ class FoodSearchActivity : BaseActivity() {
const val FOOD_ITEM = "foodItem"
const val FOOD_ID = "foodId"
const val FOOD_NAME = "foodName"
const val DINNER_TYPE = "dinnerType"
}
private lateinit var binding: ActivityFoodSearchBinding
private var list: MutableList<FoodRecord> = mutableListOf()
private var dinnerType = "1"
private val recordAdapter by lazy {
Food2Adapter(list).apply {
isStateViewEnable = true
setOnItemClickListener { adapter, view, position ->
var jumpItem = list.getOrNull(position) ?: return@setOnItemClickListener
if (SpTool.cookMode == 0) {
jumpItem.dinnerType = dinnerType
startActivity<PrepareFoodActivity> {
putExtra(PrepareFoodActivity.FOOD_ITEM, jumpItem as Serializable)
}
return@setOnItemClickListener
}
val jumpItem = list.getOrNull(position) ?: return@setOnItemClickListener
setResult(RESULT_OK, Intent().apply {
putExtra(FOOD_ITEM, jumpItem as Serializable)
})
@@ -70,7 +56,6 @@ class FoodSearchActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivityFoodSearchBinding.inflate(layoutInflater)
setContentView(binding.root)
dinnerType = intent.getStringExtra(DINNER_TYPE) ?: "0"
setHeaderBackground()
foodName = intent.getStringExtra(FOOD_NAME)
binding.etInputDish.setText(foodName)
@@ -91,31 +76,6 @@ class FoodSearchActivity : BaseActivity() {
adapter = recordAdapter
}
addViewListener()
initObserver()
}
/**
* 收集 searchFoodState,统一处理 Loading / Success / Error 状态
*/
private fun initObserver() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
netViewModel.searchFoodState.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> handleSearchResult(state.data)
is UiState.Error -> {
delayDismissLoading()
finishRefresh()
binding.refreshLayout.setEnableRefresh(true)
toast(state.msg)
if (pageNo == 1) loadEmptyView()
}
is UiState.Idle -> {}
}
}
}
}
}
private fun addViewListener() {
@@ -161,54 +121,65 @@ class FoodSearchActivity : BaseActivity() {
}
private var pageNo = 1
private var pageSize = 10
fun getDinnerTypeText(): String {
return when (dinnerType) {
"1" -> "早餐"
"2" -> "午餐"
"3" -> "晚餐"
else -> "早餐"
}
}
private var pageSize = 20
@SuppressLint("NotifyDataSetChanged")
private fun queryListInfo(input: String) {
val map = mutableMapOf<String, Any>(
"foodName" to input.trim(),
"pageNum" to pageNo,
"pageSize" to pageSize,
"placeId" to BaseApp.canteenId,
"dinnerType" to getDinnerTypeText()
netViewModel.foodSearch(
keyword = input.trim(),
pageNum = pageNo,
pageSize = pageSize,
onLoading = { showLoading() },
onResult = { state ->
when (state) {
is UiState.Success -> handleSearchResult(state.data)
is UiState.Error -> {
delayDismissLoading()
finishRefresh()
binding.refreshLayout.setEnableRefresh(true)
toast(state.msg)
if (pageNo == 1) loadEmptyView()
}
else -> {}
}
}
)
netViewModel.searchFoodList(param = map)
}
/**
* 处理搜索结果,填充列表或展示空视图
* FoodSearchItem → FoodRecord 映射,供 Food2Adapter 展示
* 采样模式下返回给 PrepareFoodActivity 作为称量模板
*/
private fun FoodSearchItem.toFoodRecord(): FoodRecord = FoodRecord(
foodId = foodId,
foodName = foodName,
cookMode = 1
)
/**
* 处理搜索结果:FoodSearchItem → FoodRecord 填充列表
*/
@SuppressLint("NotifyDataSetChanged")
private fun handleSearchResult(records: MutableList<FoodRecord>?) {
private fun handleSearchResult(items: List<FoodSearchItem>?) {
delayDismissLoading()
finishRefresh()
binding.refreshLayout.setEnableRefresh(true)
if (records.isNullOrEmpty()) {
if (pageNo == 1) loadEmptyView()
if (items.isNullOrEmpty()) {
if (pageNo == 1) {
loadEmptyView()
}
return
}
if (pageNo == 1) list.clear()
list.addAll(records)
list.addAll(items.map { it.toFoodRecord() })
recordAdapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= pageSize
val isLoadMoreEnable = items.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) pageNo++
}
private fun finishRefresh() {
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
} else {
binding.refreshLayout.finishLoadMore(1200)
}
binding.refreshLayout.finishRefresh(1200)
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null
@@ -1,204 +0,0 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.chad.library.adapter4.util.setOnDebouncedItemClick
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.InboundIngredientV2Adapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityFoodSearchV2Binding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.launch
/**
* 食材搜索页面 V2,使用 NetViewModelV2 的回调接口查询已入库净菜食材
*/
class FoodSearchV2Activity : BaseActivity() {
companion object {
const val INGREDIENT_ITEM = "ingredientItem"
const val INGREDIENT_NAME = "ingredientName"
const val PAGE_SIZE = 30
}
private lateinit var binding: ActivityFoodSearchV2Binding
private val viewModelV2 by lazy { NetViewModelV2() }
private val list = mutableListOf<NutCleanInboundIngredientVO>()
private val recordAdapter by lazy {
InboundIngredientV2Adapter(list).apply {
isStateViewEnable = true
setOnDebouncedItemClick { _, _, position ->
val item = list.getOrNull(position) ?: return@setOnDebouncedItemClick
setResult(RESULT_OK, Intent().apply {
putExtra(INGREDIENT_ITEM, item)
})
finish()
}
}
}
private var ingredientName: String? = null
private var currentPage = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFoodSearchV2Binding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
ingredientName = intent.getStringExtra(INGREDIENT_NAME)
binding.etInputIngredient.setText(ingredientName)
setTitleBar(titleBarAction = {
it.visible()
}, titleAction = {
it.text = "食材搜索"
}, rightIconAction = {
it.gone()
})
binding.rvIngredientList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
it.adapter = recordAdapter
}
addViewListener()
// 若有传入食材名称,自动触发搜索
queryIngredientList(ingredientName)
}
private fun addViewListener() {
binding.ivIngredientSearch.setOnClickListener {
getInputAndSearch()
}
binding.refreshLayout.setEnableRefresh(true)
binding.refreshLayout.setEnableLoadMore(false)
binding.refreshLayout.run {
setOnRefreshListener {
currentPage = 1
val input = binding.etInputIngredient.text.toString().trim()
queryIngredientList(input)
}
setOnLoadMoreListener {
val input = binding.etInputIngredient.text.toString().trim()
queryIngredientList(input)
}
}
binding.root.setOnClickListener {
hideKeyboard()
}
binding.etInputIngredient.run {
setOnEditorActionListener { _, actionId, _ ->
if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH) {
getInputAndSearch()
true
} else {
false
}
}
addTextChangedListener(onTextChanged = { text, _, _, _ ->
if (text.isNullOrBlank()) {
currentPage = 1
queryIngredientList()
}
})
}
}
private fun getInputAndSearch() {
val input = binding.etInputIngredient.text.toString().trim()
if (input.isBlank()) {
toast("请输入食材名称")
binding.refreshLayout.finishRefresh()
return
}
queryIngredientList(input)
}
/**
* 查询已入库净菜食材列表
*/
private fun queryIngredientList(keyword: String? = null) {
hideKeyboard()
showLoading()
viewModelV2.getInboundIngredientOptionsWithCallback(
keyword = keyword,
pageNum = currentPage,
pageSize = PAGE_SIZE,
onLoading = {},
onResult = { state ->
lifecycleScope.launch {
when (state) {
is UiState.Success -> handleSearchResult(state.data)
is UiState.Error -> {
delayDismissLoading()
if (currentPage == 1) {
binding.refreshLayout.finishRefresh()
} else {
binding.refreshLayout.finishLoadMore()
}
toast(state.msg)
loadEmptyView()
}
else -> {}
}
}
})
}
/**
* 处理搜索结果,填充列表或展示空视图
*/
@SuppressLint("NotifyDataSetChanged")
private fun handleSearchResult(records: List<NutCleanInboundIngredientVO>?) {
delayDismissLoading()
if (currentPage == 1 && records.isNullOrEmpty()) {
loadEmptyView()
return
}
list.clear()
list.addAll(records!!)
recordAdapter.notifyDataSetChanged()
val isLoadMore = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(isLoadMore)
if (isLoadMore) {
currentPage++
binding.refreshLayout.finishLoadMore()
}
else {
binding.refreshLayout.finishLoadMoreWithNoMoreData()
}
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
list.clear()
recordAdapter.notifyDataSetChanged()
if (emptyViewBinding == null) {
emptyViewBinding = LayoutEmptyViewBinding.inflate(
LayoutInflater.from(this), binding.rvIngredientList, false
)
}
emptyViewBinding!!.tvContent.text = "暂无数据"
emptyViewBinding!!.tvSubContent.text = "可以尝试换个名称重新搜索"
recordAdapter.stateView = emptyViewBinding!!.root
binding.refreshLayout.run {
setEnableRefresh(false)
setEnableLoadMore(false)
}
}
}
@@ -1,40 +0,0 @@
package com.shuwei.dish.match.ui
import android.os.Bundle
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityHomeV2Binding
import com.shuwei.dish.match.utils.ext.startActivity
/**
* 主页V2 - 净菜包装页面
*/
class HomeV2Activity : BaseActivity() {
private lateinit var binding: ActivityHomeV2Binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
statusBarDarkFont(enable = true)
binding = ActivityHomeV2Binding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvTitle.setOnClickListener {
startActivity<SettingActivity> {}
}
binding.btnCleanPack.setOnClickListener {
startActivity<CleanPackActivity> {
putExtra(CleanPackActivity.EXTRA_TYPE, CleanPackActivity.TYPE_CLEAN)
}
}
binding.btnMealPack.setOnClickListener {
startActivity<MealListActivity> {}
}
binding.btnTask.setOnClickListener {
startActivity<TaskActivity> {}
}
}
}
@@ -11,8 +11,12 @@ import androidx.activity.addCallback
import androidx.lifecycle.lifecycleScope
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivityInitBinding
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.AppUtil
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.SpTool
@@ -87,28 +91,27 @@ class InitActivity : BaseActivity() {
lifecycleScope.launch {
if (withDelay) delay(2000)
// 子设备:直接进入小屏专属页面,不走原有大屏业务流程
// if (GlobalData.deviceRole == DeviceRole.SLAVE) {
// startActivity<SlaveActivity>()
// // 延迟到下一帧 finish,避免与 LaunchActivityItem 事务竞态导致
// // "Activity client record must not be null" 偶发崩溃
// window.decorView.post { finish() }
// return@launch
// }
if (GlobalData.deviceRole == DeviceRole.SLAVE) {
startActivity<SlaveActivity>()
// 延迟到下一帧 finish,避免与 LaunchActivityItem 事务竞态导致
// "Activity client record must not be null" 偶发崩溃
window.decorView.post { finish() }
return@launch
}
// 主设备:走原有路由逻辑
// when (SpTool.cookMode) {
// 0 -> {
// startActivity<CookingModeActivity>()
// }
//
// 1 -> {
// goSampling()
// }
//
// else -> {
// startActivity<HomeActivity>()
// }
// }
startActivity<HomeV2Activity>()
when (SpTool.cookMode) {
0 -> {
startActivity<CookingModeActivity>()
}
1 -> {
goSampling()
}
else -> {
startActivity<HomeActivity>()
}
}
// 延迟到下一帧 finish,避免与 LaunchActivityItem 事务竞态导致
// "Activity client record must not be null" 偶发崩溃
window.decorView.post { finish() }
@@ -1,12 +0,0 @@
package com.shuwei.dish.match.ui
object MaterialTypeTool {
fun getMaterialText(type: Int) = when (type) {
1 -> "主料"
2 -> "辅料"
3 -> "调料"
else -> ""
}
}
@@ -1,153 +0,0 @@
package com.shuwei.dish.match.ui
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.adapter.MealListAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityMealListBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
/**
* 餐品列表页面
*/
class MealListActivity : BaseActivity() {
companion object {
const val MEAL_ITEM = "mealItem"
const val PAGE_SIZE = 30
}
private val binding by lazy { ActivityMealListBinding.inflate(layoutInflater) }
private val viewModelV2 by lazy { NetViewModelV2() }
private val mealList = mutableListOf<NutFoodOptionVO>()
private val mealAdapter by lazy { MealListAdapter(mealList).apply {
isStateViewEnable = true
setOnItemClickListener { _, _, position ->
val item = mealList.getOrNull(position) ?: return@setOnItemClickListener
startActivity<PackActivity>{
putExtra(PackActivity.MEAL_ITEM, item)
}
}
} }
private var emptyViewBinding: LayoutEmptyViewBinding? = null
private var currentPage = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setHeaderBackground()
setTitleBar(titleBarAction = {
it.visible()
}, titleAction = {
it.text = "餐品列表"
}, rightIconAction = {
it.gone()
}, backAction = {
it.setOnClickListener {
finish()
}
})
initRecyclerView()
loadMealList()
}
/**
* 初始化 RecyclerView
*/
private fun initRecyclerView() {
binding.rvMealList.layoutManager = LinearLayoutManager(this)
binding.rvMealList.adapter = mealAdapter
binding.refreshLayout.setEnableRefresh(true)
binding.refreshLayout.setEnableLoadMore(true)
binding.refreshLayout.setOnRefreshListener {
currentPage = 1
loadMealList()
}
binding.refreshLayout.setOnLoadMoreListener {
currentPage++
loadMealList()
}
}
/**
* 加载餐品列表
*/
private fun loadMealList() {
showLoading("加载中……")
viewModelV2.getFoodOptionsWithCallback(
keyword = null,
pageNum = currentPage,
pageSize = PAGE_SIZE,
onLoading = {},
onResult = { state ->
dismissLoading()
when (state) {
is UiState.Success -> {
val records = state.data?.records
if (currentPage == 1 && records.isNullOrEmpty()) {
binding.refreshLayout.finishRefresh()
loadEmptyView()
return@getFoodOptionsWithCallback
}
if (currentPage == 1) {
mealList.clear()
}
mealList.addAll(records!!)
mealAdapter.notifyDataSetChanged()
val isLoadMore = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(isLoadMore)
if (isLoadMore) {
binding.refreshLayout.finishLoadMore()
} else {
binding.refreshLayout.finishLoadMoreWithNoMoreData()
}
}
is UiState.Error -> {
toast(state.msg)
if (currentPage == 1) {
binding.refreshLayout.finishRefresh()
loadEmptyView()
} else {
currentPage--
binding.refreshLayout.finishLoadMore()
}
}
else -> {}
}
}
)
}
/**
* 加载空视图
*/
private fun loadEmptyView() {
mealList.clear()
mealAdapter.notifyDataSetChanged()
if (emptyViewBinding == null) {
emptyViewBinding = LayoutEmptyViewBinding.inflate(
LayoutInflater.from(this), binding.rvMealList, false
)
}
emptyViewBinding?.run {
tvContent.text = "暂无数据"
tvSubContent.text = "暂无餐品数据"
mealAdapter.stateView = root
}
binding.refreshLayout.run {
setEnableRefresh(true)
setEnableLoadMore(false)
}
}
}
@@ -1,868 +0,0 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FoodMaterialAdapter
import com.shuwei.dish.match.adapter.FormFieldAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPackBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.dialog.InboundIngredientSearchDialog
import com.shuwei.dish.match.model.DictType
import com.shuwei.dish.match.model.FieldType
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.FormField
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.yanzhenjie.recyclerview.SwipeMenuItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import com.shuwei.dish.match.model.NutFoodComposition
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.utils.DateTimeUtil
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import java.time.LocalDateTime
@SuppressLint("NotifyDataSetChanged")
class PackActivity : BaseActivity() {
companion object {
const val TAG = "PackActivity"
const val MEAL_ITEM = "mealItem"
const val PAGE_FROM = "pageFrom"
const val HOME = "home"
const val WEIGHT_CHANGE_VALUE = 5
const val WEIGHT_RECOGNIZE_VALUE = 10
/** 主材数量上限 */
const val MATERIAL_COUNT = 3
}
private val binding by lazy { ActivityPackBinding.inflate(layoutInflater) }
private val cameraUtils: CameraUtils by lazy { CameraUtils(this) }
private var mealItem: NutFoodOptionVO? = null
/** 来源页面标识,HOME 时返回跳转 SamplingListActivity */
private var pageFrom: String? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null
private val viewModelV2 by lazy { NetViewModelV2() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setHeaderBackground()
pageFrom = intent.getStringExtra(PAGE_FROM)
mealItem = IntentCompat.getParcelableExtra(intent, MEAL_ITEM, NutFoodOptionVO::class.java)
mealItem?.let {
binding.etInputDish.run {
setText(it.foodName)
tag = it.foodName
}
}
setTitleBar(titleBarAction = {
it.visible()
}, titleAction = {
it.text = "餐品净菜包"
}, rightIconAction = {
it.gone()
}, backAction = {
it.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
})
// 不可编辑,仅展示菜名
binding.etInputDish.isFocusable = false
binding.etInputDish.isFocusableInTouchMode = false
binding.ivDishSearch.gone()
// binding.tvGoodsName.text = "食材名称"
addViewClickListener()
addBackKeyListener()
initCamera(binding.flCameraContainer)
initRecyclerView()
initObserver()
// 根据传入的餐品数据加载食材列表
mealItem?.let {
val compositions = it.compositions
if (!compositions.isNullOrEmpty()) {
loadDishCompositions(compositions)
} else {
toast("该餐品无食材构成信息")
}
}
}
private var currentWeight = 0.0
private fun addViewClickListener() {
// ========== 重量识别相关功能已注释 ==========
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread {
currentWeight = weight - initialWeight
binding.tvShowWeight.run {
if (tag != currentWeight) {
// text = "称重:${netWeight}g"
val weightText = "${currentWeight.roundedOneDecimalPlace()}g"
text = weightText
tag = currentWeight
}
}
//recognizeFood(netWeight)
}
}
})
if (SpTool.cookMode == 1) {
// 采集模式:绑定搜索栏交互
binding.ivDishSearch.visible()
binding.etInputDish.isFocusable = true
binding.etInputDish.isFocusableInTouchMode = true
binding.ivDishSearch.setOnClickListener { jumpSearch() }
binding.etInputDish.addOnActionSearchListener { jumpSearch() }
}
// binding.btnCook.clickWithDebounce {
// if (SpTool.cookMode == 1) {
// // 采集模式:从输入框创建 FoodRecord
// val showFoodName = binding.etInputDish.text.toString().trim()
// if (showFoodName.isBlank()) {
// toast("菜品名称为空")
// return@clickWithDebounce
// }
// if (list.isEmpty()) {
// toast("菜品构成信息未设置")
// return@clickWithDebounce
// }
// val foodRecord = FoodRecord().apply {
// foodId = mealItem?.foodId?.toString()
// foodName = showFoodName
// cookMode = 1
// }
// Log.d(TAG, "goToSubmit: goodsList:${list.toJsonString()}")
// val submitList = list.map { it.toCookFoodGoodsEntity() }.toMutableList()
// startActivity<SubmitFoodActivity> {
// putExtra(SubmitFoodActivity.GOODS_LIST, submitList as Serializable)
// putExtra(SubmitFoodActivity.FOOD_ITEM, foodRecord as Serializable)
// }
// } else {
// // 包装模式:检查食材是否全部设置完成
// val count = list.count { it.isSetFinished.not() }
// if (count > 0) {
// showRemindDialog()
// return@clickWithDebounce
// }
// openSubmitPage()
// }
// }
binding.ivClearWeight.clickWithDebounce {
initialWeight = 0.0
SpTool.configWeight = 0
WeightUtil.tareTwo(1)
}
binding.ivAddWeight.clickWithDebounce {
if (currentWeight <= 0.0) {
toast("当前秤重量无效")
return@clickWithDebounce
}
list.getOrNull(clickIndex)?.let {
it.useWeight = currentWeight
it.isSetFinished = true
materialAdapter.notifyItemChanged(clickIndex)
}
}
binding.btnGetTraceCode.clickWithDebounce {
val item = list.getOrNull(clickIndex) ?: return@clickWithDebounce
InboundIngredientSearchDialog(
activity = this,
viewModelV2 = viewModelV2,
defIngredientName = item.goodsName
) { vo ->
materialAdapter.getItem(clickIndex)?.let {
it.materCode = vo.traceCode
materialAdapter.notifyItemChanged(clickIndex)
}
}.show()
}
binding.btnSubmit.clickWithDebounce {
submit()
}
}
/**
* 收集字段提交值,根据 apiKey / extraApiKeys 规则汇总
* @return 提交参数 Mapkey 为接口参数名,value 为提交值
*/
private fun collectSubmitValues(): MutableMap<String, String> {
val params = mutableMapOf<String, String>()
for (field in fields) {
if (field.hidden) continue
// 跳过 CHILDREN 类型,暂不处理
if (field.type == FieldType.CHILDREN) continue
if (field.extraApiKeys.isNotEmpty()) {
// 下拉字段:直接读取 bindDropdown 已解析好的 extraValues
field.extraValues.forEach { (apiKey, value) ->
params[apiKey] = value
}
} else if (field.apiKey.isNotBlank()) {
// 普通字段
params[field.apiKey] = field.value
}
}
return params
}
/**
* 提交餐品净菜包装
*/
private fun submit() {
val food = mealItem ?: return
//包装方式、包装份数、保质期至、包装人、存放温度、终端设备
val values = collectSubmitValues()
val params = mutableMapOf<String, Any?>()
values.forEach { (key, value) ->
params[key] = value
}
//关联餐品id
params["foodId"] = food.foodId
//包装餐品名称
params["mealName"] = food.foodName
//包装日期
params["packageDate"] = DateTimeUtil.formatDateTime(
dateTime = LocalDateTime.now(),
pattern = DateTimeUtil.YYYY_MM_DD
)
// //操作类型
// params["opType"] = "设备自动"
//食材明细列表
params["ingredients"] = list.map {
NutFoodComposition(
materId = it.goodsId.toLongOrNull(),
ingredientName = it.goodsName,
traceCode = it.materCode,
amount = it.useWeight,
isMain = it.materialType,
mainText = MaterialTypeTool.getMaterialText(it.materialType)
)
}.toMutableList()
viewModelV2.addMealPackage(params) { state, msg ->
if (state.not()) {
toast("提交失败:$msg")
return@addMealPackage
}
toast("提交成功")
finish()
}
}
// private val isTakingPhoto = AtomicBoolean(false)
// ========== 重量识别相关变量已注释 ==========
// private var showRecognizePage = false
// private var pageVisible = true
// private var manualCancelFlag = false
// private var lastPhotoUri: Uri? = null
// private var lastWeight = 0.0
// private var currentWeight = 0.0
private var initialWeight = SpTool.configWeight.toDouble()
// private fun recognizeFood(weight: Double) {
// // 重量识别逻辑已注释
// }
/**
* 重新计算 list 中非接口数据(isOriginalData=false)的 materialType
*
* 规则:
* - 接口数据主材数量 n = list 中 isOriginalData=true 且 materialType=1 的数量
* - 有接口数据且 MATERIAL_COUNT - n <= 0:所有非接口数据均为辅材(2)
* - 无接口数据,或 MATERIAL_COUNT - n > 0m = MATERIAL_COUNT - n
* 非接口数据按 useWeight 从大到小排序,前 m 个为主材(1),其余为辅材(2)
*/
private fun updateMaterialTypes() {
val hasOriginalData = list.any { it.isOriginalData }
// 接口数据中主材数量
val n = list.count { it.isOriginalData && it.materialType == 1 }
// 待分配的非接口数据
val nonOriginalList = list.filter { !it.isOriginalData }
if (hasOriginalData && MATERIAL_COUNT - n <= 0) {
// 主材已满,所有非接口数据均为辅材
nonOriginalList.forEach { it.materialType = 2 }
} else {
// 剩余可分配主材名额
val m = MATERIAL_COUNT - n
// 按 useWeight 从大到小排序
val sorted = nonOriginalList.sortedByDescending { it.useWeight ?: 0.0 }
sorted.forEachIndexed { index, item ->
item.materialType = if (index < m) 1 else 2
}
}
materialAdapter.notifyDataSetChanged()
}
private fun openSubmitPage() {
getGoodsList()
if (goodsList.isNullOrEmpty()) {
toast("无菜品构成信息")
return
}
Log.d(TAG, "goToSubmit: goodsList:${goodsList?.toJsonString()}")
startActivity<SubmitFoodActivity> {
putExtra(SubmitFoodActivity.GOODS_LIST, goodsList as Serializable)
putExtra(SubmitFoodActivity.FOOD_ITEM, FoodRecord().apply {
foodId = mealItem?.foodId?.toString()
foodName = mealItem?.foodName
})
}
}
private fun getGoodsList() {
if (goodsList == null) {
goodsList = mutableListOf()
} else {
goodsList!!.clear()
}
// 将 Goods Item 转换为 CookFoodGoodsEntity 并补充菜品相关字段
list.filter { it.isSetFinished }.forEach {
goodsList?.add(it.toCookFoodGoodsEntity().apply {
foodId = mealItem?.foodId?.toString()
})
}
}
private fun showRemindDialog() {
CommonDialog(this)
.setTitle(getString(R.string.food_remind_01))
.setContent(getString(R.string.food_remind_02))
.setNegativeButton("返回调整")
.setPositiveButton("确认无误") { openSubmitPage() }
.setOnDismissCallback { hideStatusBar() }
.show()
}
private fun initObserver() {
lifecycleScope.launch {
netViewModel.foodDetailState.collect { state ->
when (state) {
is UiState.Success -> {
val detail = state.data
if (detail == null) {
toast("查询菜品信息为空")
return@collect
}
loadDishDetail(detail)
}
is UiState.Error -> toast(state.msg)
else -> {}
}
}
}
}
// ========== getDishDetail 方法已删除,不再通过接口查询 ==========
private fun loadDishDetail(detail: CookFoodDTO) {
val voList = detail.foodConstituteList
if (voList.isNullOrEmpty()) {
return
}
// 筛选出主材和辅材,转换为 Goods ItemuseWeight 置零)
val tempData = voList.filter { it.materialType == 1 || it.materialType == 2 }
.map { it.toGoodsItem().also { item -> item.useWeight = 0.0 } }
// 直接替换 adapter.items
list.clear()
list.addAll(tempData)
materialAdapter.items = list
materialAdapter.notifyDataSetChanged()
firstReqSize = list.size
}
/**
* 加载餐品食材构成(从 NutFoodOptionVO.compositions 解析)
*/
private fun loadDishCompositions(compositions: List<NutFoodComposition>) {
// 将 NutFoodComposition 转换为 Goods Item
val tempData = compositions.map { comp ->
GoodsItem().apply {
goodsId = comp.materId?.toString() ?: ""
goodsName = comp.ingredientName ?: ""
materialType = comp.isMain ?: 0
useWeight = 0.0
isOriginalData = true
isSetFinished = false
}
}.toMutableList()
list.clear()
list.addAll(tempData)
materialAdapter.items = list
materialAdapter.notifyDataSetChanged()
firstReqSize = list.size
}
private var firstReqSize = 0
private val list = mutableListOf<GoodsItem>()
private var clickIndex = -1
private val fields = mutableListOf<FormField>()
private val formAdapter by lazy {
FormFieldAdapter(fields)
}
private val materialAdapter by lazy {
FoodMaterialAdapter(list).apply {
onItemClick = onItemClick@{ positon ->
val item = getItem(positon) ?: return@onItemClick
this@PackActivity.clickIndex = positon
list.forEachIndexed { index, entity ->
entity.isClicked = index == positon
}
notifyDataSetChanged()
// binding.tvGoodsName.text = item.goodsName
// binding.tvMaterialType.text = when (item.materialType) {
// 1 -> "主材"
// 2 -> "辅材"
// else -> "未知"
// }
}
addOnItemChildClickListener(R.id.ivClearIcon) { _, _, position ->
val item = list.getOrNull(position) ?: return@addOnItemChildClickListener
Log.d(TAG, "onFoodItemClick: ${item.toJsonString()}")
if (item.isOriginalData) {
// 无重量信息时转为 item 整体点击(触发选中效果)
if (item.useWeight == null || item.useWeight == 0.0) {
onItemClick?.invoke(position)
return@addOnItemChildClickListener
}
deleteRemindDialog("清除确认", "确定清除食材「${item.goodsName}」添加的重量吗?") {
item.run {
isNewDishType = false
useWeight = 0.0
isSetFinished = false
}
notifyItemChanged(position)
}
return@addOnItemChildClickListener
}
deleteRemindDialog("删除确认", "确定删除食材「${item.goodsName}」吗?") {
removeFood(position)
}
}
}
}
private var packageMethodField: FormField? = null
private var deviceField: FormField? = null
private fun initFormList() {
packageMethodField =
FormField(
label = "包装方式", type = FieldType.DROPDOWN,
// extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
extraApiKeys = mapOf("packageMethod" to "type"),
required = true
)
fields.add(packageMethodField!!)
fields.add(
FormField(
label = "包装份数", type = FieldType.INTEGER,
apiKey = "quantity", required = true, hint = "请输入份数"
)
)
// fields.add(
// FormField(
// label = "包装日期",
// type = FieldType.DATE_PICKER,
// apiKey = "packageDate",
// required = true
// )
// )
fields.add(
FormField(
label = "保质期至",
type = FieldType.DATE_PICKER,
apiKey = "expiryDate",
required = true
)
)
fields.add(
FormField(
label = "存放温度(℃)",
type = FieldType.DECIMAL,
apiKey = "storageTemp",
required = false,
hint = "请输入小数"
)
)
fields.add(
FormField(
label = "包装人",
type = FieldType.TEXT,
apiKey = "operator",
required = false
)
)
deviceField = FormField(
label = "终端设备",
type = FieldType.DROPDOWN,
extraApiKeys = mapOf("deviceId" to "id"),
required = false
)
fields.add(deviceField!!)
fields.add(
FormField(
label = "操作类型", type = FieldType.DROPDOWN,
apiKey = "opType",
extraApiKeys = mapOf("opType" to "id"),
required = true,
options = listOf(
DictType(id = "1", value = "设备自动"),
DictType(id = "2", value = "人工录入")
).toMutableList()
)
)
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
loadDeviceOptionsWithCallback(deviceField)
}
/**
* 加载字典项下拉(回调版本),适合多次不同 dictType 的场景
*/
private fun loadDictItemsWithCallback(dictType: String, targetField: FormField) {
viewModelV2.getDictItemsWithCallback(
dictType = dictType,
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.dictLabel ?: "",
type = vo.dictCode ?: ""
)
} ?: emptyList()
targetField.options.clear()
targetField.options.addAll(options)
val index = fields.indexOf(targetField)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载字典项失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 加载设备下拉(回调版本),用于餐品净菜包装
*/
private fun loadDeviceOptionsWithCallback(deviceField: FormField? = null) {
val field = deviceField ?: return
viewModelV2.getDeviceOptionsWithCallback(
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.deviceName ?: "",
type = vo.deviceNo ?: ""
)
} ?: emptyList()
field.options.clear()
field.options.addAll(options)
val index = fields.indexOf(field)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载设备列表失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* RecyclerView初始化
*/
@SuppressLint("ClickableViewAccessibility")
private fun initRecyclerView() {
initFormList()
binding.rvFormList.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
it.adapter = formAdapter
}
binding.rvMaterialList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
// 仅 isNewDishType=true 的 item 显示侧滑删除菜单(必须在 setAdapter 之前调用)
// it.setSwipeMenuCreator { _, rightMenu, position ->
// if (list.getOrNull(position)?.isNewDishType == true) {
// rightMenu.addMenuItem(buildDeleteMenuItem())
// }
// }
// // 点击侧滑菜单项:先关闭菜单,再弹窗确认删除
// it.setOnItemMenuClickListener { menuBridge, position ->
// menuBridge.closeMenu()
// val item = list.getOrNull(position) ?: return@setOnItemMenuClickListener
// deleteRemindDialog("删除确认", "确定删除食材「${item.goodsName}」吗?") {
// removeFood(position)
// }
// }
// // item 点击事件
// it.setOnItemClickListener { _, position ->
// materialAdapter.onItemClick?.invoke(position)
// }
handleRvTouchHideKeyboard()
it.adapter = materialAdapter
}
}
/**
* 删除提醒
*/
private fun deleteRemindDialog(title: String, content: String, action: () -> Unit) {
CommonDialog(this@PackActivity)
.setTitle(title)
.setContent(content)
.setNegativeButton("取消")
.setPositiveButton("确认") {
action()
}.show()
}
/**
* 移除菜品
*/
private fun removeFood(position: Int) {
materialAdapter.removeAt(position)
toast("已删除")
updateMaterialTypes()
}
/**
* 构建侧滑删除菜单项
*/
private fun buildDeleteMenuItem(): SwipeMenuItem {
return SwipeMenuItem(this).apply {
setImage(R.drawable.ic_trash_white)
setBackground(R.drawable.bg_swipe_delete)
width = 160.dp
height = ViewGroup.LayoutParams.MATCH_PARENT
}
}
override fun onResume() {
super.onResume()
// pageVisible = true
// showRecognizePage = false
cameraUtils.bind()
}
override fun onPause() {
super.onPause()
// pageVisible = false
cameraUtils.unbind()
}
/**
* 初始化相机并绑定预览容器
* @param container 相机预览容器
*/
fun initCamera(container: ViewGroup) {
cameraUtils.initCamera()
val previewBinding = LayoutCameraPreviewBinding.inflate(layoutInflater, container)
cameraUtils.setPreviewController(previewBinding.previewView)
}
/**
* 注册返回键监听,替代已废弃的 onBackPressed()
*/
private fun addBackKeyListener() {
onBackPressedDispatcher.addCallback(this) {
if (SpTool.cookMode == 1) {
// 采集模式:有新增食材时提示保存
if (list.isEmpty().not()) {
saveDataRemindDialog()
return@addCallback
}
if (pageFrom == HOME) {
startActivity<SamplingModeActivity>()
}
} else {
// 包装模式:有未保存数据时提示
val count = list.count { !it.isOriginalData }
if (count > 0) {
saveDataRemindDialog()
return@addCallback
}
}
finish()
}
}
/**
* 未保存数据提醒
*/
private fun saveDataRemindDialog() {
CommonDialog(this)
.setTitle("返回提示")
.setContent("您好,当前页面存在未保存的数据,\n确认返回吗?")
.setNegativeButton("取消")
.setPositiveButton("确认") {
if (SpTool.cookMode == 1 && pageFrom == HOME) {
startActivity<SamplingModeActivity>()
}
finish()
}
.setOnDismissCallback { hideStatusBar() }
.show()
}
// ========== 拍照回调相关功能已注释 ==========
// private val cameraSuccessCallback: (Uri) -> Unit = { uri ->
// // 拍照成功回调已注释
// }
// private var notRecognizeDialog: CommonDialog? = null
// private fun showNotRecognizeDialog() {
// // 未识别弹窗已注释
// }
// private val cameraFailureCallback: (String) -> Unit = { errMsg ->
// // 拍照失败回调已注释
// }
// private fun loadRecognizeResultPage(nameScoreList: List<FoodModule.IdNameScore>) {
// // 识别结果页加载已注释
// }
/**
* 跳转食物搜索页(采集模式专用)
*/
private fun jumpSearch() {
val searchContent = binding.etInputDish.text.toString().trim()
if (searchContent.isBlank()) {
toast(binding.etInputDish.hint.toString())
return
}
val launchIntent = Intent(this, FoodSearchActivity::class.java).apply {
putExtra(FoodSearchActivity.FOOD_NAME, searchContent)
}
startActivity(launchIntent) { resultIntent ->
if (resultIntent == null) return@startActivity
val food = IntentCompat.getSerializableExtra(
resultIntent,
FoodSearchActivity.FOOD_ITEM,
FoodRecord::class.java
)
Log.d(TAG, "jumpSearch: record=$food")
if (food == null) return@startActivity
binding.etInputDish.run {
setText(food.foodName)
tag = food.foodName
setSelection(text.length)
}
}
hideKeyboard()
}
private fun handleRvTouchHideKeyboard() {
val gestureDetector =
GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
hideKeyboard()
return false
}
})
binding.rvMaterialList.addOnItemTouchListener(object :
RecyclerView.SimpleOnItemTouchListener() {
private var startX = 0f
private var startY = 0f
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
startX = e.x
startY = e.y
}
MotionEvent.ACTION_MOVE -> {
val dx = abs(e.x - startX)
val dy = abs(e.y - startY)
if (dx > dy && dx > 10) {
hideKeyboard()
}
}
}
gestureDetector.onTouchEvent(e)
return false
}
})
}
override fun onDestroy() {
super.onDestroy()
WeightUtil.removeWeightListener(TAG)
}
}
@@ -19,7 +19,9 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPrepareFoodBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.model.CookOrderCompositionItem
import com.shuwei.dish.match.model.FoodCompositionResponse
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.GoodsItem
@@ -34,6 +36,7 @@ import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast
@@ -53,6 +56,7 @@ class PrepareFoodActivity : BaseActivity() {
companion object {
const val TAG = "PrepareFoodActivity"
const val FOOD_ITEM = "foodItem"
const val COOK_ORDER_ID = "cookOrderId"
const val PAGE_FROM = "pageFrom"
const val HOME = "home"
const val WEIGHT_CHANGE_VALUE = 5
@@ -68,10 +72,19 @@ class PrepareFoodActivity : BaseActivity() {
private var food: FoodRecord? = null
/** 烹制单 ID,制作模式从菜品列表传入,用于调用新配比秤接口 */
private var cookOrderId: String? = null
/** 来源页面标识,HOME 时返回跳转 SamplingListActivity */
private var pageFrom: String? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null
/** 食材检索弹窗,手动新增构成时复用 */
private var foodSearchDialog: FoodSearchDialog? = null
/** 手动录入卡当前选中的食材(含 materId),点击搜索后回填 */
private var selectedIngredient: GoodsItem? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@@ -79,6 +92,7 @@ class PrepareFoodActivity : BaseActivity() {
pageFrom = intent.getStringExtra(PAGE_FROM)
//food = intent.extras?.getSerializable(FOOD_ITEM) as FoodRecord?
food = IntentCompat.getSerializableExtra(intent, FOOD_ITEM, FoodRecord::class.java)
cookOrderId = intent.getStringExtra(COOK_ORDER_ID)
val isSamplingMode = SpTool.cookMode == 1
val titleText = if (isSamplingMode) "菜品采集" else "菜品制作"
setTitleBar(titleBarAction = {
@@ -105,30 +119,40 @@ class PrepareFoodActivity : BaseActivity() {
binding.etInputDish.let {
it.setSelection(it.length())
}
// 采集模式:启用食材信息录入卡(手动新增构成)
initIngredientInput()
} else {
// 制作模式:不可编辑,仅展示菜名
binding.etInputDish.isFocusable = false
binding.etInputDish.isFocusableInTouchMode = false
binding.ivDishSearch.gone()
// 制作模式:不展示手动新增卡
binding.llIngredientInput.gone()
}
addViewClickListener()
addBackKeyListener()
initCamera(binding.flCameraContainer)
// 仅采样模式启用摄像头识别,制作模式不需要
// if (isSamplingMode) {
// initCamera(binding.flCameraContainer)
// }
initRecyclerView()
initObserver()
if (food?.foodId.isNullOrBlank().not()) {
// 制作模式优先用 cookOrderId 调新接口,采样模式回退到旧 foodId
if (!cookOrderId.isNullOrBlank()) {
getCookOrderComposition(cookOrderId!!)
} else if (food?.foodId.isNullOrBlank().not()) {
getDishDetail(food!!.foodId!!)
}
}
private fun addViewClickListener() {
// binding.tvShowWeight.setOnClickListener {
// showLoading("正在清零……")
// WeightUtil.tareTwo(AddressUtil.ONE)
// delayDismissLoading()
// }
binding.tvShowWeight.setOnClickListener {
showLoading("正在清零……")
WeightUtil.tareTwo(AddressUtil.ONE)
delayDismissLoading()
}
// // TODO: 测试功能-------------------------------
// binding.btnTestConfirm.clickWithDebounce {
// if (currentWeight <= 0.0) return@clickWithDebounce
@@ -152,13 +176,20 @@ class PrepareFoodActivity : BaseActivity() {
// }
// 净重 = 秤读数 - 初始读数
val netWeight = weight - initialWeight
currentWeight = netWeight
binding.tvShowWeight.run {
if (tag != netWeight) {
text = "称重:${netWeight}g"
tag = netWeight
}
}
recognizeFood(netWeight)
// 仅采样模式:用量自动回填秤重 + 触发食材识别
if (SpTool.cookMode == 1) {
val displayWeight =
if (netWeight > 0) netWeight.roundedOneDecimalPlace().toString() else ""
binding.etAmount.setText(displayWeight)
recognizeFood(netWeight)
}
}
}
})
@@ -304,8 +335,8 @@ class PrepareFoodActivity : BaseActivity() {
val hasOriginalData = list.any { it.isOriginalData }
// 接口数据中主材数量
val n = list.count { it.isOriginalData && it.materialType == 1 }
// 待分配的非接口数据
val nonOriginalList = list.filter { !it.isOriginalData }
// 待分配的非接口数据(跳过手动指定过主辅材的项)
val nonOriginalList = list.filter { !it.isOriginalData && !it.isMaterialTypeManual }
if (hasOriginalData && MATERIAL_COUNT - n <= 0) {
// 主材已满,所有非接口数据均为辅材
@@ -322,6 +353,79 @@ class PrepareFoodActivity : BaseActivity() {
materialAdapter.notifyDataSetChanged()
}
/**
* 食材信息录入卡初始化(仅采样模式):
* 搜索食材 → 选中回填名称并暂存 GoodsItem(含 materId)→ 选择主辅材 → 输入用量 → 添加
*/
private fun initIngredientInput() {
binding.ivIngredientSearch.setOnClickListener { openIngredientSearch() }
binding.etIngredientName.setOnClickListener { openIngredientSearch() }
binding.btnReset.setOnClickListener {
binding.etAmount.setText("")
}
binding.btnAdd.clickWithDebounce {
addManualIngredient()
}
}
/** 打开食材检索弹窗(食材名称不可手输,点击即搜索) */
private fun openIngredientSearch() {
if (foodSearchDialog?.isShowing == true) return
foodSearchDialog = FoodSearchDialog(
activity = this,
onItemSelected = { item -> onIngredientSelected(item) }
).also { it.show() }
}
/**
* 食材检索选中回调:回填名称,暂存 GoodsItemmaterId 用于提交)
*/
private fun onIngredientSelected(item: GoodsItem) {
selectedIngredient = item
binding.etIngredientName.setText(item.goodsName)
}
/**
* 手动添加食材到构成列表:名称(materId)+ 主辅材 + 用量(克,取自秤重)
*/
private fun addManualIngredient() {
val ingredient = selectedIngredient
if (ingredient == null || ingredient.materId.isNullOrBlank()) {
toast("请先搜索选择食材")
return
}
val amount = currentWeight.roundedOneDecimalPlace()
if (amount <= 0) {
toast("请先称重获取用量")
return
}
val type = if (binding.rbAux.isChecked) 2 else 1
val existing = list.firstOrNull { it.goodsId == ingredient.goodsId }
if (existing != null) {
// 已存在同食材:累加用量并同步主辅材
existing.useWeight = (existing.useWeight ?: 0.0) + amount
existing.materialType = type
existing.isMaterialTypeManual = true
existing.isSetFinished = true
existing.isNewDishType = true
} else {
ingredient.materialType = type
ingredient.useWeight = amount
ingredient.isMaterialTypeManual = true
ingredient.isNewDishType = true
ingredient.isOriginalData = false
ingredient.isSetFinished = true
ingredient.isClicked = false
list.add(ingredient)
binding.rvMaterialList.smoothScrollToPosition(list.size - 1)
}
// 清空录入区
selectedIngredient = null
binding.etIngredientName.setText("")
binding.etAmount.setText("")
updateMaterialTypes()
}
private fun openSubmitPage() {
getGoodsList()
if (goodsList.isNullOrEmpty()) {
@@ -332,6 +436,7 @@ class PrepareFoodActivity : BaseActivity() {
startActivity<SubmitFoodActivity> {
putExtra(SubmitFoodActivity.GOODS_LIST, goodsList as Serializable)
putExtra(SubmitFoodActivity.FOOD_ITEM, food as Serializable)
putExtra(SubmitFoodActivity.COOK_ORDER_ID, cookOrderId)
}
}
@@ -361,16 +466,17 @@ class PrepareFoodActivity : BaseActivity() {
private fun initObserver() {
// 新配比秤接口 observer(制作模式使用)
lifecycleScope.launch {
netViewModel.foodDetailState.collect { state ->
netViewModel.cookOrderCompositionState.collect { state ->
when (state) {
is UiState.Success -> {
val detail = state.data
if (detail == null) {
toast("查询菜品信息为空")
val items = state.data
if (items.isNullOrEmpty()) {
toast("查询菜品构成为空")
return@collect
}
loadDishDetail(detail)
loadDishComposition(items)
}
is UiState.Error -> toast(state.msg)
else -> {}
@@ -379,22 +485,74 @@ class PrepareFoodActivity : BaseActivity() {
}
}
/** 新接口:通过 foodId 查询菜品构成(采样模式称量模板) */
private fun getDishDetail(foodId: String) {
netViewModel.getFoodDetail(foodId)
netViewModel.getFoodComposition(
foodId = foodId,
onResult = { state ->
when (state) {
is UiState.Success -> {
val response = state.data
if (response == null) {
toast("查询菜品构成为空")
} else {
loadFoodComposition(response)
}
}
is UiState.Error -> toast(state.msg)
else -> {}
}
}
)
}
/** 新接口:通过 cookOrderId 查询菜品食材构成(制作模式使用) */
private fun getCookOrderComposition(cookOrderId: String) {
netViewModel.getCookOrderComposition(cookOrderId)
}
private fun loadDishDetail(detail: CookFoodDTO) {
// val voList = detail.stFoodInfoConstituteList
val voList = detail.foodConstituteList
if (voList.isNullOrEmpty()) {
return
/**
* 采样模式:将 food/{foodId}/composition 返回的构成映射为 GoodsItem 并刷新 UI
* 筛选主材(1)/辅材(2),调料(3)仍走槽位秤;useWeight 置零待重新称量
*/
private fun loadFoodComposition(response: FoodCompositionResponse) {
val tempData = response.ingredients
.filter { it.isMain == 1 || it.isMain == 2 }
.map {
GoodsItem(
goodsId = it.materId,
materId = it.materId,
goodsName = it.materName,
materialType = it.isMain,
useWeight = 0.0,
isOriginalData = true
)
}
list.clear()
list.addAll(tempData)
materialAdapter.items = list
materialAdapter.notifyDataSetChanged()
firstReqSize = list.size
}
/**
* 将新配比秤接口的食材构成列表映射为 GoodsItem 并刷新 UI
* 新接口天然仅返回主材(ingredientClass=1)和辅材(ingredientClass=2),不含调料
* useWeight 保留接口返回值,不再强制置零
*/
private fun loadDishComposition(items: List<CookOrderCompositionItem>) {
val tempData = items.map { item ->
GoodsItem(
goodsId = item.materId,
goodsName = item.ingredientName,
materId = item.materId,
materialType = item.ingredientClass,
useWeight = item.useWeight,
rawMaterialsType = item.vegTypeId,
isOriginalData = true
)
}
// 筛选出主材和辅材,转换为 GoodsItemuseWeight 置零)
val tempData = voList.filter { it.materialType == 1 || it.materialType == 2 }
.map { it.toGoodsItem().also { item -> item.useWeight = 0.0 } }
// 直接替换 adapter.items,确保 adapter 数据源被完全替换
// 同时同步外部 list,保证后续 updateMaterialTypes/removeFood 等操作正确
list.clear()
list.addAll(tempData)
materialAdapter.items = list
@@ -406,9 +564,15 @@ class PrepareFoodActivity : BaseActivity() {
private val list = mutableListOf<GoodsItem>()
private val materialAdapter by lazy {
FoodMaterialAdapter(list).apply {
onItemClick = { positon ->
onItemClick = { position ->
list.forEachIndexed { index, entity ->
entity.isClicked = index == positon
entity.isClicked = index == position
}
// 选中食材时同步当前秤重(制作/采样均支持)
val item = list.getOrNull(position)
if (item != null && currentWeight > 0) {
item.useWeight = currentWeight
item.isSetFinished = true
}
notifyDataSetChanged()
}
@@ -446,9 +610,9 @@ class PrepareFoodActivity : BaseActivity() {
private fun initRecyclerView() {
binding.rvMaterialList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
// 仅 isNewDishType=true 的 item 显示侧滑删除菜单(必须在 setAdapter 之前调用)
// 采样模式下所有食材均可侧滑删除(必须在 setAdapter 之前调用)
it.setSwipeMenuCreator { _, rightMenu, position ->
if (list.getOrNull(position)?.isNewDishType == true) {
if (SpTool.cookMode == 1) {
rightMenu.addMenuItem(buildDeleteMenuItem())
}
}
@@ -507,13 +671,17 @@ class PrepareFoodActivity : BaseActivity() {
super.onResume()
pageVisible = true
showRecognizePage = false
cameraUtils.bind()
if (SpTool.cookMode == 1) {
cameraUtils.bind()
}
}
override fun onPause() {
super.onPause()
pageVisible = false
cameraUtils.unbind()
if (SpTool.cookMode == 1) {
cameraUtils.unbind()
}
}
/**
@@ -13,11 +13,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FoodListAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySamplingModeBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.SampleHistoryItem
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.DateTimeUtil
import com.shuwei.dish.match.utils.ext.dp
@@ -251,12 +251,9 @@ class SamplingModeActivity : BaseActivity() {
@SuppressLint("NotifyDataSetChanged")
fun getSamplingList() {
netViewModel.getSamplingListWithCallback(
param = mutableMapOf(
"pageNum" to pageNo,
"pageSize" to pageSize,
"placeId" to BaseApp.canteenId
),
netViewModel.sampleHistory(
pageNum = pageNo,
pageSize = pageSize,
onLoading = { showLoading() }
) { state ->
when (state) {
@@ -264,14 +261,14 @@ class SamplingModeActivity : BaseActivity() {
// 回调期间用户可能已切换 Tab,需再次校验
if (binding.rbSamplingFinished.isChecked.not()) {
delayDismissLoading()
return@getSamplingListWithCallback
return@sampleHistory
}
loadDishList(state.data)
loadDishList(state.data?.map { it.toFoodRecord() }?.toMutableList())
}
is UiState.Error -> {
if (binding.rbSamplingFinished.isChecked.not()) {
delayDismissLoading()
return@getSamplingListWithCallback
return@sampleHistory
}
binding.refreshLayout.setEnableRefresh(true)
toast(state.msg)
@@ -284,6 +281,17 @@ class SamplingModeActivity : BaseActivity() {
}
}
/** 已提交采样列表条目 → FoodRecord 映射,供 FoodListAdapter(SAMPLING_MODE) 展示 */
private fun SampleHistoryItem.toFoodRecord(): FoodRecord = FoodRecord(
foodId = innovId,
foodName = foodName,
foodWeight = foodWeight,
cookMode = 1,
innovNo = innovNo,
status = status,
statusName = statusName
)
private fun finishRefresh() {
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
@@ -304,7 +312,7 @@ class SamplingModeActivity : BaseActivity() {
)
}
emptyViewBinding!!.tvContent.text = "暂无数据"
emptyViewBinding!!.tvSubContent.text = "今日暂无采样数据,点击下方按钮新增采样"
emptyViewBinding!!.tvSubContent.text = "暂无采样数据,点击下方按钮新增采样"
// 方案B:沿用 adapter stateView,使用 FrameLayout.LayoutParams + minimumHeight 避免空态贴顶
emptyViewBinding!!.root.layoutParams = FrameLayout.LayoutParams(
@@ -77,6 +77,7 @@ class SettingActivity : BaseActivity() {
private fun buildAllItems(): List<SettingItem> = listOf(
SettingItem(
type = SettingItem.Type.COOK_MODE,
title = "菜品模式",
subtitle = when (SpTool.cookMode) {
0 -> "当前:制作模式"
1 -> "当前:采样模式"
@@ -88,50 +89,26 @@ class SettingActivity : BaseActivity() {
),
SettingItem(
type = SettingItem.Type.SEASONING_CONFIG,
title = "调料区设置",
onClick = {
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.SEASONING_CONFIG)
}
),
SettingItem(
type = SettingItem.Type.FOOD_COLLECT,
title = "食材采集",
onClick = { checkCameraPermission() }
),
SettingItem(
type = SettingItem.Type.WEIGHT_CONFIG,
title = "减重配置",
onClick = {
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.WEIGHT_CONFIG)
}
),
SettingItem(
type = SettingItem.Type.CLEAN_PACKAGE,
onClick = {
startActivity<CleanPackActivity> {
putExtra(CleanPackActivity.EXTRA_TYPE, CleanPackActivity.TYPE_CLEAN)
}
}
),
SettingItem(
type = SettingItem.Type.MEAL_PACKAGE,
onClick = {
startActivity<MealListActivity> { }
}
),
SettingItem(
type = SettingItem.Type.GROUP_TASK,
onClick = {
startActivity<TaskActivity> { }
}
),
// SettingItem(
// type = SettingItem.Type.MEAL_PACKAGE_TEST,
// onClick = {
// startActivity<CleanPackActivity> {
// putExtra(CleanPackActivity.EXTRA_TYPE, CleanPackActivity.TYPE_MEAL)
// }
// }
// ),
SettingItem(
type = SettingItem.Type.DB_INSPECT,
title = "数据库查看",
isHidden = true,
onClick = {
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.DB_INSPECT)
@@ -139,21 +116,24 @@ class SettingActivity : BaseActivity() {
),
SettingItem(
type = SettingItem.Type.SCALE_OBSERVE,
title = "秤数据监控",
isHidden = true,
onClick = { startActivity<MasterScaleActivity>() }
),
SettingItem(
type = SettingItem.Type.ENV_SWITCH,
isHidden = true,
title = "切换环境",
onClick = { EnvSwitchDialog(this).show() }
),
SettingItem(
type = SettingItem.Type.CLEAR_DATA,
title = "数据清除",
isHidden = true,
onClick = { showClearDataDialog() }
),
SettingItem(
type = SettingItem.Type.TEST_SEASONING,
title = "测试调料拿取",
isHidden = true,
onClick = { startActivity<SubmitFoodActivity> { } }
),
@@ -21,12 +21,17 @@ import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.GoodsPreviewDialog
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.model.ConstituteSaveItem
import com.shuwei.dish.match.model.ConstituteSaveRequest
import com.shuwei.dish.match.model.CookCompleteItem
import com.shuwei.dish.match.model.CookCompleteRequest
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.toDTO
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.DateTimeUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString
@@ -38,6 +43,7 @@ import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.launch
import java.time.LocalDateTime
@SuppressLint("UseSparseArrays")
class SubmitFoodActivity : BaseActivity() {
@@ -46,6 +52,7 @@ class SubmitFoodActivity : BaseActivity() {
const val TAG = "SubmitFoodActivity"
const val FOOD_ITEM = "foodItem"
const val GOODS_LIST = "goodsList"
const val COOK_ORDER_ID = "cookOrderId"
/** 本地临时 foodId 前缀;以此开头说明菜品尚未在服务器登记,提交时需清空让服务器分配真实 ID */
const val LOCAL_ID_PREFIX = "localId_"
@@ -58,6 +65,9 @@ class SubmitFoodActivity : BaseActivity() {
private var food: FoodRecord? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null
/** 烹制单 ID,制作模式从 PrepareFoodActivity 传入,用于新配比秤烹饪完成接口 */
private var cookOrderId: String? = null
private val cookFoodEntity by lazy {
CookFoodEntity().apply {
food?.let {
@@ -105,6 +115,15 @@ class SubmitFoodActivity : BaseActivity() {
/** 主设备 2格秤的初始重量,首次收到数据时赋值;调用清零后重置为 0.0 */
private var initWeight: Double = 0.0
/** 烹饪开始时间(进入本页面的时刻),提交烹饪完成接口时上报 */
private var cookStartTime: String = ""
/**
* 调料首次使用时间,key = goodsNamevalue = 首次拿取时间 yyyy-MM-dd HH:mm:ss
* 秤首次检测到拿取(用量超阈值)时记录,提交时按 goodsName 匹配填入 putTime
*/
private val seasoningFirstUseMap = mutableMapOf<String, String>()
/** initWeight 是否已完成初始化 */
// private var initWeightSet: Boolean = false
@@ -113,10 +132,13 @@ class SubmitFoodActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivitySubmitFoodBinding.inflate(layoutInflater)
setContentView(binding.root)
// 以进入本页面的时刻作为烹饪开始时间
cookStartTime = DateTimeUtil.formatDateTime(LocalDateTime.now())
setHeaderBackground()
intent.extras?.apply {
food = getSerializable(FOOD_ITEM) as FoodRecord?
Log.d(TAG, "onCreate: cookMode=${food?.cookMode}")
cookOrderId = getString(COOK_ORDER_ID)
Log.d(TAG, "onCreate: cookMode=${food?.cookMode}, cookOrderId=$cookOrderId")
// isCooking=false:从前一页面带入主辅材数据;isCooking=true:从数据库查询
if (food?.isCooking == false) {
goodsList = getSerializable(GOODS_LIST) as MutableList<CookFoodGoodsEntity>?
@@ -273,6 +295,11 @@ class SubmitFoodActivity : BaseActivity() {
// 拿取场景:实时响应,无需等待稳定
val newTriple = Triple(goodsId, goodsName, useWeight)
if (rawWeightMap[key] != newTriple) {
// 该调料首次检测到拿取时,记录首次使用时间(同名调料只记最早一次)
if (!seasoningFirstUseMap.containsKey(goodsName)) {
seasoningFirstUseMap[goodsName] =
DateTimeUtil.formatDateTime(LocalDateTime.now())
}
Log.d(TAG, "observeScaleData,新增:$newTriple")
rawWeightMap[key] = newTriple
refreshAdapterByName(goodsName)
@@ -339,8 +366,10 @@ class SubmitFoodActivity : BaseActivity() {
}
binding.btnSubmit.clickWithDebounce {
if (cookFoodEntity.foodWeight <= 0.toDouble()) {
toast("未识别到菜品熟重")
return@clickWithDebounce
// 测试设备无秤硬件,使用模拟熟重 500g
cookFoodEntity.foodWeight = 500.0
binding.tvTotalWeight.text = "500.0"
binding.tvWeightUnit.text = ""
}
val content = getRemindSpannable("制作完成")
showRemindDialog(content) { submit() }
@@ -418,12 +447,24 @@ class SubmitFoodActivity : BaseActivity() {
lifecycleScope.launch {
// 先移除 goodsList 中已有的调料数据(materialType==3),再以 adapter 中最新调料覆盖
goodsList?.removeAll { it.materialType == 3 }
// 主辅材 foodId 统一以菜品 foodId 为准;新加调料行的 foodId 由 buildSeasoningEntity 内部设置
goodsList?.forEach { it.foodId = cookFoodEntity.foodId }
seasoningAdapter.items
.filter { it.useWeight > 0.0 }
.forEach { goodsList?.add(buildSeasoningEntity(it)) }
// 制作模式 + 有烹制单 ID → 调新配比秤烹饪完成接口
if (!cookOrderId.isNullOrBlank() && food?.cookMode == 0) {
submitWithNewApi()
return@launch
}
// 采样模式 → 调新配比秤提交采样接口(落库味养创新实验,状态=实验中)
if (food?.cookMode == 1) {
submitConstitute()
return@launch
}
// 旧接口路径(无烹制单 ID 的制作模式兜底)
cookFoodEntity.let {
it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList
it.dinnerType = when (it.dinnerType) {
@@ -434,18 +475,142 @@ class SubmitFoodActivity : BaseActivity() {
}
}
// 仅本地生成的临时 foodId 需清空,由服务器分配真实 ID;服务器下发的真实 foodId 直接保留
if (cookFoodEntity.foodId.startsWith(LOCAL_ID_PREFIX)) {
cookFoodEntity.foodId = ""
goodsList?.forEach { it.foodId = "" }
}
Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}")
// 接口提交使用 DTO,与本地 Room 实体解耦
netViewModel.submitCookFood(entity = cookFoodEntity.toDTO())
}
}
/**
* 新配比秤烹饪完成接口:从 goodsList 提取主辅材和调料,组装 CookCompleteRequest 提交
* 熟重 foodWeight(g) → cookedWeight(kg)
*/
private fun submitWithNewApi() {
val allGoods = goodsList ?: return
// 主辅材:materialType 1(主材) 或 2(辅材)
val items = allGoods
.filter { it.materialType == 1 || it.materialType == 2 }
.map { CookCompleteItem(
materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId,
actualQty = it.useWeight ?: 0.0
) }
if (items.isEmpty()) {
toast("无主辅材构成信息")
dismissLoading()
return
}
// 调料:materialType 3,按 goodsName 匹配首次使用时间填入 putTime
val seasonings = allGoods
.filter { it.materialType == 3 }
.map { CookCompleteItem(
materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId,
actualQty = it.useWeight ?: 0.0,
putTime = it.goodsName?.let { name -> seasoningFirstUseMap[name] }
) }
.takeIf { it.isNotEmpty() }
// 熟重 g → kg,保留 3 位小数
val cookedWeightKg = (cookFoodEntity.foodWeight / 1000.0).roundedDecimalPlace(3)
val request = CookCompleteRequest(
cookOrderId = cookOrderId ?: "",
cookStart = cookStartTime,
items = items,
cookedWeight = cookedWeightKg,
seasonings = seasonings
)
Log.d(TAG, "submitWithNewApi: ${request.toJsonString()}")
val isSamplingData = food?.cookMode == 1
netViewModel.cookOrderComplete(
request = request,
onResult = { state ->
when (state) {
is UiState.Success -> submitSuccess(isSamplingData)
is UiState.Error -> {
toast(state.msg)
dismissLoading()
}
else -> {}
}
}
)
}
/**
* 提交采样:从 goodsList 提取全部构成(主材/辅材/调料),组装 ConstituteSaveRequest 提交
* 落库味养创新实验;熟重 foodWeight 单位为 gisMain 直接取 materialType(1主/2辅/3调)
*/
private fun submitConstitute() {
val allGoods = goodsList ?: run {
toast("未获取到菜品或构成信息")
dismissLoading()
return
}
val foodName = cookFoodEntity.foodName ?: food?.foodName ?: ""
if (foodName.isBlank()) {
toast("菜品名称为空")
dismissLoading()
return
}
// 仅保留用量 > 0 的食材;materId 优先取 materId,为空时回退 goodsId
val items = allGoods
.filter { (it.useWeight ?: 0.0) > 0.0 }
.map {
ConstituteSaveItem(
materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId,
useWeight = it.useWeight ?: 0.0,
isMain = it.materialType
)
}
if (items.isEmpty()) {
toast("请完善菜品构成信息")
dismissLoading()
return
}
if (items.any { it.materId.isBlank() }) {
toast("菜品食材不能为空")
dismissLoading()
return
}
val foodWeight = cookFoodEntity.foodWeight
if (foodWeight <= 0.0) {
toast("熟重必须大于0")
dismissLoading()
return
}
val request = ConstituteSaveRequest(
foodName = foodName,
foodWeight = foodWeight,
items = items
)
Log.d(TAG, "submitConstitute: ${request.toJsonString()}")
netViewModel.constituteSave(
request = request,
onResult = { state ->
when (state) {
is UiState.Success -> submitSuccess(isSamplingData = true)
is UiState.Error -> {
toast(state.msg)
dismissLoading()
}
else -> {}
}
}
)
}
/**
* 将 adapter 中的调料 item 转换为 CookFoodGoodsEntity
* 从本地 dm_seasoning 表补充完整字段(popularName、zjmCode、materId、goodsCode 等)
@@ -1,212 +0,0 @@
package com.shuwei.dish.match.ui
import android.content.Intent
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.adapter.TaskAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivityTaskBinding
import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
/**
* 组配任务列表页面
*/
class TaskActivity : BaseActivity() {
companion object {
const val TAG = "TaskActivity"
const val TASK_ITEM = "taskItem"
}
private val binding by lazy { ActivityTaskBinding.inflate(layoutInflater) }
private val viewModelV2 by lazy { NetViewModelV2() }
private val taskList = mutableListOf<NutComboTaskVO>()
private val taskAdapter by lazy {
TaskAdapter(taskList).apply {
setOnItemClickListener { adapter, view, position ->
startActivity<TaskDetailActivity> {
putExtra(TaskDetailActivity.TASK_ITEM, taskList[position])
}
}
}
}
private var currentPage = 1
private var keyword: String? = null
private var isLoading = false
private var hasMore = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setHeaderBackground()
setTitleBar(titleBarAction = {
it.visible()
}, titleAction = {
it.text = "组配任务"
}, rightIconAction = {
it.gone()
}, backAction = {
it.setOnClickListener {
finish()
}
})
initRecyclerView()
initRefreshLayout()
initSearchListener()
}
override fun onResume() {
super.onResume()
currentPage = 1
loadTaskList()
}
/**
* 初始化 RecyclerView
*/
private fun initRecyclerView() {
binding.rvTaskList.layoutManager = LinearLayoutManager(this)
binding.rvTaskList.adapter = taskAdapter
taskAdapter.setOnItemClickListener { _, _, position ->
val item = taskList.getOrNull(position) ?: return@setOnItemClickListener
val intent = Intent(this, TaskDetailActivity::class.java).apply {
putExtra(TASK_ITEM, item)
}
startActivity(intent)
}
}
/**
* 初始化 SmartRefreshLayout 下拉刷新和上拉加载
*/
private fun initRefreshLayout() {
binding.refreshLayout.apply {
// 下拉刷新
setOnRefreshListener {
currentPage = 1
hasMore = true
loadTaskList()
}
// 上拉加载
setOnLoadMoreListener { refreshLayout ->
if (isLoading || !hasMore) {
refreshLayout.finishLoadMoreWithNoMoreData()
return@setOnLoadMoreListener
}
currentPage++
loadTaskList()
}
}
}
/**
* 搜索栏交互
*/
private fun initSearchListener() {
binding.ivSearch.clickWithDebounce {
performSearch()
}
binding.etInputKeyword.addOnActionSearchListener {
performSearch()
}
}
/**
* 执行搜索
*/
private fun performSearch() {
keyword = binding.etInputKeyword.text.toString().trim()
if (keyword.isNullOrBlank()) {
toast("请输入搜索关键字")
return
}
currentPage = 1
hasMore = true
taskList.clear()
taskAdapter.notifyDataSetChanged()
binding.rvTaskList.visible()
binding.tvEmpty.gone()
loadTaskList()
}
/**
* 加载组配任务列表
*/
private fun loadTaskList() {
isLoading = true
viewModelV2.getIncompleteComboTasksWithCallback(
deviceCode = GlobalData.deviceId,
keyword = keyword,
pageNum = currentPage,
pageSize = 10,
onLoading = {
if (currentPage == 1) {
showLoading("加载中……")
}
},
onResult = onResult@{ state ->
dismissLoading()
isLoading = false
when (state) {
is UiState.Success -> {
val records = state.data?.records
if (currentPage == 1 && records.isNullOrEmpty()) {
binding.rvTaskList.gone()
binding.tvEmpty.visible()
binding.refreshLayout.finishRefresh()
binding.refreshLayout.finishLoadMoreWithNoMoreData()
return@onResult
}
if (!records.isNullOrEmpty()) {
binding.rvTaskList.visible()
binding.tvEmpty.gone()
if (currentPage == 1) {
taskList.clear()
}
taskList.addAll(records)
taskAdapter.notifyDataSetChanged()
// 判断是否还有更多数据
if (records.size < 10) {
hasMore = false
binding.refreshLayout.finishLoadMoreWithNoMoreData()
}
} else {
hasMore = false
binding.refreshLayout.finishLoadMoreWithNoMoreData()
}
binding.refreshLayout.finishRefresh()
binding.refreshLayout.finishLoadMore()
}
is UiState.Error -> {
toast(state.msg)
if (currentPage > 1) {
currentPage--
}
if (currentPage == 1 && taskList.isEmpty()) {
binding.rvTaskList.gone()
binding.tvEmpty.visible()
}
binding.refreshLayout.finishRefresh()
binding.refreshLayout.finishLoadMore(false)
}
else -> {}
}
}
)
}
}
@@ -1,319 +0,0 @@
package com.shuwei.dish.match.ui
import android.os.Bundle
import android.util.Log
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.TaskDetailAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivityTaskDetailBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.InboundIngredientSearchDialog
import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskItemDetail
import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.launch
/**
* 组配任务详情页面
*/
class TaskDetailActivity : BaseActivity() {
companion object {
const val TAG = "TaskDetailActivity"
const val TASK_ITEM = "taskItem"
}
private var taskDetailVO: NutComboTaskDetailVO? = null
private val binding by lazy { ActivityTaskDetailBinding.inflate(layoutInflater) }
private val viewModelV2 by lazy { NetViewModelV2() }
private var taskItem: NutComboTaskVO? = null
private var clickIndex = -1
private val comboTaskList = mutableListOf<NutComboTaskItemDetail>()
private val itemAdapter by lazy {
TaskDetailAdapter(comboTaskList).apply {
setOnItemClickListener { adapter, view, position ->
clickItem(this, position)
}
addOnItemChildClickListener(R.id.ivClearIcon) { _, _, position ->
val item = getItem(position) ?: return@addOnItemChildClickListener
Log.d(PrepareFoodActivity.Companion.TAG, "onFoodItemClick: ${item.toJsonString()}")
// 无重量信息时转为 item 整体点击(触发选中效果)
if (item.actualQty.isNullOrBlank()) {
clickItem(this, position)
return@addOnItemChildClickListener
}
deleteRemindDialog("清除确认", "确定清除食材「${item.ingredientName}」添加的溯源码和重量吗?") {
item.run {
actualQty = null
traceCode = null
isSetFinished = false
}
notifyItemChanged(position)
}
}
}
}
/**
* 删除提醒
*/
private fun deleteRemindDialog(title: String, content: String, action: () -> Unit) {
CommonDialog(this)
.setTitle(title)
.setContent(content)
.setNegativeButton("取消")
.setPositiveButton("确认") {
action()
}.show()
}
private fun clickItem(adapter: TaskDetailAdapter, position: Int) {
adapter.getItem(position) ?: return
comboTaskList.forEachIndexed { index, detail ->
detail.isClicked = index == position
}
adapter.notifyDataSetChanged()
this@TaskDetailActivity.clickIndex = position
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setHeaderBackground()
taskItem = IntentCompat.getParcelableExtra(intent, TASK_ITEM, NutComboTaskVO::class.java)
setTitleBar(titleBarAction = {
it.visible()
}, titleAction = {
it.text = "任务详情"
}, rightIconAction = {
it.gone()
}, backAction = {
it.setOnClickListener {
finish()
}
})
initRecyclerView()
addWeightListener()
initView()
loadDetail()
lifecycleScope.launch {
viewModelV2.startComboTaskState.collect { state ->
when (state) {
is UiState.Success -> {
dismissLoading()
toast("已开始组配")
// 刷新详情
loadDetail()
// 更新本地状态
taskItem?.comboStatus = 1
}
is UiState.Error -> {
dismissLoading()
toast(state.msg)
}
else -> {}
}
}
}
}
/**
* 初始化任务概要信息
*/
private fun initView() {
taskItem?.let {
binding.tvTaskNo.text = "任务编号:${it.taskNo}"
binding.tvTargetDish.text = it.targetDish
binding.tvSpec.text = it.spec
binding.tvPlanDate.text = "计划日期:${it.planDate}"
binding.tvPortions.text = "总份数:${it.portions}"
binding.tvOwnerName.text = "负责人:${it.ownerName}"
binding.tvStatus.text = getStatusText(it.comboStatus)
// 仅待组配状态显示"开始组配"按钮
// if (it.comboStatus == 0) {
// binding.btnStart.visible()
binding.btnStart.clickWithDebounce {
//val taskId = it.id ?: return@clickWithDebounce
startComboTask()
}
// } else {
// binding.btnStart.gone()
// }
}
binding.btnGetTraceCode.clickWithDebounce {
val item = itemAdapter.getItem(clickIndex) ?: return@clickWithDebounce
InboundIngredientSearchDialog(
activity = this,
viewModelV2 = viewModelV2,
defIngredientName = item.ingredientName
) { vo ->
itemAdapter.getItem(clickIndex)?.let {
it.traceCode = vo.traceCode
itemAdapter.notifyItemChanged(clickIndex)
}
}.show()
}
binding.ivAddWeight.clickWithDebounce {
if (currentWeight <= 0.0) {
toast("当前秤重量无效")
return@clickWithDebounce
}
itemAdapter.getItem(clickIndex)?.let {
it.actualQty = currentWeight.toString()
it.isSetFinished = true
itemAdapter.notifyItemChanged(clickIndex)
}
}
binding.ivClearWeight.clickWithDebounce {
initialWeight = 0.0
SpTool.configWeight = 0
WeightUtil.tareTwo(1)
}
}
private var currentWeight: Double = 0.0
/** 初始秤读数,净重 = 秤读数 - initialWeight,从 SpTool.configWeight 读取 */
private var initialWeight = SpTool.configWeight.toDouble()
private fun addWeightListener() {
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread {
currentWeight = weight - initialWeight
binding.tvShowWeight.run {
if (tag != currentWeight) {
// text = "称重:${netWeight}g"
val weightText = "${currentWeight.roundedOneDecimalPlace()}g"
text = weightText
tag = currentWeight
}
}
//recognizeFood(netWeight)
}
}
})
}
/**
* 初始化 RecyclerView
*/
private fun initRecyclerView() {
binding.rvItemList.layoutManager = LinearLayoutManager(this)
binding.rvItemList.adapter = itemAdapter
}
/**
* 加载任务详情(食材清单)
*/
private fun loadDetail() {
val taskId = taskItem?.id ?: return
viewModelV2.getComboTaskDetailWithCallback(
id = taskId,
onLoading = {
showLoading("加载中……")
},
onResult = onResult@{ state ->
dismissLoading()
when (state) {
is UiState.Success -> {
taskDetailVO = state.data
if (taskDetailVO == null) {
toast("详情为空")
return@onResult
}
// 更新状态(可能已被其他设备修改)
binding.tvStatus.text = getStatusText(taskDetailVO!!.comboStatus)
if (taskDetailVO!!.comboStatus != 0) {
binding.btnStart.gone()
}
// 加载食材列表
val items = taskDetailVO!!.items
if (!items.isNullOrEmpty()) {
comboTaskList.clear()
comboTaskList.addAll(items)
itemAdapter.notifyDataSetChanged()
} else {
toast("该任务无食材信息")
}
}
is UiState.Error -> toast(state.msg)
else -> {}
}
}
)
}
/**
* 开始组配
*/
private fun startComboTask() {
val task = taskDetailVO ?: return
val count = comboTaskList.count { it.traceCode.isNullOrBlank() }
if (count > 0) {
toast("存在未设置的溯源码")
return
}
task.deviceCode = GlobalData.deviceId
task.portions = 1
showLoading("正在开始组配……")
lifecycleScope.launch {
//viewModelV2.startComboTask(taskId)
task.items = comboTaskList
viewModelV2.addComboTask(param = task) { state, msg ->
dismissLoading()
if (state.not()) {
toast(msg)
return@addComboTask
}
toast("组配成功")
finish()
}
}
}
/**
* 状态文本映射
*/
private fun getStatusText(status: Int?): String {
return when (status) {
0 -> "待组配"
1 -> "组配中"
2 -> "已完成"
else -> "未知"
}
}
override fun onDestroy() {
super.onDestroy()
WeightUtil.removeWeightListener(TAG)
}
}
@@ -17,6 +17,9 @@ import com.shuwei.dish.match.databinding.FragmentFoodListBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.model.CookOrderItem
import com.shuwei.dish.match.model.CookOrderMealGroup
import com.shuwei.dish.match.model.CookOrderPageRequest
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.ui.CookingModeActivity
@@ -64,6 +67,12 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
private lateinit var activity: CookingModeActivity
/** 首次加载标记,首次请求时传 mealType=null 获取全餐次数据,后续切换传具体餐次 */
private var isFirstLoad = true
/** foodId → cookOrderId 映射,用于跳转 PrepareFoodActivity 时传递烹制单 ID */
private val cookOrderIdMap = mutableMapOf<String, String>()
/** 第一页时先查数据库存入此字段,网络结果回来后直接使用,避免时序问题 */
private var pendingLocalList: MutableList<CookFoodEntity>? = null
@@ -87,12 +96,14 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
// 烹饪中,跳到称熟重页面
activity.startActivity<SubmitFoodActivity> {
putExtra(SubmitFoodActivity.FOOD_ITEM, item as Serializable)
putExtra(SubmitFoodActivity.COOK_ORDER_ID, cookOrderIdMap[item.foodId])
}
return
}
item.dinnerType = dinnerType
activity.startActivity<PrepareFoodActivity> {
putExtra(PrepareFoodActivity.FOOD_ITEM, item as Serializable)
putExtra(PrepareFoodActivity.COOK_ORDER_ID, cookOrderIdMap[item.foodId])
}
}
@@ -212,42 +223,44 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
currentFoodName = foodName
currentJob?.cancel()
currentJob = viewLifecycleOwner.lifecycleScope.launch {
val param = mutableMapOf<String, Any>(
"pageNum" to pageNo,
"pageSize" to pageSize,
"placeId" to BaseApp.canteenId,
"dinnerType" to getDinnerTypeText()
// 首次加载传 mealType=null 获取所有餐次,切换 Tab 时传当前餐次精确请求
val mealType = if (isFirstLoad) null else when (dinnerType) {
"1", "2", "3", "4" -> dinnerType.toInt()
else -> null // "0" 或非法值不传,由后端返回全部餐次
}
val request = CookOrderPageRequest(
mealType = mealType,
keyword = foodName
)
foodName?.let { param["foodName"] = it }
if (pageNo == 1) {
pendingLocalList = activity.dbViewModel.getCookFoodListDirect(
cookMode = 0,
dinnerType = dinnerType
)
}
val localList = if (pageNo == 1) pendingLocalList else null
netViewModel.searchFoodListWithCallback(
param = param,
val localList = pendingLocalList
netViewModel.getCookOrderPageWithCallback(
request = request,
onLoading = { activity.showLoading(timeoutMs = 35_000L) }
) { result ->
when (result) {
is UiState.Success -> {
if (isAdded.not()) return@searchFoodListWithCallback
if (isAdded.not()) return@getCookOrderPageWithCallback
Log.d(TAG, "getDishList, UiState.Success")
isFirstLoad = false
activity.delayDismissLoading()
finishRefresh()
loadAndMergeDishList(result.data, localList)
loadAndMergeCookOrders(result.data, localList)
}
is UiState.Error -> {
if (isAdded.not()) return@searchFoodListWithCallback
if (isAdded.not()) return@getCookOrderPageWithCallback
Log.d(TAG, "getDishList, UiState.Error")
isFirstLoad = false
toast(result.msg)
finishRefresh()
activity.delayDismissLoading()
if (pageNo == 1) {
if (localList.isNullOrEmpty()) loadEmptyView()
else loadAndMergeDishList(null, localList)
}
if (localList.isNullOrEmpty()) loadEmptyView()
else loadAndMergeCookOrders(null, localList)
}
else -> {}
}
@@ -287,36 +300,65 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
}
private fun finishRefresh() {
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
} else {
binding.refreshLayout.finishLoadMore(1200)
}
binding.refreshLayout.finishRefresh(1200)
}
/**
* 将网络数据与本地烹饪中数据合并后渲染列表
* 将 CookOrderItem 映射为 FoodRecord,适配现有 Adapter
* rawWeight 单位 kg → totalWeight 单位 g
*/
private fun CookOrderItem.toFoodRecord(): FoodRecord {
return FoodRecord(
foodId = foodId,
foodName = dishName,
totalWeight = ((cookedWeight ?: 0.0) * 1000),
foodWeight = ((cookedWeight ?: 0.0) * 1000),
count = cookedPortions,
cookMode = 0,
isCooking = cookStatus == 1,
dinnerType = mealType.toString(),
isOriginalData = true
)
}
/**
* 将新接口的餐次分组响应与本地烹饪中数据合并后渲染列表
* 从分组中按当前 dinnerType 筛选对应餐次的 cookOrders,映射为 FoodRecord 后合并本地数据
*/
@SuppressLint("NotifyDataSetChanged")
private fun loadAndMergeDishList(
records: MutableList<FoodRecord>?,
private fun loadAndMergeCookOrders(
groups: List<CookOrderMealGroup>?,
localList: MutableList<CookFoodEntity>?
) {
try {
if (isAdded.not()) return
val effectiveLocalList = if (pageNo == 1) localList else null
if (records.isNullOrEmpty() && effectiveLocalList.isNullOrEmpty()) {
if (pageNo == 1) loadEmptyView()
val targetMealType = dinnerType.toIntOrNull() ?: return
// 从分组中筛选当前餐次的烹制单列表
val cookOrders = groups
?.firstOrNull { it.mealType == targetMealType }
?.cookOrders
// 更新 foodId → cookOrderId 映射(从全量分组构建,跨餐次)
groups?.forEach { group ->
group.cookOrders.forEach { order ->
cookOrderIdMap[order.foodId] = order.cookOrderId
}
}
if (cookOrders.isNullOrEmpty() && localList.isNullOrEmpty()) {
loadEmptyView()
finishRefresh()
binding.refreshLayout.setEnableLoadMore(false)
return
}
if (pageNo == 1) list.clear()
list.clear()
val mergedList = records?.toMutableList() ?: mutableListOf()
mergeCookingItems(mergedList, effectiveLocalList)
deduplicateForPaging(mergedList, localList)
renderList(mergedList, records)
val mergedList = cookOrders?.map { it.toFoodRecord() }?.toMutableList() ?: mutableListOf()
mergeCookingItems(mergedList, localList)
list.addAll(mergedList)
dishAdapter.notifyDataSetChanged()
binding.refreshLayout.setEnableRefresh(true)
binding.refreshLayout.setEnableLoadMore(false)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -354,29 +396,4 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
}
mergedList.addAll(0, cookingItems)
}
/**
* 翻页时从网络结果中剔除第一页已加载的本地烹饪中条目,避免重复出现
*/
private fun deduplicateForPaging(
mergedList: MutableList<FoodRecord>,
localList: List<CookFoodEntity>?
) {
if (pageNo < 2 || localList.isNullOrEmpty()) return
val localIds = localList.map { it.foodId }.toHashSet()
mergedList.removeAll { it.foodId in localIds }
}
/**
* 将合并结果追加到列表并刷新 UI,按网络返回条数决定是否开启加载更多
*/
@SuppressLint("NotifyDataSetChanged")
private fun renderList(mergedList: List<FoodRecord>, records: List<FoodRecord>?) {
list.addAll(mergedList)
dishAdapter.notifyDataSetChanged()
val isLoadMoreEnable = (records?.size ?: 0) >= pageSize
binding.refreshLayout.setEnableRefresh(true)
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) pageNo++
}
}
@@ -9,7 +9,6 @@ import java.util.Date
object DateTimeUtil {
const val YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"
const val YYYY_MM_DD = "yyyy-MM-dd"
fun formatDateTime(dateTime: LocalDateTime, pattern: String = YYYY_MM_DD_HH_MM_SS): String {
val formatter = DateTimeFormatter.ofPattern(pattern)
@@ -26,8 +26,8 @@ public class Weigher2 {
public static final int ERR_004 = 1004;
public static final int ERR_PCB_NOT_SUPPORT = 2000;
private static FixedSensorScale mSensorScale;
private static String mDevicePort = "/dev/ttyS4";
// private static String mDevicePort = "/dev/ttyS7";
// private static String mDevicePort = "/dev/ttyS4";
private static String mDevicePort = "/dev/ttyS7";
private static boolean mConnect = false;
private static Listener mListener;
@@ -10,7 +10,6 @@ import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
@@ -162,21 +161,3 @@ fun Double.roundedOneDecimalPlace(): Double {
fun ImageView.load(url: Any?) {
GlideUtils.loadImage(context, url, this)
}
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0)
this.clearFocus() // 清除焦点避免键盘再次弹出
}
/**
* 格式化用量字符串,去掉小数点后末尾的0
*/
fun String?.formatDecimal(): String {
if (this.isNullOrBlank()) return "0"
return try {
BigDecimal(this).stripTrailingZeros().toPlainString()
} catch (e: Exception) {
this
}
}
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 从上到下:浅蓝(占比多)→ 深蓝,angle=270 表示从上到下 -->
<gradient
android:angle="270"
android:startColor="#88BBFF"
android:centerColor="#1A6EFF"
android:centerX="0.65"
android:endColor="#0032C8"
android:type="linear" />
<corners android:radius="16dp" />
</shape>
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 从上到下:浅橙(占比多)→ 深橙,angle=270 表示从上到下 -->
<gradient
android:angle="270"
android:startColor="#FFD08A"
android:centerColor="#FF9632"
android:centerX="0.65"
android:endColor="#CC6400"
android:type="linear" />
<corners android:radius="16dp" />
</shape>
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 从上到下:浅红(占比多)→ 深红,angle=270 表示从上到下 -->
<gradient
android:angle="270"
android:startColor="#FFB3B3"
android:centerColor="#E53935"
android:centerX="0.65"
android:endColor="#CC0000"
android:type="linear" />
<corners android:radius="16dp" />
</shape>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFDCDCF0"/>
<corners android:radius="10dp"/>
</shape>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:color="#FFDCDCF0" android:width="1dp"/>
</shape>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="10dp"/>
<stroke android:color="#FFD5E1FF" android:width="2dp"/>
</shape>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="10dp"/>
<stroke android:color="#FFDCDCF0" android:width="2dp"/>
</shape>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="#99FFFFFF"/>
</shape>
</item>
</ripple>
@@ -1,144 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@drawable/bg_other_page"
tools:ignore="HardcodedText">
<FrameLayout
android:id="@+id/flSearchBar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="30dp"
android:background="@drawable/shape_white_30_corners"
android:clipToOutline="true"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="80dp"
android:layout_gravity="center"
android:layout_marginHorizontal="28dp"
android:background="@color/white"
android:ellipsize="end"
android:gravity="center"
android:hint="搜索菜品名称"
android:maxLines="1"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:textColor="@color/black333"
android:textColorHint="@color/gray_c8"
android:textSize="40sp"
tools:ignore="TextFields" />
<ImageView
android:id="@+id/ivDishSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:src="@drawable/ic_search_gray"
android:visibility="visible"
tools:ignore="ContentDescription" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_marginHorizontal="30dp"
android:background="@drawable/shape_white_30_corners"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="用量(克)"
android:textColor="@color/black666"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvShowWeight"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginStart="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_f6_10_corners"
android:gravity="center"
android:text="-"
android:textColor="@color/gray_d6"
android:textSize="50sp" />
<ImageView
android:id="@+id/ivClearWeight"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginHorizontal="30dp"
android:src="@drawable/ic_weight_clear" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<TextView
android:id="@+id/btnTestConfirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="15dp"
android:layout_marginStart="20dp"
android:paddingVertical="10dp"
android:text="净菜包信息"
android:textColor="@color/black666"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvForm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:layout_marginHorizontal="14dp"
android:layout_marginBottom="20dp"
app:layoutManager="GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_form_field" />
</LinearLayout>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_margin="30dp"
android:background="@drawable/shape_green_bg"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center"
android:text="确认提交"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold" />
</LinearLayout>
@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
tools:background="@drawable/bg_other_page">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="30dp"
android:background="@drawable/shape_white_30_corners"
android:gravity="center_vertical"
android:orientation="horizontal">
<EditText
android:id="@+id/etInputIngredient"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="28dp"
android:layout_weight="1"
android:autofillHints=""
android:background="@color/white"
android:gravity="center"
android:hint="@string/dish_search_hint2"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:inputType="text"
android:imeOptions="actionSearch"
android:textColor="@color/black333"
android:textColorHint="@color/gray_c8"
android:textSize="40sp"
tools:ignore="TextFields" />
<ImageView
android:id="@+id/ivIngredientSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:src="@drawable/ic_search_gray"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="40dp"
android:layout_marginBottom="40dp"
android:text="食材列表"
android:textColor="@color/black666"
android:textSize="28sp" />
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvIngredientList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:overScrollMode="never"
tools:listitem="@layout/list_item_inbound_ingredient" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
</LinearLayout>
@@ -1,60 +0,0 @@
<?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="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:background="@color/bg_color"
tools:ignore="HardcodedText">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="300dp"
android:text="配比秤"
android:textColor="@color/home_sub_title"
android:textSize="50sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnCleanPack"
android:layout_width="500dp"
android:layout_height="300dp"
android:layout_marginTop="60dp"
android:text="净菜包"
android:textColor="@color/white"
android:textSize="35sp"
android:textStyle="bold"
android:gravity="center"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/bg_gradient_blue_btn"/>
<TextView
android:id="@+id/btnMealPack"
android:layout_width="500dp"
android:layout_height="300dp"
android:layout_marginTop="60dp"
android:gravity="center"
android:text="餐品净菜包"
android:textColor="@color/white"
android:textSize="35sp"
android:textStyle="bold"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/bg_gradient_orange_btn"/>
<TextView
android:id="@+id/btnTask"
android:layout_width="500dp"
android:layout_height="300dp"
android:layout_marginTop="60dp"
android:gravity="center"
android:text="组配任务"
android:textColor="@color/white"
android:textSize="35sp"
android:textStyle="bold"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/bg_gradient_red_btn"/>
</LinearLayout>
@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@drawable/bg_other_page">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:padding="10dp"
android:text="餐品列表"
android:textColor="@color/black666"
android:textSize="28sp" />
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMealList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:overScrollMode="never"
android:scrollbars="vertical"
tools:itemCount="3"
tools:listitem="@layout/list_item_food2" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
</LinearLayout>
-205
View File
@@ -1,205 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@drawable/bg_other_page">
<LinearLayout
android:id="@+id/llSearchBar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginHorizontal="30dp"
android:layout_marginVertical="20dp"
android:background="@drawable/shape_white_30_corners"
android:gravity="center_vertical"
android:orientation="horizontal">
<EditText
android:id="@+id/etInputDish"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginHorizontal="28dp"
android:layout_weight="1"
android:autofillHints=""
android:background="@color/white"
android:ellipsize="end"
android:gravity="center"
android:hint="@string/dish_search_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:textColor="@color/black333"
android:textColorHint="@color/gray_c8"
android:textSize="40sp"
tools:ignore="TextFields" />
<ImageView
android:id="@+id/ivDishSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:src="@drawable/ic_search_gray"
android:visibility="gone"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFormList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="10dp"
android:overScrollMode="never" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginTop="20dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="140dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="用量(克)"
android:textColor="@color/black666"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvShowWeight"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginStart="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_f6_10_corners"
android:gravity="center"
android:text="-"
android:textColor="@color/gray_d6"
android:textSize="50sp" />
<ImageView
android:id="@+id/ivClearWeight"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginStart="30dp"
android:src="@drawable/ic_weight_clear" />
<ImageView
android:id="@+id/ivAddWeight"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:src="@drawable/ic_weight_add" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="10dp"
android:text="在下方列表中选择食材计重"
android:textColor="@color/black999"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="30dp"
android:layout_marginTop="20dp"
android:layout_weight="1"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginVertical="20dp"
android:layout_marginStart="20dp"
android:padding="10dp"
android:text="@string/dish_composition"
android:textColor="@color/black666"
android:textSize="28sp" />
<TextView
android:id="@+id/btnGetTraceCode"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="30dp"
android:background="@drawable/ripple_effect_light"
android:gravity="center"
android:paddingHorizontal="20dp"
android:text="获取溯源码"
android:textColor="@color/dish_green"
android:textSize="18sp" />
<FrameLayout
android:id="@+id/flCameraContainer"
android:layout_width="1dp"
android:layout_height="1dp" />
</FrameLayout>
<!-- com.yanzhenjie.recyclerview.SwipeRecyclerView-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMaterialList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:overScrollMode="never"
android:scrollbars="vertical"
tools:itemCount="3"
tools:listitem="@layout/list_item_food_material" />
</LinearLayout>
<TextView
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginVertical="20dp"
android:layout_marginHorizontal="30dp"
android:background="@drawable/shape_green_bg"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center"
android:text="提交"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold" />
</LinearLayout>
@@ -51,6 +51,178 @@
</LinearLayout>
<!-- 食材信息录入卡:手动新增菜品构成 -->
<LinearLayout
android:id="@+id/llIngredientInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginBottom="30dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical"
android:padding="20dp">
<!-- 食材名称 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dish_name"
android:textColor="@color/black666"
android:textSize="28sp" />
<EditText
android:id="@+id/etIngredientName"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:autofillHints=""
android:background="@drawable/shape_white_f6_10_corners"
android:clickable="true"
android:ellipsize="end"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical"
android:hint="@string/click_search_ingredient"
android:maxLines="1"
android:paddingHorizontal="20dp"
android:textColor="@color/black333"
android:textColorHint="@color/gray_c8"
android:textSize="30sp"
tools:ignore="TextFields" />
<ImageView
android:id="@+id/ivIngredientSearch"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginStart="16dp"
android:padding="20dp"
android:src="@drawable/ic_search_green"
tools:ignore="ContentDescription" />
</LinearLayout>
<!-- 主辅材 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dish_from_type"
android:textColor="@color/black666"
android:textSize="28sp" />
<RadioGroup
android:id="@+id/rgMainAux"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbMain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/white"
android:button="@null"
android:checked="true"
android:drawablePadding="20dp"
android:drawableStart="@drawable/selector_dish_type"
android:gravity="center"
android:paddingHorizontal="30dp"
android:text="@string/dish_type_first"
android:textColor="@color/dish_type_font"
android:textSize="30sp"
android:textStyle="bold" />
<RadioButton
android:id="@+id/rbAux"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/white"
android:button="@null"
android:drawablePadding="20dp"
android:drawableStart="@drawable/selector_dish_type"
android:gravity="center"
android:paddingHorizontal="30dp"
android:text="@string/dish_type_second"
android:textColor="@color/dish_type_font"
android:textSize="30sp"
android:textStyle="bold" />
</RadioGroup>
</LinearLayout>
<!-- 用量(克) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dish_part_weight_remind"
android:textColor="@color/black666"
android:textSize="28sp" />
<EditText
android:id="@+id/etAmount"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:autofillHints=""
android:background="@drawable/shape_white_f6_10_corners"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical"
android:hint="@string/weight_from_scale"
android:maxLines="1"
android:paddingHorizontal="20dp"
android:textColor="@color/black333"
android:textColorHint="@color/gray_c8"
android:textSize="30sp"
tools:ignore="TextFields" />
<TextView
android:id="@+id/btnReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:padding="10dp"
android:text="@string/clear_zero"
android:textColor="@color/red_ff4444"
android:textSize="28sp"
android:visibility="gone" />
<TextView
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="70dp"
android:layout_marginStart="16dp"
android:background="@drawable/shape_green_bg"
android:gravity="center"
android:paddingHorizontal="40dp"
android:text="@string/add"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
-104
View File
@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@drawable/bg_other_page">
<LinearLayout
android:id="@+id/llSearchBar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="30dp"
android:background="@drawable/shape_white_30_corners"
android:gravity="center_vertical"
android:orientation="horizontal">
<EditText
android:id="@+id/etInputKeyword"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginHorizontal="28dp"
android:layout_weight="1"
android:autofillHints=""
android:background="@color/white"
android:gravity="center"
android:hint="请输入菜品名称"
android:inputType="text"
android:imeOptions="actionSearch"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/black333"
android:textColorHint="@color/gray_c8"
android:textSize="40sp"
tools:ignore="TextFields" />
<ImageView
android:id="@+id/ivSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:src="@drawable/ic_search_gray"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTaskList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:overScrollMode="never"
android:scrollbars="vertical"
tools:itemCount="3"
tools:listitem="@layout/list_item_task" />
<TextView
android:id="@+id/tvEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="暂无数据"
android:textColor="@color/black999"
android:textSize="26sp"
android:visibility="gone" />
</FrameLayout>
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
</LinearLayout>
@@ -1,225 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@drawable/bg_other_page">
<LinearLayout
android:id="@+id/llTaskInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="30dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tvTaskNo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:textSize="20sp"
tools:text="任务编号:RW20260601001" />
<TextView
android:id="@+id/tvTargetDish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="@color/black"
android:textSize="32sp"
android:textStyle="bold"
tools:text="红烧肉" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvSpec"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black666"
android:textSize="24sp"
tools:text="中组(8份)·50组" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/dish_green"
android:textSize="24sp"
tools:text="待组配" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPlanDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:textSize="20sp"
tools:text="计划日期:2026-06-01" />
<TextView
android:id="@+id/tvPortions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="@color/black999"
android:textSize="20sp"
tools:text="总份数:400" />
<TextView
android:id="@+id/tvOwnerName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="@color/black999"
android:textSize="20sp"
tools:text="负责人:张三" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="160dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="用量(克)"
android:textColor="@color/black666"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvShowWeight"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginStart="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_f6_10_corners"
android:gravity="center"
android:text="-"
android:textColor="@color/gray_d6"
android:textSize="50sp" />
<ImageView
android:id="@+id/ivClearWeight"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginStart="30dp"
android:src="@drawable/ic_weight_clear" />
<ImageView
android:id="@+id/ivAddWeight"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:src="@drawable/ic_weight_add" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="30dp"
android:text="在下方列表中选择食材计重"
android:textColor="@color/black999"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="30dp"
android:layout_marginTop="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:padding="10dp"
android:text="食材明细"
android:textColor="@color/black666"
android:textSize="28sp" />
<TextView
android:id="@+id/btnGetTraceCode"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginEnd="30dp"
android:background="@drawable/ripple_effect_light"
android:gravity="center"
android:paddingHorizontal="20dp"
android:text="获取溯源码"
android:textColor="@color/dish_green"
android:textSize="18sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvItemList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:overScrollMode="never"
android:layout_marginHorizontal="20dp"
android:scrollbars="vertical"
tools:itemCount="3"
tools:listitem="@layout/list_item_task_detail" />
</LinearLayout>
<TextView
android:id="@+id/btnStart"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_margin="30dp"
android:background="@drawable/shape_green_bg"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center"
android:text="完成组配"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold"
android:visibility="visible" />
</LinearLayout>
@@ -1,98 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
tools:background="@color/white"
android:orientation="vertical">
<View
android:id="@+id/viewLine"
android:layout_width="90dp"
android:layout_height="10dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_gray_dc_5" />
<TextView
android:id="@+id/tvSheetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:textColor="@color/black333"
android:textSize="36sp"
android:textStyle="bold"
tools:text="已入库净菜检索" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginStart="60dp"
android:layout_marginTop="50dp"
android:layout_marginEnd="60dp"
android:background="@drawable/shape_gray_f8_15"
android:gravity="center_vertical">
<EditText
android:id="@+id/etSheetInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start|center_vertical"
android:background="@null"
android:ellipsize="end"
android:gravity="center_vertical"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:textColor="@color/black666"
android:textColorHint="@color/gray_c8"
android:textSize="36sp"
tools:hint="输入食材名称"
tools:ignore="Autofill,LabelFor,TextFields" />
<ImageView
android:id="@+id/ivSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="20dp"
android:padding="10dp"
android:src="@drawable/ic_search_gray"
tools:ignore="ContentDescription" />
</FrameLayout>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="700dp"
android:layout_marginTop="10dp"
app:srlEnableOverScrollDrag="false">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="45dp"
android:layout_marginTop="15dp"
android:layout_marginEnd="45dp"
android:layout_marginBottom="15dp"
android:minHeight="380dp"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_inbound_ingredient" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvWarehouse"
android:layout_width="280dp"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="10"
android:layout_margin="1dp"
android:overScrollMode="never"
tools:listitem="@layout/list_item_dropdown"
android:background="@drawable/bg_white_radius10_stroke" />
</FrameLayout>
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/ripple_effect_light3">
<TextView
android:id="@+id/tvDropdown"
android:layout_width="match_parent"
android:layout_height="70dp"
android:gravity="center"
android:textColor="#ff000033"
android:textSize="24sp"
android:textStyle="bold"
tools:text="选择仓库" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#90D5E1FF" />
</LinearLayout>
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/clBlock"
android:layout_width="match_parent"
android:layout_height="140dp"
android:layout_height="120dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="15dp"
@@ -17,6 +17,7 @@
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="30sp"
@@ -32,31 +33,18 @@
android:id="@+id/tvDishType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:layout_marginVertical="5dp"
android:textColor="@color/black999"
android:textSize="22sp"
app:layout_constraintBottom_toTopOf="@id/tvGoodsCode"
android:textSize="26sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/tvDishName"
app:layout_constraintStart_toStartOf="@id/tvDishName"
app:layout_constraintTop_toBottomOf="@id/tvDishName"
tools:text="主辅材:主材" />
<TextView
android:id="@+id/tvGoodsCode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black999"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/tvDishName"
app:layout_constraintStart_toStartOf="@id/tvDishName"
app:layout_constraintTop_toBottomOf="@id/tvDishType"
android:text="溯源码:-" />
<ImageView
android:id="@+id/ivOperateIcon"
android:layout_width="80dp"
@@ -73,12 +61,11 @@
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_dish_clear"
android:src="@drawable/ic_delete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
android:visibility="gone"/>
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tvDishWeight"
@@ -1,230 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
tools:background="@color/white">
<!-- 字段标签,必填项红色星号由 Adapter 通过 SpannableString 追加 -->
<TextView
android:id="@+id/tvLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6dp"
android:layout_marginBottom="6dp"
android:textColor="#FF141428"
android:textSize="20sp"
android:textStyle="bold"
tools:text="食材名称" />
<!-- CHILDREN 类型:子列表容器 -->
<LinearLayout
android:id="@+id/llChildren"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<!-- 子列表标题行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="4dp"
android:gravity="center_vertical"
android:background="@drawable/bg_rect_header">
<!-- 食材溯源码表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginEnd="2dp"
android:gravity="center"
android:text="食材溯源码"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 食材名称表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginHorizontal="2dp"
android:gravity="center"
android:text="食材名称"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 分类表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginHorizontal="2dp"
android:gravity="center"
android:text="分类"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 每份用量表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:layout_marginStart="2dp"
android:maxLines="1"
android:gravity="center"
android:text="每份用量"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 操作表头 -->
<TextView
android:layout_width="50dp"
android:layout_height="match_parent"
android:text="操作"
android:gravity="center"
android:textColor="#FF141428"
android:textSize="16sp" />
</LinearLayout>
<!-- 嵌套 RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvChildren"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
tools:listitem="@layout/list_item_ingredient_row"/>
<!-- 添加按钮 -->
<TextView
android:id="@+id/tvAddChild"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:paddingVertical="5dp"
android:paddingStart="5dp"
android:paddingEnd="10dp"
android:textColor="#FF0066CC"
android:textSize="18sp"
android:textStyle="bold"
android:text="+ 添加食材"/>
</LinearLayout>
<!-- 输入区域容器,三种控件叠放,按 FieldType 切换 visibility -->
<FrameLayout
android:id="@+id/flInputArea"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginHorizontal="6dp"
android:visibility="visible">
<!-- TEXT / NUMBER / FIXED 类型:EditText -->
<EditText
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_white_radius10_stroke2"
android:focusableInTouchMode="true"
android:maxLines="1"
android:paddingHorizontal="20dp"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="20sp"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="Autofill,TextFields" />
<!-- DROPDOWN 类型:点击触发 DropdownPopup -->
<FrameLayout
android:id="@+id/flDropdown"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_white_radius10_stroke2"
android:visibility="gone">
<TextView
android:id="@+id/tvDropdownValue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="44dp"
android:layout_marginStart="20dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="20sp"
android:textStyle="bold" />
<ImageView
android:layout_width="32dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="16dp"
android:src="@mipmap/ic_triangle_down"
tools:ignore="ContentDescription" />
</FrameLayout>
<!-- DATE_PICKER 类型:点击触发 DatePickerDialog -->
<FrameLayout
android:id="@+id/flDatePicker"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_white_radius10_stroke2"
android:visibility="gone">
<TextView
android:id="@+id/tvDateValue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="44dp"
android:layout_marginStart="20dp"
android:gravity="center_vertical"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="20sp"
android:textStyle="bold" />
<ImageView
android:layout_width="32dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="16dp"
android:src="@mipmap/ic_triangle_down"
tools:ignore="ContentDescription" />
</FrameLayout>
</FrameLayout>
</LinearLayout>
@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/clBlock"
android:layout_width="match_parent"
android:layout_height="130dp"
android:layout_margin="15dp"
android:background="@drawable/shape_white_12_corners"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp">
<TextView
android:id="@+id/tvIngredientName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/black333"
android:textSize="28sp"
android:textStyle="bold"
tools:text="土豆" />
<TextView
android:id="@+id/tvStock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="@color/black999"
android:textSize="20sp"
tools:text="库存:12.5kg" />
<TextView
android:id="@+id/tvTraceCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/dish_green"
android:textSize="20sp"
tools:text="溯源码:TR20260101001" />
</LinearLayout>
@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/clBlock"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="15dp"
android:background="@drawable/shape_white_12_corners"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineH"
android:layout_width="1dp"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.7" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineV"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/tvIngredientName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:maxLength="20"
android:maxLines="1"
android:textColor="@color/black333"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/guidelineV"
app:layout_constraintEnd_toStartOf="@id/guidelineH"
app:layout_constraintStart_toStartOf="parent"
tools:text="土豆" />
<TextView
android:id="@+id/tvStock"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end|center_vertical"
android:textColor="@color/black999"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="@id/tvIngredientName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guidelineH"
app:layout_constraintTop_toTopOf="@id/tvIngredientName"
tools:text="库存:12.5kg" />
<TextView
android:id="@+id/tvTraceCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/dish_green"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guidelineV"
tools:text="溯源码:TR20260101001" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp"
tools:ignore="HardcodedText">
<!-- 食材溯源码 -->
<EditText
android:id="@+id/etTraceCode"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_white_radius10_stroke2"
android:hint="溯源码"
android:paddingHorizontal="10dp"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp"
tools:ignore="Autofill,TextFields" />
<!-- 食材名称 -->
<FrameLayout
android:id="@+id/flMaterName"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_white_radius10_stroke2">
<TextView
android:id="@+id/tvMaterName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp" />
<!-- android:layout_marginEnd="24dp"-->
<!-- <ImageView-->
<!-- android:layout_width="18dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="end|center_vertical"-->
<!-- android:layout_marginEnd="6dp"-->
<!-- android:src="@mipmap/ic_triangle_down"-->
<!-- tools:ignore="ContentDescription" />-->
</FrameLayout>
<!-- 分类 -->
<FrameLayout
android:id="@+id/flClassify"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_white_radius10_stroke2">
<TextView
android:id="@+id/tvClassify"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="24dp"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp" />
<ImageView
android:layout_width="18dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="6dp"
android:src="@mipmap/ic_triangle_down"
tools:ignore="ContentDescription" />
</FrameLayout>
<!-- 每份用量 -->
<EditText
android:id="@+id/etAmount"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/bg_white_radius10_stroke2"
android:hint="如 120g"
android:paddingHorizontal="10dp"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp"
tools:ignore="Autofill,TextFields" />
<!-- 删除按钮 -->
<TextView
android:id="@+id/tvRemove"
android:layout_width="50dp"
android:layout_height="50dp"
android:gravity="center"
android:text="×"
android:textColor="#FF96A0AA"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_margin="15dp"
android:background="@drawable/shape_white_12_corners"
android:paddingHorizontal="20dp"
android:paddingVertical="15dp">
<TextView
android:id="@+id/tvTaskNo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="任务编号:RW20260601001" />
<TextView
android:id="@+id/tvTargetDish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tvPlanDate"
app:layout_constraintStart_toStartOf="@id/tvTaskNo"
app:layout_constraintTop_toBottomOf="@id/tvTaskNo"
tools:text="红烧肉" />
<TextView
android:id="@+id/tvSpec"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:textColor="@color/black666"
android:textSize="22sp"
app:layout_constraintBottom_toBottomOf="@id/tvTargetDish"
app:layout_constraintStart_toEndOf="@id/tvTargetDish"
app:layout_constraintTop_toTopOf="@id/tvTargetDish"
tools:text="中组(8份)·50组" />
<TextView
android:id="@+id/tvPlanDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@id/tvPortions"
app:layout_constraintStart_toStartOf="@id/tvTargetDish"
app:layout_constraintTop_toBottomOf="@id/tvTargetDish"
tools:text="计划日期:2026-06-01" />
<TextView
android:id="@+id/tvPortions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/tvTargetDish"
app:layout_constraintTop_toBottomOf="@id/tvPlanDate"
tools:text="总份数:400" />
<TextView
android:id="@+id/tvOwnerName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textColor="@color/black999"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="@id/tvPortions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvPortions"
tools:text="负责人:张三" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textColor="@color/dish_green"
android:textSize="22sp"
app:layout_constraintBottom_toBottomOf="@id/tvTargetDish"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvTargetDish"
tools:text="待组配" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:background="@drawable/shape_white_fb_15_corners"
android:orientation="vertical"
android:paddingVertical="15dp">
<ImageView
android:id="@+id/ivClearIcon"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="20dp"
android:src="@drawable/ic_dish_clear"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
android:visibility="gone"/>
<ImageView
android:id="@+id/ivOperateIcon"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivClearIcon"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_dish_selected" />
<TextView
android:id="@+id/tvActualQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:hint="-"
android:textColor="@color/dish_green"
android:textColorHint="@color/gray_d6"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivOperateIcon"
app:layout_constraintTop_toTopOf="parent"
tools:text="200g" />
<LinearLayout
android:id="@+id/llFoodInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
app:layout_constraintEnd_toStartOf="@id/tvActualQty"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<TextView
android:id="@+id/tvIngredientName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="26sp"
android:textStyle="bold"
tools:text="五花肉" />
<TextView
android:id="@+id/tvIngredientClass"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="@color/black999"
android:textSize="20sp"
tools:text="主材" />
</LinearLayout>
<TextView
android:id="@+id/tvPerPortionQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:textColor="@color/black666"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/tvTraceCode"
app:layout_constraintStart_toStartOf="@id/llFoodInfo"
app:layout_constraintTop_toBottomOf="@id/llFoodInfo"
tools:text="每份用量:200g" />
<TextView
android:id="@+id/tvTraceCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="@id/llFoodInfo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPerPortionQty"
tools:text="溯源码:TRACE-20260601001"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

+5 -1
View File
@@ -1,5 +1,5 @@
<resources>
<string name="app_name">配比秤新</string>
<string name="app_name">菜品配比终端</string>
<string name="sp_save_name">dish_match_terminal</string>
<string name="home_title">菜品配比终端</string>
<string name="home_sub_title">选择菜品模式</string>
@@ -35,4 +35,8 @@
<string name="dish_type_first">主材</string>
<string name="add_sampling">新增采样</string>
<string name="add_food">添加食材</string>
<string name="clear_zero">清零</string>
<string name="add">添加</string>
<string name="click_search_ingredient">点击搜索食材</string>
<string name="weight_from_scale">称重获取</string>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

+423
View File
@@ -0,0 +1,423 @@
# 配比秤(新设备)— 制作菜品 API 文档
> Controller: `NutRatioScaleController`
> 路径前缀: `/neglect/ratio-scale`Nacos 白名单 `/nutrition/neglect/**`,无需 Sa-Token
> 设备上下文通过请求头 `X-DEVICE-CODE` 解析(`TerminalContextHelper`
> 用户上下文通过请求头 `X-DEVICE-TOKEN` 解析(`DeviceAuthContextHelper`,获取登录厨师信息)
> 日期: 2026-07-29
>
> **序列化规则**:Long 类型字段响应中均为字符串(Jackson `ToStringSerializer`),`BigDecimal` 为普通数字
> **单位约定**
> - 食材用量(`actualQty`、`useWeight`)统一为 **克 (g)**
> - 烹制单生重(`rawWeight`)、熟重(`cookedWeight`)为 **千克 (kg)**
---
## 业务背景
配比秤是一款独立于现有组配秤(`/neglect` 路径)的新硬件设备,部署在食堂后厨。第一期实现"按单烹制"功能:厨师在配比秤终端上查看当日烹饪任务 → 查看菜品食材构成 → 提交主辅材实际用量+熟重+调料用量,一次性完成烹制。
### 核心流程
```
当日烹饪任务列表 → 查看菜品食材构成 → 烹饪完成(提交主辅材用量 + 熟重 + 调料)
```
### 烹制状态枚举
| 状态码 | 状态名 | 说明 |
|:------:|--------|------|
| 0 | 待烹制 | 初始状态,可进行"烹饪完成"操作 |
| 2 | 已完成 | 烹制结束,不可再操作 |
| 3 | 异常 | 异常状态 |
### 食材分类枚举
| 分类 | 名称 | 说明 |
|:----:|------|------|
| 1 | 主材 | 菜品主要食材 |
| 2 | 辅材 | 菜品辅助食材 |
| 3 | 调料 | 油盐酱醋等,烹饪完成时提交 |
---
## 接口清单
| 序号 | 接口 | 路径 | 用途 |
|:----:|------|------|------|
| 一 | 当日烹饪任务列表(按餐次分组) | `POST /neglect/ratio-scale/cook-orders/list` | 按餐次分组返回今日需烹制的菜品列表 |
| 二 | 菜品食材构成 | `GET /neglect/ratio-scale/cook-orders/{cookOrderId}/composition` | 查看菜品的主材和辅材清单(含标准用量) |
| 三 | 烹饪完成 | `POST /neglect/ratio-scale/cook-orders/complete` | 提交主辅材用量+熟重+调料,一次完成烹制 |
| 四 | 调料模糊搜索 | `POST /neglect/ratio-scale/seasoning-search` | 模糊搜索调料(糖蜜饯+油脂+调味品三大类) |
---
## 一、当日烹饪任务列表(按餐次分组)
```
POST /neglect/ratio-scale/cook-orders/list
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
X-DEVICE-TOKEN: <设备登录Token>
```
查询当前食堂当日的烹饪任务,后端按餐次分组返回,每个餐次为一个独立对象,内含该餐次下的烹制单列表。
### 请求体
```json
{
"date": "2026-07-29", // 选填 — 计划日期 yyyy-MM-dd,默认当天
"mealType": 2, // 选填 — 餐次筛选: 1=早餐 / 2=午餐 / 3=晚餐 / 4=加餐
"keyword": "红烧肉" // 选填 — 菜品名称模糊搜索
}
```
> 注:`mealType` 不传则返回全部餐次;传了只返回对应餐次的单个分组。
### 响应
```json
{
"code": "200",
"msg": "操作成功",
"data": [
{
"mealType": 2, // 餐次: 1=早餐 / 2=午餐 / 3=晚餐 / 4=加餐
"mealTypeName": "午餐", // 餐次中文名称
"cookOrders": [
{
"cookOrderId": "10001", // 烹制单id
"cookNo": "CK20260729001", // 烹制单号
"dishName": "红烧肉", // 菜品名称
"foodId": "5001", // 菜品id(关联 nut_food
"mealType": 2, // 餐次
"mealTypeName": "午餐", // 餐次中文
"cookStatus": 0, // 烹制状态: 0=待烹制 / 1=烹制中 / 2=已完成 / 3=异常
"cookStatusName": "待烹制", // 烹制状态中文
"rawWeight": null, // 生重(kg) — 烹制完成后才有值
"needPortions": 5, // 需求份数
"cookedPortions": null // 烹制份数 — 烹制完成后才有值
},
{
"cookOrderId": "10002",
"cookNo": "CK20260729002",
"dishName": "清炒时蔬",
"foodId": "5002",
"mealType": 2,
"mealTypeName": "午餐",
"cookStatus": 1,
"cookStatusName": "烹制中",
"rawWeight": 3.500,
"needPortions": 3,
"cookedPortions": 3
}
]
},
{
"mealType": 3,
"mealTypeName": "晚餐",
"cookOrders": [
{
"cookOrderId": "10003",
"cookNo": "CK20260729003",
"dishName": "清蒸鲈鱼",
"foodId": "5003",
"mealType": 3,
"mealTypeName": "晚餐",
"cookStatus": 0,
"cookStatusName": "待烹制",
"rawWeight": null,
"needPortions": 2,
"cookedPortions": null
}
]
}
]
}
```
### 逻辑说明
- 数据源:`nut_prod_cook_order`
- 查询条件:`canteen_id` = 终端食堂ID(由 `X-DEVICE-CODE` 解析) + `plan_date` = 指定日期(默认当天)
- 排序规则:`meal_type ASC, create_time DESC`
- 后端按 `mealType` 分组,每组为一个 `NutRatioCookMealGroupVO` 对象,内含 `mealType` + `mealTypeName` + `cookOrders` 列表
- `keyword` 字段对 `dish_name` 做 LIKE 模糊匹配
- `rawWeight` 在烹制完成后为累计主辅材实际用量之和(kg),烹制中为 null
---
## 二、菜品食材构成(主材+辅材)
```
GET /neglect/ratio-scale/cook-orders/{cookOrderId}/composition
X-DEVICE-CODE: <设备编码>
X-DEVICE-TOKEN: <设备登录Token>
```
查询烹制单对应菜品的主材和辅材清单(不包含调料,调料在烹饪完成时单独提交)。
### 路径参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| cookOrderId | Long | 是 | 烹制单id |
### 响应
```json
{
"code": "200",
"msg": "操作成功",
"data": [
{
"materId": "20001", // 食材id
"ingredientName": "五花肉", // 食材名称
"ingredientClass": 1, // 食材分类: 1=主材 / 2=辅材
"ingredientClassName": "主材", // 食材分类中文
"useWeight": 500.0, // 标准用量(g)
"vegTypeId": "10" // 净菜类型id
},
{
"materId": "20002",
"ingredientName": "土豆",
"ingredientClass": 2,
"ingredientClassName": "辅材",
"useWeight": 300.0,
"vegTypeId": "10"
}
]
}
```
### 逻辑说明
- 先通过烹制单查询 `food_id`(菜品id),再查 `nut_food_composition`
- 仅返回 `is_main IN (1, 2)` 的食材(主材和辅材),调料(`is_main=3`)不在此接口返回
- 食材名称通过 `nut_mater_base` 批量查询后回填
- `vegTypeId` 为净菜类型id,用于前端展示毛菜/净菜标识
---
## 三、烹饪完成:提交主辅材用量+熟重+调料
```
POST /neglect/ratio-scale/cook-orders/complete
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
X-DEVICE-TOKEN: <设备登录Token>
```
厨师在秤上完成称重后一次性提交全部数据,后端自动完成:组配任务完善 → 组配完成 → 计算生重生熟比 → 保存调料 → 烹制单置为已完成。
**该接口为事务性操作(`@Transactional`),所有步骤在同一事务中执行,任意步骤失败则全部回滚。**
### 请求体
```json
{
"cookOrderId": 10001, // 必填 — 烹制单id
"items": [ // 必填 — 主辅材实际用量列表,至少1条
{
"materId": 20001, // 必填 — 食材id
"actualQty": 520.5 // 必填 — 实际用量(g)
},
{
"materId": 20002,
"actualQty": 310.0
}
],
"cookedWeight": 3.200, // 必填 — 熟重(kg)
"seasonings": [ // 选填 — 调料用量列表
{
"materId": 30001, // 必填 — 调料食材id
"actualQty": 15.0 // 必填 — 实际用量(g)
}
]
}
```
### 响应
```json
{
"code": "200",
"msg": "操作成功",
"data": null
}
```
### 处理流程
```
1. 校验烹制单状态 = 0(待烹制),否则报错
2. 通过内供申领单关联餐品净菜包:
链路: nut_prod_cook_order(food_id + canteen_id + meal_type) → nut_prod_clean_order(food_id + canteen_id + meal_type + clean_type=2)
→ clean_order_id → nut_sup_internal_supply(clean_order_id + clean_type=2)
→ batch_no → nut_sup_meal_package → nut_sup_meal_pkg_ingredient → trace_code
餐次匹配:通过 nut_prod_clean_order.meal_type 过滤,确保同菜不同餐次不会串单
3. 解析各食材的溯源码(materId → traceCode 映射)
4. 完善组配任务明细(按 materCode 匹配 comboTaskItem,更新 actualQty + traceCode
5. 完成组配任务(comboStatus=2,记录完成时间)
6. 计算生重(Σ actualQty / 1000g→kg,保留3位小数)
7. 从设备Token获取厨师信息(chef + chefId
8. 更新烹制食材明细(主辅材 actualQty + traceCode,无已有明细时兜底创建)
9. 保存调料用量(ingredientClass=3
10. 计算生熟比 = 熟重 / 生重(保留3位小数,HALF_UP,生重>0且熟重≠null时才计算)
11. 更新烹制单:
- cookStatus → 2(已完成)
- cookStart / cookEnd → 当前时间
- rawWeight → 生重(kg)
- cookedWeight → 熟重(kg)
- rawCookedRatio → 生熟比
- chef / chefId → 登录厨师
- needPortions / cookedPortions → 份数
```
### 份数计算优先级
```
内供申领单 quantity > 烹制单 needPortions > 兜底值 1
```
### 异常场景
| 场景 | 错误信息 |
|------|---------|
| 烹制单不存在 | `烹制单不存在` |
| 当前状态不是"待烹制" | `当前状态不允许此操作,仅待烹制状态可提交` |
| 烹制单未关联组配任务(comboNo 为空) | `烹制单未关联组配任务,无法开始烹制` |
| 组配任务不存在 | `组配任务不存在,comboNo=xxx` |
### 涉及数据表
| 表名 | 操作 | 说明 |
|------|:--:|------|
| `nut_prod_cook_order` | 查询 + 更新 | 校验状态,更新为已完成 |
| `nut_prod_clean_order` | 查询 | 通过 foodId + canteenId + mealType + cleanType=2 匹配净菜订单,获取 clean_order_id 用于内供申领单的餐次过滤 |
| `nut_sup_internal_supply` | 查询 | 通过 clean_order_id + cleanType=2 关联,获取 batchNo 和 quantity |
| `nut_sup_meal_package` | 查询 | 通过 batchNo 查餐品净菜包 |
| `nut_sup_meal_pkg_ingredient` | 查询 | 获取各食材的溯源码 |
| `nut_prod_combo_task` | 查询 + 更新 | 完善组配状态为已完成 |
| `nut_prod_combo_task_item` | 查询 + 更新 | 更新 actualQty + traceCode |
| `nut_prod_cook_ingredient` | 查询 + 新增/更新 | 更新主辅材实际用量 + 保存调料用量 |
| `nut_mater_base` | 查询 | 获取食材编码和名称 |
| `nut_food_composition` | 查询 | 兜底创建 cook_ingredient 时获取食材分类等信息 |
---
## 四、调料模糊搜索
```
POST /neglect/ratio-scale/seasoning-search
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
```
搜索调料食材库,数据源 `nut_mater_base` 一级分类为 18(糖蜜饯)、19(油脂)、20(调味品) 的食材。
### 请求体
```json
{
"keyword": "酱油", // 选填 — 模糊匹配食材名称和别名
"pageNum": 1, // 必填 — 页码
"pageSize": 20 // 必填 — 每页条数
}
```
### 响应
```json
{
"code": "200",
"msg": "操作成功",
"data": [
{
"id": "30001",
"materName": "酱油",
"materCode": "19192004",
"alias": "生抽",
"canteenId": "2"
}
],
"total": 1
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long → String | 食材id |
| materName | String | 食材名称 |
| materCode | String | 食材编码 |
| alias | String | 别名 |
| canteenId | Long → String | 归属食堂id |
---
## 总体数据链路图
```
nut_prod_cook_order (烹制单)
├── food_id ──→ nut_food_composition (菜品食材构成: 主材/辅材/调料)
│ │
│ └── mater_id ──→ nut_mater_base (食材库: 编码、名称、分类)
├── food_id + canteen_id + meal_type ──→ nut_prod_clean_order (净菜订单, clean_type=2)
│ │
│ └── id = clean_order_id
├── clean_order_id ──→ nut_sup_internal_supply (内供申领单, 按 clean_order_id + clean_type=2 匹配)
│ │
│ ├── batch_no ──→ nut_sup_meal_package (餐品净菜包)
│ │ │
│ │ └── nut_sup_meal_pkg_ingredient (溯源码)
│ │
│ └── quantity ──→ 份数
├── combo_no ──→ nut_prod_combo_task (组配任务)
│ │
│ └── nut_prod_combo_task_item (组配明细: ingredientCode)
└── cook_ingredient: nut_prod_cook_ingredient (烹制食材明细)
```
---
## 接口调用时序
```
┌─────────┐ ┌─────────────┐ ┌──────────┐
│ 配比秤终端 │ │ Controller │ │ Database │
└────┬────┘ └──────┬──────┘ └────┬─────┘
│ │ │
│ 1. POST /cook-orders/page │
│─────────────────────→│ │
│ │──→ nut_prod_cook_order (当日+食堂)
│ ← 任务列表(按餐次) │←────────────────────│
│ │ │
│ 2. GET /cook-orders/{id}/composition │
│─────────────────────→│ │
│ │──→ nut_food_composition (主材+辅材)
│ │──→ nut_mater_base (食材名称)
│ ← 食材清单(含标准用量) │←────────────────────│
│ │ │
│ 3. POST /cook-orders/complete │
│─────────────────────→│ │
│ │──→ 校验状态=待烹制 │
│ │──→ 匹配净菜订单(餐次过滤) │
│ │──→ 关联internal_supply │
│ │──→ 解析溯源码 │
│ │──→ 完善+完成组配任务 │
│ │──→ 计算生重 │
│ │──→ 更新食材明细 │
│ │──→ 保存调料用量 │
│ │──→ 计算生熟比 │
│ │──→ 更新烹制单=已完成 │
│ ← 操作成功 │←──── 事务提交 ──────│
│ │ │
```
+611
View File
@@ -0,0 +1,611 @@
# 配比秤(新设备)— 采样与制作模式 API 文档
> Controller: `NutRatioScaleController`
> 路径前缀: `/neglect/ratio-scale`Nacos 白名单 `/nutrition/neglect/**`,无需 Sa-Token
> 设备上下文通过请求头 `X-DEVICE-CODE` 解析(`TerminalContextHelper`,据此拿到食堂 id
> 日期: 2026-09-09
>
> **序列化规则**:Long 类型字段响应中均为字符串(Jackson `ToStringSerializer`),`BigDecimal` 为普通数字
> **单位约定**
> - 提交构成的熟重 `foodWeight` 单位为 **克 (g)**(与 `nut_food_cook.output_weight` 口径一致)
> - 食材称量用量 `useWeight` 单位为 **克 (g)**
> - 烹制单生重 `rawWeight`、熟重 `cookedWeight` 单位为 **千克 (kg)**
> - 食材实际用量 `actualQty` 单位为 **克 (g)**
---
## 业务背景
配比秤新增「采样模式」与「制作模式」,两者通过**同一个提交接口** `POST /constitute/save``cookMode` 字段区分:
| cookMode | 模式 | 说明 |
|:--------:|------|------|
| 2 | 采样模式 | **从零建菜**:称量食材 → 新增菜品(生成构成/营养/宝塔)→ 进入「待审核」→ 后台审核通过后上架 |
| 1 | 制作模式 | **基于已有菜品**:称量食材 → 记录熟重+构成快照(历史参考,不覆盖正式构成)→ 标记菜品为「配比秤制作」 |
### 制作方式枚举
| 值 | 名称 | 菜品对象 | 数据来源 data_source |
|:--:|------|---------|---------------------|
| 1 | 制作模式 | 已有菜品(foodId 必填) | 3(配比秤制作) |
| 2 | 采样模式 | 新增菜品(foodName 必填) | 2(配比秤采样) |
### 采样菜品审核状态枚举
| 状态码 | 状态名 | 说明 |
|:------:|--------|------|
| 1 | 待审核 | 采样提交后的初始状态,后台可审核 |
| 2 | 已通过 | 审核通过,菜品上架生效 |
| 3 | 已驳回 | 审核驳回,菜品保持下架,可重新采样同名菜品 |
### 食材分类枚举
| 分类 | 名称 | 说明 |
|:----:|------|------|
| 1 | 主材 | 菜品主要食材 |
| 2 | 辅材 | 菜品辅助食材 |
| 3 | 调料 | 油盐酱醋等 |
---
## 接口清单
| 序号 | 接口 | 路径 | 用途 |
|:----:|------|------|------|
| 一 | 食材模糊搜索 | `POST /neglect/ratio-scale/mater-search` | 称量前检索食材(设备对应食堂食材库) |
| 二 | 搜菜品 | `POST /neglect/ratio-scale/food-search` | 制作模式选已有菜品(按名称模糊查上架菜品) |
| 三 | 查菜品构成 | `GET /neglect/ratio-scale/food/{foodId}/composition` | 制作模式查看已有菜品的主辅料+调料构成 |
| 四 | 提交构成 | `POST /neglect/ratio-scale/constitute/save` | 统一提交,cookMode 区分制作/采样 |
| 五 | 采样历史 | `POST /neglect/ratio-scale/sample/history` | 查询本食堂采样提交历史(默认当天) |
| 六 | 制作历史 | `POST /neglect/ratio-scale/make/history` | 按设备+日期查询制作记录分页 |
| 七 | 制作历史图表 | `GET /neglect/ratio-scale/make/history/{foodId}` | 按菜品id查制作历史图表数据 |
| 八 | 烹饪完成 | `POST /neglect/ratio-scale/cook-orders/complete` | 按单烹制,任意状态提交均累加 |
---
## 一、食材模糊搜索
```
POST /neglect/ratio-scale/mater-search
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
```
称量食材前,从食材库检索食材(设备对应食堂食材)。
### 请求体
```json
{
"keyword": "萝卜", // 选填 — 模糊匹配食材名称和别名
"pageNum": 1, // 必填 — 页码
"pageSize": 20 // 必填 — 每页条数
}
```
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": [
{
"id": "1979825467306471454", // 食材id
"materName": "樱桃萝卜", // 食材名称
"materCode": "04041012", // 食材编码
"alias": null, // 别名
"materFirstClass": "04", // 食材一级分类
"materSecondClass": "043", // 食材二级分类
"materUrl": null, // 食材图片url
"canteenId": "2" // 归属食堂id
}
],
"total": 1
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long → String | 食材id(提交构成 items.materId 使用) |
| materName | String | 食材名称 |
| materCode | String | 食材编码(一级+二级+自编码,全局唯一) |
| alias | String | 别名 |
| materFirstClass | String | 食材一级分类 |
| materSecondClass | String | 食材二级分类 |
| materUrl | String | 食材图片url |
| canteenId | Long → String | 归属食堂id |
### 逻辑说明
- 数据源:`nut_mater_base`
- 过滤:`canteen_id` = 当前食堂(由 `X-DEVICE-CODE` 解析)
- 关键词模糊匹配 `mater_name``alias`
- 排序:`mater_name ASC`
---
## 二、搜菜品
```
POST /neglect/ratio-scale/food-search
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
```
制作模式下,检索本食堂已上架的菜品,供后厨选择目标菜品。
### 请求体
```json
{
"keyword": "西兰花", // 选填 — 菜品名称或编号模糊搜索
"pageNum": 1, // 必填 — 页码
"pageSize": 20 // 必填 — 每页条数
}
```
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": [
{
"foodId": "2091839370283118593", // 菜品id
"foodCode": "FOOD00012", // 菜品编号
"foodName": "清炒西兰花" // 菜品名称
}
],
"total": 1
}
```
### 逻辑说明
- 数据源:`nut_food``status=1` 上架)
- 过滤:`canteen_id` = 当前食堂
- 关键词模糊匹配 `food_name``food_code`
- 排序:`id DESC`
---
## 三、查菜品构成
```
GET /neglect/ratio-scale/food/{foodId}/composition
X-DEVICE-CODE: <设备编码>
```
制作模式下,根据菜品 id 查询已有菜品的主辅料+调料构成及熟重。
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": {
"foodId": "2091839370283118593", // 菜品id
"foodName": "清炒西兰花", // 菜品名称
"foodWeight": 450.0, // 熟重(g),从 nut_food_cook.output_weight 反查
"ingredients": [
{
"materId": "1979825467306471454", // 食材id
"materName": "樱桃萝卜", // 食材名称
"useWeight": 300.0, // 用量(g)
"isMain": 1, // 1=主材 / 2=辅材 / 3=调料
"isMainName": "主材"
}
]
}
}
```
### 逻辑说明
- 数据源:`nut_food_composition`(按 `food_id` 查全部构成)
- 食材名称从 `nut_mater_base` 批量反查
---
## 四、提交构成(cookMode 区分采样/制作)
```
POST /neglect/ratio-scale/constitute/save
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
```
**统一提交接口**,通过 `cookMode` 区分采样/制作。
### 请求体
```json
{
"cookMode": 2, // 必填 — 1=制作模式 / 2=采样模式
"foodId": 2091839370283118593, // 制作模式必填 — 已有菜品id
"foodName": "清炒西兰花", // 采样模式必填 — 菜品名称
"foodWeight": 500, // 必填 — 熟重(g),必须 > 0
"mealType": 2, // 选填 — 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐
"items": [ // 必填 — 食材构成列表,至少1条
{
"materId": 20001, // 必填 — 食材idnut_mater_base.id
"useWeight": 300, // 必填 — 食材称量用量(g),必须 > 0
"isMain": 1 // 必填 — 食材分类: 1=主材 / 2=辅材 / 3=调料
},
{
"materId": 30001,
"useWeight": 50,
"isMain": 3
}
]
}
```
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": null
}
```
### 处理流程(采样模式 cookMode=2
```
1. 校验食材构成非空 + materId 非空且唯一
2. 校验菜品名称在同食堂下唯一(排除已驳回菜品,允许驳回后重新采样同名)
3. 生成菜品编号(FOOD + 5位序号)
4. 保存菜品 nut_fooddata_source=2(配比秤采样) / audit_status=1(待审核) / status=0(下架)
5. 保存熟重 nut_food_cookoutput_weight = foodWeight
6. 保存食材构成 nut_food_compositionmater_id + use_weight + is_main
7. 根据构成自动计算营养指标 nut_food_nutri + 膳食宝塔 nut_food_pagoda
8. 发起审核记录 nut_food_audit_recordaudit_status=1(待审核)
```
### 处理流程(制作模式 cookMode=1
```
1. 校验食材构成非空 + materId 非空且唯一
2. 校验菜品存在(foodId 必填)
3. 标记菜品 nut_food.data_source = 3(配比秤制作)
4. 保存制作记录 nut_food_matching_usedevice_code + food_id + food_name + meal_type + day + food_weight(熟重)
5. 保存制作明细 nut_food_matching_use_itemmater_id + mater_name + use_weight + is_main(构成快照)
6. 不覆盖菜品的正式构成/营养/宝塔(历史参考)
```
### 异常场景
| 场景 | 错误信息 |
|------|---------|
| 食材构成列表为空 | `请完善菜品构成信息` |
| 食材id为空 | `菜品食材不能为空` |
| 食材id重复 | `菜品食材不能重复,重复食材id:xxx` |
| 制作方式非法 | `不支持的制作方式:xxx` |
| 采样:同食堂已存在同名菜品(非驳回) | `该食堂下已存在同名餐品` |
| 采样:食材id无效 | `存在无效的食材id` |
| 制作:菜品不存在 | `菜品不存在` |
| 熟重/用量 ≤ 0 | 参数校验:`熟重必须大于0` / `食材用量必须大于0` |
### 涉及数据表
**采样模式(cookMode=2**
| 表名 | 操作 | 说明 |
|------|:--:|------|
| `nut_food` | 新增 | 采样菜品主表(采样来源 + 待审核 + 下架) |
| `nut_food_cook` | 新增 | 保存熟重 |
| `nut_food_composition` | 新增 | 食材构成 |
| `nut_food_nutri` | 新增 | 每100g营养指标(自动计算) |
| `nut_food_pagoda` | 新增 | 膳食宝塔(自动计算) |
| `nut_food_audit_record` | 新增 | 待审核记录 |
**制作模式(cookMode=1**
| 表名 | 操作 | 说明 |
|------|:--:|------|
| `nut_food` | 更新 | 标记 data_source=3(配比秤制作) |
| `nut_food_matching_use` | 新增 | 制作记录(熟重记录) |
| `nut_food_matching_use_item` | 新增 | 制作明细(构成快照) |
---
## 五、采样历史
```
POST /neglect/ratio-scale/sample/history
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
```
查询本食堂(由 `X-DEVICE-CODE` 解析)的采样提交历史,默认查询当天。
### 请求体
```json
{
"pageNum": 1, // 必填 — 页码
"pageSize": 10, // 必填 — 每页条数
"keyword": "西兰花", // 选填 — 菜品名称模糊搜索
"auditStatus": 1, // 选填 — 审核状态筛选: 1=待审核 / 2=已通过 / 3=已驳回
"sampleDate": "2026-09-09" // 选填 — 采样日期,不传默认当天
}
```
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": [
{
"foodId": "2097486806812893186", // 菜品id
"foodCode": "FOOD00030", // 菜品编号
"foodName": "清炒西兰花", // 菜品名称
"foodWeight": 500.0, // 熟重(g)
"auditStatus": 1, // 审核状态: 1=待审核 / 2=已通过 / 3=已驳回
"auditStatusName": "待审核", // 审核状态中文
"auditRecordId": "2097486809375612929", // 审核记录id
"createTime": "2026-09-09 08:46:39" // 提交时间
}
],
"total": 1
}
```
### 逻辑说明
- 数据源:`nut_food``data_source=2` 配比秤采样)
- `canteenId``X-DEVICE-CODE` 解析终端后自动限定
- 排序:`create_time DESC`
- `foodWeight``nut_food_cook.output_weight` 反查
- `auditRecordId``nut_food_audit_record` 反查
---
## 六、制作历史
```
POST /neglect/ratio-scale/make/history
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
```
查询本设备(由 `X-DEVICE-CODE` 解析)的制作记录。
### 请求体
```json
{
"pageNum": 1, // 必填 — 页码
"pageSize": 10, // 必填 — 每页条数
"day": "2026-09-09" // 选填 — 制作日期,不传查全部
}
```
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": [
{
"id": "2097499999999999999", // 制作记录id
"foodId": "2091839370283118593", // 菜品id
"foodName": "清炒西兰花", // 菜品名称
"foodWeight": 500.0, // 熟重(g)
"mealType": 2, // 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐
"mealTypeName": "午餐",
"day": "2026-09-09", // 制作日期
"createTime": "2026-09-09 08:46:39"
}
],
"total": 1
}
```
### 逻辑说明
- 数据源:`nut_food_matching_use`
- 过滤:`device_code` = 当前设备(由 `X-DEVICE-CODE` 解析)
- 排序:`create_time DESC`
---
## 七、制作历史图表(按菜品id
```
GET /neglect/ratio-scale/make/history/{foodId}
X-DEVICE-CODE: <设备编码>
```
根据菜品 id 查询该菜品的所有制作记录,返回图表数据(对齐借鉴项目 `queryGoodsInfoByFoodIdMatchingList`),用于前端渲染制作趋势图。
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": [
{
"xAxes": [
{"goodId": "", "goodName": "时间"},
{"goodId": "", "goodName": "熟重"},
{"goodId": "20001", "goodName": "樱桃萝卜"},
{"goodId": "30001", "goodName": "酱油"}
],
"yAxes": [
{"value": "2026-09-09 08:46:39"},
{"value": 500.0},
{"value": 300.0},
{"value": 50.0}
]
}
]
}
```
### 字段说明
| 字段 | 说明 |
|------|------|
| xAxes | 横轴模板:时间、熟重、该菜品所有出现过的食材(去重保序),所有记录共用 |
| xAxes[].goodId | 食材id(时间/熟重项为空字符串) |
| xAxes[].goodName | 项名称(时间/熟重/食材名称) |
| yAxes | 纵轴值,与 xAxes 一一对应 |
| yAxes[].value | 值:时间项为时间戳,其余为数值(缺失食材补 0) |
### 逻辑说明
- 数据源:`nut_food_matching_use`(主表)+ `nut_food_matching_use_item`(子表)
- 主表按 `food_id` 过滤,`create_time DESC` 排序
- 子表按 `matching_use_id` 批量查询并分组(消除 N+1
- 时间项取 `create_time`,熟重项取 `food_weight`,食材项取 `use_weight`(缺失补 0
---
## 八、烹饪完成(支持再次烹制)
```
POST /neglect/ratio-scale/cook-orders/complete
Content-Type: application/json
X-DEVICE-CODE: <设备编码>
```
按单烹制提交。**改造点**:原逻辑一次性完成(`待烹制 → 已完成`),现支持**任意状态下再次烹制**——每次提交累加份数/生重/熟重并留存一条流水,不校验烹制状态,需求达标后仍可继续烹制(应对菜不够卖接着做的场景)。
### 请求体
```json
{
"cookOrderId": 10001, // 必填 — 烹制单id
"portions": 10, // 选填 — 本次烹制份数,不传则按申领单/需求份数兜底
"cookStart": "2026-09-09 10:00:00", // 选填 — 本次烹制开始时间
"cookedWeight": 3.5, // 必填 — 本次熟重(kg)
"items": [ // 必填 — 主辅材实际用量列表,至少1条
{
"materId": 20001, // 必填 — 食材id
"actualQty": 5000 // 必填 — 实际用量(g)
}
],
"seasonings": [ // 选填 — 调料用量列表
{
"materId": 30001,
"actualQty": 15.0
}
]
}
```
### 响应
```json
{
"code": "00000",
"msg": "操作成功",
"data": null
}
```
### 状态流转规则(改造核心)
```
每次提交后(不校验状态,任何状态下提交均累加):
cooked_portions 累加 += 本次份数
raw_weight 累加 += 本次生重
cooked_weight 累加 += 本次熟重
cook_status → 2(已完成,每次提交都置此状态,允许继续烹制累加)
生熟比 = 累计熟重 / 累计生重(用累加后的值)
时间语义:
cook_start = 首次烹制开始时间(后续烹制不覆盖)
cook_end = 最后一次烹制结束时间(每次更新)
```
### 处理流程
```
1. 校验烹制单存在 + 食堂权限(不校验烹制状态,任何状态下提交均累加数据)
2. 本次烹制份数 = 入参 portions(优先)→ 申领单 quantity → 需求份数 → 1
3. 首次烹制(cooked_portions=0)才完善组配任务并完成
4. 计算本次生重(Σ actualQty / 1000g→kg
5. 计算生熟比 = 累计熟重 / 累计生重(用累加后的值)
6. 写烹制流水 nut_prod_cook_flow(本次生重/熟重/份数/起止时间)
7. 保存食材明细 + 调料(首次更新预生成明细,再次插入新明细,均关联 flow_id)
8. 累加主表:份数/生重/熟重累加 + 状态置已完成 + 起止时间
```
### 异常场景
| 场景 | 错误信息 |
|------|---------|
| 烹制单不存在 | `烹制单不存在` |
| 首次烹制未关联组配任务 | `烹制单未关联组配任务,无法开始烹制` |
| 组配任务不存在 | `组配任务不存在,comboNo=xxx` |
### 涉及数据表
| 表名 | 操作 | 说明 |
|------|:--:|------|
| `nut_prod_cook_order` | 查询 + 更新 | 累加份数/生重/熟重 + 状态置已完成 |
| `nut_prod_cook_flow` | 新增 | **烹制流水**,每次烹制一条 |
| `nut_prod_cook_ingredient` | 查询 + 更新/新增 | 首次更新预生成明细,再次插入新明细,均带 `flow_id` |
| `nut_prod_combo_task` / `_item` | 查询 + 更新 | 仅首次烹制时完善组配 |
| `nut_sup_internal_supply` / `nut_sup_meal_package` / `_ingredient` | 查询 | 解析溯源码与份数 |
| `nut_mater_base` / `nut_food_composition` | 查询 | 食材编码/名称/分类 |
---
## 数据链路图(采样模式 cookMode=2
```
nut_mater_base (食材库)
│ 采样称量 mater_id + use_weight(g)
POST /constitute/save (cookMode=2)
├── 生成 nut_food (采样来源 + 待审核 + 下架)
├── 生成 nut_food_cook (熟重 = foodWeight)
├── 生成 nut_food_composition (食材构成)
├── 自动计算 nut_food_nutri (每100g营养) + nut_food_pagoda (膳食宝塔)
└── 生成 nut_food_audit_record (待审核)
```
## 数据链路图(制作模式 cookMode=1
```
nut_food (已有菜品)
│ 制作称量 mater_id + use_weight(g) + 熟重
POST /constitute/save (cookMode=1)
├── 标记 nut_food.data_source = 3 (配比秤制作)
├── 生成 nut_food_matching_use (制作记录:熟重/餐次/日期/设备)
└── 生成 nut_food_matching_use_item (构成快照,历史参考)
(不覆盖 nut_food_composition / nut_food_nutri / nut_food_pagoda
```
## 数据链路图(再次烹制)
```
nut_prod_cook_order (烹制单,累加数据)
├── POST /cook-orders/complete (每次烹制)
│ ├── 生成 nut_prod_cook_flow (流水,每次一条)
│ └── 生成/更新 nut_prod_cook_ingredient (明细,带 flow_id)
└── 主表累加: cooked_portions / raw_weight / cooked_weight
cook_start = 首次开始 / cook_end = 末次结束
cook_status = 已完成(每次提交都置此状态,允许继续烹制累加)
```