diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseDialogFrame.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseDialogFrame.kt index b063d2d..e33a9c7 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/base/BaseDialogFrame.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseDialogFrame.kt @@ -21,6 +21,10 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -34,6 +38,8 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.bbitcn.f8.pad.M import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.ui.screen.view.GlobalDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty import com.blankj.utilcode.util.ActivityUtils @Preview(showBackground = true, widthDp = 1280, heightDp = 800) @@ -68,7 +74,44 @@ fun MyDialog( onClickOK: () -> Unit = {}, content: @Composable () -> Unit, ) { - MyAnimatedVisibility(showDialog) { + val ownerKey = remember { Any() } + val latestOnDismissRequest = rememberUpdatedState(onDismissRequest) + val latestOnClickOK = rememberUpdatedState(onClickOK) + val latestContent = rememberUpdatedState(content) + + LaunchedEffect(showDialog, title, clickOKStr) { + if (showDialog) { + Toasty.showGlobalDialog( + GlobalDialogData( + ownerKey = ownerKey, + title = title, + onDismissRequest = { + latestOnDismissRequest.value() + }, + clickOKStr = clickOKStr, + onClickOK = { + latestOnClickOK.value() + }, + content = { + latestContent.value() + } + ) + ) + } else { + Toasty.hideGlobalDialog(ownerKey) + } + } + + DisposableEffect(ownerKey) { + onDispose { + Toasty.hideGlobalDialog(ownerKey) + } + } +} + +@Composable +fun GlobalDialogHost(data: GlobalDialogData) { + MyAnimatedVisibility(data.showDialog) { Box( modifier = M .fillMaxSize() @@ -78,8 +121,8 @@ fun MyDialog( }, contentAlignment = Alignment.Center ) { - BaseDialogFrame(title, { - content() + BaseDialogFrame(data.title, { + data.content() }) { Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { BigButton( @@ -87,13 +130,13 @@ fun MyDialog( .weight(1f) .widthIn(max = 100.dp) ) { - onDismissRequest() + data.onDismissRequest() } // 判断是否有点击事件 - if (clickOKStr != "") { + if (data.clickOKStr != "") { Spacer(modifier = M.width(30.dp)) - BigButton(clickOKStr, modifier = M.weight(1f), true) { - onClickOK() + BigButton(data.clickOKStr, modifier = M.weight(1f), true) { + data.onClickOK() } } } @@ -110,7 +153,7 @@ fun BaseDialogFrame( ) { MyCard( modifier = M - .padding(top = 10.dp, bottom = 50.dp, start = 100.dp, end = 100.dp) + .padding(vertical = 40.dp, horizontal = 100.dp) .noVisualFeedbackClickable { } .fillMaxSize() ) { @@ -161,4 +204,4 @@ fun BaseDialogFrame( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseViewModel.kt index 2a2346e..c44daf2 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/base/BaseViewModel.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseViewModel.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import java.net.SocketException import kotlin.coroutines.cancellation.CancellationException +import retrofit2.HttpException open class BaseViewModel : ViewModel() { @@ -78,6 +79,11 @@ open class BaseViewModel : ViewModel() { MyLog.test("协程被取消:${exception.javaClass.simpleName},message=${exception.message}") return@onFailure } + if (exception is HttpException && exception.code() == 401) { + Toasty.loginExpired() + Toasty.error("登录已过期") + return@onFailure + } // 其他异常继续处理 exception.printStackTrace() onError(exception) @@ -208,6 +214,7 @@ open class BaseViewModel : ViewModel() { override fun onCleared() { super.onCleared() + pollingTask.stopAllTasks() taskMap.values.forEach { it.cancel() } taskMap.clear() } diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/CustomMaterialComponents.kt b/app/src/main/java/com/bbitcn/f8/pad/base/CustomMaterialComponents.kt index 7faf1c0..e47fd2a 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/base/CustomMaterialComponents.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/base/CustomMaterialComponents.kt @@ -172,13 +172,23 @@ fun MyButton( onClick: () -> Unit, ) { Button( - onClick = onClick, + onClick = { + if (enabled) { + onClick() + } else { + Toasty.showToast("正在开发中,敬请期待") + } + }, modifier = modifier, - enabled = enabled, + enabled = true, interactionSource = interactionSource, shape = shape, border = border, - colors = ButtonDefaults.buttonColors(containerColor = colors), + colors = ButtonDefaults.buttonColors( + containerColor = if (enabled) colors else MyColors.Disabled, + disabledContainerColor = MyColors.Disabled, + disabledContentColor = MyColors.Gray + ), contentPadding = contentPadding, ) { Text( diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivity.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivity.kt index 5834958..79f28a9 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivity.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivity.kt @@ -42,6 +42,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.GlobalDialogHost import com.bbitcn.f8.pad.ui.screen.TopInfoViewModel import com.bbitcn.f8.pad.ui.screen.LoginScreen import com.bbitcn.f8.pad.ui.screen.MainScreen @@ -273,6 +274,7 @@ class MainActivity : ComponentActivity() { val loadingDialog by Toasty.loadingDialog.collectAsState() val confirmDialog by Toasty.confirmDialog.collectAsState() val inputDialog by Toasty.inputDialog.collectAsState() + val globalDialog by Toasty.globalDialog.collectAsState() TipsDialog( showDialog = tipsDialog.showDialog, onDismiss = { Toasty.hideTipsDialog() }, @@ -281,6 +283,7 @@ class MainActivity : ComponentActivity() { LoadingDialog(loadingDialog) ConfirmDialog(confirmDialog) InputDialog(inputDialog) + GlobalDialogHost(globalDialog) } } } @@ -313,4 +316,4 @@ class MainActivity : ComponentActivity() { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopBar.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopBar.kt index 8b541d2..3f628ae 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopBar.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopBar.kt @@ -51,6 +51,7 @@ import com.bbitcn.f8.pad.base.isBluetoothEnabled import com.bbitcn.f8.pad.receiver.SystemInfoReceiver import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleBT import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB @@ -88,6 +89,7 @@ fun MyTopBar( val date by topInfoViewModel.date.collectAsState() val time by topInfoViewModel.time.collectAsState() val logoState by topInfoViewModel.logoState.collectAsState() + val versionName = remember { MyUtil.getVersionName() } DisposableEffect(context) { systemInfoReceiver.register() @@ -151,13 +153,21 @@ fun MyTopBar( ) }, label = "animated content" ) { value -> - Text( - text = value.first.second, - M.padding(horizontal = 8.dp), - color = MyColors.White, - fontWeight = FontWeight.Bold, - fontSize = MaterialTheme.typography.headlineMedium.fontSize - ) + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = value.first.second, + M.padding(horizontal = 8.dp), + color = MyColors.White, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize + ) + Text( + text = "v$versionName", + M.padding(bottom = 2.dp), + color = MyColors.LightGray, + fontSize = MaterialTheme.typography.bodySmall.fontSize + ) + } } Spacer(modifier = M.weight(1f)) diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/MessageDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/MessageDialog.kt index d7fcb62..1f0e6fb 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/MessageDialog.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/MessageDialog.kt @@ -1,5 +1,7 @@ package com.bbitcn.f8.pad.ui.screen.dialog +import android.webkit.WebView +import android.webkit.WebViewClient import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -28,6 +30,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.bbitcn.f8.pad.M @@ -46,6 +49,8 @@ data class MessageDialogData( val time: String = "", val username: String = "", val content: String = "", + val title: String = "", + val isWebUrl: Boolean = false, ) @Preview(showBackground = true, widthDp = 1280, heightDp = 800) @@ -63,7 +68,7 @@ fun MessagePreview() { @Composable fun MessageDialog(info: MessageDialogData) { - MyDialog("查看消息", + MyDialog(info.title.ifEmpty { "查看消息" }, info.showDialog, onDismissRequest = { info.onDismiss() } ) { @@ -98,11 +103,29 @@ fun MessageDialog(info: MessageDialogData) { .padding(30.dp) .verticalScroll(rememberScrollState()) // 添加滚动支持 ) { - Text( - text = info.content, - fontSize = MaterialTheme.typography.headlineMedium.fontSize, - fontWeight = FontWeight.Normal - ) + if (info.isWebUrl) { + AndroidView( + modifier = M.fillMaxSize(), + factory = { context -> + WebView(context).apply { + webViewClient = WebViewClient() + settings.javaScriptEnabled = true + loadUrl(info.content) + } + }, + update = { webView -> + if (webView.url != info.content) { + webView.loadUrl(info.content) + } + } + ) + } else { + Text( + text = info.content, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + fontWeight = FontWeight.Normal + ) + } } } } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialog.kt index be4f003..b333256 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialog.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialog.kt @@ -48,6 +48,7 @@ import com.bbitcn.f8.pad.base.VipBadge import com.bbitcn.f8.pad.model.net.request.CocoonTypeTranslateRequest import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors import com.bbitcn.f8.pad.utils.MyUtil import kotlin.collections.map @@ -157,13 +158,12 @@ fun TicketMoreDialog( } } item { - MyImageButton("茧别转换", R.drawable.ic_ticket_convert) { + MyImageButton("茧别转换", R.drawable.ic_ticket_convert, enabled = false) { isTranslated = !isTranslated } } item { - MyImageButton("茧票过户", R.drawable.ic_ticket_transfer, true) { - + MyImageButton("茧票过户", R.drawable.ic_ticket_transfer, vip = true, enabled = false) { } } item { @@ -176,13 +176,11 @@ fun TicketMoreDialog( } } item { - MyImageButton("上传图片", R.drawable.ic_upload_pic, true) { - + MyImageButton("上传图片", R.drawable.ic_upload_pic, vip = true, enabled = false) { } } item { - MyImageButton("隔日作废", R.drawable.ic_ticket_delete_next_day, true) { - + MyImageButton("隔日作废", R.drawable.ic_ticket_delete_next_day, vip = true, enabled = false) { } } } @@ -238,12 +236,18 @@ fun CocoonTypeTranslate( } @Composable -fun MyImageButton(text: String, resId: Int, vip: Boolean = false, onClick: () -> Unit) { +fun MyImageButton(text: String, resId: Int, vip: Boolean = false, enabled: Boolean = true, onClick: () -> Unit) { Column( modifier = M .fillMaxWidth() .padding(10.dp) - .clickable { onClick() }, + .clickable { + if (enabled) { + onClick() + } else { + Toasty.showToast("正在开发中,敬请期待") + } + }, horizontalAlignment = Alignment.CenterHorizontally ) { if (vip) { @@ -251,18 +255,21 @@ fun MyImageButton(text: String, resId: Int, vip: Boolean = false, onClick: () -> Image( painter = painterResource(id = resId), contentDescription = text, - modifier = M.size(90.dp) + modifier = M.size(90.dp), + alpha = if (enabled) 1f else 0.35f ) } } else { Image( painter = painterResource(id = resId), contentDescription = text, - modifier = M.size(90.dp) + modifier = M.size(90.dp), + alpha = if (enabled) 1f else 0.35f ) } Text( text = text, + color = if (enabled) MyColors.Black else MyColors.Gray, fontSize = MaterialTheme.typography.headlineMedium.fontSize, textAlign = TextAlign.Center ) diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsScreen.kt index e362de8..5ab9ce7 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsScreen.kt @@ -153,15 +153,15 @@ fun FundsScreen( ) } Spacer(modifier = M.weight(1f)) - MyButton(modifier = M.fillMaxWidth(), text = "查询配额") { + MyButton(modifier = M.fillMaxWidth(), text = "查询配额", enabled = false) { fundsViewModel.showQueryBalanceDialog() } VipBadge { - MyButton(modifier = M.fillMaxWidth(), text = "查询余额") { + MyButton(modifier = M.fillMaxWidth(), text = "查询余额", enabled = false) { fundsViewModel.showQueryBalanceDialog() } } - MyButton(modifier = M.fillMaxWidth(), text = "修改支付密码") { + MyButton(modifier = M.fillMaxWidth(), text = "修改支付密码", enabled = false) { Toasty.showToast("正在开发中,敬请期待!") } } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeScreen.kt index 2d1f705..b401a8d 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeScreen.kt @@ -413,6 +413,7 @@ fun WeatherItem( @Composable fun MessageInfo(homeViewModel: HomeViewModel) { + val notices by homeViewModel.notices.collectAsState() Box(modifier = M.padding(5.dp)) { Image( painter = painterResource(id = R.drawable.bg_weather), @@ -429,14 +430,16 @@ fun MessageInfo(homeViewModel: HomeViewModel) { }) } LazyColumn { - items(count = 0) { index -> + items(count = notices.size) { index -> + val notice = notices[index] MessageItem( homeViewModel, true, - true, - "管理员", - "2024-08-02 11:25:32", - "正在开发中" + !homeViewModel.isNoticeRead(notice.id), + notice.sender, + notice.time, + notice.title, + onClick = { homeViewModel.showNoticeDialog(notice) } ) } } @@ -451,12 +454,13 @@ fun MessageItem( unRead: Boolean, name: String, time: String, - message: String + message: String, + onClick: (() -> Unit)? = null ) { Row( modifier = M .clickable { - homeViewModel.showMsgDialog(name, time, message) + onClick?.invoke() ?: homeViewModel.showMsgDialog(name, time, message) } .padding(bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { @@ -640,4 +644,4 @@ fun AcquireDynamic(homeViewModel: HomeViewModel) { verticalPadding = 7.5.dp ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeViewModel.kt index a1ca3b3..c52186c 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeViewModel.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeViewModel.kt @@ -22,6 +22,14 @@ import java.util.Date class HomeViewModel : BaseViewModel() { + data class Notice( + val id: String, + val title: String, + val sender: String, + val time: String, + val content: String + ) + private val _todayPrice = MutableStateFlow>(emptyList()) val todayPrice = _todayPrice.asStateFlow() @@ -37,7 +45,11 @@ class HomeViewModel : BaseViewModel() { private val _messageDialogData = MutableStateFlow(MessageDialogData()) val messageDialogData: StateFlow = _messageDialogData.asStateFlow() + private val _notices = MutableStateFlow>(emptyList()) + val notices = _notices.asStateFlow() + init { + loadMockNotices() refreshWeatherInfo(false) refreshTodayPrice(false) doInIoThreadNoDialog { @@ -58,6 +70,47 @@ class HomeViewModel : BaseViewModel() { } } + private fun loadMockNotices() { + val readIds = getReadNoticeIds() + _notices.value = listOf( + Notice( + id = "notice_001", + title = "收购系统版本提示", + sender = "系统通知", + time = "2026-05-27 09:30:00", + content = "本次前端已调整农户建档、通知展示和称重信息展示,后端接口将陆续配合。" + ), + Notice( + id = "notice_002", + title = "操作指引", + sender = "管理员", + time = "2026-05-27 10:15:00", + content = "https://www.bbitcn.com" + ), + Notice( + id = "notice_003", + title = "设备巡检提醒", + sender = "运维", + time = "2026-05-27 11:00:00", + content = "请在每日开秤前检查打印机、电子秤、读卡器连接状态。" + ) + ).map { it.copy(title = if (readIds.contains(it.id)) it.title else it.title) } + } + + fun isNoticeRead(id: String): Boolean = getReadNoticeIds().contains(id) + + private fun getReadNoticeIds(): Set { + return MMKVUtil.get("HOME_NOTICE_READ_IDS", "") + .split(",") + .filter { it.isNotBlank() } + .toSet() + } + + private fun markNoticeRead(id: String) { + val ids = getReadNoticeIds() + id + MMKVUtil.put("HOME_NOTICE_READ_IDS", ids.joinToString(",")) + } + fun refreshAcquireData( startDate: Date = _dateRange2.value.first, endDate: Date = _dateRange2.value.second, @@ -106,6 +159,22 @@ class HomeViewModel : BaseViewModel() { ) } + fun showNoticeDialog(notice: Notice) { + markNoticeRead(notice.id) + _notices.update { it.toList() } + _messageDialogData.value = MessageDialogData( + showDialog = true, + username = notice.sender, + time = notice.time, + content = notice.content, + title = notice.title, + isWebUrl = notice.content.startsWith("http://") || notice.content.startsWith("https://"), + onDismiss = { + _messageDialogData.update { it.copy(showDialog = false) } + } + ) + } + fun logout(onFinished: () -> Unit) { // 退出登录 doInIoThreadThenUI(loadingTips = "正在退出登录", onIO = { diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseScreen.kt index 81c270e..b4b7cea 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseScreen.kt @@ -118,6 +118,7 @@ fun PurchaseScreen( var queryInput by rememberSaveable { mutableStateOf("") } // 筛选条件3:查询类型 var queryType by rememberSaveable { mutableStateOf(0) } + var queryInputInitialized by rememberSaveable { mutableStateOf(false) } val info = purchaseViewModel.infoPager.collectAsLazyPagingItems() // 查询方法 @@ -131,6 +132,10 @@ fun PurchaseScreen( info.refresh() } LaunchedEffect(queryInput) { + if (!queryInputInitialized) { + queryInputInitialized = true + return@LaunchedEffect + } delay(350) updateParams() } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseViewModel.kt index 55c8c26..e62091c 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseViewModel.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseViewModel.kt @@ -65,7 +65,10 @@ class PurchaseViewModel : BaseViewModel() { init { doInIoThreadNoDialog { // 获取一个月前的日期 - updateParams(getRecentMonthsDate(1), Date(), "", 0) + val start = getRecentMonthsDate(1) + val end = Date() + _dateRange.value = start to end + updateParams(start, end, "", 0) } } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsScreen.kt index a87996d..c0f4e6e 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsScreen.kt @@ -167,7 +167,7 @@ fun StatisticsLeft( } Spacer(modifier = M.weight(1f)) VipBadge { - MyButton(modifier = M.fillMaxWidth(), text = "发送短信") { + MyButton(modifier = M.fillMaxWidth(), text = "发送短信", enabled = false) { Toasty.showToast("正在开发中,敬请期待") } } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserScreen.kt index 1028991..9a12dd2 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserScreen.kt @@ -1,77 +1,50 @@ package com.bbitcn.f8.pad.ui.screen.mainFunc -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.background 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 -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ExpandLess -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.bbitcn.f8.pad.M -import com.bbitcn.f8.pad.base.MainFuncFrame import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.M import com.bbitcn.f8.pad.base.AssistChipFilter import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MainFuncFrame import com.bbitcn.f8.pad.base.MyButton import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog import com.bbitcn.f8.pad.base.MyInfoCard import com.bbitcn.f8.pad.base.MyRefreshTable import com.bbitcn.f8.pad.base.MyTableData import com.bbitcn.f8.pad.base.QueryTextField -import com.bbitcn.f8.pad.base.isLandscape import com.bbitcn.f8.pad.model.net.response.UserDataResponse import com.bbitcn.f8.pad.ui.theme.MyColors -import com.bbitcn.f8.pad.utils.MyUtil import com.bbitcn.f8.pad.utils.TimeUtils -import com.blankj.utilcode.util.StringUtils -import kotlinx.coroutines.launch -/** - * - * @Description 主功能-预约售茧 - * @Author DuanKaiji - * @CreateTime 2024年08月02日 11:25:32 - */ @Preview(showBackground = true, widthDp = 1280, heightDp = 800) @Composable fun UserScreenPV() { @@ -83,339 +56,14 @@ fun UserScreen( navController: NavController, userViewModel: UserViewModel = viewModel() ) { - val treeData by userViewModel.treeData.collectAsState() val userData = userViewModel.usersInfoPager.collectAsLazyPagingItems() MainFuncFrame { - if (isLandscape()) { - UserScreenInLandscape(navController, userViewModel, treeData, userData) - } else { - UserScreenInPortrait(navController, userViewModel, treeData, userData) - } - } -} - -@Composable -fun UserScreenInPortrait( - navController: NavController, - userViewModel: UserViewModel, - treeData: List, Any>>, - userData: LazyPagingItems -) { - Column( - modifier = M.fillMaxSize() - ) { - MyInfoCard( - modifier = M - .weight(2f) - .padding(bottom = 15.dp) - ) { - CollapsibleList(userViewModel, treeData, userData) - } - MyInfoCard( - modifier = M.weight(8f) - ) { + MyInfoCard(modifier = M.fillMaxSize()) { UserManageList(navController, userViewModel, userData) } } } -@Composable -fun UserScreenInLandscape( - navController: NavController, - userViewModel: UserViewModel, - treeData: List, Any>>, - userData: LazyPagingItems -) { - var leftWeight by rememberSaveable { mutableStateOf(2.5f) } - var rightWeight by rememberSaveable { mutableStateOf(7.5f) } - Row( - modifier = M.fillMaxSize() - ) { - MyInfoCard( - modifier = M - .weight(leftWeight) // 使用动态权重 - .fillMaxHeight() - ) { - CollapsibleList(userViewModel, treeData, userData) - } - Box( - modifier = M - .fillMaxHeight() - .pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() // 消费掉手势事件 - val dragDelta = dragAmount.x // 获取横向拖动的距离 - leftWeight = (leftWeight + dragDelta * 0.01f).coerceIn(2.5f, 4.5f) - rightWeight = 10f - leftWeight -// rightWeight = (rightWeight - dragDelta * 0.01f).coerceIn(5f, 8f) - } - }, - contentAlignment = Alignment.Center - ) { - MyInfoCard( - modifier = M - .width(15.dp) - .height(50.dp) - .padding(horizontal = 5.dp) - ) { - } - } - MyInfoCard( - modifier = M - .weight(rightWeight) // 使用动态权重 - .fillMaxHeight() - ) { - UserManageList(navController, userViewModel, userData) - } - } -} - -@Composable -fun CollapsibleList( - userViewModel: UserViewModel, - listData: List, Any>>, - pager: LazyPagingItems -) { - val queryInput by userViewModel.areaLike.collectAsState() - Column { - Row( - modifier = M - .fillMaxWidth() - .padding(15.dp), - verticalAlignment = Alignment.CenterVertically - ) { - QueryTextField(M.weight(1f), queryInput) { - userViewModel.updateAreaLike(it) - userViewModel.getUsersArea(false) - } - Image( - imageVector = Icons.Default.Refresh, - contentDescription = "Refresh", - modifier = M.clickable { - userViewModel.updateParams() - userViewModel.getUsersArea(showLoading = true) - pager.refresh() - } - ) - } - LazyColumn { - items(listData) { item -> - CollapsibleItem( - userViewModel, - pager, - item = item, - currentLevel = 1, - needExpand = queryInput != "" - ) - } - } - } -} - -@Composable -fun CollapsibleItem( - userViewModel: UserViewModel, - pager: LazyPagingItems, - item: Pair, Any>, - currentLevel: Int, - xian: String = "", - xiang: String = "", - cun: String = "", - needExpand: Boolean = false -) { - val titleAndCount = item.first - if (StringUtils.isEmpty(titleAndCount.first)) { - return - } - val subItems = item.second - // 当前项的展开状态 0表示展开,-1表示折叠 1表示加载中 - var expandedIndex by rememberSaveable { mutableStateOf(if (needExpand) 0 else -1) } - LaunchedEffect(needExpand) { - if (needExpand != (expandedIndex == 0)) { - expandedIndex = if (needExpand) 0 else -1 - } - } - // 被选中的项 - val xianTmp = if (currentLevel == 1) titleAndCount.first else xian - val xiangTmp = if (currentLevel == 2) titleAndCount.first else xiang - val cunTmp = if (currentLevel == 3) titleAndCount.first else cun - - val expendListener = { - if (expandedIndex == -1) { - // 加载子项数据 - expandedIndex = 1 - userViewModel.loadArea( - currentLevel, - xian = xianTmp, - xiang = xiangTmp, - cun = cunTmp, - ) { - expandedIndex = 0 - } - } else { - expandedIndex = -1 - } - } - - Column { - val titleIsSelect = - userViewModel.areaFilter.collectAsState().value.contains(item.first.first) - // 顶层项的显示和点击逻辑 - MyCard(radius = 20.dp, elevation = 0.dp, modifier = M.padding(1.5.dp)) { - Row( - modifier = M - .fillMaxWidth() - .background(if (titleIsSelect) MyColors.BlueGreen else MyColors.White) - .pointerInput(Unit) { - detectTapGestures( - onDoubleTap = { - expendListener() - }, - onTap = { - userViewModel.updateParams(xianTmp, xiangTmp, cunTmp) - pager.refresh() - } - ) - } - .padding(horizontal = 15.dp, vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - modifier = M.weight(1f), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = titleAndCount.first, - color = if (titleIsSelect) MyColors.White else MyColors.Black, - fontSize = if (currentLevel == 1) MaterialTheme.typography.titleLarge.fontSize - else if (currentLevel == 2) MaterialTheme.typography.bodyLarge.fontSize - else MaterialTheme.typography.bodyMedium.fontSize, - ) - if (titleAndCount.second != -1) { - Text( - modifier = M.padding(end = 5.dp), - text = "${titleAndCount.second}", - color = if (titleIsSelect) MyColors.LightGray else MyColors.Gray, - fontSize = MaterialTheme.typography.bodyMedium.fontSize, - style = MaterialTheme.typography.bodyMedium - ) - } - - } - if (expandedIndex == 1) { - CircularProgressIndicator( - strokeWidth = 3.dp, - modifier = M.size(20.dp), - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } else { - Icon( - modifier = M - .clickable { - expendListener() - }, - tint = if (titleIsSelect) MyColors.White else MyColors.Black, - imageVector = if (expandedIndex == 0) - Icons.Default.ExpandLess - else - Icons.Default.ExpandMore, - contentDescription = if (expandedIndex != -1) "Collapse" else "Expand" - ) - } - } - } - - AnimatedVisibility(visible = expandedIndex != -1) { - Column(modifier = M.padding(start = 10.dp)) { - when (subItems) { - // 如果子项是 Map 类型,表示还有嵌套项,需要递归渲染 - is Map<*, *> -> { - subItems.forEach { subItem -> - if (subItem is Map.Entry<*, *>) { - val subPair = subItem.key to subItem.value - CollapsibleItem( - userViewModel, - pager, - subPair as Pair, Any>, - currentLevel + 1, - xianTmp, - xiangTmp, - cunTmp, - needExpand - ) - } - } - } - // 如果子项是 List 类型,表示这是最底层的组数据 - is List<*> -> { - subItems.forEach { subItem -> - if (subItem is Pair<*, *>) { - if (StringUtils.isEmpty(subItem.first.toString())) { - return@forEach - } - val isSelect = titleIsSelect - && userViewModel.areaFilter.collectAsState().value.contains( - subItem.first.toString() - ) - // 最底层的组 - MyCard( - radius = 20.dp, - elevation = 0.dp, - modifier = M.padding(2.5.dp) - ) { - Row( - modifier = M - .fillMaxWidth() - .background( - if (isSelect) MyColors.BlueGreen else MyColors.White - ) - .clickable { - userViewModel.updateParams( - xianTmp, - xiangTmp, - cunTmp, - subItem.first.toString() - ) - pager.refresh() - } - .padding(10.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = subItem.first.toString(), - color = if (isSelect) MyColors.White else MyColors.Black, - fontSize = MaterialTheme.typography.bodyMedium.fontSize, - style = MaterialTheme.typography.bodyMedium - ) - if (subItem.second != -1) { - Text( - modifier = M.padding(end = 33.dp), - text = "${subItem.second}", - color = if (isSelect) MyColors.White else MyColors.Gray, - fontSize = MaterialTheme.typography.bodyMedium.fontSize, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } - } - // 未来处理更多层级时,可以继续添加逻辑 -// else if (subItem is Pair<*, *>) { -// // 递归处理更深层的结构 -// (subItem as? Pair)?.let { subItemPair -> -// CollapsibleItem(userViewModel,subItemPair, -// currentLevel + 1) -// } -// } - } - } - } - } - } - } -} - @Composable fun UserManageList( navController: NavController, @@ -424,6 +72,7 @@ fun UserManageList( ) { var queryInput by rememberSaveable { mutableStateOf("") } val areaFilter by userViewModel.areaFilter.collectAsState() + var detailUser by remember { mutableStateOf(null) } val onFilterLikeChanged: (String) -> Unit = { queryInput = it userViewModel.updateParamsLike(it) @@ -434,25 +83,14 @@ fun UserManageList( .padding(15.dp) .fillMaxSize() ) { - Row( - modifier = M - .fillMaxWidth() - .padding(bottom = 5.dp), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - MyButton(text = "新增", onClick = { - navController.navigate("addUser") - }) - AssistChipFilter("区域:", areaFilter, onClick = { - userViewModel.updateParams() - userData.refresh() - }) - AssistChipFilter("筛选:", queryInput, onFilterLikeChanged) - Spacer(modifier = M.weight(1f)) - QueryTextField(M.width(200.dp), queryInput, onValueChange = onFilterLikeChanged) -// MyButton(text = "更多", modifier = M.padding(horizontal = 10.dp), onClick = {}) - } + UserFilterBar( + userViewModel = userViewModel, + userData = userData, + queryInput = queryInput, + onQueryChanged = onFilterLikeChanged, + areaFilter = areaFilter, + navController = navController + ) val myPager = userViewModel.usersInfoMyPager val isRefreshing by myPager.listIsRefreshing.collectAsState() MyRefreshTable( @@ -467,8 +105,8 @@ fun UserManageList( MyTableData(1, isIndex = true), MyTableData("姓名", 1, { it.nhname }), MyTableData("手机号", 2, { it.phone }), - MyTableData("身份证", 1, { if (it.idcard != "") "✔️" else "" }), - MyTableData("银行卡", 1, { if (it.bankcode != "") "✔️" else "" }), + MyTableData("身份证", 1, { if (it.idcard != "") "已录" else "" }), + MyTableData("银行卡", 1, { if (it.bankcode != "") "已录" else "" }), MyTableData("所属地址", 3, { "${it.xian}${it.xiang}${it.cun}${it.zu}" }), MyTableData("建档时间", 2, { TimeUtils.formatDateTimeStrToDateStr(it.createtime) }), MyTableData("", 1, { "修改" }, true) { @@ -478,27 +116,8 @@ fun UserManageList( navController.navigate("weight/${it.sysid}") }, ), - onExpend = { - Column(modifier = M.fillMaxWidth().border(1.dp, MyColors.Gray).padding(15.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - InfoText("姓名", it.nhname,M.weight(2f),true) - InfoText("电话", it.phone,M.weight(2f),true) - } - Row(verticalAlignment = Alignment.CenterVertically) { - InfoText("银行", it.bankname,M.weight(2f),true) - InfoText("卡号", it.bankcode,M.weight(2f),true) - } - Row(verticalAlignment = Alignment.CenterVertically) { - InfoText("建档时间", it.createtime,M.weight(2f),true) - InfoText("身份证", it.idcard,M.weight(2f),true) - } - Row(verticalAlignment = Alignment.CenterVertically) { - InfoText("县", it.xian,M.weight(1f),true) - InfoText("乡", it.xiang,M.weight(1f),true) - InfoText("村", it.cun,M.weight(1f),true) - InfoText("组", it.zu,M.weight(1f),true) - } - } + onClick = { + detailUser = it }, onLongClick = { userViewModel.deleteUser(it.nhname, it.sysid) { @@ -507,4 +126,130 @@ fun UserManageList( } ) } + UserDetailDialog(detailUser) { + detailUser = null + } +} + +@Composable +private fun UserDetailDialog( + user: UserDataResponse.Data?, + onDismiss: () -> Unit +) { + MyDialog( + title = "农户详情", + showDialog = user != null, + onDismissRequest = onDismiss + ) { + if (user == null) { + return@MyDialog + } + Column( + modifier = M + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 8.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + DetailSection("基本信息") { + DetailRow("姓名", user.nhname, "手机号", user.phone) + DetailRow("身份证号", user.idcard, "建档时间", user.createtime) + } + DetailSection("银行卡") { + DetailRow("银行卡号", user.bankcode, "银行名称", user.bankname) + DetailRow("银行简称", user.bankshortname, "联行号", user.recbankcode) + } + DetailSection("地址") { + DetailRow("县", user.xian, "乡", user.xiang) + DetailRow("村", user.cun, "组", user.zu) + DetailSingleRow("完整地址", "${user.xian}${user.xiang}${user.cun}${user.zu}") + } + DetailSection("系统信息") { + DetailRow("系统ID", user.sysid, "状态标记", user.flag.toString()) + } + } + } +} + +@Composable +private fun DetailSection(title: String, content: @Composable () -> Unit) { + MyCard(elevation = 0.5.dp, radius = 6.dp, modifier = M.fillMaxWidth()) { + Column( + modifier = M + .fillMaxWidth() + .background(MyColors.LightGray) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = title, + color = MyColors.BlueGreen, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.titleMedium.fontSize + ) + content() + } + } +} + +@Composable +private fun DetailRow(label1: String, value1: String, label2: String, value2: String) { + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { + DetailField(label1, value1, M.weight(1f)) + DetailField(label2, value2, M.weight(1f)) + } +} + +@Composable +private fun DetailSingleRow(label: String, value: String) { + DetailField(label, value, M.fillMaxWidth()) +} + +@Composable +private fun DetailField(label: String, value: String, modifier: androidx.compose.ui.Modifier = M) { + Column(modifier = modifier) { + Text( + text = label, + color = MyColors.Gray, + fontSize = MaterialTheme.typography.bodySmall.fontSize + ) + Text( + text = value.ifBlank { "-" }, + color = MyColors.Black, + fontWeight = FontWeight.Medium, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + } +} + +@Composable +private fun UserFilterBar( + userViewModel: UserViewModel, + userData: LazyPagingItems, + queryInput: String, + onQueryChanged: (String) -> Unit, + areaFilter: String, + navController: NavController +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = M.padding(bottom = 8.dp)) { + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MyButton(text = "新增", onClick = { + navController.navigate("addUser") + }) + Spacer(modifier = M.weight(1f)) + QueryTextField(M.width(200.dp), queryInput, onValueChange = onQueryChanged) + } + Row(verticalAlignment = Alignment.CenterVertically) { + AssistChipFilter("区域:", areaFilter, onClick = { + userViewModel.clearAreaFilter() + userData.refresh() + }) + AssistChipFilter("筛选:", queryInput, onQueryChanged) + Spacer(modifier = M.weight(1f)) + } + } } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserViewModel.kt index f01c3b0..1f8e538 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserViewModel.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserViewModel.kt @@ -3,28 +3,51 @@ package com.bbitcn.f8.pad.ui.screen.mainFunc; import androidx.lifecycle.viewModelScope import com.bbitcn.f8.pad.base.BaseViewModel import com.bbitcn.f8.pad.model.net.request.UserListDataRequest -import com.bbitcn.f8.pad.model.net.response.UsersAreaResponse import com.bbitcn.f8.pad.ui.screen.view.Toasty import com.bbitcn.f8.pad.utils.pager.MyPager import com.bbitcn.f8.pad.utils.pager.UsersInfoPagingSource -import com.blankj.utilcode.util.StringUtils import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class UserViewModel : BaseViewModel() { - private val _areaLike = MutableStateFlow("") - val areaLike = _areaLike.asStateFlow() - - private val _treeData = MutableStateFlow, Any>>>(emptyList()) - val treeData: StateFlow, Any>>> = _treeData.asStateFlow() - - private val _tempData: MutableList = mutableListOf() - private val _areaFilter = MutableStateFlow("") val areaFilter = _areaFilter.asStateFlow() + data class AreaOption( + val xian: String, + val xiang: String, + val cun: String, + val zu: String + ) + + private val mockAreas = listOf( + AreaOption("江安县", "怡乐镇", "公平村", "一组"), + AreaOption("江安县", "怡乐镇", "公平村", "二组"), + AreaOption("江安县", "大井镇", "友好村", "三组"), + AreaOption("长宁县", "竹海镇", "联盟村", "一组"), + AreaOption("长宁县", "梅硐镇", "石陇村", "五组"), + AreaOption("兴文县", "古宋镇", "建设村", "二组") + ) + + private val _xianList = MutableStateFlow>(emptyList()) + val xianList = _xianList.asStateFlow() + private val _xiangList = MutableStateFlow>(emptyList()) + val xiangList = _xiangList.asStateFlow() + private val _cunList = MutableStateFlow>(emptyList()) + val cunList = _cunList.asStateFlow() + private val _zuList = MutableStateFlow>(emptyList()) + val zuList = _zuList.asStateFlow() + + private val _selectedXian = MutableStateFlow("") + val selectedXian = _selectedXian.asStateFlow() + private val _selectedXiang = MutableStateFlow("") + val selectedXiang = _selectedXiang.asStateFlow() + private val _selectedCun = MutableStateFlow("") + val selectedCun = _selectedCun.asStateFlow() + private val _selectedZu = MutableStateFlow("") + val selectedZu = _selectedZu.asStateFlow() + val usersInfoMyPager = MyPager( pagingSourceFactory = { UsersInfoPagingSource(it) }, initialRequestData = UserListDataRequest(), // 传入初始的请求数据 @@ -33,7 +56,7 @@ class UserViewModel : BaseViewModel() { val usersInfoPager = usersInfoMyPager.createPager(viewModelScope) init { - getUsersArea(false) + refreshAreaOptions() } fun updateParamsLike(like: String) { @@ -51,6 +74,67 @@ class UserViewModel : BaseViewModel() { _areaFilter.value = buildAreaFilter(xian, xiang, cun, zu) } + fun selectXian(value: String) { + _selectedXian.value = value + _selectedXiang.value = "" + _selectedCun.value = "" + _selectedZu.value = "" + refreshAreaOptions() + updateParams(value) + } + + fun selectXiang(value: String) { + _selectedXiang.value = value + _selectedCun.value = "" + _selectedZu.value = "" + refreshAreaOptions() + updateParams(_selectedXian.value, value) + } + + fun selectCun(value: String) { + _selectedCun.value = value + _selectedZu.value = "" + refreshAreaOptions() + updateParams(_selectedXian.value, _selectedXiang.value, value) + } + + fun selectZu(value: String) { + _selectedZu.value = value + updateParams(_selectedXian.value, _selectedXiang.value, _selectedCun.value, value) + } + + fun clearAreaFilter() { + _selectedXian.value = "" + _selectedXiang.value = "" + _selectedCun.value = "" + _selectedZu.value = "" + refreshAreaOptions() + updateParams() + } + + private fun refreshAreaOptions() { + val xian = _selectedXian.value + val xiang = _selectedXiang.value + val cun = _selectedCun.value + _xianList.value = mockAreas.map { it.xian }.distinct() + _xiangList.value = mockAreas + .filter { xian.isEmpty() || it.xian == xian } + .map { it.xiang } + .distinct() + _cunList.value = mockAreas + .filter { (xian.isEmpty() || it.xian == xian) && (xiang.isEmpty() || it.xiang == xiang) } + .map { it.cun } + .distinct() + _zuList.value = mockAreas + .filter { + (xian.isEmpty() || it.xian == xian) && + (xiang.isEmpty() || it.xiang == xiang) && + (cun.isEmpty() || it.cun == cun) + } + .map { it.zu } + .distinct() + } + fun buildAreaFilter(xian: String, xiang: String, cun: String, zu: String): String { return buildString { append(xian) @@ -60,154 +144,6 @@ class UserViewModel : BaseViewModel() { } } - fun updateAreaLike(like: String) { - _areaLike.value = like - } - - fun getUsersArea(showLoading: Boolean) { - doInIoThreadWith(showLoading, "正在加载区域数据...") { - _treeData.value = emptyList() - val like = _areaLike.value - if (StringUtils.isEmpty(like)) { - val response = apiService.getUsersAreaForXian() - if (response.code == 1) { - response.data.forEach { - _tempData.add(UsersAreaResponse.Data(xian = it.xian, count = it.count)) - } - _treeData.value = convertToNestedStructure(_tempData) - } - } else { - val response = apiService.getUsersArea(like) - if (response.code == 1) { - _treeData.value = convertToNestedStructure(response.data) - } else { - Toasty.showTipsDialog(response.msg) - } - } - } - } - - fun loadArea( - currentLevel: Int, - xian: String, - xiang: String, - cun: String, - onFinish: () -> Unit - ) { - // 只有在搜索信息为空的时候才逐级加载 - if (StringUtils.isEmpty(_areaLike.value)) { - doInIoThreadNoDialog { - if (currentLevel == 1) { - // 级别为1时,加载乡 - val list = apiService.getUsersAreaForXiang(xian = xian) - if (list.code == 1) { - list.data.forEach { - _tempData.add( - UsersAreaResponse.Data( - xian = xian, - xiang = it.xiang, - count = it.count - ) - ) - } - _treeData.value = convertToNestedStructure(_tempData) - } else { - Toasty.showTipsDialog(list.msg) - } - } else if (currentLevel == 2) { - // 级别为2时,加载村 - val list = apiService.getUsersAreaForCun(xian = xian, xiang = xiang) - if (list.code == 1) { - list.data.forEach { - _tempData.add( - UsersAreaResponse.Data( - xian = xian, - xiang = xiang, - cun = it.cun, - count = it.count - ) - ) - } - _treeData.value = convertToNestedStructure(_tempData) - } else { - Toasty.showTipsDialog(list.msg) - } - } else if (currentLevel == 3) { - // 级别为3时,加载组 - val list = apiService.getUsersAreaForZu(xian = xian, xiang = xiang, cun = cun) - if (list.code == 1) { - list.data.forEach { - _tempData.add( - UsersAreaResponse.Data( - xian = xian, - xiang = xiang, - cun = cun, - zu = it.zu, - count = it.count - ) - ) - } - _treeData.value = convertToNestedStructure(_tempData) - } else { - Toasty.showTipsDialog(list.msg) - } - } - onFinish() - } - } - } - - private suspend fun convertToNestedStructure(data: List): List, Any>> { - val result = mutableListOf, Any>>() -// val result = mutableListOf, Pair, Pair, Pair, String>>>>>() - // 按县(Xian)分组 - data.groupBy { it.xian }.forEach { (xian, xiangList) -> - // 按乡(Xiang)分组 - val xiangGroups = xiangList.groupBy { it.xiang }.map { (xiang, cunList) -> - // 按村(Cun)分组 - val cunGroups = cunList.groupBy { it.cun }.map { (cun, zuList) -> - // 按组(Zu)分组 - val zuListWithCount = zuList.map { it.zu to it.count } - Pair(cun to getCount(data, xian, xiang, cun), zuListWithCount) - }.toMap() - // 返回乡及其村的分组 - Pair(xiang to getCount(data, xian, xiang), cunGroups) - }.toMap() - result.add(Pair(xian to getCount(data, xian), xiangGroups)) - } - return result - } - - fun getCount( - list: List, - xian: String, xiang: String? = null, cun: String? = null, zu: String? = null - ): Int { - for (item in list) { - // 找县的数量 - if (xiang == null) { - if (item.xian == xian && item.xiang == "" && item.cun == "" && item.zu == "") { - return item.count - } - } else if (cun == null) { - // 找乡的数量 - if (item.xian == xian && item.xiang == xiang && item.cun == "" && item.zu == "") { - return item.count - } - } else if (zu == null) { - // 找村的数量 - if (item.xian == xian && item.xiang == xiang && item.cun == cun && item.zu == "") { - return item.count - } - } else { - // 找组的数量 - if (item.xian == xian && item.xiang == xiang && item.cun == cun && item.zu == zu) { - return item.count - } - } - } - return -1 - } - fun deleteUser(name: String, sysid: String, onSuccess: () -> Unit) { Toasty.showConfirmDialog("确定删除农户" + name + "吗?") { doInIoThread("正在删除农户...") { diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserScreen.kt index db0bd7f..41c86b4 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserScreen.kt @@ -1,11 +1,15 @@ package com.bbitcn.f8.pad.ui.screen.secondFunc +import android.graphics.Bitmap +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -26,8 +30,10 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -74,29 +80,23 @@ fun AddUserScreen( addUserViewModel.initExtendData() } } - val scanDialogData by addUserViewModel.scanDialogData.collectAsState() val ocrDialogData by addUserViewModel.ocrDialogData.collectAsState() - val faceDialogData by addUserViewModel.faceDialogData.collectAsState() MainFuncFrame { MyCard(modifier = M.fillMaxSize(), radius = 7.dp, elevation = 1.dp) { VerticalTabPages( modifier = M .fillMaxHeight() .padding(vertical = 15.dp), - tabs = listOf("基本信息", "人脸录入", "附件上传", "拓展信息"), + tabs = listOf("基本信息", "拓展信息"), ) { when (it) { 0 -> AddUserBaseInfo(addUserViewModel) - 1 -> AddUserFaceInfo(addUserViewModel) - 2 -> AddUserAttachmentInfo(addUserViewModel) - 3 -> AddUSerExtendInfo(addUserViewModel) + 1 -> AddUSerExtendInfo(addUserViewModel) } } } } - ScanDialog(scanDialogData) OCRDialog(ocrDialogData) - FaceDialog(faceDialogData) } @Composable @@ -170,6 +170,12 @@ fun AddUserBaseInfo( addUserViewModel: AddUserViewModel ) { val configuration = LocalConfiguration.current + var selectedCard by rememberSaveable { mutableStateOf("id") } + var extraBankCount by rememberSaveable { mutableStateOf(0) } + var avatar by remember { mutableStateOf(null) } + val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { + avatar = it + } // 判断设备是否为竖屏 if (configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT) { // 竖屏模式 @@ -182,10 +188,18 @@ fun AddUserBaseInfo( ) { addUserViewModel.editFarmer() } - AddUserLeftColumn( - addUserViewModel = addUserViewModel + FarmerCardWallet( + selectedCard = selectedCard, + extraBankCount = extraBankCount, + avatar = avatar, + onSelect = { selectedCard = it }, + onAddBank = { + extraBankCount += 1 + selectedCard = "bank_extra_$extraBankCount" + }, + onAvatarClick = { cameraLauncher.launch(null) } ) - AddUserRightColumn(addUserViewModel = addUserViewModel) + FarmerCardEditor(selectedCard, addUserViewModel) } } } else { @@ -198,12 +212,103 @@ fun AddUserBaseInfo( ) { addUserViewModel.editFarmer() } - AddUserLeftColumn( + FarmerCardWallet( modifier = M.weight(1f), - addUserViewModel, + selectedCard = selectedCard, + extraBankCount = extraBankCount, + avatar = avatar, + onSelect = { selectedCard = it }, + onAddBank = { + extraBankCount += 1 + selectedCard = "bank_extra_$extraBankCount" + }, + onAvatarClick = { cameraLauncher.launch(null) } ) } - AddUserRightColumn(M.weight(1f), addUserViewModel) + FarmerCardEditor(selectedCard, addUserViewModel, M.weight(1.6f)) + } + } +} + +@Composable +fun FarmerCardWallet( + modifier: Modifier = M, + selectedCard: String, + extraBankCount: Int, + avatar: Bitmap?, + onSelect: (String) -> Unit, + onAddBank: () -> Unit, + onAvatarClick: () -> Unit, +) { + Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + MyCard( + radius = 8.dp, + elevation = 0.5.dp, + modifier = M.fillMaxWidth().clickable(onClick = onAvatarClick) + ) { + Row(M.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) { + if (avatar == null) { + Image( + painter = painterResource(id = R.drawable.icon_user), + contentDescription = null, + modifier = M.size(54.dp) + ) + } else { + Image( + bitmap = avatar.asImageBitmap(), + contentDescription = null, + modifier = M.size(54.dp) + ) + } + Column(M.padding(start = 12.dp)) { + Text("头像", fontWeight = FontWeight.Bold) + Text("点击拍照", fontSize = MaterialTheme.typography.bodySmall.fontSize) + } + } + } + WalletCard("id", "身份证", "基础身份信息", selectedCard == "id", onSelect) + WalletCard("bank_main", "银行卡", "主银行卡", selectedCard == "bank_main", onSelect) + repeat(extraBankCount) { index -> + val id = "bank_extra_${index + 1}" + WalletCard(id, "银行卡 ${index + 2}", "备用银行卡", selectedCard == id, onSelect) + } + MyButton(text = "新增银行卡", modifier = M.fillMaxWidth()) { + onAddBank() + } + } +} + +@Composable +private fun WalletCard( + id: String, + title: String, + subTitle: String, + selected: Boolean, + onSelect: (String) -> Unit +) { + MyCard( + radius = 8.dp, + elevation = if (selected) 1.5.dp else 0.3.dp, + modifier = M.fillMaxWidth().clickable { onSelect(id) } + ) { + Column(M.fillMaxWidth().padding(14.dp)) { + Text(title, fontWeight = FontWeight.Bold) + Text(subTitle, fontSize = MaterialTheme.typography.bodySmall.fontSize) + } + } +} + +@Composable +fun FarmerCardEditor( + selectedCard: String, + addUserViewModel: AddUserViewModel, + modifier: Modifier = M +) { + Column(modifier, verticalArrangement = Arrangement.spacedBy(5.dp)) { + when { + selectedCard == "id" -> AddUserIdentityEditor(addUserViewModel) + selectedCard == "bank_main" -> AddUserBankEditor(addUserViewModel, true, "主银行卡") + selectedCard.startsWith("bank_extra_") -> AddUserBankEditor(addUserViewModel, false, "备用银行卡") } } } @@ -299,6 +404,134 @@ fun AddUSerExtendInfo(addUserViewModel: AddUserViewModel) { } } +@Composable +fun AddUserIdentityEditor(addUserViewModel: AddUserViewModel) { + val userTypeList by addUserViewModel.userTypeList.collectAsState() + val userLabelList by addUserViewModel.userLabelList.collectAsState() + val xianList by addUserViewModel.xianList.collectAsState() + val xiangList by addUserViewModel.xiangList.collectAsState() + val cunList by addUserViewModel.cunList.collectAsState() + val zuList by addUserViewModel.zuList.collectAsState() + + InputFrame("基本信息") { + MyTextField(modifier = M.weight(1f), hint = "姓名", value = addUserViewModel.idName) { + addUserViewModel.idName = it + } + CombinedDropdownMenu(M.weight(1f), listOf("男", "女"), "性别", addUserViewModel.idGender) { + addUserViewModel.idGender = it + } + CombinedDropdownMenu(M.weight(1f), userTypeList.map { it.name }, "农户类别", addUserViewModel.userType) { + addUserViewModel.userType = it + } + } + InputFrame("农户标签") { + SelectableChipGroup(M.weight(1f), userLabelList) { + addUserViewModel.switchUserLabel(it) + } + } + InputFrame("手机号码") { + MyTextField( + modifier = M.weight(1f), + hint = "请输入", + value = addUserViewModel.userPhone, + isNumberInputType = true, + ) { + addUserViewModel.userPhone = it + } + } + InputFrame("身份证号") { + MyTextField( + modifier = M.weight(1f), + hint = "请输入", + value = addUserViewModel.idCardNumber, + isNumberInputType = true, + ) { + addUserViewModel.idCardNumber = it + } + VipBadge { + MyButton(text = "拍照识别") { + addUserViewModel.recognizeIdCard({ name, gender, idCard, address -> + addUserViewModel.idName = name + addUserViewModel.idGender = gender + addUserViewModel.idCardNumber = idCard + addUserViewModel.idCardAddress = address + }) { xian, xiang, cun, zu -> + addUserViewModel.userXian = xian + addUserViewModel.userXiang = xiang + addUserViewModel.userCun = cun + addUserViewModel.userZu = zu + } + } + } + } + InputFrame("身份证地址") { + MyTextField( + modifier = M.weight(1f), + hint = "请输入", + readOnly = true, + value = addUserViewModel.idCardAddress + ) + } + InputFrame("地址") { + CombinedDropdownMenu(M.weight(1f), xianList, "县", addUserViewModel.userXian, true) { + addUserViewModel.userXian = it + addUserViewModel.onAddressSelected() + } + CombinedDropdownMenu(M.weight(1f), xiangList, "乡", addUserViewModel.userXiang, true) { + addUserViewModel.userXiang = it + addUserViewModel.onAddressSelected() + } + CombinedDropdownMenu(M.weight(1f), cunList, "村", addUserViewModel.userCun, true) { + addUserViewModel.userCun = it + addUserViewModel.onAddressSelected() + } + CombinedDropdownMenu(M.weight(1f), zuList, "组", addUserViewModel.userZu, true) { + addUserViewModel.userZu = it + } + } +} + +@Composable +fun AddUserBankEditor( + addUserViewModel: AddUserViewModel, + isMainCard: Boolean, + title: String +) { + val cardInfo by if (isMainCard) { + addUserViewModel.mainCardInfo.collectAsState() + } else { + addUserViewModel.subCardInfo.collectAsState() + } + InputFrame(title) { + MyTextField( + modifier = M.weight(1f), + hint = "请输入", + value = cardInfo.first, + isNumberInputType = true, + ) { + addUserViewModel.clearBankCardInfo(isMainCard) + addUserViewModel.updateBankCardCode(isMainCard, it) + } + VipBadge { + MyButton(text = "拍照识别") { + addUserViewModel.recognizeBankCard(isMainCard) + } + } + } + InputFrame("开户银行") { + MyTextField( + modifier = M.weight(1f), + hint = "请输入", + value = cardInfo.second.bankName, + readOnly = true, + isNumberInputType = true, + ) + MyButton(text = "查询归属行") { + addUserViewModel.analysisBankCard(cardInfo.first, isMainCard) + } + } +} + @Composable fun AddUserLeftColumn( modifier: Modifier = M, @@ -478,7 +711,7 @@ fun AddUserRightColumn( } @Composable -fun InputFrame(title: String, content: @Composable () -> Unit) { +fun InputFrame(title: String, content: @Composable RowScope.() -> Unit) { Column(modifier = M.padding(top = 5.dp)) { Text(text = title, fontSize = MaterialTheme.typography.headlineMedium.fontSize, fontWeight = FontWeight.Bold, modifier = M.padding(vertical = 7.5.dp)) Row( diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserViewModel.kt index ffc159c..6490cf9 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserViewModel.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserViewModel.kt @@ -99,11 +99,12 @@ class AddUserViewModel : BaseViewModel() { doInIoThreadNoDialog { // 更新四级联动数据 _xianList.value = _address.map { it.first } - _xiangList.value = _address.first { it.first == xian }.second.map { it.first } - _cunList.value = - _address.first { it.first == xian }.second.first { it.first == xiang }.second.map { it.first } - _zuList.value = - _address.first { it.first == xian }.second.first { it.first == xiang }.second.first { it.first == cun }.second + val xianNode = _address.firstOrNull { it.first == xian } + _xiangList.value = xianNode?.second?.map { it.first } ?: emptyList() + val xiangNode = xianNode?.second?.firstOrNull { it.first == xiang } + _cunList.value = xiangNode?.second?.map { it.first } ?: emptyList() + val cunNode = xiangNode?.second?.firstOrNull { it.first == cun } + _zuList.value = cunNode?.second ?: emptyList() } } @@ -500,4 +501,4 @@ class AddUserViewModel : BaseViewModel() { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayScreen.kt index e1bb719..1248d35 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayScreen.kt @@ -179,7 +179,7 @@ fun PayByElectronic(payViewModel: PayViewModel) { MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) { input = it } - MyButton(modifier = M.padding(horizontal = 10.dp), text = "电子支付") { } + MyButton(modifier = M.padding(horizontal = 10.dp), text = "电子支付", enabled = false) { } } } @@ -191,7 +191,7 @@ fun PayByCash(payViewModel: PayViewModel) { MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) { input = it } - MyButton(modifier = M.padding(horizontal = 10.dp), text = "现金支付") { } + MyButton(modifier = M.padding(horizontal = 10.dp), text = "现金支付", enabled = false) { } } } @@ -208,6 +208,6 @@ fun PayByMix(payViewModel: PayViewModel) { MyTextField(value = input2, isNumberInputType = true, modifier = M.width(200.dp)) { input2 = it } - MyButton(modifier = M.padding(horizontal = 10.dp), text = "开始支付") { } + MyButton(modifier = M.padding(horizontal = 10.dp), text = "开始支付", enabled = false) { } } } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightScreen.kt index 8062eb7..bd5bfe3 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightScreen.kt @@ -64,6 +64,7 @@ import com.bbitcn.f8.pad.ui.screen.dialog.SplitDialog import com.bbitcn.f8.pad.ui.screen.dialog.WaterCutRecordDialog import com.bbitcn.f8.pad.ui.screen.view.Toasty import com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale.MyWeightShow +import com.bbitcn.f8.pad.utils.WeightUnitFormatter import kotlinx.coroutines.launch @Preview(showBackground = true, widthDp = 1280, heightDp = 800) @@ -231,7 +232,7 @@ fun WeightList(weightViewModel: WeightViewModel) { ) Text( color = MyColors.Gray, - text = "单位公斤、元、元/公斤,支持长按删除", + text = "单位${WeightUnitFormatter.unitLabel}、元、${WeightUnitFormatter.priceUnitLabel},支持长按删除", fontSize = MaterialTheme.typography.bodyMedium.fontSize ) } @@ -267,13 +268,13 @@ fun WeightList(weightViewModel: WeightViewModel) { item.categoryName to 1, item.packaging to 1, item.basketCount.toString() to 1, - item.grossWeight.toString() to 1, - item.tareWeight.toString() to 1, - item.netWeight.toString() to 1, - item.unitPrice.toString() to 1, + WeightUnitFormatter.formatWeight(item.grossWeight) to 1, + WeightUnitFormatter.formatWeight(item.tareWeight) to 1, + WeightUnitFormatter.formatWeight(item.netWeight) to 1, + WeightUnitFormatter.formatPrice(item.unitPrice) to 1, item.time to 2 ), - verticalPadding = 15.dp, + verticalPadding = 20.dp, onLongClick = { Toasty.showConfirmDialog("是否删除第${item.weighingTimes}磅次记录?") { weightViewModel.deleteDetail(item.sysid) @@ -304,10 +305,10 @@ fun WeightList(weightViewModel: WeightViewModel) { listOf( it.key, sumBasketCount, - sumGrossWeight, - sumTareWeight, - sumNetWeight, - sumUnitPrice + WeightUnitFormatter.formatWeight(sumGrossWeight), + WeightUnitFormatter.formatWeight(sumTareWeight), + WeightUnitFormatter.formatWeight(sumNetWeight), + WeightUnitFormatter.formatPrice(sumUnitPrice) ) } ) @@ -464,14 +465,12 @@ fun WeightRight(navController: NavController, weightViewModel: WeightViewModel) horizontalArrangement = Arrangement.spacedBy(20.dp), ) { VipBadge(modifier = M.weight(1f)) { - SaveButton(title = "按重量\n拆分茧别") { - Toasty.showToast("VIP功能,暂未开放") + SaveButton(title = "按重量\n拆分茧别", enabled = false) { // weightViewModel.showSplitDialog(true) } } VipBadge(modifier = M.weight(1f)) { - SaveButton(title = "按比例\n拆分茧别") { - Toasty.showToast("VIP功能,暂未开放") + SaveButton(title = "按比例\n拆分茧别", enabled = false) { // weightViewModel.showSplitDialog(false) } } @@ -535,7 +534,7 @@ fun AddKindsButton( ) Text( modifier = M.padding(top = 5.dp), - text = "${info.minPrice}-${info.maxPrice}元/公斤", + text = "${WeightUnitFormatter.formatPrice(info.minPrice)}-${WeightUnitFormatter.formatPrice(info.maxPrice)}${WeightUnitFormatter.priceUnitLabel}", color = MyColors.Black, fontSize = MaterialTheme.typography.bodyMedium.fontSize ) @@ -545,15 +544,21 @@ fun AddKindsButton( } @Composable -fun SaveButton(modifier: Modifier = M, title: String, onClick: () -> Unit) { - MyCard(colors = MyColors.BlueGreen, modifier = modifier) { - Box(modifier = M.clickable { onClick() }) { +fun SaveButton(modifier: Modifier = M, title: String, enabled: Boolean = true, onClick: () -> Unit) { + MyCard(colors = if (enabled) MyColors.BlueGreen else MyColors.Disabled, modifier = modifier) { + Box(modifier = M.clickable { + if (enabled) { + onClick() + } else { + Toasty.showToast("正在开发中,敬请期待") + } + }) { Text( modifier = M .fillMaxWidth() - .padding(10.dp), + .padding(vertical = 16.dp, horizontal = 10.dp), text = title, - color = MyColors.White, + color = if (enabled) MyColors.White else MyColors.Gray, fontSize = MaterialTheme.typography.bodyMedium.fontSize, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/Toasty.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/Toasty.kt index 474e13b..be1598b 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/Toasty.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/Toasty.kt @@ -51,6 +51,8 @@ object Toasty : BaseViewModel() { val confirmDialog = _confirmDialog.asStateFlow() private val _inputDialog = MutableStateFlow(InputDialogData()) val inputDialog = _inputDialog.asStateFlow() + private val _globalDialog = MutableStateFlow(GlobalDialogData()) + val globalDialog = _globalDialog.asStateFlow() // 控制 Drawer 状态的 Boolean Flow private val _isDrawerOpen = MutableStateFlow(false) @@ -233,6 +235,17 @@ object Toasty : BaseViewModel() { _confirmDialog.value = ConfirmDialogData(showDialog = false) } + fun showGlobalDialog(data: GlobalDialogData) { + _globalDialog.value = data.copy(showDialog = true) + } + + fun hideGlobalDialog(ownerKey: Any? = null) { + val current = _globalDialog.value + if (ownerKey == null || current.ownerKey == ownerKey) { + _globalDialog.value = GlobalDialogData(showDialog = false) + } + } + /** * 登录过期事件 */ @@ -254,3 +267,13 @@ object Toasty : BaseViewModel() { } } + +data class GlobalDialogData( + val ownerKey: Any? = null, + val title: String = "", + val showDialog: Boolean = false, + val onDismissRequest: () -> Unit = {}, + val clickOKStr: String = "", + val onClickOK: () -> Unit = {}, + val content: @Composable () -> Unit = {} +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Color.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Color.kt index 0f33600..d0d6cd5 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Color.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Color.kt @@ -17,6 +17,7 @@ object MyColors { val Orange = Color(0xFFFFAE00) val LightGray = Color(0xCCEEEEEE) val Gray = Color(0xFF999999) + val Disabled = Color(0xFFE5E7EB) val BlueGreen = Color(0xFF2FBAA3) val LightBlueGreen = Color(0x802FBAA3) val LightLightBlueGreen = Color(0xFFF7FFFE) @@ -28,4 +29,4 @@ object MyColors { val Red = Color(0xFFF44336) val LigntRed = Color(0x80E53935) // 添加更多的颜色常量... -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/PollingTask.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/PollingTask.kt index 4708760..a90045f 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/utils/PollingTask.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/PollingTask.kt @@ -33,6 +33,7 @@ class PollingTask private constructor() { * @param task 需要执行的任务 */ fun startDelayedTask(taskId: String, delaySeconds: Long, task: Runnable) { + ensureScheduler() stopTask(taskId) // 如果已有相同ID的任务,先停止 val future = scheduler!!.schedule({ @@ -99,6 +100,7 @@ class PollingTask private constructor() { * @param task 需要执行的任务 */ private fun startPollingTask(taskId: String, intervalSeconds: Long, task: Runnable) { + ensureScheduler() stopTask(taskId) // 如果已有相同ID的任务,先停止 val future = scheduler!!.scheduleWithFixedDelay(task, 0, intervalSeconds, TimeUnit.SECONDS) @@ -132,6 +134,12 @@ class PollingTask private constructor() { } } + private fun ensureScheduler() { + if (scheduler == null || scheduler!!.isShutdown || scheduler!!.isTerminated) { + createNewScheduler() + } + } + // 定义IO任务接口 interface IRxIOTask { fun doInIOThread(): T diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/WeightUnitFormatter.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/WeightUnitFormatter.kt new file mode 100644 index 0000000..a6782af --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/WeightUnitFormatter.kt @@ -0,0 +1,35 @@ +package com.bbitcn.f8.pad.utils + +object WeightUnitFormatter { + private const val KEY = "GLOBAL_WEIGHT_UNIT" + const val UNIT_KG = "公斤" + const val UNIT_JIN = "斤" + + var displayUnit: String + get() = MMKVUtil.get(KEY, UNIT_KG) + set(value) { + MMKVUtil.put(KEY, if (value == UNIT_JIN) UNIT_JIN else UNIT_KG) + } + + val unitLabel: String + get() = displayUnit + + val priceUnitLabel: String + get() = "元/$unitLabel" + + fun weight(kg: Double): Double = if (displayUnit == UNIT_JIN) kg * 2 else kg + + fun price(pricePerKg: Double): Double = if (displayUnit == UNIT_JIN) pricePerKg / 2 else pricePerKg + + fun formatWeight(kg: Double): String = trim(weight(kg)) + + fun formatPrice(pricePerKg: Double): String = trim(price(pricePerKg)) + + private fun trim(value: Double): String { + return if (value % 1.0 == 0.0) { + value.toInt().toString() + } else { + "%.2f".format(value).trimEnd('0').trimEnd('.') + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG06M_G25M.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG06M_G25M.kt index d802d87..485bdfc 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG06M_G25M.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG06M_G25M.kt @@ -15,10 +15,14 @@ import com.bbitcn.f8.pad.ui.screen.view.Toasty import com.bbitcn.f8.pad.utils.ConverterUtil import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import java.io.ByteArrayOutputStream import java.util.UUID import kotlin.experimental.xor @@ -28,6 +32,7 @@ object UHFReaderG06M_G25M : UHFReaderForSerial() { private var mRcpBase: RcpBase? = null private var mSioBase: SioCom? = null private var onCommListener: OnCommListener? = null + private val protocolScope = CoroutineScope(SupervisorJob() + Dispatchers.IO.limitedParallelism(1)) private var mBattaryLevel = 0 @@ -128,7 +133,7 @@ object UHFReaderG06M_G25M : UHFReaderForSerial() { Command(RcpMM.RCP_MM_PARA, RcpBase.RCP_MSG_GET) mRcpBase?.setOnProtocolListener({ obj, protocolEventArg -> - doInIoThreadNoDialog { + protocolScope.launch { val psData = protocolEventArg.protocolPacket when (psData.Code) { RcpMM.RCP_MM_READ_C_UII -> @@ -296,4 +301,4 @@ object UHFReaderG06M_G25M : UHFReaderForSerial() { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClient.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClient.kt index 7c093ff..cacfc31 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClient.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClient.kt @@ -45,7 +45,13 @@ object RetrofitClient { try { val response = chain.proceed(chain.request()) - if (response.code !in listOf(200, 401)) { + if (response.code == 401) { + Toasty.loginExpired() + Toasty.error("登录已过期") + return@Interceptor response + } + + if (response.code != 200) { val content = response.peekBody(ERROR_BODY_PEEK_BYTES).string() runCatching { Gson().fromJson(content, CommonResponse::class.java) diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/TokenAuthenticator.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/TokenAuthenticator.kt index e5e3775..53469ce 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/utils/network/TokenAuthenticator.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/TokenAuthenticator.kt @@ -8,9 +8,13 @@ import com.bbitcn.f8.pad.utils.global.RxTag import com.bbitcn.f8.pad.utils.log.MyLog import kotlinx.coroutines.runBlocking import okhttp3.Authenticator +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.Route +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit /** * 401 token refresh interceptor. @@ -18,6 +22,20 @@ import okhttp3.Route class TokenAuthenticator : Authenticator { private val lock = Any() + private val refreshApi: ApiService by lazy { + Retrofit.Builder() + .baseUrl(RetrofitClient.BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client( + OkHttpClient.Builder() + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .build() + ) + .build() + .create(ApiService::class.java) + } override fun authenticate(route: Route?, response: Response): Request? { MyLog.network("TokenAuthenticator: authenticate") @@ -45,7 +63,7 @@ class TokenAuthenticator : Authenticator { val newToken = runBlocking { MyLog.network("刷新Token: $currentToken, $refreshToken") - val result = RetrofitClient.apiInterface().refreshToken( + val result = refreshApi.refreshToken( RefreshTokenRequest( hardwareid = Global.getDeviceId(), refToken = refreshToken,