|
@@ -0,0 +1,412 @@
|
|
|
+package com.example.pda.ui
|
|
|
+
|
|
|
+import android.content.Context
|
|
|
+import androidx.compose.foundation.clickable
|
|
|
+import androidx.compose.runtime.*
|
|
|
+import androidx.compose.ui.platform.LocalContext
|
|
|
+import androidx.compose.ui.platform.LocalConfiguration
|
|
|
+import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.foundation.layout.*
|
|
|
+import androidx.compose.foundation.lazy.LazyColumn
|
|
|
+import androidx.compose.foundation.lazy.itemsIndexed
|
|
|
+import androidx.compose.material.icons.Icons
|
|
|
+import androidx.compose.material.icons.filled.ArrowBack
|
|
|
+import androidx.compose.material.icons.filled.Delete
|
|
|
+import androidx.compose.material3.*
|
|
|
+import androidx.compose.ui.graphics.Color
|
|
|
+import androidx.compose.ui.res.painterResource
|
|
|
+import androidx.compose.ui.unit.dp
|
|
|
+import android.media.AudioManager
|
|
|
+import android.media.ToneGenerator
|
|
|
+import androidx.compose.material.icons.filled.List
|
|
|
+import androidx.compose.material.icons.filled.Search
|
|
|
+import com.example.pda.R
|
|
|
+import com.example.pda.function.UBXScan
|
|
|
+import com.example.pda.ui.viewmodel.InventoryViewModel
|
|
|
+import androidx.compose.material3.SnackbarHostState
|
|
|
+import androidx.compose.material3.Scaffold
|
|
|
+import androidx.compose.material3.TabRowDefaults
|
|
|
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
|
|
+import androidx.compose.runtime.Composable
|
|
|
+import androidx.compose.runtime.LaunchedEffect
|
|
|
+import androidx.compose.runtime.collectAsState
|
|
|
+import androidx.compose.runtime.mutableStateOf
|
|
|
+import androidx.compose.runtime.remember
|
|
|
+import androidx.compose.runtime.rememberCoroutineScope
|
|
|
+import androidx.compose.ui.Alignment
|
|
|
+import androidx.lifecycle.viewmodel.compose.viewModel
|
|
|
+import com.example.pda.model.BatchTotal
|
|
|
+import com.example.pda.model.ContainerItemDetail
|
|
|
+import com.example.pda.model.MaterialResult
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+import java.text.SimpleDateFormat
|
|
|
+import java.util.Date
|
|
|
+import java.util.Locale
|
|
|
+
|
|
|
+// 托盘屏幕,用于托盘扫描和物料信息显示
|
|
|
+@OptIn(ExperimentalMaterial3Api::class)
|
|
|
+@Composable
|
|
|
+fun OutTaskScreen(
|
|
|
+ onBack: () -> Unit,
|
|
|
+ onContainerItems: (String) -> Unit // 新增回调参数
|
|
|
+) {
|
|
|
+ val viewModel: InventoryViewModel = viewModel()
|
|
|
+ val uiState = viewModel.uiState.collectAsState()
|
|
|
+ val snackbarHostState = remember { SnackbarHostState() }
|
|
|
+ val coroutineScope = rememberCoroutineScope()
|
|
|
+ // 新增输入状态
|
|
|
+ val inputIp = remember { mutableStateOf("100") }
|
|
|
+ val context = LocalContext.current
|
|
|
+ val ubxScan = remember { UBXScan() }
|
|
|
+ var isScanning by remember { mutableStateOf(true) }
|
|
|
+
|
|
|
+ val container = remember { mutableStateOf("") }
|
|
|
+ var showinput by remember { mutableStateOf(false) }
|
|
|
+ var showContainer by remember { mutableStateOf(true) }
|
|
|
+ val configuration = LocalConfiguration.current
|
|
|
+ val screenHeight = configuration.screenHeightDp.dp
|
|
|
+ val boxHeight = screenHeight * 0.05f
|
|
|
+ var selectedTab by remember { mutableStateOf(0) }
|
|
|
+
|
|
|
+ val toneGenerator = remember { ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100) }
|
|
|
+
|
|
|
+ // 根据 viewModel 的状态显示 Snackbar
|
|
|
+ LaunchedEffect(uiState.value) {
|
|
|
+ when (val currentState = uiState.value) {
|
|
|
+ is InventoryViewModel.UiState.Success -> {
|
|
|
+ coroutineScope.launch {
|
|
|
+ snackbarHostState.showSnackbar(currentState.message)
|
|
|
+ viewModel.resetState()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ is InventoryViewModel.UiState.Error -> {
|
|
|
+ coroutineScope.launch {
|
|
|
+ snackbarHostState.showSnackbar("错误:${currentState.message}")
|
|
|
+ viewModel.resetState()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ else -> {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始扫描托盘编码,并在扫描结果处理逻辑中更新托盘编码
|
|
|
+ DisposableEffect(Unit) {
|
|
|
+ startScanning(context, ubxScan) { result ->
|
|
|
+ if (container.value.isEmpty()) {
|
|
|
+ toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP)
|
|
|
+ container.value = result
|
|
|
+ viewModel.getOutItems(container.value)
|
|
|
+ showContainer = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 停止扫描并释放 ToneGenerator 资源
|
|
|
+ onDispose {
|
|
|
+ stopScanning(ubxScan)
|
|
|
+ isScanning = false
|
|
|
+ toneGenerator.release()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建 Scaffold 布局,包含顶部导航栏和主要内容区域
|
|
|
+ Scaffold(
|
|
|
+ snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
|
+ topBar = {
|
|
|
+ TopAppBar(
|
|
|
+ navigationIcon = {
|
|
|
+ IconButton(onClick = onBack) {
|
|
|
+ Icon(
|
|
|
+ imageVector = Icons.Default.ArrowBack,
|
|
|
+ contentDescription = "返回"
|
|
|
+ )
|
|
|
+ }
|
|
|
+ },
|
|
|
+ title = {
|
|
|
+ Row(verticalAlignment = Alignment.CenterVertically) {
|
|
|
+ Icon(
|
|
|
+ painter = painterResource(id = R.drawable.logo),
|
|
|
+ contentDescription = "PDA Logo",
|
|
|
+ modifier = Modifier.size(40.dp),
|
|
|
+ tint = MaterialTheme.colorScheme.surfaceTint
|
|
|
+ )
|
|
|
+ Spacer(Modifier.width(8.dp))
|
|
|
+ Text("信泰PDA—出库确认")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ colors = TopAppBarDefaults.topAppBarColors(
|
|
|
+ containerColor = Color(0xFFBCD0C5), // 自定义颜色
|
|
|
+ titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
|
|
+ actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
|
|
+ )
|
|
|
+ )
|
|
|
+ },
|
|
|
+
|
|
|
+ ) { innerPadding ->
|
|
|
+ Column(
|
|
|
+ modifier = Modifier
|
|
|
+ .padding(innerPadding)
|
|
|
+ .fillMaxSize()
|
|
|
+ .padding(24.dp)
|
|
|
+ ) {
|
|
|
+ // 扫描提示区域
|
|
|
+ Box(
|
|
|
+ modifier = Modifier
|
|
|
+ .height(boxHeight)
|
|
|
+ .fillMaxWidth()
|
|
|
+ .clickable(enabled = isScanning) {
|
|
|
+ container.value = ""
|
|
|
+ showinput = true
|
|
|
+ showContainer = false
|
|
|
+ },
|
|
|
+ contentAlignment = Alignment.Center
|
|
|
+ ) {
|
|
|
+ if (isScanning && showContainer) {
|
|
|
+ Text(text = "托盘条码扫描")
|
|
|
+ } else if (isScanning && showinput) {
|
|
|
+ Text(text = "输入条形码")
|
|
|
+ } else {
|
|
|
+ Text(text = "物料信息")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (showinput) {
|
|
|
+ Row(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .padding(8.dp),
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
+
|
|
|
+ ) {
|
|
|
+ // 新增输入框
|
|
|
+ TextField(
|
|
|
+ value = inputIp.value,
|
|
|
+ onValueChange = { inputIp.value = it },
|
|
|
+ label = { Text("请输入托盘编码") },
|
|
|
+ )
|
|
|
+
|
|
|
+ IconButton(
|
|
|
+ onClick = {
|
|
|
+ container.value = inputIp.value;
|
|
|
+ viewModel.getOutItems(container.value)
|
|
|
+ },
|
|
|
+ ) {
|
|
|
+ Icon(
|
|
|
+ imageVector = Icons.Default.Search,
|
|
|
+ contentDescription = "确定"
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加托盘扫描
|
|
|
+ Row(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .padding(8.dp),
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
+ ) {
|
|
|
+ Text(
|
|
|
+ text = "托盘编码:${container.value}",
|
|
|
+ modifier = Modifier.weight(1f)
|
|
|
+ )
|
|
|
+
|
|
|
+ Spacer(Modifier.height(8.dp))
|
|
|
+ IconButton(
|
|
|
+ onClick = {
|
|
|
+ onContainerItems(container.value)
|
|
|
+ },
|
|
|
+ ) {
|
|
|
+ Icon(Icons.Default.List, "确定")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 已扫描列表
|
|
|
+ // 分页导航
|
|
|
+ TabRow(
|
|
|
+ selectedTabIndex = selectedTab,
|
|
|
+ indicator = { tabPositions ->
|
|
|
+ TabRowDefaults.Indicator(
|
|
|
+ modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTab]),
|
|
|
+ height = 2.dp,
|
|
|
+ color = MaterialTheme.colorScheme.primary
|
|
|
+ )
|
|
|
+ }
|
|
|
+ ) {
|
|
|
+ listOf("批次统计", "物料明细").forEachIndexed { index, title ->
|
|
|
+ Tab(
|
|
|
+ selected = selectedTab == index,
|
|
|
+ onClick = { selectedTab = index },
|
|
|
+ text = { Text(title) },
|
|
|
+ selectedContentColor = MaterialTheme.colorScheme.primary,
|
|
|
+ unselectedContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 内容区域
|
|
|
+ // Tab 内容区域添加权重
|
|
|
+ when (selectedTab) {
|
|
|
+ 0 -> BatchTotalList(
|
|
|
+
|
|
|
+ data = OutItemsToBatchTotal(viewModel.containerItemsDetails.collectAsState().value)
|
|
|
+
|
|
|
+ )
|
|
|
+
|
|
|
+ 1 -> MaterialResultList(
|
|
|
+ data = OutItemsToMaterialResult(viewModel.containerItemsDetails.collectAsState().value)
|
|
|
+
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// (不再直接调用onResult)
|
|
|
+private fun startScanning(
|
|
|
+ context: Context,
|
|
|
+ ubxScan: UBXScan,
|
|
|
+ onScanned: (String) -> Unit
|
|
|
+) {
|
|
|
+ ubxScan.setOnScanListener(context) { result ->
|
|
|
+ onScanned(result)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 停止扫描
|
|
|
+private fun stopScanning(ubxScan: UBXScan) {
|
|
|
+ ubxScan.destroy()
|
|
|
+}
|
|
|
+
|
|
|
+private fun OutItemsToBatchTotal(input: List<ContainerItemDetail>): List<BatchTotal> {
|
|
|
+ // 1. 按批次号分组并计算每个批次的总数量
|
|
|
+ val batchMap = mutableMapOf<String, Int>()
|
|
|
+
|
|
|
+ input.forEach { item ->
|
|
|
+ // 计算当前物料的数量(入库-出库)
|
|
|
+ val currentQty =item.goods_out_qty
|
|
|
+
|
|
|
+ // 累加到批次总数
|
|
|
+ batchMap[item.batch_number] = batchMap.getOrDefault(item.batch_number, 0) + currentQty
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 转换为 BatchTotal 对象列表
|
|
|
+ return batchMap.map { (batchNumber, totalQty) ->
|
|
|
+ BatchTotal(
|
|
|
+ bound_number = batchNumber,
|
|
|
+ total_batch_qty = totalQty
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+private fun OutItemsToMaterialResult(input: List<ContainerItemDetail>): List<MaterialResult> {
|
|
|
+ // 1. 按(批次号, 出库数量)分组并统计每个分组的记录数
|
|
|
+ val materialGroupMap = mutableMapOf<Pair<String, Int>, Int>()
|
|
|
+
|
|
|
+ input.forEach { item ->
|
|
|
+ val key = Pair(item.batch_number, item.goods_out_qty)
|
|
|
+ materialGroupMap[key] = materialGroupMap.getOrDefault(key, 0) + 1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取当前时间格式化器
|
|
|
+ val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.getDefault())
|
|
|
+ val currentTime = dateFormat.format(Date())
|
|
|
+
|
|
|
+ // 3. 转换为 MaterialResult 对象列表
|
|
|
+ return materialGroupMap.map { (groupKey, count) ->
|
|
|
+ // 查找该分组中的第一个物料作为代表(或根据业务需求调整)
|
|
|
+ val representativeItem = input.firstOrNull {
|
|
|
+ it.batch_number == groupKey.first && it.goods_out_qty == groupKey.second
|
|
|
+ }
|
|
|
+
|
|
|
+ MaterialResult(
|
|
|
+ goods_code = representativeItem?.goods_code ?: "",
|
|
|
+ goods_desc = representativeItem?.goods_desc ?: "",
|
|
|
+ current_qty = groupKey.second, // 出库数量作为当前数量
|
|
|
+ group_qty = count, // 分组中的记录数
|
|
|
+ bound_number = groupKey.first, // 批次号
|
|
|
+ create_time = currentTime
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@Composable
|
|
|
+private fun BatchTotalList(data: List<BatchTotal>) {
|
|
|
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
|
+ itemsIndexed(data) { index, item ->
|
|
|
+ Card(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .padding(8.dp),
|
|
|
+ elevation = CardDefaults.cardElevation(4.dp)
|
|
|
+ ) {
|
|
|
+ Column(modifier = Modifier.padding(16.dp)) {
|
|
|
+ Text(
|
|
|
+ "批次号:${item.bound_number}",
|
|
|
+ style = MaterialTheme.typography.titleMedium
|
|
|
+ )
|
|
|
+ Spacer(Modifier.height(8.dp))
|
|
|
+ Text(
|
|
|
+ "总数量:${item.total_batch_qty}",
|
|
|
+ style = MaterialTheme.typography.bodyLarge
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@Composable
|
|
|
+private fun MaterialResultList(data: List<MaterialResult>) {
|
|
|
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
|
+ itemsIndexed(data) { index, item ->
|
|
|
+ Column(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .padding(16.dp)
|
|
|
+ ) {
|
|
|
+
|
|
|
+ Text(
|
|
|
+ "所属批次:${item.bound_number}",
|
|
|
+ style = MaterialTheme.typography.bodyLarge,
|
|
|
+ color = MaterialTheme.colorScheme.primary
|
|
|
+ )
|
|
|
+
|
|
|
+ Spacer(Modifier.height(4.dp))
|
|
|
+ Text(
|
|
|
+ "物料编码:${item.goods_code}",
|
|
|
+ style = MaterialTheme.typography.bodyMedium
|
|
|
+ )
|
|
|
+
|
|
|
+ Text(
|
|
|
+ "物料描述:${item.goods_desc}",
|
|
|
+ style = MaterialTheme.typography.bodyMedium
|
|
|
+ )
|
|
|
+
|
|
|
+ Spacer(Modifier.height(8.dp))
|
|
|
+
|
|
|
+ Row(
|
|
|
+ horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
+ modifier = Modifier.fillMaxWidth()
|
|
|
+ ) {
|
|
|
+ Text("当前数量:${item.current_qty}")
|
|
|
+ Text("✖:${item.group_qty}组数")
|
|
|
+ }
|
|
|
+
|
|
|
+ Spacer(Modifier.height(4.dp))
|
|
|
+
|
|
|
+ Text(
|
|
|
+ "创建时间:${item.create_time}",
|
|
|
+ style = MaterialTheme.typography.bodySmall,
|
|
|
+ color = Color.Gray
|
|
|
+ )
|
|
|
+ }
|
|
|
+ Divider(thickness = 0.8.dp)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|