diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt index 7626cc5..e720d3f 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt @@ -137,18 +137,11 @@ class FormFieldAdapter( popHeight = popHeight ) { dictType -> item.value = dictType.value ?: "" - item.valueId = dictType.id ?: "" b.tvDropdownValue.text = item.value // 将 extraApiKeys 映射的 DictType 字段值写入 extraValues item.extraValues.clear() item.extraApiKeys.forEach { (apiKey, dictField) -> - val fieldValue = when (dictField) { - "id" -> dictType.id ?: "" - "value" -> dictType.value ?: "" - "type" -> dictType.type - else -> "" - } - item.extraValues[apiKey] = fieldValue + item.extraValues[apiKey] = dictType[dictField] } }.also { it.bgLayout = b.flDropdown @@ -175,7 +168,6 @@ class FormFieldAdapter( } // value 存显示格式,valueId 存 ISO 格式供提交使用 item.value = displayDateFormat.format(selected.time) - item.valueId = isoFormat.format(selected.time) b.tvDateValue.text = item.value }, cal.get(Calendar.YEAR), @@ -224,7 +216,6 @@ class FormFieldAdapter( required = field.required, hint = field.hint, options = field.options, - submitValueId = field.submitValueId ) }.toMutableList() item.childrenRows.add(newRow) diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt index 16cc48b..7045843 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt @@ -122,7 +122,6 @@ class IngredientRowAdapter( popHeight = kotlin.math.min(field.options.size.coerceAtLeast(1), 4) * 50.dp ) { dictType -> field.value = dictType.value ?: "" - field.valueId = dictType.id ?: "" valueView.text = field.value // 回调通知外部更新 onChildClick(rowPosition, childIndex, field) diff --git a/app/src/main/java/com/shuwei/dish/match/model/DictType.kt b/app/src/main/java/com/shuwei/dish/match/model/DictType.kt index faf55c7..5bfcb66 100644 --- a/app/src/main/java/com/shuwei/dish/match/model/DictType.kt +++ b/app/src/main/java/com/shuwei/dish/match/model/DictType.kt @@ -11,4 +11,14 @@ data class DictType( val id: String? = null, val value: String? = null, val type: String = "" -) : Parcelable +) : Parcelable { + + /** + * 按字段名取值,供 extraApiKeys 映射直接使用 + */ + operator fun get(fieldName: String): String = when (fieldName) { + "id" -> id ?: "" + "value" -> value ?: "" + else -> type + } +} diff --git a/app/src/main/java/com/shuwei/dish/match/model/FormField.kt b/app/src/main/java/com/shuwei/dish/match/model/FormField.kt index 16b0eed..4c18eb0 100644 --- a/app/src/main/java/com/shuwei/dish/match/model/FormField.kt +++ b/app/src/main/java/com/shuwei/dish/match/model/FormField.kt @@ -28,9 +28,7 @@ enum class FieldType { * @param hidden 是否隐藏,hidden=true 时不渲染该字段 * @param hint 校验失败提示文案,同时作为输入框 hint * @param options 下拉框选项列表(DROPDOWN 类型使用) - * @param submitValueId 提交时使用 valueId 而非 value;DATE_PICKER 选 ISO 格式时设为 true * @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式 - * @param valueId DATE_PICKER 存 ISO 格式 yyyy-MM-dd'T'HH:mm:ss * @param extraValues 下拉选中后按 extraApiKeys 映射存储的提交值,key=接口参数名 * @param children 子字段模板(CHILDREN 类型使用),每行按此模板创建一组字段 * @param childAddLabel 添加按钮文案(CHILDREN 类型使用),如 "+ 添加食材" @@ -44,10 +42,8 @@ data class FormField( val required: Boolean = false, val hidden: Boolean = false, val hint: String = "", - val options: List = emptyList(), - val submitValueId: Boolean = false, + var options: MutableList = mutableListOf(), var value: String = "", - var valueId: String = "", val extraValues: MutableMap = mutableMapOf(), val children: List? = null, val childAddLabel: String? = null, diff --git a/app/src/main/java/com/shuwei/dish/match/net/ApiServiceV2.kt b/app/src/main/java/com/shuwei/dish/match/net/ApiServiceV2.kt index 6a3fd8c..cfb7f67 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/ApiServiceV2.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/ApiServiceV2.kt @@ -1,8 +1,6 @@ package com.shuwei.dish.match.net import com.shuwei.dish.match.base.GlobalData -import com.shuwei.dish.match.model.NutSupCleanPackageDTO -import com.shuwei.dish.match.model.NutSupMealPackageDTO import com.shuwei.dish.match.model.NutSupPkgDeviceVO import com.shuwei.dish.match.model.NutCanteenVO import com.shuwei.dish.match.model.NutDictItemVO @@ -123,7 +121,7 @@ interface ApiServiceV2 { @POST suspend fun addCleanPackage( @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/clean-package/add", - @Body param: NutSupCleanPackageDTO + @Body param: Map ): ApiResponse // ========== 四、餐品净菜包装接口 ========== @@ -134,7 +132,7 @@ interface ApiServiceV2 { @POST suspend fun addMealPackage( @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/meal-package/add", - @Body param: NutSupMealPackageDTO + @Body param: Map ): ApiResponse } diff --git a/app/src/main/java/com/shuwei/dish/match/net/NetViewModelV2.kt b/app/src/main/java/com/shuwei/dish/match/net/NetViewModelV2.kt index 25e1afb..31cb015 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/NetViewModelV2.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/NetViewModelV2.kt @@ -8,9 +8,7 @@ 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.NutSupCleanPackageDTO import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO -import com.shuwei.dish.match.model.NutSupMealPackageDTO import com.shuwei.dish.match.model.NutSupPkgDeviceVO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -81,6 +79,49 @@ class NetViewModelV2( } } + /** + * 字典项下拉(回调版本),不依赖 StateFlow,适合多次不同 dictType 的场景 + */ + fun getDictItemsWithCallback( + dictType: String, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.getDictItems(dictType)) + } + } + + /** + * 餐品下拉(回调版本),不依赖 StateFlow,适合多次调用的场景 + */ + fun getFoodOptionsWithCallback( + keyword: String? = null, + pageNum: Int = 1, + pageSize: Int = 10, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.getFoodOptions(keyword, pageNum, pageSize)) + } + } + + /** + * 设备下拉(回调版本),不依赖 StateFlow,适合多次调用的场景 + */ + fun getDeviceOptionsWithCallback( + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.getDeviceOptions()) + } + } + // 按溯源码查食材信息 private val _ingredientByTraceState = MutableStateFlow?>>(UiState.Idle) val ingredientByTraceState: StateFlow?>> = _ingredientByTraceState.asStateFlow() @@ -143,7 +184,7 @@ class NetViewModelV2( private val _addCleanPackageState = MutableStateFlow>(UiState.Idle) val addCleanPackageState: StateFlow> = _addCleanPackageState.asStateFlow() - fun addCleanPackage(param: NutSupCleanPackageDTO) { + fun addCleanPackage(param: Map) { viewModelScope.launch { _addCleanPackageState.value = UiState.Loading _addCleanPackageState.value = repository.addCleanPackage(param) @@ -155,7 +196,7 @@ class NetViewModelV2( private val _addMealPackageState = MutableStateFlow>(UiState.Idle) val addMealPackageState: StateFlow> = _addMealPackageState.asStateFlow() - fun addMealPackage(param: NutSupMealPackageDTO) { + fun addMealPackage(param: Map) { viewModelScope.launch { _addMealPackageState.value = UiState.Loading _addMealPackageState.value = repository.addMealPackage(param) diff --git a/app/src/main/java/com/shuwei/dish/match/net/RemoteRepositoryV2.kt b/app/src/main/java/com/shuwei/dish/match/net/RemoteRepositoryV2.kt index 249cc39..7158e0f 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/RemoteRepositoryV2.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/RemoteRepositoryV2.kt @@ -7,9 +7,7 @@ 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.NutSupCleanPackageDTO import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO -import com.shuwei.dish.match.model.NutSupMealPackageDTO import com.shuwei.dish.match.model.NutSupPkgDeviceVO import com.shuwei.dish.match.base.GlobalData import okhttp3.OkHttpClient @@ -81,12 +79,12 @@ class RemoteRepositoryV2 { // ========== 单品净菜包装接口 ========== - suspend fun addCleanPackage(param: NutSupCleanPackageDTO): UiState = + suspend fun addCleanPackage(param: Map): UiState = safeApiCall { apiService.addCleanPackage(param = param) } // ========== 餐品净菜包装接口 ========== - suspend fun addMealPackage(param: NutSupMealPackageDTO): UiState = + suspend fun addMealPackage(param: Map): UiState = safeApiCall { apiService.addMealPackage(param = param) } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt index 60197fb..09bd47e 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt @@ -1,10 +1,8 @@ package com.shuwei.dish.match.ui import android.os.Bundle +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager -import com.google.android.flexbox.AlignItems -import com.google.android.flexbox.FlexDirection -import com.google.android.flexbox.FlexWrap import com.shuwei.dish.match.R import com.shuwei.dish.match.adapter.FormFieldAdapter import com.shuwei.dish.match.base.BaseActivity @@ -12,9 +10,14 @@ 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.utils.ext.dp +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.toast import com.shuwei.dish.match.utils.ext.visible +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * 净菜包装表单页面,根据类型显示不同的表单字段 @@ -34,6 +37,17 @@ class CleanPackActivity : BaseActivity() { private lateinit var binding: ActivityCleanPackBinding private val fields = mutableListOf() private val formAdapter by lazy { FormFieldAdapter(this, 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 traceCodeJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -42,7 +56,7 @@ class CleanPackActivity : BaseActivity() { binding = ActivityCleanPackBinding.inflate(layoutInflater) setContentView(binding.root) setHeaderBackground() - val packType = intent.getIntExtra(EXTRA_TYPE, TYPE_CLEAN) + packType = intent.getIntExtra(EXTRA_TYPE, TYPE_CLEAN) // 根据类型设置标题 val title = when (packType) { @@ -84,7 +98,13 @@ class CleanPackActivity : BaseActivity() { } binding.btnSubmit.setOnClickListener { - // TODO: 提交表单数据 + hideKeyboard() + if (!validateFields()) return@setOnClickListener + + when (packType) { + TYPE_CLEAN -> submitCleanPackage() + TYPE_MEAL -> submitMealPackage() + } } } @@ -94,33 +114,38 @@ class CleanPackActivity : BaseActivity() { */ private fun initCleanPackFields() { fields.clear() - fields.add( - FormField( - label = "溯源码", - type = FieldType.TEXT, - apiKey = "traceCode", - required = true - ) + + // 溯源码 + traceCodeField = FormField( + label = "溯源码", + type = FieldType.TEXT, + apiKey = "traceCode", + required = true ) - fields.add( - FormField( - label = "净菜名称", - type = FieldType.TEXT, - apiKey = "cleanName", - required = true - ) + fields.add(traceCodeField!!) + + // 净菜名称 + cleanNameField = FormField( + label = "净菜名称", + type = FieldType.TEXT, + apiKey = "cleanName", + required = true ) - fields.add( - FormField( - label = "包装方式", - type = FieldType.TEXT, - apiKey = "packageMethod", - required = true - ) + fields.add(cleanNameField!!) + + // 包装方式(下拉) + packageMethodField = FormField( + label = "包装方式", + type = FieldType.DROPDOWN, + extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"), + required = true ) + fields.add(packageMethodField!!) + fields.add( FormField(label = "包装规格", type = FieldType.TEXT, apiKey = "spec", required = true) ) + fields.add( FormField( label = "包装份数", @@ -130,6 +155,7 @@ class CleanPackActivity : BaseActivity() { hint = "请输入整数" ) ) + fields.add( FormField( label = "包装日期", @@ -138,6 +164,7 @@ class CleanPackActivity : BaseActivity() { required = true ) ) + fields.add( FormField( label = "保质期至", @@ -146,6 +173,7 @@ class CleanPackActivity : BaseActivity() { required = true ) ) + fields.add( FormField( label = "存放温度(℃)", @@ -155,6 +183,7 @@ class CleanPackActivity : BaseActivity() { hint = "请输入小数" ) ) + fields.add( FormField( label = "包装人", @@ -163,10 +192,23 @@ class CleanPackActivity : BaseActivity() { required = false ) ) - fields.add( - FormField(label = "终端设备", type = FieldType.TEXT, apiKey = "", 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!!) + loadDeviceOptions() } /** @@ -176,37 +218,29 @@ class CleanPackActivity : BaseActivity() { private fun initMealPackFields() { fields.clear() - // 包装餐品 - fields.add( - FormField( - label = "包装餐品", type = FieldType.DROPDOWN, - apiKey = "foodId", required = true, - options = listOf( - DictType(id = "1", value = "1", type = "宫保鸡丁"), - DictType(id = "2", value = "2", type = "鱼香肉丝"), - DictType(id = "3", value = "3", type = "红烧茄子"), - ) - ) + // 包装餐品(下拉) + foodOptionsField = FormField( + label = "包装餐品", type = FieldType.DROPDOWN, + extraApiKeys = mapOf("foodId" to "id"), + required = true ) + fields.add(foodOptionsField!!) - // 包装规格 - fields.add( - FormField( - label = "包装规格", type = FieldType.DROPDOWN, - apiKey = "spec", required = true, - options = listOf( - DictType(id = "1", value = "500g/盒", type = "500g/盒"), - DictType(id = "2", value = "1kg/盒", type = "1kg/盒"), - ) - ) + // 包装规格(下拉) + packagingSpecField = FormField( + label = "包装规格", type = FieldType.DROPDOWN, + extraApiKeys = mapOf("spec" to "value"), + required = true ) + fields.add(packagingSpecField!!) - // 包装方式 + // 包装方式(下拉) fields.add( FormField( label = "包装方式", type = FieldType.DROPDOWN, - apiKey = "packageMethod", required = true, - options = listOf( + extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"), + required = true, + options = mutableListOf( DictType(id = "1", value = "真空包装", type = "真空包装"), DictType(id = "2", value = "气调包装", type = "气调包装"), ) @@ -223,24 +257,28 @@ class CleanPackActivity : BaseActivity() { // 包含食材(子列表容器) val ingredientTemplate = listOf( - FormField(label = "食材溯源码", type = FieldType.TEXT, required = true, hint = "溯源码"), + FormField(label = "食材溯源码", type = FieldType.TEXT, apiKey = "traceCode", required = true, hint = "溯源码"), FormField( - label = "食材名称", type = FieldType.DROPDOWN, required = true, - options = listOf( + label = "食材名称", type = FieldType.DROPDOWN, + extraApiKeys = mapOf("materId" to "id", "ingredientName" to "value"), + required = true, + options = mutableListOf( DictType(id = "1", value = "土豆", type = "土豆"), DictType(id = "2", value = "鸡肉", type = "鸡肉"), DictType(id = "3", value = "青椒", type = "青椒"), ) ), FormField( - label = "分类", type = FieldType.DROPDOWN, required = true, - options = listOf( + label = "分类", type = FieldType.DROPDOWN, + extraApiKeys = mapOf("classify" to "value"), + required = true, + options = mutableListOf( DictType(id = "1", value = "蔬菜类", type = "蔬菜类"), DictType(id = "2", value = "肉禽类", type = "肉禽类"), DictType(id = "3", value = "水产类", type = "水产类"), ) ), - FormField(label = "每份用量", type = FieldType.TEXT, required = true, hint = "如 120g"), + FormField(label = "每份用量", type = FieldType.TEXT, apiKey = "amount", required = true, hint = "如 120g"), ) fields.add( FormField( @@ -283,24 +321,21 @@ class CleanPackActivity : BaseActivity() { ) ) - // 终端设备 - fields.add( - FormField( - label = "终端设备", type = FieldType.DROPDOWN, - apiKey = "deviceId", required = false, - options = listOf( - DictType(id = "1", value = "1", type = "包装机-01"), - DictType(id = "2", value = "2", type = "包装机-02"), - ) - ) + // 终端设备(下拉) + 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", required = true, - options = listOf( + extraApiKeys = mapOf("opType" to "value"), + required = true, + options = mutableListOf( DictType(id = "1", value = "1", type = "设备自动"), DictType(id = "2", value = "2", type = "人工录入"), ) @@ -308,6 +343,287 @@ class CleanPackActivity : BaseActivity() { ) formAdapter.submitList(fields.filter { !it.hidden }) + + // 加载下拉选项 + loadFoodOptionsWithKeyword() + loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!) + loadDeviceOptionsWithCallbackForMeal() + } + + /** + * 监听溯源码字段变化,输入后 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() + } + + /** + * 加载餐品下拉选项(回调版本) + */ + private fun loadFoodOptionsWithKeyword() { + val field = foodOptionsField ?: return + viewModelV2.getFoodOptionsWithCallback( + onLoading = {}, + onResult = { state -> + when (state) { + is UiState.Success -> { + val options = state.data?.records?.map { vo -> + DictType( + id = vo.foodId?.toString() ?: "", + value = vo.foodId?.toString() ?: "", + type = vo.foodName ?: "" + ) + } ?: 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 -> {} + } + } + ) + } + + /** + * 加载字典项下拉(回调版本),适合多次不同 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.dictCode ?: "", + type = vo.dictLabel ?: "" + ) + } ?: 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 loadDeviceOptionsWithCallbackForMeal() { + 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.id?.toString() ?: "", + type = "${vo.deviceName ?: ""}(${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 -> {} + } + } + ) + } + + /** + * 加载字典项下拉选项 + * @param dictType 字典类型编码 + * @param targetField 目标字段 + */ + private fun loadDictItems(dictType: String, targetField: FormField) { + viewModelV2.getDictItems(dictType) + lifecycleScope.launch { + viewModelV2.dictItemsState.collect { state -> + when (state) { + is UiState.Success -> { + val options = state.data?.map { vo -> + DictType( + id = vo.id?.toString() ?: "", + value = vo.dictCode ?: "", + type = vo.dictLabel ?: "" + ) + } ?: 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 loadDeviceOptions() { + viewModelV2.getDeviceOptions() + val field = deviceField ?: return + lifecycleScope.launch { + viewModelV2.deviceOptionsState.collect { state -> + when (state) { + is UiState.Success -> { + val options = state.data?.map { vo -> + DictType( + id = vo.id?.toString() ?: "", + value = vo.id?.toString() ?: "", + type = "${vo.deviceName ?: ""}(${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 提交参数 Map,key 为接口参数名,value 为提交值 + */ + private fun collectSubmitValues(): MutableMap { + val params = mutableMapOf() + 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() + viewModelV2.addCleanPackage(params) + toast("提交成功") + finish() + } + + /** + * 提交餐品净菜包装 + */ + private fun submitMealPackage() { + val params = collectSubmitValues() + viewModelV2.addMealPackage(params) + toast("提交成功") + finish() } } diff --git a/screenshot-20260601-114152.png b/screenshot-20260601-114152.png new file mode 100644 index 0000000..69d2e1e Binary files /dev/null and b/screenshot-20260601-114152.png differ