添加对话列表、导出数据、订单和升级计划等新功能

This commit is contained in:
徐在坤
2026-01-17 19:35:09 +08:00
parent 6d59348ff6
commit bf9f3cf363
7 changed files with 901 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
package com.huaga.life_echo.ui.icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.ui.graphics.vector.ImageVector
/**
* 应用图标常量文件
* 统一管理应用中使用的所有图标
*/
object AppIcons {
// 导航栏图标
val Chat = Icons.Default.Chat
val Memoir = Icons.Default.MenuBook
val Profile = Icons.Default.Person
// 聊天界面图标
val ArrowBack = Icons.Default.ArrowBack
val Mic = Icons.Default.Mic
val MicOff = Icons.Default.MicOff
val SentimentSatisfied = Icons.Default.SentimentSatisfied
val Add = Icons.Default.Add
val Send = Icons.Default.Send
val Book = Icons.Default.MenuBook
// 对话列表图标
val Conversation = Icons.Default.ChatBubble
val DateRange = Icons.Default.DateRange
val History = Icons.Default.History
// 回忆录界面图标
val Chapter = Icons.Default.Book
val Reading = Icons.Default.MenuBook
val ChevronRight = Icons.Default.ChevronRight
val Edit = Icons.Default.Edit
val Share = Icons.Default.Share
val Download = Icons.Default.Download
// 个人设置界面图标
val Person = Icons.Default.Person
val Upgrade = Icons.Default.Star
val Receipt = Icons.Default.Receipt
val FileDownload = Icons.Default.FileDownload
val AccessTime = Icons.Default.AccessTime
val FormatSize = Icons.Default.FormatSize
val Brightness2 = Icons.Default.Brightness2
val Settings = Icons.Default.Settings
val Help = Icons.Default.Help
val Info = Icons.Default.Info
// 其他常用图标
val Check = Icons.Default.Check
val Close = Icons.Default.Close
val Delete = Icons.Default.Delete
val MoreVert = Icons.Default.MoreVert
val Search = Icons.Default.Search
val Filter = Icons.Default.FilterList
val Refresh = Icons.Default.Refresh
val Favorite = Icons.Default.Favorite
val FavoriteBorder = Icons.Default.FavoriteBorder
val Star = Icons.Default.Star
val StarBorder = Icons.Default.StarBorder
// 状态图标
val Online = Icons.Default.Circle
val Offline = Icons.Outlined.Circle
val CheckCircle = Icons.Default.CheckCircle
val Error = Icons.Default.Error
val Warning = Icons.Default.Warning
val InfoCircle = Icons.Default.Info
}

View File

@@ -0,0 +1,187 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.background
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 com.huaga.life_echo.ui.icons.AppIcons
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.huaga.life_echo.data.database.Conversation
import com.huaga.life_echo.ui.theme.LightPurple
import com.huaga.life_echo.ui.viewmodel.ConversationListViewModel
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun ConversationListScreen(
onConversationClick: (String) -> Unit = {},
viewModel: ConversationListViewModel = viewModel(
factory = ViewModelFactory(LocalContext.current)
)
) {
val conversations by viewModel.conversations.collectAsState(initial = emptyList())
Column(
modifier = Modifier.fillMaxSize()
) {
// 浅紫色标题栏
Surface(
modifier = Modifier.fillMaxWidth(),
color = LightPurple
) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.statusBars)
.padding(top = 16.dp, bottom = 24.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "往事拾遗",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "用对话,留住珍贵的记忆",
fontSize = 14.sp,
color = Color.White.copy(alpha = 0.9f)
)
}
}
// 对话列表区域
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
Text(
text = "我的对话",
modifier = Modifier.padding(16.dp, 16.dp, 16.dp, 8.dp),
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
if (conversations.isEmpty()) {
// 空状态 - 显示示例对话项
ConversationItem(
conversation = Conversation(
id = "demo",
userId = "user",
startedAt = System.currentTimeMillis(),
endedAt = null,
durationSeconds = 0,
summary = "您想从哪里开始呢?可以聊聊童年...",
currentTopic = null,
conversationStage = null
),
onClick = { onConversationClick("demo") }
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(conversations) { conversation ->
ConversationItem(
conversation = conversation,
onClick = { onConversationClick(conversation.id) }
)
}
}
}
}
}
}
@Composable
fun ConversationItem(
conversation: Conversation,
onClick: () -> Unit
) {
val timeText = if (conversation.endedAt != null) {
val date = Date(conversation.endedAt)
val now = Date()
val diff = now.time - date.time
when {
diff < 60000 -> "刚刚"
diff < 3600000 -> "${diff / 60000}分钟前"
diff < 86400000 -> "${diff / 3600000}小时前"
else -> SimpleDateFormat("MM月dd日", Locale.getDefault()).format(date)
}
} else {
"刚刚"
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 浅紫色图标背景
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(8.dp))
.background(LightPurple),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = AppIcons.Conversation,
contentDescription = "对话",
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(12.dp))
// 对话信息
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "回忆录助手",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = conversation.summary ?: conversation.currentTopic ?: "您想从哪里开始呢?可以聊聊童年...",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1
)
}
// 时间戳
Text(
text = timeText,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 8.dp)
)
}
}

View File

