This commit is contained in:
Eric85710 2026-05-31 15:52:05 +08:00
parent 1ecbc7b156
commit f088fc746f
3 changed files with 135 additions and 89 deletions

View file

@ -1,6 +1,7 @@
package com.example.login_v3.home.Message.UI.Detail package com.example.login_v3.home.Message.UI.Detail
import android.net.Uri import android.net.Uri
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -307,6 +308,7 @@ fun MessageList(
} }
} }
private fun String?.isNullTabOrBlank(): Boolean = this == null || this.trim().isBlank()
@Composable @Composable
fun MessageRow( fun MessageRow(
message: Message, message: Message,
@ -324,8 +326,30 @@ fun MessageRow(
val isLegacyImage = contentLowerCase.startsWith("http") && imageExtensions.any { contentLowerCase.contains(it) } val isLegacyImage = contentLowerCase.startsWith("http") && imageExtensions.any { contentLowerCase.contains(it) }
val isImage = isAttachmentImage || isLegacyImage val isImage = isAttachmentImage || isLegacyImage
val rawContent = message.content ?: "" val rawContent = message.content ?: ""
val finalImageModel = if (rawContent.startsWith("/")) "http://192.168.0.217$rawContent" else rawContent // 💡 核心修正:利用 remember 根據 message 狀態動態計算正確的圖片 URL
val finalImageModel = remember(message) {
// 1. 如果有 attachment,優先去拿裡面的 filename;如果沒有,再拿一般的 content
// 2. 假設後端儲存路徑通常是在某個特定資料夾(例如 /uploads/),如果 filename 沒帶路徑,記得補上
val rawPath = if (isAttachmentImage && !message.attachment?.filename.isNullTabOrBlank()) {
message.attachment!!.filename
} else {
message.content
}
when {
rawPath.isBlank() -> ""
rawPath.startsWith("https") -> rawPath // 已經是完整網址,直接回傳
// 💡 注意:請根據你後端圖片實際存放的路由修改這裡。
// 如果後端 filename 只有 "abc.jpg",而實際上網址是 "http://192.168.0.217/uploads/abc.jpg"
// 你需要把下面改成 "/uploads/$rawPath"
rawPath.startsWith("/") -> "https://192.168.0.217$rawPath"
else -> "https://192.168.0.217/$rawPath"
}
}
//emoji value //emoji value
var showEmojiPanel by remember { mutableStateOf(false) } var showEmojiPanel by remember { mutableStateOf(false) }
@ -424,7 +448,15 @@ fun MessageRow(
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = 300.dp) .heightIn(max = 300.dp),
// 🛠️ 移除了 unresolved 的 debugPlaceholder
onError = { errorState ->
// 這樣就能在 Logcat 裡過濾 "ChatImageError" 關鍵字,看看到底是哪個網址拼錯了!
android.util.Log.e(
"ChatImageError",
"圖片載入失敗, 網址為: $finalImageModel, 原因: ${errorState.result.throwable}"
)
}
) )
} }
} else { } else {

View file

@ -5,6 +5,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
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
@ -32,6 +33,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
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.blur
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -61,100 +63,112 @@ fun MessageInputBar(
.navigationBarsPadding() // 自動適應 Android 系統導覽列高度 .navigationBarsPadding() // 自動適應 Android 系統導覽列高度
.padding(horizontal = 16.dp, vertical = 8.dp) // 懸浮的外邊距 .padding(horizontal = 16.dp, vertical = 8.dp) // 懸浮的外邊距
) { ) {
// 使用 Surface 或 Card 來實作圓角與陰影懸浮感
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), // 整體大圓角化
color = Color.White,
tonalElevation = 6.dp, // 頂級懸浮感
shadowElevation = 6.dp // 實體陰影
) {
Column(modifier = Modifier.fillMaxWidth()) {
// 1. 回覆預覽列 (加入頂部圓角修飾) //glass effect
AnimatedVisibility(visible = replyingMessage != null) { Box(
if (replyingMessage != null) { modifier = Modifier
Row( .matchParentSize()
modifier = Modifier .blur(10.dp)
.fillMaxWidth() )
.background(Color(0xFFF8F9FA)) // 更輕盈的底色 Box(
.padding(horizontal = 16.dp, vertical = 8.dp), modifier = Modifier
verticalAlignment = Alignment.CenterVertically, .matchParentSize()
horizontalArrangement = Arrangement.SpaceBetween .background(
) { color = MaterialTheme.colorScheme.primary,
Column(modifier = Modifier.weight(1f)) { RoundedCornerShape(16.dp)
Text( )
text = replyingMessage.content, .border(
style = MaterialTheme.typography.bodySmall, 1.dp,
color = Color.Gray, Color.White.copy(alpha = 0.3f),
maxLines = 1, RoundedCornerShape(16.dp)
overflow = TextOverflow.Ellipsis )
) )
}
IconButton(onClick = onCancelReply, modifier = Modifier.size(24.dp)) { Column(modifier = Modifier.fillMaxWidth()) {
Icon(
imageVector = Icons.Default.Close, // 1. 回覆預覽列 (加入頂部圓角修飾)
contentDescription = "取消回覆", AnimatedVisibility(visible = replyingMessage != null) {
tint = Color.Gray if (replyingMessage != null) {
) Row(
} modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFF8F9FA)) // 更輕盈的底色
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = replyingMessage.content,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
IconButton(onClick = onCancelReply, modifier = Modifier.size(24.dp)) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "取消回覆",
tint = Color.Gray
)
} }
} }
} }
}
// 2. 實際的輸入工具列 // 2. 實際的輸入工具列
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp), .padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 圖片選擇按鈕 // 圖片選擇按鈕
IconButton(onClick = { imagePickerLauncher.launch("image/*") }) { IconButton(onClick = { imagePickerLauncher.launch("image/*") }) {
Icon( Icon(
imageVector = Icons.Default.Photo, imageVector = Icons.Default.Photo,
contentDescription = "選擇圖片", contentDescription = "選擇圖片",
tint = Color.Gray tint = Color.Gray
)
}
// 輸入文字框 (改為內嵌樣式,去掉自帶的邊框,讓整體更有一體感)
BasicTextField(
value = textState,
onValueChange = { textState = it },
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp),
textStyle = MaterialTheme.typography.bodyLarge,
decorationBox = { innerTextField ->
if (textState.isEmpty()) {
Text("請輸入訊息...", color = Color.LightGray)
}
innerTextField()
}
) )
}
// 送出按鈕 // 輸入文字框 (改為內嵌樣式,去掉自帶的邊框,讓整體更有一體感)
IconButton( BasicTextField(
onClick = { value = textState,
if (textState.isNotBlank() && !isLoading) { onValueChange = { textState = it },
onSendClick(textState) modifier = Modifier
textState = "" .weight(1f)
} .padding(horizontal = 8.dp),
}, textStyle = MaterialTheme.typography.bodyLarge,
enabled = textState.isNotBlank() && !isLoading decorationBox = { innerTextField ->
) { if (textState.isEmpty()) {
if (isLoading) { Text("請輸入訊息...", color = Color.LightGray)
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
} else {
// 加上小背景讓送出按鈕更有重點
val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray
Icon(
imageVector = Icons.Default.Send,
contentDescription = "送出",
tint = iconColor
)
} }
innerTextField()
}
)
// 送出按鈕
IconButton(
onClick = {
if (textState.isNotBlank() && !isLoading) {
onSendClick(textState)
textState = ""
}
},
enabled = textState.isNotBlank() && !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
} else {
// 加上小背景讓送出按鈕更有重點
val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray
Icon(
imageVector = Icons.Default.Send,
contentDescription = "送出",
tint = iconColor
)
} }
} }
} }

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config>
<domain-config cleartextTrafficPermitted="false"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.0.217</domain> <domain includeSubdomains="true">192.168.0.217</domain>
<trust-anchors> <trust-anchors>