feat: 用药打卡全面重构 - 按次追踪 + 独立打卡页 + 任务区汇总

- 后端用药提醒改为按次粒度(每个服药时间独立判断)
- 新增 per-dose 打卡/取消打卡 API
- 新增 MedicationCheckInPage 独立打卡页(每药每时间单独toggle)
- 用药列表删底部弹窗(编辑/停药),点药品直接进该药打卡页
- 任务区用药汇总为一行(过期待服已服)
- 支持隔天(EveryOtherDay)/每周频率判断
- 删历史对话功能(ConversationItem + conversationListProvider + UI)
This commit is contained in:
MingNian
2026-06-08 16:21:13 +08:00
parent fc7150ad60
commit 16d1d3d305
17 changed files with 519 additions and 373 deletions

View File

@@ -13,7 +13,7 @@ class ChatMessage {
String content;
final DateTime createdAt;
MessageType type;
final Map<String, dynamic>? metadata;
Map<String, dynamic>? metadata;
bool confirmed;
ChatMessage({
required this.id,
@@ -53,22 +53,6 @@ class ChatState {
);
}
class ConversationItem {
final String id;
final String title;
final String lastMessage;
final DateTime updatedAt;
final ActiveAgent agent;
ConversationItem({
required this.id,
required this.title,
required this.lastMessage,
required this.updatedAt,
required this.agent,
});
}
class SelectedAgentNotifier extends Notifier<ActiveAgent?> {
@override
ActiveAgent? build() => null;
@@ -78,28 +62,6 @@ class SelectedAgentNotifier extends Notifier<ActiveAgent?> {
final selectedAgentProvider =
NotifierProvider<SelectedAgentNotifier, ActiveAgent?>(SelectedAgentNotifier.new);
final chatProvider = NotifierProvider<ChatNotifier, ChatState>(ChatNotifier.new);
final conversationListProvider = FutureProvider<List<ConversationItem>>((ref) async {
final api = ref.watch(apiClientProvider);
final token = await api.accessToken;
if (token == null) return [];
try {
final res = await api.get('/api/ai/conversations');
final list = res.data['data'] as List? ?? [];
return list.map((item) {
final data = item as Map<String, dynamic>;
return ConversationItem(
id: data['id']?.toString() ?? '',
title: data['title']?.toString() ?? '对话',
lastMessage: data['lastMessage']?.toString() ?? '',
updatedAt: DateTime.parse(data['updatedAt']?.toString() ?? DateTime.now().toIso8601String()),
agent: _parseAgent(data['agentType']?.toString()),
);
}).toList();
} catch (_) {
return [];
}
});
ActiveAgent _parseAgent(String? type) {
switch (type?.toLowerCase()) {
@@ -138,6 +100,8 @@ class ChatNotifier extends Notifier<ChatState> {
}
void setAgent(ActiveAgent a) {
// 流式回复中忽略胶囊切换,防止状态混乱
if (state.isStreaming) return;
_subscription?.cancel();
state = state.copyWith(activeAgent: a);
ref.read(selectedAgentProvider.notifier).select(a);
@@ -190,16 +154,12 @@ class ChatNotifier extends Notifier<ChatState> {
);
}).toList();
// 从对话列表中找到对应的 agent 类型
final convList = ref.read(conversationListProvider).value ?? [];
final conv = convList.firstWhere((c) => c.id == convId, orElse: () => ConversationItem(id: convId, title: '', lastMessage: '', updatedAt: DateTime.now(), agent: ActiveAgent.default_));
state = state.copyWith(
messages: messages,
conversationId: convId,
activeAgent: conv.agent,
activeAgent: ActiveAgent.default_,
);
ref.read(selectedAgentProvider.notifier).select(conv.agent);
ref.read(selectedAgentProvider.notifier).select(ActiveAgent.default_);
} catch (_) {}
}
@@ -331,6 +291,10 @@ class ChatNotifier extends Notifier<ChatState> {
aiMsg.content += (j['data'] as String?) ?? '';
final messageType = j['type'] as String? ?? 'text';
aiMsg.type = _parseMessageType(messageType);
// 从 SSE 中获取元数据(用药/健康数据的确认信息)
if (j['metadata'] is Map) {
aiMsg.metadata = Map<String, dynamic>.from(j['metadata']);
}
state = state.copyWith(thinkingText: null);
_update(aiMsg);
case 'notice':