From b75205e53d475f0d6293f51a5bd54645505ed6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=9C=A8=E5=9D=A4?= Date: Sun, 18 Jan 2026 15:58:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0Android=20UI=E7=95=8C?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新创建记忆界面,改进用户体验 - 更新个人资料界面,添加更多功能 - 更新应用图标资源 - 更新CreateMemoryViewModel以支持新功能 - 更新ViewModelFactory --- .../com/huaga/life_echo/ui/icons/AppIcons.kt | 23 ++- .../ui/screens/CreateMemoryScreen.kt | 134 ++++++++++-- .../life_echo/ui/screens/ProfileScreen.kt | 193 ++++++++++++++---- .../ui/viewmodel/CreateMemoryViewModel.kt | 40 +++- .../ui/viewmodel/ViewModelFactory.kt | 6 +- 5 files changed, 335 insertions(+), 61 deletions(-) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/icons/AppIcons.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/icons/AppIcons.kt index 4fc6c85..5de7d83 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/icons/AppIcons.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/icons/AppIcons.kt @@ -1,6 +1,11 @@ package com.huaga.life_echo.ui.icons import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Chat +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.automirrored.filled.MenuBook +import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.ui.graphics.vector.ImageVector @@ -11,18 +16,18 @@ import androidx.compose.ui.graphics.vector.ImageVector */ object AppIcons { // 导航栏图标 - val Chat = Icons.Default.Chat - val Memoir = Icons.Default.MenuBook + val Chat = Icons.AutoMirrored.Filled.Chat + val Memoir = Icons.AutoMirrored.Filled.MenuBook val Profile = Icons.Default.Person // 聊天界面图标 - val ArrowBack = Icons.Default.ArrowBack + val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack val Mic = Icons.Default.Mic val MicOff = Icons.Default.MicOff val SentimentSatisfied = Icons.Default.SentimentSatisfied val Add = Icons.Default.Add - val Send = Icons.Default.Send - val Book = Icons.Default.MenuBook + val Send = Icons.AutoMirrored.Filled.Send + val Book = Icons.AutoMirrored.Filled.MenuBook // 对话列表图标 val Conversation = Icons.Default.ChatBubble @@ -31,7 +36,7 @@ object AppIcons { // 回忆录界面图标 val Chapter = Icons.Default.Book - val Reading = Icons.Default.MenuBook + val Reading = Icons.AutoMirrored.Filled.MenuBook val ChevronRight = Icons.Default.ChevronRight val Edit = Icons.Default.Edit val Share = Icons.Default.Share @@ -46,7 +51,7 @@ object AppIcons { val FormatSize = Icons.Default.FormatSize val Brightness2 = Icons.Default.Brightness2 val Settings = Icons.Default.Settings - val Help = Icons.Default.Help + val Help = Icons.AutoMirrored.Filled.Help val Info = Icons.Default.Info // 其他常用图标 @@ -69,4 +74,8 @@ object AppIcons { val Error = Icons.Default.Error val Warning = Icons.Default.Warning val InfoCircle = Icons.Default.Info + + // 认证相关图标 + val Visibility = Icons.Default.Visibility + val VisibilityOff = Icons.Default.VisibilityOff } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt index 35ec79d..1770c8d 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt @@ -1,6 +1,7 @@ package com.huaga.life_echo.ui.screens import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* @@ -12,6 +13,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -35,6 +37,12 @@ fun CreateMemoryScreen( val transcript by viewModel.transcript.collectAsState() val agentResponse by viewModel.agentResponse.collectAsState() val connectionStatus by viewModel.connectionStatus.collectAsState() + val userMessages by viewModel.userMessages.collectAsState() + + // 输入框状态 + var showTextInput by remember { mutableStateOf(false) } + var inputText by remember { mutableStateOf("") } + val keyboardController = LocalSoftwareKeyboardController.current Column( modifier = Modifier @@ -142,6 +150,31 @@ fun CreateMemoryScreen( } } + // 用户文本消息 + userMessages.forEach { message -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.End + ) { + Card( + modifier = Modifier + .weight(1f) + .shadow(2.dp, RoundedCornerShape(12.dp)), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = LightPurple.copy(alpha = 0.2f)) + ) { + Text( + text = message, + modifier = Modifier.padding(12.dp), + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + // 用户消息(转写文本) if (transcript.isNotEmpty()) { Row( @@ -236,21 +269,92 @@ fun CreateMemoryScreen( ) } - // 语音输入提示区域(只读,不支持文本输入) - Box( - modifier = Modifier - .weight(1f) - .height(40.dp) - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)), - contentAlignment = Alignment.CenterStart - ) { - Text( - text = if (isRecording) "正在录音..." else "说点什么...", - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp - ) + // 输入框区域 - 点击弹出文本输入 + if (showTextInput) { + // 显示可编辑的文本输入框 + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = inputText, + onValueChange = { inputText = it }, + modifier = Modifier + .weight(1f) + .height(40.dp), + placeholder = { + Text( + text = "输入消息...", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp + ) + }, + shape = RoundedCornerShape(20.dp), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = Color.Transparent, + focusedBorderColor = LightPurple, + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + singleLine = true, + maxLines = 1 + ) + + // 发送按钮 + if (inputText.isNotBlank()) { + IconButton( + onClick = { + viewModel.sendTextMessage(inputText) + inputText = "" + showTextInput = false + keyboardController?.hide() + } + ) { + Icon( + imageVector = AppIcons.Send, + contentDescription = "发送", + tint = LightPurple, + modifier = Modifier.size(24.dp) + ) + } + } else { + // 关闭按钮(当输入框为空时) + IconButton( + onClick = { + showTextInput = false + keyboardController?.hide() + } + ) { + Icon( + imageVector = AppIcons.Close, + contentDescription = "关闭", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + } + } + } + } else { + // 显示只读提示区域 + Box( + modifier = Modifier + .weight(1f) + .height(40.dp) + .clip(RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) + .clickable { + showTextInput = true + keyboardController?.show() + }, + contentAlignment = Alignment.CenterStart + ) { + Text( + text = if (isRecording) "正在录音..." else "说点什么...", + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp + ) + } } // 表情图标 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 a1ccb22..043817e 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 @@ -35,7 +35,10 @@ import androidx.compose.runtime.setValue import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.TextButton +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -43,18 +46,37 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.huaga.life_echo.data.auth.TokenManager +import com.huaga.life_echo.navigation.Screen import com.huaga.life_echo.ui.icons.AppIcons import com.huaga.life_echo.ui.settings.AppSettings import com.huaga.life_echo.ui.theme.LightPurple +import com.huaga.life_echo.ui.viewmodel.AuthViewModel +import com.huaga.life_echo.ui.viewmodel.ViewModelFactory +import androidx.compose.ui.platform.LocalContext @Composable fun ProfileScreen( navController: androidx.navigation.NavHostController? = null ) { + val context = LocalContext.current + val authViewModel: AuthViewModel = viewModel(factory = ViewModelFactory(context)) + var largeFontMode by remember { mutableStateOf(AppSettings.largeFontMode) } var darkMode by remember { mutableStateOf(AppSettings.darkMode) } var speechRate by remember { mutableStateOf(AppSettings.speechRate) } var showSpeechRateDialog by remember { mutableStateOf(false) } + var showLogoutDialog by remember { mutableStateOf(false) } + + val isLoggedIn by authViewModel.isLoggedIn.collectAsState() + val currentUser by authViewModel.currentUser.collectAsState() + + // 初始化TokenManager + LaunchedEffect(Unit) { + TokenManager.initialize(context) + authViewModel.checkAuthStatus() + } // 应用设置变化 - 立即更新全局设置 LaunchedEffect(largeFontMode) { @@ -121,48 +143,123 @@ fun ProfileScreen( .padding(top = 16.dp, bottom = 32.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // 用户头像 - Box( - modifier = Modifier - .size(80.dp) - .clip(CircleShape) - .background(LightPurple.copy(alpha = 0.2f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = AppIcons.Person, - contentDescription = "用户头像", - tint = LightPurple, - modifier = Modifier.size(48.dp) - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "李明华", - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { + if (isLoggedIn && currentUser != null) { + // 已登录:显示用户信息 + // 用户头像 Box( modifier = Modifier - .size(8.dp) + .size(80.dp) .clip(CircleShape) - .background(LightPurple) - ) - Spacer(modifier = Modifier.width(4.dp)) + .background(LightPurple.copy(alpha = 0.2f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = AppIcons.Person, + contentDescription = "用户头像", + tint = LightPurple, + modifier = Modifier.size(48.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + Text( - text = "免费体验版", - fontSize = 12.sp, + text = currentUser!!.nickname, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(LightPurple) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = when (currentUser!!.subscription_type) { + "free" -> "免费体验版" + "premium" -> "高级版" + "professional" -> "专业版" + else -> "免费体验版" + }, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 登出按钮 + Button( + onClick = { showLogoutDialog = true }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .height(40.dp) + ) { + Text("登出", fontSize = 14.sp) + } + } else { + // 未登录:显示登录提示 + Box( + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .background(LightPurple.copy(alpha = 0.2f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = AppIcons.Person, + contentDescription = "用户头像", + tint = LightPurple, + modifier = Modifier.size(48.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "未登录", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "登录以同步您的数据", + fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) + + Spacer(modifier = Modifier.height(24.dp)) + + // 登录按钮 + Button( + onClick = { + navController?.navigate(Screen.Login.route) + }, + colors = ButtonDefaults.buttonColors( + containerColor = LightPurple + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .height(48.dp) + ) { + Text("登录", fontSize = 16.sp, fontWeight = FontWeight.Bold) + } } } } @@ -263,6 +360,30 @@ fun ProfileScreen( Spacer(modifier = Modifier.height(32.dp)) } } + + // 登出确认对话框 + if (showLogoutDialog) { + AlertDialog( + onDismissRequest = { showLogoutDialog = false }, + title = { Text("确认登出") }, + text = { Text("确定要登出吗?") }, + confirmButton = { + TextButton( + onClick = { + authViewModel.logout() + showLogoutDialog = false + } + ) { + Text("确认", color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { + TextButton(onClick = { showLogoutDialog = false }) { + Text("取消") + } + } + ) + } } @Composable diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt index 4c5c825..a895972 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt @@ -1,7 +1,9 @@ package com.huaga.life_echo.ui.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.huaga.life_echo.data.auth.TokenManager import com.huaga.life_echo.data.repository.ConversationRepository import com.huaga.life_echo.data.repository.ChapterRepository import com.huaga.life_echo.network.WebSocketClient @@ -13,7 +15,8 @@ import kotlinx.coroutines.launch class CreateMemoryViewModel( private val conversationRepository: ConversationRepository, - private val chapterRepository: ChapterRepository + private val chapterRepository: ChapterRepository, + private val context: Context ) : ViewModel() { private val webSocketClient = WebSocketClient() @@ -23,6 +26,11 @@ class CreateMemoryViewModel( val agentResponse = MutableStateFlow("") val connectionStatus = MutableStateFlow("未连接") val conversationId = MutableStateFlow(null) + val userMessages = MutableStateFlow>(emptyList()) // 用户发送的文本消息列表 + + init { + TokenManager.initialize(context) + } fun startConversation() { viewModelScope.launch { @@ -32,7 +40,9 @@ class CreateMemoryViewModel( connectionStatus.value = "连接中..." try { - webSocketClient.connect(convId) { message -> + // 获取访问令牌 + val token = TokenManager.getAccessToken() + webSocketClient.connect(convId, token) { message -> handleWebSocketMessage(message) } } catch (e: Exception) { @@ -61,6 +71,29 @@ class CreateMemoryViewModel( } } + fun sendTextMessage(text: String) { + if (text.isBlank()) return + + viewModelScope.launch { + // 确保已连接 + if (conversationId.value == null) { + startConversation() + } + + conversationId.value?.let { id -> + // 添加到用户消息列表 + userMessages.value = userMessages.value + text + + // 发送文本消息 + try { + webSocketClient.sendTextMessage(text, id) + } catch (e: Exception) { + connectionStatus.value = "发送失败: ${e.message}" + } + } + } + } + private fun handleWebSocketMessage(message: WebSocketMessage) { when (message.type) { MessageType.transcript -> { @@ -69,6 +102,9 @@ class CreateMemoryViewModel( MessageType.agent_response -> { agentResponse.value = message.data["text"] ?: "" } + MessageType.text -> { + // 处理文本消息响应(如果需要) + } MessageType.connect -> { connectionStatus.value = "已连接" } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt index 2bf59a6..3d7a294 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt @@ -30,7 +30,8 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory modelClass.isAssignableFrom(CreateMemoryViewModel::class.java) -> { CreateMemoryViewModel( conversationRepository = conversationRepository, - chapterRepository = chapterRepository + chapterRepository = chapterRepository, + context = context ) as T } modelClass.isAssignableFrom(ConversationListViewModel::class.java) -> { @@ -44,6 +45,9 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory apiService = apiService ) as T } + modelClass.isAssignableFrom(AuthViewModel::class.java) -> { + AuthViewModel(context = context) as T + } else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } }