refactor: 优化前端功能屏幕

- 优化ConversationListScreen对话列表页面
- 优化LoginScreen登录页面
- 优化MyMemoirScreen我的回忆录页面
- 优化ProfileScreen个人资料页面
- 优化ResetPasswordScreen重置密码页面
This commit is contained in:
iammm0
2026-01-29 10:57:18 +08:00
parent 5508d94e54
commit e0199b13f5
5 changed files with 254 additions and 93 deletions

View File

@@ -2,20 +2,38 @@ package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.LazyColumn
import androidx.compose.foundation.lazy.items 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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
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
@@ -24,12 +42,14 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.huaga.life_echo.network.models.ConversationListItemDto 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.EmptyStateView
import com.huaga.life_echo.ui.components.common.LoadingIndicator 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.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.ConversationListViewModel
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ConversationListScreen( fun ConversationListScreen(
onConversationClick: (String) -> Unit = {}, onConversationClick: (String) -> Unit = {},
@@ -41,6 +61,10 @@ fun ConversationListScreen(
val isLoading by viewModel.isLoading.collectAsState() val isLoading by viewModel.isLoading.collectAsState()
val error by viewModel.error.collectAsState() val error by viewModel.error.collectAsState()
// 多选模式状态
var isSelectionMode by remember { mutableStateOf(false) }
var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) }
// 刷新对话列表 // 刷新对话列表
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refreshConversations() 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background) .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 { when {
isLoading -> { isLoading -> {
LoadingIndicator() LoadingIndicator()
@@ -98,7 +213,7 @@ fun ConversationListScreen(
// 空状态 - 提示用户创建新对话 // 空状态 - 提示用户创建新对话
EmptyStateView( EmptyStateView(
title = "还没有对话", title = "还没有对话",
message = "点击上方「新建对话」按钮开始您的回忆录之旅", message = "点击右上角「+」按钮开始您的回忆录之旅",
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }
@@ -121,11 +236,22 @@ fun ConversationListScreen(
) )
ConversationListItem( ConversationListItem(
conversation = dto, conversation = dto,
onClick = { onConversationClick(conversation.id) }, onClick = {
if (isSelectionMode) {
handleToggleSelection(conversation.id)
} else {
onConversationClick(conversation.id)
}
},
onDelete = { onDelete = {
scope.launch { scope.launch {
viewModel.deleteConversation(conversation.id) viewModel.deleteConversation(conversation.id)
} }
},
isSelected = selectedIds.contains(conversation.id),
isSelectionMode = isSelectionMode,
onLongClick = {
handleLongClick(conversation.id)
} }
) )
} }

View File

@@ -30,7 +30,6 @@ import com.huaga.life_echo.ui.components.auth.SmsCodeInput
@Composable @Composable
fun LoginScreen( fun LoginScreen(
onLoginSuccess: () -> Unit, onLoginSuccess: () -> Unit,
onNavigateToRegister: () -> Unit,
onNavigateToResetPassword: (() -> Unit)? = null, onNavigateToResetPassword: (() -> Unit)? = null,
onNavigateToTerms: () -> Unit = {}, onNavigateToTerms: () -> Unit = {},
onNavigateToPrivacy: () -> Unit = {}, onNavigateToPrivacy: () -> Unit = {},
@@ -45,6 +44,8 @@ fun LoginScreen(
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) } var passwordVisible by remember { mutableStateOf(false) }
var smsCode by remember { mutableStateOf("") } var smsCode by remember { mutableStateOf("") }
var nickname by remember { mutableStateOf("") } // 首次登录时的昵称
var needsNickname by remember { mutableStateOf(false) } // 是否需要输入昵称
var agreedToTerms by remember { mutableStateOf(false) } var agreedToTerms by remember { mutableStateOf(false) }
var showResultDialog by remember { mutableStateOf(false) } var showResultDialog by remember { mutableStateOf(false) }
@@ -55,10 +56,15 @@ fun LoginScreen(
val isLoggedIn by viewModel.isLoggedIn.collectAsState() val isLoggedIn by viewModel.isLoggedIn.collectAsState()
val smsCountdown by viewModel.smsCountdown.collectAsState() val smsCountdown by viewModel.smsCountdown.collectAsState()
// 显示操作结果弹窗 // 显示操作结果弹窗,并检查是否需要输入昵称
LaunchedEffect(operationResult) { LaunchedEffect(operationResult) {
operationResult?.let { operationResult?.let {
showResultDialog = true showResultDialog = true
// 检查是否是首次登录需要昵称
if (!it.success && (it.details?.contains("首次登录") == true ||
it.details?.contains("需要设置昵称") == true)) {
needsNickname = true
}
} }
} }
@@ -101,7 +107,7 @@ fun LoginScreen(
// 标题 // 标题
Text( Text(
text = "欢迎回来", text = if (needsNickname) "欢迎加入" else "欢迎",
fontSize = 28.sp, fontSize = 28.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
@@ -110,7 +116,13 @@ fun LoginScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "登录您的账号以继续", text = if (needsNickname) {
"设置昵称完成注册"
} else if (!isPasswordMode) {
"使用手机号验证码登录,首次登录将自动注册"
} else {
"登录您的账号以继续"
},
fontSize = 14.sp, fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -165,6 +177,25 @@ fun LoginScreen(
Spacer(modifier = Modifier.height(16.dp)) 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) { if (isPasswordMode) {
// 密码输入框 // 密码输入框
@@ -256,6 +287,14 @@ fun LoginScreen(
modifier = Modifier.padding(bottom = 8.dp) 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)) Spacer(modifier = Modifier.height(24.dp))
@@ -332,9 +371,14 @@ fun LoginScreen(
viewModel.login(trimmedPhone, password, agreedToTerms) viewModel.login(trimmedPhone, password, agreedToTerms)
} }
} else { } else {
// 验证码登录 // 验证码登录/注册
val trimmedNickname = if (needsNickname) nickname.trim() else null
if (trimmedPhone.length == 11 && smsCode.length == 6 && agreedToTerms) { 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() .fillMaxWidth()
.height(48.dp), .height(48.dp),
enabled = !isLoading && phone.trim().length == 11 && 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, agreedToTerms,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = LightPurple, containerColor = LightPurple,
@@ -356,7 +404,7 @@ fun LoginScreen(
) )
} else { } else {
Text( Text(
text = "登录", text = if (needsNickname) "注册并登录" else "登录",
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -378,15 +426,6 @@ fun LoginScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// 注册链接
TextButton(onClick = onNavigateToRegister) {
Text(
text = "还没有账号?立即注册",
color = LightPurple,
fontSize = 14.sp
)
}
} }
// 操作结果弹窗 // 操作结果弹窗
@@ -448,11 +487,20 @@ fun LoginScreen(
viewModel.clearOperationResult() viewModel.clearOperationResult()
if (operationResult?.success == true) { if (operationResult?.success == true) {
// 成功时延迟跳转已在LaunchedEffect中处理 // 成功时延迟跳转已在LaunchedEffect中处理
needsNickname = false
nickname = ""
} else {
// 失败时如果是需要昵称的错误不清除needsNickname状态
if (!(operationResult?.details?.contains("首次登录") == true ||
operationResult?.details?.contains("需要设置昵称") == true)) {
needsNickname = false
nickname = ""
}
} }
} }
) { ) {
Text( 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 color = if (operationResult?.success == true) LightPurple else MaterialTheme.colorScheme.error
) )
} }

View File

@@ -48,6 +48,7 @@ fun MyMemoirScreen(
) )
) { ) {
val chapters by viewModel.chapters.collectAsState(initial = emptyList()) val chapters by viewModel.chapters.collectAsState(initial = emptyList())
val chapterDtosFromApi by viewModel.chapterDtos.collectAsState(initial = emptyList())
val selectedChapter by viewModel.selectedChapter.collectAsState() val selectedChapter by viewModel.selectedChapter.collectAsState()
val isLoading by viewModel.isLoading.collectAsState() val isLoading by viewModel.isLoading.collectAsState()
val bookInfo by viewModel.bookInfo.collectAsState() val bookInfo by viewModel.bookInfo.collectAsState()
@@ -78,8 +79,13 @@ fun MyMemoirScreen(
viewModel.refreshChapters() viewModel.refreshChapters()
} }
// 转换Chapter为ChapterDto用于显示 // 转换Chapter为ChapterDto用于显示优先使用API返回的ChapterDto包含images
val chapterDtos = remember(chapters) { val chapterDtos = remember(chapters, chapterDtosFromApi) {
if (chapterDtosFromApi.isNotEmpty()) {
// 使用API返回的ChapterDto包含images信息
chapterDtosFromApi
} else {
// 如果没有API数据从本地Chapter创建
chapters.map { chapter -> chapters.map { chapter ->
ChapterDto( ChapterDto(
id = chapter.id, id = chapter.id,
@@ -95,6 +101,7 @@ fun MyMemoirScreen(
) )
} }
} }
}
AnimatedContent( AnimatedContent(
targetState = showFullTextReading, targetState = showFullTextReading,
@@ -128,7 +135,8 @@ fun MyMemoirScreen(
System.currentTimeMillis() System.currentTimeMillis()
} }
} ?: System.currentTimeMillis(), } ?: System.currentTimeMillis(),
quotes = emptyList() quotes = emptyList(),
images = dto.images
) )
}, },
onBackClick = { viewModel.toggleFullTextReading() } onBackClick = { viewModel.toggleFullTextReading() }
@@ -249,7 +257,8 @@ fun MyMemoirScreen(
} }
} else { } else {
// 章节阅读视图 // 章节阅读视图
val chapterContent = remember(selectedChapter) { val chapterContent = remember(selectedChapter, chapterDtos) {
val chapterDto = chapterDtos.find { it.id == selectedChapter!!.id }
ChapterContentDto( ChapterContentDto(
id = selectedChapter!!.id, id = selectedChapter!!.id,
title = selectedChapter!!.title, title = selectedChapter!!.title,
@@ -259,7 +268,8 @@ fun MyMemoirScreen(
category = selectedChapter!!.category, category = selectedChapter!!.category,
pageCount = null, pageCount = null,
updatedAt = selectedChapter!!.updatedAt, updatedAt = selectedChapter!!.updatedAt,
quotes = emptyList() quotes = emptyList(),
images = chapterDto?.images ?: emptyList()
) )
} }

View File

@@ -150,51 +150,25 @@ fun ProfileScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.windowInsetsPadding(WindowInsets.statusBars) .windowInsetsPadding(WindowInsets.statusBars)
.padding(top = 16.dp, bottom = 32.dp), .padding(top = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (isLoggedIn && currentUser != null) { if (isLoggedIn && currentUser != null) {
// 已登录:显示用户信息(暂时不显示头像) // 已登录:显示个人信息栏
com.huaga.life_echo.ui.components.profile.PersonalInfoCard(
Text( nickname = userProfile?.nickname ?: currentUser!!.nickname,
text = userProfile?.nickname ?: currentUser!!.nickname, phone = currentUser!!.phone,
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(
planName = currentPlan?.displayName ?: when (currentUser!!.subscription_type) { planName = currentPlan?.displayName ?: when (currentUser!!.subscription_type) {
"free" -> "免费体验版" "free" -> "免费体验版"
"premium" -> "高级版" "premium" -> "高级版"
"professional" -> "专业版" "professional" -> "专业版"
else -> "免费体验版" 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 { } else {
// 未登录:显示登录提示 // 未登录:显示登录提示
Box( Box(

View File

@@ -84,12 +84,15 @@ fun ResetPasswordScreen(
} }
} }
// 监听成功消息 // 监听密码重置成功(只监听密码重置操作的成功,不监听发送验证码的成功)
LaunchedEffect(successMessage) { LaunchedEffect(operationResult) {
successMessage?.let { operationResult?.let { result ->
// 只有当操作成功且消息是"密码重置成功"时才显示对话框
if (result.success && result.message == "密码重置成功") {
showSuccessDialog = true showSuccessDialog = true
} }
} }
}
Scaffold( Scaffold(
topBar = { topBar = {