refactor: 4层架构重构 + 饮食VLM接入 + 多项修复
- 后端: remaining_endpoints拆分为6个独立文件 - 后端: AI Agent Handler从ai_chat_endpoints抽取为7个独立处理器 - 后端: 食物识别prompt改为输出结构化JSON - 前端: 饮食识别从Mock替换为真实VLM API调用 - 前端: 首页图片上传URL修复(/api/upload→/api/files/upload) - 前端: 拍饮食按钮导航到独立DietCapturePage - 前端: 删除无用agent_bar.dart - 前端: 修复widget_test.dart过期属性名 - 前端: 恢复ServicePackageCard和详情页 - 新增6份实施文档(情况/问诊/报告/建档/日历/视觉统一)
This commit is contained in:
@@ -12,6 +12,7 @@ import '../pages/settings/settings_pages.dart';
|
||||
import '../pages/settings/notification_prefs_page.dart';
|
||||
import '../pages/profile/profile_page.dart';
|
||||
import '../pages/profile/profile_detail_page.dart';
|
||||
import '../pages/profile/service_package_detail_page.dart';
|
||||
import '../pages/diet/diet_capture_page.dart';
|
||||
import '../pages/remaining_pages.dart';
|
||||
|
||||
@@ -65,6 +66,8 @@ Widget buildPage(RouteInfo route) {
|
||||
return const NotificationPrefsPage();
|
||||
case 'staticText':
|
||||
return StaticTextPage(type: params['type']!);
|
||||
case 'servicePackageDetail':
|
||||
return ServicePackageDetailPage(packageId: params['id']!);
|
||||
default:
|
||||
return const LoginPage();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'dart:convert';
|
||||
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 '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
final dietProvider = NotifierProvider<DietNotifier, DietState>(DietNotifier.new);
|
||||
|
||||
@@ -41,12 +44,14 @@ class DietState {
|
||||
class FoodItem {
|
||||
final String id;
|
||||
String name;
|
||||
String portion;
|
||||
int calories;
|
||||
bool selected;
|
||||
|
||||
FoodItem({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.portion,
|
||||
required this.calories,
|
||||
this.selected = true,
|
||||
});
|
||||
@@ -60,36 +65,94 @@ class DietNotifier extends Notifier<DietState> {
|
||||
state = state.copyWith(imagePath: path);
|
||||
}
|
||||
|
||||
void analyzeImage() async {
|
||||
String? _analysisError;
|
||||
|
||||
Future<void> analyzeImage() async {
|
||||
state = state.copyWith(isAnalyzing: true);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
final mockFoods = [
|
||||
FoodItem(id: '1', name: '米饭', calories: 150),
|
||||
FoodItem(id: '2', name: '番茄炒蛋', calories: 200),
|
||||
FoodItem(id: '3', name: '红烧肉', calories: 350),
|
||||
FoodItem(id: '4', name: '青菜', calories: 50),
|
||||
];
|
||||
state = state.copyWith(foods: mockFoods, isAnalyzing: false, healthScore: 3);
|
||||
_analysisError = null;
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final imageFile = File(state.imagePath!);
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'images': await MultipartFile.fromFile(
|
||||
imageFile.path,
|
||||
filename: imageFile.path.split('/').last,
|
||||
),
|
||||
});
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
final raw = data['data'] as String? ?? '[]';
|
||||
final foods = _parseFoodItems(raw);
|
||||
state = state.copyWith(
|
||||
foods: foods,
|
||||
isAnalyzing: false,
|
||||
healthScore: foods.isNotEmpty ? 3 : null,
|
||||
);
|
||||
} catch (e) {
|
||||
_analysisError = '识别失败: $e';
|
||||
state = state.copyWith(isAnalyzing: false);
|
||||
}
|
||||
}
|
||||
|
||||
List<FoodItem> _parseFoodItems(String raw) {
|
||||
var json = raw.trim();
|
||||
if (json.startsWith('```')) {
|
||||
final start = json.indexOf('\n');
|
||||
if (start != -1) json = json.substring(start + 1);
|
||||
final end = json.lastIndexOf('```');
|
||||
if (end != -1) json = json.substring(0, end);
|
||||
json = json.trim();
|
||||
}
|
||||
try {
|
||||
final list = jsonDecode(json) as List;
|
||||
return list.asMap().entries.map((e) {
|
||||
final item = e.value as Map<String, dynamic>;
|
||||
return FoodItem(
|
||||
id: 'food_${DateTime.now().millisecondsSinceEpoch}_${e.key}',
|
||||
name: item['name']?.toString() ?? '未知食物',
|
||||
portion: item['portion']?.toString() ?? '',
|
||||
calories: (item['calories'] as num?)?.toInt() ?? 0,
|
||||
selected: true,
|
||||
);
|
||||
}).toList();
|
||||
} catch (_) {
|
||||
return [
|
||||
FoodItem(
|
||||
id: 'food_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: '识别结果(手动编辑)',
|
||||
portion: raw.length > 50 ? raw.substring(0, 50) : raw,
|
||||
calories: 0,
|
||||
selected: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
void updateFoodName(String id, String name) {
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: name, calories: f.calories, selected: f.selected) : f).toList();
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: name, portion: f.portion, calories: f.calories, selected: f.selected) : f).toList();
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
void updateFoodCalories(String id, int calories) {
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, calories: calories, selected: f.selected) : f).toList();
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, portion: f.portion, calories: calories, selected: f.selected) : f).toList();
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
void toggleFood(String id) {
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, calories: f.calories, selected: !f.selected) : f).toList();
|
||||
final foods = state.foods.map((f) => f.id == id ? FoodItem(id: f.id, name: f.name, portion: f.portion, calories: f.calories, selected: !f.selected) : f).toList();
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
void addFood() {
|
||||
final newId = '${DateTime.now().millisecondsSinceEpoch}';
|
||||
final foods = [...state.foods, FoodItem(id: newId, name: '新食物', calories: 100)];
|
||||
final foods = [...state.foods, FoodItem(id: newId, name: '新食物', portion: '', calories: 100)];
|
||||
state = state.copyWith(foods: foods);
|
||||
}
|
||||
|
||||
@@ -330,6 +393,8 @@ class DietCapturePage extends ConsumerWidget {
|
||||
onChanged: (v) => ref.read(dietProvider.notifier).updateFoodName(food.id, v),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
if (food.portion.isNotEmpty)
|
||||
Text(food.portion, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Row(children: [
|
||||
const Text('热量:', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
SizedBox(
|
||||
|
||||
@@ -1455,8 +1455,8 @@ final _agentActions = <ActiveAgent, List<_AgentAction>>{
|
||||
_AgentAction(label: '录入体重', icon: Icons.monitor_weight_outlined, route: 'trend'),
|
||||
],
|
||||
ActiveAgent.diet: [
|
||||
_AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true, route: 'camera'),
|
||||
_AgentAction(label: '上传照片', icon: Icons.photo_library_outlined, isWide: true, route: 'gallery'),
|
||||
_AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true, route: 'dietCapture'),
|
||||
_AgentAction(label: '上传照片', icon: Icons.photo_library_outlined, isWide: true, route: 'dietCapture'),
|
||||
],
|
||||
ActiveAgent.medication: [
|
||||
_AgentAction(label: '用药管理', icon: Icons.medication_liquid_outlined, isWide: true, route: 'medications'),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/service_package_card.dart';
|
||||
|
||||
class ProfilePage extends ConsumerWidget {
|
||||
const ProfilePage({super.key});
|
||||
@@ -33,6 +34,11 @@ class ProfilePage extends ConsumerWidget {
|
||||
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')),
|
||||
|
||||
125
health_app/lib/pages/profile/service_package_detail_page.dart
Normal file
125
health_app/lib/pages/profile/service_package_detail_page.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../widgets/service_package_card.dart';
|
||||
|
||||
class ServicePackageDetailPage extends ConsumerWidget {
|
||||
final String packageId;
|
||||
const ServicePackageDetailPage({super.key, required this.packageId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final package = servicePackages.where((p) => p.id == packageId).firstOrNull;
|
||||
if (package == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('服务包详情')),
|
||||
body: const Center(child: Text('未找到该服务包')),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(
|
||||
title: Text(package.title, style: const TextStyle(fontSize: 16)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 头部卡片
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [package.headerColor, package.headerColor.withAlpha(180)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(40),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text('VIP 产品权益', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(package.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.white)),
|
||||
const SizedBox(height: 8),
|
||||
Text(package.subtitle, style: TextStyle(fontSize: 14, color: Colors.white.withAlpha(200))),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 服务图标
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: package.services.map((s) => SizedBox(
|
||||
width: (MediaQuery.of(context).size.width - 72) / 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 48, height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryLight,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(s.icon, size: 22, color: AppTheme.primary),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(s.label, style: const TextStyle(fontSize: 12, color: AppTheme.textSub), textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
),
|
||||
// 适用人群
|
||||
_Section(title: '适用人群', child: Text(package.targetAudience, style: const TextStyle(fontSize: 14, color: AppTheme.textSub, height: 1.6))),
|
||||
// 详细说明
|
||||
...package.detailSections.map((s) => _Section(title: s.title, child: Text(s.content, style: const TextStyle(fontSize: 14, color: AppTheme.textSub, height: 1.6)))),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Section extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
const _Section({required this.title, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.text)),
|
||||
const SizedBox(height: 10),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
String? uploadedUrl;
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
uploadedUrl = await api.uploadFile('/api/upload', file);
|
||||
uploadedUrl = await api.uploadFile('/api/files/upload', file);
|
||||
} catch (_) {
|
||||
// 上传失败:保留本地路径,仍然可以本地显示
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/chat_provider.dart';
|
||||
|
||||
/// 智能体胶囊栏——横向滑动
|
||||
class AgentBar extends ConsumerWidget {
|
||||
const AgentBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selected = ref.watch(selectedAgentProvider);
|
||||
final chatNotifier = ref.read(chatProvider.notifier);
|
||||
|
||||
void onTap(ActiveAgent agent) {
|
||||
final notifier = ref.read(selectedAgentProvider.notifier);
|
||||
notifier.select(agent == selected ? null : agent);
|
||||
chatNotifier.setAgent(agent);
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 48,
|
||||
color: Colors.white,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
children: [
|
||||
_buildCapsule('AI问诊', Icons.medical_services, ActiveAgent.consultation, selected, onTap),
|
||||
_buildCapsule('记数据', Icons.edit_note, ActiveAgent.health, selected, onTap),
|
||||
_buildCapsule('拍饮食', Icons.restaurant, ActiveAgent.diet, selected, onTap),
|
||||
_buildCapsule('药管家', Icons.medication, ActiveAgent.medication, selected, onTap),
|
||||
_buildCapsule('看报告', Icons.description, ActiveAgent.report, selected, onTap),
|
||||
_buildCapsule('运动计划', Icons.fitness_center, ActiveAgent.exercise, selected, onTap),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCapsule(String label, IconData icon, ActiveAgent agent, ActiveAgent? selected, void Function(ActiveAgent) onTap) {
|
||||
final isSelected = selected == agent;
|
||||
return GestureDetector(
|
||||
onTap: () => onTap(agent),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF8B9CF7) : Colors.white,
|
||||
border: Border.all(color: const Color(0xFF8B9CF7)),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: isSelected ? Colors.white : const Color(0xFF8B9CF7)),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: TextStyle(fontSize: 13, color: isSelected ? Colors.white : const Color(0xFF8B9CF7))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import '../core/navigation_provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/data_providers.dart';
|
||||
import '../providers/chat_provider.dart';
|
||||
import 'service_package_card.dart';
|
||||
|
||||
/// 侧滑抽屉——彩色分区卡片式设计
|
||||
class HealthDrawer extends ConsumerWidget {
|
||||
@@ -169,6 +170,11 @@ class HealthDrawer extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ════════════ 产品服务包 ════════════
|
||||
const ServicePackageCard(),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ════════════ 历史对话区 ════════════
|
||||
_SectionCard(
|
||||
color: const Color(0xFFF0F4FF),
|
||||
|
||||
337
health_app/lib/widgets/service_package_card.dart
Normal file
337
health_app/lib/widgets/service_package_card.dart
Normal file
@@ -0,0 +1,337 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// 服务包数据模型
|
||||
class ServicePackage {
|
||||
final String id;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Color headerColor;
|
||||
final List<ServiceItem> services;
|
||||
final String targetAudience;
|
||||
final List<DetailSection> detailSections;
|
||||
|
||||
const ServicePackage({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.headerColor,
|
||||
required this.services,
|
||||
required this.targetAudience,
|
||||
required this.detailSections,
|
||||
});
|
||||
}
|
||||
|
||||
class ServiceItem {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
const ServiceItem({required this.icon, required this.label});
|
||||
}
|
||||
|
||||
class DetailSection {
|
||||
final String title;
|
||||
final String content;
|
||||
|
||||
const DetailSection({required this.title, required this.content});
|
||||
}
|
||||
|
||||
/// 预定义服务包数据 —— 基于项目实际功能
|
||||
final List<ServicePackage> servicePackages = [
|
||||
ServicePackage(
|
||||
id: 'vip_comprehensive',
|
||||
title: '心血管健康管理服务包',
|
||||
subtitle: 'VIP 产品权益',
|
||||
headerColor: const Color(0xFF4A90D9),
|
||||
services: [
|
||||
ServiceItem(icon: Icons.phone_in_talk_outlined, label: '电话咨询'),
|
||||
ServiceItem(icon: Icons.chat_bubble_outline, label: '在线咨询'),
|
||||
ServiceItem(icon: Icons.calendar_month_outlined, label: '个性化随访'),
|
||||
ServiceItem(icon: Icons.medication_outlined, label: '调药管理'),
|
||||
ServiceItem(icon: Icons.monitor_heart_outlined, label: '风险监测'),
|
||||
ServiceItem(icon: Icons.devices_outlined, label: '智能硬件'),
|
||||
ServiceItem(icon: Icons.family_restroom_outlined, label: '亲情账号'),
|
||||
ServiceItem(icon: Icons.verified_user_outlined, label: '健康保障'),
|
||||
],
|
||||
targetAudience:
|
||||
'心血管疾病患者(如高血压、冠心病、心律失常等)\n'
|
||||
'高危人群(如高血脂、糖尿病、肥胖、长期吸烟饮酒者)\n'
|
||||
'术后康复人群(如心脏支架/搭桥术后、PCI术后患者)',
|
||||
detailSections: [
|
||||
DetailSection(
|
||||
title: '1、个性化康复管理',
|
||||
content:
|
||||
'设定动态化的危险因素管理目标,根据患者的住院数据(如基础疾病、高危因素、合并症、当前用药等),确定高危因素(如高脂血症、高血糖等),设定高危因子(如低密度脂蛋白等)的管控目标。基于药物不良反应,制定个性化管理及复查方案。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '2、家庭医生专业团队',
|
||||
content:
|
||||
'由心内科主诊医生团队联合康复管理团队共同参与患者管理,将疾病康复从院内延伸至院外。团队由专科医生、康复治疗师、健康管理师、营养师等多学科成员组成。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '3、全年不限次数在线咨询',
|
||||
content:
|
||||
'为患者提供全年不限次数的咨询服务,支持微信(如文字、图文、语音等)、电话咨询等方式。如检查报告解读、用药咨询、病情咨询、饮食咨询、心理咨询等。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '4、主动跟踪随访管理',
|
||||
content:
|
||||
'动态临床评估:动态评估疾病复发、出血、致死致残等风险,实时优化管理方案。\n'
|
||||
'主动症状管理:全年主动跟踪患者临床症状,实现疾病恶化早发现、早处理。\n'
|
||||
'精准药物管理:全程用药指导,严密监控药物副作用,及时处理药物不良反应,确保药物治疗效果。\n'
|
||||
'生活方式干预:个性化指导患者合理膳食、运动康复、心理调节、戒烟限酒等。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '5、可穿戴智能设备',
|
||||
content:
|
||||
'配备可穿戴智能监测设备,患者居家测量后实时上传管理中心,医生和家属均可远程实时监测数据(血压和心率),记录数据变化趋势,智能预警,及时干预异常指标,降低不良事件发生率。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '6、亲情账号联动管理',
|
||||
content:
|
||||
'支持5名家属参与管理,实时多端同步患者病情,医生、患者、家属三方共享,患者安心,家属放心。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '7、健康档案与报告分析',
|
||||
content:
|
||||
'自动整合血压、心率、血糖、血氧、体重等健康数据,生成可视化健康趋势报告。\n'
|
||||
'AI 智能分析健康数据变化,提前预警潜在风险,提供个性化健康建议。',
|
||||
),
|
||||
],
|
||||
),
|
||||
ServicePackage(
|
||||
id: 'vip_premium',
|
||||
title: '心力衰竭专项管理服务包',
|
||||
subtitle: 'VIP 产品权益',
|
||||
headerColor: const Color(0xFF2BA87E),
|
||||
services: [
|
||||
ServiceItem(icon: Icons.phone_in_talk_outlined, label: '电话咨询'),
|
||||
ServiceItem(icon: Icons.chat_bubble_outline, label: '在线咨询'),
|
||||
ServiceItem(icon: Icons.calendar_month_outlined, label: '个性化随访'),
|
||||
ServiceItem(icon: Icons.medication_outlined, label: '调药管理'),
|
||||
ServiceItem(icon: Icons.monitor_heart_outlined, label: '风险监测'),
|
||||
ServiceItem(icon: Icons.devices_outlined, label: '智能硬件'),
|
||||
ServiceItem(icon: Icons.family_restroom_outlined, label: '亲情账号'),
|
||||
],
|
||||
targetAudience:
|
||||
'心力衰竭患者\n'
|
||||
'心力衰竭高危人群(心肌梗死、瓣膜病、心肌病、高血压、代谢综合征等)',
|
||||
detailSections: [
|
||||
DetailSection(
|
||||
title: '1、个性化康复管理',
|
||||
content:
|
||||
'设定动态化的危险因素管理目标,根据患者的住院数据(如基础疾病、高危因素、合并症、当前用药等),确定高危因素管控目标。基于药物不良反应,制定个性化管理及复查方案。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '2、家庭医生专业团队',
|
||||
content:
|
||||
'由心内科主诊医生团队联合哈瑞特医疗院外康复管理团队共同参与患者管理,将疾病康复从院内延伸至院外,哈瑞特医疗康复管理团队由专科医生、康复治疗师、健康管理师、营养师等多学科成员组成。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '3、全年不限次数在线咨询',
|
||||
content:
|
||||
'为患者提供全年不限次数的咨询服务,支持微信(如文字、图文、语音等)、电话咨询(400-1666199),如检查报告解读、用药咨询、病情咨询、饮食咨询、心理咨询等。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '4、主动跟踪随访管理',
|
||||
content:
|
||||
'动态临床评估:动态评估疾病复发、出血、致死致残等风险,实时优化管理方案。\n'
|
||||
'主动症状管理:全年主动跟踪患者临床症状,实现疾病恶化早发现、早处理。\n'
|
||||
'精准药物管理:全程用药指导,严密监控药物副作用,及时处理药物不良反应,确保药物治疗效果。\n'
|
||||
'生活方式干预:个性化指导患者合理膳食、运动康复、心理调节、戒烟限酒等。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '5、可穿戴智能设备',
|
||||
content:
|
||||
'配备可穿戴智能监测设备,患者居家测量后实时上传管理中心,医生和家属均可远程实时监测数据(血压和心率),记录数据变化趋势,智能预警,及时干预异常指标,降低不良事件发生率。',
|
||||
),
|
||||
DetailSection(
|
||||
title: '6、亲情账号联动管理',
|
||||
content:
|
||||
'支持5名家属参与管理,实时多端同步患者病情,医生、患者、家属三方共享,患者安心,家属放心。',
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
/// 服务包卡片 —— 展示在"我的"页面中
|
||||
class ServicePackageCard extends ConsumerWidget {
|
||||
const ServicePackageCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final package = servicePackages.first;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(8),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 16, 18, 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题行 + 详情入口
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
package.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.text,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'详情',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textSub,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 18,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// VIP 标签
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFFF5A623).withAlpha(200),
|
||||
const Color(0xFFE8930C),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'VIP',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
package.subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 服务图标网格
|
||||
Wrap(
|
||||
spacing: 0,
|
||||
runSpacing: 12,
|
||||
children: package.services.take(8).map((item) {
|
||||
return SizedBox(
|
||||
width: (MediaQuery.of(context).size.width - 48 - 36) / 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF8EE),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
item.icon,
|
||||
size: 20,
|
||||
color: const Color(0xFFF5A623),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppTheme.textSub,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 查看更多
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'查看更多服务包',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user