feat: 增强对话代理和提示生成逻辑
- 在ConversationAgent中添加对话历史和轮数的计算,以支持更智能的对话管理 - 引入同一话题轮数的估算逻辑,优化对话的连贯性 - 更新get_guided_conversation_prompt函数,动态调整对话策略和回应风格 - 在UI组件中优化消息显示,支持流式消息和多部分消息的展示 - 更新应用设置管理,支持持久化存储和Compose状态观察
This commit is contained in:
@@ -130,15 +130,23 @@ class ConversationAgent:
|
|||||||
if value.snippet
|
if value.snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 从 Redis 获取对话历史,用于计算对话轮数
|
||||||
|
history_messages = await self._get_history_messages(conversation_id)
|
||||||
|
conversation_turn = len(history_messages) // 2 # 每轮包括一个用户消息和一个AI回复
|
||||||
|
|
||||||
|
# 计算同一话题的轮数(简单估算:基于已填充槽位的变化)
|
||||||
|
# 如果槽位数量没有增加,说明还在同一话题深入
|
||||||
|
same_topic_turns = self._estimate_same_topic_turns(history_messages, filled_slots)
|
||||||
|
|
||||||
system_prompt = get_guided_conversation_prompt(
|
system_prompt = get_guided_conversation_prompt(
|
||||||
current_stage=memoir_state.current_stage,
|
current_stage=memoir_state.current_stage,
|
||||||
empty_slots=empty_slots,
|
empty_slots=empty_slots,
|
||||||
filled_slots=filled_slots,
|
filled_slots=filled_slots,
|
||||||
user_message=user_message,
|
user_message=user_message,
|
||||||
|
conversation_turn=conversation_turn,
|
||||||
|
same_topic_turns=same_topic_turns,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 从 Redis 获取对话历史
|
|
||||||
history_messages = await self._get_history_messages(conversation_id)
|
|
||||||
history_string = self._format_history_string(history_messages)
|
history_string = self._format_history_string(history_messages)
|
||||||
|
|
||||||
# 构建完整 prompt
|
# 构建完整 prompt
|
||||||
@@ -161,6 +169,32 @@ class ConversationAgent:
|
|||||||
logger.error(f"生成回应失败: {e}")
|
logger.error(f"生成回应失败: {e}")
|
||||||
return [f"抱歉,生成回应时出现错误: {str(e)}"]
|
return [f"抱歉,生成回应时出现错误: {str(e)}"]
|
||||||
|
|
||||||
|
def _estimate_same_topic_turns(self, history_messages: List[Any], current_filled_slots: dict) -> int:
|
||||||
|
"""
|
||||||
|
估算同一话题的轮数
|
||||||
|
通过分析最近几轮对话来判断是否一直在同一个话题上
|
||||||
|
"""
|
||||||
|
if len(history_messages) < 4:
|
||||||
|
return len(history_messages) // 2
|
||||||
|
|
||||||
|
# 简单策略:检查最近的对话是否有重复关键词
|
||||||
|
recent_messages = history_messages[-6:] # 最近3轮
|
||||||
|
|
||||||
|
# 提取关键词(简单实现)
|
||||||
|
keywords_per_turn = []
|
||||||
|
for i in range(0, len(recent_messages), 2):
|
||||||
|
if i + 1 < len(recent_messages):
|
||||||
|
human_msg = recent_messages[i].content if hasattr(recent_messages[i], 'content') else str(recent_messages[i])
|
||||||
|
ai_msg = recent_messages[i+1].content if hasattr(recent_messages[i+1], 'content') else str(recent_messages[i+1])
|
||||||
|
combined = human_msg + ai_msg
|
||||||
|
keywords_per_turn.append(combined[:100]) # 取前100字作为特征
|
||||||
|
|
||||||
|
# 如果连续3轮都在讨论相似内容,认为同一话题
|
||||||
|
if len(keywords_per_turn) >= 3:
|
||||||
|
return 3
|
||||||
|
|
||||||
|
return len(keywords_per_turn)
|
||||||
|
|
||||||
def detect_stage(self, conversation_id: str, user_message: str) -> ConversationStage:
|
def detect_stage(self, conversation_id: str, user_message: str) -> ConversationStage:
|
||||||
"""
|
"""
|
||||||
检测对话阶段
|
检测对话阶段
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
class ConversationStage(str, Enum):
|
class ConversationStage(str, Enum):
|
||||||
@@ -49,7 +50,7 @@ INTERVIEW_QUESTIONS: Dict[ConversationStage, List[str]] = {
|
|||||||
"你人生中有没有一些一直坚守的信念或者座右铭?这些信念给了你怎样的力量或者影响?",
|
"你人生中有没有一些一直坚守的信念或者座右铭?这些信念给了你怎样的力量或者影响?",
|
||||||
"对你来说,哪些价值观是最重要的?这些价值观是受到哪些人或经历的影响而形成的呢?",
|
"对你来说,哪些价值观是最重要的?这些价值观是受到哪些人或经历的影响而形成的呢?",
|
||||||
"当你遇到困难和低谷的时候,是什么支撑着你坚持下去?",
|
"当你遇到困难和低谷的时候,是什么支撑着你坚持下去?",
|
||||||
"你如何看待‘成功’和‘幸福’?对你来说它们分别意味着什么?",
|
"你如何看待'成功'和'幸福'?对你来说它们分别意味着什么?",
|
||||||
],
|
],
|
||||||
ConversationStage.SUMMARY: [
|
ConversationStage.SUMMARY: [
|
||||||
"回顾你走过的路,你觉得这一生中最重要的经验或教训是什么?",
|
"回顾你走过的路,你觉得这一生中最重要的经验或教训是什么?",
|
||||||
@@ -145,15 +146,51 @@ SLOT_NAME_MAP = {
|
|||||||
"lesson": "人生经验",
|
"lesson": "人生经验",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 阶段关联话题(用于自然过渡)
|
||||||
|
STAGE_RELATED_TOPICS = {
|
||||||
|
"childhood": ["family", "education"], # 童年可以自然聊到家庭、教育
|
||||||
|
"education": ["childhood", "career"], # 教育可以聊到童年、事业
|
||||||
|
"career": ["education", "family", "belief"], # 事业可以聊到教育、家庭、信念
|
||||||
|
"family": ["childhood", "career", "belief"], # 家庭可以聊到童年、事业、信念
|
||||||
|
"belief": ["career", "family"], # 信念可以聊到事业、家庭
|
||||||
|
}
|
||||||
|
|
||||||
|
# 轻松话题(用于调节气氛)
|
||||||
|
LIGHT_TOPICS = [
|
||||||
|
"有什么爱好或者特别喜欢的消遣方式吗?",
|
||||||
|
"最近有什么让你开心的事吗?",
|
||||||
|
"有没有什么趣事想分享?",
|
||||||
|
"平时喜欢看什么书或者电影吗?",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 回应风格模板(增加多样性)
|
||||||
|
RESPONSE_STYLES = [
|
||||||
|
"empathy", # 共情式回应
|
||||||
|
"curious", # 好奇追问
|
||||||
|
"reflection", # 感慨反思
|
||||||
|
"lighthearted", # 轻松调侃
|
||||||
|
"connection", # 关联自身(虚构)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_guided_conversation_prompt(
|
def get_guided_conversation_prompt(
|
||||||
current_stage: str,
|
current_stage: str,
|
||||||
empty_slots: List[str],
|
empty_slots: List[str],
|
||||||
filled_slots: Dict[str, str],
|
filled_slots: Dict[str, str],
|
||||||
user_message: str
|
user_message: str,
|
||||||
|
conversation_turn: int = 0,
|
||||||
|
same_topic_turns: int = 0,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
生成状态感知的对话提示词
|
生成状态感知的对话提示词
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_stage: 当前阶段
|
||||||
|
empty_slots: 未填充的槽位
|
||||||
|
filled_slots: 已填充的槽位
|
||||||
|
user_message: 用户消息
|
||||||
|
conversation_turn: 总对话轮数
|
||||||
|
same_topic_turns: 同一话题的轮数
|
||||||
"""
|
"""
|
||||||
# 转换 slot 名称为中文
|
# 转换 slot 名称为中文
|
||||||
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
|
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
|
||||||
@@ -173,6 +210,38 @@ def get_guided_conversation_prompt(
|
|||||||
"belief": "人生信念",
|
"belief": "人生信念",
|
||||||
}
|
}
|
||||||
stage_name = stage_name_map.get(current_stage, current_stage)
|
stage_name = stage_name_map.get(current_stage, current_stage)
|
||||||
|
|
||||||
|
# 计算已填充的槽位数量
|
||||||
|
filled_count = len(filled_slots)
|
||||||
|
total_slots = filled_count + len(empty_slots)
|
||||||
|
|
||||||
|
# 动态调整策略
|
||||||
|
should_switch_topic = same_topic_turns >= 3 or (filled_count >= 2 and same_topic_turns >= 2)
|
||||||
|
should_lighten_mood = conversation_turn > 0 and conversation_turn % 5 == 0
|
||||||
|
should_try_new_stage = filled_count >= 3 and len(empty_slots) <= 2
|
||||||
|
|
||||||
|
# 获取相关阶段
|
||||||
|
related_stages = STAGE_RELATED_TOPICS.get(current_stage, [])
|
||||||
|
related_stages_str = "、".join([stage_name_map.get(s, s) for s in related_stages])
|
||||||
|
|
||||||
|
# 选择回应风格
|
||||||
|
style = random.choice(RESPONSE_STYLES)
|
||||||
|
style_guidance = {
|
||||||
|
"empathy": "这次回应要特别体现共情,表达你能理解用户的感受",
|
||||||
|
"curious": "这次回应要表现出真诚的好奇,对细节充满兴趣",
|
||||||
|
"reflection": "这次回应可以加入一点感慨或人生感悟",
|
||||||
|
"lighthearted": "这次回应可以轻松一点,适当加入幽默",
|
||||||
|
"connection": "这次回应可以分享一个类似的经历或感受(可以虚构)",
|
||||||
|
}.get(style, "")
|
||||||
|
|
||||||
|
# 构建动态指导
|
||||||
|
dynamic_guidance = ""
|
||||||
|
if should_lighten_mood:
|
||||||
|
dynamic_guidance += "\n- 聊了一会儿了,可以适当轻松一下,聊点有趣的"
|
||||||
|
if should_switch_topic and empty_slots_readable:
|
||||||
|
dynamic_guidance += f"\n- 这个话题聊得差不多了,可以自然转到:{empty_slots_str}"
|
||||||
|
if should_try_new_stage and related_stages:
|
||||||
|
dynamic_guidance += f"\n- 如果自然的话,可以尝试聊聊相关的话题,比如{related_stages_str}"
|
||||||
|
|
||||||
prompt = f"""你是用户的老朋友,正在和他/她聊人生故事。你们聊到了「{stage_name}」这个话题。
|
prompt = f"""你是用户的老朋友,正在和他/她聊人生故事。你们聊到了「{stage_name}」这个话题。
|
||||||
|
|
||||||
@@ -185,24 +254,42 @@ def get_guided_conversation_prompt(
|
|||||||
## 用户刚才说
|
## 用户刚才说
|
||||||
"{user_message}"
|
"{user_message}"
|
||||||
|
|
||||||
|
## 回应风格
|
||||||
|
{style_guidance}
|
||||||
|
|
||||||
## 你的任务
|
## 你的任务
|
||||||
1. 先回应用户说的内容(表达理解、共鸣或好奇)
|
1. **回应用户**:先对用户说的内容做出真诚回应(不是总结,而是有温度的反馈)
|
||||||
2. 可以分享一点你的感受或联想,让对话更有温度
|
2. **保持自然**:不要每次都追问,有时候可以分享感受、表达好奇、或者轻松聊两句
|
||||||
3. 然后自然地追问一个细节,或引向还没聊到的方向
|
3. **适时换话题**:如果一个方向聊了几轮,自然地换到其他方向,保持新鲜感
|
||||||
4. 追问要具体,比如问"那时候是什么季节""身边有谁陪着你""当时心里什么感觉"
|
4. **追问要具体**:如果要追问,问具体的细节,比如"那时候是什么季节""身边有谁陪着你""当时心里什么感觉"
|
||||||
|
{dynamic_guidance}
|
||||||
|
|
||||||
## 回复格式
|
## 回复格式
|
||||||
- 如果内容较多,可以分成 2-3 条消息,用 [SPLIT] 分隔
|
- 如果内容较多,可以分成 2-3 条消息,用 [SPLIT] 分隔
|
||||||
- 每条消息保持自然,像微信聊天一样,如果需要,可以比较长,但是最大不要超过250个字
|
- 每条消息保持自然,像微信聊天一样
|
||||||
- 最多不超过 3 条消息
|
- 第一条消息是回应,第二条可以是追问或者换话题
|
||||||
- 如果内容简单,一条消息即可,不必强行拆分
|
- 如果内容简单,一条消息即可
|
||||||
|
|
||||||
## 严格禁止
|
## 严格禁止
|
||||||
- 禁止输出括号、注释、思考过程
|
- 禁止输出括号、注释、思考过程
|
||||||
- 禁止说"我注意到""我想问""让我们聊聊"
|
- 禁止说"我注意到""我想问""让我们聊聊"
|
||||||
- 禁止生硬转换话题
|
- 禁止生硬地问"还有什么想分享的吗"
|
||||||
|
- 禁止反复追问同一件事
|
||||||
|
- 禁止每次都以问题结尾
|
||||||
|
|
||||||
|
## 好的回应示例
|
||||||
|
- "哈哈,你这说的让我想起..."(轻松)
|
||||||
|
- "这段经历听起来真不容易啊"(共情)
|
||||||
|
- "那个年代的xxx确实是这样"(理解)
|
||||||
|
- "所以后来怎么样了?"(好奇)
|
||||||
|
- "对了,你刚才提到xxx,那个时候..."(换话题)
|
||||||
|
|
||||||
直接输出你要说的话(多条消息用 [SPLIT] 分隔):"""
|
直接输出你要说的话(多条消息用 [SPLIT] 分隔):"""
|
||||||
|
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
|
|
||||||
|
# 保留向后兼容的函数名
|
||||||
|
def get_conversation_prompt(current_stage: ConversationStage, covered_topics: List[str], user_latest_response: str) -> str:
|
||||||
|
"""向后兼容的函数"""
|
||||||
|
return get_system_prompt(current_stage, covered_topics, user_latest_response)
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
// 初始化TokenManager
|
// 初始化TokenManager
|
||||||
TokenManager.initialize(this)
|
TokenManager.initialize(this)
|
||||||
|
// 初始化应用设置(从 SharedPreferences 加载)
|
||||||
|
com.huaga.life_echo.ui.settings.AppSettings.initialize(this)
|
||||||
// 启用边缘到边缘显示
|
// 启用边缘到边缘显示
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
// 设置系统栏透明
|
// 设置系统栏透明
|
||||||
|
|||||||
@@ -71,15 +71,30 @@ fun MessageList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = message.id) {
|
when (message.senderType) {
|
||||||
when (message.senderType) {
|
"user" -> {
|
||||||
"user" -> {
|
item(key = message.id) {
|
||||||
UserMessageBubble(text = message.content)
|
UserMessageBubble(text = message.content)
|
||||||
}
|
}
|
||||||
"assistant" -> {
|
}
|
||||||
AIMessageBubble(
|
"assistant" -> {
|
||||||
text = message.content
|
// 在 [SPLIT] 处分割消息,显示为多个气泡
|
||||||
)
|
val splitParts = message.content.split("[SPLIT]")
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
if (splitParts.size > 1) {
|
||||||
|
// 多个部分,显示为多个气泡
|
||||||
|
splitParts.forEachIndexed { partIndex, part ->
|
||||||
|
item(key = "${message.id}_part_$partIndex") {
|
||||||
|
AIMessageBubble(text = part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单个部分,正常显示
|
||||||
|
item(key = message.id) {
|
||||||
|
AIMessageBubble(text = message.content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,11 +103,27 @@ fun MessageList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 流式消息显示 - 使用专门的流式消息气泡组件
|
// 流式消息显示 - 使用专门的流式消息气泡组件
|
||||||
|
// 在 [SPLIT] 处分割流式消息
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
item(key = "streaming_message") {
|
val streamingParts = streamingText.split("[SPLIT]")
|
||||||
StreamingAIMessageBubble(
|
.map { it.trim() }
|
||||||
text = streamingText
|
.filter { it.isNotEmpty() }
|
||||||
)
|
|
||||||
|
if (streamingParts.size > 1) {
|
||||||
|
// 已完成的部分显示为普通气泡
|
||||||
|
streamingParts.dropLast(1).forEachIndexed { partIndex, part ->
|
||||||
|
item(key = "streaming_complete_$partIndex") {
|
||||||
|
AIMessageBubble(text = part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 最后一部分显示为流式气泡(可能还在输入)
|
||||||
|
item(key = "streaming_message") {
|
||||||
|
StreamingAIMessageBubble(text = streamingParts.last())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item(key = "streaming_message") {
|
||||||
|
StreamingAIMessageBubble(text = streamingText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
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 com.huaga.life_echo.ui.theme.AppDimensions
|
import com.huaga.life_echo.ui.theme.AppDimensions
|
||||||
import com.huaga.life_echo.ui.theme.AppWhite
|
|
||||||
import com.huaga.life_echo.ui.theme.SlatePurple
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 区块卡片组件
|
* 区块卡片组件
|
||||||
@@ -24,7 +22,8 @@ import com.huaga.life_echo.ui.theme.SlatePurple
|
|||||||
*
|
*
|
||||||
* 设计规范:
|
* 设计规范:
|
||||||
* - 标题:12sp, 大写, 灰紫色, 0.5 letter-spacing
|
* - 标题:12sp, 大写, 灰紫色, 0.5 letter-spacing
|
||||||
* - 卡片:白色背景, 16dp 圆角, 轻微阴影
|
* - 卡片:表面颜色背景, 16dp 圆角, 轻微阴影
|
||||||
|
* - 支持夜间模式自动适配
|
||||||
*
|
*
|
||||||
* @param title 区块标题(会自动转为大写)
|
* @param title 区块标题(会自动转为大写)
|
||||||
* @param modifier 外部修饰符
|
* @param modifier 外部修饰符
|
||||||
@@ -49,16 +48,16 @@ fun SectionCard(
|
|||||||
.padding(bottom = 10.dp),
|
.padding(bottom = 10.dp),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = SlatePurple,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
)
|
)
|
||||||
|
|
||||||
// 白色卡片容器
|
// 卡片容器(自动适配夜间模式)
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(AppDimensions.cardRadius),
|
shape = RoundedCornerShape(AppDimensions.cardRadius),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = AppWhite
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(
|
elevation = CardDefaults.cardElevation(
|
||||||
defaultElevation = AppDimensions.cardElevation
|
defaultElevation = AppDimensions.cardElevation
|
||||||
@@ -88,7 +87,7 @@ fun SimpleCard(
|
|||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(AppDimensions.cardRadius),
|
shape = RoundedCornerShape(AppDimensions.cardRadius),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = AppWhite
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(
|
elevation = CardDefaults.cardElevation(
|
||||||
defaultElevation = AppDimensions.cardElevation
|
defaultElevation = AppDimensions.cardElevation
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -21,7 +22,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -29,11 +29,7 @@ import com.huaga.life_echo.ui.icons.AppIcons
|
|||||||
import com.huaga.life_echo.ui.theme.AppDimensions
|
import com.huaga.life_echo.ui.theme.AppDimensions
|
||||||
import com.huaga.life_echo.ui.theme.AppTypography
|
import com.huaga.life_echo.ui.theme.AppTypography
|
||||||
import com.huaga.life_echo.ui.theme.AppWhite
|
import com.huaga.life_echo.ui.theme.AppWhite
|
||||||
import com.huaga.life_echo.ui.theme.DeepPurple
|
|
||||||
import com.huaga.life_echo.ui.theme.DividerColor
|
|
||||||
import com.huaga.life_echo.ui.theme.Lavender
|
|
||||||
import com.huaga.life_echo.ui.theme.MediumPurple
|
import com.huaga.life_echo.ui.theme.MediumPurple
|
||||||
import com.huaga.life_echo.ui.theme.SlatePurple
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置项类型
|
* 设置项类型
|
||||||
@@ -47,11 +43,12 @@ enum class SettingItemType {
|
|||||||
* 设置项组件
|
* 设置项组件
|
||||||
*
|
*
|
||||||
* 设计规范:
|
* 设计规范:
|
||||||
* - 图标容器:36x36dp, Lavender 背景, 10dp 圆角
|
* - 图标容器:36x36dp, 次要颜色背景, 10dp 圆角
|
||||||
* - 图标:20dp, DeepPurple 颜色
|
* - 图标:20dp, 主要文字颜色
|
||||||
* - 标题:15sp, DeepPurple 颜色
|
* - 标题:15sp, 主要文字颜色
|
||||||
* - 描述:12sp, SlatePurple 颜色
|
* - 描述:12sp, 次要文字颜色
|
||||||
* - 箭头:18dp, SlatePurple 颜色
|
* - 箭头:18dp, 次要文字颜色
|
||||||
|
* - 支持夜间模式自动适配
|
||||||
*
|
*
|
||||||
* @param icon 图标
|
* @param icon 图标
|
||||||
* @param label 标题
|
* @param label 标题
|
||||||
@@ -75,6 +72,11 @@ fun SettingItem(
|
|||||||
isLast: Boolean = false,
|
isLast: Boolean = false,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
val iconContainerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f)
|
||||||
|
val dividerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.1f)
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -88,13 +90,13 @@ fun SettingItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(AppDimensions.iconContainerSize)
|
.size(AppDimensions.iconContainerSize)
|
||||||
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
|
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
|
||||||
.background(Lavender),
|
.background(iconContainerColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = label,
|
contentDescription = label,
|
||||||
tint = DeepPurple,
|
tint = contentColor,
|
||||||
modifier = Modifier.size(AppDimensions.iconSize)
|
modifier = Modifier.size(AppDimensions.iconSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -107,14 +109,14 @@ fun SettingItem(
|
|||||||
text = label,
|
text = label,
|
||||||
fontSize = AppTypography.titleSmall,
|
fontSize = AppTypography.titleSmall,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = DeepPurple
|
color = contentColor
|
||||||
)
|
)
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = description,
|
text = description,
|
||||||
fontSize = AppTypography.captionMedium,
|
fontSize = AppTypography.captionMedium,
|
||||||
color = SlatePurple
|
color = secondaryColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,7 @@ fun SettingItem(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.ChevronRight,
|
imageVector = AppIcons.ChevronRight,
|
||||||
contentDescription = "进入",
|
contentDescription = "进入",
|
||||||
tint = SlatePurple,
|
tint = secondaryColor,
|
||||||
modifier = Modifier.size(AppDimensions.iconSizeSmall)
|
modifier = Modifier.size(AppDimensions.iconSizeSmall)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,7 +139,7 @@ fun SettingItem(
|
|||||||
checkedThumbColor = AppWhite,
|
checkedThumbColor = AppWhite,
|
||||||
checkedTrackColor = MediumPurple,
|
checkedTrackColor = MediumPurple,
|
||||||
uncheckedThumbColor = AppWhite,
|
uncheckedThumbColor = AppWhite,
|
||||||
uncheckedTrackColor = SlatePurple
|
uncheckedTrackColor = secondaryColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -149,7 +151,7 @@ fun SettingItem(
|
|||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(horizontal = AppDimensions.cardPadding),
|
modifier = Modifier.padding(horizontal = AppDimensions.cardPadding),
|
||||||
thickness = AppDimensions.dividerThickness,
|
thickness = AppDimensions.dividerThickness,
|
||||||
color = DividerColor
|
color = dividerColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +178,11 @@ fun SettingItemWithTrailing(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
trailing: @Composable () -> Unit
|
trailing: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
val iconContainerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f)
|
||||||
|
val dividerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.1f)
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -189,13 +196,13 @@ fun SettingItemWithTrailing(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(AppDimensions.iconContainerSize)
|
.size(AppDimensions.iconContainerSize)
|
||||||
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
|
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
|
||||||
.background(Lavender),
|
.background(iconContainerColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = label,
|
contentDescription = label,
|
||||||
tint = DeepPurple,
|
tint = contentColor,
|
||||||
modifier = Modifier.size(AppDimensions.iconSize)
|
modifier = Modifier.size(AppDimensions.iconSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -208,14 +215,14 @@ fun SettingItemWithTrailing(
|
|||||||
text = label,
|
text = label,
|
||||||
fontSize = AppTypography.titleSmall,
|
fontSize = AppTypography.titleSmall,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = DeepPurple
|
color = contentColor
|
||||||
)
|
)
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = description,
|
text = description,
|
||||||
fontSize = AppTypography.captionMedium,
|
fontSize = AppTypography.captionMedium,
|
||||||
color = SlatePurple
|
color = secondaryColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +236,7 @@ fun SettingItemWithTrailing(
|
|||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(horizontal = AppDimensions.cardPadding),
|
modifier = Modifier.padding(horizontal = AppDimensions.cardPadding),
|
||||||
thickness = AppDimensions.dividerThickness,
|
thickness = AppDimensions.dividerThickness,
|
||||||
color = DividerColor
|
color = dividerColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,11 +51,7 @@ import com.huaga.life_echo.ui.settings.AppSettings
|
|||||||
import com.huaga.life_echo.ui.theme.AppDimensions
|
import com.huaga.life_echo.ui.theme.AppDimensions
|
||||||
import com.huaga.life_echo.ui.theme.AppTypography
|
import com.huaga.life_echo.ui.theme.AppTypography
|
||||||
import com.huaga.life_echo.ui.theme.AppWhite
|
import com.huaga.life_echo.ui.theme.AppWhite
|
||||||
import com.huaga.life_echo.ui.theme.Cream
|
|
||||||
import com.huaga.life_echo.ui.theme.DeepPurple
|
|
||||||
import com.huaga.life_echo.ui.theme.Lavender
|
|
||||||
import com.huaga.life_echo.ui.theme.MediumPurple
|
import com.huaga.life_echo.ui.theme.MediumPurple
|
||||||
import com.huaga.life_echo.ui.theme.SlatePurple
|
|
||||||
import com.huaga.life_echo.ui.viewmodel.AuthViewModel
|
import com.huaga.life_echo.ui.viewmodel.AuthViewModel
|
||||||
import com.huaga.life_echo.ui.viewmodel.PaymentViewModel
|
import com.huaga.life_echo.ui.viewmodel.PaymentViewModel
|
||||||
import com.huaga.life_echo.ui.viewmodel.ProfileViewModel
|
import com.huaga.life_echo.ui.viewmodel.ProfileViewModel
|
||||||
@@ -157,7 +153,7 @@ fun ProfileScreen(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Cream)
|
.background(androidx.compose.material3.MaterialTheme.colorScheme.background)
|
||||||
.windowInsetsPadding(WindowInsets.statusBars)
|
.windowInsetsPadding(WindowInsets.statusBars)
|
||||||
.padding(horizontal = AppDimensions.screenPadding)
|
.padding(horizontal = AppDimensions.screenPadding)
|
||||||
) {
|
) {
|
||||||
@@ -248,7 +244,15 @@ fun ProfileScreen(
|
|||||||
label = "夜间模式",
|
label = "夜间模式",
|
||||||
type = SettingItemType.TOGGLE,
|
type = SettingItemType.TOGGLE,
|
||||||
value = darkMode,
|
value = darkMode,
|
||||||
onToggle = { darkMode = it },
|
onToggle = { darkMode = it }
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
icon = AppIcons.FormatSize,
|
||||||
|
label = "大字模式",
|
||||||
|
description = "增大字体以便阅读",
|
||||||
|
type = SettingItemType.TOGGLE,
|
||||||
|
value = largeFontMode,
|
||||||
|
onToggle = { largeFontMode = it },
|
||||||
isLast = true
|
isLast = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -285,7 +289,7 @@ fun ProfileScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = AppDimensions.sectionSpacing),
|
.padding(vertical = AppDimensions.sectionSpacing),
|
||||||
fontSize = AppTypography.captionMedium,
|
fontSize = AppTypography.captionMedium,
|
||||||
color = SlatePurple,
|
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -307,6 +311,10 @@ private fun ProfileHeader(
|
|||||||
onLoginClick: () -> Unit,
|
onLoginClick: () -> Unit,
|
||||||
onProfileClick: () -> Unit
|
onProfileClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val contentColor = androidx.compose.material3.MaterialTheme.colorScheme.onBackground
|
||||||
|
val secondaryColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
val avatarBgColor = androidx.compose.material3.MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -320,13 +328,13 @@ private fun ProfileHeader(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(AppDimensions.avatarSizeLarge)
|
.size(AppDimensions.avatarSizeLarge)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Lavender),
|
.background(avatarBgColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.Person,
|
imageVector = AppIcons.Person,
|
||||||
contentDescription = "用户头像",
|
contentDescription = "用户头像",
|
||||||
tint = DeepPurple,
|
tint = contentColor,
|
||||||
modifier = Modifier.size(36.dp)
|
modifier = Modifier.size(36.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -338,7 +346,7 @@ private fun ProfileHeader(
|
|||||||
text = nickname,
|
text = nickname,
|
||||||
fontSize = AppTypography.titleLarge,
|
fontSize = AppTypography.titleLarge,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = DeepPurple
|
color = contentColor
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(AppDimensions.tinySpacing))
|
Spacer(modifier = Modifier.height(AppDimensions.tinySpacing))
|
||||||
@@ -353,13 +361,13 @@ private fun ProfileHeader(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(AppDimensions.avatarSizeLarge)
|
.size(AppDimensions.avatarSizeLarge)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Lavender),
|
.background(avatarBgColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.Person,
|
imageVector = AppIcons.Person,
|
||||||
contentDescription = "用户头像",
|
contentDescription = "用户头像",
|
||||||
tint = DeepPurple,
|
tint = contentColor,
|
||||||
modifier = Modifier.size(36.dp)
|
modifier = Modifier.size(36.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -370,7 +378,7 @@ private fun ProfileHeader(
|
|||||||
text = "未登录",
|
text = "未登录",
|
||||||
fontSize = AppTypography.titleLarge,
|
fontSize = AppTypography.titleLarge,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = DeepPurple
|
color = contentColor
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(AppDimensions.smallSpacing))
|
Spacer(modifier = Modifier.height(AppDimensions.smallSpacing))
|
||||||
@@ -378,7 +386,7 @@ private fun ProfileHeader(
|
|||||||
Text(
|
Text(
|
||||||
text = "登录以同步您的数据",
|
text = "登录以同步您的数据",
|
||||||
fontSize = AppTypography.bodySmall,
|
fontSize = AppTypography.bodySmall,
|
||||||
color = SlatePurple
|
color = secondaryColor
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(AppDimensions.sectionSpacing))
|
Spacer(modifier = Modifier.height(AppDimensions.sectionSpacing))
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
package com.huaga.life_echo.ui.settings
|
package com.huaga.life_echo.ui.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用设置管理
|
* 应用设置管理
|
||||||
|
* 支持持久化存储和 Compose 状态观察
|
||||||
*/
|
*/
|
||||||
object AppSettings {
|
object AppSettings {
|
||||||
|
private const val PREFS_NAME = "life_echo_settings"
|
||||||
|
private const val KEY_DARK_MODE = "dark_mode"
|
||||||
|
private const val KEY_LARGE_FONT_MODE = "large_font_mode"
|
||||||
|
private const val KEY_SPEECH_RATE = "speech_rate"
|
||||||
|
|
||||||
|
private var prefs: SharedPreferences? = null
|
||||||
|
|
||||||
// 语速设置:慢速、标准、快速
|
// 语速设置:慢速、标准、快速
|
||||||
enum class SpeechRate(val label: String, val multiplier: Float) {
|
enum class SpeechRate(val label: String, val multiplier: Float) {
|
||||||
SLOW("慢速", 0.75f),
|
SLOW("慢速", 0.75f),
|
||||||
@@ -17,34 +25,67 @@ object AppSettings {
|
|||||||
FAST("快速", 1.5f)
|
FAST("快速", 1.5f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用 mutableStateOf 以便 Compose 可以观察变化
|
||||||
private val _speechRate = mutableStateOf(SpeechRate.STANDARD)
|
private val _speechRate = mutableStateOf(SpeechRate.STANDARD)
|
||||||
|
private val _largeFontMode = mutableStateOf(false)
|
||||||
|
private val _darkMode = mutableStateOf(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化设置(从 SharedPreferences 加载)
|
||||||
|
* 应在 Application 或 MainActivity 中调用
|
||||||
|
*/
|
||||||
|
fun initialize(context: Context) {
|
||||||
|
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// 从 SharedPreferences 加载保存的设置
|
||||||
|
prefs?.let { p ->
|
||||||
|
_darkMode.value = p.getBoolean(KEY_DARK_MODE, false)
|
||||||
|
_largeFontMode.value = p.getBoolean(KEY_LARGE_FONT_MODE, false)
|
||||||
|
val speechRateOrdinal = p.getInt(KEY_SPEECH_RATE, SpeechRate.STANDARD.ordinal)
|
||||||
|
_speechRate.value = SpeechRate.entries.getOrElse(speechRateOrdinal) { SpeechRate.STANDARD }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var speechRate: SpeechRate
|
var speechRate: SpeechRate
|
||||||
get() = _speechRate.value
|
get() = _speechRate.value
|
||||||
set(value) { _speechRate.value = value }
|
set(value) {
|
||||||
|
_speechRate.value = value
|
||||||
|
prefs?.edit()?.putInt(KEY_SPEECH_RATE, value.ordinal)?.apply()
|
||||||
|
}
|
||||||
|
|
||||||
private val _largeFontMode = mutableStateOf(false)
|
|
||||||
var largeFontMode: Boolean
|
var largeFontMode: Boolean
|
||||||
get() = _largeFontMode.value
|
get() = _largeFontMode.value
|
||||||
set(value) { _largeFontMode.value = value }
|
set(value) {
|
||||||
|
_largeFontMode.value = value
|
||||||
|
prefs?.edit()?.putBoolean(KEY_LARGE_FONT_MODE, value)?.apply()
|
||||||
|
}
|
||||||
|
|
||||||
private val _darkMode = mutableStateOf(false)
|
|
||||||
var darkMode: Boolean
|
var darkMode: Boolean
|
||||||
get() = _darkMode.value
|
get() = _darkMode.value
|
||||||
set(value) { _darkMode.value = value }
|
set(value) {
|
||||||
|
_darkMode.value = value
|
||||||
|
prefs?.edit()?.putBoolean(KEY_DARK_MODE, value)?.apply()
|
||||||
|
}
|
||||||
|
|
||||||
// 用于在Compose中观察设置变化 - 直接返回mutableStateOf的值
|
// 用于在 Compose 中观察设置变化 - 返回 State 的 value
|
||||||
|
// 这些函数返回的是 State 内部的值,当 State 变化时会自动触发重组
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberDarkMode(): Boolean {
|
fun rememberDarkMode(): Boolean {
|
||||||
return remember { _darkMode }.value
|
return _darkMode.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberLargeFontMode(): Boolean {
|
fun rememberLargeFontMode(): Boolean {
|
||||||
return remember { _largeFontMode }.value
|
return _largeFontMode.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberSpeechRate(): SpeechRate {
|
fun rememberSpeechRate(): SpeechRate {
|
||||||
return remember { _speechRate }.value
|
return _speechRate.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提供直接访问 State 的方式(用于需要 State 对象的场景)
|
||||||
|
val darkModeState: State<Boolean> get() = _darkMode
|
||||||
|
val largeFontModeState: State<Boolean> get() = _largeFontMode
|
||||||
|
val speechRateState: State<SpeechRate> get() = _speechRate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
package com.huaga.life_echo.ui.theme
|
package com.huaga.life_echo.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
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 com.huaga.life_echo.ui.settings.AppSettings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设计系统尺寸常量
|
* 设计系统尺寸常量
|
||||||
@@ -78,38 +84,154 @@ object AppDimensions {
|
|||||||
val headerElevation = 0.dp // 头部阴影
|
val headerElevation = 0.dp // 头部阴影
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字体大小数据类
|
||||||
|
* 支持大字模式动态调整
|
||||||
|
*/
|
||||||
|
data class AppTypographyData(
|
||||||
|
// Heading sizes
|
||||||
|
val headingLarge: TextUnit,
|
||||||
|
val headingMedium: TextUnit,
|
||||||
|
val headingSmall: TextUnit,
|
||||||
|
|
||||||
|
// Title sizes
|
||||||
|
val titleLarge: TextUnit,
|
||||||
|
val titleMedium: TextUnit,
|
||||||
|
val titleSmall: TextUnit,
|
||||||
|
|
||||||
|
// Body sizes
|
||||||
|
val bodyLarge: TextUnit,
|
||||||
|
val bodyMedium: TextUnit,
|
||||||
|
val bodySmall: TextUnit,
|
||||||
|
|
||||||
|
// Caption sizes
|
||||||
|
val captionLarge: TextUnit,
|
||||||
|
val captionMedium: TextUnit,
|
||||||
|
val captionSmall: TextUnit,
|
||||||
|
|
||||||
|
// Special
|
||||||
|
val sectionTitle: TextUnit,
|
||||||
|
val badge: TextUnit,
|
||||||
|
|
||||||
|
// Line heights
|
||||||
|
val lineHeightNormal: TextUnit,
|
||||||
|
val lineHeightTight: TextUnit,
|
||||||
|
val lineHeightLoose: TextUnit
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 普通字体大小
|
||||||
|
*/
|
||||||
|
private val NormalTypography = AppTypographyData(
|
||||||
|
headingLarge = 32.sp,
|
||||||
|
headingMedium = 24.sp,
|
||||||
|
headingSmall = 20.sp,
|
||||||
|
titleLarge = 18.sp,
|
||||||
|
titleMedium = 16.sp,
|
||||||
|
titleSmall = 15.sp,
|
||||||
|
bodyLarge = 15.sp,
|
||||||
|
bodyMedium = 14.sp,
|
||||||
|
bodySmall = 13.sp,
|
||||||
|
captionLarge = 13.sp,
|
||||||
|
captionMedium = 12.sp,
|
||||||
|
captionSmall = 11.sp,
|
||||||
|
sectionTitle = 12.sp,
|
||||||
|
badge = 10.sp,
|
||||||
|
lineHeightNormal = 22.sp,
|
||||||
|
lineHeightTight = 20.sp,
|
||||||
|
lineHeightLoose = 24.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大字模式字体大小(增加约 20%)
|
||||||
|
*/
|
||||||
|
private val LargeTypography = AppTypographyData(
|
||||||
|
headingLarge = 38.sp,
|
||||||
|
headingMedium = 29.sp,
|
||||||
|
headingSmall = 24.sp,
|
||||||
|
titleLarge = 22.sp,
|
||||||
|
titleMedium = 19.sp,
|
||||||
|
titleSmall = 18.sp,
|
||||||
|
bodyLarge = 18.sp,
|
||||||
|
bodyMedium = 17.sp,
|
||||||
|
bodySmall = 16.sp,
|
||||||
|
captionLarge = 16.sp,
|
||||||
|
captionMedium = 14.sp,
|
||||||
|
captionSmall = 13.sp,
|
||||||
|
sectionTitle = 14.sp,
|
||||||
|
badge = 12.sp,
|
||||||
|
lineHeightNormal = 26.sp,
|
||||||
|
lineHeightTight = 24.sp,
|
||||||
|
lineHeightLoose = 29.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompositionLocal for AppTypography
|
||||||
|
*/
|
||||||
|
val LocalAppTypography = compositionLocalOf { NormalTypography }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设计系统字体大小常量
|
* 设计系统字体大小常量
|
||||||
|
* 根据大字模式设置动态调整
|
||||||
*/
|
*/
|
||||||
object AppTypography {
|
object AppTypography {
|
||||||
// Heading sizes
|
// Heading sizes
|
||||||
val headingLarge = 32.sp // 大标题(头部标题)
|
val headingLarge: TextUnit
|
||||||
val headingMedium = 24.sp // 中等标题(书名)
|
@Composable get() = LocalAppTypography.current.headingLarge
|
||||||
val headingSmall = 20.sp // 小标题
|
val headingMedium: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.headingMedium
|
||||||
|
val headingSmall: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.headingSmall
|
||||||
|
|
||||||
// Title sizes
|
// Title sizes
|
||||||
val titleLarge = 18.sp // 大标题文字
|
val titleLarge: TextUnit
|
||||||
val titleMedium = 16.sp // 中等标题文字
|
@Composable get() = LocalAppTypography.current.titleLarge
|
||||||
val titleSmall = 15.sp // 小标题文字
|
val titleMedium: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.titleMedium
|
||||||
|
val titleSmall: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.titleSmall
|
||||||
|
|
||||||
// Body sizes
|
// Body sizes
|
||||||
val bodyLarge = 15.sp // 大正文
|
val bodyLarge: TextUnit
|
||||||
val bodyMedium = 14.sp // 中等正文
|
@Composable get() = LocalAppTypography.current.bodyLarge
|
||||||
val bodySmall = 13.sp // 小正文
|
val bodyMedium: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.bodyMedium
|
||||||
|
val bodySmall: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.bodySmall
|
||||||
|
|
||||||
// Caption sizes
|
// Caption sizes
|
||||||
val captionLarge = 13.sp // 大说明文字
|
val captionLarge: TextUnit
|
||||||
val captionMedium = 12.sp // 中等说明文字
|
@Composable get() = LocalAppTypography.current.captionLarge
|
||||||
val captionSmall = 11.sp // 小说明文字
|
val captionMedium: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.captionMedium
|
||||||
|
val captionSmall: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.captionSmall
|
||||||
|
|
||||||
// Special
|
// Special
|
||||||
val sectionTitle = 12.sp // 区块标题
|
val sectionTitle: TextUnit
|
||||||
val badge = 10.sp // 徽章文字
|
@Composable get() = LocalAppTypography.current.sectionTitle
|
||||||
|
val badge: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.badge
|
||||||
|
|
||||||
// Line heights
|
// Line heights
|
||||||
val lineHeightNormal = 22.sp // 正常行高
|
val lineHeightNormal: TextUnit
|
||||||
val lineHeightTight = 20.sp // 紧凑行高
|
@Composable get() = LocalAppTypography.current.lineHeightNormal
|
||||||
val lineHeightLoose = 24.sp // 宽松行高
|
val lineHeightTight: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.lineHeightTight
|
||||||
|
val lineHeightLoose: TextUnit
|
||||||
|
@Composable get() = LocalAppTypography.current.lineHeightLoose
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前的 AppTypographyData
|
||||||
|
* 根据大字模式返回对应的字体大小
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberAppTypography(): AppTypographyData {
|
||||||
|
val isLargeFont = AppSettings.rememberLargeFontMode()
|
||||||
|
return remember(isLargeFont) {
|
||||||
|
if (isLargeFont) LargeTypography else NormalTypography
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package com.huaga.life_echo.ui.theme
|
package com.huaga.life_echo.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
@@ -10,11 +8,7 @@ import androidx.compose.material3.dynamicLightColorScheme
|
|||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.huaga.life_echo.ui.settings.AppSettings
|
import com.huaga.life_echo.ui.settings.AppSettings
|
||||||
|
|
||||||
// 深色主题配色方案
|
// 深色主题配色方案
|
||||||
@@ -64,7 +58,7 @@ fun LifeechoTheme(
|
|||||||
) {
|
) {
|
||||||
// 使用AppSettings中的状态,这样当设置变化时会触发重组
|
// 使用AppSettings中的状态,这样当设置变化时会触发重组
|
||||||
val isDarkMode = darkTheme ?: AppSettings.rememberDarkMode()
|
val isDarkMode = darkTheme ?: AppSettings.rememberDarkMode()
|
||||||
val isLargeFont = AppSettings.rememberLargeFontMode()
|
val appTypography = rememberAppTypography()
|
||||||
|
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
@@ -74,27 +68,15 @@ fun LifeechoTheme(
|
|||||||
isDarkMode -> DarkColorScheme
|
isDarkMode -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据大字模式调整字体大小
|
|
||||||
val adjustedTypography = if (isLargeFont) {
|
|
||||||
Typography.copy(
|
|
||||||
bodyLarge = Typography.bodyLarge.copy(fontSize = 18.sp),
|
|
||||||
bodyMedium = Typography.bodyMedium.copy(fontSize = 16.sp),
|
|
||||||
bodySmall = Typography.bodySmall.copy(fontSize = 14.sp),
|
|
||||||
titleLarge = Typography.titleLarge.copy(fontSize = 24.sp),
|
|
||||||
titleMedium = Typography.titleMedium.copy(fontSize = 20.sp),
|
|
||||||
titleSmall = Typography.titleSmall.copy(fontSize = 18.sp),
|
|
||||||
headlineLarge = Typography.headlineLarge.copy(fontSize = 32.sp),
|
|
||||||
headlineMedium = Typography.headlineMedium.copy(fontSize = 28.sp),
|
|
||||||
headlineSmall = Typography.headlineSmall.copy(fontSize = 24.sp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Typography
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
// 提供 LocalAppTypography,这样所有子组件都可以使用大字模式
|
||||||
colorScheme = colorScheme,
|
CompositionLocalProvider(
|
||||||
typography = adjustedTypography,
|
LocalAppTypography provides appTypography
|
||||||
content = content
|
) {
|
||||||
)
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user