feat: 通知推送/免打扰偏好 + 饮食记录侧滑删除修复 + 聊天交互优化

- 通知: 新增推送开关/免打扰时段偏好持久化 + EF 迁移; 通知管线支持免打扰过滤
- 饮食: 侧滑删除 confirmDismiss 按方向放行(修复删不掉); 新增 diet_nutrition_widgets; 饮食录入页重构
- 聊天: 智能体胶囊点击锁防误触; 流式状态 select 优化; 卡片入场动画
- 主页: 消息列表提取 _HomeMessages + 侧滑手势打开抽屉
- 其他: api_client IP 适配; 通知/用药/饮食端点微调; 测试更新
This commit is contained in:
MingNian
2026-07-13 10:39:34 +08:00
parent 335a3e6440
commit e654c1e0cc
19 changed files with 2140 additions and 343 deletions

View File

@@ -76,13 +76,13 @@ class ChatNotifier extends Notifier<ChatState> {
StreamSubscription<Map<String, dynamic>>? _subscription;
Completer<void>? _streamDone;
ActiveAgent? _lastTriggeredAgent;
Timer? _agentWelcomeTimer;
String? _pendingAgentTriggerMessageId;
Timer? _agentTapLockTimer;
/// 重置整个会话:取消正在进行的 SSE清空消息和会话 ID。
/// 历史记录页一键清空 / 删除当前会话时调用。
Future<void> resetSession() async {
await _cancelActiveStream();
_cancelPendingAgentWelcome();
_lastTriggeredAgent = null;
state = const ChatState();
}
@@ -133,7 +133,7 @@ class ChatNotifier extends Notifier<ChatState> {
ChatState build() {
ref.onDispose(() {
_subscription?.cancel();
_agentWelcomeTimer?.cancel();
_agentTapLockTimer?.cancel();
_subscription = null;
if (_streamDone != null && !_streamDone!.isCompleted) {
_streamDone!.complete();
@@ -229,61 +229,41 @@ class ChatNotifier extends Notifier<ChatState> {
}
}
void insertAgentWelcome(ActiveAgent agent) {
_pendingAgentTriggerMessageId = null;
state = state.copyWith(
messages: [
...state.messages,
ChatMessage(
id: 'welcome_${agent.name}_${DateTime.now().millisecondsSinceEpoch}',
role: 'assistant',
content: '',
createdAt: DateTime.now(),
type: MessageType.agentWelcome,
metadata: {'agent': agent.name},
),
],
);
}
/// 点击胶囊:先出用户标签 → 0.4 秒后出欢迎卡片,不走 AI
/// 点击胶囊:用户标签和欢迎卡片立即出现,不走 AI。
/// 300ms 点击锁只防止误触,不延迟界面反馈。
/// 重复点击同一胶囊不重复弹卡片
void triggerAgent(ActiveAgent agent, String label) {
if (_pendingAgentTriggerMessageId != null) {
return;
}
if (_lastTriggeredAgent == agent && _pendingAgentTriggerMessageId == null) {
return;
}
if (_agentTapLockTimer != null || _lastTriggeredAgent == agent) return;
_lastTriggeredAgent = agent;
final now = DateTime.now();
final userMsg = ChatMessage(
id: 'agent_trigger_${DateTime.now().millisecondsSinceEpoch}',
id: 'agent_trigger_${now.microsecondsSinceEpoch}',
role: 'user',
content: label,
createdAt: DateTime.now(),
createdAt: now,
);
final welcomeMsg = ChatMessage(
id: 'welcome_${agent.name}_${now.microsecondsSinceEpoch}',
role: 'assistant',
content: '',
createdAt: now,
type: MessageType.agentWelcome,
metadata: {'agent': agent.name},
);
state = state.copyWith(
messages: [...state.messages, userMsg, welcomeMsg],
activeAgent: agent,
);
final messages = state.messages.toList();
messages.add(userMsg);
_pendingAgentTriggerMessageId = userMsg.id;
state = state.copyWith(messages: messages, activeAgent: agent);
final expectedConversationId = state.conversationId;
_agentWelcomeTimer = Timer(Duration.zero, () {
if (state.conversationId != expectedConversationId ||
state.activeAgent != agent ||
_lastTriggeredAgent != agent) {
return;
}
_agentWelcomeTimer = null;
insertAgentWelcome(agent);
_agentTapLockTimer = Timer(const Duration(milliseconds: 300), () {
_agentTapLockTimer = null;
});
}
void _cancelPendingAgentWelcome() {
_agentWelcomeTimer?.cancel();
_agentWelcomeTimer = null;
_pendingAgentTriggerMessageId = null;
_agentTapLockTimer?.cancel();
_agentTapLockTimer = null;
}
Future<void> sendImage(String imagePath, String text) async {