diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/AboutScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/AboutScreen.kt new file mode 100644 index 0000000..91a5fdb --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/AboutScreen.kt @@ -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)) + } + } +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/FAQScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/FAQScreen.kt new file mode 100644 index 0000000..33e65dd --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/FAQScreen.kt @@ -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) + } + } + } + } + } +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/FeedbackScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/FeedbackScreen.kt new file mode 100644 index 0000000..4ae04a8 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/FeedbackScreen.kt @@ -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) + } + } + ) + } + } +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/PlanDetailsScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/PlanDetailsScreen.kt new file mode 100644 index 0000000..8393c21 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/PlanDetailsScreen.kt @@ -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) + } + } + } + } + } +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/utils/AnimationUtils.kt b/app-android/app/src/main/java/com/huaga/life_echo/utils/AnimationUtils.kt new file mode 100644 index 0000000..462b1e2 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/utils/AnimationUtils.kt @@ -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 { + 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 { + 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) +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/utils/PaymentUtils.kt b/app-android/app/src/main/java/com/huaga/life_echo/utils/PaymentUtils.kt new file mode 100644 index 0000000..ee2472f --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/utils/PaymentUtils.kt @@ -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 + } + } +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/utils/TextUtils.kt b/app-android/app/src/main/java/com/huaga/life_echo/utils/TextUtils.kt new file mode 100644 index 0000000..d330a15 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/utils/TextUtils.kt @@ -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 { + if (text.isNullOrBlank()) return emptyList() + + // 使用原始字符串避免转义问题 + val quotePattern = Regex("""["'](.*?)["']""") + return quotePattern.findAll(text).map { it.groupValues[1] }.toList() + } +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/utils/TimeUtils.kt b/app-android/app/src/main/java/com/huaga/life_echo/utils/TimeUtils.kt new file mode 100644 index 0000000..4ba2861 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/utils/TimeUtils.kt @@ -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) + } + } + } +}