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),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Start
) {
// 头像
Box(
modifier = Modifier
.size(32.dp)
.padding(end = 8.dp)
) {
avatar()
}
// 消息气泡
// 消息气泡(暂时不显示头像
Card(
modifier = Modifier
.weight(1f)

View File

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

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.huaga.life_echo.ui.theme.LightPurple
/**
@@ -32,20 +33,7 @@ fun TypingIndicator(
.padding(horizontal = 16.dp, vertical = 8.dp),
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(
modifier = Modifier
.shadow(2.dp, RoundedCornerShape(12.dp)),
@@ -116,6 +104,6 @@ private fun Dot(alpha: Float) {
modifier = Modifier
.size(8.dp)
.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
fun EmptyStateView(
title: String? = null,
message: String,
icon: String = "📭",
modifier: Modifier = Modifier
@@ -31,6 +32,16 @@ fun EmptyStateView(
fontSize = 48.sp
)
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 = message,
fontSize = 16.sp,

View File

@@ -1,7 +1,11 @@
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.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.unit.dp
import androidx.compose.ui.unit.sp
import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
/**
@@ -18,7 +23,8 @@ import com.huaga.life_echo.ui.theme.LightPurple
*/
@Composable
fun ConversationListHeader(
modifier: Modifier = Modifier
onCreateConversation: () -> Unit = {},
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
) {
Surface(
modifier = modifier.fillMaxWidth(),
@@ -31,18 +37,28 @@ fun ConversationListHeader(
.padding(top = 16.dp, bottom = 24.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "往事拾遗",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "用对话,留住珍贵的记忆",
fontSize = 14.sp,
color = Color.White.copy(alpha = 0.9f)
)
Spacer(modifier = Modifier.height(16.dp))
// 新建对话按钮
Button(
onClick = onCreateConversation,
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.width(8.dp))
Text(
text = "新建对话",
fontSize = 16.sp,
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.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -29,6 +30,7 @@ import com.huaga.life_echo.utils.TextUtils
fun ConversationListItem(
conversation: ConversationListItemDto,
onClick: () -> Unit,
onDelete: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Row(
@@ -38,16 +40,7 @@ fun ConversationListItem(
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 头像
ConversationAvatar(
avatarUrl = conversation.avatarUrl,
isDefaultAssistant = conversation.isDefaultAssistant,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.width(12.dp))
// 对话信息
// 对话信息(暂时不显示头像
Column(
modifier = Modifier.weight(1f)
) {
@@ -98,6 +91,22 @@ fun ConversationListItem(
timestamp = conversation.latestMessageTime,
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
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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
fun BookInfoCard(
@@ -38,15 +24,6 @@ fun BookInfoCard(
onSubtitleChange: ((String?) -> Unit)? = null,
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(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
@@ -60,54 +37,14 @@ fun BookInfoCard(
.padding(20.dp),
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 = book.title,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = AppIcons.Edit,
contentDescription = "编辑",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
}
// 书名(只显示,不可编辑)
Text(
text = book.title,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp)
)
Spacer(modifier = Modifier.height(4.dp))

View File

@@ -1,5 +1,7 @@
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.clickable
import androidx.compose.foundation.layout.*
@@ -9,7 +11,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
/**
* 章节卡片组件(显示页数
* 章节卡片组件(可展开显示详细内容
*/
@Composable
fun ChapterCard(
@@ -31,6 +33,8 @@ fun ChapterCard(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
var isExpanded by remember { mutableStateOf(false) }
val statusText = when (chapter.status) {
"completed" -> "已整理"
"partial" -> "部分整理"
@@ -40,62 +44,98 @@ fun ChapterCard(
Card(
modifier = modifier
.fillMaxWidth()
.clickable { onClick() }
.shadow(2.dp, RoundedCornerShape(12.dp)),
.shadow(2.dp, RoundedCornerShape(12.dp))
.animateContentSize(animationSpec = tween(300)),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
Column(
modifier = Modifier.fillMaxWidth()
) {
// 浅紫色编号背景
Box(
// 章节头部(可点击展开/收起)
Row(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(8.dp))
.background(LightPurple),
contentAlignment = Alignment.Center
.fillMaxWidth()
.clickable { isExpanded = !isExpanded }
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = String.format("%02d", chapter.order_index),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color.White
// 浅紫色编号背景
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(8.dp))
.background(LightPurple),
contentAlignment = Alignment.Center
) {
Text(
text = String.format("%02d", chapter.order_index),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
Spacer(modifier = Modifier.width(16.dp))
// 章节信息
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = chapter.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = statusText,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// 展开/收起图标
Icon(
imageVector = if (isExpanded) AppIcons.ExpandLess else AppIcons.ExpandMore,
contentDescription = if (isExpanded) "收起" else "展开",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
// 章节信息
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = chapter.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = statusText,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
// 展开时显示详细内容
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)
}
}
}
// 右箭头
Icon(
imageVector = AppIcons.ChevronRight,
contentDescription = "进入",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
}

View File

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