添加了图片拍照及识别逻辑

This commit is contained in:
zxj
2025-08-14 09:34:49 +08:00
parent 1ac73df8d9
commit 10d4a4a41d
16 changed files with 542 additions and 396 deletions
@@ -20,7 +20,7 @@ import javax.inject.Singleton
object NetworkModule {
// private const val BASE_URL = "https://127.0.0.1"
// private const val BASE_URL = "https://vip.shuziweidao.com"
// private const val BASE_URL = "https://yyjk.shuziweidao.com/gateway/"
private const val BASE_URL = "http://192.168.1.237:9002/"
// private const val BASE_URL = "https://vip.shuziweidao.com/gateway/"
@@ -116,6 +116,7 @@ object SensorScaleUtils {
* 去皮置零
*/
fun tare() {
Timber.d("tare isOpened = $isOpened")
if (!isOpened) return
mSensorScale?.tare {
Timber.d("去皮置零操作成功")
@@ -98,6 +98,16 @@ fun ReceiptProductScreen(
}
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
Timber.d("开始传感器采集")
viewModel.startSensorScale()
onDispose {
Timber.d("停止传感器采集")
viewModel.stopSensorScale()
}
}
Column(modifier = modifier) {
TopTitleBar()
Box(
@@ -241,6 +251,10 @@ fun TabScreen(viewModel: ReceiptViewModel) {
val tabs =
listOf("未确认(${purchaseUnadjustedList.size})", "已确认(${purchaseAdjustList.size})")
LaunchedEffect(purchaseUnadjustedList) {
viewModel.updateReceiptList(purchaseUnadjustedList)
}
Column(
modifier = Modifier
.background(
@@ -493,7 +507,6 @@ private fun AdjustedRowInputLayout(
@Composable
fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel) {
val searchResultList by viewModel.searchListItems.collectAsState()
val selectedItem by viewModel.selectedItem.collectAsState()
Column(
modifier = modifier
@@ -507,7 +520,11 @@ fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel)
) {
if (selectedItem == null) {
Spacer(modifier = Modifier.height(30.dp))
IdentityView(list = searchResultList, showSearchView = false, onOptionSelected = {})
IdentityView(
showSearchView = false,
onOptionSelected = {},
baseViewModel = viewModel
)
} else {
ReceiptProductEditView(viewModel)
}
@@ -525,16 +542,6 @@ private fun ReceiptProductEditView(
) {
val selectedItem by viewModel.selectedItem.collectAsState()
val warehouseTypeList = GlobalData.warehouseTypeList
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
Timber.d("开始传感器采集")
viewModel.startSensorScale()
onDispose {
Timber.d("停止传感器采集")
viewModel.stopSensorScale()
}
}
selectedItem?.let {
Column(
@@ -24,10 +24,6 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -256,13 +252,7 @@ fun ContentRightView(
*/
@Composable
fun ProductIdentification(viewModel: SelfProcurementViewModel) {
val searchResultList by viewModel.searchListItems.collectAsState()
// 需要搜索的信息
var searchInfo by remember { mutableStateOf("") }
var pageNum by remember { mutableIntStateOf(1) }
val isMoreLoading by viewModel.isMoreLoading.collectAsState()
val canLoadMore by viewModel.canLoadMore.collectAsState()
val selectedItem by viewModel.selectedItem.collectAsState()
Column(
modifier = Modifier
.width(457.dp)
@@ -275,19 +265,9 @@ fun ProductIdentification(viewModel: SelfProcurementViewModel) {
.fillMaxWidth()
) {
// 菜品识别
IdentityView(list = searchResultList, showSearchView = true, onSearchClick = {
searchInfo = it
viewModel.searchGoodsInfoList(it)
}, onOptionSelected = {
IdentityView(showSearchView = true, onOptionSelected = {
viewModel.updateSelectedItemWithSearch(it)
}, onLoadMore = {
Timber.d("加载更多 canLoadMore = $canLoadMore")
if (!canLoadMore) {
return@IdentityView
}
pageNum++
viewModel.searchGoodsInfoList(goodsName = searchInfo, pageNo = pageNum)
}, isMoreLoading = isMoreLoading, canLoadMore = canLoadMore)
}, checkedItem = selectedItem, baseViewModel = viewModel)
}
Spacer(modifier = Modifier.height(10.dp))
Row(
@@ -873,7 +853,9 @@ private fun SelfProductEditView(
textColor = colorResource(R.color.green),
borderColor = colorResource(R.color.green),
onClick = {
viewModel.updateLastPhotoUri(null)
viewModel.updateSelectedItem(null)
viewModel.updateIdentityList(emptyList())
})
CustomButton(
modifier = Modifier
@@ -1,18 +1,12 @@
package com.sw.inbound.ui.weight
import android.net.Uri
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.Toast
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -21,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -37,8 +30,6 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import coil.compose.AsyncImage
import com.sw.inbound.GlobalData
@@ -47,8 +38,8 @@ import com.sw.inbound.ext.dashedBorder
import com.sw.inbound.ui.theme.AppTypography
import com.sw.inbound.utils.FileUtils
import com.sw.inbound.utils.ToastUtils
import com.sw.inbound.utils.rememberPhotoCapture
import timber.log.Timber
import java.io.File
/**
* 相机预览 -默认不预览,支持拍照
@@ -72,6 +63,24 @@ fun CameraCaptureLayout(
}
}
// 创建拍照工具实例
val (photoCaptureHelper, takePhoto) = rememberPhotoCapture(
cameraController = cameraController,
onSuccess = { savedUri ->
// 处理拍照成功的逻辑
photoUri = savedUri
Timber.d("photoUri = $photoUri")
Toast.makeText(context, "图片已保存", Toast.LENGTH_SHORT).show()
showCamera = false
GlobalData.imageUri = photoUri
},
onError = { error ->
// 处理拍照失败的逻辑
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
GlobalData.imageUri = null
}
)
Column(
modifier = modifier
.width(428.dp)
@@ -165,38 +174,7 @@ fun CameraCaptureLayout(
ToastUtils.showToast("请先开启图片预览")
return@CustomButton
}
val executor = ContextCompat.getMainExecutor(context)
val cacheDir = context.cacheDir
val photoFile = File.createTempFile(
"IMG_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
val cacheOutputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).build()
cameraController.takePicture(
cacheOutputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
photoUri = savedUri
Timber.d("photoUri = $photoUri")
Toast.makeText(context, "图片已保存", Toast.LENGTH_SHORT).show()
showCamera = false
GlobalData.imageUri = photoUri
}
override fun onError(exception: ImageCaptureException) {
GlobalData.imageUri = null
Toast.makeText(
context,
"拍照失败: ${exception.message}",
Toast.LENGTH_SHORT
).show()
}
})
takePhoto()
},
borderColor = colorResource(R.color.green),
textColor = colorResource(R.color.white),
@@ -212,53 +190,4 @@ fun CameraCaptureLayout(
cameraController.bindToLifecycle(lifecycleOwner)
onDispose { }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CameraContent(modifier: Modifier, cameraController: LifecycleCameraController) {
val lifecycleOwner = LocalLifecycleOwner.current
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
//在Compose中使用View系统中的PreviewView
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = { context ->
PreviewView(context).apply {
//设置布局宽度和高度占据全屏
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
//设置背景颜色
setBackgroundColor(android.graphics.Color.BLACK)
//设置渲染的实现模式
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
//设置缩放方式
scaleType = PreviewView.ScaleType.FILL_START
}.also {
it.controller = cameraController
cameraController.bindToLifecycle(lifecycleOwner)
}
},
onReset = {},
onRelease = {
Timber.d("cameraController.unbind()")
cameraController.unbind()
}
)
Image(
modifier = Modifier
.fillMaxSize()
.padding(30.dp),
painter = painterResource(R.mipmap.ic_scan),
contentDescription = "扫描"
)
}
}
}
@@ -1,5 +1,6 @@
package com.sw.inbound.ui.weight
import android.net.Uri
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
@@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import coil.compose.AsyncImage
import com.sw.inbound.R
/**
@@ -19,7 +21,8 @@ import com.sw.inbound.R
@Composable
fun CameraPreview(
controller: LifecycleCameraController,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
photoUri: Uri? = null
) {
Box(modifier = modifier) {
AndroidView(
@@ -31,6 +34,13 @@ fun CameraPreview(
},
modifier = Modifier.fillMaxSize()
)
if (photoUri != null) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = photoUri,
contentDescription = "照片",
)
}
Image(
modifier = Modifier
.fillMaxSize()
@@ -1,5 +1,6 @@
package com.sw.inbound.ui.weight
import android.widget.Toast
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.layout.Column
@@ -8,39 +9,103 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.sw.inbound.ext.medium
import com.sw.inbound.model.request.PurchaseWarehouseParam
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.ui.theme.AppTypography
import com.sw.inbound.utils.InteractionUtils.Debouncer
import com.sw.inbound.utils.rememberPhotoCapture
import com.sw.inbound.viewmodel.BaseViewModel
import timber.log.Timber
/**
* 菜品识别组件
*/
@Composable
fun IdentityView(
list: List<SearchGoodsInfo.Record>,
showSearchView: Boolean = false,
onSearchClick: (String) -> Unit = {},
onOptionSelected: (SearchGoodsInfo.Record) -> Unit = {},
onLoadMore: (Int) -> Unit = {},
isMoreLoading: Boolean = false,
canLoadMore: Boolean = true
checkedItem: PurchaseWarehouseParam? = null,
baseViewModel: BaseViewModel,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val weightInfo by baseViewModel.weightInfo.collectAsState()
val identityList by baseViewModel.identityListItems.collectAsState()
val searchList by baseViewModel.searchListItems.collectAsState()
// var lastWeight by remember { mutableStateOf<Double?>(null) }
var lastWeight by remember { mutableDoubleStateOf(0.0) }
val lastPhotoUri by baseViewModel.lastPhotoUri.collectAsState()
var pageNum by remember { mutableIntStateOf(1) }
val isMoreLoading by baseViewModel.isMoreLoading.collectAsState()
val canLoadMore by baseViewModel.canLoadMore.collectAsState()
var list = if (identityList.isNotEmpty()) {
identityList
} else {
searchList
}
val debouncer = remember { Debouncer(2000) }
// CameraX 控制器
val cameraController = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.IMAGE_CAPTURE)
bindToLifecycle(lifecycleOwner)
// 必须设置有效的用例
setEnabledUseCases(
CameraController.IMAGE_CAPTURE or
CameraController.VIDEO_CAPTURE
)
}
}
// 需要搜索的信息
var searchInfo by remember { mutableStateOf("") }
// 创建拍照工具实例
val (photoCaptureHelper, takePhoto) = rememberPhotoCapture(
cameraController = cameraController,
onSuccess = { savedUri ->
// 处理拍照成功的逻辑
baseViewModel.updateLastPhotoUri(savedUri)
Timber.d("photoUri = $savedUri")
baseViewModel.getIdentityList(savedUri)
},
onError = { error ->
// 处理拍照失败的逻辑
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
baseViewModel.updateLastPhotoUri(null)
}
)
// 绑定生命周期
DisposableEffect(lifecycleOwner) {
cameraController.bindToLifecycle(lifecycleOwner)
onDispose { /* 清理资源 */ }
}
LaunchedEffect(weightInfo) {
Timber.d("LaunchedEffect weightInfo1 = $weightInfo, lastWeight = $lastWeight")
if (weightInfo - lastWeight > 1) {
Timber.d("weightInfo2 = $weightInfo")
debouncer.debounce {
takePhoto()
}
}
lastWeight = weightInfo
}
Column(
modifier = Modifier
.fillMaxSize(),
@@ -50,11 +115,15 @@ fun IdentityView(
modifier = Modifier
.width(397.dp)
.height(298.dp),
controller = cameraController
controller = cameraController,
photoUri = lastPhotoUri
)
if (showSearchView) {
Spacer(modifier = Modifier.height(30.dp))
CustomSearchView(onSearchClick = onSearchClick)
CustomSearchView(onSearchClick = {
searchInfo = it
baseViewModel.searchGoodsInfoList(it)
})
}
Spacer(modifier = Modifier.height(30.dp))
if (showSearchView) {
@@ -63,7 +132,15 @@ fun IdentityView(
.height(64.dp)
.width(192.dp), options = list,
onOptionSelected = onOptionSelected,
onLoadMore = onLoadMore,
onLoadMore = {
Timber.d("加载更多 canLoadMore = $canLoadMore")
if (!canLoadMore) {
return@SingleSelectButtonGroup
}
pageNum++
baseViewModel.searchGoodsInfoList(goodsName = searchInfo, pageNo = pageNum)
},
checkedItem = list.find { it.goodsId == checkedItem?.goodsId },
isMoreLoading = isMoreLoading,
canLoadMore = canLoadMore
)
@@ -96,7 +96,7 @@ fun ReceiptUnadjustedListView(
key = { index, it -> it.id ?: index }) { index, it ->
val checkedBg =
if (selectIndex == index) Gray_DCDCF0 else Color.Transparent
if (/*selectIndex == index ||*/ checkedItem?.id == it.id) Gray_DCDCF0 else Color.Transparent
Row(
modifier = Modifier
.background(color = checkedBg)
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
@@ -72,10 +72,11 @@ fun SingleSelectButtonGroup(
unSelectedTextStyle: TextStyle = AppTypography.gray96a0aaTextStyle,
onLoadMore: (Int) -> Unit = {},
isMoreLoading: Boolean = false,
canLoadMore: Boolean = true
canLoadMore: Boolean = true,
checkedItem: SearchGoodsInfo.Record? = null
) {
val context = LocalContext.current
var selectedOption by remember { mutableStateOf(options.firstOrNull() ?: "") }
var selectedItem by remember { mutableStateOf(options.firstOrNull() ?: "") }
val lazyGridState = rememberLazyGridState()
var pageNum by remember { mutableIntStateOf(1) }
@@ -109,26 +110,27 @@ fun SingleSelectButtonGroup(
horizontalArrangement = Arrangement.spacedBy(horizontalSpacing),
verticalArrangement = Arrangement.spacedBy(verticalSpacing)
) {
items(items = options, key = { it.goodsId ?: it.goodsCode!! }) { option ->
itemsIndexed(items = options, key = { index, it -> index }) { index, item ->
val isChecked = item.goodsId == checkedItem?.goodsId
Box(
modifier = modifier
.border(
width = 2.dp,
shape = RoundedCornerShape(10.dp),
color = if (option == selectedOption) selectedColor else unselectedColor,
color = if (isChecked) selectedColor else unselectedColor,
)
.clip(RoundedCornerShape(10.dp))
.background(if (option == selectedOption) selectedColor else Color.Transparent)
.background(if (isChecked) selectedColor else Color.Transparent)
.clickable {
selectedOption = option
onOptionSelected(option)
// selectedItem = item
onOptionSelected(item)
},
contentAlignment = Alignment.Center
// .padding(vertical = 20.dp)
) {
Text(
text = option.goodsNameStr,
style = if (option == selectedOption) AppTypography.BlueTextStyle else unSelectedTextStyle,
text = item.goodsNameStr,
style = if (isChecked) AppTypography.BlueTextStyle else unSelectedTextStyle,
modifier = Modifier
// .width(192.dp)
// .height(64.dp)
@@ -63,65 +63,27 @@ object InteractionUtils {
// ======================== 防抖处理 ========================
/**
* 防抖处理器
* 防抖工具类
* @param delayMillis 防抖延迟时间(毫秒)
*/
class Debouncer(
private val delayMillis: Long = 300L,
private val coroutineScope: CoroutineScope
) {
private var debounceJob: Job? = null
class Debouncer(private val delayMillis: Long) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param block 要执行的代码块
* @return Boolean 是否实际执行了操作
*/
fun <T> debounce(value: T, action: (T) -> Unit) {
debounceJob?.cancel()
debounceJob = coroutineScope.launch {
delay(delayMillis)
action(value)
}
}
}
/**
* 记住防抖处理器
*/
@Composable
fun rememberDebouncer(
delayMillis: Long = 300L,
coroutineScope: CoroutineScope = rememberCoroutineScope()
): Debouncer {
return remember { Debouncer(delayMillis, coroutineScope) }
}
// ======================== 节流处理 ========================
/**
* 节流处理器
*/
class Throttler(private val timeoutMs: Long = 300L) {
private var lastRunTime: Long = 0
/**
* 执行节流操作
*/
fun throttle(block: () -> Unit) {
val now = System.currentTimeMillis()
if (now - lastRunTime > timeoutMs) {
lastRunTime = now
fun debounce(block: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
block()
return true
}
return false
}
}
/**
* 记住节流处理器
*/
@Composable
fun rememberThrottler(timeoutMs: Long = 300L): Throttler {
return remember { Throttler(timeoutMs) }
}
// ======================== 双击检测 ========================
/**
@@ -0,0 +1,100 @@
package com.sw.inbound.utils
import android.content.Context
import android.net.Uri
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import timber.log.Timber
import java.io.File
/**
* 拍照工具类
* @param context Context 上下文
* @param cameraController CameraController 相机控制器
* @param onSuccess (Uri) -> Unit 拍照成功回调
* @param onError (String) -> Unit 拍照失败回调
*/
class PhotoCaptureHelper(
private val context: Context,
private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {}
) {
/**
* 拍照方法
* @param fileNamePrefix 文件名前缀,默认为"IMG_"
* @param fileExtension 文件扩展名,默认为".jpg"
*/
fun takePhoto(
fileNamePrefix: String = "IMG_",
fileExtension: String = ".jpg"
) {
Timber.d("开始拍照采集")
try {
val executor = ContextCompat.getMainExecutor(context)
val cacheDir = context.cacheDir
val photoFile = File.createTempFile(
"${fileNamePrefix}${System.currentTimeMillis()}",
fileExtension,
cacheDir
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
cameraController.takePicture(
outputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Timber.d("照片保存成功: $photoUri")
onSuccess(photoUri)
}
override fun onError(exception: ImageCaptureException) {
val errorMsg = "拍照失败: ${exception.message}"
Timber.e(exception, errorMsg)
onError(errorMsg)
}
}
)
} catch (e: Exception) {
val errorMsg = "创建临时文件失败: ${e.message}"
Timber.e(e, errorMsg)
onError(errorMsg)
}
}
}
/**
* 用于Compose的拍照Hook
* @param cameraController CameraController 相机控制器
* @param onSuccess (Uri) -> Unit 拍照成功回调
* @param onError (String) -> Unit 拍照失败回调
* @return Pair<PhotoCaptureHelper, () -> Unit> 返回工具类实例和拍照函数
*/
@Composable
fun rememberPhotoCapture(
cameraController: CameraController,
onSuccess: (Uri) -> Unit = {},
onError: (String) -> Unit = {}
): Pair<PhotoCaptureHelper, () -> Unit> {
val context = LocalContext.current
val photoCaptureHelper = remember {
PhotoCaptureHelper(
context = context,
cameraController = cameraController,
onSuccess = onSuccess,
onError = onError
)
}
return Pair(photoCaptureHelper) { photoCaptureHelper.takePhoto() }
}
@@ -1,13 +1,19 @@
package com.sw.inbound.viewmodel
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.inbound.GlobalData
import com.sw.inbound.model.response.ApiResponse
import com.sw.inbound.model.response.DictType
import com.sw.inbound.model.response.GoodsInfo
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.network.LoadingState
import com.sw.inbound.repository.RemoteRepository
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.ToastUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import retrofit2.HttpException
import timber.log.Timber
@@ -16,6 +22,40 @@ import java.io.IOException
abstract class BaseViewModel(
private val repository: RemoteRepository
) : ViewModel() {
// 需要拍照
private val _takePhotoState = MutableStateFlow(false)
val takePhotoState: StateFlow<Boolean> = _takePhotoState
// 称重结果
private val _weightInfo = MutableStateFlow<Double>(0.0)
val weightInfo: StateFlow<Double> = _weightInfo
// 搜索物品列表
private val _searchListItems = MutableStateFlow<List<SearchGoodsInfo.Record>>(emptyList())
val searchListItems: StateFlow<List<SearchGoodsInfo.Record>> = _searchListItems
// 识别列表
private val _identityListItems = MutableStateFlow<List<SearchGoodsInfo.Record>>(emptyList())
val identityListItems: StateFlow<List<SearchGoodsInfo.Record>> = _identityListItems
// 待收货的列表 用于识别时从中获取
private val _receiptList = MutableStateFlow<List<SearchGoodsInfo.Record>>(emptyList())
// 拍照uri
private val _lastPhotoUri = MutableStateFlow<Uri?>(null)
val lastPhotoUri: StateFlow<Uri?> = _lastPhotoUri
/**
* 是否可以加载更多
*/
private val _canLoadMore = MutableStateFlow(true)
val canLoadMore: StateFlow<Boolean> = _canLoadMore
/**
* 是否显示进度条
*/
private val _isMoreLoading = MutableStateFlow(false)
val isMoreLoading = _isMoreLoading
/**
* 带进度条的请求
@@ -67,6 +107,107 @@ abstract class BaseViewModel(
ToastUtils.showToast(message)
}
fun startSensorScale() {
Timber.d("开始称重")
SensorScaleUtils.startScale(callback = { weight ->
_weightInfo.value = weight
handleSensorScale(weight)
})
}
fun stopSensorScale() {
Timber.d("关闭称重")
SensorScaleUtils.stopContinuousRead()
}
abstract fun handleSensorScale(weight: Double)
fun updateCanLoadMore(canLoad: Boolean) {
_canLoadMore.value = canLoad
}
fun updateLastPhotoUri(photoUri: Uri?) {
_lastPhotoUri.value = photoUri
}
fun updateReceiptList(list: List<GoodsInfo>) {
_receiptList.value = list.map { goods ->
SearchGoodsInfo.Record(
goodsId = goods.goodId,
goodsName = goods.goodName
)
}
}
fun updateIdentityList(list: List<SearchGoodsInfo.Record>) {
_identityListItems.value = list
}
/**
* 获取识别列表
*/
fun getIdentityList(photoUri: Uri) {
Timber.d("getIdentityList")
launchWithLoading {
_searchListItems.value = emptyList<SearchGoodsInfo.Record>()
// 有收货列表,则返回收货列表
if (_receiptList.value.isNotEmpty()) {
_identityListItems.value = _receiptList.value
if (_identityListItems.value.isNotEmpty()) {
handleIdentityItem(_identityListItems.value[0])
}
} else {
val response = repository.searchGoodsInfoList("", 1, 10)
if (parseResponse(response)) {
_identityListItems.value = response.result?.records ?: emptyList()
if (_identityListItems.value.isNotEmpty()) {
handleIdentityItem(_identityListItems.value[0])
}
}
}
}
}
/**
* 处理识别后要选中的值
*/
open fun handleIdentityItem(goodsInfo: SearchGoodsInfo.Record) {}
fun updateMoreLoading(moreLoading: Boolean) {
_isMoreLoading.value = moreLoading
}
/**
* 搜索物品
*/
fun searchGoodsInfoList(
goodsName: String,
pageNo: Int = 1,
pageSize: Int = 10
) {
launch {
if (pageNo > 1) {
_isMoreLoading.value = true
}
_identityListItems.value = emptyList<SearchGoodsInfo.Record>()
val response = repository.searchGoodsInfoList(goodsName, pageNo, pageSize)
_isMoreLoading.value = false
if (parseResponse(response)) {
val data = response.result
data?.records?.let {
_canLoadMore.value = data.records.size >= pageSize
if (pageNo > 1) {
_searchListItems.value = _searchListItems.value + data.records
} else {
_searchListItems.value = data.records
}
}
} else {
_searchListItems.value = emptyList<SearchGoodsInfo.Record>()
}
}
}
fun getDictType() {
Timber.d("获取所有字典列表")
launchWithLoading {
@@ -21,32 +21,18 @@ class PurchaseOrderViewModel @Inject constructor(
*/
private val _supplierList = MutableStateFlow<List<SupplierInfo?>>(emptyList())
val supplierList: StateFlow<List<SupplierInfo?>> = _supplierList
/**
* 是否可以加载更多
*/
private val _canLoadMore = MutableStateFlow(true)
val canLoadMore: StateFlow<Boolean> = _canLoadMore
/**
* 是否显示进度条
*/
private val _isMoreLoading = MutableStateFlow(false)
val isMoreLoading = _isMoreLoading
/**
* 获取采购订单-供应商列表
*/
fun getOrderList(pageNum: Int = 1, pageSize: Int = 5) {
if (pageNum != 1) {
launch {
_isMoreLoading.value = true
updateMoreLoading(true)
val response = repository.getReceiveList(pageNum, pageSize)
_isMoreLoading.value = false
updateMoreLoading(false)
if (parseResponse(response)) {
response.result?.records?.let {
_canLoadMore.value = it.size >= pageSize
updateCanLoadMore(it.size >= pageSize)
Timber.d("加载更多 canLoadMore =${it.size >= pageSize}")
_supplierList.value = _supplierList.value + it
}
@@ -63,5 +49,9 @@ class PurchaseOrderViewModel @Inject constructor(
}
}
}
override fun handleSensorScale(weight: Double) {
}
}
@@ -10,7 +10,6 @@ import com.sw.inbound.model.response.GoodsInfo
import com.sw.inbound.model.response.PurchaseInfo
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.repository.RemoteRepository
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.ToastUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -63,10 +62,6 @@ class ReceiptViewModel @Inject constructor(
private val _selectedItem = MutableStateFlow<GoodsInfo?>(null)
val selectedItem: StateFlow<GoodsInfo?> = _selectedItem
// 搜索物品列表
private val _searchListItems = MutableStateFlow<List<SearchGoodsInfo.Record>>(emptyList())
val searchListItems: StateFlow<List<SearchGoodsInfo.Record>> = _searchListItems
// 收货请求结果
private val _receiptResult = MutableStateFlow<Boolean>(false)
val receiptResult: StateFlow<Boolean> = _receiptResult
@@ -109,6 +104,16 @@ class ReceiptViewModel @Inject constructor(
}
}
override fun handleIdentityItem(goodsInfo: SearchGoodsInfo.Record) {
val firstItem = unadjustedOrders.value.find { order ->
order.goodId == goodsInfo.goodsId
}
updateLastPhotoUri(null)
updateIdentityList(emptyList())
_selectedItem.value = firstItem
// updateSelectedItemWithSwitch(firstItem)
}
/**
* 更新选中的item
*/
@@ -237,75 +242,67 @@ class ReceiptViewModel @Inject constructor(
_amountUserInput.value = boolean
}
fun startSensorScale() {
Timber.d("开始称重")
SensorScaleUtils.startScale(callback = { weight ->
val currentItem = _selectedItem.value
currentItem?.let { info ->
// 用户输入数量后不再通过重量反算
if (_countUserInput.value) {
_selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(),
)
return@startScale
}
var consumeValue = currentItem.consumeValue?.toDouble() ?: 1.0
var purchaseValue = currentItem.purchaseValue?.toDouble() ?: 1.0
if (consumeValue == 0.0) {
consumeValue = 1.0
}
if (purchaseValue == 0.0) {
purchaseValue = 1.0
}
val count =
weight * 1000 / consumeValue / purchaseValue
val finalCount = count.toSafeFloat(currentItem.unitName)
Timber.d("startSensorScale weight = $weight, count = $count, finalCount = $finalCount, consumeValue = $consumeValue, purchaseValue = $purchaseValue")
// 计算单价
val finalPrice = when {
finalCount == 0f -> {
0f
}
_amountUserInput.value && !_priceUserInput.value -> {
info.recPriceExItem!!.div(finalCount).toSafeFloat()
}
else -> 0f
}
// 计算金额
val finalAmount = when {
finalCount == 0f -> {
0f
}
_priceUserInput.value && !_amountUserInput.value -> {
finalCount.times(info.newRecUnitPriceTaxIn!!).toSafeFloat()
}
else -> 0f
}
override fun handleSensorScale(weight: Double) {
val currentItem = _selectedItem.value
currentItem?.let { info ->
// 用户输入数量后不再通过重量反算
if (_countUserInput.value) {
_selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(),
// 收货数量
receivedNumTemp = finalCount.toFormattedString(),
receivedNum = finalCount,
// 单价
newRecUnitPriceTaxIn = if (_priceUserInput.value) info.newRecUnitPriceTaxIn else finalPrice,
newRecUnitPriceTaxInTemp = if (_priceUserInput.value) info.newRecUnitPriceTaxInTemp else finalPrice.toFormattedString(),
// 金额
recPriceExItem = if (_amountUserInput.value) info.recPriceExItem else finalAmount,
recPriceExItemTemp = if (_amountUserInput.value) info.recPriceExItemTemp else finalAmount.toFormattedString(),
)
return
}
})
}
var consumeValue = currentItem.consumeValue?.toDouble() ?: 1.0
var purchaseValue = currentItem.purchaseValue?.toDouble() ?: 1.0
if (consumeValue == 0.0) {
consumeValue = 1.0
}
if (purchaseValue == 0.0) {
purchaseValue = 1.0
}
val count =
weight * 1000 / consumeValue / purchaseValue
val finalCount = count.toSafeFloat(currentItem.unitName)
Timber.d("startSensorScale weight = $weight, count = $count, finalCount = $finalCount, consumeValue = $consumeValue, purchaseValue = $purchaseValue")
// 计算单价
val finalPrice = when {
finalCount == 0f -> {
0f
}
fun stopSensorScale() {
Timber.d("关闭称重")
SensorScaleUtils.stopContinuousRead()
_amountUserInput.value && !_priceUserInput.value -> {
info.recPriceExItem!!.div(finalCount).toSafeFloat()
}
else -> 0f
}
// 计算金额
val finalAmount = when {
finalCount == 0f -> {
0f
}
_priceUserInput.value && !_amountUserInput.value -> {
finalCount.times(info.newRecUnitPriceTaxIn!!).toSafeFloat()
}
else -> 0f
}
_selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(),
// 收货数量
receivedNumTemp = finalCount.toFormattedString(),
receivedNum = finalCount,
// 单价
newRecUnitPriceTaxIn = if (_priceUserInput.value) info.newRecUnitPriceTaxIn else finalPrice,
newRecUnitPriceTaxInTemp = if (_priceUserInput.value) info.newRecUnitPriceTaxInTemp else finalPrice.toFormattedString(),
// 金额
recPriceExItem = if (_amountUserInput.value) info.recPriceExItem else finalAmount,
recPriceExItemTemp = if (_amountUserInput.value) info.recPriceExItemTemp else finalAmount.toFormattedString(),
)
}
}
fun partialReceipt(uploadInfo: UploadInfo) {
@@ -11,7 +11,6 @@ import com.sw.inbound.model.request.PurchaseWarehouseParam
import com.sw.inbound.model.response.DictType
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.repository.RemoteRepository
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.ContextUtils
import com.sw.inbound.utils.FileUtils
import com.sw.inbound.utils.ToastUtils
@@ -68,13 +67,6 @@ class SelfProcurementViewModel @Inject constructor(
private val _showAddPurchaseUnitDialog = MutableStateFlow<Boolean>(false)
val showAddPurchaseUnitDialog: StateFlow<Boolean> = _showAddPurchaseUnitDialog
// 搜索物品列表
private val _searchListItems = MutableStateFlow<List<SearchGoodsInfo.Record>>(emptyList())
val searchListItems: StateFlow<List<SearchGoodsInfo.Record>> = _searchListItems
// 称重结果
private val _weightInfo = MutableStateFlow<Double?>(0.0)
val weightInfo: StateFlow<Double?> = _weightInfo
// 是否可以下拉
private val _dropdownTextChange = MutableStateFlow<Boolean>(false)
@@ -95,18 +87,6 @@ class SelfProcurementViewModel @Inject constructor(
*/
private val _amountUserInput = MutableStateFlow<Boolean>(false)
/**
* 是否可以加载更多
*/
private val _canLoadMore = MutableStateFlow(true)
val canLoadMore: StateFlow<Boolean> = _canLoadMore
/**
* 是否显示进度条
*/
private val _isMoreLoading = MutableStateFlow(false)
val isMoreLoading = _isMoreLoading
/**
* 更新全局仓库
*/
@@ -114,76 +94,65 @@ class SelfProcurementViewModel @Inject constructor(
_globalWarehouse.value = dictType
}
/**
* 开始称重
*/
fun startSensorScale() {
Timber.d("开始称重")
SensorScaleUtils.startScale(callback = { weight ->
val currentItem = _selectedItem.value
override fun handleSensorScale(weight: Double) {
val currentItem = _selectedItem.value
currentItem?.let { info ->
val selectUnitType = currentItem.selectUnitType
// 用户输入数量后不再通过重量反算
if (_countUserInput.value || selectUnitType == null) {
_selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(),
)
return@startScale
}
currentItem?.let { info ->
val selectUnitType = currentItem.selectUnitType
// 用户输入数量后不再通过重量反算
if (_countUserInput.value || selectUnitType == null) {
_selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(),
)
return
}
// val selectUnitType = currentItem.selectUnitType
// if (selectUnitType == null) return@startScale
var consumeValue = selectUnitType.consumeValue?.toDouble() ?: 1.0
var purchaseValue = selectUnitType.purchaseValue?.toDouble() ?: 1.0
if (consumeValue == 0.0) {
consumeValue = 1.0
}
if (purchaseValue == 0.0) {
purchaseValue = 1.0
}
val count =
weight * 1000 / consumeValue / purchaseValue
val finalCount = count.toSafeDouble(currentItem.unitName)
Timber.d("startSensorScale weight = $weight, count = $count, finalCount = $finalCount, consumeValue = $consumeValue, purchaseValue = $purchaseValue")
Timber.d("startSensorScale goodsUnitPrice = ${info.goodsUnitPrice}, goodsPrice = ${info.goodsPrice} ")
// 计算单价 金额不为空,数量也不为空, 不是数量输入 也不是单价输入
val finalPrice =
if (finalCount == 0.0) {
0.0
} else if (_amountUserInput.value && !_priceUserInput.value) {
info.goodsPrice.div(finalCount).toSafeDouble()
} else 0.0
// 计算金额 单价不为空,不是数量输入 也不是金额输入
val finalAmount =
if (finalCount == 0.0) {
0.0
} else if (_priceUserInput.value && !_amountUserInput.value) {
finalCount.times(info.goodsUnitPrice).toSafeDouble()
} else 0.0
Timber.d("startSensorScale finalPrice = $finalPrice, finalAmount = $finalAmount ")
_selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(),
// 数量
goodsCountTemp = finalCount.toFormattedString(),
goodsCount = finalCount,
// 单价
goodsUnitPrice = if (_priceUserInput.value) info.goodsUnitPrice else finalPrice,
goodsUnitPriceTemp = if (_priceUserInput.value) info.goodsUnitPriceTemp else finalPrice.toFormattedString(),
// 金额
goodsPrice = if (_amountUserInput.value) info.goodsPrice else finalAmount,
goodsPriceTemp = if (_amountUserInput.value) info.goodsPriceTemp else finalAmount.toFormattedString()
)
var consumeValue = selectUnitType.consumeValue?.toDouble() ?: 1.0
var purchaseValue = selectUnitType.purchaseValue?.toDouble() ?: 1.0
if (consumeValue == 0.0) {
consumeValue = 1.0
}
})
}
if (purchaseValue == 0.0) {
purchaseValue = 1.0
}
val count =
weight * 1000 / consumeValue / purchaseValue
val finalCount = count.toSafeDouble(currentItem.unitName)
Timber.d("startSensorScale weight = $weight, count = $count, finalCount = $finalCount, consumeValue = $consumeValue, purchaseValue = $purchaseValue")
fun stopSensorScale() {
Timber.d("关闭称重")
SensorScaleUtils.stopContinuousRead()
Timber.d("startSensorScale goodsUnitPrice = ${info.goodsUnitPrice}, goodsPrice = ${info.goodsPrice} ")
// 计算单价 金额不为空,数量也不为空, 不是数量输入 也不是单价输入
val finalPrice =
if (finalCount == 0.0) {
0.0
} else if (_amountUserInput.value && !_priceUserInput.value) {
info.goodsPrice.div(finalCount).toSafeDouble()
} else 0.0
// 计算金额 单价不为空,不是数量输入 也不是金额输入
val finalAmount =
if (finalCount == 0.0) {
0.0
} else if (_priceUserInput.value && !_amountUserInput.value) {
finalCount.times(info.goodsUnitPrice).toSafeDouble()
} else 0.0
Timber.d("startSensorScale finalPrice = $finalPrice, finalAmount = $finalAmount ")
_selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(),
// 数量
goodsCountTemp = finalCount.toFormattedString(),
goodsCount = finalCount,
// 单价
goodsUnitPrice = if (_priceUserInput.value) info.goodsUnitPrice else finalPrice,
goodsUnitPriceTemp = if (_priceUserInput.value) info.goodsUnitPriceTemp else finalPrice.toFormattedString(),
// 金额
goodsPrice = if (_amountUserInput.value) info.goodsPrice else finalAmount,
goodsPriceTemp = if (_amountUserInput.value) info.goodsPriceTemp else finalAmount.toFormattedString()
)
}
}
fun updateAddProductDialog(show: Boolean) {
@@ -194,6 +163,7 @@ class SelfProcurementViewModel @Inject constructor(
}
fun updateSelectedItem(purchaseOrder: PurchaseWarehouseParam?) {
// updateLastPhotoUri(null)
if (purchaseOrder != null) {
if (purchaseOrder.goodsCount != 0.0) {
if (purchaseOrder.goodsUnitPriceTemp.isNotEmpty()) { // 计算金额 单价不为空,并且金额没有手动输入
@@ -277,34 +247,8 @@ class SelfProcurementViewModel @Inject constructor(
_purchaseList.value = emptyList()
}
/**
* 搜索物品
*/
fun searchGoodsInfoList(
goodsName: String,
pageNo: Int = 1,
pageSize: Int = 10
) {
launch {
if (pageNo > 1) {
_isMoreLoading.value = true
}
val response = repository.searchGoodsInfoList(goodsName, pageNo, pageSize)
_isMoreLoading.value = false
if (parseResponse(response)) {
val data = response.result
data?.records?.let {
_canLoadMore.value = data.records.size >= pageSize
if (pageNo > 1) {
_searchListItems.value = _searchListItems.value + data.records
} else {
_searchListItems.value = data.records
}
}
} else {
_searchListItems.value = emptyList<SearchGoodsInfo.Record>()
}
}
override fun handleIdentityItem(goodsInfo: SearchGoodsInfo.Record) {
updateSelectedItemWithSearch(goodsInfo)
}
fun updateSelectedItemWithSearch(searchFirst: SearchGoodsInfo.Record) {
@@ -64,4 +64,8 @@ class UserViewModel @Inject constructor(
SPUtil.getInstance().remove(GlobalKey.KEY_USER_INFO)
SPUtil.getInstance().remove(GlobalKey.KEY_TOKEN)
}
override fun handleSensorScale(weight: Double) {
}
}