Merge remote-tracking branch 'origin/development' into development

This commit is contained in:
Kevin
2026-03-11 15:21:08 +08:00
9 changed files with 309 additions and 59 deletions

View File

@@ -11,9 +11,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.layout.onSizeChanged
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -25,6 +30,7 @@ import com.huaga.life_echo.ui.theme.AppTypography
import com.huaga.life_echo.ui.theme.LightPurple
import com.huaga.life_echo.utils.TimeUtils
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* 消息列表组件
@@ -52,7 +58,9 @@ fun MessageList(
audioDurations: Map<String, Int> = emptyMap() // messageId -> 时长(秒)
) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
var lastHeightPx by remember { mutableIntStateOf(0) }
// 计算实际的列表项数量(考虑分割消息和附加项)
val estimatedItemCount = remember(messages, isStreaming, streamingText, isTyping) {
var count = 0
@@ -110,7 +118,25 @@ fun MessageList(
LazyColumn(
state = listState,
modifier = modifier.fillMaxSize(),
modifier = modifier
.fillMaxSize()
.onSizeChanged { size ->
val h = size.height
// 键盘弹出导致列表高度变矮时,滚到最底部,让最后一条气泡紧贴输入框上方
if (lastHeightPx > 0 && h < lastHeightPx) {
scope.launch {
delay(80)
val count = listState.layoutInfo.totalItemsCount
if (count > 0) {
val viewportHeight = listState.layoutInfo.viewportSize.height
// 一次平滑滚到底:用 scrollOffset 让最后一项贴底,避免两段滚动和中间停顿
val scrollOffset = (viewportHeight - 120).coerceAtLeast(0)
listState.animateScrollToItem(count - 1, scrollOffset)
}
}
}
lastHeightPx = h
},
contentPadding = PaddingValues(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {

View File

@@ -14,12 +14,15 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -37,8 +40,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -54,6 +59,7 @@ import com.huaga.life_echo.ui.theme.AppWhite
import com.huaga.life_echo.ui.theme.Cream
import com.huaga.life_echo.ui.theme.DeepPurple
import com.huaga.life_echo.ui.theme.DividerColor
import com.huaga.life_echo.ui.theme.Lavender
import com.huaga.life_echo.ui.theme.MediumPurple
import com.huaga.life_echo.ui.theme.SlatePurple
import com.huaga.life_echo.ui.viewmodel.ConversationListViewModel
@@ -77,33 +83,11 @@ fun ConversationListScreen(
var isSelectionMode by remember { mutableStateOf(false) }
var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) }
// 是否正在自动创建对话
var isAutoCreating by remember { mutableStateOf(false) }
// 是否正在创建对话(点击「打个招呼」)
var isCreating by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
// 加载完成后,当对话列表为空时,自动创建一个对话并进入
LaunchedEffect(conversations, isLoading, isAutoCreating, hasLoadedInitialConversations, error) {
if (
hasLoadedInitialConversations &&
!isLoading &&
error == null &&
conversations.isEmpty() &&
!isAutoCreating
) {
isAutoCreating = true
val result = viewModel.createConversation()
result.fold(
onSuccess = { conversationId ->
onConversationClick(conversationId)
},
onFailure = { exception ->
isAutoCreating = false
}
)
}
}
// 处理长按进入多选模式
val handleLongClick: (String) -> Unit = { conversationId ->
if (!isSelectionMode) {
@@ -192,22 +176,43 @@ fun ConversationListScreen(
modifier = Modifier.fillMaxSize()
)
}
conversations.isEmpty() -> {
if (isAutoCreating) {
LoadingIndicator()
} else {
EmptyStateView(
title = "正在初始化",
message = "正在为您准备回忆录对话...",
modifier = Modifier.fillMaxSize()
)
}
}
else -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = AppDimensions.screenPadding)
) {
// 置顶入口:打个招呼(点击即新建空会话)
item(key = "say_hi") {
SayHiEntry(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = !isCreating) {
if (isCreating) return@clickable
scope.launch {
isCreating = true
viewModel.createConversation()
.fold(
onSuccess = { conversationId ->
onConversationClick(conversationId)
},
onFailure = { }
)
isCreating = false
}
}
.padding(
horizontal = AppDimensions.screenPadding,
vertical = AppDimensions.itemSpacing
),
isLoading = isCreating
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = AppDimensions.screenPadding),
thickness = AppDimensions.dividerThickness,
color = DividerColor
)
}
// 区块标题(与章节正文字号一致,大字模式更易读)
item {
Text(
@@ -224,7 +229,7 @@ fun ConversationListScreen(
// 对话列表
items(conversations, key = { it.id }) { conversation ->
// 兼容新旧数据:将"岁月知己"和旧名称"回忆录助手"都识别为默认助手
// 兼容新旧数据:将"岁月知己"和旧名称"回忆录助手"都识别为默认助手,展示为「岁月知己」
val isAssistant = conversation.title == null || conversation.title == "岁月知己" || conversation.title == "回忆录助手"
val displayTitle = if (isAssistant) "岁月知己" else conversation.title!!
val dto = ConversationListItemDto(
@@ -284,6 +289,68 @@ fun ConversationListScreen(
}
}
/**
* 置顶入口:「打个招呼」— 点击即新建空会话
*/
@Composable
private fun SayHiEntry(
modifier: Modifier = Modifier,
isLoading: Boolean = false
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(AppDimensions.avatarSizeSmall)
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
.background(Lavender),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = AppIcons.Conversation,
contentDescription = "打个招呼",
tint = DeepPurple,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "打个招呼",
fontSize = AppTypography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = DeepPurple,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "开始新对话",
fontSize = AppTypography.bodyMedium,
color = SlatePurple,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MediumPurple,
strokeWidth = 2.dp
)
} else {
Icon(
imageVector = AppIcons.ChevronRight,
contentDescription = null,
tint = SlatePurple,
modifier = Modifier.size(AppDimensions.iconSizeSmall)
)
}
}
}
/**
* 多选模式头部
*/

View File

@@ -152,7 +152,12 @@ fun CreateMemoryScreen(
contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier.fillMaxSize()
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.imePadding()
) {
Column(
modifier = Modifier.fillMaxSize()
) {
@@ -200,7 +205,6 @@ fun CreateMemoryScreen(
// 使用新的ChatInputField组件支持语音输入
ChatInputField(
modifier = Modifier.imePadding(),
value = inputText,
onValueChange = {
conversationDrafts = ConversationDrafts.updateDraft(