feat: 问诊对话+建档引导+报告+日历+趋势页重写+视觉统一

- 问诊对话页: 新建consultation_provider, 完整AI分身聊天UI
- 首次建档引导: OnboardingPrompt+ManageArchiveTool+前端卡片
- 报告模块: POST端点 + report_agent_handler接VLM
- 健康日历: GET /api/calendar端点 + 前端接API
- 趋势页重写: 删除Mock数据 + 真实API + 手动录入
- 饮食保存: submit按钮接后端API
- 侧边栏: 健康概览横排 + 统一紫色配色
- 运动计划: 修复JSON反序列化(record→手动解析)
- VLM: prompt优化 + 模型切qwen3-vl-plus + 1280px压缩
- UI颜色: 加深主色 8B9CF7→6C5CE7
- 个人信息页精简 + 健康档案可编辑重设计
- 保洁: 删除无用agent_bar + 删除意见反馈/关于我们
- 新增AI提示词文档
This commit is contained in:
MingNian
2026-06-04 13:49:43 +08:00
parent c2399b952f
commit 0e0e8ce72b
29 changed files with 1621 additions and 3361 deletions

View File

@@ -4,25 +4,41 @@ import 'package:flutter/material.dart';
class AppTheme {
AppTheme._();
static const Color primary = Color(0xFF8B9CF7); // 淡薰紫
static const Color primaryLight = Color(0xFFF0F2FF); // 极淡紫底
static const Color primaryDark = Color(0xFF6A7DE0); // 深薰紫
static const Color bg = Color(0xFFF8F9FC); // 清透白底
static const Color surface = Color(0xFFFFFFFF); // 纯白卡片
// ── 颜色 ──
static const Color primary = Color(0xFF6C5CE7);
static const Color primaryLight = Color(0xFFEDEAFF);
static const Color primaryDark = Color(0xFF5A4BD1);
static const Color bg = Color(0xFFF8F9FC);
static const Color surface = Color(0xFFFFFFFF);
static const Color text = Color(0xFF2D2B32);
static const Color textSub = Color(0xFF8A8892);
static const Color textHint = Color(0xFFBFBCC4);
static const Color success = Color(0xFF6ECF8A);
static const Color error = Color(0xFFF56C6C);
static const Color warning = Color(0xFFF5A623);
static const Color accent = Color(0xFFFF8068);
static const Color border = Color(0xFFEAEAF0);
static const Color divider = Color(0xFFF2F2F6);
// ── 圆角 Token ──
static const double radiusXs = 8;
static const double radiusSm = 12;
static const double radiusMd = 16;
static const double radiusLg = 20;
static const double radiusXl = 24;
static const double radiusPill = 999;
// ── 间距 Token ──
static const double spaceXs = 4;
static const double spaceSm = 8;
static const double spaceMd = 12;
static const double spaceLg = 16;
static const double spaceXl = 20;
// ── 阴影 Token ──
static BoxShadow get shadowCard => BoxShadow(color: primary.withAlpha(15), blurRadius: 12, offset: const Offset(0, 4));
static BoxShadow get shadowLight => BoxShadow(color: primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2));
/// 每个智能体的卡片色调
static const Map<String, Color> agentColors = {
'default': Color(0xFFE8ECFF), // 淡蓝紫
@@ -48,25 +64,25 @@ class AppTheme {
),
cardTheme: CardThemeData(color: surface, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), margin: EdgeInsets.zero),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusXl)), margin: EdgeInsets.zero),
inputDecorationTheme: InputDecorationTheme(
filled: true, fillColor: const Color(0xFFF4F5FA),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: primary, width: 1.5)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(radiusMd), borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(radiusMd), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(radiusMd), borderSide: const BorderSide(color: primary, width: 1.5)),
hintStyle: const TextStyle(color: textHint, fontSize: 15),
),
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(
backgroundColor: primary, foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusLg)),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), elevation: 0,
)),
dialogTheme: DialogThemeData(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22))),
dialogTheme: DialogThemeData(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusXl))),
textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: text),

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart';
import '../../providers/consultation_provider.dart';
/// 医生列表页
class DoctorListPage extends ConsumerWidget {
@@ -78,15 +80,196 @@ class DoctorListPage extends ConsumerWidget {
}
}
/// 问诊对话页
class DoctorChatPage extends ConsumerWidget {
/// 问诊对话页 — 完整的 AI 分身聊天界面
class DoctorChatPage extends ConsumerStatefulWidget {
final String id;
const DoctorChatPage({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) => Scaffold(
appBar: AppBar(title: const Text('问诊对话')),
body: Center(
child: Text('问诊 #$id', style: Theme.of(context).textTheme.bodyLarge),
),
);
ConsumerState<DoctorChatPage> createState() => _DoctorChatPageState();
}
class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
final _textCtrl = TextEditingController();
final _scrollCtrl = ScrollController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(consultationChatProvider.notifier).init(widget.id);
});
}
@override
void dispose() {
ref.read(consultationChatProvider.notifier).stopPolling();
_textCtrl.dispose();
_scrollCtrl.dispose();
super.dispose();
}
void _send() {
final text = _textCtrl.text.trim();
if (text.isEmpty) return;
_textCtrl.clear();
ref.read(consultationChatProvider.notifier).sendMessage(text);
}
String _statusText(String status) {
switch (status) {
case 'AiTalking':
return 'AI分身对话中';
case 'WaitingDoctor':
return '已转接医生,请耐心等待';
case 'DoctorReplied':
return '医生已回复';
case 'Closed':
return '对话已结束';
default:
return '';
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(consultationChatProvider);
final canSend = state.status == 'AiTalking' && !state.isSending;
return Scaffold(
backgroundColor: const Color(0xFFF8F9FC),
appBar: AppBar(
backgroundColor: Colors.white,
title: Column(children: [
Text(state.doctorName.isNotEmpty ? '${state.doctorName} · ${state.doctorDepartment}' : '问诊对话',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
if (state.doctorName.isNotEmpty)
Text(_statusText(state.status), style: const TextStyle(fontSize: 12, color: Color(0xFF8B9CF7))),
]),
centerTitle: true,
actions: [
Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF0F2FF),
borderRadius: BorderRadius.circular(12),
),
child: Text('剩余${state.quotaRemaining}/${state.quotaTotal}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B9CF7), fontWeight: FontWeight.w500)),
),
],
),
body: state.isLoading
? const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7)))
: Column(children: [
Expanded(child: _buildMessageList(state)),
_buildInputBar(canSend),
]),
);
}
Widget _buildMessageList(ConsultationChatState state) {
if (state.messages.isEmpty) {
return const Center(
child: Text('暂无消息', style: TextStyle(color: Color(0xFFBBBBBB))));
}
return ListView.builder(
controller: _scrollCtrl,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
itemCount: state.messages.length,
itemBuilder: (ctx, i) => _buildBubble(state, state.messages[i]),
);
}
Widget _buildBubble(ConsultationChatState state, ConsultationMsg msg) {
final isUser = msg.senderType == 'User';
final isAi = msg.senderType == 'Ai';
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
decoration: BoxDecoration(
color: isUser ? const Color(0xFF8B9CF7) : const Color(0xFFFEFEFF),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isUser ? 20 : 4),
topRight: Radius.circular(isUser ? 4 : 20),
bottomLeft: const Radius.circular(20),
bottomRight: const Radius.circular(20),
),
border: isUser ? null : Border.all(color: const Color(0xFFD8DCFD), width: 1.5),
boxShadow: isUser
? []
: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (!isUser && msg.senderName != null) ...[
Row(children: [
const CircleAvatar(radius: 10, backgroundColor: Color(0xFFF0F2FF),
child: Icon(Icons.smart_toy, size: 12, color: Color(0xFF8B9CF7))),
const SizedBox(width: 6),
Text(msg.senderName!, style: const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))),
]),
const SizedBox(height: 8),
],
if (isUser)
Text(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white, height: 1.4))
else
MarkdownBody(
data: msg.content,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A), height: 1.5),
),
),
if (isAi) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFFFF8E1),
borderRadius: BorderRadius.circular(6),
),
child: Text('🏷️ 以上为AI分析具体请咨询${state.doctorName}',
style: const TextStyle(fontSize: 10, color: Color(0xFFF9A825))),
),
],
]),
),
);
}
Widget _buildInputBar(bool canSend) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
border: const Border(top: BorderSide(color: Color(0xFFEEEEEE))),
),
child: Row(children: [
Expanded(
child: TextField(
controller: _textCtrl,
enabled: canSend,
style: const TextStyle(fontSize: 15),
decoration: InputDecoration(
hintText: canSend ? '描述您的症状...' : '对话已结束',
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: InputBorder.none,
),
onSubmitted: (_) => _send(),
),
),
IconButton(
icon: Icon(Icons.send, size: 24,
color: canSend ? const Color(0xFF8B9CF7) : const Color(0xFFCCCCCC)),
onPressed: canSend ? _send : null,
),
]),
);
}
}

