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

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 { object NetworkModule {
// private const val BASE_URL = "https://127.0.0.1" // 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 = "http://192.168.1.237:9002/"
// private const val BASE_URL = "https://vip.shuziweidao.com/gateway/" // private const val BASE_URL = "https://vip.shuziweidao.com/gateway/"
@@ -116,6 +116,7 @@ object SensorScaleUtils {
* 去皮置零 * 去皮置零
*/ */
fun tare() { fun tare() {
Timber.d("tare isOpened = $isOpened")
if (!isOpened) return if (!isOpened) return
mSensorScale?.tare { mSensorScale?.tare {
Timber.d("去皮置零操作成功") 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) { Column(modifier = modifier) {
TopTitleBar() TopTitleBar()
Box( Box(
@@ -241,6 +251,10 @@ fun TabScreen(viewModel: ReceiptViewModel) {
val tabs = val tabs =
listOf("未确认(${purchaseUnadjustedList.size})", "已确认(${purchaseAdjustList.size})") listOf("未确认(${purchaseUnadjustedList.size})", "已确认(${purchaseAdjustList.size})")
LaunchedEffect(purchaseUnadjustedList) {
viewModel.updateReceiptList(purchaseUnadjustedList)
}
Column( Column(
modifier = Modifier modifier = Modifier
.background( .background(
@@ -493,7 +507,6 @@ private fun AdjustedRowInputLayout(
@Composable @Composable
fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel) { fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel) {
val searchResultList by viewModel.searchListItems.collectAsState()
val selectedItem by viewModel.selectedItem.collectAsState() val selectedItem by viewModel.selectedItem.collectAsState()
Column( Column(
modifier = modifier modifier = modifier
@@ -507,7 +520,11 @@ fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel)
) { ) {
if (selectedItem == null) { if (selectedItem == null) {
Spacer(modifier = Modifier.height(30.dp)) Spacer(modifier = Modifier.height(30.dp))
IdentityView(list = searchResultList, showSearchView = false, onOptionSelected = {}) IdentityView(
showSearchView = false,
onOptionSelected = {},
baseViewModel = viewModel
)
} else { } else {
ReceiptProductEditView(viewModel) ReceiptProductEditView(viewModel)
} }
@@ -525,16 +542,6 @@ private fun ReceiptProductEditView(
) { ) {
val selectedItem by viewModel.selectedItem.collectAsState() val selectedItem by viewModel.selectedItem.collectAsState()
val warehouseTypeList = GlobalData.warehouseTypeList val warehouseTypeList = GlobalData.warehouseTypeList
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
Timber.d("开始传感器采集")
viewModel.startSensorScale()
onDispose {
Timber.d("停止传感器采集")
viewModel.stopSensorScale()
}
}
selectedItem?.let { selectedItem?.let {
Column( Column(
@@ -24,10 +24,6 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -256,13 +252,7 @@ fun ContentRightView(
*/ */
@Composable @Composable
fun ProductIdentification(viewModel: SelfProcurementViewModel) { fun ProductIdentification(viewModel: SelfProcurementViewModel) {
val searchResultList by viewModel.searchListItems.collectAsState() val selectedItem by viewModel.selectedItem.collectAsState()
// 需要搜索的信息
var searchInfo by remember { mutableStateOf("") }
var pageNum by remember { mutableIntStateOf(1) }
val isMoreLoading by viewModel.isMoreLoading.collectAsState()
val canLoadMore by viewModel.canLoadMore.collectAsState()
Column( Column(
modifier = Modifier modifier = Modifier
.width(457.dp) .width(457.dp)
@@ -275,19 +265,9 @@ fun ProductIdentification(viewModel: SelfProcurementViewModel) {
.fillMaxWidth() .fillMaxWidth()
) { ) {
// 菜品识别 // 菜品识别
IdentityView(list = searchResultList, showSearchView = true, onSearchClick = { IdentityView(showSearchView = true, onOptionSelected = {
searchInfo = it
viewModel.searchGoodsInfoList(it)
}, onOptionSelected = {
viewModel.updateSelectedItemWithSearch(it) viewModel.updateSelectedItemWithSearch(it)
}, onLoadMore = { }, checkedItem = selectedItem, baseViewModel = viewModel)
Timber.d("加载更多 canLoadMore = $canLoadMore")
if (!canLoadMore) {
return@IdentityView
}
pageNum++
viewModel.searchGoodsInfoList(goodsName = searchInfo, pageNo = pageNum)
}, isMoreLoading = isMoreLoading, canLoadMore = canLoadMore)
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Row( Row(
@@ -873,7 +853,9 @@ private fun SelfProductEditView(
textColor = colorResource(R.color.green), textColor = colorResource(R.color.green),
borderColor = colorResource(R.color.green), borderColor = colorResource(R.color.green),
onClick = { onClick = {
viewModel.updateLastPhotoUri(null)
viewModel.updateSelectedItem(null) viewModel.updateSelectedItem(null)
viewModel.updateIdentityList(emptyList())
}) })
CustomButton( CustomButton(
modifier = Modifier modifier = Modifier
@@ -1,18 +1,12 @@
package com.sw.inbound.ui.weight package com.sw.inbound.ui.weight
import android.net.Uri import android.net.Uri
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect 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.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.sw.inbound.GlobalData 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.ui.theme.AppTypography
import com.sw.inbound.utils.FileUtils import com.sw.inbound.utils.FileUtils
import com.sw.inbound.utils.ToastUtils import com.sw.inbound.utils.ToastUtils
import com.sw.inbound.utils.rememberPhotoCapture
import timber.log.Timber 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( Column(
modifier = modifier modifier = modifier
.width(428.dp) .width(428.dp)
@@ -165,38 +174,7 @@ fun CameraCaptureLayout(
ToastUtils.showToast("请先开启图片预览") ToastUtils.showToast("请先开启图片预览")
return@CustomButton return@CustomButton
} }
val executor = ContextCompat.getMainExecutor(context) takePhoto()
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()
}
})
}, },
borderColor = colorResource(R.color.green), borderColor = colorResource(R.color.green),
textColor = colorResource(R.color.white), textColor = colorResource(R.color.white),
@@ -213,52 +191,3 @@ fun CameraCaptureLayout(
onDispose { } 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 package com.sw.inbound.ui.weight
import android.net.Uri
import androidx.camera.view.LifecycleCameraController import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import coil.compose.AsyncImage
import com.sw.inbound.R import com.sw.inbound.R
/** /**
@@ -19,7 +21,8 @@ import com.sw.inbound.R
@Composable @Composable
fun CameraPreview( fun CameraPreview(
controller: LifecycleCameraController, controller: LifecycleCameraController,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
photoUri: Uri? = null
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
AndroidView( AndroidView(
@@ -31,6 +34,13 @@ fun CameraPreview(
}, },
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
if (photoUri != null) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = photoUri,
contentDescription = "照片",
)
}
Image( Image(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -1,5 +1,6 @@
package com.sw.inbound.ui.weight package com.sw.inbound.ui.weight
import android.widget.Toast
import androidx.camera.view.CameraController import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.layout.Column 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.height
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable 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.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import com.sw.inbound.ext.medium import com.sw.inbound.ext.medium
import com.sw.inbound.model.request.PurchaseWarehouseParam
import com.sw.inbound.model.response.SearchGoodsInfo import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.ui.theme.AppTypography 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 @Composable
fun IdentityView( fun IdentityView(
list: List<SearchGoodsInfo.Record>,
showSearchView: Boolean = false, showSearchView: Boolean = false,
onSearchClick: (String) -> Unit = {},
onOptionSelected: (SearchGoodsInfo.Record) -> Unit = {}, onOptionSelected: (SearchGoodsInfo.Record) -> Unit = {},
onLoadMore: (Int) -> Unit = {}, checkedItem: PurchaseWarehouseParam? = null,
isMoreLoading: Boolean = false, baseViewModel: BaseViewModel,
canLoadMore: Boolean = true
) { ) {
val context = LocalContext.current val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.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 控制器 // CameraX 控制器
val cameraController = remember { val cameraController = remember {
LifecycleCameraController(context).apply { 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
@@ -50,11 +115,15 @@ fun IdentityView(
modifier = Modifier modifier = Modifier
.width(397.dp) .width(397.dp)
.height(298.dp), .height(298.dp),
controller = cameraController controller = cameraController,
photoUri = lastPhotoUri
) )
if (showSearchView) { if (showSearchView) {
Spacer(modifier = Modifier.height(30.dp)) Spacer(modifier = Modifier.height(30.dp))
CustomSearchView(onSearchClick = onSearchClick) CustomSearchView(onSearchClick = {
searchInfo = it
baseViewModel.searchGoodsInfoList(it)
})
} }
Spacer(modifier = Modifier.height(30.dp)) Spacer(modifier = Modifier.height(30.dp))
if (showSearchView) { if (showSearchView) {
@@ -63,7 +132,15 @@ fun IdentityView(
.height(64.dp) .height(64.dp)
.width(192.dp), options = list, .width(192.dp), options = list,
onOptionSelected = onOptionSelected, 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, isMoreLoading = isMoreLoading,
canLoadMore = canLoadMore canLoadMore = canLoadMore
) )
@@ -96,7 +96,7 @@ fun ReceiptUnadjustedListView(
key = { index, it -> it.id ?: index }) { index, it -> key = { index, it -> it.id ?: index }) { index, it ->
val checkedBg = val checkedBg =
if (selectIndex == index) Gray_DCDCF0 else Color.Transparent if (/*selectIndex == index ||*/ checkedItem?.id == it.id) Gray_DCDCF0 else Color.Transparent
Row( Row(
modifier = Modifier modifier = Modifier
.background(color = checkedBg) .background(color = checkedBg)
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 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.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -72,10 +72,11 @@ fun SingleSelectButtonGroup(
unSelectedTextStyle: TextStyle = AppTypography.gray96a0aaTextStyle, unSelectedTextStyle: TextStyle = AppTypography.gray96a0aaTextStyle,
onLoadMore: (Int) -> Unit = {}, onLoadMore: (Int) -> Unit = {},
isMoreLoading: Boolean = false, isMoreLoading: Boolean = false,
canLoadMore: Boolean = true canLoadMore: Boolean = true,
checkedItem: SearchGoodsInfo.Record? = null
) { ) {
val context = LocalContext.current val context = LocalContext.current
var selectedOption by remember { mutableStateOf(options.firstOrNull() ?: "") } var selectedItem by remember { mutableStateOf(options.firstOrNull() ?: "") }
val lazyGridState = rememberLazyGridState() val lazyGridState = rememberLazyGridState()
var pageNum by remember { mutableIntStateOf(1) } var pageNum by remember { mutableIntStateOf(1) }
@@ -109,26 +110,27 @@ fun SingleSelectButtonGroup(
horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), horizontalArrangement = Arrangement.spacedBy(horizontalSpacing),
verticalArrangement = Arrangement.spacedBy(verticalSpacing) 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( Box(
modifier = modifier modifier = modifier
.border( .border(
width = 2.dp, width = 2.dp,
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
color = if (option == selectedOption) selectedColor else unselectedColor, color = if (isChecked) selectedColor else unselectedColor,
) )
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
.background(if (option == selectedOption) selectedColor else Color.Transparent) .background(if (isChecked) selectedColor else Color.Transparent)
.clickable { .clickable {
selectedOption = option // selectedItem = item
onOptionSelected(option) onOptionSelected(item)
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
// .padding(vertical = 20.dp) // .padding(vertical = 20.dp)
) { ) {
Text( Text(
text = option.goodsNameStr, text = item.goodsNameStr,
style = if (option == selectedOption) AppTypography.BlueTextStyle else unSelectedTextStyle, style = if (isChecked) AppTypography.BlueTextStyle else unSelectedTextStyle,
modifier = Modifier modifier = Modifier
// .width(192.dp) // .width(192.dp)
// .height(64.dp) // .height(64.dp)
@@ -63,65 +63,27 @@ object InteractionUtils {
// ======================== 防抖处理 ======================== // ======================== 防抖处理 ========================
/** /**
* 防抖处理器 * 防抖工具类
* @param delayMillis 防抖延迟时间(毫秒)
*/ */
class Debouncer( class Debouncer(private val delayMillis: Long) {
private val delayMillis: Long = 300L, private var lastActionTime = 0L
private val coroutineScope: CoroutineScope
) {
private var debounceJob: Job? = null
/** /**
* 执行防抖操作 * 执行防抖操作
* @param block 要执行的代码块
* @return Boolean 是否实际执行了操作
*/ */
fun <T> debounce(value: T, action: (T) -> Unit) { fun debounce(block: () -> Unit): Boolean {
debounceJob?.cancel() val currentTime = System.currentTimeMillis()
debounceJob = coroutineScope.launch { if (currentTime - lastActionTime >= delayMillis) {
delay(delayMillis) lastActionTime = currentTime
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
block() 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 package com.sw.inbound.viewmodel
import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.sw.inbound.GlobalData import com.sw.inbound.GlobalData
import com.sw.inbound.model.response.ApiResponse import com.sw.inbound.model.response.ApiResponse
import com.sw.inbound.model.response.DictType 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.network.LoadingState
import com.sw.inbound.repository.RemoteRepository import com.sw.inbound.repository.RemoteRepository
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.ToastUtils import com.sw.inbound.utils.ToastUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
@@ -16,6 +22,40 @@ import java.io.IOException
abstract class BaseViewModel( abstract class BaseViewModel(
private val repository: RemoteRepository private val repository: RemoteRepository
) : ViewModel() { ) : 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) 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() { fun getDictType() {
Timber.d("获取所有字典列表") Timber.d("获取所有字典列表")
launchWithLoading { launchWithLoading {
@@ -21,32 +21,18 @@ class PurchaseOrderViewModel @Inject constructor(
*/ */
private val _supplierList = MutableStateFlow<List<SupplierInfo?>>(emptyList()) private val _supplierList = MutableStateFlow<List<SupplierInfo?>>(emptyList())
val supplierList: StateFlow<List<SupplierInfo?>> = _supplierList 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) { fun getOrderList(pageNum: Int = 1, pageSize: Int = 5) {
if (pageNum != 1) { if (pageNum != 1) {
launch { launch {
_isMoreLoading.value = true updateMoreLoading(true)
val response = repository.getReceiveList(pageNum, pageSize) val response = repository.getReceiveList(pageNum, pageSize)
_isMoreLoading.value = false updateMoreLoading(false)
if (parseResponse(response)) { if (parseResponse(response)) {
response.result?.records?.let { response.result?.records?.let {
_canLoadMore.value = it.size >= pageSize updateCanLoadMore(it.size >= pageSize)
Timber.d("加载更多 canLoadMore =${it.size >= pageSize}") Timber.d("加载更多 canLoadMore =${it.size >= pageSize}")
_supplierList.value = _supplierList.value + it _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.PurchaseInfo
import com.sw.inbound.model.response.SearchGoodsInfo import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.repository.RemoteRepository import com.sw.inbound.repository.RemoteRepository
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.ToastUtils import com.sw.inbound.utils.ToastUtils
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -63,10 +62,6 @@ class ReceiptViewModel @Inject constructor(
private val _selectedItem = MutableStateFlow<GoodsInfo?>(null) private val _selectedItem = MutableStateFlow<GoodsInfo?>(null)
val selectedItem: StateFlow<GoodsInfo?> = _selectedItem 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) private val _receiptResult = MutableStateFlow<Boolean>(false)
val receiptResult: StateFlow<Boolean> = _receiptResult 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 * 更新选中的item
*/ */
@@ -237,9 +242,7 @@ class ReceiptViewModel @Inject constructor(
_amountUserInput.value = boolean _amountUserInput.value = boolean
} }
fun startSensorScale() { override fun handleSensorScale(weight: Double) {
Timber.d("开始称重")
SensorScaleUtils.startScale(callback = { weight ->
val currentItem = _selectedItem.value val currentItem = _selectedItem.value
currentItem?.let { info -> currentItem?.let { info ->
@@ -248,7 +251,7 @@ class ReceiptViewModel @Inject constructor(
_selectedItem.value = info.copy( _selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(), goodsWeight = weight.toSafeBigDecimal(),
) )
return@startScale return
} }
var consumeValue = currentItem.consumeValue?.toDouble() ?: 1.0 var consumeValue = currentItem.consumeValue?.toDouble() ?: 1.0
var purchaseValue = currentItem.purchaseValue?.toDouble() ?: 1.0 var purchaseValue = currentItem.purchaseValue?.toDouble() ?: 1.0
@@ -300,12 +303,6 @@ class ReceiptViewModel @Inject constructor(
recPriceExItemTemp = if (_amountUserInput.value) info.recPriceExItemTemp else finalAmount.toFormattedString(), recPriceExItemTemp = if (_amountUserInput.value) info.recPriceExItemTemp else finalAmount.toFormattedString(),
) )
} }
})
}
fun stopSensorScale() {
Timber.d("关闭称重")
SensorScaleUtils.stopContinuousRead()
} }
fun partialReceipt(uploadInfo: UploadInfo) { 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.DictType
import com.sw.inbound.model.response.SearchGoodsInfo import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.repository.RemoteRepository import com.sw.inbound.repository.RemoteRepository
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.ContextUtils import com.sw.inbound.utils.ContextUtils
import com.sw.inbound.utils.FileUtils import com.sw.inbound.utils.FileUtils
import com.sw.inbound.utils.ToastUtils import com.sw.inbound.utils.ToastUtils
@@ -68,13 +67,6 @@ class SelfProcurementViewModel @Inject constructor(
private val _showAddPurchaseUnitDialog = MutableStateFlow<Boolean>(false) private val _showAddPurchaseUnitDialog = MutableStateFlow<Boolean>(false)
val showAddPurchaseUnitDialog: StateFlow<Boolean> = _showAddPurchaseUnitDialog 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) private val _dropdownTextChange = MutableStateFlow<Boolean>(false)
@@ -95,18 +87,6 @@ class SelfProcurementViewModel @Inject constructor(
*/ */
private val _amountUserInput = MutableStateFlow<Boolean>(false) 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,12 +94,7 @@ class SelfProcurementViewModel @Inject constructor(
_globalWarehouse.value = dictType _globalWarehouse.value = dictType
} }
/** override fun handleSensorScale(weight: Double) {
* 开始称重
*/
fun startSensorScale() {
Timber.d("开始称重")
SensorScaleUtils.startScale(callback = { weight ->
val currentItem = _selectedItem.value val currentItem = _selectedItem.value
currentItem?.let { info -> currentItem?.let { info ->
@@ -129,7 +104,7 @@ class SelfProcurementViewModel @Inject constructor(
_selectedItem.value = info.copy( _selectedItem.value = info.copy(
goodsWeight = weight.toSafeBigDecimal(), goodsWeight = weight.toSafeBigDecimal(),
) )
return@startScale return
} }
// val selectUnitType = currentItem.selectUnitType // val selectUnitType = currentItem.selectUnitType
@@ -178,12 +153,6 @@ class SelfProcurementViewModel @Inject constructor(
goodsPriceTemp = if (_amountUserInput.value) info.goodsPriceTemp else finalAmount.toFormattedString() goodsPriceTemp = if (_amountUserInput.value) info.goodsPriceTemp else finalAmount.toFormattedString()
) )
} }
})
}
fun stopSensorScale() {
Timber.d("关闭称重")
SensorScaleUtils.stopContinuousRead()
} }
fun updateAddProductDialog(show: Boolean) { fun updateAddProductDialog(show: Boolean) {
@@ -194,6 +163,7 @@ class SelfProcurementViewModel @Inject constructor(
} }
fun updateSelectedItem(purchaseOrder: PurchaseWarehouseParam?) { fun updateSelectedItem(purchaseOrder: PurchaseWarehouseParam?) {
// updateLastPhotoUri(null)
if (purchaseOrder != null) { if (purchaseOrder != null) {
if (purchaseOrder.goodsCount != 0.0) { if (purchaseOrder.goodsCount != 0.0) {
if (purchaseOrder.goodsUnitPriceTemp.isNotEmpty()) { // 计算金额 单价不为空,并且金额没有手动输入 if (purchaseOrder.goodsUnitPriceTemp.isNotEmpty()) { // 计算金额 单价不为空,并且金额没有手动输入
@@ -277,34 +247,8 @@ class SelfProcurementViewModel @Inject constructor(
_purchaseList.value = emptyList() _purchaseList.value = emptyList()
} }
/** override fun handleIdentityItem(goodsInfo: SearchGoodsInfo.Record) {
* 搜索物品 updateSelectedItemWithSearch(goodsInfo)
*/
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>()
}
}
} }
fun updateSelectedItemWithSearch(searchFirst: SearchGoodsInfo.Record) { 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_USER_INFO)
SPUtil.getInstance().remove(GlobalKey.KEY_TOKEN) SPUtil.getInstance().remove(GlobalKey.KEY_TOKEN)
} }
override fun handleSensorScale(weight: Double) {
}
} }