From c7fd095c98db3800db21369792ae30f845088016 Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Thu, 28 May 2026 17:15:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=8D=E6=AC=A1=E4=BC=98=E5=8C=96=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/bbitcn/f8/pad/base/BaseList.kt | 62 ++++--- .../com/bbitcn/f8/pad/base/BaseViewModel.kt | 57 +++---- .../com/bbitcn/f8/pad/ui/screen/MainScreen.kt | 159 +++++++++++++++--- .../ui/screen/mainFunc/StatisticsScreen.kt | 33 +++- .../ui/screen/view/common/DatePickerRange.kt | 3 +- 5 files changed, 224 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseList.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseList.kt index 51cf826..b18e99c 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/base/BaseList.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseList.kt @@ -240,6 +240,7 @@ fun MyRefreshTableForCard( ) { val state = rememberPullToRefreshState() val pagerSate = info.loadState.append + val refreshState = info.loadState.refresh LaunchedEffect(pagerSate) { if (pagerSate is LoadState.NotLoading) { onFinishRefresh() @@ -272,30 +273,37 @@ fun MyRefreshTableForCard( } } ) { - LazyVerticalGrid( - modifier = M.fillMaxSize(), - columns = GridCells.Fixed(columns), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - items( - count = info.itemCount, - key = info.itemKey { key(it) }, - contentType = info.itemContentType { "paging-card" } - ) { index -> - info[index]?.let { lineItem -> - Box { - item(lineItem, index) + Box(modifier = M.fillMaxSize()) { + LazyVerticalGrid( + modifier = M.fillMaxSize(), + columns = GridCells.Fixed(columns), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items( + count = info.itemCount, + key = info.itemKey { key(it) }, + contentType = info.itemContentType { "paging-card" } + ) { index -> + info[index]?.let { lineItem -> + Box { + item(lineItem, index) + } + } + } + item(contentType = "paging-footer") { + when (pagerSate) { + is LoadState.NotLoading -> if (pagerSate.endOfPaginationReached) + ListNoMore() + + is LoadState.Loading -> ListLoading() + is LoadState.Error -> ListError(message = pagerSate.error.message.toString()) } } } - item(contentType = "paging-footer") { - when (pagerSate) { - is LoadState.NotLoading -> if (pagerSate.endOfPaginationReached) - ListNoMore() - - is LoadState.Loading -> ListLoading() - is LoadState.Error -> ListError(message = pagerSate.error.message.toString()) + if (refreshState is LoadState.Loading && info.itemCount == 0) { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { + ListLoading() } } } @@ -512,6 +520,7 @@ fun MyRefreshTable( val listState: LazyListState = rememberLazyListState() val state = rememberPullToRefreshState() val pagerSate = info.loadState.append + val refreshState = info.loadState.refresh LaunchedEffect(pagerSate) { if (pagerSate is LoadState.NotLoading) { onFinishRefresh() @@ -553,8 +562,9 @@ fun MyRefreshTable( } } ) { - var selectIndex by rememberSaveable { mutableStateOf(-1) } - LazyColumn(state = listState) { + Box(modifier = M.fillMaxSize()) { + var selectIndex by rememberSaveable { mutableStateOf(-1) } + LazyColumn(state = listState) { items( count = info.itemCount, key = info.itemKey { key(it) }, @@ -647,6 +657,12 @@ fun MyRefreshTable( is LoadState.Error -> ListError(message = pagerSate.error.message.toString()) } } + } + if (refreshState is LoadState.Loading && info.itemCount == 0) { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { + ListLoading() + } + } } } 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 c44daf2..aba394d 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 @@ -54,11 +54,12 @@ open class BaseViewModel : ViewModel() { onUI: (T) -> Unit, ) { viewModelScope.launch { + if (showDialog) { + Toasty.showLoadingDialog(loadingTips) + } + val result = kotlin.runCatching { withContext(Dispatchers.IO) { - if (showDialog) { - Toasty.showLoadingDialog(loadingTips) - } onIO() } } @@ -67,33 +68,31 @@ open class BaseViewModel : ViewModel() { hideLoadingDialog() } - withContext(Dispatchers.Main) { - result.onSuccess { data -> - onUI(data) - }.onFailure { exception -> - // ✅ 如果是协程取消,不处理,只记录日志 - if (exception is CancellationException || exception.cause is SocketException && exception.cause?.message?.contains( - "Socket closed" - ) == true - ) { - 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) - exception.message?.let { - Toasty.error(it) - } - }.also { - // ✅ 最终执行的操作 - onFinish() + result.onSuccess { data -> + onUI(data) + }.onFailure { exception -> + // ✅ 如果是协程取消,不处理,只记录日志 + if (exception is CancellationException || exception.cause is SocketException && exception.cause?.message?.contains( + "Socket closed" + ) == true + ) { + 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) + exception.message?.let { + Toasty.error(it) + } + }.also { + // ✅ 最终执行的操作 + onFinish() } } } diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainScreen.kt index 6daa5b2..2e7e9d0 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainScreen.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainScreen.kt @@ -20,12 +20,13 @@ import androidx.navigation.compose.rememberNavController import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Rect import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import com.bbitcn.f8.pad.M @@ -47,8 +48,8 @@ import com.bbitcn.f8.pad.ui.screen.mainFunc.UserScreen import com.bbitcn.f8.pad.ui.screen.view.Toasty import com.bbitcn.f8.pad.ui.viewmodel.UpdateViewModel import com.cyberecho.ui.screen.ChatDialog +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.xutils.db.annotation.Column @Preview(showBackground = true, widthDp = 1280) @Composable @@ -74,9 +75,43 @@ fun MainScreen( } Box(modifier = M.fillMaxWidth(), contentAlignment = Alignment.BottomStart) { var selectedPage by rememberSaveable { mutableStateOf(0) } + var contentPage by rememberSaveable { mutableStateOf(0) } + var isSwitchingPage by remember { mutableStateOf(false) } + val cachedPages = remember { mutableStateListOf() } val coroutineScope = rememberCoroutineScope() val menu by mainViewModel.menu.collectAsState() + LaunchedEffect(menu, selectedPage) { + if (menu.isEmpty()) { + contentPage = 0 + isSwitchingPage = false + cachedPages.clear() + return@LaunchedEffect + } + + val safeSelectedPage = selectedPage.coerceIn(0, menu.lastIndex) + if (safeSelectedPage != selectedPage) { + selectedPage = safeSelectedPage + return@LaunchedEffect + } + + cachedPages.removeAll { it !in menu.indices } + if (cachedPages.isEmpty()) { + cachedPages.add(safeSelectedPage) + } + + if (contentPage != safeSelectedPage && safeSelectedPage !in cachedPages) { + isSwitchingPage = true + withFrameNanos { } + delay(180) + cachedPages.add(safeSelectedPage) + contentPage = safeSelectedPage + isSwitchingPage = false + } else if (contentPage != safeSelectedPage) { + contentPage = safeSelectedPage + isSwitchingPage = false + } + } Row(modifier = M.fillMaxWidth()) { NavigationRail( containerColor = MyColors.Black, @@ -99,35 +134,37 @@ fun MainScreen( ) }, selected = selectedPage == index, - onClick = { selectedPage = index } - ) - } - } - Box { - var index = -1 - if (menu.isNotEmpty()) { - index = menu[selectedPage].index - } - when (index) { - -1 -> { - Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("正在加载菜单,如长时间未响应请点击重新加载") - MyButton(text = "刷新菜单") { - mainViewModel.refreshMenuPermission() + onClick = { + if (selectedPage != index) { + selectedPage = index + isSwitchingPage = index !in cachedPages + if (index in cachedPages) { + contentPage = index } } } - } - - 0 -> HomeScreen(navController) - 1 -> UserScreen(navController) - 2 -> PurchaseScreen(navController, drawerViewModel = drawerViewModel) - 3 -> FundsScreen(navController, drawerViewModel = drawerViewModel) - 4 -> StatisticsScreen(drawerViewModel = drawerViewModel) - 5 -> DryCoonScreen(navController, drawerViewModel) - 6 -> SettingScreen(navController, updateViewModel = updateViewModel, topInfoViewmodel = topInfoViewmodel){ - mainViewModel.refreshMenuPermission() + ) + } + } + Box(modifier = M.weight(1f).fillMaxSize()) { + val selectedMenu = menu.getOrNull(selectedPage) + if (isSwitchingPage && selectedMenu != null && selectedPage !in cachedPages) { + MainPageLoading(selectedMenu.title) + } else { + cachedPages.forEach { page -> + key(page) { + val pageIndex = menu.getOrNull(page)?.index ?: -1 + MainPageHost(visible = page == contentPage) { + MainPageContent( + index = pageIndex, + navController = navController, + drawerViewModel = drawerViewModel, + updateViewModel = updateViewModel, + topInfoViewmodel = topInfoViewmodel, + mainViewModel = mainViewModel + ) + } + } } } } @@ -208,3 +245,69 @@ fun MainScreen( val chatDialogData by mainViewModel.chatDialogData.collectAsState() ChatDialog(chatDialogData) } + +@Composable +private fun MainPageLoading(title: String) { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = "正在加载$title...", + color = MyColors.Gray, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + } +} + +@Composable +private fun MainPageHost( + visible: Boolean, + content: @Composable () -> Unit +) { + Box( + modifier = if (visible) { + M + .fillMaxSize() + .alpha(1f) + .zIndex(1f) + } else { + M + .size(0.dp) + .alpha(0f) + .zIndex(0f) + } + ) { + content() + } +} + +@Composable +private fun MainPageContent( + index: Int, + navController: NavHostController, + drawerViewModel: DrawerViewModel, + updateViewModel: UpdateViewModel, + topInfoViewmodel: TopInfoViewModel, + mainViewModel: MainViewModel +) { + when (index) { + -1 -> { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("正在加载菜单,如长时间未响应请点击重新加载") + MyButton(text = "刷新菜单") { + mainViewModel.refreshMenuPermission() + } + } + } + } + + 0 -> HomeScreen(navController) + 1 -> UserScreen(navController) + 2 -> PurchaseScreen(navController, drawerViewModel = drawerViewModel) + 3 -> FundsScreen(navController, drawerViewModel = drawerViewModel) + 4 -> StatisticsScreen(drawerViewModel = drawerViewModel) + 5 -> DryCoonScreen(navController, drawerViewModel) + 6 -> SettingScreen(navController, updateViewModel = updateViewModel, topInfoViewmodel = topInfoViewmodel) { + mainViewModel.refreshMenuPermission() + } + } +} 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 c0f4e6e..08bb7ed 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 @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width 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 @@ -45,6 +46,7 @@ import com.bbitcn.f8.pad.ui.screen.view.Toasty import com.bbitcn.f8.pad.ui.screen.view.common.DatePickerRange import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel import com.bbitcn.f8.pad.ui.theme.MyColors +import kotlinx.coroutines.delay import java.text.SimpleDateFormat import java.time.LocalDate import java.util.Locale @@ -69,8 +71,18 @@ fun StatisticsScreen( val ticketMoreDialogData by remember { mutableStateOf(TicketMoreDialogData()) } var curType by remember { mutableStateOf(StatisticsResponse.Data()) } var queryInput by rememberSaveable { mutableStateOf("") } + var queryInputInitialized by rememberSaveable { mutableStateOf(false) } val pager = statisticsViewModel.statisticsPager.collectAsLazyPagingItems() val dateRangeSelectDialogData by statisticsViewModel.dateRangeSelectDialogData.collectAsState() + LaunchedEffect(queryInput) { + if (!queryInputInitialized) { + queryInputInitialized = true + return@LaunchedEffect + } + delay(350) + statisticsViewModel.updateParamsLike(queryInput) + pager.refresh() + } MainFuncFrame { MyCard(colors = MyColors.LightLightBlueGreen) { Row( @@ -79,9 +91,11 @@ fun StatisticsScreen( .padding(10.dp) ) { StatisticsLeft(modifier = M.weight(2f), statisticsViewModel, curType, pager) { - curType = it - statisticsViewModel.updateParamsCocoonType(it.sgtypesysid) - pager.refresh() + if (curType.sgtypesysid != it.sgtypesysid) { + curType = it + statisticsViewModel.updateParamsCocoonType(it.sgtypesysid) + pager.refresh() + } } StatisticsMain( modifier = M.weight(8f), @@ -90,8 +104,6 @@ fun StatisticsScreen( queryInput, { queryInput = it - statisticsViewModel.updateParamsLike(queryInput) - pager.refresh() }, curType.name, { @@ -119,6 +131,7 @@ fun StatisticsLeft( ) { val daysInfo by statisticsViewModel.daysInfo.collectAsState() val statistics by statisticsViewModel.statistics.collectAsState() + val dayFormat = remember { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) } Column( modifier = modifier @@ -131,10 +144,12 @@ fun StatisticsLeft( statisticsViewModel.calDate = it statisticsViewModel.getDaysInfo(it.year.toString(), it.monthValue.toString()) }) { start, end -> - val dateStart = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(start) - val dateEnd = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(end) - statisticsViewModel.refreshDateRange(dateStart to dateEnd) - pager.refresh() + val dateStart = dayFormat.parse(start) + val dateEnd = dayFormat.parse(end) + if (dateStart != null && dateEnd != null) { + statisticsViewModel.refreshDateRange(dateStart to dateEnd) + pager.refresh() + } } val dateRange by statisticsViewModel.dateRange.collectAsState() DateRangePickTextFiled(M.fillMaxWidth(), dateRange = dateRange) { diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/DatePickerRange.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/DatePickerRange.kt index 5224147..54041f5 100644 --- a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/DatePickerRange.kt +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/DatePickerRange.kt @@ -67,6 +67,7 @@ fun DatePickerRange( // 处理拖动选择日期范围 var selectedStartDay by remember { mutableStateOf(-1) } var selectedEndDay by remember { mutableStateOf(-1) } + val importantDaySet = remember(importantDateInfoList) { importantDateInfoList.toSet() } val onClickChangeDate: (date: LocalDate) -> Unit = { date -> currentDate = date @@ -158,7 +159,7 @@ fun DatePickerRange( items(currentDate.lengthOfMonth() + 1) { day -> CalendarDay( day, - importantDateInfoList.contains(day), + importantDaySet.contains(day), rangeType = if (day == selectedStartDay) if (selectedEndDay == -1 || selectedEndDay == selectedStartDay