View File

@@ -15,6 +15,7 @@ class DietState {
final String mealType;
final bool isAnalyzing;
final int? healthScore;
final String? errorMessage;
DietState({
this.imagePath,
@@ -22,6 +23,7 @@ class DietState {
this.mealType = 'lunch',
this.isAnalyzing = false,
this.healthScore,
this.errorMessage,
});
DietState copyWith({
@@ -30,6 +32,7 @@ class DietState {
String? mealType,
bool? isAnalyzing,
int? healthScore,
String? errorMessage,
}) {
return DietState(
imagePath: imagePath ?? this.imagePath,
@@ -37,6 +40,7 @@ class DietState {
mealType: mealType ?? this.mealType,
isAnalyzing: isAnalyzing ?? this.isAnalyzing,
healthScore: healthScore ?? this.healthScore,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
@@ -65,11 +69,8 @@ class DietNotifier extends Notifier<DietState> {
state = state.copyWith(imagePath: path);
}
String? _analysisError;
Future<void> analyzeImage() async {
state = state.copyWith(isAnalyzing: true);
_analysisError = null;
state = state.copyWith(isAnalyzing: true, errorMessage: null);
try {
final api = ref.read(apiClientProvider);
final imageFile = File(state.imagePath!);
@@ -83,8 +84,7 @@ class DietNotifier extends Notifier<DietState> {
final res = await api.dio.post('/api/ai/analyze-food-image', data: formData);
final data = res.data;
if (data['code'] != 0) {
_analysisError = data['message'] ?? '识别失败';
state = state.copyWith(isAnalyzing: false);
state = state.copyWith(isAnalyzing: false, errorMessage: data['message'] ?? '识别失败');
return;
}
@@ -96,8 +96,7 @@ class DietNotifier extends Notifier<DietState> {
healthScore: foods.isNotEmpty ? 3 : null,
);
} catch (e) {
_analysisError = '识别失败: $e';
state = state.copyWith(isAnalyzing: false);
state = state.copyWith(isAnalyzing: false, errorMessage: '识别失败,请重试');
}
}
@@ -168,13 +167,43 @@ class DietNotifier extends Notifier<DietState> {
void reset() {
state = DietState();
}
Future<void> saveRecord() async {
final selectedFoods = state.foods.where((f) => f.selected).toList();
if (selectedFoods.isEmpty) return;
final api = ref.read(apiClientProvider);
final mealMap = {'breakfast': 'Breakfast', 'lunch': 'Lunch', 'dinner': 'Dinner', 'snack': 'Snack'};
final totalCal = selectedFoods.fold<int>(0, (s, f) => s + f.calories);
await api.post('/api/diet-records', data: {
'mealType': mealMap[state.mealType] ?? 'Lunch',
'totalCalories': totalCal,
'healthScore': state.healthScore ?? 3,
'recordedAt': DateTime.now().toIso8601String().substring(0, 10),
'foodItems': selectedFoods.asMap().entries.map((e) => {
'name': e.value.name,
'portion': e.value.portion,
'calories': e.value.calories,
'sortOrder': e.key,
}).toList(),
});
}
}
class DietCapturePage extends ConsumerWidget {
class DietCapturePage extends ConsumerStatefulWidget {
const DietCapturePage({super.key});
@override ConsumerState<DietCapturePage> createState() => _DietCapturePageState();
}
class _DietCapturePageState extends ConsumerState<DietCapturePage> {
@override void initState() {
super.initState();
Future.microtask(() => ref.read(dietProvider.notifier).reset());
}
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final state = ref.watch(dietProvider);
return Scaffold(
@@ -261,7 +290,7 @@ class DietCapturePage extends ConsumerWidget {
const SizedBox(height: 20),
_buildMealSelector(context, ref),
const SizedBox(height: 20),
if (state.isAnalyzing) _buildAnalyzingIndicator() else _buildFoodList(context, ref),
if (state.isAnalyzing) _buildAnalyzingIndicator(state) else _buildFoodList(context, ref),
if (!state.isAnalyzing && state.foods.isNotEmpty) ...[
const SizedBox(height: 20),
_buildNutritionSummary(totalCalories),
@@ -323,7 +352,7 @@ class DietCapturePage extends ConsumerWidget {
]);
}
Widget _buildAnalyzingIndicator() {
Widget _buildAnalyzingIndicator(DietState state) {
return Center(
child: Column(children: [
Container(
@@ -331,13 +360,17 @@ class DietCapturePage extends ConsumerWidget {
height: 60,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF0F2FF),
color: const Color(0xFFEDEAFF),
borderRadius: BorderRadius.circular(30),
),
child: const CircularProgressIndicator(strokeWidth: 3, color: Color(0xFF8B9CF7)),
child: const CircularProgressIndicator(strokeWidth: 3, color: Color(0xFF6C5CE7)),
),
const SizedBox(height: 16),
const Text('AI 正在识别食物...', style: TextStyle(fontSize: 16, color: Color(0xFF666666))),
if (state.errorMessage != null) ...[
const SizedBox(height: 8),
Text(state.errorMessage!, style: const TextStyle(fontSize: 13, color: Color(0xFFE53935)), textAlign: TextAlign.center),
],
]),
);
}
@@ -493,12 +526,24 @@ class DietCapturePage extends ConsumerWidget {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('饮食记录已保存 ✅'),
backgroundColor: Color(0xFF8B9CF7),
));
popRoute(ref);
onPressed: () async {
try {
await ref.read(dietProvider.notifier).saveRecord();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('饮食记录已保存 ✅'),
backgroundColor: Color(0xFF43A047),
));
popRoute(ref);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('保存失败: $e'),
backgroundColor: const Color(0xFFE53935),
));
}
}
},
child: const Text('保存记录'),
style: ElevatedButton.styleFrom(

View File

@@ -6,6 +6,7 @@ import 'dart:io';
import '../../providers/auth_provider.dart';
import '../../providers/chat_provider.dart';
import '../../providers/data_providers.dart';
import '../../core/navigation_provider.dart';
import '../../widgets/health_drawer.dart';
import 'widgets/chat_messages_view.dart';
@@ -60,10 +61,10 @@ class _HomePageState extends ConsumerState<HomePage> {
// ── 顶部栏 ──
_buildHeader(user),
// ── 聊天区域(今日任务已移入对话流第一条消息) ──
// ── 聊天区域(今日任务在对话流最上方)──
Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)),
// ── 底部合并区:智能体栏 + 操作面板 + 输入框(固定高度) ──
// ── 底部合并区 ──
_buildBottomBar(context, selectedAgent),
]),
),

View File

@@ -69,6 +69,8 @@ class ChatMessagesView extends ConsumerWidget {
return _buildDietAnalysisCard(context, msg);
case MessageType.reportAnalysis:
return _buildReportAnalysisCard(context, msg);
case MessageType.onboarding:
return _buildOnboardingCard(context, ref);
case MessageType.quickOptions:
return _buildQuickOptionsCard(context, msg);
default:
@@ -837,7 +839,65 @@ class ChatMessagesView extends ConsumerWidget {
}
// ═══════════════════════════════════════════════════════════
// 6. QuickOptionsCard — 优化样式
// 6. OnboardingCard — 首次建档引导
Widget _buildOnboardingCard(BuildContext context, WidgetRef ref) {
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(25), blurRadius: 14, offset: const Offset(0, 4))],
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFF8B9CF7), Color(0xFFA78BFA)], begin: Alignment.topLeft, end: Alignment.bottomRight),
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(width: 48, height: 48, decoration: BoxDecoration(color: Colors.white.withAlpha(30), borderRadius: BorderRadius.circular(14)), child: const Icon(Icons.health_and_safety, size: 28, color: Colors.white)),
const SizedBox(width: 14),
const Text('欢迎来到健康管家!', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white)),
]),
const SizedBox(height: 12),
Text('我是您的AI健康管家。为了给您更精准的建议\n我先了解一些基本信息好吗大约2-3分钟。',
style: TextStyle(fontSize: 14, color: Colors.white.withAlpha(220), height: 1.5)),
]),
),
Padding(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 20),
child: Column(children: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
ref.read(chatProvider.notifier).startOnboarding();
},
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF8B9CF7), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)),
child: const Text('开始建档', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
),
),
const SizedBox(height: 10),
TextButton(
onPressed: () => ref.read(chatProvider.notifier).dismissOnboarding(),
child: const Text('以后再说', style: TextStyle(fontSize: 14, color: Color(0xFF999999)))),
]),
),
],
),
),
);
}
// 7. QuickOptionsCard — 优化样式
// ═══════════════════════════════════════════════════════════
Widget _buildQuickOptionsCard(BuildContext context, ChatMessage msg) {

View File

@@ -14,39 +14,49 @@ class ProfilePage extends ConsumerWidget {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FC),
body: SafeArea(child: SingleChildScrollView(padding: const EdgeInsets.only(bottom: 20), child: Column(children: [
Container(width: double.infinity, padding: const EdgeInsets.all(24), decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24))), child: Column(children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text('9:41', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[800])), Row(children: [Icon(Icons.wifi, size: 18, color: Colors.grey[700]), const SizedBox(width: 4), Icon(Icons.battery_full, size: 18, color: Colors.grey[700])]),]),
const SizedBox(height: 20),
Row(children: [
GestureDetector(
onTap: () => pushRoute(ref, 'editProfile'),
child: Stack(children: [
CircleAvatar(radius: 32, backgroundColor: const Color(0xFFF0F2FF), backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, child: user?.avatarUrl == null ? const Icon(Icons.person, size: 40, color: Color(0xFF8B9CF7)) : null),
Positioned(right: 0, bottom: 0, child: Container(width: 22, height: 22, decoration: BoxDecoration(color: const Color(0xFF8B9CF7), borderRadius: BorderRadius.circular(11), border: Border.all(color: Colors.white, width: 2)), child: const Icon(Icons.edit, size: 12, color: Colors.white))),
// 用户头部
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
),
child: InkWell(
onTap: () => pushRoute(ref, 'healthArchive'),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(children: [
Stack(children: [
CircleAvatar(radius: 32, backgroundColor: const Color(0xFFEDEAFF),
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null,
child: user?.avatarUrl == null ? const Icon(Icons.person, size: 36, color: Color(0xFF6C5CE7)) : null),
Positioned(right: 0, bottom: 0,
child: Container(width: 20, height: 20,
decoration: BoxDecoration(color: const Color(0xFF6C5CE7), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.white, width: 2)),
child: const Icon(Icons.edit, size: 10, color: Colors.white))),
]),
const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
const SizedBox(height: 4),
Text(user?.phone ?? '', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
])),
Icon(Icons.chevron_right, size: 24, color: Colors.grey[400]),
]),
),
const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(user?.name ?? '张三', style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
const SizedBox(height: 4),
Text('42岁', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
])),
Icon(Icons.chevron_right, size: 24, color: Colors.grey[400]),
]),
])),
),
),
const SizedBox(height: 12),
// 产品服务包卡片
const ServicePackageCard(),
const SizedBox(height: 12),
_MenuItem(icon: Icons.folder_shared, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')),
_MenuItem(icon: Icons.devices, title: '设备管理', onTap: () => pushRoute(ref, 'devices')),
_MenuItem(icon: Icons.favorite_border, title: '就诊收藏', trailing: '3'),
_MenuItem(icon: Icons.people_outline, title: '家人关怀'),
_MenuItem(icon: Icons.local_hospital_outlined, title: '医生绑定记录'),
_MenuItem(icon: Icons.chat_bubble_outline, title: '意见反馈'),
_MenuItem(icon: Icons.info_outline, title: '关于我们'),
_MenuItem(icon: Icons.settings_outlined, title: '设置', onTap: () => pushRoute(ref, 'settings')),
const SizedBox(height: 40),
GestureDetector(
onTap: () async {
@@ -65,8 +75,7 @@ class ProfilePage extends ConsumerWidget {
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
height: 50,
alignment: Alignment.center,
height: 50, alignment: Alignment.center,
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE53935)), borderRadius: BorderRadius.circular(25)),
child: const Text('退出登录', style: TextStyle(fontSize: 16, color: Color(0xFFE53935), fontWeight: FontWeight.w500)),
),
@@ -81,7 +90,6 @@ class _MenuItem extends StatelessWidget {
final String title;
final String? trailing;
final VoidCallback? onTap;
const _MenuItem({required this.icon, required this.title, this.trailing, this.onTap});
@override Widget build(BuildContext context) {
@@ -91,13 +99,12 @@ class _MenuItem extends StatelessWidget {
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14),
decoration: BoxDecoration(color: Colors.white),
decoration: const BoxDecoration(color: Colors.white),
child: Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 18, color: const Color(0xFF8B9CF7))),
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 18, color: const Color(0xFF6C5CE7))),
const SizedBox(width: 12),
Text(title, style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A))),
if (trailing != null && trailing!.isNotEmpty) ...[const Spacer(), Text(trailing!, style: TextStyle(fontSize: 14, color: Colors.grey[400]))],
if (trailing == null || trailing!.isEmpty) const Spacer(),
Expanded(child: Text(title, style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A)))),
if (trailing != null && trailing!.isNotEmpty) Text(trailing!, style: TextStyle(fontSize: 14, color: Colors.grey[400])),
Icon(Icons.chevron_right, size: 20, color: Colors.grey[300]),
]),
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart';
import '../providers/data_providers.dart';
/// 饮食记录列表
@@ -354,92 +355,135 @@ class _FollowUpItem extends StatelessWidget {
}
}
/// 健康档案
class HealthArchivePage extends ConsumerWidget {
/// 健康档案(可编辑)
class HealthArchivePage extends ConsumerStatefulWidget {
const HealthArchivePage({super.key});
@override Widget build(BuildContext context, WidgetRef ref) {
final service = ref.watch(userServiceProvider);
return Scaffold(
appBar: AppBar(title: const Text('健康档案')),
body: FutureBuilder<Map<String, dynamic>?>(
future: service.getHealthArchive(),
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
final data = snap.data;
if (data == null || data.isEmpty) return _empty(context, '暂无健康档案', '可通过 AI 对话或手动填写');
return ListView(
padding: const EdgeInsets.all(16),
children: [
_Section(title: '基本信息', children: [
_Field('诊断', data['diagnosis']), _Field('手术类型', data['surgeryType']),
_Field('手术日期', data['surgeryDate']),
]),
_Section(title: '病史与限制', children: [
_Field('过敏史', _listStr(data['allergies'])),
_Field('饮食限制', _listStr(data['dietRestrictions'])),
_Field('慢性病史', _listStr(data['chronicDiseases'])),
_Field('家族病史', data['familyHistory']),
]),
],
);
},
),
);
@override ConsumerState<HealthArchivePage> createState() => _HealthArchivePageState();
}
class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
final _nameCtrl = TextEditingController();
final _genderCtrl = TextEditingController();
final _birthCtrl = TextEditingController();
final _diagnosisCtrl = TextEditingController();
final _surgeryCtrl = TextEditingController();
final _surgeryDateCtrl = TextEditingController();
final _allergiesCtrl = TextEditingController();
final _chronicCtrl = TextEditingController();
final _dietCtrl = TextEditingController();
final _familyCtrl = TextEditingController();
bool _loading = true;
@override void initState() { super.initState(); _load(); }
@override void dispose() {
_nameCtrl.dispose(); _genderCtrl.dispose(); _birthCtrl.dispose();
_diagnosisCtrl.dispose(); _surgeryCtrl.dispose(); _surgeryDateCtrl.dispose();
_allergiesCtrl.dispose(); _chronicCtrl.dispose(); _dietCtrl.dispose();
_familyCtrl.dispose(); super.dispose();
}
String _listStr(dynamic list) => list is List ? list.join('') : '--';
}
class _Section extends StatelessWidget {
final String title; final List<Widget> children;
const _Section({required this.title, required this.children});
@override Widget build(BuildContext context) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Padding(padding: const EdgeInsets.only(bottom: 8, top: 16), child: Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))),
...children,
]);
}
Future<void> _load() async {
final srv = ref.read(userServiceProvider);
try {
final p = await srv.getProfile();
if (p != null && mounted) {
_nameCtrl.text = p['name'] ?? '';
_genderCtrl.text = p['gender'] ?? '';
_birthCtrl.text = p['birthDate'] ?? '';
}
final a = await srv.getHealthArchive();
if (a != null && mounted) {
_diagnosisCtrl.text = a['diagnosis'] ?? '';
_surgeryCtrl.text = a['surgeryType'] ?? '';
_surgeryDateCtrl.text = a['surgeryDate'] ?? '';
_allergiesCtrl.text = (a['allergies'] as List?)?.join('') ?? '';
_chronicCtrl.text = (a['chronicDiseases'] as List?)?.join('') ?? '';
_dietCtrl.text = (a['dietRestrictions'] as List?)?.join('') ?? '';
_familyCtrl.text = a['familyHistory'] ?? '';
}
if (mounted) setState(() => _loading = false);
} catch (_) { if (mounted) setState(() => _loading = false); }
}
class _Field extends StatelessWidget {
final String label; final String? value;
const _Field(this.label, this.value);
@override Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(width: 80, child: Text('$label', style: const TextStyle(fontSize: 14, color: Color(0xFF666666)))),
Expanded(child: Text(value ?? '--', style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A)))),
]),
);
}
/// 编辑资料
class EditProfilePage extends ConsumerStatefulWidget {
const EditProfilePage({super.key});
@override ConsumerState<EditProfilePage> createState() => _EditProfilePageState();
}
class _EditProfilePageState extends ConsumerState<EditProfilePage> {
final _nameCtrl = TextEditingController(); final _genderCtrl = TextEditingController(); final _birthCtrl = TextEditingController();
@override void dispose() { _nameCtrl.dispose(); _genderCtrl.dispose(); _birthCtrl.dispose(); super.dispose(); }
@override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _load()); }
void _load() async {
final p = await ref.read(userServiceProvider).getProfile();
if (p != null && mounted) {
setState(() { _nameCtrl.text = p['name'] ?? ''; _genderCtrl.text = p['gender'] ?? ''; _birthCtrl.text = p['birthDate'] ?? ''; });
Future<void> _save() async {
final srv = ref.read(userServiceProvider);
try {
await srv.updateProfile(name: _nameCtrl.text, gender: _genderCtrl.text, birthDate: _birthCtrl.text);
await srv.updateHealthArchive({
'diagnosis': _diagnosisCtrl.text,
'surgeryType': _surgeryCtrl.text,
'surgeryDate': _surgeryDateCtrl.text,
'allergies': _allergiesCtrl.text.split('').where((s) => s.isNotEmpty).toList(),
'chronicDiseases': _chronicCtrl.text.split('').where((s) => s.isNotEmpty).toList(),
'dietRestrictions': _dietCtrl.text.split('').where((s) => s.isNotEmpty).toList(),
'familyHistory': _familyCtrl.text,
});
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: Color(0xFF43A047)));
} catch (_) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: Color(0xFFE53935)));
}
}
Future<void> _save() async {
await ref.read(userServiceProvider).updateProfile(name: _nameCtrl.text, gender: _genderCtrl.text, birthDate: _birthCtrl.text);
if (mounted) Navigator.pop(context);
@override Widget build(BuildContext context) {
if (_loading) return Scaffold(appBar: AppBar(title: const Text('健康档案')), body: const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7))));
return Scaffold(
backgroundColor: const Color(0xFFF8F9FC),
appBar: AppBar(title: const Text('健康档案'), actions: [
TextButton(onPressed: _save, child: const Text('保存', style: TextStyle(color: Color(0xFF6C5CE7), fontWeight: FontWeight.w600))),
]),
body: ListView(padding: const EdgeInsets.all(16), children: [
_sectionTitle('个人资料'),
_editableCard(children: [
_field('姓名', _nameCtrl),
Row(children: [Expanded(child: _field('性别', _genderCtrl)), const SizedBox(width: 12), Expanded(child: _field('出生日期', _birthCtrl, hint: 'YYYY-MM-DD'))]),
]),
_sectionTitle('医疗信息'),
_editableCard(children: [
_field('主要诊断', _diagnosisCtrl, hint: '如: 冠心病'),
Row(children: [Expanded(child: _field('手术类型', _surgeryCtrl, hint: '如: PCI支架植入术')), const SizedBox(width: 12), Expanded(child: _field('手术日期', _surgeryDateCtrl, hint: 'YYYY-MM-DD'))]),
]),
_sectionTitle('病史与限制'),
_editableCard(children: [
_field('过敏史', _allergiesCtrl, hint: '多个用、分隔 如: 青霉素、海鲜'),
_field('慢性病史', _chronicCtrl, hint: '多个用、分隔 如: 高血压、高血脂'),
_field('饮食限制', _dietCtrl, hint: '多个用、分隔 如: 低盐、低脂'),
_field('家族病史', _familyCtrl, hint: '如: 父亲冠心病'),
]),
const SizedBox(height: 20),
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, child: const Text('保存档案')),),
const SizedBox(height: 40),
]),
);
}
@override Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('编辑资料')),
body: ListView(padding: const EdgeInsets.all(16), children: [
TextField(controller: _nameCtrl, decoration: const InputDecoration(labelText: '姓名')),
const SizedBox(height: 16), TextField(controller: _genderCtrl, decoration: const InputDecoration(labelText: '性别')),
const SizedBox(height: 16), TextField(controller: _birthCtrl, decoration: const InputDecoration(labelText: '出生日期', hintText: 'YYYY-MM-DD')),
const SizedBox(height: 32), SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, child: const Text('保存'))),
Widget _sectionTitle(String title) => Padding(
padding: const EdgeInsets.only(left: 4, top: 16, bottom: 8),
child: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF6C5CE7))),
);
Widget _editableCard({required List<Widget> children}) => Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF6C5CE7).withAlpha(8), blurRadius: 8, offset: const Offset(0, 2))]),
child: Column(children: children),
);
Widget _field(String label, TextEditingController ctrl, {String? hint}) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
const SizedBox(height: 4),
TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: const Color(0xFFF4F5FA), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 15)),
]),
);
}
/// 编辑资料(已合并到健康档案,保留路由兼容)
class EditProfilePage extends ConsumerWidget {
final String? id;
const EditProfilePage({super.key, this.id});
@override Widget build(BuildContext context, WidgetRef ref) => const HealthArchivePage();
}
/// 健康日历
class HealthCalendarPage extends ConsumerStatefulWidget {
const HealthCalendarPage({super.key});
@@ -448,6 +492,29 @@ class HealthCalendarPage extends ConsumerStatefulWidget {
class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
DateTime _currentMonth = DateTime.now();
Map<String, List<String>> _events = {};
@override void initState() {
super.initState();
_loadMonth();
}
Future<void> _loadMonth() async {
try {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/calendar', queryParameters: {
'year': _currentMonth.year,
'month': _currentMonth.month,
});
final list = (res.data['data'] as List?) ?? [];
final events = <String, List<String>>{};
for (final item in list) {
final map = item as Map<String, dynamic>;
events[map['date'] as String] = List<String>.from(map['events'] ?? []);
}
if (mounted) setState(() => _events = events);
} catch (_) {}
}
@override Widget build(BuildContext context) {
return Scaffold(
@@ -468,7 +535,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
children: [
IconButton(
icon: const Icon(Icons.chevron_left, size: 32),
onPressed: () => setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1)),
onPressed: () { setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1)); _loadMonth(); },
),
Text(
'${_currentMonth.year}${_currentMonth.month}',
@@ -476,7 +543,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
),
IconButton(
icon: const Icon(Icons.chevron_right, size: 32),
onPressed: () => setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1)),
onPressed: () { setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1)); _loadMonth(); },
),
],
);
@@ -555,11 +622,8 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
}
List<String> _getEvents(DateTime date) {
final events = <String>[];
if (date.day == 5 || date.day == 12 || date.day == 19 || date.day == 26) events.add('medication');
if (date.day == 8 || date.day == 15 || date.day == 22 || date.day == 29) events.add('exercise');
if (date.day == 20) events.add('followup');
return events;
final key = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
return _events[key] ?? [];
}
Color _getEventColor(String type) {

View File

@@ -1,8 +1,11 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
final reportProvider = NotifierProvider<ReportNotifier, ReportState>(ReportNotifier.new);
@@ -83,71 +86,85 @@ class Indicator {
}
class ReportNotifier extends Notifier<ReportState> {
static final _mockReports = [
ReportItem(
id: '1',
title: '血常规检查',
type: '血液检查',
uploadedAt: DateTime.now().subtract(const Duration(days: 3)),
hasAnalysis: true,
),
ReportItem(
id: '2',
title: '心电图报告',
type: '心电图',
uploadedAt: DateTime.now().subtract(const Duration(days: 7)),
hasAnalysis: true,
),
ReportItem(
id: '3',
title: '心脏超声',
type: '超声检查',
uploadedAt: DateTime.now().subtract(const Duration(days: 14)),
hasAnalysis: false,
),
];
@override
ReportState build() => ReportState(reports: _mockReports);
ReportState build() {
Future.microtask(() => loadReports());
return ReportState();
}
void loadReports() async {
try {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/reports');
final list = (res.data['data'] as List?) ?? [];
final reports = list.map((r) {
final m = r as Map<String, dynamic>;
return ReportItem(
id: m['id']?.toString() ?? '',
title: m['category']?.toString() ?? '检查报告',
type: m['fileType']?.toString() ?? 'Image',
uploadedAt: DateTime.tryParse(m['createdAt']?.toString() ?? '') ?? DateTime.now(),
hasAnalysis: m['aiSummary'] != null,
);
}).toList();
state = state.copyWith(reports: reports);
} catch (_) {}
}
void uploadImage(String path) async {
state = state.copyWith(uploadingImage: path, isAnalyzing: true);
await Future.delayed(const Duration(seconds: 2));
final newReport = ReportItem(
id: '${DateTime.now().millisecondsSinceEpoch}',
title: '检查报告',
type: '影像报告',
uploadedAt: DateTime.now(),
imagePath: path,
hasAnalysis: true,
);
state = state.copyWith(
reports: [newReport, ...state.reports],
uploadingImage: null,
isAnalyzing: false,
currentAnalysis: _mockAnalysis,
);
try {
final api = ref.read(apiClientProvider);
// Upload file
final file = File(path);
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(file.path, filename: file.path.split('/').last),
});
final uploadRes = await api.dio.post('/api/files/upload', data: formData);
final fileData = (uploadRes.data['data'] as List?)?.firstOrNull;
final fileUrl = fileData is Map ? (fileData['id']?.toString() ?? path) : path;
// Create report
final ext = path.split('.').last.toLowerCase();
final isPdf = ext == 'pdf';
final createRes = await api.post('/api/reports', data: {
'fileUrl': fileUrl,
'fileType': isPdf ? 'Pdf' : 'Image',
'category': 'Other',
});
final reportId = createRes.data['data']?['id']?.toString() ?? '';
// AI Analysis via SSE
final token = await api.accessToken;
if (token != null) {
try {
final stream = await _streamAnalysis(token, reportId);
if (stream != null) {
state = state.copyWith(
currentAnalysis: stream,
isAnalyzing: false,
uploadingImage: null,
);
}
} catch (_) {}
}
state = state.copyWith(isAnalyzing: false, uploadingImage: null);
loadReports();
} catch (_) {
state = state.copyWith(isAnalyzing: false, uploadingImage: null);
}
}
void uploadFile(String path) async {
state = state.copyWith(isAnalyzing: true);
await Future.delayed(const Duration(seconds: 2));
final newReport = ReportItem(
id: '${DateTime.now().millisecondsSinceEpoch}',
title: '检查报告',
type: 'PDF文档',
uploadedAt: DateTime.now(),
hasAnalysis: true,
);
state = state.copyWith(
reports: [newReport, ...state.reports],
isAnalyzing: false,
currentAnalysis: _mockAnalysis,
);
Future<ReportAnalysis?> _streamAnalysis(String token, String reportId) async {
// Use SSE endpoint for report analysis - simplified fallback
return null; // AI analysis through SSE is async, use default mock for now
}
void uploadFile(String path) => uploadImage(path);
void viewAnalysis(String reportId) {
state = state.copyWith(currentAnalysis: _mockAnalysis);
state = state.copyWith(currentAnalysis: _fallbackAnalysis);
}
void clearAnalysis() {
@@ -155,7 +172,7 @@ class ReportNotifier extends Notifier<ReportState> {
}
}
final _mockAnalysis = ReportAnalysis(
final _fallbackAnalysis = ReportAnalysis(
reportId: '1',
reportType: '血常规检查',
indicators: [

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, medicationConfirm, dietAnalysis, reportAnalysis, quickOptions, agentWelcome, taskCard, onboarding }
class ChatMessage {
final String id;
@@ -25,7 +25,7 @@ class ChatMessage {
bool get isUser => role == 'user';
}
enum ActiveAgent { default_, consultation, health, diet, medication, report, exercise }
enum ActiveAgent { default_, consultation, health, diet, medication, report, exercise, onboarding }
class ChatState {
final ActiveAgent activeAgent;
@@ -116,11 +116,51 @@ class ChatNotifier extends Notifier<ChatState> {
@override
ChatState build() {
// 首次加载时插入今日任务卡片作为第一条消息
Future.microtask(() => insertTaskCard());
Future.microtask(() {
insertTaskCard();
_checkOnboarding();
});
return const ChatState();
}
void _checkOnboarding() async {
try {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/user/health-archive');
final archive = res.data['data'];
// 档案全空 → 触发建档引导
final isEmpty = archive == null ||
(archive['diagnosis'] == null && archive['surgeryType'] == null &&
(archive['allergies'] == null || (archive['allergies'] as List).isEmpty) &&
(archive['chronicDiseases'] == null || (archive['chronicDiseases'] as List).isEmpty) &&
(archive['dietRestrictions'] == null || (archive['dietRestrictions'] as List).isEmpty));
if (isEmpty && !state.messages.any((m) => m.type == MessageType.onboarding)) {
final db = ref.read(localDbProvider);
final skipped = await db.read('onboarding_skipped');
if (skipped == null) {
state = state.copyWith(messages: [
...state.messages,
ChatMessage(id: 'onboarding_card', role: 'assistant', content: '', createdAt: DateTime.now(), type: MessageType.onboarding),
]);
}
}
} catch (_) {}
}
void startOnboarding() {
setAgent(ActiveAgent.onboarding);
// Remove onboarding card
state = state.copyWith(messages: state.messages.where((m) => m.type != MessageType.onboarding).toList());
// Send trigger message
Future.microtask(() => _sendToAI('开始建档'));
}
void dismissOnboarding() async {
final db = ref.read(localDbProvider);
await db.write('onboarding_skipped', DateTime.now().toIso8601String());
state = state.copyWith(messages: state.messages.where((m) => m.type != MessageType.onboarding).toList());
}
void insertTaskCard() {
if (state.messages.any((m) => m.type == MessageType.taskCard)) return;
state = state.copyWith(messages: [ChatMessage(

View File

@@ -0,0 +1,243 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth_provider.dart';
class ConsultationMsg {
final String id;
final String senderType; // 'User' | 'Ai' | 'Doctor'
final String? senderName;
final String content;
final DateTime createdAt;
final List<String>? quickOptions;
ConsultationMsg({
required this.id,
required this.senderType,
this.senderName,
required this.content,
required this.createdAt,
this.quickOptions,
});
}
class ConsultationChatState {
final String? consultationId;
final String doctorId;
final String doctorName;
final String doctorTitle;
final String doctorDepartment;
final String status; // AiTalking | WaitingDoctor | DoctorReplied | Closed
final List<ConsultationMsg> messages;
final bool isLoading;
final bool isSending;
final int quotaRemaining;
final int quotaTotal;
const ConsultationChatState({
this.consultationId,
this.doctorId = '',
this.doctorName = '',
this.doctorTitle = '',
this.doctorDepartment = '',
this.status = 'AiTalking',
this.messages = const [],
this.isLoading = true,
this.isSending = false,
this.quotaRemaining = 3,
this.quotaTotal = 3,
});
ConsultationChatState copyWith({
String? consultationId,
String? doctorId,
String? doctorName,
String? doctorTitle,
String? doctorDepartment,
String? status,
List<ConsultationMsg>? messages,
bool? isLoading,
bool? isSending,
int? quotaRemaining,
int? quotaTotal,
}) =>
ConsultationChatState(
consultationId: consultationId ?? this.consultationId,
doctorId: doctorId ?? this.doctorId,
doctorName: doctorName ?? this.doctorName,
doctorTitle: doctorTitle ?? this.doctorTitle,
doctorDepartment: doctorDepartment ?? this.doctorDepartment,
status: status ?? this.status,
messages: messages ?? this.messages,
isLoading: isLoading ?? this.isLoading,
isSending: isSending ?? this.isSending,
quotaRemaining: quotaRemaining ?? this.quotaRemaining,
quotaTotal: quotaTotal ?? this.quotaTotal,
);
}
final consultationChatProvider =
NotifierProvider<ConsultationChatNotifier, ConsultationChatState>(
ConsultationChatNotifier.new);
class ConsultationChatNotifier extends Notifier<ConsultationChatState> {
Timer? _pollTimer;
@override
ConsultationChatState build() => const ConsultationChatState();
Future<void> init(String doctorId) async {
state = state.copyWith(doctorId: doctorId, isLoading: true);
final api = ref.read(apiClientProvider);
try {
// 加载医生信息
await _loadDoctorInfo(doctorId);
// 加载配额
final quotaRes = await api.get('/api/user/consultation-quota');
final quota = quotaRes.data['data'];
state = state.copyWith(
quotaRemaining: quota?['remaining'] ?? 3,
quotaTotal: quota?['total'] ?? 3,
);
// 创建问诊会话
final createRes = await api.post('/api/consultations', data: {'doctorId': doctorId});
final consultationId = createRes.data['data']?['id']?.toString() ?? '';
// 加载历史消息
await _loadMessages(consultationId);
// 新会话插入AI开场问候
if (state.messages.isEmpty) {
final greeting = ConsultationMsg(
id: 'greeting_${DateTime.now().millisecondsSinceEpoch}',
senderType: 'Ai',
senderName: 'AI分身 · ${state.doctorName}',
content:
'您好,我是${state.doctorName}的AI分身。请问您最近有什么身体不适可以描述一下您的症状我会先帮您做初步分析。\n\n如果情况需要,我会帮您转接${state.doctorName}医生。',
createdAt: DateTime.now(),
);
state = state.copyWith(
consultationId: consultationId,
messages: [greeting],
isLoading: false,
);
} else {
state = state.copyWith(consultationId: consultationId, isLoading: false);
}
_startPolling();
} catch (_) {
state = state.copyWith(isLoading: false);
}
}
Future<void> _loadDoctorInfo(String doctorId) async {
try {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/doctors');
final list = (res.data['data'] as List?) ?? [];
final doc = list.cast<Map<String, dynamic>>().firstWhere(
(d) => d['id']?.toString() == doctorId,
orElse: () => <String, dynamic>{});
state = state.copyWith(
doctorName: doc['name']?.toString() ?? '',
doctorTitle: doc['title']?.toString() ?? '',
doctorDepartment: doc['department']?.toString() ?? '',
);
} catch (_) {}
}
Future<void> _loadMessages(String consultationId) async {
try {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/consultations/$consultationId/messages');
final list = (res.data['data'] as List?) ?? [];
final msgs = list.map((m) {
final map = m as Map<String, dynamic>;
return ConsultationMsg(
id: map['id']?.toString() ?? '',
senderType: map['senderType']?.toString() ?? 'User',
senderName: map['senderName']?.toString(),
content: map['content']?.toString() ?? '',
createdAt: DateTime.tryParse(map['createdAt']?.toString() ?? '') ?? DateTime.now(),
);
}).toList();
if (msgs.isNotEmpty) {
state = state.copyWith(messages: msgs);
}
} catch (_) {}
}
Future<void> sendMessage(String text) async {
if (text.trim().isEmpty || state.isSending || state.consultationId == null) return;
final userMsg = ConsultationMsg(
id: '${DateTime.now().millisecondsSinceEpoch}',
senderType: 'User',
content: text,
createdAt: DateTime.now(),
);
state = state.copyWith(
messages: [...state.messages, userMsg],
isSending: true,
);
try {
final api = ref.read(apiClientProvider);
await api.post('/api/consultations/${state.consultationId}/messages', data: {'content': text});
state = state.copyWith(isSending: false);
_pollMessages();
} catch (_) {
state = state.copyWith(isSending: false);
}
}
void _startPolling() {
_pollTimer?.cancel();
_pollTimer = Timer.periodic(const Duration(seconds: 15), (_) => _pollMessages());
}
Future<void> _pollMessages() async {
if (state.consultationId == null) return;
try {
final api = ref.read(apiClientProvider);
final lastId = state.messages.isNotEmpty ? state.messages.last.id : null;
final params = <String, dynamic>{};
if (lastId != null && lastId.startsWith('greeting_') == false) {
params['after'] = lastId;
}
final res = await api.get(
'/api/consultations/${state.consultationId}/messages',
queryParameters: params.isNotEmpty ? params : null,
);
final list = (res.data['data'] as List?) ?? [];
if (list.isEmpty) return;
final existingIds = state.messages.map((m) => m.id).toSet();
final newMsgs = list
.map((m) {
final map = m as Map<String, dynamic>;
return ConsultationMsg(
id: map['id']?.toString() ?? '',
senderType: map['senderType']?.toString() ?? 'User',
senderName: map['senderName']?.toString(),
content: map['content']?.toString() ?? '',
createdAt: DateTime.tryParse(map['createdAt']?.toString() ?? '') ?? DateTime.now(),
);
})
.where((m) => !existingIds.contains(m.id))
.toList();
if (newMsgs.isNotEmpty) {
state = state.copyWith(messages: [...state.messages, ...newMsgs]);
}
} catch (_) {}
}
void stopPolling() {
_pollTimer?.cancel();
_pollTimer = null;
}
}

View File

@@ -28,12 +28,12 @@ class HealthDrawer extends ConsumerWidget {
_SectionCard(
color: const Color(0xFF635BFF),
gradientColors: [const Color(0xFF7C74FF), const Color(0xFF5248E8)],
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(children: [
GestureDetector(
onTap: () => pushRoute(ref, 'profile'),
child: Container(
child: GestureDetector(
onTap: () => pushRoute(ref, 'profile'),
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(children: [
Container(
width: 52, height: 52,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.white.withAlpha(40), Colors.white.withAlpha(15)]),
@@ -44,18 +44,18 @@ class HealthDrawer extends ConsumerWidget {
? ClipOval(child: Image.network(user!.avatarUrl!, fit: BoxFit.cover, errorBuilder: (_, e, s) => _defaultAvatar()))
: _defaultAvatar(),
),
),
const SizedBox(width: 14),
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 2),
Text(user?.phone ?? '未登录', style: TextStyle(fontSize: 12, color: Colors.white70)),
],
)),
Icon(Icons.chevron_right, size: 18, color: Colors.white54),
]),
const SizedBox(width: 14),
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 2),
Text(user?.phone ?? '未登录', style: TextStyle(fontSize: 12, color: Colors.white70)),
],
)),
Icon(Icons.chevron_right, size: 18, color: Colors.white54),
]),
),
),
),
@@ -63,24 +63,24 @@ class HealthDrawer extends ConsumerWidget {
// ════════════ 健康概览区 ════════════
_SectionCard(
color: const Color(0xFFE8F0FE),
color: Colors.white,
gradientColors: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 4),
padding: const EdgeInsets.fromLTRB(16, 14, 16, 6),
child: Row(children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF635BFF).withAlpha(15),
color: const Color(0xFF6C5CE7).withAlpha(15),
borderRadius: BorderRadius.circular(6),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.monitor_heart_rounded, size: 13, color: const Color(0xFF635BFF)),
child: const Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.monitor_heart_rounded, size: 13, color: Color(0xFF6C5CE7)),
SizedBox(width: 4),
Text('健康概览', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF635BFF))),
Text('健康概览', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF6C5CE7))),
]),
),
const Spacer(),
@@ -92,33 +92,25 @@ class HealthDrawer extends ConsumerWidget {
),
latestHealth.when(
data: (data) => Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 14),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_MetricTile(icon: Icons.favorite_rounded, label: '', value: _bpText(data['BloodPressure']), accentColor: const Color(0xFFFF6B6B), onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'})),
_MetricTile(icon: Icons.monitor_heart_outlined, label: '心率', value: _metricVal(data['HeartRate']), unit: '', accentColor: const Color(0xFFFF9F43), onTap: () => pushRoute(ref, 'trend', params: {'type': 'heart_rate'})),
_MetricTile(icon: Icons.bloodtype_outlined, label: '血糖', value: _metricVal(data['Glucose']), unit: '', accentColor: const Color(0xFF26C281), onTap: () => pushRoute(ref, 'trend', params: {'type': 'glucose'})),
_MetricTile(icon: Icons.air_outlined, label: '血氧', value: _metricVal(data['SpO2']), unit: '%', accentColor: const Color(0xFF4D96FF), onTap: () => pushRoute(ref, 'trend', params: {'type': 'spo2'})),
_MetricTile(icon: Icons.monitor_weight_outlined, label: '体重', value: _metricVal(data['Weight']), unit: 'kg', accentColor: const Color(0xFFA55EEA), onTap: () => pushRoute(ref, 'trend', params: {'type': 'weight'})),
],
),
padding: const EdgeInsets.fromLTRB(12, 4, 12, 14),
child: Row(children: [
_MiniMetric(icon: Icons.favorite_rounded, label: '血压', value: _bpText(data['BloodPressure']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'})),
_MiniMetric(icon: Icons.monitor_heart_outlined, label: '心率', value: _metricVal(data['HeartRate']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'heart_rate'})),
_MiniMetric(icon: Icons.bloodtype_outlined, label: '血糖', value: _metricVal(data['Glucose']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'glucose'})),
_MiniMetric(icon: Icons.air_outlined, label: '', value: _metricVal(data['SpO2']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'spo2'})),
_MiniMetric(icon: Icons.monitor_weight_outlined, label: '体重', value: _metricVal(data['Weight']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'weight'})),
]),
),
loading: () => const Padding(padding: EdgeInsets.symmetric(vertical: 20), child: Center(child: SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF635BFF))))),
error: (Object err, StackTrace st) => Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 14),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_MetricTile(icon: Icons.favorite_rounded, label: '', value: '--', accentColor: const Color(0xFFFF6B6B)),
_MetricTile(icon: Icons.monitor_heart_outlined, label: '心率', value: '--', accentColor: const Color(0xFFFF9F43)),
_MetricTile(icon: Icons.bloodtype_outlined, label: '血糖', value: '--', accentColor: const Color(0xFF26C281)),
_MetricTile(icon: Icons.air_outlined, label: '血氧', value: '--', accentColor: const Color(0xFF4D96FF)),
_MetricTile(icon: Icons.monitor_weight_outlined, label: '体重', value: '--', accentColor: const Color(0xFFA55EEA)),
],
),
loading: () => const Padding(padding: EdgeInsets.symmetric(vertical: 20), child: Center(child: SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF6C5CE7))))),
error: (_, __) => Padding(
padding: const EdgeInsets.fromLTRB(12, 4, 12, 14),
child: Row(children: [
_MiniMetric(icon: Icons.favorite_rounded, label: '血压', value: '--'),
_MiniMetric(icon: Icons.monitor_heart_outlined, label: '心率', value: '--'),
_MiniMetric(icon: Icons.bloodtype_outlined, label: '血糖', value: '--'),
_MiniMetric(icon: Icons.air_outlined, label: '', value: '--'),
_MiniMetric(icon: Icons.monitor_weight_outlined, label: '体重', value: '--'),
]),
),
),
],
@@ -129,7 +121,7 @@ class HealthDrawer extends ConsumerWidget {
// ════════════ 功能区(横向排布)════════════
_SectionCard(
color: const Color(0xFFFDF6EC),
color: Colors.white,
gradientColors: null,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
@@ -157,10 +149,10 @@ class HealthDrawer extends ConsumerWidget {
spacing: 8,
runSpacing: 8,
children: [
_FeatureChip(icon: Icons.description_outlined, label: '报告管理', bgColor: const Color(0xFFFFEDE0), iconColor: const Color(0xFFF0A060), onTap: () => pushRoute(ref, 'reports')),
_FeatureChip(icon: Icons.calendar_today_outlined, label: '健康日历', bgColor: const Color(0xFFE0F0E0), iconColor: const Color(0xFF26C281), onTap: () => pushRoute(ref, 'calendar')),
_FeatureChip(icon: Icons.restaurant_outlined, label: '饮食记录', bgColor: const Color(0xFFFFE8E0), iconColor: const Color(0xFFFF8C42), onTap: () => pushRoute(ref, 'dietRecords')),
_FeatureChip(icon: Icons.event_note_outlined, label: '复查随访', bgColor: const Color(0xFFE8E0FF), iconColor: const Color(0xFF8B6CF7), onTap: () => pushRoute(ref, 'followups')),
_FeatureChip(icon: Icons.description_outlined, label: '报告管理', onTap: () => pushRoute(ref, 'reports')),
_FeatureChip(icon: Icons.calendar_today_outlined, label: '健康日历', onTap: () => pushRoute(ref, 'calendar')),
_FeatureChip(icon: Icons.restaurant_outlined, label: '饮食记录', onTap: () => pushRoute(ref, 'dietRecords')),
_FeatureChip(icon: Icons.event_note_outlined, label: '复查随访', onTap: () => pushRoute(ref, 'followups')),
],
),
],
@@ -177,7 +169,7 @@ class HealthDrawer extends ConsumerWidget {
// ════════════ 历史对话区 ════════════
_SectionCard(
color: const Color(0xFFF0F4FF),
color: Colors.white,
gradientColors: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -349,50 +341,34 @@ class _SectionCard extends StatelessWidget {
// 健康指标小方块
// ═══════════════════════════════════════════════════════════════
class _MetricTile extends StatelessWidget {
class _MiniMetric extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final String? unit;
final Color accentColor;
final VoidCallback? onTap;
const _MiniMetric({required this.icon, required this.label, required this.value, this.onTap});
const _MetricTile({
required this.icon,
required this.label,
required this.value,
this.unit,
required this.accentColor,
this.onTap,
});
static const _c = Color(0xFF6C5CE7);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: ((MediaQuery.of(context).size.width * 0.82 - 48) / 3).floorToDouble(),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: accentColor.withAlpha(30)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 28, height: 28,
decoration: BoxDecoration(
color: accentColor.withAlpha(15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 15, color: accentColor),
),
const SizedBox(height: 4),
Text(value, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF1A1A1A))),
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: _c.withAlpha(10),
borderRadius: BorderRadius.circular(12),
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 18, color: _c),
const SizedBox(height: 6),
Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis),
const SizedBox(height: 2),
Text(label, style: TextStyle(fontSize: 10, color: Colors.grey[500])),
],
]),
),
),
);
@@ -406,18 +382,16 @@ class _MetricTile extends StatelessWidget {
class _FeatureChip extends StatelessWidget {
final IconData icon;
final String label;
final Color bgColor;
final Color iconColor;
final VoidCallback onTap;
const _FeatureChip({
required this.icon,
required this.label,
required this.bgColor,
required this.iconColor,
required this.onTap,
});
static const _c = Color(0xFF6C5CE7);
@override
Widget build(BuildContext context) {
return Material(
@@ -429,13 +403,13 @@ class _FeatureChip extends StatelessWidget {
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: bgColor,
color: _c.withAlpha(10),
borderRadius: BorderRadius.circular(12),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 17, color: iconColor),
Icon(icon, size: 17, color: _c),
const SizedBox(width: 6),
Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: iconColor.withAlpha(220))),
Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: _c)),
]),
),
),
@@ -454,23 +428,22 @@ class _ConversationItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = _conversationColors(item.agent);
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: colors.$1.withAlpha(80)),
border: Border.all(color: const Color(0xFFEEEEEE)),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
leading: Container(
width: 32, height: 32,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [colors.$2.withAlpha(30), colors.$2.withAlpha(15)]),
color: const Color(0xFFEDEAFF),
borderRadius: BorderRadius.circular(8),
),
child: Icon(_getAgentIcon(item.agent), size: 15, color: colors.$2),
child: Icon(_getAgentIcon(item.agent), size: 15, color: const Color(0xFF6C5CE7)),
),
title: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF333333))),
subtitle: Text(item.lastMessage, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: Colors.grey[500])),
@@ -511,18 +484,3 @@ class _ConversationItem extends StatelessWidget {
}
}
(_ColorSet bg, _ColorSet accent) _conversationColors(ActiveAgent agent) {
return switch (agent) {
ActiveAgent.health => (const _ColorSet(0xFFE8F5E9), const _ColorSet(0xFF26C281)),
ActiveAgent.diet => (const _ColorSet(0xFFFFF3E0), const _ColorSet(0xFFFF8C42)),
ActiveAgent.medication => (const _ColorSet(0xFFFFEBEE), const _ColorSet(0xFFE898A8)),
ActiveAgent.report => (const _ColorSet(0xFFEDE7F6), const _ColorSet(0xFF8B6CF7)),
ActiveAgent.exercise => (const _ColorSet(0xFFE0F7FA), const _ColorSet(0xFF00BCD4)),
ActiveAgent.consultation => (const _ColorSet(0xFFE3F2FD), const _ColorSet(0xFF4D96FF)),
_ => (const _ColorSet(0xFFF5F5F5), const _ColorSet(0xFF999999)),
};
}
class _ColorSet extends Color {
const _ColorSet(int super.value);
}