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:
MingNian
2026-06-09 17:04:11 +08:00
parent 5f8964e03f
commit f01fc9268d
14 changed files with 1228 additions and 866 deletions

View File

@@ -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);