feat: UI清爽紫蓝风全面改造 + 对话卡片重构 + 配色体系升级
- 新增 AppColors 统一配色方案(清爽紫蓝,参考蚂蚁阿福风格) - 新增 common_widgets(GradientBorderButton / CardActionButton / IconBox) - AgentWelcomeCard 美化:渐变 header + AI标签 + 核心功能区 + 快捷操作区 - DataConfirmCard 统一:三合一(健康/药品/运动),可编辑字段列表 - 删除死代码:medicationConfirm/dietAnalysis/reportAnalysis/quickOptions 卡片 - 删除死路由:doctors/profileEdit/editProfile - AI 对话去除 Markdown 符号(_stripMd),统一行间距 1.5 - 逐字淡入动画修复:key 改用 stableId 避免重复触发 - 健康概览时间修复:前端发送 UTC 避免时区歧义 - PG 数据目录迁入项目 backend/pgdata/,加入 .gitignore - 新增欧姆龙血压计实施方案文档
This commit is contained in:
@@ -5,7 +5,7 @@ import 'auth_provider.dart';
|
||||
import 'data_providers.dart';
|
||||
import '../utils/sse_handler.dart';
|
||||
|
||||
enum MessageType { text, dataConfirm, medicationConfirm, dietAnalysis, reportAnalysis, quickOptions, agentWelcome, taskCard }
|
||||
enum MessageType { text, dataConfirm, agentWelcome, taskCard }
|
||||
|
||||
class ChatMessage {
|
||||
final String id;
|
||||
@@ -77,6 +77,8 @@ ActiveAgent _parseAgent(String? type) {
|
||||
|
||||
class ChatNotifier extends Notifier<ChatState> {
|
||||
StreamSubscription<Map<String, dynamic>>? _subscription;
|
||||
String _streamBuffer = '';
|
||||
Timer? _streamTimer;
|
||||
|
||||
void markNeedsRebuild() => state = state.copyWith();
|
||||
|
||||
@@ -174,6 +176,23 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
)]);
|
||||
}
|
||||
|
||||
/// 点击胶囊:先出用户标签 → 0.5 秒后出欢迎卡片,不走 AI
|
||||
void triggerAgent(ActiveAgent agent, String label) {
|
||||
final userMsg = ChatMessage(
|
||||
id: 'agent_trigger_${DateTime.now().millisecondsSinceEpoch}',
|
||||
role: 'user',
|
||||
content: label,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
// 先出用户消息
|
||||
state = state.copyWith(messages: [...state.messages, userMsg]);
|
||||
|
||||
// 短暂延迟后弹出卡片
|
||||
Future.delayed(const Duration(milliseconds: 400), () {
|
||||
insertAgentWelcome(agent);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> sendImage(String imagePath, String text) async {
|
||||
final file = File(imagePath);
|
||||
if (!await file.exists()) return;
|
||||
@@ -241,7 +260,8 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
state = state.copyWith(isStreaming: true);
|
||||
// 立即加入空 AI 消息,让思考动画有载体
|
||||
state = state.copyWith(messages: [...state.messages, aiMsg], isStreaming: true);
|
||||
|
||||
try {
|
||||
final token = await ref.read(apiClientProvider).accessToken;
|
||||
@@ -288,15 +308,21 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
case 'conversation_id':
|
||||
state = state.copyWith(conversationId: j['data']?.toString());
|
||||
case 'answer':
|
||||
aiMsg.content += (j['data'] as String?) ?? '';
|
||||
_streamBuffer += (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);
|
||||
// 逐字释放
|
||||
_streamTimer?.cancel();
|
||||
_streamTimer = Timer.periodic(const Duration(milliseconds: 40), (t) {
|
||||
if (_streamBuffer.isEmpty) { t.cancel(); state = state.copyWith(thinkingText: null); return; }
|
||||
aiMsg.content += _streamBuffer[0];
|
||||
_streamBuffer = _streamBuffer.substring(1);
|
||||
state = state.copyWith(thinkingText: null);
|
||||
_update(aiMsg);
|
||||
});
|
||||
case 'notice':
|
||||
state = state.copyWith(thinkingText: j['message'] as String?);
|
||||
case 'tool_result':
|
||||
@@ -315,13 +341,13 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
|
||||
MessageType _parseMessageType(String type) {
|
||||
switch (type) {
|
||||
case 'data_confirm': return MessageType.dataConfirm;
|
||||
case 'medication_confirm': return MessageType.medicationConfirm;
|
||||
case 'diet_analysis': return MessageType.dietAnalysis;
|
||||
case 'report_analysis': return MessageType.reportAnalysis;
|
||||
case 'quick_options': return MessageType.quickOptions;
|
||||
case 'agent_welcome': return MessageType.agentWelcome;
|
||||
default: return MessageType.text;
|
||||
case 'data_confirm':
|
||||
case 'medication_confirm':
|
||||
return MessageType.dataConfirm;
|
||||
case 'agent_welcome':
|
||||
return MessageType.agentWelcome;
|
||||
default:
|
||||
return MessageType.text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,13 +356,19 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
final i = u.indexWhere((x) => x.id == m.id);
|
||||
if (i >= 0) {
|
||||
u[i] = m;
|
||||
} else if (m.content.isNotEmpty) {
|
||||
u.add(m);
|
||||
} else {
|
||||
u.add(m); // 空内容也加入,让思考气泡有载体
|
||||
}
|
||||
state = state.copyWith(messages: u);
|
||||
}
|
||||
|
||||
void _done(ChatMessage m) {
|
||||
_streamTimer?.cancel();
|
||||
// 释放剩余 buffer
|
||||
while (_streamBuffer.isNotEmpty) {
|
||||
m.content += _streamBuffer[0];
|
||||
_streamBuffer = _streamBuffer.substring(1);
|
||||
}
|
||||
final u = state.messages.toList();
|
||||
if (!u.any((x) => x.id == m.id) && m.content.isNotEmpty) u.add(m);
|
||||
state = state.copyWith(messages: u, isStreaming: false, thinkingText: null);
|
||||
|
||||
Reference in New Issue
Block a user