feat: 新增关于、常见问题、反馈和套餐详情屏幕

- 新增AboutScreen关于页面
- 新增FAQScreen常见问题页面
- 新增FeedbackScreen反馈页面
- 新增PlanDetailsScreen套餐详情页面
- 新增工具类(DateUtils、TextUtils、ValidationUtils)
This commit is contained in:
徐在坤
2026-01-21 18:18:00 +08:00
parent c65eeb987a
commit 8a1659176f
8 changed files with 716 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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 AboutScreen(
navController: androidx.navigation.NavHostController? = null
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("关于我们") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = LightPurple,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Logo 或图标区域
Box(
modifier = Modifier
.size(120.dp)
.padding(bottom = 24.dp),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = AppIcons.Info,
contentDescription = "应用图标",
tint = LightPurple,
modifier = Modifier.size(80.dp)
)
}
// 应用名称
Text(
text = "Life Echo",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 8.dp)
)
// 版本信息
Text(
text = "版本 1.0.0",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 32.dp)
)
Divider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 应用介绍
Text(
text = "应用介绍",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
)
Text(
text = "Life Echo 是一款智能回忆录助手应用帮助您记录和整理生活中的美好回忆。通过AI对话的方式轻松创建属于您的个人回忆录。",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
lineHeight = 24.sp
)
Divider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 联系方式
Text(
text = "联系我们",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
)
Text(
text = "如有任何问题或建议,欢迎通过应用内的"反馈与客服"功能联系我们。",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
lineHeight = 24.sp
)
Divider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 版权信息
Text(
text = "© 2024 Life Echo. All rights reserved.",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
Spacer(modifier = Modifier.height(32.dp))
}
}
}

View File

@@ -0,0 +1,89 @@
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.lazy.items
import androidx.compose.material3.*
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.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.ui.components.common.EmptyStateView
import com.huaga.life_echo.ui.components.common.LoadingIndicator
import com.huaga.life_echo.ui.components.profile.FAQItem
import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
import com.huaga.life_echo.ui.viewmodel.ProfileViewModel
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FAQScreen(
navController: androidx.navigation.NavHostController? = null,
viewModel: ProfileViewModel = viewModel(
factory = ViewModelFactory(LocalContext.current)
)
) {
val faqs by viewModel.faqs.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("常见问题") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = LightPurple,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
when {
isLoading -> {
LoadingIndicator(modifier = Modifier.padding(paddingValues))
}
faqs.isEmpty() -> {
EmptyStateView(
message = "暂无常见问题",
icon = "",
modifier = Modifier.padding(paddingValues)
)
}
else -> {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = faqs,
key = { faq -> faq.id }
) { faq ->
FAQItem(faq = faq)
}
}
}
}
}
}

View File

@@ -0,0 +1,96 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.wrapContentHeight
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.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.ui.components.profile.FeedbackForm
import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
import com.huaga.life_echo.ui.viewmodel.ProfileViewModel
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FeedbackScreen(
navController: androidx.navigation.NavHostController? = null,
viewModel: ProfileViewModel = viewModel(
factory = ViewModelFactory(LocalContext.current)
)
) {
var showSuccessDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("反馈与客服") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = LightPurple,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
FeedbackForm(
onSubmit = { content, contact ->
viewModel.submitFeedback(
content = content,
contact = contact,
onSuccess = {
showSuccessDialog = true
},
onError = { }
)
}
)
}
// 提交成功对话框
if (showSuccessDialog) {
AlertDialog(
onDismissRequest = { showSuccessDialog = false },
title = { Text("提交成功") },
text = {
Text(
text = "感谢您的反馈,我们会尽快处理!",
modifier = Modifier.wrapContentHeight()
)
},
confirmButton = {
TextButton(
onClick = {
showSuccessDialog = false
navController?.popBackStack()
}
) {
Text("确定", color = LightPurple)
}
}
)
}
}
}

View File

