feat: 新增功能组件和应用配置
- 新增OrganizeConversationDialog组织对话对话框 - 新增debug调试组件 - 新增network_security_config网络安全配置 - 更新MainActivity主活动 - 更新AppConfig应用配置 - 更新ConversationRepository数据仓库 - 更新build.gradle和AndroidManifest配置
This commit is contained in:
@@ -63,7 +63,8 @@ dependencies {
|
|||||||
|
|
||||||
// Ktor Client
|
// Ktor Client
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.android)
|
implementation(libs.ktor.client.android) // 用于普通HTTP请求
|
||||||
|
implementation(libs.ktor.client.okhttp) // 用于WebSocket(OkHttp引擎支持WebSocket)
|
||||||
implementation(libs.ktor.client.websockets)
|
implementation(libs.ktor.client.websockets)
|
||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
@@ -81,6 +82,9 @@ dependencies {
|
|||||||
// Serialization
|
// Serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
|
// Image Loading
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Lifeecho" >
|
android:theme="@style/Theme.Lifeecho" >
|
||||||
|
|||||||
@@ -46,8 +46,11 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.huaga.life_echo.data.auth.TokenManager
|
import com.huaga.life_echo.data.auth.TokenManager
|
||||||
import com.huaga.life_echo.navigation.AppNavigation
|
import com.huaga.life_echo.navigation.AppNavigation
|
||||||
import com.huaga.life_echo.navigation.Screen
|
import com.huaga.life_echo.navigation.Screen
|
||||||
@@ -81,12 +84,61 @@ class MainActivity : ComponentActivity() {
|
|||||||
windowInsetsController.isAppearanceLightStatusBars = !darkMode
|
windowInsetsController.isAppearanceLightStatusBars = !darkMode
|
||||||
windowInsetsController.isAppearanceLightNavigationBars = !darkMode
|
windowInsetsController.isAppearanceLightNavigationBars = !darkMode
|
||||||
}
|
}
|
||||||
|
// 隐藏系统状态栏和导航栏
|
||||||
|
SystemUiController(
|
||||||
|
isStatusBarVisible = false,
|
||||||
|
isNavigationBarVisible = false
|
||||||
|
)
|
||||||
LifeechoApp(TokenManager.isLoggedIn)
|
LifeechoApp(TokenManager.isLoggedIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统UI控制器 Composable 函数
|
||||||
|
* 用于控制状态栏和导航栏的显示/隐藏
|
||||||
|
*
|
||||||
|
* @param isStatusBarVisible 状态栏是否可见,false 表示隐藏
|
||||||
|
* @param isNavigationBarVisible 导航栏是否可见,false 表示隐藏
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SystemUiController(
|
||||||
|
isStatusBarVisible: Boolean = true,
|
||||||
|
isNavigationBarVisible: Boolean = true
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val activity = context as? ComponentActivity
|
||||||
|
|
||||||
|
androidx.compose.runtime.LaunchedEffect(isStatusBarVisible, isNavigationBarVisible) {
|
||||||
|
activity?.window?.let { window ->
|
||||||
|
// 让内容延伸到系统栏下方
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
// 获取 WindowInsetsController
|
||||||
|
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
|
||||||
|
// 控制状态栏的显示/隐藏
|
||||||
|
if (isStatusBarVisible) {
|
||||||
|
windowInsetsController.show(WindowInsetsCompat.Type.statusBars())
|
||||||
|
} else {
|
||||||
|
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制导航栏的显示/隐藏
|
||||||
|
if (isNavigationBarVisible) {
|
||||||
|
windowInsetsController.show(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
} else {
|
||||||
|
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置系统栏行为:当隐藏时,内容可以延伸到系统栏区域
|
||||||
|
windowInsetsController.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@PreviewScreenSizes
|
@PreviewScreenSizes
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
package com.huaga.life_echo.config
|
package com.huaga.life_echo.config
|
||||||
|
|
||||||
object AppConfig {
|
object AppConfig {
|
||||||
// API 基础 URL - 内网地址,用于同一局域网下的物理机测试
|
// API 基础 URL 配置
|
||||||
// 当前主机IP: 192.168.10.9,端口: 8000
|
//
|
||||||
// 如果IP地址变化,请修改此处
|
// 物理机测试:使用实际的内网IP地址(如 192.168.10.9)
|
||||||
|
// Android模拟器:使用 10.0.2.2 来访问主机(开发机器)
|
||||||
|
//
|
||||||
|
// 注意:从Android 9 (API 28)开始,默认禁止明文HTTP流量
|
||||||
|
// 已在 network_security_config.xml 中配置允许明文流量(仅用于开发环境)
|
||||||
|
// 生产环境应该使用HTTPS并移除明文流量配置
|
||||||
|
|
||||||
|
// 当前配置:物理机测试
|
||||||
const val BASE_URL = "http://192.168.10.9:8000"
|
const val BASE_URL = "http://192.168.10.9:8000"
|
||||||
const val WS_BASE_URL = "ws://192.168.10.9:8000"
|
const val WS_BASE_URL = "ws://192.168.10.9:8000"
|
||||||
|
|
||||||
|
// 如果需要在Android模拟器上测试,请使用以下配置:
|
||||||
|
// const val BASE_URL = "http://10.0.2.2:8000"
|
||||||
|
// const val WS_BASE_URL = "ws://10.0.2.2:8000"
|
||||||
|
|
||||||
// 生产环境应该从配置文件或环境变量读取
|
// 生产环境应该从配置文件或环境变量读取
|
||||||
// const val BASE_URL = BuildConfig.API_BASE_URL
|
// const val BASE_URL = BuildConfig.API_BASE_URL
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,22 @@ class ConversationRepository(
|
|||||||
conversationDao.updateConversation(conversation)
|
conversationDao.updateConversation(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun deleteConversation(id: String): Result<Unit> {
|
||||||
|
val result = apiService.deleteConversation(id)
|
||||||
|
return result.fold(
|
||||||
|
onSuccess = {
|
||||||
|
// 从本地数据库删除
|
||||||
|
conversationDao.getConversationById(id)?.let { conversation ->
|
||||||
|
conversationDao.deleteConversation(conversation)
|
||||||
|
}
|
||||||
|
Result.success(Unit)
|
||||||
|
},
|
||||||
|
onFailure = { exception ->
|
||||||
|
Result.failure(exception)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun getSegmentsByConversationId(conversationId: String): Flow<List<ConversationSegment>> {
|
fun getSegmentsByConversationId(conversationId: String): Flow<List<ConversationSegment>> {
|
||||||
return segmentDao.getSegmentsByConversationId(conversationId)
|
return segmentDao.getSegmentsByConversationId(conversationId)
|
||||||
}
|
}
|
||||||
@@ -38,12 +54,48 @@ class ConversationRepository(
|
|||||||
segmentDao.insertSegments(segments)
|
segmentDao.insertSegments(segments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新对话
|
||||||
|
*/
|
||||||
|
suspend fun createConversation(): Result<String> {
|
||||||
|
val result = apiService.createConversation()
|
||||||
|
return result.fold(
|
||||||
|
onSuccess = { response ->
|
||||||
|
// 将新对话保存到本地数据库
|
||||||
|
val conversation = Conversation(
|
||||||
|
id = response.id,
|
||||||
|
userId = response.user_id,
|
||||||
|
startedAt = try {
|
||||||
|
java.time.Instant.parse(response.started_at).toEpochMilli()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
System.currentTimeMillis()
|
||||||
|
},
|
||||||
|
endedAt = null,
|
||||||
|
durationSeconds = 0,
|
||||||
|
summary = null,
|
||||||
|
currentTopic = null,
|
||||||
|
conversationStage = null,
|
||||||
|
avatarUrl = null,
|
||||||
|
title = null,
|
||||||
|
latestMessagePreview = null,
|
||||||
|
latestMessageTime = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
conversationDao.insertConversation(conversation)
|
||||||
|
Result.success(response.id)
|
||||||
|
},
|
||||||
|
onFailure = { exception ->
|
||||||
|
Result.failure(exception)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从API同步对话列表
|
* 从API同步对话列表
|
||||||
*/
|
*/
|
||||||
suspend fun syncConversations() {
|
suspend fun syncConversations() {
|
||||||
val result = apiService.getConversationList()
|
val result = apiService.getConversationList()
|
||||||
result.getOrNull()?.let { conversations ->
|
result.fold(
|
||||||
|
onSuccess = { conversations ->
|
||||||
// 将DTO转换为Entity并保存到数据库
|
// 将DTO转换为Entity并保存到数据库
|
||||||
conversations.forEach { dto ->
|
conversations.forEach { dto ->
|
||||||
val conversation = Conversation(
|
val conversation = Conversation(
|
||||||
@@ -62,7 +114,12 @@ class ConversationRepository(
|
|||||||
)
|
)
|
||||||
conversationDao.insertConversation(conversation)
|
conversationDao.insertConversation(conversation)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onFailure = { exception ->
|
||||||
|
// 失败时抛出异常,让调用方处理
|
||||||
|
throw exception
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package com.huaga.life_echo.ui.components.debug
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket调试面板 - 开发测试用
|
||||||
|
* 实时显示WebSocket连接状态、消息收发情况等调试信息
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun WebSocketDebugPanel(
|
||||||
|
connectionStatus: String,
|
||||||
|
conversationId: String?,
|
||||||
|
isConnected: Boolean,
|
||||||
|
isStreaming: Boolean,
|
||||||
|
isTyping: Boolean,
|
||||||
|
lastMessageType: String?,
|
||||||
|
lastMessageTime: String?,
|
||||||
|
errorMessages: List<String>,
|
||||||
|
messageCount: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 根据连接状态决定颜色
|
||||||
|
val statusColor = when {
|
||||||
|
connectionStatus.contains("已连接") -> Color(0xFF4CAF50)
|
||||||
|
connectionStatus.contains("连接中") -> Color(0xFFFF9800)
|
||||||
|
connectionStatus.contains("错误") || connectionStatus.contains("失败") -> Color(0xFFF44336)
|
||||||
|
else -> Color(0xFF9E9E9E)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
// 标题栏(可点击展开/收起)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { isExpanded = !isExpanded },
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// 状态指示器
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.background(
|
||||||
|
color = statusColor,
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "WS调试",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = connectionStatus,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = statusColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isExpanded) AppIcons.ExpandLess else AppIcons.ExpandMore,
|
||||||
|
contentDescription = if (isExpanded) "收起" else "展开",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详细信息(展开时显示)
|
||||||
|
AnimatedVisibility(visible = isExpanded) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// 基本信息
|
||||||
|
DebugInfoRow("连接状态", connectionStatus, statusColor)
|
||||||
|
DebugInfoRow("是否已连接", if (isConnected) "是" else "否", if (isConnected) Color(0xFF4CAF50) else Color(0xFFF44336))
|
||||||
|
DebugInfoRow("对话ID", conversationId ?: "未设置", MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
|
// 消息统计
|
||||||
|
DebugInfoRow("消息总数", "$messageCount", MaterialTheme.colorScheme.primary)
|
||||||
|
DebugInfoRow("流式状态", if (isStreaming) "正在接收" else "空闲", if (isStreaming) Color(0xFFFF9800) else Color(0xFF4CAF50))
|
||||||
|
DebugInfoRow("输入状态", if (isTyping) "AI正在输入" else "空闲", if (isTyping) Color(0xFFFF9800) else Color(0xFF4CAF50))
|
||||||
|
|
||||||
|
// 最后一条消息信息
|
||||||
|
if (lastMessageType != null) {
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
DebugInfoRow("最后消息类型", lastMessageType, MaterialTheme.colorScheme.primary)
|
||||||
|
if (lastMessageTime != null) {
|
||||||
|
DebugInfoRow("最后消息时间", lastMessageTime, MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误信息
|
||||||
|
if (errorMessages.isNotEmpty()) {
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
Text(
|
||||||
|
text = "错误日志",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFFF44336),
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
errorMessages.takeLast(5).forEach { error ->
|
||||||
|
Text(
|
||||||
|
text = "• $error",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color(0xFFF44336),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
modifier = Modifier.padding(start = 8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DebugInfoRow(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
valueColor: Color
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$label:",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = valueColor,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.huaga.life_echo.ui.components.memoir
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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 androidx.compose.ui.window.Dialog
|
||||||
|
import com.huaga.life_echo.network.models.ConversationListItemDto
|
||||||
|
import com.huaga.life_echo.ui.theme.LightPurple
|
||||||
|
import com.huaga.life_echo.utils.TimeUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 整理对话对话框
|
||||||
|
* 用于选择要整理的对话
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun OrganizeConversationDialog(
|
||||||
|
conversations: List<ConversationListItemDto>,
|
||||||
|
isLoading: Boolean = false,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSelectConversation: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.8f),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 标题栏
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "选择要整理的对话",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Text("✕", fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// 对话列表
|
||||||
|
if (isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = LightPurple)
|
||||||
|
}
|
||||||
|
} else if (conversations.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "还没有对话",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = PaddingValues(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
items(conversations, key = { it.id }) { conversation ->
|
||||||
|
ConversationItem(
|
||||||
|
conversation = conversation,
|
||||||
|
onClick = {
|
||||||
|
onSelectConversation(conversation.id)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ConversationItem(
|
||||||
|
conversation: ConversationListItemDto,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.clickable { onClick() },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = conversation.title,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
if (conversation.latestMessagePreview != null) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = conversation.latestMessagePreview,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = TimeUtils.formatDateTime(conversation.latestMessageTime),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app-android/app/src/main/res/xml/network_security_config.xml
Normal file
20
app-android/app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<!-- 开发环境:允许明文HTTP流量 -->
|
||||||
|
<!-- 注意:生产环境应该移除此配置,强制使用HTTPS -->
|
||||||
|
<base-config cleartextTrafficPermitted="true">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
|
||||||
|
<!-- 可选:仅允许特定域名的明文流量(更安全的方式) -->
|
||||||
|
<!--
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">192.168.10.9</domain>
|
||||||
|
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||||
|
<domain includeSubdomains="true">localhost</domain>
|
||||||
|
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||||
|
</domain-config>
|
||||||
|
-->
|
||||||
|
</network-security-config>
|
||||||
@@ -16,6 +16,7 @@ navigationCompose = "2.8.4"
|
|||||||
permissions = "0.34.0"
|
permissions = "0.34.0"
|
||||||
coroutines = "1.9.0"
|
coroutines = "1.9.0"
|
||||||
serialization = "1.7.3"
|
serialization = "1.7.3"
|
||||||
|
coil = "2.5.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -38,6 +39,7 @@ androidx-compose-material3-adaptive-navigation-suite = { group = "androidx.compo
|
|||||||
# Ktor
|
# Ktor
|
||||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||||
ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" }
|
ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" }
|
||||||
|
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
|
||||||
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
|
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
|
||||||
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
|
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
|
||||||
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||||
@@ -61,6 +63,9 @@ kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-
|
|||||||
# Serialization
|
# Serialization
|
||||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
|
|
||||||
|
# Image Loading
|
||||||
|
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user