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
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
}

View File

@@ -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
)
}
}
// 表情图标

View File

@@ -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

View File

@@ -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<String?>(null)
val userMessages = MutableStateFlow<List<String>>(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 = "已连接"
}

View File

@@ -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}")
}
}