@@ -0,0 +1,77 @@
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.material3.*
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.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.ui.components.common.LoadingIndicator
import com.huaga.life_echo.ui.components.payment.PlanCard
import com.huaga.life_echo.ui.components.profile.PlanDetailsCard
import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.LightPurple
import com.huaga.life_echo.ui.viewmodel.PaymentViewModel
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlanDetailsScreen(
navController: androidx.navigation.NavHostController? = null,
viewModel: PaymentViewModel = viewModel(
factory = ViewModelFactory(LocalContext.current)
)
) {
val currentPlan by viewModel.currentPlan.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("套餐详情") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = LightPurple,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
if (isLoading) {
LoadingIndicator(modifier = Modifier.padding(paddingValues))
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
currentPlan?.let { plan ->
item {
PlanDetailsCard(plan = plan)
}
}
}
}
}
}

View File

@@ -0,0 +1,71 @@
package com.huaga.life_echo.utils
import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
/**
* 动画工具类
*/
object AnimationUtils {
/**
* 创建打字机效果的动画值
*/
@Composable
fun createTypingAnimation(
durationMillis: Int = 1000,
delayMillis: Int = 0
): androidx.compose.runtime.State<Float> {
val infiniteTransition = rememberInfiniteTransition(label = "typing")
return infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis, delayMillis, FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "typing_alpha"
)
}
/**
* 创建脉冲动画
*/
@Composable
fun createPulseAnimation(
durationMillis: Int = 1000
): androidx.compose.runtime.State<Float> {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
return infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "pulse_scale"
)
}
}
/**
* 打字机效果修饰符
*/
@Composable
fun Modifier.typingEffect(visible: Boolean): Modifier {
val alpha by AnimationUtils.createTypingAnimation()
return this.alpha(if (visible) alpha else 1f)
}
/**
* 脉冲效果修饰符
*/
@Composable
fun Modifier.pulseEffect(): Modifier {
val scale by AnimationUtils.createPulseAnimation()
return this.scale(scale)
}

View File

@@ -0,0 +1,71 @@
package com.huaga.life_echo.utils
/**
* 支付工具类
* 用于处理支付相关的工具函数
*/
object PaymentUtils {
/**
* 格式化价格
* @param price 价格
* @param currency 货币符号
* @return 格式化后的价格字符串
*/
fun formatPrice(price: Double, currency: String = "CNY"): String {
val symbol = when (currency) {
"CNY" -> "¥"
"USD" -> "$"
"EUR" -> ""
else -> currency
}
return if (price == 0.0) {
"免费"
} else {
"$symbol${String.format("%.2f", price)}"
}
}
/**
* 格式化计费周期
* @param cycle 计费周期monthly/yearly
* @return 格式化后的周期字符串
*/
fun formatBillingCycle(cycle: String): String {
return when (cycle.lowercase()) {
"monthly" -> "/月"
"yearly" -> "/年"
else -> ""
}
}
/**
* 格式化订单状态
* @param status 订单状态
* @return 格式化后的状态字符串
*/
fun formatOrderStatus(status: String): String {
return when (status.lowercase()) {
"pending" -> "待支付"
"paid" -> "已支付"
"failed" -> "支付失败"
"cancelled" -> "已取消"
else -> status
}
}
/**
* 格式化订阅状态
* @param status 订阅状态
* @return 格式化后的状态字符串
*/
fun formatSubscriptionStatus(status: String): String {
return when (status.lowercase()) {
"active" -> "有效"
"expired" -> "已过期"
"cancelled" -> "已取消"
else -> status
}
}
}

View File

