fix(chat): 附件芯片改换行排布 —— 多附件不再被裁到屏幕外

主人 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`)。
This commit is contained in:
AskaEth committed 2026-10-04 19:08:26 +08:00
1 parent 2369dd2115
commit ca5596a732
2 files changed
+19 -15

No files matched your search

@@ -363,7 +363,7 @@ private fun InputBar(
} }
} }
/** 待发附件的芯片排:横向可滚(5 枚芯片不挤成两行,也不会把输入框顶上去)。 */ /** 待发附件的芯片排:换行排布(5 枚最多占 3 行;宁可占点高度,也不把附件藏进横向滚动里)。 */
@Composable @Composable
private fun DraftDocumentChips(documents: List<DraftDocument>, onRemove: (Int) -> Unit) { private fun DraftDocumentChips(documents: List<DraftDocument>, onRemove: (Int) -> Unit) {
DocumentChipRow( DocumentChipRow(
@@ -1,16 +1,15 @@
package top.yeij.cyrene.ui.chat package top.yeij.cyrene.ui.chat
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -41,9 +40,6 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
/** 流式进行中的光标。 */ /** 流式进行中的光标。 */
private const val CURSOR = "▍" private const val CURSOR = "▍"
/** 附件芯片排的最大宽度:与气泡同级,避免 5 枚芯片把整行铺满屏幕。 */
private val CHIP_ROW_MAX_WIDTH = 320.dp
/** /**
* 一条落库消息渲染成一个或多个气泡: * 一条落库消息渲染成一个或多个气泡:
* - `mode` 为 `chat`(或缺失——后端当前不落库 mode,一律回落 chat)时按 `<p>` 切分, * - `mode` 为 `chat`(或缺失——后端当前不落库 mode,一律回落 chat)时按 `<p>` 切分,
@@ -70,7 +66,7 @@ fun MessageBubble(message: HistoryMessageDto) {
MessageGroup(isUser = isUser, footer = messageSourceLabel(message, LocalEnumCatalog.current)) { MessageGroup(isUser = isUser, footer = messageSourceLabel(message, LocalEnumCatalog.current)) {
message.thinking?.takeIf { it.isNotBlank() }?.let { ThinkingCard(it) } 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) } message.image?.takeIf { it.isNotBlank() }?.let { ChatImage(it) }
bubbles.forEachIndexed { index, bubble -> bubbles.forEachIndexed { index, bubble ->
Bubble(bubble, isUser, isLast = index == bubbles.lastIndex) Bubble(bubble, isUser, isLast = index == bubbles.lastIndex)
@@ -88,16 +84,21 @@ fun MessageBubble(message: HistoryMessageDto) {
@Composable @Composable
fun PendingBubble(item: PendingMessage) { fun PendingBubble(item: PendingMessage) {
MessageGroup(isUser = true, footer = "发送中…") { 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) } item.imageUri?.takeIf { it.isNotBlank() }?.let { ChatImage(it) }
if (item.text.isNotEmpty()) Bubble(plainBubble(item.text), isUser = true, isLast = true) 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] 非空即带移除按钮——待发位(输入栏上方)要能反悔,历史气泡里则不给。 * [onRemove] 非空即带移除按钮——待发位(输入栏上方)要能反悔,历史气泡里则不给。
* *
* 附件可能是空列表(绝大多数消息都是),此时**不占位、不留空白**。 * 附件可能是空列表(绝大多数消息都是),此时**不占位、不留空白**。
@@ -106,14 +107,17 @@ fun PendingBubble(item: PendingMessage) {
fun DocumentChipRow( fun DocumentChipRow(
chips: List<DocumentChipModel>, chips: List<DocumentChipModel>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
alignEnd: Boolean = false,
onRemove: ((Int) -> Unit)? = null, onRemove: ((Int) -> Unit)? = null,
) { ) {
if (chips.isEmpty()) return if (chips.isEmpty()) return
Row( FlowRow(
modifier = modifier modifier = modifier,
.widthIn(max = CHIP_ROW_MAX_WIDTH) horizontalArrangement = Arrangement.spacedBy(
.horizontalScroll(rememberScrollState()), space = 6.dp,
horizontalArrangement = Arrangement.spacedBy(6.dp), alignment = if (alignEnd) Alignment.End else Alignment.Start,
),
verticalArrangement = Arrangement.spacedBy(6.dp),
) { ) {
chips.forEachIndexed { index, chip -> chips.forEachIndexed { index, chip ->
DocumentChip(model = chip, onRemove = onRemove?.let { remove -> { remove(index) } }) DocumentChip(model = chip, onRemove = onRemove?.let { remove -> { remove(index) } })