refactor: 优化前端UI组件

- 优化聊天相关组件(MessageBubble、MessageList、TypingIndicator)
- 优化对话相关组件(ConversationListHeader、ConversationListItem)
- 优化回忆录相关组件(BookInfoCard、ChapterCard)
- 优化通用组件(EmptyStateView、UserAvatar)
This commit is contained in:
iammm0
2026-01-23 14:02:47 +08:00
parent 09c50bc320
commit 26247c3427
9 changed files with 188 additions and 199 deletions

View File

@@ -67,16 +67,7 @@ fun AIMessageBubble(
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Start horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Start
) { ) {
// 头像 // 消息气泡(暂时不显示头像
Box(
modifier = Modifier
.size(32.dp)
.padding(end = 8.dp)
) {
avatar()
}
// 消息气泡
Card( Card(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)

View File

@@ -69,19 +69,7 @@ fun MessageList(
} }
"assistant" -> { "assistant" -> {
AIMessageBubble( AIMessageBubble(
text = message.content, text = message.content
avatar = {
// AI头像
Box(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
.background(LightPurple),
contentAlignment = Alignment.Center
) {
Text("📖", fontSize = 20.sp)
}
}
) )
} }
} }
@@ -94,24 +82,15 @@ fun MessageList(
if (isStreaming) { if (isStreaming) {
item { item {
AIMessageBubble( AIMessageBubble(
text = streamingText, text = streamingText
avatar = {
Box(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
.background(LightPurple),
contentAlignment = Alignment.Center
) {
Text("📖", fontSize = 20.sp)
}
}
) )
} }
} }
// 正在输入指示器 // 正在输入指示器 - 显示加载动画
if ((isStreaming && streamingText.isEmpty()) || (isTyping && !isStreaming)) { // 1. 如果正在流式接收但还没有内容,显示加载动画
// 2. 如果设置了isTyping且不在流式状态显示加载动画
if (isTyping || (isStreaming && streamingText.isEmpty())) {
item { item {
TypingIndicator() TypingIndicator()
} }

View File

@@ -17,6 +17,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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.huaga.life_echo.ui.theme.LightPurple import com.huaga.life_echo.ui.theme.LightPurple
/** /**
@@ -32,20 +33,7 @@ fun TypingIndicator(
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Start horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Start
) { ) {
// 头像占位 // 消息气泡(暂时不显示头像)
Box(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
.background(LightPurple),
contentAlignment = Alignment.Center
) {
// 可以显示一个图标或占位符
}
Spacer(modifier = Modifier.width(8.dp))
// 消息气泡
Card( Card(
modifier = Modifier modifier = Modifier
.shadow(2.dp, RoundedCornerShape(12.dp)), .shadow(2.dp, RoundedCornerShape(12.dp)),
@@ -116,6 +104,6 @@ private fun Dot(alpha: Float) {
modifier = Modifier modifier = Modifier
.size(8.dp) .size(8.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color.Gray.copy(alpha = alpha)) .background(LightPurple.copy(alpha = alpha))
) )
} }

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.unit.sp
*/ */
@Composable @Composable
fun EmptyStateView( fun EmptyStateView(
title: String? = null,
message: String, message: String,
icon: String = "📭", icon: String = "📭",
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -31,6 +32,16 @@ fun EmptyStateView(
fontSize = 48.sp fontSize = 48.sp
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (title != null) {
Text(
text = title,
fontSize = 20.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
}
Text( Text(
text = message, text = message,
fontSize = 16.sp, fontSize = 16.sp,

View File

@@ -1,7 +1,11 @@
package com.huaga.life_echo.ui.components.conversation package com.huaga.life_echo.ui.components.conversation
import android.annotation.SuppressLint
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.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -11,6 +15,7 @@ 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 com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple import com.huaga.life_echo.ui.theme.LightPurple
/** /**
@@ -18,7 +23,8 @@ import com.huaga.life_echo.ui.theme.LightPurple
*/ */
@Composable @Composable
fun ConversationListHeader( fun ConversationListHeader(
modifier: Modifier = Modifier onCreateConversation: () -> Unit = {},
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
) { ) {
Surface( Surface(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@@ -31,18 +37,28 @@ fun ConversationListHeader(
.padding(top = 16.dp, bottom = 24.dp, start = 16.dp, end = 16.dp), .padding(top = 16.dp, bottom = 24.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Spacer(modifier = Modifier.height(16.dp))
text = "往事拾遗", // 新建对话按钮
fontSize = 24.sp, Button(
fontWeight = FontWeight.Bold, onClick = onCreateConversation,
color = Color.White colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = LightPurple
),
modifier = Modifier.fillMaxWidth()
) {
androidx.compose.material3.Icon(
imageVector = AppIcons.Add,
contentDescription = "新建对话",
modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "用对话,留住珍贵的记忆", text = "新建对话",
fontSize = 14.sp, fontSize = 16.sp,
color = Color.White.copy(alpha = 0.9f) fontWeight = FontWeight.Bold
) )
} }
} }
} }
}

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -29,6 +30,7 @@ import com.huaga.life_echo.utils.TextUtils
fun ConversationListItem( fun ConversationListItem(
conversation: ConversationListItemDto, conversation: ConversationListItemDto,
onClick: () -> Unit, onClick: () -> Unit,
onDelete: (() -> Unit)? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
@@ -38,16 +40,7 @@ fun ConversationListItem(
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 头像 // 对话信息(暂时不显示头像
ConversationAvatar(
avatarUrl = conversation.avatarUrl,
isDefaultAssistant = conversation.isDefaultAssistant,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.width(12.dp))
// 对话信息
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
@@ -98,6 +91,22 @@ fun ConversationListItem(
timestamp = conversation.latestMessageTime, timestamp = conversation.latestMessageTime,
modifier = Modifier.padding(start = 8.dp) modifier = Modifier.padding(start = 8.dp)
) )
// 删除按钮
if (onDelete != null) {
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = { onDelete() },
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = AppIcons.Delete,
contentDescription = "删除",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
}
}
} }
} }

View File

@@ -1,35 +1,21 @@
package com.huaga.life_echo.ui.components.memoir package com.huaga.life_echo.ui.components.memoir
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.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
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 com.huaga.life_echo.network.models.BookDto import com.huaga.life_echo.network.models.BookDto
import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
import com.huaga.life_echo.utils.TimeUtils
/** /**
* 书籍信息卡片(支持书名编辑) * 书籍信息卡片
*/ */
@Composable @Composable
fun BookInfoCard( fun BookInfoCard(
@@ -38,15 +24,6 @@ fun BookInfoCard(
onSubtitleChange: ((String?) -> Unit)? = null, onSubtitleChange: ((String?) -> Unit)? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var isEditingTitle by remember { mutableStateOf(false) }
var editedTitle by remember { mutableStateOf(book.title) }
val keyboardController = LocalSoftwareKeyboardController.current
// 当book变化时更新编辑状态
LaunchedEffect(book.title) {
editedTitle = book.title
}
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
@@ -60,54 +37,14 @@ fun BookInfoCard(
.padding(20.dp), .padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 书名(可编辑) // 书名(只显示,不可编辑)
if (isEditingTitle) {
TextField(
value = editedTitle,
onValueChange = { editedTitle = it },
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
isEditingTitle = false
keyboardController?.hide()
if (editedTitle.isNotBlank()) {
onTitleChange(editedTitle)
} else {
editedTitle = book.title
}
}
),
colors = TextFieldDefaults.colors(
focusedTextColor = MaterialTheme.colorScheme.primary,
unfocusedTextColor = MaterialTheme.colorScheme.primary
),
textStyle = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.Bold
)
)
} else {
Row(
modifier = Modifier
.clickable { isEditingTitle = true }
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = book.title, text = book.title,
fontSize = 32.sp, fontSize = 32.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp)
) )
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = AppIcons.Edit,
contentDescription = "编辑",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
}
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))

View File

@@ -1,5 +1,7 @@
package com.huaga.life_echo.ui.components.memoir package com.huaga.life_echo.ui.components.memoir
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -9,7 +11,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
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
@@ -23,7 +25,7 @@ import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple import com.huaga.life_echo.ui.theme.LightPurple
/** /**
* 章节卡片组件(显示页数 * 章节卡片组件(可展开显示详细内容
*/ */
@Composable @Composable
fun ChapterCard( fun ChapterCard(
@@ -31,6 +33,8 @@ fun ChapterCard(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var isExpanded by remember { mutableStateOf(false) }
val statusText = when (chapter.status) { val statusText = when (chapter.status) {
"completed" -> "已整理" "completed" -> "已整理"
"partial" -> "部分整理" "partial" -> "部分整理"
@@ -40,16 +44,21 @@ fun ChapterCard(
Card( Card(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick() } .shadow(2.dp, RoundedCornerShape(12.dp))
.shadow(2.dp, RoundedCornerShape(12.dp)), .animateContentSize(animationSpec = tween(300)),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface
) )
) { ) {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 章节头部(可点击展开/收起)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { isExpanded = !isExpanded }
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -89,13 +98,44 @@ fun ChapterCard(
) )
} }
// 右箭头 // 展开/收起图标
Icon( Icon(
imageVector = AppIcons.ChevronRight, imageVector = if (isExpanded) AppIcons.ExpandLess else AppIcons.ExpandMore,
contentDescription = "进入", contentDescription = if (isExpanded) "收起" else "展开",
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
// 展开时显示详细内容
if (isExpanded) {
androidx.compose.material3.Divider(
modifier = Modifier.padding(horizontal = 16.dp)
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = chapter.content,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface,
lineHeight = 22.sp
)
Spacer(modifier = Modifier.height(12.dp))
// 查看详情按钮
androidx.compose.material3.Button(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = LightPurple
)
) {
Text("查看详情", color = Color.White)
}
}
}
}
} }
} }

