feat(inbound): 新增入库秤毛菜/净材入库功能
- 新增入库专用网络层(InboundApiService、InboundRepository、InboundNetworkModule、InboundInterceptor) - 新增入库相关数据模型(IngredientByTrace、SupplierOption、ZoneOption、GoodsSearchModel、CleanInboundParam、RawInboundParam、InboundApiResponse) - 新增 FormField 动态表单模型(FieldType 枚举、apiKey 字段映射、submitValueId 控制提交值) - 新增 FormFieldAdapter,基于 FlexboxLayoutManager 实现两列自动换行表单,支持五种字段类型 - 新增 InboundGoodsSearchDialog(溯源码检索食材)和 InboundGoodsStoreDialog(入库录入,表单转 Map 提交) - 新增 StoreViewModel,提交接口入参改为 Map<String, Any> - 重构 StoreActivity:注入 StoreViewModel,onResume 预加载供应商/区域列表到 GlobalData - 修改 GoodsListActivity:抽取 onGoodsSearchRequested() 供子类重写 - 修改 GoodsRecognizeDialog:StoreActivity 场景下打开 InboundGoodsStoreDialog - GlobalData 新增 inboundSupplierList / inboundZoneList 全局缓存 - build.gradle.kts 新增 flexbox:3.0.0 依赖 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -156,6 +156,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation("org.greenrobot:eventbus:3.3.1")
|
implementation("org.greenrobot:eventbus:3.3.1")
|
||||||
|
|
||||||
|
// flexbox 布局,用于动态表单两列排列
|
||||||
|
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(plugin = "io.objectbox")
|
apply(plugin = "io.objectbox")
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.sw.inbound
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.sw.inbound.model.response.DictType
|
import com.sw.inbound.model.response.DictType
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
import com.sw.inbound.model.response.User
|
import com.sw.inbound.model.response.User
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
|
||||||
object GlobalData {
|
object GlobalData {
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +42,16 @@ object GlobalData {
|
|||||||
*/
|
*/
|
||||||
var unitTypeList: List<DictType> = arrayListOf()
|
var unitTypeList: List<DictType> = arrayListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库供应商列表(入库秤使用)
|
||||||
|
*/
|
||||||
|
var inboundSupplierList: List<SupplierOption> = emptyList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库仓库区域列表(入库秤使用)
|
||||||
|
*/
|
||||||
|
var inboundZoneList: List<ZoneOption> = emptyList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 具体业务 BaseUrl
|
* 具体业务 BaseUrl
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
goodsFile = file,
|
goodsFile = file,
|
||||||
goodsName = name
|
goodsName = name
|
||||||
) {
|
) {
|
||||||
GoodsSearchDialog(this).show()
|
onGoodsSearchRequested()
|
||||||
}.apply {
|
}.apply {
|
||||||
if (isFinishing.not() && isDestroyed.not()) {
|
if (isFinishing.not() && isDestroyed.not()) {
|
||||||
//isShowRecognizeDialog = false
|
//isShowRecognizeDialog = false
|
||||||
@@ -469,6 +469,11 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
|
|
||||||
var goodsRecognizeDialog: GoodsRecognizeDialog? = null
|
var goodsRecognizeDialog: GoodsRecognizeDialog? = null
|
||||||
|
|
||||||
|
/** 子类可重写以替换物品检索弹窗 */
|
||||||
|
protected open fun onGoodsSearchRequested() {
|
||||||
|
GoodsSearchDialog(this).show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun replaceIncludeContent() {
|
private fun replaceIncludeContent() {
|
||||||
binding.flContainer.removeAllViews()
|
binding.flContainer.removeAllViews()
|
||||||
//isReceiptPage
|
//isReceiptPage
|
||||||
|
|||||||
@@ -111,14 +111,14 @@ class HomeActivity : BaseActivity() {
|
|||||||
// 净材按钮点击提示
|
// 净材按钮点击提示
|
||||||
binding.btnOrderPurchase.setOnClickListener {
|
binding.btnOrderPurchase.setOnClickListener {
|
||||||
startActivity<StoreActivity> {
|
startActivity<StoreActivity> {
|
||||||
//putExtra(GoodsListActivity.IS_RECEIPT_PAGE, false)
|
putExtra(StoreActivity.EXTRA_STORE_TYPE, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 毛菜按钮点击提示
|
// 毛菜按钮点击提示
|
||||||
binding.btnOrderSelfProcurement.setOnClickListener {
|
binding.btnOrderSelfProcurement.setOnClickListener {
|
||||||
startActivity<StoreActivity> {
|
startActivity<StoreActivity> {
|
||||||
//putExtra(GoodsListActivity.IS_RECEIPT_PAGE, false)
|
putExtra(StoreActivity.EXTRA_STORE_TYPE, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,44 @@
|
|||||||
package com.sw.inbound.activity
|
package com.sw.inbound.activity
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.activity.viewModels
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.dialog.InboundGoodsSearchDialog
|
||||||
import com.sw.inbound.model.response.DictType
|
import com.sw.inbound.model.response.DictType
|
||||||
import com.sw.inbound.utils.ext.gone
|
import com.sw.inbound.utils.ext.gone
|
||||||
|
import com.sw.inbound.viewmodel.StoreViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入库称重页面
|
* 入库称重页面
|
||||||
* 继承 GoodsListActivity 以复用秤监听、相机拍照、菜品识别弹窗等核心能力,
|
* 继承 GoodsListActivity 以复用秤监听、相机拍照、菜品识别弹窗等核心能力,
|
||||||
* 同时通过隐藏不需要的 View 精简界面,避免大量重复代码。
|
* 通过隐藏不需要的 View 精简界面,避免大量重复代码。
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class StoreActivity : GoodsListActivity() {
|
class StoreActivity : GoodsListActivity() {
|
||||||
|
|
||||||
|
val storeViewModel: StoreViewModel by viewModels()
|
||||||
|
|
||||||
|
/** 0=净材,1=毛菜,由 HomeActivity 通过 Intent 传入 */
|
||||||
|
var storeType: Int = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
storeType = intent.getIntExtra(EXTRA_STORE_TYPE, 0)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
hideUnusedViews()
|
hideUnusedViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
preloadInboundOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预加载供应商和仓库区域列表到 GlobalData,供弹窗直接使用 */
|
||||||
|
private fun preloadInboundOptions() {
|
||||||
|
storeViewModel.loadSupplierOptions(callback = { GlobalData.inboundSupplierList = it })
|
||||||
|
storeViewModel.loadZoneOptions(callback = { GlobalData.inboundZoneList = it })
|
||||||
|
}
|
||||||
|
|
||||||
/** 隐藏采购单列表、仓库下拉、提交按钮等本页面不需要的控件 */
|
/** 隐藏采购单列表、仓库下拉、提交按钮等本页面不需要的控件 */
|
||||||
private fun hideUnusedViews() {
|
private fun hideUnusedViews() {
|
||||||
binding.flWarehouseDropdown.gone()
|
binding.flWarehouseDropdown.gone()
|
||||||
@@ -31,13 +51,18 @@ class StoreActivity : GoodsListActivity() {
|
|||||||
binding.rvGoodsList.gone()
|
binding.rvGoodsList.gone()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initRecyclerView() {
|
override fun initRecyclerView() {}
|
||||||
// 本页面无列表,空实现
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initView() {
|
override fun initView() {}
|
||||||
// 本页面无额外初始化逻辑,空实现
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getWarehouseList(): List<DictType> = emptyList()
|
override fun getWarehouseList(): List<DictType> = emptyList()
|
||||||
|
|
||||||
|
/** 使用入库专用检索弹窗替代原有 GoodsSearchDialog */
|
||||||
|
override fun onGoodsSearchRequested() {
|
||||||
|
InboundGoodsSearchDialog(this).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_STORE_TYPE = "storeType"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package com.sw.inbound.adapter
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.InputType
|
||||||
|
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 com.chad.library.adapter4.BaseQuickAdapter
|
||||||
|
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||||
|
import com.google.android.flexbox.FlexboxLayoutManager
|
||||||
|
import com.sw.inbound.R
|
||||||
|
import com.sw.inbound.databinding.ListItemFormFieldBinding
|
||||||
|
import com.sw.inbound.dialog.DropdownPopup
|
||||||
|
import com.sw.inbound.model.FieldType
|
||||||
|
import com.sw.inbound.model.FormField
|
||||||
|
import com.sw.inbound.utils.ext.dp
|
||||||
|
import com.sw.inbound.utils.ext.gone
|
||||||
|
import com.sw.inbound.utils.ext.hideKeyboard
|
||||||
|
import com.sw.inbound.utils.ext.visible
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态表单字段 Adapter,配合 FlexboxLayoutManager 实现两列自动换行布局
|
||||||
|
* 单一 item 布局,通过 visibility 控制 TEXT/NUMBER/FIXED/DROPDOWN/DATE_PICKER 五种类型渲染
|
||||||
|
*
|
||||||
|
* @param activityContext Activity context,用于创建 DatePickerDialog
|
||||||
|
* @param fields 表单字段列表(hidden=false 的字段,由外部过滤后传入)
|
||||||
|
*/
|
||||||
|
class FormFieldAdapter(
|
||||||
|
private val activityContext: Context,
|
||||||
|
fields: MutableList<FormField>
|
||||||
|
) : BaseQuickAdapter<FormField, FormFieldAdapter.VH>(fields) {
|
||||||
|
|
||||||
|
private val isoFormat = SimpleDateFormat("yyyy-MM-dd'T'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)
|
||||||
|
// 设置每个 item 宽度占 FlexboxLayout 的 50%,实现两列布局
|
||||||
|
(binding.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.flexBasisPercent = 0.5f
|
||||||
|
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.etInput.gone()
|
||||||
|
b.flDropdown.gone()
|
||||||
|
b.flDatePicker.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.NUMBER -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定可编辑文本框(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 = activityContext,
|
||||||
|
list = item.options.toMutableList(),
|
||||||
|
popWidth = v.width,
|
||||||
|
popHeight = popHeight
|
||||||
|
) { dictType ->
|
||||||
|
item.value = dictType.value ?: ""
|
||||||
|
item.valueId = dictType.id ?: ""
|
||||||
|
b.tvDropdownValue.text = item.value
|
||||||
|
}.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(
|
||||||
|
activityContext,
|
||||||
|
{ _, year, month, dayOfMonth ->
|
||||||
|
val selected = Calendar.getInstance().apply {
|
||||||
|
set(year, month, dayOfMonth, 0, 0, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}
|
||||||
|
// value 存显示格式,valueId 存 ISO 格式供提交使用
|
||||||
|
item.value = displayDateFormat.format(selected.time)
|
||||||
|
item.valueId = isoFormat.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,
|
||||||
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RecyclerView 回收 ViewHolder 时移除 TextWatcher,防止复用错误 */
|
||||||
|
override fun onViewRecycled(holder: VH) {
|
||||||
|
super.onViewRecycled(holder)
|
||||||
|
val watcher = holder.binding.etInput.tag as? TextWatcher
|
||||||
|
watcher?.let { holder.binding.etInput.removeTextChangedListener(it) }
|
||||||
|
holder.binding.etInput.tag = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.sw.inbound.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import com.chad.library.adapter4.BaseDifferAdapter
|
||||||
|
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||||
|
import com.sw.inbound.databinding.ListItemGoodsSearchBinding
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
|
import com.sw.inbound.utils.ext.dp
|
||||||
|
import com.sw.inbound.utils.ext.setShapeDrawable
|
||||||
|
|
||||||
|
class InboundSearchAdapter :
|
||||||
|
BaseDifferAdapter<GoodsSearchModel, InboundSearchAdapter.VH>(DIFF_CALLBACK) {
|
||||||
|
|
||||||
|
inner class VH(val binding: ListItemGoodsSearchBinding) : QuickViewHolder(binding.root)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val binding = ListItemGoodsSearchBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
|
return VH(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int, item: GoodsSearchModel?) {
|
||||||
|
holder.binding.tvGoodsName.run {
|
||||||
|
text = item?.materName
|
||||||
|
setTextColor(
|
||||||
|
if (item?.isSelected == true) "#FF0032C8".toColorInt()
|
||||||
|
else "#FF666666".toColorInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
holder.binding.root.run {
|
||||||
|
if (item?.isSelected == true) {
|
||||||
|
setShapeDrawable(
|
||||||
|
solidColor = "#FFEAF0FF",
|
||||||
|
strokeColor = "#FF0033CC",
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
radius = 10.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setShapeDrawable(solidColor = "#FFF5F5F5", radius = 10.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<GoodsSearchModel>() {
|
||||||
|
override fun areItemsTheSame(old: GoodsSearchModel, new: GoodsSearchModel) =
|
||||||
|
old.materId == new.materId
|
||||||
|
|
||||||
|
override fun areContentsTheSame(old: GoodsSearchModel, new: GoodsSearchModel) =
|
||||||
|
old == new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.sw.inbound.di
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.sw.inbound.MyApp
|
||||||
|
import com.sw.inbound.network.api.InboundApiService
|
||||||
|
import com.sw.inbound.network.interceptor.InboundInterceptor
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object InboundNetworkModule {
|
||||||
|
|
||||||
|
private const val INBOUND_BASE_URL = "http://192.168.10.101:24801/"
|
||||||
|
private const val TIME_OUT = 30L
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named("inbound")
|
||||||
|
fun provideInboundHttpClient(): OkHttpClient {
|
||||||
|
return OkHttpClient.Builder()
|
||||||
|
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
|
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
|
||||||
|
Timber.d("inbound okhttp ==> $it")
|
||||||
|
}).apply {
|
||||||
|
level = if (MyApp.DEBUG) HttpLoggingInterceptor.Level.BODY
|
||||||
|
else HttpLoggingInterceptor.Level.NONE
|
||||||
|
})
|
||||||
|
.addInterceptor(InboundInterceptor())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named("inbound")
|
||||||
|
fun provideInboundRetrofit(@Named("inbound") okHttpClient: OkHttpClient): Retrofit {
|
||||||
|
val gson = GsonBuilder().setLenient().create()
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(INBOUND_BASE_URL)
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideInboundApiService(@Named("inbound") retrofit: Retrofit): InboundApiService {
|
||||||
|
return retrofit.create(InboundApiService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.sw.inbound.activity.GoodsListActivity
|
import com.sw.inbound.activity.GoodsListActivity
|
||||||
|
import com.sw.inbound.activity.StoreActivity
|
||||||
import com.sw.inbound.adapter.RecognizeAdapter
|
import com.sw.inbound.adapter.RecognizeAdapter
|
||||||
import com.sw.inbound.databinding.DialogGoodsRecognizeBinding
|
import com.sw.inbound.databinding.DialogGoodsRecognizeBinding
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
import com.sw.inbound.model.response.SearchGoodsInfo
|
import com.sw.inbound.model.response.SearchGoodsInfo
|
||||||
import com.sw.inbound.utils.ext.clickWithDebounce
|
import com.sw.inbound.utils.ext.clickWithDebounce
|
||||||
import com.sw.inbound.utils.ext.gone
|
import com.sw.inbound.utils.ext.gone
|
||||||
@@ -57,6 +59,17 @@ class GoodsRecognizeDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showGoodsStoreDialog(position: Int) {
|
private fun showGoodsStoreDialog(position: Int) {
|
||||||
|
if (activity is StoreActivity) {
|
||||||
|
// 入库称重页面:识别结果无溯源码,直接用物品名称构造 GoodsSearchModel 打开入库录入弹窗
|
||||||
|
val goods = list[position]
|
||||||
|
InboundGoodsStoreDialog(
|
||||||
|
context = context,
|
||||||
|
goods = GoodsSearchModel(materName = goods.goodsName ?: "")
|
||||||
|
) {
|
||||||
|
dismiss()
|
||||||
|
}.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
GoodsStoreDialog(
|
GoodsStoreDialog(
|
||||||
context = context,
|
context = context,
|
||||||
funcType = 0,
|
funcType = 0,
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.sw.inbound.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.sw.inbound.activity.StoreActivity
|
||||||
|
import com.sw.inbound.adapter.InboundSearchAdapter
|
||||||
|
import com.sw.inbound.databinding.DialogInboundGoodsSearchBinding
|
||||||
|
import com.sw.inbound.databinding.LayoutEmptySearchBinding
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
|
import com.sw.inbound.utils.ext.addOnActionSearchListener
|
||||||
|
import com.sw.inbound.utils.ext.hideKeyboard
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
|
|
||||||
|
/** 入库物品检索弹窗,使用溯源码查询食材,供 StoreActivity 使用 */
|
||||||
|
class InboundGoodsSearchDialog(
|
||||||
|
context: Context
|
||||||
|
) : BaseDialog(context) {
|
||||||
|
|
||||||
|
private lateinit var inflater: LayoutInflater
|
||||||
|
private var emptyBinding: LayoutEmptySearchBinding? = null
|
||||||
|
private lateinit var binding: DialogInboundGoodsSearchBinding
|
||||||
|
|
||||||
|
override fun getRootView(): View {
|
||||||
|
inflater = LayoutInflater.from(context)
|
||||||
|
binding = DialogInboundGoodsSearchBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private var storeActivity: StoreActivity? = null
|
||||||
|
|
||||||
|
override fun initView() {
|
||||||
|
storeActivity = getReceiptActivity() as? StoreActivity
|
||||||
|
binding.ivClose.setOnClickListener { dismiss() }
|
||||||
|
binding.ivGoodsSearch.setOnClickListener { searchGoods() }
|
||||||
|
binding.etInputGoods.addOnActionSearchListener { searchGoods() }
|
||||||
|
binding.btnConfirm.setOnClickListener {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
context.toast("请搜索物品")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
if (clickIndex == -1) {
|
||||||
|
context.toast("请选择物品")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
InboundGoodsStoreDialog(
|
||||||
|
context = context,
|
||||||
|
goods = list[clickIndex]
|
||||||
|
) {
|
||||||
|
storeActivity?.goodsRecognizeDialog?.dismiss()
|
||||||
|
dismiss()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
binding.rvSearch.let {
|
||||||
|
it.layoutManager = GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false)
|
||||||
|
it.adapter = searchAdapter
|
||||||
|
}
|
||||||
|
binding.refreshLayout.let {
|
||||||
|
it.setEnableRefresh(false)
|
||||||
|
it.setEnableLoadMore(false)
|
||||||
|
}
|
||||||
|
loadEmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchGoods() {
|
||||||
|
val traceCode = binding.etInputGoods.text.toString().trim()
|
||||||
|
if (traceCode.isBlank()) {
|
||||||
|
context.toast("请输入溯源码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeActivity?.storeViewModel?.getIngredientByTrace(traceCode) { items ->
|
||||||
|
binding.etInputGoods.hideKeyboard()
|
||||||
|
val models = items.map { GoodsSearchModel.from(it) }
|
||||||
|
if (models.isEmpty()) {
|
||||||
|
loadEmptyView()
|
||||||
|
searchAdapter.submitList(emptyList())
|
||||||
|
return@getIngredientByTrace
|
||||||
|
}
|
||||||
|
list.clear()
|
||||||
|
list.addAll(models)
|
||||||
|
binding.rvSearch.layoutManager =
|
||||||
|
GridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false)
|
||||||
|
searchAdapter.submitList(list.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadEmptyView() {
|
||||||
|
binding.rvSearch.layoutManager =
|
||||||
|
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
if (emptyBinding == null) {
|
||||||
|
emptyBinding = LayoutEmptySearchBinding.inflate(inflater, binding.rvSearch, false)
|
||||||
|
}
|
||||||
|
emptyBinding?.root?.let { layout ->
|
||||||
|
layout.setOnClickListener { layout.hideKeyboard() }
|
||||||
|
searchAdapter.stateView = layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var clickIndex = -1
|
||||||
|
private val list: MutableList<GoodsSearchModel> = mutableListOf()
|
||||||
|
|
||||||
|
private val searchAdapter by lazy {
|
||||||
|
InboundSearchAdapter().apply {
|
||||||
|
isStateViewEnable = true
|
||||||
|
setOnItemClickListener { _, _, position ->
|
||||||
|
clickIndex = position
|
||||||
|
if (list[position].isSelected) return@setOnItemClickListener
|
||||||
|
list.forEach { it.isSelected = false }
|
||||||
|
list[position].isSelected = true
|
||||||
|
submitList(list.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package com.sw.inbound.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import com.google.android.flexbox.FlexDirection
|
||||||
|
import com.google.android.flexbox.FlexWrap
|
||||||
|
import com.google.android.flexbox.FlexboxLayoutManager
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.activity.StoreActivity
|
||||||
|
import com.sw.inbound.adapter.FormFieldAdapter
|
||||||
|
import com.sw.inbound.databinding.DialogInboundGoodsStoreBinding
|
||||||
|
import com.sw.inbound.databinding.LayoutTakePhotoBinding
|
||||||
|
import com.sw.inbound.model.FieldType
|
||||||
|
import com.sw.inbound.model.FormField
|
||||||
|
import com.sw.inbound.model.response.DictType
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
|
import com.sw.inbound.sdk.SensorScaleUtils
|
||||||
|
import com.sw.inbound.utils.CameraUtils
|
||||||
|
import com.sw.inbound.utils.ext.appendText
|
||||||
|
import com.sw.inbound.utils.ext.buildSpannableString
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/** 入库录入弹窗,使用 FlexboxLayoutManager 动态表单,根据 storeType 展示净材/毛菜对应字段 */
|
||||||
|
class InboundGoodsStoreDialog(
|
||||||
|
context: Context,
|
||||||
|
private val goods: GoodsSearchModel,
|
||||||
|
private val confirmBlock: () -> Unit = {}
|
||||||
|
) : BaseDialog(context) {
|
||||||
|
|
||||||
|
private lateinit var binding: DialogInboundGoodsStoreBinding
|
||||||
|
private lateinit var inflater: LayoutInflater
|
||||||
|
|
||||||
|
private var storeActivity: StoreActivity? = null
|
||||||
|
|
||||||
|
/** 0=净材,1=毛菜 */
|
||||||
|
private var storeType: Int = 0
|
||||||
|
|
||||||
|
private var realWeight: Int = 0
|
||||||
|
|
||||||
|
private val isoFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())
|
||||||
|
|
||||||
|
/** 当前渲染的表单字段列表 */
|
||||||
|
private lateinit var fields: MutableList<FormField>
|
||||||
|
|
||||||
|
private var previewView: PreviewView? = null
|
||||||
|
private var takePhotoBinding: LayoutTakePhotoBinding? = null
|
||||||
|
private val cameraUtils: CameraUtils by lazy { CameraUtils(storeActivity!!) }
|
||||||
|
|
||||||
|
override fun getRootView(): View {
|
||||||
|
inflater = LayoutInflater.from(context)
|
||||||
|
binding = DialogInboundGoodsStoreBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initView() {
|
||||||
|
storeActivity = getReceiptActivity() as? StoreActivity
|
||||||
|
storeType = storeActivity?.storeType ?: 0
|
||||||
|
|
||||||
|
buildFormFields()
|
||||||
|
setupRecyclerView()
|
||||||
|
setupButtons()
|
||||||
|
addChildToContainer()
|
||||||
|
addWeightListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFormFields() {
|
||||||
|
fields = if (storeType == 1) buildRawFields() else buildCleanFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 毛菜字段列表
|
||||||
|
* 接口字段:traceCode, supplierType(额外注入), materialName, supplierId, spec, quantity,
|
||||||
|
* weight(额外注入), inboundTime, zoneId, operator
|
||||||
|
* 注:来源类型无对应接口字段,apiKey="" 不提交
|
||||||
|
*/
|
||||||
|
private fun buildRawFields(): MutableList<FormField> {
|
||||||
|
val supplierOptions = GlobalData.inboundSupplierList.mapNotNull { opt ->
|
||||||
|
val id = opt.id?.toString() ?: return@mapNotNull null
|
||||||
|
DictType(id = id, value = opt.name)
|
||||||
|
}
|
||||||
|
val zoneOptions = GlobalData.inboundZoneList.mapNotNull { zone ->
|
||||||
|
val id = zone.id?.toString() ?: return@mapNotNull null
|
||||||
|
DictType(id = id, value = zone.zoneName)
|
||||||
|
}
|
||||||
|
return mutableListOf(
|
||||||
|
FormField("溯源码", FieldType.TEXT, apiKey = "traceCode", value = goods.traceCode),
|
||||||
|
FormField("来源类型", FieldType.DROPDOWN, apiKey = "", options = listOf(
|
||||||
|
DictType("0", "手动录入"), DictType("1", "系统识别")
|
||||||
|
)),
|
||||||
|
FormField("食材名称", FieldType.FIXED, apiKey = "materialName", required = true, value = goods.materName),
|
||||||
|
FormField("供应商", FieldType.DROPDOWN, apiKey = "supplierId", required = true,
|
||||||
|
hint = "请选择供应商", options = supplierOptions, submitValueId = true),
|
||||||
|
FormField("入库规格", FieldType.TEXT, apiKey = "spec", required = true, hint = "请录入入库规格"),
|
||||||
|
FormField("入库数量", FieldType.NUMBER, apiKey = "quantity", required = true, hint = "请录入入库数量"),
|
||||||
|
FormField("入库时间", FieldType.DATE_PICKER, apiKey = "inboundTime", required = true,
|
||||||
|
hint = "请选择入库时间", submitValueId = true),
|
||||||
|
FormField("存放区域", FieldType.DROPDOWN, apiKey = "zoneId", required = true,
|
||||||
|
hint = "请选择存放区域", options = zoneOptions, submitValueId = true),
|
||||||
|
FormField("操作人", FieldType.TEXT, apiKey = "operator", value = GlobalData.user?.name ?: "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 净材字段列表
|
||||||
|
* 接口字段:traceCode, ingredientName, cleanType, spec, quantity,
|
||||||
|
* weight(额外注入), inboundTime(额外注入当前时间), zoneId, operator,
|
||||||
|
* expiryDate, storageTemp
|
||||||
|
*/
|
||||||
|
private fun buildCleanFields(): MutableList<FormField> {
|
||||||
|
val vegTypes = goods.vegTypeList()
|
||||||
|
val cleanTypeOptions = vegTypes.mapIndexed { index, type ->
|
||||||
|
DictType(id = index.toString(), value = type)
|
||||||
|
}
|
||||||
|
val zoneOptions = GlobalData.inboundZoneList.mapNotNull { zone ->
|
||||||
|
val id = zone.id?.toString() ?: return@mapNotNull null
|
||||||
|
DictType(id = id, value = zone.zoneName)
|
||||||
|
}
|
||||||
|
return mutableListOf(
|
||||||
|
FormField("溯源码", FieldType.TEXT, apiKey = "traceCode", value = goods.traceCode),
|
||||||
|
FormField("食材名称", FieldType.FIXED, apiKey = "ingredientName", required = true, value = goods.materName),
|
||||||
|
FormField("净材类型", FieldType.DROPDOWN, apiKey = "cleanType",
|
||||||
|
required = cleanTypeOptions.isNotEmpty(), hint = "请选择净材类型", options = cleanTypeOptions),
|
||||||
|
FormField("入库规格", FieldType.TEXT, apiKey = "spec", required = true, hint = "请录入入库规格"),
|
||||||
|
FormField("入库数量", FieldType.NUMBER, apiKey = "quantity", required = true, hint = "请录入入库数量"),
|
||||||
|
FormField("保质期至", FieldType.DATE_PICKER, apiKey = "expiryDate",
|
||||||
|
hint = "请选择保质期", submitValueId = true),
|
||||||
|
FormField("存放区域", FieldType.DROPDOWN, apiKey = "zoneId", required = true,
|
||||||
|
hint = "请选择存放区域", options = zoneOptions, submitValueId = true),
|
||||||
|
FormField("存放温度(℃)", FieldType.NUMBER, apiKey = "storageTemp"),
|
||||||
|
FormField("操作人", FieldType.TEXT, apiKey = "operator", value = GlobalData.user?.name ?: "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
val flexboxLM = FlexboxLayoutManager(context).apply {
|
||||||
|
flexDirection = FlexDirection.ROW
|
||||||
|
flexWrap = FlexWrap.WRAP
|
||||||
|
}
|
||||||
|
binding.rvFormFields.layoutManager = flexboxLM
|
||||||
|
binding.rvFormFields.adapter = FormFieldAdapter(storeActivity ?: context, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupButtons() {
|
||||||
|
binding.btnCancel.setOnClickListener { dismiss() }
|
||||||
|
binding.btnConfirm.setOnClickListener { confirmInputData() }
|
||||||
|
binding.tvTitle.setOnClickListener { SensorScaleUtils.startScale(autoScale = true) }
|
||||||
|
binding.btnClearZero.setOnClickListener { SensorScaleUtils.tare() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addWeightListener() {
|
||||||
|
storeActivity?.readWeightInfo { weight ->
|
||||||
|
if (abs(this.realWeight - weight) < 10) return@readWeightInfo
|
||||||
|
this.realWeight = weight
|
||||||
|
binding.tvTotalWeight.text = buildWeightSpannable(weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWeightSpannable(num: Int): SpannableStringBuilder {
|
||||||
|
return buildSpannableString {
|
||||||
|
appendText(num.toString(), ForegroundColorSpan("#FF141428".toColorInt()), StyleSpan(Typeface.BOLD))
|
||||||
|
appendText(" 克", ForegroundColorSpan("#FF000033".toColorInt()), StyleSpan(Typeface.BOLD))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验必填字段,通过后将 fields 转为 Map 提交 */
|
||||||
|
private fun confirmInputData() {
|
||||||
|
val invalid = fields.firstOrNull { it.required && it.value.isBlank() }
|
||||||
|
if (invalid != null) {
|
||||||
|
context.toast(invalid.hint.ifBlank { "${invalid.label}不能为空" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (realWeight <= 0) {
|
||||||
|
context.toast("物品重量需大于0")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (storeType == 1) submitRawInbound() else submitCleanInbound()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 fields 转为接口 Map,规则:
|
||||||
|
* - apiKey 为空的字段跳过
|
||||||
|
* - submitValueId=true 时取 valueId,否则取 value
|
||||||
|
* - 空字符串跳过(可选字段无值不传)
|
||||||
|
* - quantity → Int,zoneId/supplierId → Long,storageTemp → Double,其余 → String
|
||||||
|
*/
|
||||||
|
private fun buildParamMap(): MutableMap<String, Any> {
|
||||||
|
val map = mutableMapOf<String, Any>()
|
||||||
|
for (field in fields) {
|
||||||
|
if (field.apiKey.isBlank()) continue
|
||||||
|
val raw = if (field.submitValueId) field.valueId else field.value
|
||||||
|
if (raw.isBlank()) continue
|
||||||
|
val converted: Any = when (field.apiKey) {
|
||||||
|
"quantity" -> raw.toIntOrNull() ?: continue
|
||||||
|
"zoneId", "supplierId" -> raw.toLongOrNull() ?: continue
|
||||||
|
"storageTemp" -> raw.toDoubleOrNull() ?: continue
|
||||||
|
else -> raw
|
||||||
|
}
|
||||||
|
map[field.apiKey] = converted
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitRawInbound() {
|
||||||
|
val param = buildParamMap()
|
||||||
|
// weight 来自秤,不在表单中,额外注入
|
||||||
|
param["weight"] = realWeight / 1000.0
|
||||||
|
// supplierType 从 GlobalData 查找,不在表单中
|
||||||
|
val supplierId = param["supplierId"]?.toString()
|
||||||
|
val supplierOpt = GlobalData.inboundSupplierList.find { it.id?.toString() == supplierId }
|
||||||
|
param["supplierType"] = (supplierOpt?.type ?: 1)
|
||||||
|
|
||||||
|
Loading.show(storeActivity ?: return)
|
||||||
|
storeActivity?.storeViewModel?.submitRawInbound(param) { success, msg ->
|
||||||
|
Loading.dismiss()
|
||||||
|
if (success) {
|
||||||
|
context.toast("入库成功")
|
||||||
|
confirmBlock()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
context.toast(msg ?: "入库失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitCleanInbound() {
|
||||||
|
val param = buildParamMap()
|
||||||
|
// weight 来自秤,不在表单中,额外注入
|
||||||
|
param["weight"] = realWeight / 1000.0
|
||||||
|
// inboundTime 净菜取当前时间,不在表单中
|
||||||
|
param["inboundTime"] = isoFormat.format(Date())
|
||||||
|
|
||||||
|
Loading.show(storeActivity ?: return)
|
||||||
|
storeActivity?.storeViewModel?.submitCleanInbound(param) { success, msg ->
|
||||||
|
Loading.dismiss()
|
||||||
|
if (success) {
|
||||||
|
context.toast("入库成功")
|
||||||
|
confirmBlock()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
context.toast(msg ?: "入库失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addChildToContainer() {
|
||||||
|
takePhotoBinding = LayoutTakePhotoBinding.inflate(inflater, binding.flContainer, true)
|
||||||
|
storeActivity?.unBindCamera()
|
||||||
|
setOnDismissListener { cameraUtils.unbind() }
|
||||||
|
cameraUtils.initCamera()
|
||||||
|
takePhotoBinding?.let {
|
||||||
|
previewView = it.previewView
|
||||||
|
it.flCameraPreview.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun show() {
|
||||||
|
super.show()
|
||||||
|
takePhotoBinding?.let {
|
||||||
|
cameraUtils.setPreviewController(previewView)
|
||||||
|
cameraUtils.bind()
|
||||||
|
it.llCameraFlag.postDelayed({ it.llCameraFlag.visibility = View.GONE }, 1000)
|
||||||
|
}
|
||||||
|
binding.root.postDelayed({ SensorScaleUtils.startScale() }, 2600)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.sw.inbound.model
|
||||||
|
|
||||||
|
import com.sw.inbound.model.response.DictType
|
||||||
|
|
||||||
|
/** 表单字段类型枚举 */
|
||||||
|
enum class FieldType {
|
||||||
|
/** 普通文本输入 */
|
||||||
|
TEXT,
|
||||||
|
/** 数字输入(支持小数和负数) */
|
||||||
|
NUMBER,
|
||||||
|
/** 下拉选择 */
|
||||||
|
DROPDOWN,
|
||||||
|
/** 日期选择弹窗 */
|
||||||
|
DATE_PICKER,
|
||||||
|
/** 固定值,不可编辑 */
|
||||||
|
FIXED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态表单字段数据模型
|
||||||
|
* @param label 字段标签名称
|
||||||
|
* @param type 字段类型,决定渲染哪种输入控件
|
||||||
|
* @param apiKey 对应接口的字段名,空字符串表示该字段不提交(如来源类型)
|
||||||
|
* @param required 是否必填,提交时校验
|
||||||
|
* @param hidden 是否隐藏,hidden=true 时不渲染该字段
|
||||||
|
* @param hint 校验失败提示文案,同时作为输入框 hint
|
||||||
|
* @param options 下拉框选项列表(DROPDOWN 类型使用)
|
||||||
|
* @param submitValueId 提交时使用 valueId 而非 value;DROPDOWN 选 id、DATE_PICKER 选 ISO 格式时设为 true
|
||||||
|
* @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式
|
||||||
|
* @param valueId 下拉框选中项的 id;DATE_PICKER 存 ISO 格式 yyyy-MM-dd'T'HH:mm:ss
|
||||||
|
*/
|
||||||
|
data class FormField(
|
||||||
|
val label: String,
|
||||||
|
val type: FieldType,
|
||||||
|
val apiKey: String = "",
|
||||||
|
val required: Boolean = false,
|
||||||
|
val hidden: Boolean = false,
|
||||||
|
val hint: String = "",
|
||||||
|
val options: List<DictType> = emptyList(),
|
||||||
|
val submitValueId: Boolean = false,
|
||||||
|
var value: String = "",
|
||||||
|
var valueId: String = ""
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.sw.inbound.model.request
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 净菜入库提交参数 */
|
||||||
|
data class CleanInboundParam(
|
||||||
|
@SerializedName("traceCode") val traceCode: String,
|
||||||
|
@SerializedName("ingredientName") val ingredientName: String,
|
||||||
|
/** 净菜类型,如"切丁"、"切片",参考 IngredientByTrace.vegTypes */
|
||||||
|
@SerializedName("cleanType") val cleanType: String,
|
||||||
|
/** 入库规格,如"袋装/500g"、"盒装/300g" */
|
||||||
|
@SerializedName("spec") val spec: String,
|
||||||
|
@SerializedName("quantity") val quantity: Int,
|
||||||
|
/** 入库重量(kg) */
|
||||||
|
@SerializedName("weight") val weight: Double,
|
||||||
|
/** 入库时间,格式:yyyy-MM-dd'T'HH:mm:ss */
|
||||||
|
@SerializedName("inboundTime") val inboundTime: String,
|
||||||
|
@SerializedName("zoneId") val zoneId: Long,
|
||||||
|
@SerializedName("operator") val operator: String? = null,
|
||||||
|
/** 保质期至,格式同 inboundTime */
|
||||||
|
@SerializedName("expiryDate") val expiryDate: String? = null,
|
||||||
|
@SerializedName("storageTemp") val storageTemp: Double? = null,
|
||||||
|
@SerializedName("cameraCode") val cameraCode: String? = null,
|
||||||
|
@SerializedName("videoUrl") val videoUrl: String? = null,
|
||||||
|
@SerializedName("remark") val remark: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.sw.inbound.model.request
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 毛菜入库提交参数 */
|
||||||
|
data class RawInboundParam(
|
||||||
|
@SerializedName("traceCode") val traceCode: String,
|
||||||
|
/** 供应商类型:1-内部,2-外部 */
|
||||||
|
@SerializedName("supplierType") val supplierType: Short,
|
||||||
|
@SerializedName("materialName") val materialName: String,
|
||||||
|
@SerializedName("supplierId") val supplierId: Long,
|
||||||
|
/** 入库规格,如"25kg/件"、"散装(kg)" */
|
||||||
|
@SerializedName("spec") val spec: String,
|
||||||
|
@SerializedName("quantity") val quantity: Int,
|
||||||
|
/** 入库重量(kg) */
|
||||||
|
@SerializedName("weight") val weight: Double,
|
||||||
|
/** 入库时间,格式:yyyy-MM-dd'T'HH:mm:ss */
|
||||||
|
@SerializedName("inboundTime") val inboundTime: String,
|
||||||
|
@SerializedName("zoneId") val zoneId: Long,
|
||||||
|
@SerializedName("operator") val operator: String,
|
||||||
|
@SerializedName("storageArea") val storageArea: String? = null,
|
||||||
|
@SerializedName("reportNo") val reportNo: String? = null,
|
||||||
|
@SerializedName("cameraCode") val cameraCode: String? = null,
|
||||||
|
@SerializedName("videoUrl") val videoUrl: String? = null,
|
||||||
|
@SerializedName("remark") val remark: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
/** 溯源码查食材结果,用于 InboundGoodsSearchDialog 列表展示 */
|
||||||
|
data class GoodsSearchModel(
|
||||||
|
val materId: Long = 0L,
|
||||||
|
val materName: String = "",
|
||||||
|
/** 净菜类型,逗号分隔,如"切丁,切片" */
|
||||||
|
val vegTypes: String = "",
|
||||||
|
val traceCode: String = "",
|
||||||
|
var isSelected: Boolean = false
|
||||||
|
) {
|
||||||
|
/** 将 vegTypes 拆分为列表,供净菜类型下拉使用 */
|
||||||
|
fun vegTypeList(): List<String> =
|
||||||
|
if (vegTypes.isBlank()) emptyList()
|
||||||
|
else vegTypes.split(",").map { it.trim() }.filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(item: IngredientByTrace): GoodsSearchModel = GoodsSearchModel(
|
||||||
|
materId = item.materId ?: 0L,
|
||||||
|
materName = item.materName ?: "",
|
||||||
|
vegTypes = item.vegTypes ?: "",
|
||||||
|
traceCode = item.traceCode ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口统一响应包装
|
||||||
|
* 与旧 ApiResponse 区别:code 为 Int,200 表示成功
|
||||||
|
*/
|
||||||
|
data class InboundApiResponse<T>(
|
||||||
|
@SerializedName("code") val code: Int = -1,
|
||||||
|
@SerializedName("msg") val msg: String? = null,
|
||||||
|
@SerializedName("data") val data: T? = null
|
||||||
|
) {
|
||||||
|
fun isSuccess(): Boolean = code == 200
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 按溯源码查食材的响应条目 */
|
||||||
|
data class IngredientByTrace(
|
||||||
|
@SerializedName("traceCode") val traceCode: String? = null,
|
||||||
|
@SerializedName("materId") val materId: Long? = null,
|
||||||
|
@SerializedName("materName") val materName: String? = null,
|
||||||
|
/** 净菜类型,逗号分隔,如"切丁,切片" */
|
||||||
|
@SerializedName("vegTypes") val vegTypes: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 供应商下拉选项 */
|
||||||
|
data class SupplierOption(
|
||||||
|
@SerializedName("id") val id: Long? = null,
|
||||||
|
@SerializedName("name") val name: String? = null,
|
||||||
|
/** 1-内部供应商,2-外部供应商 */
|
||||||
|
@SerializedName("type") val type: Int? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 仓库区域下拉选项 */
|
||||||
|
data class ZoneOption(
|
||||||
|
@SerializedName("id") val id: Long? = null,
|
||||||
|
@SerializedName("zoneNo") val zoneNo: String? = null,
|
||||||
|
@SerializedName("zoneName") val zoneName: String? = null,
|
||||||
|
@SerializedName("warehouseId") val warehouseId: Long? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.sw.inbound.network.api
|
||||||
|
|
||||||
|
import com.sw.inbound.model.response.IngredientByTrace
|
||||||
|
import com.sw.inbound.model.response.InboundApiResponse
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口
|
||||||
|
* Base URL: http://192.168.10.101:24801/
|
||||||
|
* 鉴权:authorization header 由 InboundInterceptor 统一注入
|
||||||
|
*/
|
||||||
|
interface InboundApiService {
|
||||||
|
|
||||||
|
/** 按溯源码查食材,用于毛菜/净菜入库时联动选择食材 */
|
||||||
|
@GET("nutrition/neglect/inbound/ingredient-by-trace")
|
||||||
|
suspend fun getIngredientByTrace(
|
||||||
|
@Query("traceCode") traceCode: String
|
||||||
|
): InboundApiResponse<List<IngredientByTrace>>
|
||||||
|
|
||||||
|
/** 仓库区域下拉列表 */
|
||||||
|
@GET("nutrition/neglect/inbound/zone-options")
|
||||||
|
suspend fun getZoneOptions(
|
||||||
|
@Query("warehouseId") warehouseId: Long? = null,
|
||||||
|
@Query("keyword") keyword: String? = null
|
||||||
|
): InboundApiResponse<List<ZoneOption>>
|
||||||
|
|
||||||
|
/** 供应商下拉列表,type: 1-内部,2-外部,不传返回全部 */
|
||||||
|
@GET("nutrition/neglect/inbound/supplier-options")
|
||||||
|
suspend fun getSupplierOptions(
|
||||||
|
@Query("type") type: Int? = null,
|
||||||
|
@Query("keyword") keyword: String? = null
|
||||||
|
): InboundApiResponse<List<SupplierOption>>
|
||||||
|
|
||||||
|
/** 毛菜入库提交,入参为 Map 以便动态组装字段 */
|
||||||
|
@POST("nutrition/neglect/inbound/raw-inbound")
|
||||||
|
suspend fun submitRawInbound(
|
||||||
|
@Body param: Map<String, Any>
|
||||||
|
): InboundApiResponse<Unit>
|
||||||
|
|
||||||
|
/** 净菜入库提交,入参为 Map 以便动态组装字段 */
|
||||||
|
@POST("nutrition/neglect/inbound/clean-inbound")
|
||||||
|
suspend fun submitCleanInbound(
|
||||||
|
@Body param: Map<String, Any>
|
||||||
|
): InboundApiResponse<Unit>
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.sw.inbound.network.interceptor
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口拦截器
|
||||||
|
* 统一注入 authorization 固定鉴权值及 Content-Type
|
||||||
|
*/
|
||||||
|
class InboundInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.sw.inbound.repository
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
|
import com.sw.inbound.model.response.IngredientByTrace
|
||||||
|
import com.sw.inbound.model.response.InboundApiResponse
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
import com.sw.inbound.network.api.InboundApiService
|
||||||
|
import com.sw.inbound.utils.LogSaveUtil
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口数据层
|
||||||
|
* 对应接口文档 Base URL: http://192.168.10.101:24801/nutrition/neglect/inbound
|
||||||
|
*/
|
||||||
|
class InboundRepository @Inject constructor(
|
||||||
|
private val apiService: InboundApiService
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** 按溯源码查食材 */
|
||||||
|
suspend fun getIngredientByTrace(traceCode: String): InboundApiResponse<List<IngredientByTrace>> {
|
||||||
|
return safeCall { apiService.getIngredientByTrace(traceCode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仓库区域下拉列表 */
|
||||||
|
suspend fun getZoneOptions(
|
||||||
|
warehouseId: Long? = null,
|
||||||
|
keyword: String? = null
|
||||||
|
): InboundApiResponse<List<ZoneOption>> {
|
||||||
|
return safeCall { apiService.getZoneOptions(warehouseId, keyword) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供应商下拉列表 */
|
||||||
|
suspend fun getSupplierOptions(
|
||||||
|
type: Int? = null,
|
||||||
|
keyword: String? = null
|
||||||
|
): InboundApiResponse<List<SupplierOption>> {
|
||||||
|
return safeCall { apiService.getSupplierOptions(type, keyword) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 毛菜入库提交 */
|
||||||
|
suspend fun submitRawInbound(param: Map<String, Any>): InboundApiResponse<Unit> {
|
||||||
|
return safeCall { apiService.submitRawInbound(param) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 净菜入库提交 */
|
||||||
|
suspend fun submitCleanInbound(param: Map<String, Any>): InboundApiResponse<Unit> {
|
||||||
|
return safeCall { apiService.submitCleanInbound(param) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> safeCall(call: suspend () -> InboundApiResponse<T>): InboundApiResponse<T> {
|
||||||
|
return try {
|
||||||
|
call()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("InboundRepository safeCall Exception: ${e.stackTraceToString()}")
|
||||||
|
LogSaveUtil.saveLogFile("InboundRepository safeCall Exception: ${e.stackTraceToString()}")
|
||||||
|
val msg = when (e) {
|
||||||
|
is HttpException -> "HTTP错误: ${e.code()} - ${e.message()}"
|
||||||
|
is SocketTimeoutException -> "请求超时: ${e.message}"
|
||||||
|
is ConnectException -> "连接失败: ${e.message}"
|
||||||
|
is JsonParseException -> "JSON解析错误: ${e.message}"
|
||||||
|
is IOException -> "网络IO错误: ${e.message}"
|
||||||
|
else -> "未知错误: ${e.message}"
|
||||||
|
}
|
||||||
|
InboundApiResponse(code = -1, msg = msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.sw.inbound.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.sw.inbound.model.response.IngredientByTrace
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
import com.sw.inbound.repository.InboundRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class StoreViewModel @Inject constructor(
|
||||||
|
private val repository: InboundRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _zoneOptions = MutableStateFlow<List<ZoneOption>>(emptyList())
|
||||||
|
val zoneOptions: StateFlow<List<ZoneOption>> = _zoneOptions
|
||||||
|
|
||||||
|
private val _supplierOptions = MutableStateFlow<List<SupplierOption>>(emptyList())
|
||||||
|
val supplierOptions: StateFlow<List<SupplierOption>> = _supplierOptions
|
||||||
|
|
||||||
|
/** 按溯源码查食材,结果通过 callback 返回 */
|
||||||
|
fun getIngredientByTrace(
|
||||||
|
traceCode: String,
|
||||||
|
callback: (List<IngredientByTrace>) -> Unit
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.getIngredientByTrace(traceCode)
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
callback(response.data ?: emptyList())
|
||||||
|
} else {
|
||||||
|
Timber.e("getIngredientByTrace failed: ${response.msg}")
|
||||||
|
callback(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载仓库区域列表,结果同时更新 StateFlow 并通过 callback 返回 */
|
||||||
|
fun loadZoneOptions(
|
||||||
|
warehouseId: Long? = null,
|
||||||
|
keyword: String? = null,
|
||||||
|
callback: ((List<ZoneOption>) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.getZoneOptions(warehouseId, keyword)
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
val list = response.data ?: emptyList()
|
||||||
|
_zoneOptions.value = list
|
||||||
|
callback?.invoke(list)
|
||||||
|
} else {
|
||||||
|
Timber.e("loadZoneOptions failed: ${response.msg}")
|
||||||
|
callback?.invoke(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载供应商列表,结果同时更新 StateFlow 并通过 callback 返回 */
|
||||||
|
fun loadSupplierOptions(
|
||||||
|
type: Int? = null,
|
||||||
|
keyword: String? = null,
|
||||||
|
callback: ((List<SupplierOption>) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.getSupplierOptions(type, keyword)
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
val list = response.data ?: emptyList()
|
||||||
|
_supplierOptions.value = list
|
||||||
|
callback?.invoke(list)
|
||||||
|
} else {
|
||||||
|
Timber.e("loadSupplierOptions failed: ${response.msg}")
|
||||||
|
callback?.invoke(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 毛菜入库提交 */
|
||||||
|
fun submitRawInbound(param: Map<String, Any>, callback: (Boolean, String?) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.submitRawInbound(param)
|
||||||
|
callback(response.isSuccess(), response.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 净菜入库提交 */
|
||||||
|
fun submitCleanInbound(param: Map<String, Any>, callback: (Boolean, String?) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.submitCleanInbound(param)
|
||||||
|
callback(response.isSuccess(), response.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?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="1500dp"
|
||||||
|
android:layout_height="900dp"
|
||||||
|
android:background="@drawable/bg_white_radius12"
|
||||||
|
tools:ignore="HardcodedText">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivClose"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:layout_marginEnd="30dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:src="@mipmap/ic_close"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDialogTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="物品检索"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/flSearchBlock"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/flSearchBlock"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginHorizontal="150dp"
|
||||||
|
android:layout_marginTop="180dp"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etInputGoods"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#00000000"
|
||||||
|
android:gravity="center"
|
||||||
|
android:hint="输入溯源码"
|
||||||
|
android:imeOptions="actionSearch"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingEnd="80dp"
|
||||||
|
android:textColor="#FF666666"
|
||||||
|
android:textColorHint="#FFC7C8DC"
|
||||||
|
android:textSize="30sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivGoodsSearch"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:padding="14dp"
|
||||||
|
android:src="@mipmap/ic_goods_search"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<com.scwang.smart.refresh.layout.SmartRefreshLayout
|
||||||
|
android:id="@+id/refreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginHorizontal="130dp"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:layout_marginBottom="58dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/dividerLine"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/flSearchBlock">
|
||||||
|
|
||||||
|
<com.scwang.smart.refresh.header.ClassicsHeader
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvSearch"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:spanCount="2"
|
||||||
|
tools:itemCount="8"
|
||||||
|
tools:listitem="@layout/list_item_goods_search" />
|
||||||
|
|
||||||
|
<com.scwang.smart.refresh.footer.ClassicsFooter
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/dividerLine"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginHorizontal="40dp"
|
||||||
|
android:layout_marginBottom="160dp"
|
||||||
|
android:background="#FFDCDCF0"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<!-- btnAddGoods 在入库秤场景下隐藏 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnAddGoods"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="@drawable/bg_white_stroke_blue_ripple"
|
||||||
|
android:text="添加物品"
|
||||||
|
android:textColor="#FF0033CC"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnConfirm"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/dividerLine" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnConfirm"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="@drawable/bg_blue_ripple"
|
||||||
|
android:text="确定"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/dividerLine" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?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="1500dp"
|
||||||
|
android:layout_height="900dp"
|
||||||
|
android:background="@drawable/bg_white_radius12"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="HardcodedText">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="14"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="物品入库"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginHorizontal="40dp"
|
||||||
|
android:background="#FFDCDCF0" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="70">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/guideline"
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="0.62" />
|
||||||
|
|
||||||
|
<!-- 左侧:FlexboxLayoutManager 驱动的动态表单,两列自动换行 -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvFormFields"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:descendantFocusability="afterDescendants"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingVertical="10dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:listitem="@layout/list_item_form_field" />
|
||||||
|
|
||||||
|
<!-- 右侧:相机预览 + 秤重显示 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/guideline">
|
||||||
|
|
||||||
|
<!-- 相机预览容器,由代码动态 inflate LayoutTakePhotoBinding -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/flContainer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="15dp" />
|
||||||
|
|
||||||
|
<!-- 秤重显示条 -->
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="488dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="@drawable/bg_gray_radius10">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="累计"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTotalWeight"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="120dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="24sp"
|
||||||
|
tools:text="6300 克" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btnClearZero"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="清零"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginHorizontal="40dp"
|
||||||
|
android:background="#FFDCDCF0" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="16"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="50dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="@drawable/bg_white_stroke_blue_ripple"
|
||||||
|
android:text="取消"
|
||||||
|
android:textColor="#FF0033CC"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnConfirm"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:background="@drawable/bg_blue_ripple"
|
||||||
|
android:text="确定"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?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:paddingHorizontal="20dp"
|
||||||
|
android:paddingVertical="10dp">
|
||||||
|
|
||||||
|
<!-- 字段标签,必填项红色星号由 Adapter 通过 SpannableString 追加 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="食材名称" />
|
||||||
|
|
||||||
|
<!-- 输入区域容器,三种控件叠放,按 FieldType 切换 visibility -->
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp">
|
||||||
|
|
||||||
|
<!-- 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="22sp"
|
||||||
|
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="22sp"
|
||||||
|
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="22sp"
|
||||||
|
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>
|
||||||
+487
@@ -0,0 +1,487 @@
|
|||||||
|
# 入库系统接口文档
|
||||||
|
|
||||||
|
> 生成时间:2026-05-26
|
||||||
|
> 项目:Inbound20260114
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、通用说明
|
||||||
|
|
||||||
|
### 1.1 服务器环境
|
||||||
|
|
||||||
|
| 环境 | 地址 |
|
||||||
|
|------|------|
|
||||||
|
| 本地环境 | `http://192.168.1.201:14801` |
|
||||||
|
| 测试环境 | `https://dev.yixiong-tech.com:8083` |
|
||||||
|
| 生产环境(PROD) | `https://api.dm.yixiong-tech.com:8443` |
|
||||||
|
|
||||||
|
> 代码中使用 `GlobalData.appBaseUrl` 动态切换,启动时由设备配置决定当前所用环境。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 通用请求头
|
||||||
|
|
||||||
|
以下请求头由拦截器(`RequestInterceptor`)统一注入,**无需在每个接口中单独传递**:
|
||||||
|
|
||||||
|
| Header 字段 | 说明 | 示例值 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Content-Type` | 请求体格式 | `application/json` |
|
||||||
|
| `Accept` | 响应体格式 | `application/json` |
|
||||||
|
| `deviceId` | 设备唯一标识符(UUID) | `bcf396ed-78f6-3864-...` |
|
||||||
|
| `X-DEVICE-CODE` | 设备编码(同 deviceId) | `bcf396ed-78f6-3864-...` |
|
||||||
|
| `x-access-token` | 用户登录 Token(登录后获取) | `{token}` |
|
||||||
|
| `authorization` | 固定鉴权值 | `57ee87183f2a4fa59683ec9ef41c8f5d` |
|
||||||
|
|
||||||
|
> **特殊说明**:`getDeviceInfo` 接口额外通过 `@Header("X-Access-Token")` 传入设备 Token(设备初始化阶段使用,区别于用户登录 Token)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 通用响应格式
|
||||||
|
|
||||||
|
所有接口返回结构统一如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "00000",
|
||||||
|
"msg": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `code` | String | 业务状态码,`00000` 表示成功 |
|
||||||
|
| `msg` | String | 提示信息 |
|
||||||
|
| `data` | T / null | 业务数据,各接口不同 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 接口路径前缀
|
||||||
|
|
||||||
|
业务接口通用前缀(简称 `{prefix}`):
|
||||||
|
|
||||||
|
```
|
||||||
|
/terminal/neglect/inbound
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、接口列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.1 获取存储方式列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/getDictItemByF` |
|
||||||
|
|
||||||
|
**Query 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `dictCode` | String | 否 | 字典编码,固定值 `ccfs`(存储方式) |
|
||||||
|
|
||||||
|
**响应 data(`List<StorageType>`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 存储方式 ID |
|
||||||
|
| `value` | String | 存储方式值 |
|
||||||
|
| `text` | String | 展示文本 |
|
||||||
|
| `title` | String | 标题 |
|
||||||
|
| `label` | String | 标签 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 获取物品类型列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/notLimitList` |
|
||||||
|
|
||||||
|
**Query 参数:** 无
|
||||||
|
|
||||||
|
**响应 data(`GoodsType`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `allType` | List\<AllType\> | 物品类型列表 |
|
||||||
|
|
||||||
|
**AllType 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 类型 ID |
|
||||||
|
| `typeName` | String | 类型名称 |
|
||||||
|
| `typeCode` | String | 类型编码 |
|
||||||
|
| `ftype` | String | 父类型 |
|
||||||
|
| `superName` | String | 父类型名称 |
|
||||||
|
| `status` | Boolean | 是否启用 |
|
||||||
|
| `eaId` | Int | 企业 ID |
|
||||||
|
| `kidTypes` | List\<String\> | 子类型列表 |
|
||||||
|
| `createTime` | String | 创建时间 |
|
||||||
|
| `updateTime` | String | 更新时间 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 获取字典数据列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/dataList` |
|
||||||
|
|
||||||
|
**Query 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `type` | String | 是 | 字典类型,如仓库列表、单位类型等 |
|
||||||
|
|
||||||
|
**响应 data(`List<DictType>`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 字典项 ID |
|
||||||
|
| `value` | String | 字典项名称/值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 采购单入库列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/receivePage` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(JSON,`Map<String, String>`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `pageNo` | String | 是 | 页码,从 1 开始 |
|
||||||
|
| `pageSize` | String | 是 | 每页条数 |
|
||||||
|
| *(其他筛选条件)* | String | 否 | 如供应商ID、日期范围等 |
|
||||||
|
|
||||||
|
**响应 data(`List<SupplierInfo>`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 收货单 ID |
|
||||||
|
| `purCode` | String | 采购单号 |
|
||||||
|
| `receiveCode` | String | 收货单号 |
|
||||||
|
| `supplierId` | String | 供应商 ID |
|
||||||
|
| `supplierName` | String | 供应商名称 |
|
||||||
|
| `purchaseDate` | String | 采购日期 |
|
||||||
|
| `receiveDate` | String | 收货时间 |
|
||||||
|
| `receiveStatus` | Int | 收货状态:1-收货关闭 / 2-收货完成 / 3-待收货 / 4-部分收货 |
|
||||||
|
| `receiveUser` | String | 操作人 |
|
||||||
|
| `goodCount` | Int | 物品项数 |
|
||||||
|
| `receiveCountAll` | Double | 总收货数量 |
|
||||||
|
| `receivePriceEx` | Double | 收货金额(不含税) |
|
||||||
|
| `receivePriceIn` | Double | 收货金额(含税) |
|
||||||
|
| `taxRateAll` | Double | 税率合计 |
|
||||||
|
| `giftAll` | Double | 赠品合计 |
|
||||||
|
| `warehouseId` | String | 仓库 ID |
|
||||||
|
| `eaId` | Int | 企业 ID |
|
||||||
|
| `createTime` | String | 创建时间 |
|
||||||
|
| `updateTime` | String | 更新时间 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 采购单入库详情
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/receiveDetail` |
|
||||||
|
|
||||||
|
**Query 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `id` | String | 是 | 收货单 ID |
|
||||||
|
|
||||||
|
**响应 data(`PurchaseInfo`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 收货单 ID |
|
||||||
|
| `supplierId` | String | 供应商 ID |
|
||||||
|
| `supplierName` | String | 供应商名称 |
|
||||||
|
| `purCode` | String | 采购单号 |
|
||||||
|
| `receiveCode` | String | 收货单号 |
|
||||||
|
| `receiveGoodsInfoList` | List\<GoodsInfo\> | 物品明细列表(见 **GoodsInfo 结构**) |
|
||||||
|
|
||||||
|
**GoodsInfo 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 明细 ID |
|
||||||
|
| `goodId` | String | 物品 ID |
|
||||||
|
| `goodName` | String | 物品名称 |
|
||||||
|
| `unitName` | String | 采购单位名称 |
|
||||||
|
| `purchaseUnitId` | String | 采购单位 ID |
|
||||||
|
| `goodsPurId` | String | 物品采购单位关系 ID |
|
||||||
|
| `receiveCount` | Double | 应收数量 |
|
||||||
|
| `receivedNum` | Double | 实收数量 |
|
||||||
|
| `receiveCountAll` | Double | 总收货数量 |
|
||||||
|
| `recUnitPriceTaxIn` | Double | 含税单价 |
|
||||||
|
| `newRecUnitPriceTaxIn` | Double | 调整后单价(部分收货使用) |
|
||||||
|
| `recPriceExItem` | Double | 不含税金额 |
|
||||||
|
| `recPriceInItem` | Double | 含税金额 |
|
||||||
|
| `warehouseId` | String | 仓库 ID |
|
||||||
|
| `warehouseName` | String | 仓库名称 |
|
||||||
|
| `kcUnitId` | String | 库存单位 ID |
|
||||||
|
| `kcUnitName` | String | 库存单位名称 |
|
||||||
|
| `purchaseValue` | Int | 采购库存转换值 |
|
||||||
|
| `consumeValue` | String | 库存与克的转换值 |
|
||||||
|
| `goodsWeight` | BigDecimal | 物品重量(千克) |
|
||||||
|
| `goodsCount` | Double | 物品数量 |
|
||||||
|
| `goodsUnitPrice` | Double | 物品单价 |
|
||||||
|
| `goodsPrice` | Double | 物品总价 |
|
||||||
|
| `buyToInventoryValue` | String | 采购转库存换算值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 上传单张图片
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}/terminal/neglect/upload` |
|
||||||
|
| Content-Type | `multipart/form-data` |
|
||||||
|
|
||||||
|
**Form-Data 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `file` | File(MultipartBody.Part) | 是 | 图片文件 |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
String // 图片访问路径(相对 URL)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 上传多张图片
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}/terminal/neglect/inbound/pad/inboundWeighingScale/add` |
|
||||||
|
| Content-Type | `multipart/form-data` |
|
||||||
|
|
||||||
|
**Form-Data 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodsId` | String(RequestBody) | 是 | 物品 ID |
|
||||||
|
| `goodsName` | String(RequestBody) | 是 | 物品名称 |
|
||||||
|
| `goodsPics` | List\<MultipartBody.Part\> | 是 | 多张图片文件列表,字段名固定为 `goodsPics` |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
String // 操作结果
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 搜索物品列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/goodsInfoList` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(JSON,`Map<String, String>`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodsName` | String | 否 | 物品名称关键词 |
|
||||||
|
| `pageNo` | String | 是 | 页码 |
|
||||||
|
| `pageSize` | String | 是 | 每页条数 |
|
||||||
|
| *(其他筛选条件)* | String | 否 | 按需传入 |
|
||||||
|
|
||||||
|
**响应 data(`SearchGoodsInfo`,分页结构):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `current` | Int | 当前页码 |
|
||||||
|
| `total` | Int | 总记录数 |
|
||||||
|
| `size` | Int | 每页条数 |
|
||||||
|
| `pages` | Int | 总页数 |
|
||||||
|
| `records` | List\<Record\> | 物品记录列表(见 **Record 结构**) |
|
||||||
|
|
||||||
|
**Record 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 物品 ID |
|
||||||
|
| `goodsCode` | String | 物品编码 |
|
||||||
|
| `goodsName` | String | 物品名称 |
|
||||||
|
| `zjmCode` | String | 助记码 |
|
||||||
|
| `specification` | String | 规格 |
|
||||||
|
| `typeId` | Int | 物品类型 ID |
|
||||||
|
| `typeName` | String | 物品类型名称 |
|
||||||
|
| `kcUnitId` | Int | 库存单位 ID |
|
||||||
|
| `kcUnitName` | String | 库存单位名称 |
|
||||||
|
| `consumeValue` | String | 库存与克的转换值 |
|
||||||
|
| `purchaseValue` | String | 采购库存转换值 |
|
||||||
|
| `unit` | String | 单位 |
|
||||||
|
| `relativeUrl` | String | 物品图片相对路径 |
|
||||||
|
| `unitVoList` | List\<UnitVo\> | 可选采购单位列表(见 **UnitVo 结构**) |
|
||||||
|
|
||||||
|
**UnitVo 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `businessUnitId` | String | 单位关系 ID |
|
||||||
|
| `purchaseUnitName` | String | 采购单位名称 |
|
||||||
|
| `buyToInventoryValue` | String | 采购转库存换算值 |
|
||||||
|
| `consumeValue` | String | 消耗转换值 |
|
||||||
|
| `purchaseValue` | Double | 购买单位对应库存数量 |
|
||||||
|
| `isDefault` | Int | 是否默认单位(1-是) |
|
||||||
|
| `value` | String | 单位展示值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.9 图像识别物品列表查询
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/goodsList` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
> 用于图片识别后,根据识别到的物品名称列表批量查询物品信息。
|
||||||
|
|
||||||
|
**请求体(`RecGoodsName`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `nameList` | List\<String\> | 是 | 识别到的物品名称列表 |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
List<SearchGoodsInfo.Record> // 物品记录列表(字段同 2.8 的 Record 结构)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.10 自采添加新物品
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/goodsAdd` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(`GoodsAddParam`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodName` | String | 是 | 物品名称 |
|
||||||
|
| `goodCode` | String | 否 | 物品编号(服务端自动生成) |
|
||||||
|
| `goodType` | String | 是 | 物品类型 ID |
|
||||||
|
| `storageType` | String | 是 | 存储方式 ID |
|
||||||
|
| `netRate` | Float | 是 | 净材率(0~1 之间) |
|
||||||
|
| `unitId` | String | 是 | 库存单位 ID |
|
||||||
|
| `purchaseUnit` | String | 是 | 采购单位 ID |
|
||||||
|
| `purchaseValue` | Float | 是 | 采购单位对应库存数量(转换值) |
|
||||||
|
| `purchasePrice` | BigDecimal | 是 | 采购单价 |
|
||||||
|
| `relativeUrl` | String | 否 | 物品图片相对路径 |
|
||||||
|
| `consumeValue` | String | 是 | 库存单位对应消耗值(库存与克的转换值) |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
String // 新增物品 ID 或操作结果
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.11 确认收货(全部收货)
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/saveAndReceiveAndGoWare` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(`UploadInfo`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `id` | String | 是 | 收货单 ID |
|
||||||
|
| `supplierId` | String | 否 | 供应商 ID |
|
||||||
|
| `purCode` | String | 否 | 采购单号 |
|
||||||
|
| `receiveGoodsInfos` | List\<GoodsInfo\> | 是 | 全部收货物品列表(字段同 GoodsInfo) |
|
||||||
|
| `receiveGoodsInfoList` | List\<GoodsInfo\> | 否 | 部分收货列表(全量收货时传空列表) |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Boolean // true 表示操作成功
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.12 自采入库
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/selfPurchasedGoods` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(`List<PurchaseWarehouseParam>`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodsId` | String | 是 | 物品 ID |
|
||||||
|
| `kcUnitId` | String | 否 | 库存单位 ID |
|
||||||
|
| `goodsCount` | Double | 是 | 入库数量 |
|
||||||
|
| `goodsUnitPrice` | Double | 是 | 入库单价 |
|
||||||
|
| `goodsPrice` | Double | 是 | 入库金额 |
|
||||||
|
| `goodPurId` | String | 是 | 物品采购单位关系 ID(`businessUnitId`) |
|
||||||
|
| `warehouseId` | String | 是 | 仓库 ID |
|
||||||
|
| `buyToInventoryValue` | String | 否 | 采购转库存换算值 |
|
||||||
|
| `goodsWeight` | BigDecimal | 是 | 物品重量(千克) |
|
||||||
|
| `consumeValue` | String | 否 | 库存单位对应消耗值 |
|
||||||
|
|
||||||
|
> 注:`goodsName`、`unitName`、`unitList`、`selectUnitType` 等字段为本地 UI 展示用,**不上传至服务端**。
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Boolean // true 表示入库成功
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、接口速查表
|
||||||
|
|
||||||
|
| # | 接口名称 | 方法 | 路径 |
|
||||||
|
|---|----------|------|------|
|
||||||
|
| 2.1 | 获取存储方式列表 | GET | `{prefix}/pad/getDictItemByF` |
|
||||||
|
| 2.2 | 获取物品类型列表 | GET | `{prefix}/pad/notLimitList` |
|
||||||
|
| 2.3 | 获取字典数据列表 | GET | `{prefix}/pad/dataList` |
|
||||||
|
| 2.4 | 采购单入库列表 | POST | `{prefix}/pad/receivePage` |
|
||||||
|
| 2.5 | 采购单入库详情 | GET | `{prefix}/pad/receiveDetail` |
|
||||||
|
| 2.6 | 上传单张图片 | POST | `/terminal/neglect/upload` |
|
||||||
|
| 2.7 | 上传多张图片 | POST | `/terminal/neglect/inbound/pad/inboundWeighingScale/add` |
|
||||||
|
| 2.8 | 搜索物品列表 | POST | `{prefix}/pad/goodsInfoList` |
|
||||||
|
| 2.9 | 图像识别物品查询 | POST | `{prefix}/pad/goodsList` |
|
||||||
|
| 2.10 | 自采添加新物品 | POST | `{prefix}/pad/goodsAdd` |
|
||||||
|
| 2.11 | 确认收货(全部) | POST | `{prefix}/pad/saveAndReceiveAndGoWare` |
|
||||||
|
| 2.12 | 自采入库 | POST | `{prefix}/pad/selfPurchasedGoods` |
|
||||||
|
|
||||||
|
> `{prefix}` = `/terminal/neglect/inbound`
|
||||||
+289
@@ -0,0 +1,289 @@
|
|||||||
|
# 入库秤设备端接口文档
|
||||||
|
|
||||||
|
**Base URL**:`http://192.168.10.101:24801/nutrition/neglect/inbound`
|
||||||
|
**鉴权**:无需 Sa-Token(Nacos 白名单 `/nutrition/neglect/**`)
|
||||||
|
|
||||||
|
| Header 字段 | 说明 | 示例值 |
|
||||||
|
|---|---|---|
|
||||||
|
| `authorization` | 固定鉴权值 | `57ee87183f2a4fa59683ec9ef41c8f5d` |
|
||||||
|
**Content-Type**:`application/json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用响应结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| code | int | 200 成功,其他为业务错误码 |
|
||||||
|
| msg | string | 提示信息 |
|
||||||
|
| data | any | 响应数据,无数据时为 `null` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 按溯源码查食材
|
||||||
|
|
||||||
|
**GET** `/nutrition/neglect/inbound/ingredient-by-trace`
|
||||||
|
|
||||||
|
按溯源码反查命中的食材列表,返回食材 id、名称、净菜类型,用于毛菜/净菜入库时联动选择食材。
|
||||||
|
|
||||||
|
**Query 参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| traceCode | string | ✅ | 溯源码 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"traceCode": "TC20240501001",
|
||||||
|
"materId": 1001,
|
||||||
|
"materName": "西红柿",
|
||||||
|
"vegTypes": "切丁,切片"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| traceCode | string | 溯源码 |
|
||||||
|
| materId | long | 食材 id(关联 nut_mater_base.id) |
|
||||||
|
| materName | string | 食材名称 |
|
||||||
|
| vegTypes | string | 净菜类型,逗号分隔,如 `切丁,切片` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 仓库区域下拉列表
|
||||||
|
|
||||||
|
**GET** `/nutrition/neglect/inbound/zone-options`
|
||||||
|
|
||||||
|
获取仓库区域选项,用于入库时选择存放区域。
|
||||||
|
|
||||||
|
**Query 参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| warehouseId | long | ❌ | 仓库 id,不传则返回所有区域 |
|
||||||
|
| keyword | string | ❌ | 关键字模糊搜索(区域编号/名称) |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"zoneNo": "WH-A-01",
|
||||||
|
"zoneName": "A仓-蔬菜区-01",
|
||||||
|
"warehouseId": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | long | 区域主键 id |
|
||||||
|
| zoneNo | string | 区域编号,如 `WH-A-01` |
|
||||||
|
| zoneName | string | 区域名称,如 `A仓-蔬菜区-01` |
|
||||||
|
| warehouseId | long | 关联仓库 id |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 供应商下拉列表
|
||||||
|
|
||||||
|
**GET** `/nutrition/neglect/inbound/supplier-options`
|
||||||
|
|
||||||
|
获取供应商选项,内部与外部供应商合并返回。
|
||||||
|
|
||||||
|
**Query 参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| type | int | ❌ | 供应商类型:`1`-内部,`2`-外部,不传则返回全部 |
|
||||||
|
| keyword | string | ❌ | 关键字模糊搜索(供应商名称) |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 201,
|
||||||
|
"name": "绿源农场",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 202,
|
||||||
|
"name": "鲜达食品有限公司",
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | long | 供应商 id |
|
||||||
|
| name | string | 供应商名称 |
|
||||||
|
| type | int | `1`-内部供应商,`2`-外部供应商 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 毛菜入库提交
|
||||||
|
|
||||||
|
**POST** `/nutrition/neglect/inbound/raw-inbound`
|
||||||
|
|
||||||
|
入库秤设备提交毛菜入库记录,`opType` 由后端固定为 `1`(设备自动),无需传入。
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"traceCode": "TC20240501001",
|
||||||
|
"supplierType": 1,
|
||||||
|
"materialName": "西红柿",
|
||||||
|
"supplierId": 201,
|
||||||
|
"spec": "25kg/件",
|
||||||
|
"quantity": 2,
|
||||||
|
"weight": 49.8,
|
||||||
|
"zoneId": 101,
|
||||||
|
"operator": "张三",
|
||||||
|
"storageArea": "A仓-蔬菜区-01",
|
||||||
|
"reportNo": "RPT-2024-001",
|
||||||
|
"cameraCode": "CAM-001",
|
||||||
|
"videoUrl": "http://example.com/video/001.mp4",
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| traceCode | string | ✅ | 溯源码 |
|
||||||
|
| supplierType | short | ✅ | 供应商类型:`1`-内部,`2`-外部 |
|
||||||
|
| materialName | string | ✅ | 食材名称 |
|
||||||
|
| supplierId | long | ✅ | 供应商 id |
|
||||||
|
| spec | string | ✅ | 入库规格,如 `25kg/件`、`散装(kg)` |
|
||||||
|
| quantity | int | ✅ | 入库数量 |
|
||||||
|
| weight | decimal | ✅ | 入库重量(kg) |
|
||||||
|
| zoneId | long | ✅ | 存放区域 id |
|
||||||
|
| operator | string | ✅ | 操作人姓名 |
|
||||||
|
| storageArea | string | ❌ | 存放区域名称(冗余字段,可与 zoneId 对应名称一致) |
|
||||||
|
| reportNo | string | ❌ | 卫生检测报告编号 |
|
||||||
|
| cameraCode | string | ❌ | 摄像头编号 |
|
||||||
|
| videoUrl | string | ❌ | 监控视频地址 |
|
||||||
|
| remark | string | ❌ | 备注 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "保存成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 净菜入库提交
|
||||||
|
|
||||||
|
**POST** `/nutrition/neglect/inbound/clean-inbound`
|
||||||
|
|
||||||
|
入库秤设备提交净菜入库记录,`opType` 由后端固定为 `1`(设备自动),无需传入。
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"traceCode": "TC20240501002",
|
||||||
|
"ingredientName": "西红柿",
|
||||||
|
"cleanType": "切丁",
|
||||||
|
"spec": "袋装/500g",
|
||||||
|
"quantity": 10,
|
||||||
|
"weight": 5.0,
|
||||||
|
"inboundTime": "2024-05-01T08:30:00",
|
||||||
|
"zoneId": 102,
|
||||||
|
"operator": "李四",
|
||||||
|
"expiryDate": "2024-05-03T00:00:00",
|
||||||
|
"storageTemp": 4.5,
|
||||||
|
"cameraCode": "CAM-002",
|
||||||
|
"videoUrl": "http://example.com/video/002.mp4",
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| traceCode | string | ✅ | 溯源码 |
|
||||||
|
| ingredientName | string | ✅ | 食材名称 |
|
||||||
|
| cleanType | string | ✅ | 净菜类型,如 `切丁`、`切片`(参考接口1返回的 `vegTypes`) |
|
||||||
|
| spec | string | ✅ | 入库规格,如 `袋装/500g`、`盒装/300g` |
|
||||||
|
| quantity | int | ✅ | 入库数量(包/盒/份) |
|
||||||
|
| weight | decimal | ✅ | 入库重量(kg) |
|
||||||
|
| inboundTime | datetime | ✅ | 入库时间,格式:`yyyy-MM-dd'T'HH:mm:ss` |
|
||||||
|
| zoneId | long | ✅ | 仓库区域 id |
|
||||||
|
| operator | string | ❌ | 操作人姓名 |
|
||||||
|
| expiryDate | datetime | ❌ | 保质期至,格式同 `inboundTime` |
|
||||||
|
| storageTemp | decimal | ❌ | 存放温度(℃) |
|
||||||
|
| cameraCode | string | ❌ | 摄像头编号 |
|
||||||
|
| videoUrl | string | ❌ | 监控视频地址 |
|
||||||
|
| remark | string | ❌ | 备注 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "保存成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 典型业务流程
|
||||||
|
|
||||||
|
```
|
||||||
|
设备扫溯源码
|
||||||
|
↓
|
||||||
|
GET /ingredient-by-trace?traceCode=xxx → 获取食材列表
|
||||||
|
↓
|
||||||
|
GET /zone-options → 获取区域列表
|
||||||
|
GET /supplier-options → 获取供应商列表(毛菜需要)
|
||||||
|
↓
|
||||||
|
用户确认信息 / 秤自动称重
|
||||||
|
↓
|
||||||
|
POST /raw-inbound (毛菜)
|
||||||
|
或
|
||||||
|
POST /clean-inbound(净菜)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user