feat: 更新Android UI界面

- 更新创建记忆界面,改进用户体验
- 更新个人资料界面,添加更多功能
- 更新应用图标资源
- 更新CreateMemoryViewModel以支持新功能
- 更新ViewModelFactory
This commit is contained in:
徐在坤
2026-01-18 15:58:00 +08:00
parent 2253af73a8
commit b75205e53d
5 changed files with 335 additions and 61 deletions

View File

@@ -1,6 +1,11 @@
package com.huaga.life_echo.ui.icons package com.huaga.life_echo.ui.icons
import androidx.compose.material.icons.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.filled.*
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@@ -11,18 +16,18 @@ import androidx.compose.ui.graphics.vector.ImageVector
*/ */
object AppIcons { object AppIcons {
// 导航栏图标 // 导航栏图标
val Chat = Icons.Default.Chat val Chat = Icons.AutoMirrored.Filled.Chat
val Memoir = Icons.Default.MenuBook val Memoir = Icons.AutoMirrored.Filled.MenuBook
val Profile = Icons.Default.Person val Profile = Icons.Default.Person
// 聊天界面图标 // 聊天界面图标
val ArrowBack = Icons.Default.ArrowBack val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack
val Mic = Icons.Default.Mic val Mic = Icons.Default.Mic
val MicOff = Icons.Default.MicOff val MicOff = Icons.Default.MicOff
val SentimentSatisfied = Icons.Default.SentimentSatisfied val SentimentSatisfied = Icons.Default.SentimentSatisfied
val Add = Icons.Default.Add val Add = Icons.Default.Add
val Send = Icons.Default.Send val Send = Icons.AutoMirrored.Filled.Send
val Book = Icons.Default.MenuBook val Book = Icons.AutoMirrored.Filled.MenuBook
// 对话列表图标 // 对话列表图标
val Conversation = Icons.Default.ChatBubble val Conversation = Icons.Default.ChatBubble
@@ -31,7 +36,7 @@ object AppIcons {
// 回忆录界面图标 // 回忆录界面图标
val Chapter = Icons.Default.Book val Chapter = Icons.Default.Book
val Reading = Icons.Default.MenuBook val Reading = Icons.AutoMirrored.Filled.MenuBook
val ChevronRight = Icons.Default.ChevronRight val ChevronRight = Icons.Default.ChevronRight
val Edit = Icons.Default.Edit val Edit = Icons.Default.Edit
val Share = Icons.Default.Share val Share = Icons.Default.Share
@@ -46,7 +51,7 @@ object AppIcons {
val FormatSize = Icons.Default.FormatSize val FormatSize = Icons.Default.FormatSize
val Brightness2 = Icons.Default.Brightness2 val Brightness2 = Icons.Default.Brightness2
val Settings = Icons.Default.Settings val Settings = Icons.Default.Settings
val Help = Icons.Default.Help val Help = Icons.AutoMirrored.Filled.Help
val Info = Icons.Default.Info val Info = Icons.Default.Info
// 其他常用图标 // 其他常用图标
@@ -69,4 +74,8 @@ object AppIcons {
val Error = Icons.Default.Error val Error = Icons.Default.Error
val Warning = Icons.Default.Warning val Warning = Icons.Default.Warning
val InfoCircle = Icons.Default.Info val InfoCircle = Icons.Default.Info
// 认证相关图标
val Visibility = Icons.Default.Visibility
val VisibilityOff = Icons.Default.VisibilityOff
} }

View File

@@ -1,6 +1,7 @@
package com.huaga.life_echo.ui.screens package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -12,6 +13,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -35,6 +37,12 @@ fun CreateMemoryScreen(
val transcript by viewModel.transcript.collectAsState() val transcript by viewModel.transcript.collectAsState()
val agentResponse by viewModel.agentResponse.collectAsState() val agentResponse by viewModel.agentResponse.collectAsState()
val connectionStatus by viewModel.connectionStatus.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( Column(
modifier = Modifier 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()) { if (transcript.isNotEmpty()) {
Row( Row(
@@ -236,21 +269,92 @@ fun CreateMemoryScreen(
) )
} }
// 语音输入提示区域(只读,不支持文本输入 // 输入框区域 - 点击弹出文本输入
Box( if (showTextInput) {
modifier = Modifier // 显示可编辑的文本输入框
.weight(1f) Row(
.height(40.dp) modifier = Modifier.weight(1f),
.clip(RoundedCornerShape(20.dp)) verticalAlignment = Alignment.CenterVertically
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)), ) {
contentAlignment = Alignment.CenterStart OutlinedTextField(
) { value = inputText,
Text( onValueChange = { inputText = it },
text = if (isRecording) "正在录音..." else "说点什么...", modifier = Modifier
modifier = Modifier.padding(horizontal = 16.dp), .weight(1f)
color = MaterialTheme.colorScheme.onSurfaceVariant, .height(40.dp),
fontSize = 14.sp 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
)
}
} }
// 表情图标 // 表情图标

View File

@@ -35,7 +35,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.icons.AppIcons
import com.huaga.life_echo.ui.settings.AppSettings import com.huaga.life_echo.ui.settings.AppSettings
import com.huaga.life_echo.ui.theme.LightPurple 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 @Composable
fun ProfileScreen( fun ProfileScreen(
navController: androidx.navigation.NavHostController? = null navController: androidx.navigation.NavHostController? = null
) { ) {
val context = LocalContext.current
val authViewModel: AuthViewModel = viewModel(factory = ViewModelFactory(context))
var largeFontMode by remember { mutableStateOf(AppSettings.largeFontMode) } var largeFontMode by remember { mutableStateOf(AppSettings.largeFontMode) }
var darkMode by remember { mutableStateOf(AppSettings.darkMode) } var darkMode by remember { mutableStateOf(AppSettings.darkMode) }
var speechRate by remember { mutableStateOf(AppSettings.speechRate) } var speechRate by remember { mutableStateOf(AppSettings.speechRate) }
var showSpeechRateDialog by remember { mutableStateOf(false) } 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) { LaunchedEffect(largeFontMode) {
@@ -121,48 +143,123 @@ fun ProfileScreen(
.padding(top = 16.dp, bottom = 32.dp), .padding(top = 16.dp, bottom = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 用户头像 if (isLoggedIn && currentUser != null) {
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
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(8.dp) .size(80.dp)
.clip(CircleShape) .clip(CircleShape)
.background(LightPurple) .background(LightPurple.copy(alpha = 0.2f)),
) contentAlignment = Alignment.Center
Spacer(modifier = Modifier.width(4.dp)) ) {
Icon(
imageVector = AppIcons.Person,
contentDescription = "用户头像",
tint = LightPurple,
modifier = Modifier.size(48.dp)
)
}
Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "免费体验版", text = currentUser!!.nickname,
fontSize = 12.sp, 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 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)) 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 @Composable

View File

@@ -1,7 +1,9 @@
package com.huaga.life_echo.ui.viewmodel package com.huaga.life_echo.ui.viewmodel
import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.ConversationRepository
import com.huaga.life_echo.data.repository.ChapterRepository import com.huaga.life_echo.data.repository.ChapterRepository
import com.huaga.life_echo.network.WebSocketClient import com.huaga.life_echo.network.WebSocketClient
@@ -13,7 +15,8 @@ import kotlinx.coroutines.launch
class CreateMemoryViewModel( class CreateMemoryViewModel(
private val conversationRepository: ConversationRepository, private val conversationRepository: ConversationRepository,
private val chapterRepository: ChapterRepository private val chapterRepository: ChapterRepository,
private val context: Context
) : ViewModel() { ) : ViewModel() {
private val webSocketClient = WebSocketClient() private val webSocketClient = WebSocketClient()
@@ -23,6 +26,11 @@ class CreateMemoryViewModel(
val agentResponse = MutableStateFlow("") val agentResponse = MutableStateFlow("")
val connectionStatus = MutableStateFlow("未连接") val connectionStatus = MutableStateFlow("未连接")
val conversationId = MutableStateFlow<String?>(null) val conversationId = MutableStateFlow<String?>(null)
val userMessages = MutableStateFlow<List<String>>(emptyList()) // 用户发送的文本消息列表
init {
TokenManager.initialize(context)
}
fun startConversation() { fun startConversation() {
viewModelScope.launch { viewModelScope.launch {
@@ -32,7 +40,9 @@ class CreateMemoryViewModel(
connectionStatus.value = "连接中..." connectionStatus.value = "连接中..."
try { try {
webSocketClient.connect(convId) { message -> // 获取访问令牌
val token = TokenManager.getAccessToken()
webSocketClient.connect(convId, token) { message ->
handleWebSocketMessage(message) handleWebSocketMessage(message)
} }
} catch (e: Exception) { } 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) { private fun handleWebSocketMessage(message: WebSocketMessage) {
when (message.type) { when (message.type) {
MessageType.transcript -> { MessageType.transcript -> {
@@ -69,6 +102,9 @@ class CreateMemoryViewModel(
MessageType.agent_response -> { MessageType.agent_response -> {
agentResponse.value = message.data["text"] ?: "" agentResponse.value = message.data["text"] ?: ""
} }
MessageType.text -> {
// 处理文本消息响应(如果需要)
}
MessageType.connect -> { MessageType.connect -> {
connectionStatus.value = "已连接" connectionStatus.value = "已连接"
} }

View File

@@ -30,7 +30,8 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
modelClass.isAssignableFrom(CreateMemoryViewModel::class.java) -> { modelClass.isAssignableFrom(CreateMemoryViewModel::class.java) -> {
CreateMemoryViewModel( CreateMemoryViewModel(
conversationRepository = conversationRepository, conversationRepository = conversationRepository,
chapterRepository = chapterRepository chapterRepository = chapterRepository,
context = context
) as T ) as T
} }
modelClass.isAssignableFrom(ConversationListViewModel::class.java) -> { modelClass.isAssignableFrom(ConversationListViewModel::class.java) -> {
@@ -44,6 +45,9 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
apiService = apiService apiService = apiService
) as T ) as T
} }
modelClass.isAssignableFrom(AuthViewModel::class.java) -> {
AuthViewModel(context = context) as T
}
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
} }
} }