diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ConversationListScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ConversationListScreen.kt index 4e2c7ab..259f72b 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ConversationListScreen.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ConversationListScreen.kt @@ -2,20 +2,38 @@ package com.huaga.life_echo.ui.screens 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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -24,12 +42,14 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.huaga.life_echo.network.models.ConversationListItemDto import com.huaga.life_echo.ui.components.common.EmptyStateView import com.huaga.life_echo.ui.components.common.LoadingIndicator -import com.huaga.life_echo.ui.components.conversation.ConversationListHeader import com.huaga.life_echo.ui.components.conversation.ConversationListItem +import com.huaga.life_echo.ui.icons.AppIcons +import com.huaga.life_echo.ui.theme.LightPurple import com.huaga.life_echo.ui.viewmodel.ConversationListViewModel import com.huaga.life_echo.ui.viewmodel.ViewModelFactory import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConversationListScreen( onConversationClick: (String) -> Unit = {}, @@ -41,6 +61,10 @@ fun ConversationListScreen( val isLoading by viewModel.isLoading.collectAsState() val error by viewModel.error.collectAsState() + // 多选模式状态 + var isSelectionMode by remember { mutableStateOf(false) } + var selectedIds by remember { mutableStateOf(mutableSetOf()) } + // 刷新对话列表 LaunchedEffect(Unit) { viewModel.refreshConversations() @@ -63,25 +87,116 @@ fun ConversationListScreen( } } - Column( - modifier = Modifier.fillMaxSize() - ) { - - // 对话列表区域 + // 处理长按进入多选模式 + val handleLongClick: (String) -> Unit = { conversationId -> + if (!isSelectionMode) { + isSelectionMode = true + selectedIds.add(conversationId) + } + } + + // 处理选择/取消选择 + val handleToggleSelection: (String) -> Unit = { conversationId -> + if (selectedIds.contains(conversationId)) { + selectedIds.remove(conversationId) + if (selectedIds.isEmpty()) { + isSelectionMode = false + } + } else { + selectedIds.add(conversationId) + } + } + + // 全选/取消全选 + val handleSelectAll: () -> Unit = { + if (selectedIds.size == conversations.size) { + selectedIds.clear() + isSelectionMode = false + } else { + selectedIds = conversations.map { it.id }.toMutableSet() + } + } + + // 删除选中的对话 + val handleDeleteSelected: () -> Unit = { + scope.launch { + selectedIds.forEach { id -> + viewModel.deleteConversation(id) + } + selectedIds.clear() + isSelectionMode = false + } + } + + Scaffold( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.statusBars), + topBar = { + TopAppBar( + title = { + Text( + text = if (isSelectionMode) { + "已选择 ${selectedIds.size} 项" + } else { + "我的对话" + }, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + }, + actions = { + if (isSelectionMode) { + // 多选模式下的操作 + TextButton(onClick = handleSelectAll) { + Text( + text = if (selectedIds.size == conversations.size) "取消全选" else "全选", + color = LightPurple + ) + } + IconButton( + onClick = handleDeleteSelected, + enabled = selectedIds.isNotEmpty() + ) { + Icon( + imageVector = AppIcons.Delete, + contentDescription = "删除选中", + tint = if (selectedIds.isNotEmpty()) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + IconButton(onClick = { + selectedIds.clear() + isSelectionMode = false + }) { + Icon( + imageVector = AppIcons.Close, + contentDescription = "取消选择", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + // 正常模式下的操作 + IconButton(onClick = handleCreateConversation) { + Icon( + imageVector = AppIcons.Add, + contentDescription = "新建对话", + tint = LightPurple + ) + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + } + ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() + .padding(paddingValues) .background(MaterialTheme.colorScheme.background) ) { - ConversationListHeader(onCreateConversation = handleCreateConversation) - Text( - text = "我的对话", - modifier = Modifier.padding(16.dp, 16.dp, 16.dp, 8.dp), - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - when { isLoading -> { LoadingIndicator() @@ -98,7 +213,7 @@ fun ConversationListScreen( // 空状态 - 提示用户创建新对话 EmptyStateView( title = "还没有对话", - message = "点击上方「新建对话」按钮开始您的回忆录之旅", + message = "点击右上角「+」按钮开始您的回忆录之旅", modifier = Modifier.fillMaxSize() ) } @@ -121,11 +236,22 @@ fun ConversationListScreen( ) ConversationListItem( conversation = dto, - onClick = { onConversationClick(conversation.id) }, + onClick = { + if (isSelectionMode) { + handleToggleSelection(conversation.id) + } else { + onConversationClick(conversation.id) + } + }, onDelete = { scope.launch { viewModel.deleteConversation(conversation.id) } + }, + isSelected = selectedIds.contains(conversation.id), + isSelectionMode = isSelectionMode, + onLongClick = { + handleLongClick(conversation.id) } ) } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/LoginScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/LoginScreen.kt index 79fa6f4..4c2c88a 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/LoginScreen.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/LoginScreen.kt @@ -30,7 +30,6 @@ import com.huaga.life_echo.ui.components.auth.SmsCodeInput @Composable fun LoginScreen( onLoginSuccess: () -> Unit, - onNavigateToRegister: () -> Unit, onNavigateToResetPassword: (() -> Unit)? = null, onNavigateToTerms: () -> Unit = {}, onNavigateToPrivacy: () -> Unit = {}, @@ -45,6 +44,8 @@ fun LoginScreen( var password by remember { mutableStateOf("") } var passwordVisible by remember { mutableStateOf(false) } var smsCode by remember { mutableStateOf("") } + var nickname by remember { mutableStateOf("") } // 首次登录时的昵称 + var needsNickname by remember { mutableStateOf(false) } // 是否需要输入昵称 var agreedToTerms by remember { mutableStateOf(false) } var showResultDialog by remember { mutableStateOf(false) } @@ -55,10 +56,15 @@ fun LoginScreen( val isLoggedIn by viewModel.isLoggedIn.collectAsState() val smsCountdown by viewModel.smsCountdown.collectAsState() - // 显示操作结果弹窗 + // 显示操作结果弹窗,并检查是否需要输入昵称 LaunchedEffect(operationResult) { operationResult?.let { showResultDialog = true + // 检查是否是首次登录需要昵称 + if (!it.success && (it.details?.contains("首次登录") == true || + it.details?.contains("需要设置昵称") == true)) { + needsNickname = true + } } } @@ -101,7 +107,7 @@ fun LoginScreen( // 标题 Text( - text = "欢迎回来", + text = if (needsNickname) "欢迎加入" else "欢迎", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface @@ -110,7 +116,13 @@ fun LoginScreen( Spacer(modifier = Modifier.height(8.dp)) Text( - text = "登录您的账号以继续", + text = if (needsNickname) { + "设置昵称完成注册" + } else if (!isPasswordMode) { + "使用手机号验证码登录,首次登录将自动注册" + } else { + "登录您的账号以继续" + }, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -165,6 +177,25 @@ fun LoginScreen( Spacer(modifier = Modifier.height(16.dp)) + // 如果需要输入昵称(首次登录),显示昵称输入框 + if (needsNickname && !isPasswordMode) { + OutlinedTextField( + value = nickname, + onValueChange = { + if (it.length <= 50) { + nickname = it + } + }, + label = { Text("昵称") }, + placeholder = { Text("请输入您的昵称(1-50个字符)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(16.dp), + enabled = !isLoading + ) + Spacer(modifier = Modifier.height(16.dp)) + } + // 根据登录模式显示不同的输入框 if (isPasswordMode) { // 密码输入框 @@ -256,6 +287,14 @@ fun LoginScreen( modifier = Modifier.padding(bottom = 8.dp) ) } + if (needsNickname && nickname.isNotEmpty() && nickname.trim().isEmpty()) { + Text( + text = "昵称不能为空", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(bottom = 8.dp) + ) + } Spacer(modifier = Modifier.height(24.dp)) @@ -332,9 +371,14 @@ fun LoginScreen( viewModel.login(trimmedPhone, password, agreedToTerms) } } else { - // 验证码登录 + // 验证码登录/注册 + val trimmedNickname = if (needsNickname) nickname.trim() else null if (trimmedPhone.length == 11 && smsCode.length == 6 && agreedToTerms) { - viewModel.loginWithSms(trimmedPhone, smsCode, agreedToTerms) + // 如果需要昵称但未提供,不执行登录 + if (needsNickname && trimmedNickname.isNullOrBlank()) { + return@Button + } + viewModel.loginWithSms(trimmedPhone, smsCode, agreedToTerms, trimmedNickname) } } }, @@ -342,7 +386,11 @@ fun LoginScreen( .fillMaxWidth() .height(48.dp), enabled = !isLoading && phone.trim().length == 11 && - (if (isPasswordMode) password.length >= 6 else smsCode.length == 6) && + (if (isPasswordMode) { + password.length >= 6 + } else { + smsCode.length == 6 && (!needsNickname || nickname.trim().isNotEmpty()) + }) && agreedToTerms, colors = ButtonDefaults.buttonColors( containerColor = LightPurple, @@ -356,7 +404,7 @@ fun LoginScreen( ) } else { Text( - text = "登录", + text = if (needsNickname) "注册并登录" else "登录", fontSize = 16.sp, fontWeight = FontWeight.Bold ) @@ -378,15 +426,6 @@ fun LoginScreen( Spacer(modifier = Modifier.height(8.dp)) - // 注册链接 - TextButton(onClick = onNavigateToRegister) { - Text( - text = "还没有账号?立即注册", - color = LightPurple, - fontSize = 14.sp - ) - } - } // 操作结果弹窗 @@ -448,11 +487,20 @@ fun LoginScreen( viewModel.clearOperationResult() if (operationResult?.success == true) { // 成功时延迟跳转已在LaunchedEffect中处理 + needsNickname = false + nickname = "" + } else { + // 失败时,如果是需要昵称的错误,不清除needsNickname状态 + if (!(operationResult?.details?.contains("首次登录") == true || + operationResult?.details?.contains("需要设置昵称") == true)) { + needsNickname = false + nickname = "" + } } } ) { Text( - text = if (operationResult?.success == true) "确定" else "重试", + text = if (operationResult?.success == true) "确定" else if (needsNickname) "设置昵称" else "重试", color = if (operationResult?.success == true) LightPurple else MaterialTheme.colorScheme.error ) } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/MyMemoirScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/MyMemoirScreen.kt index bf7a3b0..eca7e1b 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/MyMemoirScreen.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/MyMemoirScreen.kt @@ -48,6 +48,7 @@ fun MyMemoirScreen( ) ) { val chapters by viewModel.chapters.collectAsState(initial = emptyList()) + val chapterDtosFromApi by viewModel.chapterDtos.collectAsState(initial = emptyList()) val selectedChapter by viewModel.selectedChapter.collectAsState() val isLoading by viewModel.isLoading.collectAsState() val bookInfo by viewModel.bookInfo.collectAsState() @@ -78,21 +79,27 @@ fun MyMemoirScreen( viewModel.refreshChapters() } - // 转换Chapter为ChapterDto用于显示 - val chapterDtos = remember(chapters) { - chapters.map { chapter -> - ChapterDto( - id = chapter.id, - title = chapter.title, - content = chapter.content, - order_index = chapter.orderIndex, - status = chapter.status, - category = chapter.category, - images = emptyList(), - updated_at = java.time.Instant.ofEpochMilli(chapter.updatedAt).toString(), - is_new = chapter.isNew, - source_segments = emptyList() - ) + // 转换Chapter为ChapterDto用于显示,优先使用API返回的ChapterDto(包含images) + val chapterDtos = remember(chapters, chapterDtosFromApi) { + if (chapterDtosFromApi.isNotEmpty()) { + // 使用API返回的ChapterDto(包含images信息) + chapterDtosFromApi + } else { + // 如果没有API数据,从本地Chapter创建 + chapters.map { chapter -> + ChapterDto( + id = chapter.id, + title = chapter.title, + content = chapter.content, + order_index = chapter.orderIndex, + status = chapter.status, + category = chapter.category, + images = emptyList(), + updated_at = java.time.Instant.ofEpochMilli(chapter.updatedAt).toString(), + is_new = chapter.isNew, + source_segments = emptyList() + ) + } } } @@ -128,7 +135,8 @@ fun MyMemoirScreen( System.currentTimeMillis() } } ?: System.currentTimeMillis(), - quotes = emptyList() + quotes = emptyList(), + images = dto.images ) }, onBackClick = { viewModel.toggleFullTextReading() } @@ -249,7 +257,8 @@ fun MyMemoirScreen( } } else { // 章节阅读视图 - val chapterContent = remember(selectedChapter) { + val chapterContent = remember(selectedChapter, chapterDtos) { + val chapterDto = chapterDtos.find { it.id == selectedChapter!!.id } ChapterContentDto( id = selectedChapter!!.id, title = selectedChapter!!.title, @@ -259,7 +268,8 @@ fun MyMemoirScreen( category = selectedChapter!!.category, pageCount = null, updatedAt = selectedChapter!!.updatedAt, - quotes = emptyList() + quotes = emptyList(), + images = chapterDto?.images ?: emptyList() ) } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ProfileScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ProfileScreen.kt index c7a05b9..6015ad1 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ProfileScreen.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ProfileScreen.kt @@ -150,51 +150,25 @@ fun ProfileScreen( modifier = Modifier .fillMaxWidth() .windowInsetsPadding(WindowInsets.statusBars) - .padding(top = 16.dp, bottom = 32.dp), + .padding(top = 16.dp, bottom = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { if (isLoggedIn && currentUser != null) { - // 已登录:显示用户信息(暂时不显示头像) - - Text( - text = userProfile?.nickname ?: currentUser!!.nickname, - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // 显示邮箱 - if (!currentUser!!.email.isNullOrBlank()) { - Text( - text = currentUser!!.email ?: "", - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - } - - // 显示手机号 - Text( - text = currentUser!!.phone, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // 使用新的PlanStatusBadge组件 - com.huaga.life_echo.ui.components.profile.PlanStatusBadge( + // 已登录:显示个人信息栏 + com.huaga.life_echo.ui.components.profile.PersonalInfoCard( + nickname = userProfile?.nickname ?: currentUser!!.nickname, + phone = currentUser!!.phone, planName = currentPlan?.displayName ?: when (currentUser!!.subscription_type) { "free" -> "免费体验版" "premium" -> "高级版" "professional" -> "专业版" else -> "免费体验版" - } + }, + onClick = { + navController?.navigate(com.huaga.life_echo.navigation.Screen.PersonalInfo.route) + }, + modifier = Modifier.padding(horizontal = 16.dp) ) - - Spacer(modifier = Modifier.height(16.dp)) } else { // 未登录:显示登录提示 Box( diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt index 982b71d..f9c9bb4 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt @@ -84,10 +84,13 @@ fun ResetPasswordScreen( } } - // 监听成功消息 - LaunchedEffect(successMessage) { - successMessage?.let { - showSuccessDialog = true + // 监听密码重置成功(只监听密码重置操作的成功,不监听发送验证码的成功) + LaunchedEffect(operationResult) { + operationResult?.let { result -> + // 只有当操作成功且消息是"密码重置成功"时才显示对话框 + if (result.success && result.message == "密码重置成功") { + showSuccessDialog = true + } } }