From ca5596a73275fc7201750367ec3eeaa5b178b5e4 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 4 Oct 2026 19:08:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20=E9=99=84=E4=BB=B6=E8=8A=AF?= =?UTF-8?q?=E7=89=87=E6=94=B9=E6=8D=A2=E8=A1=8C=E6=8E=92=E5=B8=83=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E5=A4=9A=E9=99=84=E4=BB=B6=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=A2=AB=E8=A3=81=E5=88=B0=E5=B1=8F=E5=B9=95=E5=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主人 2026-10-04 在主会话里发现「用户消息显示有问题」:带 2~3 个附件的用户消息里, 第一枚芯片就吃掉了整排——服务端存储名是 32 位十六进制 + 扩展名(如 `36a4aba0…5524808.txt`), 一枚约 267dp,而芯片排是「横向可滚的单行 + `widthIn(max = 320.dp)`」,于是第二枚只剩一条边 露在屏幕外。看的人不知道还有别的附件,也看不出那一行可以滚。 改为 `FlowRow` 换行排布(用户消息整排贴右 `alignEnd`),N 枚芯片全部可见;代价是 5 枚会占 2~3 行——比「藏起来」值。去掉随之失效的 `CHIP_ROW_MAX_WIDTH` 与两个 import, 并同步 `ChatScreen` 里待发芯片排的注释(同一处行为)。 实机复核:主会话那条三附件消息现在渲染为三行,三枚芯片与各自的 「TXT · 4 B · 4 字 · 已读入」全部可见(截图 `app/build/e2e-shots/chips-after.png`)。 --- .../top/yeij/cyrene/ui/chat/ChatScreen.kt | 2 +- .../top/yeij/cyrene/ui/chat/MessageBubble.kt | 32 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt index 35170dd..7ce6dc5 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt @@ -363,7 +363,7 @@ private fun InputBar( } } -/** 待发附件的芯片排:横向可滚(5 枚芯片不挤成两行,也不会把输入框顶上去)。 */ +/** 待发附件的芯片排:换行排布(5 枚最多占 3 行;宁可占点高度,也不把附件藏进横向滚动里)。 */ @Composable private fun DraftDocumentChips(documents: List, onRemove: (Int) -> Unit) { DocumentChipRow( diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt index 842a612..0996c50 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt @@ -1,16 +1,15 @@ package top.yeij.cyrene.ui.chat import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.runtime.Composable @@ -41,9 +40,6 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme /** 流式进行中的光标。 */ private const val CURSOR = "▍" -/** 附件芯片排的最大宽度:与气泡同级,避免 5 枚芯片把整行铺满屏幕。 */ -private val CHIP_ROW_MAX_WIDTH = 320.dp - /** * 一条落库消息渲染成一个或多个气泡: * - `mode` 为 `chat`(或缺失——后端当前不落库 mode,一律回落 chat)时按 `

` 切分, @@ -70,7 +66,7 @@ fun MessageBubble(message: HistoryMessageDto) { MessageGroup(isUser = isUser, footer = messageSourceLabel(message, LocalEnumCatalog.current)) { message.thinking?.takeIf { it.isNotBlank() }?.let { ThinkingCard(it) } - DocumentChipRow(chips = message.documents.map(DocumentChips::of)) + DocumentChipRow(chips = message.documents.map(DocumentChips::of), alignEnd = isUser) message.image?.takeIf { it.isNotBlank() }?.let { ChatImage(it) } bubbles.forEachIndexed { index, bubble -> Bubble(bubble, isUser, isLast = index == bubbles.lastIndex) @@ -88,16 +84,21 @@ fun MessageBubble(message: HistoryMessageDto) { @Composable fun PendingBubble(item: PendingMessage) { MessageGroup(isUser = true, footer = "发送中…") { - DocumentChipRow(chips = item.documents.map(DocumentChips::of)) + DocumentChipRow(chips = item.documents.map(DocumentChips::of), alignEnd = true) item.imageUri?.takeIf { it.isNotBlank() }?.let { ChatImage(it) } if (item.text.isNotEmpty()) Bubble(plainBubble(item.text), isUser = true, isLast = true) } } /** - * 附件芯片排:横向可滚(最多 5 枚也不挤成两行),一枚一枚展示 - * 「名字(中略,保住扩展名)/ 类型 · 体积 · 字数 · 状态」。 + * 附件芯片排:**换行排布**——一枚放不下就落到下一行,N 枚都看得见。 * + * 它曾经是「横向可滚的单行 + `widthIn(max = 320.dp)`」,**实机一看就是坏的**(2026-10-04 + * 主人在主会话里发现):服务端存储名是 **32 位十六进制 + 扩展名**(`36a4aba0…5524808.txt`), + * 一枚芯片就能吃掉 ~267dp,于是 320dp 的视口里第二枚只剩一条边露在屏幕外——看的人根本 + * 不知道还有第二个附件,也没人看得出那一行可以滚。换行之后 5 枚会占 2~3 行,比「藏起来」值。 + * + * [alignEnd] = 用户消息(气泡在右侧):整排贴右;否则一枚短芯片会贴在该排左端、看着像错位。 * [onRemove] 非空即带移除按钮——待发位(输入栏上方)要能反悔,历史气泡里则不给。 * * 附件可能是空列表(绝大多数消息都是),此时**不占位、不留空白**。 @@ -106,14 +107,17 @@ fun PendingBubble(item: PendingMessage) { fun DocumentChipRow( chips: List, modifier: Modifier = Modifier, + alignEnd: Boolean = false, onRemove: ((Int) -> Unit)? = null, ) { if (chips.isEmpty()) return - Row( - modifier = modifier - .widthIn(max = CHIP_ROW_MAX_WIDTH) - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(6.dp), + FlowRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy( + space = 6.dp, + alignment = if (alignEnd) Alignment.End else Alignment.Start, + ), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { chips.forEachIndexed { index, chip -> DocumentChip(model = chip, onRemove = onRemove?.let { remove -> { remove(index) } })