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.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<String>()) }
// 刷新对话列表
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)
}
)
}

View File

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

View File

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

View File

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

View File

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