View File

@@ -9,12 +9,17 @@ import androidx.compose.runtime.Composable
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
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.huaga.life_echo.config.AppConfig
import com.huaga.life_echo.ui.icons.AppIcons import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple import com.huaga.life_echo.ui.theme.LightPurple
/** /**
* 用户头像组件(支持占位符) * 用户头像组件(支持占位符和网络图片加载
*/ */
@Composable @Composable
fun UserAvatar( fun UserAvatar(
@@ -22,6 +27,8 @@ fun UserAvatar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: androidx.compose.ui.unit.Dp = 80.dp size: androidx.compose.ui.unit.Dp = 80.dp
) { ) {
val context = LocalContext.current
Box( Box(
modifier = modifier modifier = modifier
.size(size) .size(size)
@@ -29,13 +36,24 @@ fun UserAvatar(
.background(LightPurple.copy(alpha = 0.2f)), .background(LightPurple.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (avatarUrl != null) { if (!avatarUrl.isNullOrBlank()) {
// TODO: 使用Coil或Glide加载网络图片 // 构建完整的图片URL
Icon( val fullUrl = if (avatarUrl.startsWith("http")) {
imageVector = AppIcons.Person, avatarUrl
} else {
"${AppConfig.BASE_URL}$avatarUrl"
}
AsyncImage(
model = ImageRequest.Builder(context)
.data(fullUrl)
.crossfade(true)
.build(),
contentDescription = "用户头像", contentDescription = "用户头像",
tint = LightPurple, contentScale = ContentScale.Crop,
modifier = Modifier.size(size * 0.6f) modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
) )
} else { } else {
Icon( Icon(