@@ -0,0 +1,68 @@
package com.huaga.life_echo.utils
/**
* 文本处理工具
*/
object TextUtils {
/**
* 单行文本省略
* @param text 原始文本
* @param maxLength 最大长度
* @return 省略后的文本
*/
fun ellipsizeSingleLine(text: String?, maxLength: Int = 50): String {
if (text.isNullOrBlank()) return ""
return if (text.length > maxLength) {
text.substring(0, maxLength) + "..."
} else {
text
}
}
/**
* 多行文本省略
* @param text 原始文本
* @param maxLines 最大行数
* @param maxLengthPerLine 每行最大长度
* @return 省略后的文本
*/
fun ellipsizeMultiLine(text: String?, maxLines: Int = 3, maxLengthPerLine: Int = 50): String {
if (text.isNullOrBlank()) return ""
val lines = text.split("\n")
if (lines.size <= maxLines) {
return text
}
val result = lines.take(maxLines).joinToString("\n")
return if (result.length > maxLines * maxLengthPerLine) {
result.substring(0, maxLines * maxLengthPerLine) + "..."
} else {
result + "..."
}
}
/**
* 移除多余的空白字符
* @param text 原始文本
* @return 处理后的文本
*/
fun trimWhitespace(text: String?): String {
return text?.replace(Regex("\\s+"), " ")?.trim() ?: ""
}
/**
* 提取引用内容(以引号包裹的内容)
* @param text 原始文本
* @return 引用内容列表
*/
fun extractQuotes(text: String?): List<String> {
if (text.isNullOrBlank()) return emptyList()
// 使用原始字符串避免转义问题
val quotePattern = Regex("""["'](.*?)["']""")
return quotePattern.findAll(text).map { it.groupValues[1] }.toList()
}
}

View File

@@ -0,0 +1,87 @@
package com.huaga.life_echo.utils
import java.text.SimpleDateFormat
import java.util.*
/**
* 时间格式化工具
*/
object TimeUtils {
/**
* 格式化时间戳为相对时间
* @param timestamp 时间戳(毫秒)
* @return 格式化后的时间字符串(刚刚/分钟前/小时前/日期)
*/
fun formatRelativeTime(timestamp: Long?): String {
if (timestamp == null) return "刚刚"
val now = System.currentTimeMillis()
val diff = now - timestamp
return when {
diff < 60000 -> "刚刚" // 1分钟内
diff < 3600000 -> "${diff / 60000}分钟前" // 1小时内
diff < 86400000 -> "${diff / 3600000}小时前" // 24小时内
diff < 604800000 -> "${diff / 86400000}天前" // 7天内
else -> {
val date = Date(timestamp)
val calendar = Calendar.getInstance()
val nowCalendar = Calendar.getInstance()
// 如果是今年,显示月日
if (calendar.get(Calendar.YEAR) == nowCalendar.get(Calendar.YEAR)) {
SimpleDateFormat("MM月dd日", Locale.getDefault()).format(date)
} else {
// 跨年显示年月日
SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault()).format(date)
}
}
}
}
/**
* 格式化时间戳为日期时间
* @param timestamp 时间戳(毫秒)
* @return 格式化后的日期时间字符串
*/
fun formatDateTime(timestamp: Long?): String {
if (timestamp == null) return ""
val date = Date(timestamp)
val calendar = Calendar.getInstance()
val nowCalendar = Calendar.getInstance()
calendar.time = date
// 判断是否是今天
val isToday = calendar.get(Calendar.YEAR) == nowCalendar.get(Calendar.YEAR) &&
calendar.get(Calendar.DAY_OF_YEAR) == nowCalendar.get(Calendar.DAY_OF_YEAR)
return if (isToday) {
"今天 ${SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)}"
} else {
SimpleDateFormat("MM月dd日 HH:mm", Locale.getDefault()).format(date)
}
}
/**
* 格式化更新时间
* @param timestamp 时间戳(毫秒)
* @return 格式化后的更新时间字符串
*/
fun formatUpdateTime(timestamp: Long?): String {
if (timestamp == null) return ""
val diff = System.currentTimeMillis() - timestamp
return when {
diff < 60000 -> "刚刚更新"
diff < 3600000 -> "${diff / 60000}分钟前更新"
diff < 86400000 -> "${diff / 3600000}小时前更新"
diff < 604800000 -> "${diff / 86400000}天前更新"
else -> {
val date = Date(timestamp)
SimpleDateFormat("MM月dd日更新", Locale.getDefault()).format(date)
}
}
}
}