package com.taskttl.presentation.onboarding import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.taskttl.core.routes.Routes import com.taskttl.data.local.model.OnboardingPage import com.taskttl.data.state.OnboardingEvent import com.taskttl.data.viewmodel.OnboardingViewModel import kotlinx.coroutines.flow.collectLatest import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import taskttl.composeapp.generated.resources.Res import taskttl.composeapp.generated.resources.continue_text import taskttl.composeapp.generated.resources.get_start_text import taskttl.composeapp.generated.resources.skip_text /** * 引导视图 * @param [navigatorToRoute] 导航到路线 * @param [viewModel] 视图模型 */ @Preview @Composable fun OnboardingScreen( navigatorToRoute: (Routes) -> Unit, viewModel: OnboardingViewModel = koinViewModel() ) { val onboardingPages = OnboardingPage.entries val pagerState = rememberPagerState(0, initialPageOffsetFraction = 0f, pageCount = { onboardingPages.size }) LaunchedEffect(Unit) { viewModel.events.collectLatest { event -> when (event) { is OnboardingEvent.NextPage -> { pagerState.animateScrollToPage(pagerState.currentPage + 1) } is OnboardingEvent.NavMain -> { navigatorToRoute(Routes.Main) } } } } Box(modifier = Modifier.fillMaxSize()) { // 右上角跳过 TextButton( onClick = { viewModel.markOnboardingCompleted() }, modifier = Modifier .align(Alignment.TopEnd) .padding(top = 32.dp, end = 16.dp) ) { Text( stringResource(Res.string.skip_text) + ">", fontSize = 16.sp, color = Color.Black.copy(alpha = 0.6f) ) } Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 30.dp, vertical = 40.dp), horizontalAlignment = Alignment.CenterHorizontally ) { HorizontalPager( state = pagerState, modifier = Modifier .weight(1f) .fillMaxWidth(), ) { page -> OnboardingPage(pageData = onboardingPages[page]) } Spacer(modifier = Modifier.height(20.dp)) // 圆点指示器 Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { repeat(onboardingPages.size) { index -> IndicatorDot(active = pagerState.currentPage == index) } } Spacer(modifier = Modifier.height(30.dp)) // 底部按钮 if (pagerState.currentPage < onboardingPages.lastIndex) { Button( onClick = { if (pagerState.currentPage < onboardingPages.lastIndex) { viewModel.sendEvent(OnboardingEvent.NextPage) } }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF667EEA)), shape = MaterialTheme.shapes.medium ) { Text( stringResource(Res.string.continue_text), color = Color.White, fontSize = 18.sp ) } } else { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { Button( onClick = { viewModel.markOnboardingCompleted() }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF667EEA)), shape = MaterialTheme.shapes.medium ) { Text( stringResource(Res.string.get_start_text), color = Color.White, fontSize = 18.sp ) } } } } } } @Composable fun OnboardingPage(pageData: OnboardingPage) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Icon( imageVector = pageData.icon, contentDescription = null, tint = pageData.color, modifier = Modifier .size(200.dp) .padding(bottom = 30.dp) ) Text( text = stringResource(pageData.titleRes), fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color(0xFF333333), textAlign = TextAlign.Center, modifier = Modifier.padding(bottom = 20.dp) ) Text( text = stringResource(pageData.descRes), fontSize = 16.sp, color = Color(0xFF666666), lineHeight = 24.sp, textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 16.dp) ) } } @Composable fun IndicatorDot(active: Boolean) { Box( modifier = Modifier .size(12.dp) .background( color = if (active) Color(0xFF667EEA) else Color(0xFFDDDDDD), shape = CircleShape ) ) }