Author SHA1 Message Date
lvmeng c8383af120 feat(meal): 添加餐品列表下拉刷新和上拉加载功能
- 集成 SmartRefreshLayout 实现下拉刷新和上拉加载更多
- 修改 MealListActivity 添加分页加载逻辑,每页30条数据
- 实现空视图显示功能,当无数据时展示暂无数据提示
- 优化 RecyclerView 点击事件处理,使用 startActivity 扩展函数
- 添加加载状态管理,区分首次加载和后续加载情况
- 实现加载失败时的状态回退和错误处理机制
- 统一网络请求回调处理,优化 Loading 状态显示逻辑
2026-06-05 10:13:05 +08:00
mazengfei 2f18376236 feat(task): 添加任务份数默认值设置
- 在任务详情页面为新任务设置默认份数为1
- 确保任务开始组配前包含正确的份数信息
2026-06-04 18:09:24 +08:00
mazengfei 14ccc39bfd fix(config): 更新应用配置和设备方向设置
- 在InitActivity中添加竖屏显示配置
- 注释掉BaseActivity中的动态屏幕方向逻辑
- 修改应用ID为com.shuwei.dish.match.new
- 将应用名称从菜品配比终端改为配比秤新
- 恢复称重设备端口为/dev/ttyS4
2026-06-04 11:22:03 +08:00
lvmeng 01ff7241cd feat(pack): 重构净菜包装页面并集成食材搜索功能
- 替换重量识别功能为食材选择搜索功能
- 新增 FoodSearchV2Activity 页面实现食材搜索选择
- 添加 InboundIngredientV2Adapter 适配器展示食材列表
- 创建 MaterialTypeTool 工具类统一处理食材类型文本
- 更新 CleanPackActivity 集成食材搜索返回逻辑
- 修改布局文件适配新的食材选择流程
- 调整环境配置中的API地址映射关系
- 优化 RecyclerView 显示和交互逻辑
- 移除原有的重量识别相关界面元素和代码
- 更新提交参数收集逻辑支持新食材数据结构
2026-06-03 16:05:19 +08:00
lvmeng ad9291243f feat(clean-pack): 添加配比秤功能和界面重构
- 新增搜索菜品名称功能和菜品搜索按钮
- 添加用量克数显示和清零重量功能
- 集成配比秤重量监听和显示功能
- 重构主页界面添加配比秤入口
- 新增组配任务按钮和相关页面跳转
- 修改餐品净菜包功能跳转至新的列表页面
- 优化净菜包装表单布局和字段配置
- 移除溯源码自动填充净菜名称功能
- 添加重量校准和去皮功能支持
- 重构提交流程增加重量验证和回调处理
2026-06-03 14:25:31 +08:00
lvmeng c81e2d14bb feat(pack): 优化净菜包页面布局和功能
- 调整 activity_pack.xml 中的边距和按钮布局
- 在 DateTimeUtil.kt 中添加日期格式常量 YYYY_MM_DD
- 优化 list_item_form_field.xml 的输入框样式和字体大小
- 将页面标题从"菜品净菜包"改为"餐品净菜包"
- 修改包装日期格式为 yyyy-MM-dd 并移除操作类型字段
- 将操作类型改为下拉选择框,支持设备自动和人工录入选项
- 将表单列表网格布局从2列改为4列显示
- 在 TaskActivity 中添加页面刷新逻辑
- 任务详情页面提交成功后返回并提示"组配成功"
2026-06-03 09:54:21 +08:00
lvmeng 0cc1d9189c feat(pack): 优化菜品净菜包装界面和功能
- 调整布局间距,移除商品名称和材料类型显示
- 修改开始组配按钮文本为完成组配并设为可见
- 更新API服务接口支持可空参数类型
- 添加餐品净菜包装数据模型字段
- 实现包装数据收集和提交功能
- 添加时间工具类导入和设备码字段
- 优化适配器中材料分类显示逻辑
2026-06-02 19:49:21 +08:00
lvmeng 32ec1d9b74 feat(pack): 添加净菜包装功能和食材追溯界面
- 在activity_pack.xml中新增rvFormList RecyclerView用于表单展示
- 在TaskDetailActivity中实现食材明细的增删改查功能
- 添加InboundIngredientSearchDialog用于食材溯源码搜索
- 更新NetViewModelV2中addComboTask方法参数类型
- 添加NutCleanInboundIngredientVO数据模型用于已入库净菜食材
- 在PackActivity中实现包装表单动态加载和设备选择功能
- 添加用量计算和重量识别相关UI组件
- 优化FormFieldAdapter适配器去除Activity上下文依赖
- 新增formatDecimal扩展函数用于格式化用量字符串
- 实现TaskDetailAdapter中食材项的选中和清除逻辑
- 添加组合约束的API接口定义和数据传输对象修改
- 重构表单字段配置为动态加载模式支持多种输入类型
- 添加删除确认对话框提升用户体验和数据安全性
- 优化布局文件结构调整控件间距和显示效果
- 添加设备选项加载回调机制确保数据实时更新
- 实现重量监听器的生命周期管理防止内存泄漏
- 更新界面跳转逻辑支持餐品选择和详情展示
- 修复多个布局和代码中的空格及格式问题
- 添加必要的依赖导入和常量定义完善功能模块
2026-06-02 18:05:47 +08:00
lvmeng 3fe66b513f feat(task): 添加组配任务功能模块
- 新增组配任务列表页面和详情页面
- 实现组配任务查询、搜索和分页功能
- 添加组配任务详情展示和状态管理
- 集成溯源码获取功能到任务详情页面
- 添加组配任务相关的API接口定义和实现
- 创建组配任务数据模型和适配器
- 在设置页面添加组配任务入口
- 重构食材物料列表项布局和显示逻辑
- 更新应用启动时的设备ID初始化逻辑
- 生成配比秤终端接口文档
2026-06-02 14:21:09 +08:00
lvmeng 9801534aad feat(ui): 新增餐品列表和菜品包装页面
- 创建了 MealListActivity 餐品列表页面,用于展示可选的餐品
- 实现了餐品列表的 RecyclerView 展示和点击跳转功能
- 添加了 PackActivity 菜品包装页面,支持食材构成管理
- 集成了相机预览功能用于食材识别
- 实现了重量识别和食材添加功能
- 添加了侧滑删除菜单和食材重量管理
- 创建了 MealListAdapter 适配器用于餐品列表展示
2026-06-02 11:14:48 +08:00
lvmeng 48d331f05d feat(ui): 添加餐品列表和套餐功能并优化食材显示
- 在 AndroidManifest.xml 中注册 MealListActivity 和 PackActivity
- 在 FoodMaterialAdapter 中添加溯源码显示功能
- 修改 GoodsItem 模型中 goodsName 和 materCode 为可变属性
- 更新 list_item_food_material 布局文件以支持溯源码显示
- 在 NutritionModels 中为 NutFoodComposition 添加 isMain 字段
- 将设置页面中的餐包功能重定向到 MealListActivity
2026-06-02 11:14:24 +08:00
lvmeng c2cf7289b7 feat(setting): 添加净菜包功能并优化设置页面
- 在BaseApp中新增ScaleDeviceConfig、AppUtil等依赖导入
- 恢复InitActivity中的原有路由逻辑,移除HomeV2Activity跳转
- 移除SettingActivity中设置项的硬编码标题,改用枚举默认值
- 新增CLEAN_PACKAGE、MEAL_PACKAGE、MEAL_PACKAGE_TEST三种设置类型
- 为净菜包功能添加对应的页面跳转逻辑
- 隐藏数据库查看、秤数据监控等调试功能项
2026-06-02 09:35:30 +08:00
lvmeng 3884738911 feat(form): 实现餐品选择后自动加载食材功能
- 添加 NutFoodOptionVO 模型导入并增加 foodOptionsData 缓存列表
- 新增 ingredientsField 表单字段用于包含食材动态加载
- 将食材名称字段类型从 DROPDOWN 改为 FIXED 类型并移除固定选项
- 实现餐品选中后根据 position 从 compositions 加载包含食材逻辑
- 在 DictType 中新增 position 属性用于索引定位
- 在 FormField 中新增 onDropdownSelected 回调函数属性
- 更新 FormFieldAdapter 中触发选中回调并在日期字段注释中调整说明
- 修改 IngredientRowAdapter 中根据字段类型区分 FIXED 和其他类型处理
- 添加 extraApiKeys 到 extraValues 映射逻辑并更新下拉选中回调
- 调整 list_item_ingredient_row.xml 中食材名称布局去除右侧边距和下拉箭头
- 注释掉 NetViewModelV2 中的 getFoodOptions 方法(暂未使用)
2026-06-01 20:05:00 +08:00
lvmeng a25ee7d987 feat(clean-pack): 实现净菜包装页面功能完善
- 集成称重功能到应用生命周期管理
- 添加全局用户名配置
- 更新测试环境基础URL地址
- 实现表单字段动态加载和回调机制
- 优化设备选项和字典项加载逻辑
- 修复餐品搜索结果显示问题
- 统一字段值映射关系配置
2026-06-01 19:17:32 +08:00
lvmeng 4a2abf6eec feat(form): 重构净菜包装表单实现动态字段和提交功能
- 移除固定 DTO 类型,改用 Map<String, Any> 接收 API 参数
- 实现动态表单字段配置,支持净菜和餐品两种包装类型
- 添加下拉选项动态加载和自动填充溯源码功能
- 实现表单验证和通用提交逻辑
- 添加回调版本的数据加载方法提高性能
- 优化字段绑定和值映射机制
2026-06-01 16:51:47 +08:00
lvmeng 5d6df58e46 feat(form): 实现餐品净菜包装表单功能
- 新增 CHILDREN 字段类型支持动态子列表
- 实现包含食材的嵌套表单结构
- 添加餐品包装相关字段配置
- 集成下拉选项和日期选择器组件
- 创建食材行适配器处理子列表渲染
- 更新表单布局和样式资源文件
2026-06-01 14:18:16 +08:00
lvmeng c4a2105737 feat(form): 新增净菜包装功能并优化表单字段类型
- 添加 CleanPackActivity 页面用于净菜包装表单
- 将 NUMBER 字段类型拆分为 INTEGER 和 DECIMAL 类型
- 更新 FormFieldAdapter 适配新的字段类型
- 在 HomeV2Activity 中添加跳转到净菜包装页面的功能
- 创建 ApiServiceV2、NetViewModelV2 和 RemoteRepositoryV2 处理新接口
- 添加营养相关模型类和分页组件
- 在 AndroidManifest.xml 中注册新 Activity
2026-06-01 11:21:04 +08:00
lvmeng d7a1b75fef feat(app): 添加 HomeV2Activity 并移除设备角色相关功能
- 在 AndroidManifest.xml 中注册新的 HomeV2Activity 页面
- 移除 BaseApp 中的设备角色判断和秤服务相关初始化代码
- 添加蓝色和橙色渐变按钮背景资源文件
- 修改 InitActivity 默认跳转至 HomeV2Activity 页面
- 移除与设备角色相关的路由逻辑代码
2026-06-01 10:42:47 +08:00
lvmeng 11963b8b12 feat(form): 添加动态表单组件和下拉选择功能
- 新增 FormField 数据模型定义五种字段类型(文本、数字、下拉、日期、固定值)
- 实现 FormFieldAdapter 适配器支持动态表单渲染和数据绑定
- 添加 DropdownPopup 弹窗组件和 DropdownAdapter 适配器
- 新增多个圆角背景资源文件(bg_gray_radius10、bg_white_radius10_stroke 等)
- 在 Common.kt 中添加 hideKeyboard 扩展函数用于隐藏软键盘
- 创建 DictType 数据模型用于字典类型数据传输
- 添加表单项和下拉项的布局文件(list_item_form_field、list_item_dropdown 等)
2026-06-01 10:19:07 +08:00
68 changed files with 6420 additions and 106 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ android {
compileSdk = 36
defaultConfig {
applicationId = "com.shuwei.dish.match"
applicationId = "com.shuwei.dish.match.new"
minSdk = 29
targetSdk = 36
versionCode = 1
+32
View File
@@ -46,6 +46,7 @@
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>
@@ -59,6 +60,36 @@
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"
@@ -128,6 +159,7 @@
android:launchMode="singleTask"
android:screenOrientation="landscape"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name="com.shuwei.dish.match.ui.FoodSearchV2Activity" />
<receiver
android:name=".utils.BootReceiver"
@@ -0,0 +1,31 @@
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()
}
}
}
@@ -14,6 +14,7 @@ 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) {
@@ -42,7 +43,11 @@ class FoodMaterialAdapter(list: MutableList<GoodsItem>) :
//} else {
// tvDishName.text = data.goodsName + "-${data.rawMaterialsType}"
//}
tvDishType.text = if (data.materialType == 1) "主辅材:主材" else if (data.materialType == 2) "主辅材:辅材" else ""
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 ""
tvDishWeight.text =
if (data.useWeight == null || data.useWeight == 0.toDouble()) "" else "${data.useWeight!!.roundedOneDecimalPlace()}"
tvDishWeight.setTextColor(
@@ -55,11 +60,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
@@ -0,0 +1,245 @@
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 中,无需额外操作
}
)
}
}
@@ -0,0 +1,40 @@
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"
}
}
}
@@ -0,0 +1,40 @@
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"
}
}
}
@@ -0,0 +1,149 @@
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)
}
}
}
@@ -0,0 +1,36 @@
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)
}
}
@@ -0,0 +1,53 @@
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)
}
}
@@ -0,0 +1,96 @@
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,15 +11,12 @@ 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() {
@@ -34,25 +31,28 @@ 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
// 根据设备 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}")
GlobalData.deviceId = AppUtil.getUDID(this)
// 启动秤服务(主设备多启动发现+聚合,子设备只启动服务端+注册)
ScaleServiceManager.start(this)
// 初始化秤,进程级生命周期管理:最后一个 Activity 销毁时才停止读取
// 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 销毁时才停止读取
WeightUtil.init()
WeightUtil.getWeight()
WeightUtil.startContinuousRead()
@@ -74,13 +74,14 @@ 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,13 +8,16 @@ package com.shuwei.dish.match.base
enum class DeviceRole { MASTER, SLAVE }
object GlobalData {
var appBaseUrl: String = TEST_BASE_URL
var appBaseUrl: String = LOCAL_BASE_URL
/**
* 具体业务 BaseUrl
*/
const val TEST_BASE_URL = "http://192.168.1.201:14801"
const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8083"
// 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"
/**
* 设备id
@@ -25,6 +28,8 @@ object GlobalData {
* 当前设备角色,启动时从 SpTool 读取,默认为 SLAVE
*/
var deviceRole: DeviceRole = DeviceRole.SLAVE
var userName = "张三"
}
/**
@@ -0,0 +1,78 @@
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.TEST_BASE_URL
contentBinding.rbUat.id -> GlobalData.UAT_BASE_URL
contentBinding.rbTest.id -> GlobalData.LOCAL_BASE_URL
contentBinding.rbUat.id -> GlobalData.TEST_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.TEST_BASE_URL -> contentBinding.rbTest.isChecked = true
GlobalData.UAT_BASE_URL -> contentBinding.rbUat.isChecked = true
GlobalData.LOCAL_BASE_URL -> contentBinding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> contentBinding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> contentBinding.rbProd.isChecked = true
}
}
@@ -0,0 +1,214 @@
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()
}
}
}
@@ -0,0 +1,25 @@
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
}
}
@@ -0,0 +1,52 @@
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 = "",
val goodsName: String? = null,
var goodsName: String? = null,
val popularName: String? = null,
val zjmCode: String? = null,
var materId: String? = null,
val materCode: String? = null,
var materCode: String? = null,
// 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用)
var materialType: Int = 0,
@@ -0,0 +1,264 @@
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
)
@@ -11,30 +11,25 @@ package com.shuwei.dish.match.model
*/
data class SettingItem(
val type: Type,
val title: String,
val title: String = type.label,
val subtitle: String? = null,
val isHidden: Boolean = false,
val onClick: () -> Unit
) {
/** 设置菜单项类型枚举 */
enum class Type {
/** 菜品模式切换 */
COOK_MODE,
/** 调料区设置 */
SEASONING_CONFIG,
/** 食材采集 */
FOOD_COLLECT,
/** 减重配置 */
WEIGHT_CONFIG,
/** 数据库查看(隐藏项) */
DB_INSPECT,
/** 秤数据监控(隐藏项) */
SCALE_OBSERVE,
/** 切换环境(隐藏项) */
ENV_SWITCH,
/** 数据清除(隐藏项) */
CLEAR_DATA,
/** 测试调料拿取(隐藏项) */
TEST_SEASONING,
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("测试调料拿取"),
}
}
@@ -0,0 +1,173 @@
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>
}
@@ -0,0 +1,327 @@
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))
}
}
}
@@ -0,0 +1,10 @@
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?
)
@@ -0,0 +1,109 @@
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) }
}
@@ -0,0 +1,716 @@
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
}
}
}
}
})
}
}
@@ -0,0 +1,204 @@
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)
}
}
}
@@ -0,0 +1,40 @@
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,12 +11,8 @@ 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
@@ -91,27 +87,28 @@ 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>()
}
}
// when (SpTool.cookMode) {
// 0 -> {
// startActivity<CookingModeActivity>()
// }
//
// 1 -> {
// goSampling()
// }
//
// else -> {
// startActivity<HomeActivity>()
// }
// }
startActivity<HomeV2Activity>()
// 延迟到下一帧 finish,避免与 LaunchActivityItem 事务竞态导致
// "Activity client record must not be null" 偶发崩溃
window.decorView.post { finish() }
@@ -0,0 +1,12 @@
package com.shuwei.dish.match.ui
object MaterialTypeTool {
fun getMaterialText(type: Int) = when (type) {
1 -> "主料"
2 -> "辅料"
3 -> "调料"
else -> ""
}
}
@@ -0,0 +1,153 @@
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)
}
}
}
@@ -0,0 +1,868 @@
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)
}
}
@@ -77,7 +77,6 @@ class SettingActivity : BaseActivity() {
private fun buildAllItems(): List<SettingItem> = listOf(
SettingItem(
type = SettingItem.Type.COOK_MODE,
title = "菜品模式",
subtitle = when (SpTool.cookMode) {
0 -> "当前:制作模式"
1 -> "当前:采样模式"
@@ -89,26 +88,50 @@ 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)
@@ -116,25 +139,21 @@ class SettingActivity : BaseActivity() {
),
SettingItem(
type = SettingItem.Type.SCALE_OBSERVE,
title = "秤数据监控",
isHidden = true,
onClick = { startActivity<MasterScaleActivity>() }
),
SettingItem(
type = SettingItem.Type.ENV_SWITCH,
title = "切换环境",
isHidden = true,
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> { } }
),
@@ -0,0 +1,212 @@
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 -> {}
}
}
)
}
}
@@ -0,0 +1,319 @@
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)
}
}
@@ -9,6 +9,7 @@ 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,6 +10,7 @@ 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
@@ -161,3 +162,21 @@ 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
}
}
@@ -0,0 +1,13 @@
<?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>
@@ -0,0 +1,13 @@
<?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>
@@ -0,0 +1,13 @@
<?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>
@@ -0,0 +1,6 @@
<?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>
@@ -0,0 +1,5 @@
<?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>
@@ -0,0 +1,7 @@
<?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>
@@ -0,0 +1,7 @@
<?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>
@@ -0,0 +1,9 @@
<?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>
@@ -0,0 +1,144 @@
<?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>
@@ -0,0 +1,92 @@
<?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>
@@ -0,0 +1,60 @@
<?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>
@@ -0,0 +1,56 @@
<?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
@@ -0,0 +1,205 @@
<?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>
+104
View File
@@ -0,0 +1,104 @@
<?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>
@@ -0,0 +1,225 @@
<?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>
@@ -0,0 +1,98 @@
<?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>
@@ -0,0 +1,19 @@
<?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>
@@ -0,0 +1,24 @@
<?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="120dp"
android:layout_height="140dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="15dp"
@@ -17,7 +17,6 @@
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="30sp"
@@ -33,18 +32,31 @@
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="26sp"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="22sp"
app:layout_constraintBottom_toTopOf="@id/tvGoodsCode"
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"
@@ -61,11 +73,12 @@
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_delete"
android:src="@drawable/ic_dish_clear"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
tools:ignore="ContentDescription"
android:visibility="gone"/>
<TextView
android:id="@+id/tvDishWeight"
@@ -0,0 +1,230 @@
<?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>
@@ -0,0 +1,49 @@
<?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>
@@ -0,0 +1,73 @@
<?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>
@@ -0,0 +1,113 @@
<?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>
@@ -0,0 +1,92 @@
<?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>
@@ -0,0 +1,104 @@
<?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.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

+1 -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB