refactor: 更新应用名称与对话提示以增强用户体验

- 将应用名称从“岁月时书”更改为“岁月留书”,并在多个文件中更新相关文本。
- 在对话提示中将“回忆录助手”替换为“岁月知己”,以统一用户体验。
- 添加新的头像资源以匹配更新后的助手名称。
- 更新多个界面和文档中的文本,以反映新的品牌形象和功能。
This commit is contained in:
penghanyuan
2026-02-13 23:04:24 +01:00
parent 7fe0b70d5c
commit 0030ea4a42
26 changed files with 469 additions and 164 deletions

View File

@@ -85,7 +85,7 @@ def get_system_prompt(current_stage: ConversationStage, covered_topics: List[str
covered_topics_str = "".join(covered_topics) if covered_topics else "暂无"
prompt = f"""你是一位资深的人生故事访谈者,专注于帮助用户回忆和讲述人生经历。
prompt = f"""你是「岁月知己」,一位资深的人生故事访谈者,专注于帮助用户回忆和讲述人生经历。
## 角色定位
你如同一位老朋友,用真诚、温暖的方式倾听用户的故事,通过自然的对话引导用户分享更多细节。
@@ -286,7 +286,7 @@ def get_guided_conversation_prompt(
else:
topic_desc = f"你们聊到了「{current_stage_name}」这个话题"
prompt = f"""你是用户的老朋友,正在和他/她聊人生故事。{topic_desc}
prompt = f"""你是「岁月知己」,用户的老朋友,正在和他/她聊人生故事。{topic_desc}
## 已经聊到的内容({current_stage_name}
{filled_slots_str}

View File

@@ -44,7 +44,7 @@ async def get_conversations(
conversation_list.append({
"id": conv.id,
"title": conv.summary[:30] if conv.summary else "回忆录助手", # 使用summary作为标题如果没有则使用默认标题
"title": conv.summary[:30] if conv.summary else "岁月知己", # 使用summary作为标题如果没有则使用默认标题
"avatarUrl": None,
"latestMessagePreview": latest_message or conv.summary,
"latestMessageTime": int(conv.started_at.timestamp() * 1000) if conv.started_at else int(datetime.now(timezone.utc).timestamp() * 1000),

View File

@@ -1,10 +1,10 @@
# 岁月书 Android 应用
# 岁月书 Android 应用
> Life Echo Android 客户端 - 基于 Jetpack Compose 的现代化 Android 应用
## 📱 项目简介
**岁月书 Android 应用**是 Life Echo 平台的移动端客户端,提供流畅的语音对话体验和回忆录管理功能。应用采用现代化的 Android 开发技术栈,实现优雅的用户界面和流畅的交互体验。
**岁月书 Android 应用**是 Life Echo 平台的移动端客户端,提供流畅的语音对话体验和回忆录管理功能。应用采用现代化的 Android 开发技术栈,实现优雅的用户界面和流畅的交互体验。
### 核心功能
@@ -437,4 +437,4 @@ MIT License
---
**岁月书 Android** - 让每一段人生故事都被温柔记录 📱✨
**岁月书 Android** - 让每一段人生故事都被温柔记录 📱✨

View File

@@ -97,7 +97,7 @@ android {
val variant = this
variant.outputs.all {
val output = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
val appName = "岁月"
val appName = "岁月"
val versionName = variant.versionName
val buildType = variant.buildType.name
output.outputFileName = "${appName}_v${versionName}_${buildType}.apk"

View File

@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
@@ -85,9 +84,9 @@ class MainActivity : ComponentActivity() {
windowInsetsController.isAppearanceLightStatusBars = !darkMode
windowInsetsController.isAppearanceLightNavigationBars = !darkMode
}
// 隐藏系统状态栏和导航栏
// 保持状态栏可见,隐藏导航栏
SystemUiController(
isStatusBarVisible = false,
isStatusBarVisible = true,
isNavigationBarVisible = false
)
LifeechoApp(TokenManager.isLoggedIn)
@@ -183,9 +182,8 @@ fun LifeechoApp(initialLoggedIn: Boolean = false) {
currentRoute == Screen.Profile.route
Scaffold(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.statusBars),
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets(0, 0, 0, 0),
bottomBar = {
// 底部导航栏 - 只在特定页面显示
if (shouldShowBottomBar) {

View File

@@ -26,16 +26,16 @@ object MockDataProvider {
return listOf(
ConversationListItemDto(
id = "conv_001",
title = "回忆录助手",
title = "岁月知己",
avatarUrl = null,
latestMessagePreview = "您好!我是您的回忆录助手,很高兴能陪您聊聊往事。您想从哪里开始呢?",
latestMessagePreview = "您好!我是您的岁月知己,很高兴能陪您聊聊往事。您想从哪里开始呢?",
latestMessageTime = System.currentTimeMillis() - 300000, // 5分钟前
unreadCount = 0,
isDefaultAssistant = true
),
ConversationListItemDto(
id = "conv_002",
title = "回忆录助手",
title = "岁月知己",
avatarUrl = null,
latestMessagePreview = "关于您的童年时光,能再详细说说吗?",
latestMessageTime = System.currentTimeMillis() - 86400000, // 1天前
@@ -51,7 +51,7 @@ object MockDataProvider {
MessageDto(
id = "msg_001",
conversationId = conversationId,
content = "您好!我是您的回忆录助手,很高兴能陪您聊聊往事。😊",
content = "您好!我是您的岁月知己,很高兴能陪您聊聊往事。😊",
senderType = "assistant",
timestamp = System.currentTimeMillis() - 3600000,
messageType = "text"
@@ -87,7 +87,7 @@ object MockDataProvider {
fun getMockConversationDetail(id: String): ConversationDetailDto {
return ConversationDetailDto(
id = id,
title = "回忆录助手",
title = "岁月知己",
avatarUrl = null,
userId = "user_001",
startedAt = System.currentTimeMillis() - 3600000,
@@ -348,8 +348,8 @@ object MockDataProvider {
return listOf(
FAQDto(
id = "faq_001",
question = "如何使用回忆录助手",
answer = "打开应用后,点击对话列表中的「回忆录助手开始与AI对话。您可以分享您的故事AI会帮您整理成回忆录。",
question = "如何使用岁月知己",
answer = "打开应用后,点击对话列表中的「岁月知己开始与AI对话。您可以分享您的故事AI会帮您整理成回忆录。",
category = "使用指南",
orderIndex = 1
),

View File

@@ -47,10 +47,12 @@ fun ChatInputField(
var inputMode by remember { mutableStateOf(InputMode.TEXT) }
// 使用 windowInsetsPadding 实现键盘自适应贴合,确保输入框紧贴键盘
// 同时处理导航栏安全区域,避免输入框被手势导航条遮挡
Surface(
modifier = modifier
.fillMaxWidth()
.shadow(4.dp)
.windowInsetsPadding(WindowInsets.navigationBars)
.windowInsetsPadding(WindowInsets.ime), // 自适应键盘高度,紧贴键盘
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)

View File

@@ -16,8 +16,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.huaga.life_echo.R
import com.huaga.life_echo.ui.components.common.MarkdownText
import com.huaga.life_echo.ui.icons.AppIcons
import com.huaga.life_echo.ui.theme.*
@@ -221,21 +223,22 @@ fun MessageAvatar(
isAI: Boolean,
modifier: Modifier = Modifier
) {
if (isAI) {
// 岁月知己头像
androidx.compose.foundation.Image(
painter = painterResource(id = R.drawable.avatar_assistant),
contentDescription = "岁月知己",
modifier = modifier
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
} else {
Box(
modifier = modifier
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
.background(if (isAI) Lavender else BlushPink),
.background(BlushPink),
contentAlignment = Alignment.Center
) {
if (isAI) {
// 使用书本图标表示AI助手
Icon(
imageVector = AppIcons.Book,
contentDescription = "AI助手",
tint = DeepPurple,
modifier = Modifier.size(20.dp)
)
} else {
// 使用人物图标表示用户
Icon(
imageVector = AppIcons.Person,

View File

@@ -16,9 +16,11 @@ 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.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.huaga.life_echo.R
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.ui.components.common.TimeFormatter
import com.huaga.life_echo.ui.icons.AppIcons
@@ -195,13 +197,23 @@ fun ConversationAvatar(
isDefaultAssistant: Boolean,
modifier: Modifier = Modifier
) {
if (isDefaultAssistant) {
// 岁月知己头像
androidx.compose.foundation.Image(
painter = painterResource(id = R.drawable.avatar_assistant),
contentDescription = "岁月知己",
modifier = modifier
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
} else {
Box(
modifier = modifier
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
.background(if (isDefaultAssistant) Lavender else BlushPink),
.background(BlushPink),
contentAlignment = Alignment.Center
) {
if (avatarUrl != null && !isDefaultAssistant) {
if (avatarUrl != null) {
// TODO: 使用 Coil 或 Glide 加载网络图片
Icon(
imageVector = AppIcons.Conversation,
@@ -209,14 +221,6 @@ fun ConversationAvatar(
tint = DeepPurple,
modifier = Modifier.size(24.dp)
)
} else if (isDefaultAssistant) {
// 回忆录助手图标
Icon(
imageVector = AppIcons.Book,
contentDescription = "回忆录助手",
tint = DeepPurple,
modifier = Modifier.size(24.dp)
)
} else {
Icon(
imageVector = AppIcons.Conversation,
@@ -227,3 +231,4 @@ fun ConversationAvatar(
}
}
}
}

View File

@@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.ui.components.common.MarkdownText
@@ -32,17 +33,45 @@ import com.huaga.life_echo.utils.TextUtils
* - 卡片:白色背景, 14dp 圆角, 轻微阴影
* - 编号徽章Lavender 背景, 32x32dp, 10dp 圆角
* - 编号格式:两位数 ("01", "02")
* - 状态文字:已完成用 MediumPurple其他用 SlatePurple
* - 箭头SlatePurple 颜色
* - 空章节:显示柔和的提示,引导用户去聊天
* - 有内容章节:显示标题、状态、页数
*/
@Composable
fun ChapterCard(
chapter: ChapterDto,
isEmpty: Boolean = false,
onClick: () -> Unit,
onGoChat: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
if (isEmpty) {
EmptyChapterCard(
chapter = chapter,
onGoChat = onGoChat,
modifier = modifier
)
} else {
FilledChapterCard(
chapter = chapter,
onClick = onClick,
modifier = modifier
)
}
}
/**
* 有内容的章节卡片
*/
@Composable
private fun FilledChapterCard(
chapter: ChapterDto,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
var isExpanded by remember { mutableStateOf(false) }
val chapterDisplayIndex = chapterOrderToDisplayIndex(chapter.order_index)
val statusText = when (chapter.status) {
"completed" -> "已整理 · 约${estimatePageCount(chapter.content)}"
"partial" -> "部分整理 · 约${estimatePageCount(chapter.content)}"
@@ -77,7 +106,7 @@ fun ChapterCard(
contentAlignment = Alignment.Center
) {
Text(
text = chapter.order_index.toString().padStart(2, '0'),
text = chapterDisplayIndex.toString().padStart(2, '0'),
fontSize = AppTypography.bodyMedium,
fontWeight = FontWeight.SemiBold,
color = DeepPurple
@@ -92,7 +121,9 @@ fun ChapterCard(
text = chapter.title,
fontSize = AppTypography.titleSmall,
fontWeight = FontWeight.Medium,
color = DeepPurple
color = DeepPurple,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
Text(
@@ -102,6 +133,24 @@ fun ChapterCard(
)
}
// 新内容标识
if (chapter.is_new) {
Box(
modifier = Modifier
.padding(end = 8.dp)
.clip(RoundedCornerShape(4.dp))
.background(MediumPurple)
.padding(horizontal = 6.dp, vertical = 2.dp)
) {
Text(
text = "",
fontSize = AppTypography.captionSmall,
color = AppWhite,
fontWeight = FontWeight.Medium
)
}
}
// 箭头
Icon(
imageVector = AppIcons.ChevronRight,
@@ -156,6 +205,100 @@ fun ChapterCard(
}
}
/**
* 空章节卡片 - 没有内容时的占位显示
*/
@Composable
private fun EmptyChapterCard(
chapter: ChapterDto,
onGoChat: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
val chapterDisplayIndex = chapterOrderToDisplayIndex(chapter.order_index)
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = AppWhite.copy(alpha = 0.7f)),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppDimensions.cardPadding),
verticalAlignment = Alignment.CenterVertically
) {
// 章节编号徽章(灰色调)
Box(
modifier = Modifier
.size(AppDimensions.chapterNumberSize)
.clip(RoundedCornerShape(AppDimensions.chapterNumberRadius))
.background(SlatePurple.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Text(
text = chapterDisplayIndex.toString().padStart(2, '0'),
fontSize = AppTypography.bodyMedium,
fontWeight = FontWeight.SemiBold,
color = SlatePurple.copy(alpha = 0.5f)
)
}
Spacer(modifier = Modifier.width(14.dp))
// 章节标题和提示
Column(modifier = Modifier.weight(1f)) {
Text(
text = chapter.title,
fontSize = AppTypography.titleSmall,
fontWeight = FontWeight.Medium,
color = SlatePurple
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "还没有内容,去和岁月知己聊聊吧",
fontSize = AppTypography.captionMedium,
color = SlatePurple.copy(alpha = 0.7f)
)
}
// 去聊天按钮
if (onGoChat != null) {
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.clip(RoundedCornerShape(AppDimensions.smallButtonRadius))
.background(Lavender.copy(alpha = 0.5f))
.clickable { onGoChat() }
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Text(
text = "去聊天",
fontSize = AppTypography.captionMedium,
fontWeight = FontWeight.Medium,
color = DeepPurple.copy(alpha = 0.7f)
)
}
}
}
}
}
/**
* 将 order_index 转换为显示用的序号
* order_index 可能不连续(如 0,1,2,5,6显示时用连续序号
*/
internal fun chapterOrderToDisplayIndex(orderIndex: Int): Int {
return when (orderIndex) {
0 -> 1 // childhood
1 -> 2 // education
2 -> 3 // career
5 -> 4 // family
6 -> 5 // belief
else -> orderIndex + 1
}
}
/**
* 估算页数(基于内容长度)
*/

View File

@@ -33,7 +33,7 @@ fun ChapterReadingView(
item {
// 章节编号
Text(
text = "${chapter.orderIndex}",
text = "${chapterOrderToDisplayIndex(chapter.orderIndex)}",
fontSize = 14.sp,
color = LightPurple,
modifier = Modifier.padding(bottom = 4.dp)

View File

@@ -34,7 +34,9 @@ fun FullTextReadingView(
) {
Box(modifier = modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.statusBars),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp)
) {
chapters.sortedBy { it.orderIndex }.forEachIndexed { index, chapter ->
@@ -47,7 +49,7 @@ fun FullTextReadingView(
// 章节标题
Text(
text = "${chapter.orderIndex}",
text = "${chapterOrderToDisplayIndex(chapter.orderIndex)}",
fontSize = 14.sp,
color = LightPurple,
fontWeight = FontWeight.Medium,
@@ -91,6 +93,7 @@ fun FullTextReadingView(
onClick = onBackClick,
modifier = Modifier
.align(Alignment.BottomEnd)
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(16.dp),
containerColor = LightPurple
) {

View File

@@ -84,7 +84,7 @@ fun AboutScreen(
// 应用名称
Text(
text = "岁月",
text = "岁月",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
@@ -118,7 +118,7 @@ fun AboutScreen(
)
Text(
text = "岁月书是一款专为长辈设计的智能回忆录助手应用帮助老年人轻松记录和整理一生中的美好回忆。通过简单的AI对话方式无需复杂操作就能将珍贵的人生故事转化为精美的个人回忆录。\n\n当然,无论您是什么年龄,都可以使用岁月书来记录属于自己的故事。",
text = "岁月书是一款专为长辈设计的智能回忆录应用帮助老年人轻松记录和整理一生中的美好回忆。通过简单的AI对话方式无需复杂操作就能将珍贵的人生故事转化为精美的个人回忆录。\n\n当然,无论您是什么年龄,都可以使用岁月书来记录属于自己的故事。",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
@@ -196,7 +196,7 @@ fun AboutScreen(
// 版权信息
Text(
text = "© 2026 岁月书. All rights reserved.",
text = "© 2026 岁月书. All rights reserved.",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)

View File

@@ -170,7 +170,7 @@ fun ConversationListScreen(
)
} else {
// 正常模式头部
ColoredHeader(title = "岁月")
ColoredHeader(title = "岁月")
}
// 内容区域
@@ -222,14 +222,17 @@ fun ConversationListScreen(
// 对话列表
items(conversations, key = { it.id }) { conversation ->
// 兼容旧数据:将旧名称"回忆录助手"也识别为默认助手
val isAssistant = conversation.title == null || conversation.title == "回忆录助手"
val displayTitle = if (isAssistant) "岁月知己" else conversation.title!!
val dto = ConversationListItemDto(
id = conversation.id,
title = conversation.title ?: "回忆录助手",
title = displayTitle,
avatarUrl = conversation.avatarUrl,
latestMessagePreview = conversation.latestMessagePreview ?: conversation.summary,
latestMessageTime = conversation.latestMessageTime ?: conversation.startedAt,
unreadCount = 0,
isDefaultAssistant = conversation.title == null
isDefaultAssistant = isAssistant
)
ConversationListItem(
@@ -359,7 +362,7 @@ private fun TipCard(modifier: Modifier = Modifier) {
)
Spacer(modifier = Modifier.padding(start = 8.dp))
Text(
text = "小贴士",
text = "说说你的故事吧",
fontSize = AppTypography.bodyMedium,
fontWeight = FontWeight.Medium,
color = DeepPurple
@@ -369,7 +372,7 @@ private fun TipCard(modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "每天花几分钟聊聊往事,AI 会帮您整理成完整的回忆录。您可以聊童年趣事、求学经历、工作故事,或者任何难忘的回忆",
text = "每天花几分钟聊聊往事,岁月知己会帮您把珍贵的回忆整理成一本专属回忆录。童年趣事、求学时光、工作历程……随时想聊就聊",
fontSize = AppTypography.captionLarge,
color = SlatePurple,
lineHeight = AppTypography.lineHeightNormal

View File

@@ -150,6 +150,7 @@ fun CreateMemoryScreen(
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier.fillMaxSize()
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
@@ -158,7 +159,7 @@ fun CreateMemoryScreen(
) {
// 使用新的ChatHeader组件内部已处理WindowInsets
ChatHeader(
title = "回忆录助手",
title = "岁月知己",
isOnline = connectionStatus == "已连接",
onBackClick = { navController?.popBackStack() },
onNewConversationClick = { viewModel.startConversation() }

View File

@@ -1,6 +1,10 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -93,11 +97,14 @@ fun LoginScreen(
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
contentWindowInsets = WindowInsets(0, 0, 0, 0),
) { paddingValues ->
// 内容区域
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.statusBars)
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(paddingValues)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,

View File

@@ -16,17 +16,17 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.huaga.life_echo.navigation.Screen
import com.huaga.life_echo.network.models.ChapterContentDto
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.ui.components.common.ColoredHeader
import com.huaga.life_echo.ui.components.common.EmptyStateView
import com.huaga.life_echo.ui.components.common.SimpleCard
import com.huaga.life_echo.ui.components.memoir.ChapterCard
import com.huaga.life_echo.ui.components.memoir.ChapterReadingView
import com.huaga.life_echo.ui.components.memoir.FullTextReadingView
@@ -36,6 +36,65 @@ import com.huaga.life_echo.ui.viewmodel.MyMemoirViewModel
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
import kotlinx.coroutines.launch
/**
* 默认章节定义5个人生阶段
* 即使没有从API获取到内容也会显示在页面上
*/
private data class DefaultChapterInfo(
val category: String,
val title: String,
val orderIndex: Int
)
private val DEFAULT_CHAPTERS = listOf(
DefaultChapterInfo("childhood", "童年时光", 0),
DefaultChapterInfo("education", "求学经历", 1),
DefaultChapterInfo("career", "职业生涯", 2),
DefaultChapterInfo("family", "家庭生活", 5),
DefaultChapterInfo("belief", "人生信念", 6),
)
/**
* 合并 API 章节和默认章节占位
* 确保所有5个阶段都有显示有内容的用API数据没内容的用占位
*/
private fun mergeChaptersWithDefaults(apiChapters: List<ChapterDto>): List<ChapterDto> {
val apiCategoryMap = apiChapters.associateBy { it.category }
val result = mutableListOf<ChapterDto>()
for (default in DEFAULT_CHAPTERS) {
val existing = apiCategoryMap[default.category]
if (existing != null) {
result.add(existing)
} else {
// 创建占位章节(空内容)
result.add(
ChapterDto(
id = "placeholder_${default.category}",
title = default.title,
content = "",
order_index = default.orderIndex,
status = "empty",
category = default.category,
images = emptyList(),
updated_at = null,
is_new = false,
source_segments = emptyList()
)
)
}
}
// 添加不在默认列表中的 API 章节(如 career_early, career_achievement 等)
for (apiChapter in apiChapters) {
if (!DEFAULT_CHAPTERS.any { it.category == apiChapter.category }) {
result.add(apiChapter)
}
}
return result.sortedBy { it.order_index }
}
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.O)
@Composable
@@ -72,9 +131,9 @@ fun MyMemoirScreen(
viewModel.refreshChapters()
}
// 转换Chapter为ChapterDto用于显示
// 转换Chapter为ChapterDto并与默认章节合并
val chapterDtos = remember(chapters, chapterDtosFromApi) {
if (chapterDtosFromApi.isNotEmpty()) {
val apiChapters = if (chapterDtosFromApi.isNotEmpty()) {
chapterDtosFromApi
} else {
chapters.map { chapter ->
@@ -92,6 +151,12 @@ fun MyMemoirScreen(
)
}
}
mergeChaptersWithDefaults(apiChapters)
}
// 有内容的章节(用于全文阅读和浮动按钮)
val contentChapters = remember(chapterDtos) {
chapterDtos.filter { it.content.isNotBlank() }
}
AnimatedContent(
@@ -109,9 +174,9 @@ fun MyMemoirScreen(
) { isReading ->
if (isReading) {
if (showFullTextReading) {
// 全文阅读视图
// 全文阅读视图(只包含有内容的章节)
FullTextReadingView(
chapters = chapterDtos.map { dto ->
chapters = contentChapters.map { dto ->
ChapterContentDto(
id = dto.id,
title = dto.title,
@@ -193,17 +258,26 @@ fun MyMemoirScreen(
MemoirTableOfContents(
bookInfo = bookInfo,
chapterDtos = chapterDtos,
contentChapterCount = contentChapters.size,
isRefreshing = isRefreshing,
onRefresh = { handleRefresh() },
onChapterClick = { chapterDto ->
// 只有有内容的章节才能点击进入阅读
if (chapterDto.content.isNotBlank()) {
chapters.find { it.id == chapterDto.id }?.let { chapter ->
viewModel.selectChapter(chapter)
}
}
},
onReadAllClick = { viewModel.toggleFullTextReading() },
onTitleChange = { title -> viewModel.updateBookTitle(title, null) },
onSubtitleChange = { subtitle ->
bookInfo?.let { viewModel.updateBookTitle(it.title, subtitle) }
},
onNavigateToChat = {
navController?.navigate(Screen.ConversationList.route) {
popUpTo(Screen.ConversationList.route) { inclusive = true }
}
}
)
}
@@ -218,12 +292,14 @@ fun MyMemoirScreen(
private fun MemoirTableOfContents(
bookInfo: com.huaga.life_echo.network.models.BookDto?,
chapterDtos: List<ChapterDto>,
contentChapterCount: Int,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onChapterClick: (ChapterDto) -> Unit,
onReadAllClick: () -> Unit,
onTitleChange: (String) -> Unit,
onSubtitleChange: (String) -> Unit
onSubtitleChange: (String) -> Unit,
onNavigateToChat: () -> Unit
) {
Box(
modifier = Modifier
@@ -260,31 +336,34 @@ private fun MemoirTableOfContents(
// 书籍信息卡片
item {
BookHeaderCard(
title = bookInfo?.title ?: "这一生",
subtitle = "我的回忆录",
updatedAt = bookInfo?.let { "更新于 2 分钟前" } ?: "",
modifier = Modifier.padding(bottom = AppDimensions.sectionSpacing)
title = bookInfo?.title ?: "我的回忆录",
subtitle = if (contentChapterCount > 0) {
"已记录 $contentChapterCount 个章节"
} else {
"开始和岁月知己聊天吧"
},
updatedAt = bookInfo?.let { "下拉刷新获取最新内容" } ?: "",
modifier = Modifier.padding(bottom = 8.dp)
)
}
// 如果没有章节,显示空状态
if (chapterDtos.isEmpty()) {
// 章节进度提示
item {
EmptyStateView(
title = "还没有章节",
message = "开始对话让AI帮您整理回忆录章节",
icon = "📖",
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp)
ChapterProgressHint(
totalChapters = chapterDtos.size,
filledChapters = contentChapterCount,
modifier = Modifier.padding(bottom = AppDimensions.itemSpacing)
)
}
} else {
// 章节列表
items(chapterDtos.sortedBy { it.order_index }, key = { it.id }) { chapterDto ->
// 章节列表(始终显示所有章节)
items(chapterDtos, key = { it.id }) { chapterDto ->
val isEmpty = chapterDto.content.isBlank()
ChapterCard(
chapter = chapterDto,
onClick = { onChapterClick(chapterDto) }
isEmpty = isEmpty,
onClick = { onChapterClick(chapterDto) },
onGoChat = if (isEmpty) onNavigateToChat else null
)
Spacer(modifier = Modifier.height(AppDimensions.itemSpacing))
}
@@ -296,20 +375,70 @@ private fun MemoirTableOfContents(
}
}
}
}
// 浮动阅读全文按钮
if (chapterDtos.isNotEmpty()) {
// 浮动阅读全文按钮(只在有内容的章节时显示)
if (contentChapterCount > 0) {
FloatingReadAllButton(
onClick = onReadAllClick,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 100.dp) // 底部导航栏留出空间
.padding(bottom = 64.dp) // 紧贴底部导航栏上方
)
}
}
}
/**
* 章节进度提示条
*/
@Composable
private fun ChapterProgressHint(
totalChapters: Int,
filledChapters: Int,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Lavender.copy(alpha = 0.3f))
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
// 进度指示器
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
repeat(totalChapters) { index ->
Box(
modifier = Modifier
.size(width = if (index < filledChapters) 16.dp else 8.dp, height = 4.dp)
.clip(RoundedCornerShape(2.dp))
.background(
if (index < filledChapters) MediumPurple
else SlatePurple.copy(alpha = 0.3f)
)
)
}
}
Text(
text = if (filledChapters == 0) {
"还没有章节内容,开始聊天吧"
} else if (filledChapters < totalChapters) {
"已完成 $filledChapters/$totalChapters 个章节"
} else {
"所有章节已完成"
},
fontSize = AppTypography.captionMedium,
color = SlatePurple,
modifier = Modifier.weight(1f)
)
}
}
/**
* 书籍头部卡片
*/
@@ -320,9 +449,8 @@ private fun BookHeaderCard(
updatedAt: String,
modifier: Modifier = Modifier
) {
SimpleCard(modifier = modifier) {
Column(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.padding(vertical = AppDimensions.sectionSpacing),
horizontalAlignment = Alignment.CenterHorizontally
@@ -345,18 +473,16 @@ private fun BookHeaderCard(
)
if (updatedAt.isNotEmpty()) {
Spacer(modifier = Modifier.height(6.dp))
Spacer(modifier = Modifier.height(4.dp))
Text(
text = updatedAt,
fontSize = AppTypography.captionMedium,
color = MediumPurple,
color = MediumPurple.copy(alpha = 0.7f),
textAlign = TextAlign.Center
)
}
}
}
}
/**
* 浮动阅读全文按钮

View File

@@ -1,6 +1,10 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
@@ -56,10 +60,13 @@ fun NicknameSetupScreen(
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
contentWindowInsets = WindowInsets(0, 0, 0, 0),
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.statusBars)
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(paddingValues)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -77,7 +84,7 @@ fun NicknameSetupScreen(
// 标题
Text(
text = "欢迎加入岁月",
text = "欢迎加入岁月",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface

View File

@@ -1,6 +1,10 @@
package com.huaga.life_echo.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
@@ -96,11 +100,14 @@ fun RegisterScreen(
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
contentWindowInsets = WindowInsets(0, 0, 0, 0),
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.statusBars)
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(paddingValues)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,

View File

@@ -163,7 +163,7 @@ class AuthViewModel(private val context: Context) : ViewModel() {
_operationResult.value = OperationResult(
success = true,
message = "注册成功",
details = "账号已创建,欢迎加入岁月书!正在跳转..."
details = "账号已创建,欢迎加入岁月书!正在跳转..."
)
},
onFailure = { exception ->
@@ -378,7 +378,7 @@ class AuthViewModel(private val context: Context) : ViewModel() {
success = true,
message = if (isNewUser) "注册成功" else "登录成功",
details = if (isNewUser) {
"账号已创建,欢迎加入岁月书!正在跳转..."
"账号已创建,欢迎加入岁月书!正在跳转..."
} else {
"欢迎回来,${userNickname}!正在跳转..."
}
@@ -440,7 +440,7 @@ class AuthViewModel(private val context: Context) : ViewModel() {
_operationResult.value = OperationResult(
success = true,
message = "注册成功",
details = "账号已创建,欢迎加入岁月书!正在跳转..."
details = "账号已创建,欢迎加入岁月书!正在跳转..."
)
},
onFailure = { exception ->
@@ -662,7 +662,7 @@ class AuthViewModel(private val context: Context) : ViewModel() {
_operationResult.value = OperationResult(
success = true,
message = "欢迎",
details = "欢迎加入岁月书,${userResponse.nickname}"
details = "欢迎加入岁月书,${userResponse.nickname}"
)
onSuccess()
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,5 +1,5 @@
<resources>
<string name="app_name">岁月时书</string>
<string name="app_name">Life Echo</string>
<!-- Navigation -->
<string name="nav_chat">Chat</string>

View File

@@ -1,5 +1,5 @@
<resources>
<string name="app_name">岁月</string>
<string name="app_name">岁月</string>
<!-- Navigation -->
<string name="nav_chat">聊天</string>

View File

@@ -13,7 +13,7 @@ export function ChapterContent({ chapter }: ChapterContentProps) {
if (!chapter.content) {
return (
<Text style={styles.emptyText}>
</Text>
);
}

View File

@@ -14,7 +14,7 @@ function renderChapterContent(content: string | undefined) {
if (!content) {
return (
<Text style={styles.emptyText}>
</Text>
);
}

View File

@@ -56,7 +56,7 @@ export interface SettingItem {
export const mockConversations: Conversation[] = [
{
id: '1',
title: '回忆录助手',
title: '岁月知己',
avatarEmoji: '📖',
lastMessage: '您想从哪里开始呢?可以聊聊童年...',
timestamp: '刚刚',
@@ -70,7 +70,7 @@ export const mockMessages: Message[] = [
conversationId: '1',
senderType: 'ai',
contentType: 'text',
content: '您好!我是您的回忆录助手,很高兴能陪您聊聊往事。😊\n\n您想从哪里开始呢可以聊聊童年、上学时光或者任何您想分享的故事。',
content: '您好!我是您的岁月知己,很高兴能陪您聊聊往事。😊\n\n您想从哪里开始呢可以聊聊童年、上学时光或者任何您想分享的故事。',
timestamp: '14:30',
},
];