@@ -0,0 +1,191 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.rememberCoroutineScope
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 com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExportDataScreen(
navController: androidx.navigation.NavHostController? = null
) {
var isExporting by remember { mutableStateOf(false) }
var exportCompleted by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar(
title = { Text("导出所有数据") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = LightPurple,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 说明信息
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "导出说明",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "导出数据将包含:",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
listOf(
"所有对话记录",
"回忆录内容",
"章节信息",
"用户设置"
).forEach { item ->
Row(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "",
fontSize = 14.sp,
color = LightPurple,
fontWeight = FontWeight.Bold
)
Text(
text = item,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
// 导出格式选择
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "选择导出格式",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(12.dp))
listOf("JSON", "CSV", "PDF").forEach { format ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = false, // TODO: 管理选中状态
onClick = { /* TODO */ }
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = format,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
Spacer(modifier = Modifier.weight(1f))
// 导出按钮
Button(
onClick = {
isExporting = true
// TODO: 执行导出操作
// 模拟导出完成
scope.launch {
delay(2000)
isExporting = false
exportCompleted = true
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isExporting && !exportCompleted,
colors = ButtonDefaults.buttonColors(
containerColor = LightPurple
)
) {
if (isExporting) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text("导出中...", color = Color.White)
} else if (exportCompleted) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "完成",
tint = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text("导出完成", color = Color.White)
} else {
Text("开始导出", color = Color.White)
}
}
}
}
}

View File

@@ -0,0 +1,177 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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 com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyOrdersScreen(
navController: androidx.navigation.NavHostController? = null
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("我的订单") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = LightPurple,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 示例订单
item {
OrderCard(
orderId = "ORD20240101001",
planName = "高级版",
price = "¥29",
date = "2024年1月1日",
status = "已完成"
)
}
item {
OrderCard(
orderId = "ORD20231215002",
planName = "专业版",
price = "¥99",
date = "2023年12月15日",
status = "已完成"
)
}
// 空状态提示
if (false) { // 当没有订单时显示
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 64.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "暂无订单",
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "升级套餐后,订单将显示在这里",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
@Composable
fun OrderCard(
orderId: String,
planName: String,
price: String,
date: String,
status: String
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = planName,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = status,
fontSize = 14.sp,
color = LightPurple,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "订单号: $orderId",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "日期: $date",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "金额",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = price,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = LightPurple
)
}
}
}
}

View File

@@ -0,0 +1,205 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpgradePlanScreen(
navController: androidx.navigation.NavHostController? = null
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("升级套餐") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = LightPurple,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text(
text = "选择套餐",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
}
// 免费版
item {
PlanCard(
title = "免费体验版",
price = "免费",
features = listOf(
"基础对话功能",
"每月3次对话",
"基础回忆录整理"
),
isSelected = true,
onClick = { }
)
}
// 高级版
item {
PlanCard(
title = "高级版",
price = "¥29/月",
features = listOf(
"无限对话次数",
"完整回忆录导出",
"PDF格式导出",
"优先客服支持",
"数据云端同步"
),
isSelected = false,
onClick = { /* TODO: 购买 */ }
)
}
// 专业版
item {
PlanCard(
title = "专业版",
price = "¥99/月",
features = listOf(
"高级版所有功能",
"AI智能整理",
"多格式导出PDF、Word、EPUB",
"专属客服支持",
"无限云端存储",
"多设备同步"
),
isSelected = false,
onClick = { /* TODO: 购买 */ }
)
}
}
}
}
@Composable
fun PlanCard(
title: String,
price: String,
features: List<String>,
isSelected: Boolean,
onClick: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) LightPurple.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surface
),
border = if (isSelected) {
androidx.compose.foundation.BorderStroke(2.dp, LightPurple)
} else null
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
if (isSelected) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "当前套餐",
tint = LightPurple,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = price,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = LightPurple
)
Spacer(modifier = Modifier.height(16.dp))
features.forEach { feature ->
Row(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "",
fontSize = 16.sp,
color = LightPurple,
fontWeight = FontWeight.Bold
)
Text(
text = feature,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}
if (!isSelected) {
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = LightPurple
)
) {
Text("立即升级", color = Color.White)
}
}
}
}
}

View File

@@ -0,0 +1,50 @@
package com.huaga.life_echo.ui.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
/**
* 应用设置管理
*/
object AppSettings {
// 语速设置:慢速、标准、快速
enum class SpeechRate(val label: String, val multiplier: Float) {
SLOW("慢速", 0.75f),
STANDARD("标准", 1.0f),
FAST("快速", 1.5f)
}
private val _speechRate = mutableStateOf(SpeechRate.STANDARD)
var speechRate: SpeechRate
get() = _speechRate.value
set(value) { _speechRate.value = value }
private val _largeFontMode = mutableStateOf(false)
var largeFontMode: Boolean
get() = _largeFontMode.value
set(value) { _largeFontMode.value = value }
private val _darkMode = mutableStateOf(false)
var darkMode: Boolean
get() = _darkMode.value
set(value) { _darkMode.value = value }
// 用于在Compose中观察设置变化 - 直接返回mutableStateOf的值
@Composable
fun rememberDarkMode(): Boolean {
return remember { _darkMode }.value
}
@Composable
fun rememberLargeFontMode(): Boolean {
return remember { _largeFontMode }.value
}
@Composable
fun rememberSpeechRate(): SpeechRate {
return remember { _speechRate }.value
}
}

View File

@@ -0,0 +1,19 @@
package com.huaga.life_echo.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.huaga.life_echo.data.repository.ConversationRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class ConversationListViewModel(
conversationRepository: ConversationRepository
) : ViewModel() {
val conversations = conversationRepository.getAllConversations()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}