优化性能问题;完成需求;重构弹窗

This commit is contained in:
BBIT-Kai
2026-05-28 09:31:06 +08:00
parent 22eeac6c62
commit d1bc715aac
26 changed files with 862 additions and 662 deletions
@@ -21,6 +21,10 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
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.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
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
@@ -34,6 +38,8 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import com.bbitcn.f8.pad.M import com.bbitcn.f8.pad.M
import com.bbitcn.f8.pad.MyApp 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 import com.blankj.utilcode.util.ActivityUtils
@Preview(showBackground = true, widthDp = 1280, heightDp = 800) @Preview(showBackground = true, widthDp = 1280, heightDp = 800)
@@ -68,7 +74,44 @@ fun MyDialog(
onClickOK: () -> Unit = {}, onClickOK: () -> Unit = {},
content: @Composable () -> 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( Box(
modifier = M modifier = M
.fillMaxSize() .fillMaxSize()
@@ -78,8 +121,8 @@ fun MyDialog(
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
BaseDialogFrame(title, { BaseDialogFrame(data.title, {
content() data.content()
}) { }) {
Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
BigButton( BigButton(
@@ -87,13 +130,13 @@ fun MyDialog(
.weight(1f) .weight(1f)
.widthIn(max = 100.dp) .widthIn(max = 100.dp)
) { ) {
onDismissRequest() data.onDismissRequest()
} }
// 判断是否有点击事件 // 判断是否有点击事件
if (clickOKStr != "") { if (data.clickOKStr != "") {
Spacer(modifier = M.width(30.dp)) Spacer(modifier = M.width(30.dp))
BigButton(clickOKStr, modifier = M.weight(1f), true) { BigButton(data.clickOKStr, modifier = M.weight(1f), true) {
onClickOK() data.onClickOK()
} }
} }
} }
@@ -110,7 +153,7 @@ fun BaseDialogFrame(
) { ) {
MyCard( MyCard(
modifier = M modifier = M
.padding(top = 10.dp, bottom = 50.dp, start = 100.dp, end = 100.dp) .padding(vertical = 40.dp, horizontal = 100.dp)
.noVisualFeedbackClickable { } .noVisualFeedbackClickable { }
.fillMaxSize() .fillMaxSize()
) { ) {
@@ -27,6 +27,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.SocketException import java.net.SocketException
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import retrofit2.HttpException
open class BaseViewModel : ViewModel() { open class BaseViewModel : ViewModel() {
@@ -78,6 +79,11 @@ open class BaseViewModel : ViewModel() {
MyLog.test("协程被取消:${exception.javaClass.simpleName}message=${exception.message}") MyLog.test("协程被取消:${exception.javaClass.simpleName}message=${exception.message}")
return@onFailure return@onFailure
} }
if (exception is HttpException && exception.code() == 401) {
Toasty.loginExpired()
Toasty.error("登录已过期")
return@onFailure
}
// 其他异常继续处理 // 其他异常继续处理
exception.printStackTrace() exception.printStackTrace()
onError(exception) onError(exception)
@@ -208,6 +214,7 @@ open class BaseViewModel : ViewModel() {
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
pollingTask.stopAllTasks()
taskMap.values.forEach { it.cancel() } taskMap.values.forEach { it.cancel() }
taskMap.clear() taskMap.clear()
} }
@@ -172,13 +172,23 @@ fun MyButton(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Button( Button(
onClick = onClick, onClick = {
if (enabled) {
onClick()
} else {
Toasty.showToast("正在开发中,敬请期待")
}
},
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = true,
interactionSource = interactionSource, interactionSource = interactionSource,
shape = shape, shape = shape,
border = border, 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, contentPadding = contentPadding,
) { ) {
Text( Text(
@@ -42,6 +42,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.bbitcn.f8.pad.M 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.TopInfoViewModel
import com.bbitcn.f8.pad.ui.screen.LoginScreen import com.bbitcn.f8.pad.ui.screen.LoginScreen
import com.bbitcn.f8.pad.ui.screen.MainScreen import com.bbitcn.f8.pad.ui.screen.MainScreen
@@ -273,6 +274,7 @@ class MainActivity : ComponentActivity() {
val loadingDialog by Toasty.loadingDialog.collectAsState() val loadingDialog by Toasty.loadingDialog.collectAsState()
val confirmDialog by Toasty.confirmDialog.collectAsState() val confirmDialog by Toasty.confirmDialog.collectAsState()
val inputDialog by Toasty.inputDialog.collectAsState() val inputDialog by Toasty.inputDialog.collectAsState()
val globalDialog by Toasty.globalDialog.collectAsState()
TipsDialog( TipsDialog(
showDialog = tipsDialog.showDialog, showDialog = tipsDialog.showDialog,
onDismiss = { Toasty.hideTipsDialog() }, onDismiss = { Toasty.hideTipsDialog() },
@@ -281,6 +283,7 @@ class MainActivity : ComponentActivity() {
LoadingDialog(loadingDialog) LoadingDialog(loadingDialog)
ConfirmDialog(confirmDialog) ConfirmDialog(confirmDialog)
InputDialog(inputDialog) InputDialog(inputDialog)
GlobalDialogHost(globalDialog)
} }
} }
} }
@@ -51,6 +51,7 @@ import com.bbitcn.f8.pad.base.isBluetoothEnabled
import com.bbitcn.f8.pad.receiver.SystemInfoReceiver import com.bbitcn.f8.pad.receiver.SystemInfoReceiver
import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel
import com.bbitcn.f8.pad.ui.theme.MyColors 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.scale.ScaleBT
import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT
import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB
@@ -88,6 +89,7 @@ fun MyTopBar(
val date by topInfoViewModel.date.collectAsState() val date by topInfoViewModel.date.collectAsState()
val time by topInfoViewModel.time.collectAsState() val time by topInfoViewModel.time.collectAsState()
val logoState by topInfoViewModel.logoState.collectAsState() val logoState by topInfoViewModel.logoState.collectAsState()
val versionName = remember { MyUtil.getVersionName() }
DisposableEffect(context) { DisposableEffect(context) {
systemInfoReceiver.register() systemInfoReceiver.register()
@@ -151,6 +153,7 @@ fun MyTopBar(
) )
}, label = "animated content" }, label = "animated content"
) { value -> ) { value ->
Row(verticalAlignment = Alignment.Bottom) {
Text( Text(
text = value.first.second, text = value.first.second,
M.padding(horizontal = 8.dp), M.padding(horizontal = 8.dp),
@@ -158,6 +161,13 @@ fun MyTopBar(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = MaterialTheme.typography.headlineMedium.fontSize 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)) Spacer(modifier = M.weight(1f))
@@ -1,5 +1,7 @@
package com.bbitcn.f8.pad.ui.screen.dialog 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column 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.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.bbitcn.f8.pad.M import com.bbitcn.f8.pad.M
@@ -46,6 +49,8 @@ data class MessageDialogData(
val time: String = "", val time: String = "",
val username: String = "", val username: String = "",
val content: String = "", val content: String = "",
val title: String = "",
val isWebUrl: Boolean = false,
) )
@Preview(showBackground = true, widthDp = 1280, heightDp = 800) @Preview(showBackground = true, widthDp = 1280, heightDp = 800)
@@ -63,7 +68,7 @@ fun MessagePreview() {
@Composable @Composable
fun MessageDialog(info: MessageDialogData) { fun MessageDialog(info: MessageDialogData) {
MyDialog("查看消息", MyDialog(info.title.ifEmpty { "查看消息" },
info.showDialog, info.showDialog,
onDismissRequest = { info.onDismiss() } onDismissRequest = { info.onDismiss() }
) { ) {
@@ -98,6 +103,23 @@ fun MessageDialog(info: MessageDialogData) {
.padding(30.dp) .padding(30.dp)
.verticalScroll(rememberScrollState()) // 添加滚动支持 .verticalScroll(rememberScrollState()) // 添加滚动支持
) { ) {
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(
text = info.content, text = info.content,
fontSize = MaterialTheme.typography.headlineMedium.fontSize, fontSize = MaterialTheme.typography.headlineMedium.fontSize,
@@ -108,3 +130,4 @@ fun MessageDialog(info: MessageDialogData) {
} }
} }
} }
}
@@ -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.request.CocoonTypeTranslateRequest
import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse
import com.bbitcn.f8.pad.ui.screen.view.Toasty 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 com.bbitcn.f8.pad.utils.MyUtil
import kotlin.collections.map import kotlin.collections.map
@@ -157,13 +158,12 @@ fun TicketMoreDialog(
} }
} }
item { item {
MyImageButton("茧别转换", R.drawable.ic_ticket_convert) { MyImageButton("茧别转换", R.drawable.ic_ticket_convert, enabled = false) {
isTranslated = !isTranslated isTranslated = !isTranslated
} }
} }
item { item {
MyImageButton("茧票过户", R.drawable.ic_ticket_transfer, true) { MyImageButton("茧票过户", R.drawable.ic_ticket_transfer, vip = true, enabled = false) {
} }
} }
item { item {
@@ -176,13 +176,11 @@ fun TicketMoreDialog(
} }
} }
item { item {
MyImageButton("上传图片", R.drawable.ic_upload_pic, true) { MyImageButton("上传图片", R.drawable.ic_upload_pic, vip = true, enabled = false) {
} }
} }
item { 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 @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( Column(
modifier = M modifier = M
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp) .padding(10.dp)
.clickable { onClick() }, .clickable {
if (enabled) {
onClick()
} else {
Toasty.showToast("正在开发中,敬请期待")
}
},
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (vip) { if (vip) {
@@ -251,18 +255,21 @@ fun MyImageButton(text: String, resId: Int, vip: Boolean = false, onClick: () ->
Image( Image(
painter = painterResource(id = resId), painter = painterResource(id = resId),
contentDescription = text, contentDescription = text,
modifier = M.size(90.dp) modifier = M.size(90.dp),
alpha = if (enabled) 1f else 0.35f
) )
} }
} else { } else {
Image( Image(
painter = painterResource(id = resId), painter = painterResource(id = resId),
contentDescription = text, contentDescription = text,
modifier = M.size(90.dp) modifier = M.size(90.dp),
alpha = if (enabled) 1f else 0.35f
) )
} }
Text( Text(
text = text, text = text,
color = if (enabled) MyColors.Black else MyColors.Gray,
fontSize = MaterialTheme.typography.headlineMedium.fontSize, fontSize = MaterialTheme.typography.headlineMedium.fontSize,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@@ -153,15 +153,15 @@ fun FundsScreen(
) )
} }
Spacer(modifier = M.weight(1f)) Spacer(modifier = M.weight(1f))
MyButton(modifier = M.fillMaxWidth(), text = "查询配额") { MyButton(modifier = M.fillMaxWidth(), text = "查询配额", enabled = false) {
fundsViewModel.showQueryBalanceDialog() fundsViewModel.showQueryBalanceDialog()
} }
VipBadge { VipBadge {
MyButton(modifier = M.fillMaxWidth(), text = "查询余额") { MyButton(modifier = M.fillMaxWidth(), text = "查询余额", enabled = false) {
fundsViewModel.showQueryBalanceDialog() fundsViewModel.showQueryBalanceDialog()
} }
} }
MyButton(modifier = M.fillMaxWidth(), text = "修改支付密码") { MyButton(modifier = M.fillMaxWidth(), text = "修改支付密码", enabled = false) {
Toasty.showToast("正在开发中,敬请期待!") Toasty.showToast("正在开发中,敬请期待!")
} }
} }
@@ -413,6 +413,7 @@ fun WeatherItem(
@Composable @Composable
fun MessageInfo(homeViewModel: HomeViewModel) { fun MessageInfo(homeViewModel: HomeViewModel) {
val notices by homeViewModel.notices.collectAsState()
Box(modifier = M.padding(5.dp)) { Box(modifier = M.padding(5.dp)) {
Image( Image(
painter = painterResource(id = R.drawable.bg_weather), painter = painterResource(id = R.drawable.bg_weather),
@@ -429,14 +430,16 @@ fun MessageInfo(homeViewModel: HomeViewModel) {
}) })
} }
LazyColumn { LazyColumn {
items(count = 0) { index -> items(count = notices.size) { index ->
val notice = notices[index]
MessageItem( MessageItem(
homeViewModel, homeViewModel,
true, true,
true, !homeViewModel.isNoticeRead(notice.id),
"管理员", notice.sender,
"2024-08-02 11:25:32", notice.time,
"正在开发中" notice.title,
onClick = { homeViewModel.showNoticeDialog(notice) }
) )
} }
} }
@@ -451,12 +454,13 @@ fun MessageItem(
unRead: Boolean, unRead: Boolean,
name: String, name: String,
time: String, time: String,
message: String message: String,
onClick: (() -> Unit)? = null
) { ) {
Row( Row(
modifier = M modifier = M
.clickable { .clickable {
homeViewModel.showMsgDialog(name, time, message) onClick?.invoke() ?: homeViewModel.showMsgDialog(name, time, message)
} }
.padding(bottom = 5.dp), .padding(bottom = 5.dp),
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
@@ -22,6 +22,14 @@ import java.util.Date
class HomeViewModel : BaseViewModel() { 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<List<TodayPriceResponse.Data>>(emptyList()) private val _todayPrice = MutableStateFlow<List<TodayPriceResponse.Data>>(emptyList())
val todayPrice = _todayPrice.asStateFlow() val todayPrice = _todayPrice.asStateFlow()
@@ -37,7 +45,11 @@ class HomeViewModel : BaseViewModel() {
private val _messageDialogData = MutableStateFlow(MessageDialogData()) private val _messageDialogData = MutableStateFlow(MessageDialogData())
val messageDialogData: StateFlow<MessageDialogData> = _messageDialogData.asStateFlow() val messageDialogData: StateFlow<MessageDialogData> = _messageDialogData.asStateFlow()
private val _notices = MutableStateFlow<List<Notice>>(emptyList())
val notices = _notices.asStateFlow()
init { init {
loadMockNotices()
refreshWeatherInfo(false) refreshWeatherInfo(false)
refreshTodayPrice(false) refreshTodayPrice(false)
doInIoThreadNoDialog { 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<String> {
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( fun refreshAcquireData(
startDate: Date = _dateRange2.value.first, startDate: Date = _dateRange2.value.first,
endDate: Date = _dateRange2.value.second, 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) { fun logout(onFinished: () -> Unit) {
// 退出登录 // 退出登录
doInIoThreadThenUI(loadingTips = "正在退出登录", onIO = { doInIoThreadThenUI(loadingTips = "正在退出登录", onIO = {
@@ -118,6 +118,7 @@ fun PurchaseScreen(
var queryInput by rememberSaveable { mutableStateOf("") } var queryInput by rememberSaveable { mutableStateOf("") }
// 筛选条件3:查询类型 // 筛选条件3:查询类型
var queryType by rememberSaveable { mutableStateOf(0) } var queryType by rememberSaveable { mutableStateOf(0) }
var queryInputInitialized by rememberSaveable { mutableStateOf(false) }
val info = purchaseViewModel.infoPager.collectAsLazyPagingItems() val info = purchaseViewModel.infoPager.collectAsLazyPagingItems()
// 查询方法 // 查询方法
@@ -131,6 +132,10 @@ fun PurchaseScreen(
info.refresh() info.refresh()
} }
LaunchedEffect(queryInput) { LaunchedEffect(queryInput) {
if (!queryInputInitialized) {
queryInputInitialized = true
return@LaunchedEffect
}
delay(350) delay(350)
updateParams() updateParams()
} }
@@ -65,7 +65,10 @@ class PurchaseViewModel : BaseViewModel() {
init { init {
doInIoThreadNoDialog { doInIoThreadNoDialog {
// 获取一个月前的日期 // 获取一个月前的日期
updateParams(getRecentMonthsDate(1), Date(), "", 0) val start = getRecentMonthsDate(1)
val end = Date()
_dateRange.value = start to end
updateParams(start, end, "", 0)
} }
} }
@@ -167,7 +167,7 @@ fun StatisticsLeft(
} }
Spacer(modifier = M.weight(1f)) Spacer(modifier = M.weight(1f))
VipBadge { VipBadge {
MyButton(modifier = M.fillMaxWidth(), text = "发送短信") { MyButton(modifier = M.fillMaxWidth(), text = "发送短信", enabled = false) {
Toasty.showToast("正在开发中,敬请期待") Toasty.showToast("正在开发中,敬请期待")
} }
} }
@@ -1,77 +1,50 @@
package com.bbitcn.f8.pad.ui.screen.mainFunc 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.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
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
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.verticalScroll
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.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.mutableStateOf 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems 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.AssistChipFilter
import com.bbitcn.f8.pad.base.InfoText 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.MyButton
import com.bbitcn.f8.pad.base.MyCard 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.MyInfoCard
import com.bbitcn.f8.pad.base.MyRefreshTable import com.bbitcn.f8.pad.base.MyRefreshTable
import com.bbitcn.f8.pad.base.MyTableData import com.bbitcn.f8.pad.base.MyTableData
import com.bbitcn.f8.pad.base.QueryTextField 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.model.net.response.UserDataResponse
import com.bbitcn.f8.pad.ui.theme.MyColors import com.bbitcn.f8.pad.ui.theme.MyColors
import com.bbitcn.f8.pad.utils.MyUtil
import com.bbitcn.f8.pad.utils.TimeUtils 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) @Preview(showBackground = true, widthDp = 1280, heightDp = 800)
@Composable @Composable
fun UserScreenPV() { fun UserScreenPV() {
@@ -83,339 +56,14 @@ fun UserScreen(
navController: NavController, navController: NavController,
userViewModel: UserViewModel = viewModel() userViewModel: UserViewModel = viewModel()
) { ) {
val treeData by userViewModel.treeData.collectAsState()
val userData = userViewModel.usersInfoPager.collectAsLazyPagingItems() val userData = userViewModel.usersInfoPager.collectAsLazyPagingItems()
MainFuncFrame { MainFuncFrame {
if (isLandscape()) { MyInfoCard(modifier = M.fillMaxSize()) {
UserScreenInLandscape(navController, userViewModel, treeData, userData)
} else {
UserScreenInPortrait(navController, userViewModel, treeData, userData)
}
}
}
@Composable
fun UserScreenInPortrait(
navController: NavController,
userViewModel: UserViewModel,
treeData: List<Pair<Pair<String, Int>, Any>>,
userData: LazyPagingItems<UserDataResponse.Data>
) {
Column(
modifier = M.fillMaxSize()
) {
MyInfoCard(
modifier = M
.weight(2f)
.padding(bottom = 15.dp)
) {
CollapsibleList(userViewModel, treeData, userData)
}
MyInfoCard(
modifier = M.weight(8f)
) {
UserManageList(navController, userViewModel, userData) UserManageList(navController, userViewModel, userData)
} }
} }
} }
@Composable
fun UserScreenInLandscape(
navController: NavController,
userViewModel: UserViewModel,
treeData: List<Pair<Pair<String, Int>, Any>>,
userData: LazyPagingItems<UserDataResponse.Data>
) {
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<Pair<Pair<String, Int>, Any>>,
pager: LazyPagingItems<UserDataResponse.Data>
) {
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<UserDataResponse.Data>,
item: Pair<Pair<String, Int>, 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<Pair<String, Int>, 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<String, Any>)?.let { subItemPair ->
// CollapsibleItem(userViewModel,subItemPair,
// currentLevel + 1)
// }
// }
}
}
}
}
}
}
}
@Composable @Composable
fun UserManageList( fun UserManageList(
navController: NavController, navController: NavController,
@@ -424,6 +72,7 @@ fun UserManageList(
) { ) {
var queryInput by rememberSaveable { mutableStateOf("") } var queryInput by rememberSaveable { mutableStateOf("") }
val areaFilter by userViewModel.areaFilter.collectAsState() val areaFilter by userViewModel.areaFilter.collectAsState()
var detailUser by remember { mutableStateOf<UserDataResponse.Data?>(null) }
val onFilterLikeChanged: (String) -> Unit = { val onFilterLikeChanged: (String) -> Unit = {
queryInput = it queryInput = it
userViewModel.updateParamsLike(it) userViewModel.updateParamsLike(it)
@@ -434,25 +83,14 @@ fun UserManageList(
.padding(15.dp) .padding(15.dp)
.fillMaxSize() .fillMaxSize()
) { ) {
Row( UserFilterBar(
modifier = M userViewModel = userViewModel,
.fillMaxWidth() userData = userData,
.padding(bottom = 5.dp), queryInput = queryInput,
horizontalArrangement = Arrangement.End, onQueryChanged = onFilterLikeChanged,
verticalAlignment = Alignment.CenterVertically, areaFilter = areaFilter,
) { navController = navController
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 = {})
}
val myPager = userViewModel.usersInfoMyPager val myPager = userViewModel.usersInfoMyPager
val isRefreshing by myPager.listIsRefreshing.collectAsState() val isRefreshing by myPager.listIsRefreshing.collectAsState()
MyRefreshTable( MyRefreshTable(
@@ -467,8 +105,8 @@ fun UserManageList(
MyTableData(1, isIndex = true), MyTableData(1, isIndex = true),
MyTableData("姓名", 1, { it.nhname }), MyTableData("姓名", 1, { it.nhname }),
MyTableData("手机号", 2, { it.phone }), MyTableData("手机号", 2, { it.phone }),
MyTableData("身份证", 1, { if (it.idcard != "") "✔️" else "" }), MyTableData("身份证", 1, { if (it.idcard != "") "已录" else "" }),
MyTableData("银行卡", 1, { if (it.bankcode != "") "✔️" else "" }), MyTableData("银行卡", 1, { if (it.bankcode != "") "已录" else "" }),
MyTableData("所属地址", 3, { "${it.xian}${it.xiang}${it.cun}${it.zu}" }), MyTableData("所属地址", 3, { "${it.xian}${it.xiang}${it.cun}${it.zu}" }),
MyTableData("建档时间", 2, { TimeUtils.formatDateTimeStrToDateStr(it.createtime) }), MyTableData("建档时间", 2, { TimeUtils.formatDateTimeStrToDateStr(it.createtime) }),
MyTableData("", 1, { "修改" }, true) { MyTableData("", 1, { "修改" }, true) {
@@ -478,27 +116,8 @@ fun UserManageList(
navController.navigate("weight/${it.sysid}") navController.navigate("weight/${it.sysid}")
}, },
), ),
onExpend = { onClick = {
Column(modifier = M.fillMaxWidth().border(1.dp, MyColors.Gray).padding(15.dp)) { detailUser = it
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)
}
}
}, },
onLongClick = { onLongClick = {
userViewModel.deleteUser(it.nhname, it.sysid) { 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<UserDataResponse.Data>,
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))
}
}
} }
@@ -3,28 +3,51 @@ package com.bbitcn.f8.pad.ui.screen.mainFunc;
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bbitcn.f8.pad.base.BaseViewModel import com.bbitcn.f8.pad.base.BaseViewModel
import com.bbitcn.f8.pad.model.net.request.UserListDataRequest 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.ui.screen.view.Toasty
import com.bbitcn.f8.pad.utils.pager.MyPager import com.bbitcn.f8.pad.utils.pager.MyPager
import com.bbitcn.f8.pad.utils.pager.UsersInfoPagingSource import com.bbitcn.f8.pad.utils.pager.UsersInfoPagingSource
import com.blankj.utilcode.util.StringUtils
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
class UserViewModel : BaseViewModel() { class UserViewModel : BaseViewModel() {
private val _areaLike = MutableStateFlow("")
val areaLike = _areaLike.asStateFlow()
private val _treeData = MutableStateFlow<List<Pair<Pair<String, Int>, Any>>>(emptyList())
val treeData: StateFlow<List<Pair<Pair<String, Int>, Any>>> = _treeData.asStateFlow()
private val _tempData: MutableList<UsersAreaResponse.Data> = mutableListOf()
private val _areaFilter = MutableStateFlow("") private val _areaFilter = MutableStateFlow("")
val areaFilter = _areaFilter.asStateFlow() 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<List<String>>(emptyList())
val xianList = _xianList.asStateFlow()
private val _xiangList = MutableStateFlow<List<String>>(emptyList())
val xiangList = _xiangList.asStateFlow()
private val _cunList = MutableStateFlow<List<String>>(emptyList())
val cunList = _cunList.asStateFlow()
private val _zuList = MutableStateFlow<List<String>>(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( val usersInfoMyPager = MyPager(
pagingSourceFactory = { UsersInfoPagingSource(it) }, pagingSourceFactory = { UsersInfoPagingSource(it) },
initialRequestData = UserListDataRequest(), // 传入初始的请求数据 initialRequestData = UserListDataRequest(), // 传入初始的请求数据
@@ -33,7 +56,7 @@ class UserViewModel : BaseViewModel() {
val usersInfoPager = usersInfoMyPager.createPager(viewModelScope) val usersInfoPager = usersInfoMyPager.createPager(viewModelScope)
init { init {
getUsersArea(false) refreshAreaOptions()
} }
fun updateParamsLike(like: String) { fun updateParamsLike(like: String) {
@@ -51,6 +74,67 @@ class UserViewModel : BaseViewModel() {
_areaFilter.value = buildAreaFilter(xian, xiang, cun, zu) _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 { fun buildAreaFilter(xian: String, xiang: String, cun: String, zu: String): String {
return buildString { return buildString {
append(xian) 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<UsersAreaResponse.Data>): List<Pair<Pair<String, Int>, Any>> {
val result = mutableListOf<Pair<Pair<String, Int>, Any>>()
// val result = mutableListOf<Pair<Pair<String, Int>, Pair<Pair<String, Int>, Pair<Pair<String, Int>, Pair<Pair<String, Int>, 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<UsersAreaResponse.Data>,
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) { fun deleteUser(name: String, sysid: String, onSuccess: () -> Unit) {
Toasty.showConfirmDialog("确定删除农户" + name + "吗?") { Toasty.showConfirmDialog("确定删除农户" + name + "吗?") {
doInIoThread("正在删除农户...") { doInIoThread("正在删除农户...") {
@@ -1,11 +1,15 @@
package com.bbitcn.f8.pad.ui.screen.secondFunc 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.animation.animateContentSize
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.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.foundation.lazy.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -74,29 +80,23 @@ fun AddUserScreen(
addUserViewModel.initExtendData() addUserViewModel.initExtendData()
} }
} }
val scanDialogData by addUserViewModel.scanDialogData.collectAsState()
val ocrDialogData by addUserViewModel.ocrDialogData.collectAsState() val ocrDialogData by addUserViewModel.ocrDialogData.collectAsState()
val faceDialogData by addUserViewModel.faceDialogData.collectAsState()
MainFuncFrame { MainFuncFrame {
MyCard(modifier = M.fillMaxSize(), radius = 7.dp, elevation = 1.dp) { MyCard(modifier = M.fillMaxSize(), radius = 7.dp, elevation = 1.dp) {
VerticalTabPages( VerticalTabPages(
modifier = M modifier = M
.fillMaxHeight() .fillMaxHeight()
.padding(vertical = 15.dp), .padding(vertical = 15.dp),
tabs = listOf("基本信息", "人脸录入", "附件上传", "拓展信息"), tabs = listOf("基本信息", "拓展信息"),
) { ) {
when (it) { when (it) {
0 -> AddUserBaseInfo(addUserViewModel) 0 -> AddUserBaseInfo(addUserViewModel)
1 -> AddUserFaceInfo(addUserViewModel) 1 -> AddUSerExtendInfo(addUserViewModel)
2 -> AddUserAttachmentInfo(addUserViewModel)
3 -> AddUSerExtendInfo(addUserViewModel)
} }
} }
} }
} }
ScanDialog(scanDialogData)
OCRDialog(ocrDialogData) OCRDialog(ocrDialogData)
FaceDialog(faceDialogData)
} }
@Composable @Composable
@@ -170,6 +170,12 @@ fun AddUserBaseInfo(
addUserViewModel: AddUserViewModel addUserViewModel: AddUserViewModel
) { ) {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
var selectedCard by rememberSaveable { mutableStateOf("id") }
var extraBankCount by rememberSaveable { mutableStateOf(0) }
var avatar by remember { mutableStateOf<Bitmap?>(null) }
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
avatar = it
}
// 判断设备是否为竖屏 // 判断设备是否为竖屏
if (configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT) { if (configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT) {
// 竖屏模式 // 竖屏模式
@@ -182,10 +188,18 @@ fun AddUserBaseInfo(
) { ) {
addUserViewModel.editFarmer() addUserViewModel.editFarmer()
} }
AddUserLeftColumn( FarmerCardWallet(
addUserViewModel = addUserViewModel 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 { } else {
@@ -198,12 +212,103 @@ fun AddUserBaseInfo(
) { ) {
addUserViewModel.editFarmer() addUserViewModel.editFarmer()
} }
AddUserLeftColumn( FarmerCardWallet(
modifier = M.weight(1f), 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 @Composable
fun AddUserLeftColumn( fun AddUserLeftColumn(
modifier: Modifier = M, modifier: Modifier = M,
@@ -478,7 +711,7 @@ fun AddUserRightColumn(
} }
@Composable @Composable
fun InputFrame(title: String, content: @Composable () -> Unit) { fun InputFrame(title: String, content: @Composable RowScope.() -> Unit) {
Column(modifier = M.padding(top = 5.dp)) { 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)) Text(text = title, fontSize = MaterialTheme.typography.headlineMedium.fontSize, fontWeight = FontWeight.Bold, modifier = M.padding(vertical = 7.5.dp))
Row( Row(
@@ -99,11 +99,12 @@ class AddUserViewModel : BaseViewModel() {
doInIoThreadNoDialog { doInIoThreadNoDialog {
// 更新四级联动数据 // 更新四级联动数据
_xianList.value = _address.map { it.first } _xianList.value = _address.map { it.first }
_xiangList.value = _address.first { it.first == xian }.second.map { it.first } val xianNode = _address.firstOrNull { it.first == xian }
_cunList.value = _xiangList.value = xianNode?.second?.map { it.first } ?: emptyList()
_address.first { it.first == xian }.second.first { it.first == xiang }.second.map { it.first } val xiangNode = xianNode?.second?.firstOrNull { it.first == xiang }
_zuList.value = _cunList.value = xiangNode?.second?.map { it.first } ?: emptyList()
_address.first { it.first == xian }.second.first { it.first == xiang }.second.first { it.first == cun }.second val cunNode = xiangNode?.second?.firstOrNull { it.first == cun }
_zuList.value = cunNode?.second ?: emptyList()
} }
} }
@@ -179,7 +179,7 @@ fun PayByElectronic(payViewModel: PayViewModel) {
MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) { MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) {
input = it 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)) { MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) {
input = it 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)) { MyTextField(value = input2, isNumberInputType = true, modifier = M.width(200.dp)) {
input2 = it input2 = it
} }
MyButton(modifier = M.padding(horizontal = 10.dp), text = "开始支付") { } MyButton(modifier = M.padding(horizontal = 10.dp), text = "开始支付", enabled = false) { }
} }
} }
@@ -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.dialog.WaterCutRecordDialog
import com.bbitcn.f8.pad.ui.screen.view.Toasty 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.ui.screen.view.deviceManager.scale.MyWeightShow
import com.bbitcn.f8.pad.utils.WeightUnitFormatter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Preview(showBackground = true, widthDp = 1280, heightDp = 800) @Preview(showBackground = true, widthDp = 1280, heightDp = 800)
@@ -231,7 +232,7 @@ fun WeightList(weightViewModel: WeightViewModel) {
) )
Text( Text(
color = MyColors.Gray, color = MyColors.Gray,
text = "单位公斤、元、元/公斤,支持长按删除", text = "单位${WeightUnitFormatter.unitLabel}、元、${WeightUnitFormatter.priceUnitLabel},支持长按删除",
fontSize = MaterialTheme.typography.bodyMedium.fontSize fontSize = MaterialTheme.typography.bodyMedium.fontSize
) )
} }
@@ -267,13 +268,13 @@ fun WeightList(weightViewModel: WeightViewModel) {
item.categoryName to 1, item.categoryName to 1,
item.packaging to 1, item.packaging to 1,
item.basketCount.toString() to 1, item.basketCount.toString() to 1,
item.grossWeight.toString() to 1, WeightUnitFormatter.formatWeight(item.grossWeight) to 1,
item.tareWeight.toString() to 1, WeightUnitFormatter.formatWeight(item.tareWeight) to 1,
item.netWeight.toString() to 1, WeightUnitFormatter.formatWeight(item.netWeight) to 1,
item.unitPrice.toString() to 1, WeightUnitFormatter.formatPrice(item.unitPrice) to 1,
item.time to 2 item.time to 2
), ),
verticalPadding = 15.dp, verticalPadding = 20.dp,
onLongClick = { onLongClick = {
Toasty.showConfirmDialog("是否删除第${item.weighingTimes}磅次记录?") { Toasty.showConfirmDialog("是否删除第${item.weighingTimes}磅次记录?") {
weightViewModel.deleteDetail(item.sysid) weightViewModel.deleteDetail(item.sysid)
@@ -304,10 +305,10 @@ fun WeightList(weightViewModel: WeightViewModel) {
listOf( listOf(
it.key, it.key,
sumBasketCount, sumBasketCount,
sumGrossWeight, WeightUnitFormatter.formatWeight(sumGrossWeight),
sumTareWeight, WeightUnitFormatter.formatWeight(sumTareWeight),
sumNetWeight, WeightUnitFormatter.formatWeight(sumNetWeight),
sumUnitPrice WeightUnitFormatter.formatPrice(sumUnitPrice)
) )
} }
) )
@@ -464,14 +465,12 @@ fun WeightRight(navController: NavController, weightViewModel: WeightViewModel)
horizontalArrangement = Arrangement.spacedBy(20.dp), horizontalArrangement = Arrangement.spacedBy(20.dp),
) { ) {
VipBadge(modifier = M.weight(1f)) { VipBadge(modifier = M.weight(1f)) {
SaveButton(title = "按重量\n拆分茧别") { SaveButton(title = "按重量\n拆分茧别", enabled = false) {
Toasty.showToast("VIP功能,暂未开放")
// weightViewModel.showSplitDialog(true) // weightViewModel.showSplitDialog(true)
} }
} }
VipBadge(modifier = M.weight(1f)) { VipBadge(modifier = M.weight(1f)) {
SaveButton(title = "按比例\n拆分茧别") { SaveButton(title = "按比例\n拆分茧别", enabled = false) {
Toasty.showToast("VIP功能,暂未开放")
// weightViewModel.showSplitDialog(false) // weightViewModel.showSplitDialog(false)
} }
} }
@@ -535,7 +534,7 @@ fun AddKindsButton(
) )
Text( Text(
modifier = M.padding(top = 5.dp), 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, color = MyColors.Black,
fontSize = MaterialTheme.typography.bodyMedium.fontSize fontSize = MaterialTheme.typography.bodyMedium.fontSize
) )
@@ -545,15 +544,21 @@ fun AddKindsButton(
} }
@Composable @Composable
fun SaveButton(modifier: Modifier = M, title: String, onClick: () -> Unit) { fun SaveButton(modifier: Modifier = M, title: String, enabled: Boolean = true, onClick: () -> Unit) {
MyCard(colors = MyColors.BlueGreen, modifier = modifier) { MyCard(colors = if (enabled) MyColors.BlueGreen else MyColors.Disabled, modifier = modifier) {
Box(modifier = M.clickable { onClick() }) { Box(modifier = M.clickable {
if (enabled) {
onClick()
} else {
Toasty.showToast("正在开发中,敬请期待")
}
}) {
Text( Text(
modifier = M modifier = M
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp), .padding(vertical = 16.dp, horizontal = 10.dp),
text = title, text = title,
color = MyColors.White, color = if (enabled) MyColors.White else MyColors.Gray,
fontSize = MaterialTheme.typography.bodyMedium.fontSize, fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -51,6 +51,8 @@ object Toasty : BaseViewModel() {
val confirmDialog = _confirmDialog.asStateFlow() val confirmDialog = _confirmDialog.asStateFlow()
private val _inputDialog = MutableStateFlow(InputDialogData()) private val _inputDialog = MutableStateFlow(InputDialogData())
val inputDialog = _inputDialog.asStateFlow() val inputDialog = _inputDialog.asStateFlow()
private val _globalDialog = MutableStateFlow(GlobalDialogData())
val globalDialog = _globalDialog.asStateFlow()
// 控制 Drawer 状态的 Boolean Flow // 控制 Drawer 状态的 Boolean Flow
private val _isDrawerOpen = MutableStateFlow(false) private val _isDrawerOpen = MutableStateFlow(false)
@@ -233,6 +235,17 @@ object Toasty : BaseViewModel() {
_confirmDialog.value = ConfirmDialogData(showDialog = false) _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 = {}
)
@@ -17,6 +17,7 @@ object MyColors {
val Orange = Color(0xFFFFAE00) val Orange = Color(0xFFFFAE00)
val LightGray = Color(0xCCEEEEEE) val LightGray = Color(0xCCEEEEEE)
val Gray = Color(0xFF999999) val Gray = Color(0xFF999999)
val Disabled = Color(0xFFE5E7EB)
val BlueGreen = Color(0xFF2FBAA3) val BlueGreen = Color(0xFF2FBAA3)
val LightBlueGreen = Color(0x802FBAA3) val LightBlueGreen = Color(0x802FBAA3)
val LightLightBlueGreen = Color(0xFFF7FFFE) val LightLightBlueGreen = Color(0xFFF7FFFE)
@@ -33,6 +33,7 @@ class PollingTask private constructor() {
* @param task 需要执行的任务 * @param task 需要执行的任务
*/ */
fun startDelayedTask(taskId: String, delaySeconds: Long, task: Runnable) { fun startDelayedTask(taskId: String, delaySeconds: Long, task: Runnable) {
ensureScheduler()
stopTask(taskId) // 如果已有相同ID的任务,先停止 stopTask(taskId) // 如果已有相同ID的任务,先停止
val future = scheduler!!.schedule({ val future = scheduler!!.schedule({
@@ -99,6 +100,7 @@ class PollingTask private constructor() {
* @param task 需要执行的任务 * @param task 需要执行的任务
*/ */
private fun startPollingTask(taskId: String, intervalSeconds: Long, task: Runnable) { private fun startPollingTask(taskId: String, intervalSeconds: Long, task: Runnable) {
ensureScheduler()
stopTask(taskId) // 如果已有相同ID的任务,先停止 stopTask(taskId) // 如果已有相同ID的任务,先停止
val future = scheduler!!.scheduleWithFixedDelay(task, 0, intervalSeconds, TimeUnit.SECONDS) 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任务接口 // 定义IO任务接口
interface IRxIOTask<T> { interface IRxIOTask<T> {
fun doInIOThread(): T fun doInIOThread(): T
@@ -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('.')
}
}
}
@@ -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.ConverterUtil
import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial
import com.bbitcn.f8.pad.utils.log.MyLog 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.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.UUID import java.util.UUID
import kotlin.experimental.xor import kotlin.experimental.xor
@@ -28,6 +32,7 @@ object UHFReaderG06M_G25M : UHFReaderForSerial() {
private var mRcpBase: RcpBase? = null private var mRcpBase: RcpBase? = null
private var mSioBase: SioCom? = null private var mSioBase: SioCom? = null
private var onCommListener: OnCommListener? = null private var onCommListener: OnCommListener? = null
private val protocolScope = CoroutineScope(SupervisorJob() + Dispatchers.IO.limitedParallelism(1))
private var mBattaryLevel = 0 private var mBattaryLevel = 0
@@ -128,7 +133,7 @@ object UHFReaderG06M_G25M : UHFReaderForSerial() {
Command(RcpMM.RCP_MM_PARA, RcpBase.RCP_MSG_GET) Command(RcpMM.RCP_MM_PARA, RcpBase.RCP_MSG_GET)
mRcpBase?.setOnProtocolListener({ obj, protocolEventArg -> mRcpBase?.setOnProtocolListener({ obj, protocolEventArg ->
doInIoThreadNoDialog { protocolScope.launch {
val psData = protocolEventArg.protocolPacket val psData = protocolEventArg.protocolPacket
when (psData.Code) { when (psData.Code) {
RcpMM.RCP_MM_READ_C_UII -> RcpMM.RCP_MM_READ_C_UII ->
@@ -45,7 +45,13 @@ object RetrofitClient {
try { try {
val response = chain.proceed(chain.request()) 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() val content = response.peekBody(ERROR_BODY_PEEK_BYTES).string()
runCatching { runCatching {
Gson().fromJson(content, CommonResponse::class.java) Gson().fromJson(content, CommonResponse::class.java)
@@ -8,9 +8,13 @@ import com.bbitcn.f8.pad.utils.global.RxTag
import com.bbitcn.f8.pad.utils.log.MyLog import com.bbitcn.f8.pad.utils.log.MyLog
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator import okhttp3.Authenticator
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.Route import okhttp3.Route
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
/** /**
* 401 token refresh interceptor. * 401 token refresh interceptor.
@@ -18,6 +22,20 @@ import okhttp3.Route
class TokenAuthenticator : Authenticator { class TokenAuthenticator : Authenticator {
private val lock = Any() 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? { override fun authenticate(route: Route?, response: Response): Request? {
MyLog.network("TokenAuthenticator: authenticate") MyLog.network("TokenAuthenticator: authenticate")
@@ -45,7 +63,7 @@ class TokenAuthenticator : Authenticator {
val newToken = runBlocking { val newToken = runBlocking {
MyLog.network("刷新Token: $currentToken, $refreshToken") MyLog.network("刷新Token: $currentToken, $refreshToken")
val result = RetrofitClient.apiInterface().refreshToken( val result = refreshApi.refreshToken(
RefreshTokenRequest( RefreshTokenRequest(
hardwareid = Global.getDeviceId(), hardwareid = Global.getDeviceId(),
refToken = refreshToken, refToken = refreshToken,