Files
AI-Health/health_app/lib/pages/report/report_pages.dart
MingNian 8dcf99cac5 style: 全项目紫色→薄荷绿 Fresh Air 清新风
- 主色 #635BFF→#14B8A6 (薄荷绿)
- 浅紫 #EDEBFF→#E6FAF6 (极浅薄荷)
- 深紫 #4B44D6→#0F9D8E (深薄荷)
- 渐变紫→薄荷渐变
- 全局13种紫色映射替换
2026-06-03 20:30:28 +08:00

535 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
final reportProvider = NotifierProvider<ReportNotifier, ReportState>(ReportNotifier.new);
class ReportState {
final List<ReportItem> reports;
final String? uploadingImage;
final bool isAnalyzing;
final ReportAnalysis? currentAnalysis;
ReportState({
this.reports = const [],
this.uploadingImage,
this.isAnalyzing = false,
this.currentAnalysis,
});
ReportState copyWith({
List<ReportItem>? reports,
String? uploadingImage,
bool? isAnalyzing,
ReportAnalysis? currentAnalysis,
}) {
return ReportState(
reports: reports ?? this.reports,
uploadingImage: uploadingImage ?? this.uploadingImage,
isAnalyzing: isAnalyzing ?? this.isAnalyzing,
currentAnalysis: currentAnalysis ?? this.currentAnalysis,
);
}
}
class ReportItem {
final String id;
final String title;
final String type;
final DateTime uploadedAt;
final String? imagePath;
final bool hasAnalysis;
ReportItem({
required this.id,
required this.title,
required this.type,
required this.uploadedAt,
this.imagePath,
this.hasAnalysis = false,
});
}
class ReportAnalysis {
final String reportId;
final String reportType;
final List<Indicator> indicators;
final String summary;
ReportAnalysis({
required this.reportId,
required this.reportType,
required this.indicators,
required this.summary,
});
}
class Indicator {
final String name;
final String value;
final String unit;
final String status;
final String? referenceRange;
Indicator({
required this.name,
required this.value,
required this.unit,
required this.status,
this.referenceRange,
});
}
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);
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,
);
}
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,
);
}
void viewAnalysis(String reportId) {
state = state.copyWith(currentAnalysis: _mockAnalysis);
}
void clearAnalysis() {
state = state.copyWith(currentAnalysis: null);
}
}
final _mockAnalysis = ReportAnalysis(
reportId: '1',
reportType: '血常规检查',
indicators: [
Indicator(name: '白细胞计数', value: '7.5', unit: '×10^9/L', status: 'normal', referenceRange: '4.0-10.0'),
Indicator(name: '红细胞计数', value: '4.2', unit: '×10^12/L', status: 'normal', referenceRange: '3.5-5.5'),
Indicator(name: '血红蛋白', value: '128', unit: 'g/L', status: 'low', referenceRange: '130-175'),
Indicator(name: '血小板计数', value: '185', unit: '×10^9/L', status: 'normal', referenceRange: '100-300'),
Indicator(name: '中性粒细胞百分比', value: '65', unit: '%', status: 'normal', referenceRange: '50-70'),
Indicator(name: '淋巴细胞百分比', value: '28', unit: '%', status: 'normal', referenceRange: '20-40'),
],
summary: '整体来看,您的血常规检查基本正常。血红蛋白略低于正常范围,建议适当补充营养,多吃富含铁质的食物如红肉、动物肝脏等。如有疲劳、头晕等症状,建议咨询医生进一步检查。',
);
/// 报告列表页
class ReportListPage extends ConsumerWidget {
const ReportListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(reportProvider);
if (state.isAnalyzing) {
return Scaffold(
appBar: AppBar(title: const Text('看报告')),
body: const Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
CircularProgressIndicator(color: Color(0xFF14B8A6)),
SizedBox(height: 16),
Text('AI 正在分析报告...'),
]),
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('看报告'),
centerTitle: true,
),
floatingActionButton: _buildUploadButton(context, ref),
body: state.reports.isEmpty
? _buildEmptyState(context)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: state.reports.length,
itemBuilder: (context, index) => _buildReportCard(context, ref, state.reports[index]),
),
);
}
Widget _buildUploadButton(BuildContext context, WidgetRef ref) {
return FloatingActionButton(
onPressed: () => _showUploadOptions(context, ref),
backgroundColor: const Color(0xFF14B8A6),
child: const Icon(Icons.add),
);
}
void _showUploadOptions(BuildContext context, WidgetRef ref) {
showModalBottomSheet(
context: context,
builder: (ctx) => SafeArea(
child: Wrap(children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('拍照上传'),
onTap: () async {
Navigator.pop(ctx);
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.camera, imageQuality: 85);
if (picked != null) {
ref.read(reportProvider.notifier).uploadImage(picked.path);
}
},
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('从相册选择'),
onTap: () async {
Navigator.pop(ctx);
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
if (picked != null) {
ref.read(reportProvider.notifier).uploadImage(picked.path);
}
},
),
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('上传PDF文件'),
onTap: () async {
Navigator.pop(ctx);
final result = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['pdf']);
if (result != null && result.files.isNotEmpty) {
ref.read(reportProvider.notifier).uploadFile(result.files.first.path!);
}
},
),
]),
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: const Color(0xFFF2FAF9),
borderRadius: BorderRadius.circular(60),
),
child: const Icon(Icons.file_open, size: 48, color: Color(0xFF14B8A6)),
),
const SizedBox(height: 20),
const Text('暂无检查报告', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
const Text('点击下方按钮上传报告', style: TextStyle(fontSize: 14, color: Color(0xFF999999))),
]),
);
}
Widget _buildReportCard(BuildContext context, WidgetRef ref, ReportItem report) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
),
child: ListTile(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF2FAF9),
borderRadius: BorderRadius.circular(12),
),
child: _getReportIcon(report.type),
),
title: Text(report.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(report.type, style: TextStyle(fontSize: 14, color: Colors.grey[500])),
Text(_formatDate(report.uploadedAt), style: TextStyle(fontSize: 12, color: Colors.grey[400])),
]),
trailing: report.hasAnalysis
? const Icon(Icons.check_circle, size: 20, color: Color(0xFF43A047))
: const Icon(Icons.arrow_forward_ios, size: 18, color: Color(0xFFCCCCCC)),
onTap: () {
ref.read(reportProvider.notifier).viewAnalysis(report.id);
pushRoute(ref, 'reportDetail', params: {'id': report.id});
},
),
);
}
Widget _getReportIcon(String type) {
final icons = {
'血液检查': const Icon(Icons.bloodtype, size: 24, color: Color(0xFF14B8A6)),
'心电图': const Icon(Icons.monitor_heart, size: 24, color: Color(0xFF14B8A6)),
'超声检查': const Icon(Icons.image, size: 24, color: Color(0xFF14B8A6)),
'影像报告': const Icon(Icons.image, size: 24, color: Color(0xFF14B8A6)),
'PDF文档': const Icon(Icons.picture_as_pdf, size: 24, color: Color(0xFF14B8A6)),
};
return icons[type] ?? const Icon(Icons.description, size: 24, color: Color(0xFF14B8A6));
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final diff = now.difference(date);
if (diff.inDays == 0) return '今天';
if (diff.inDays == 1) return '昨天';
if (diff.inDays < 7) return '${diff.inDays}天前';
return '${date.month}${date.day}';
}
}
/// 报告详情页
class ReportDetailPage extends ConsumerWidget {
final String id;
const ReportDetailPage({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final analysis = ref.watch(reportProvider.select((s) => s.currentAnalysis));
if (analysis == null) {
return Scaffold(
appBar: AppBar(title: const Text('报告详情')),
body: const Center(child: Text('暂无分析数据')),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('报告解读'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
ref.read(reportProvider.notifier).clearAnalysis();
popRoute(ref);
},
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_buildReportHeader(analysis),
const SizedBox(height: 20),
_buildAnalysisSection(analysis),
const SizedBox(height: 20),
_buildSummarySection(analysis),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('图片加载中...'), duration: Duration(seconds: 2)),
);
},
icon: const Icon(Icons.image),
label: const Text('查看原始图片'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF14B8A6),
side: const BorderSide(color: Color(0xFF14B8A6)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () => pushRoute(ref, 'aiAnalysis'),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF14B8A6), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))),
child: const Text('查看 AI 智能解读'),
),
),
const SizedBox(height: 30),
]),
),
);
}
Widget _buildReportHeader(ReportAnalysis analysis) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF2FAF9),
borderRadius: BorderRadius.circular(16),
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
const Text('📋', style: TextStyle(fontSize: 24)),
const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(analysis.reportType, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
const Text('AI 预解读结果', style: TextStyle(fontSize: 14, color: Color(0xFF666666))),
]),
]),
]),
);
}
Widget _buildAnalysisSection(ReportAnalysis analysis) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFD4EDE8), width: 1.5),
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(children: [
const Text('🧪', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
const Text('指标分析', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
]),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFFFF3E0),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFE0B2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.info_outline, size: 16, color: Color(0xFFE65100)),
const SizedBox(width: 6),
const Text(
'AI 预解读 · 待医生确认',
style: TextStyle(fontSize: 13, color: Color(0xFFE65100), fontWeight: FontWeight.w500),
),
],
),
),
const SizedBox(height: 12),
...analysis.indicators.map((ind) => _buildIndicatorRow(ind)),
]),
);
}
Widget _buildIndicatorRow(Indicator ind) {
Color statusColor;
IconData statusIcon;
switch (ind.status) {
case 'high':
statusColor = const Color(0xFFE53935);
statusIcon = Icons.arrow_upward;
break;
case 'low':
statusColor = const Color(0xFFF9A825);
statusIcon = Icons.arrow_downward;
break;
default:
statusColor = const Color(0xFF43A047);
statusIcon = Icons.check_circle;
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFF0F0F0)))),
child: Row(children: [
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(ind.name, style: const TextStyle(fontSize: 14)),
if (ind.referenceRange != null)
Text('参考值: ${ind.referenceRange}', style: TextStyle(fontSize: 12, color: Colors.grey[400])),
]),
),
const SizedBox(width: 16),
Column(children: [
Text('${ind.value} ${ind.unit}', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: statusColor)),
Icon(statusIcon, size: 16, color: statusColor),
]),
]),
);
}
Widget _buildSummarySection(ReportAnalysis analysis) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFEF3C7),
borderRadius: BorderRadius.circular(16),
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
const Text('💡', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
const Text('综合解读', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFFD97706))),
]),
const SizedBox(height: 12),
Text(analysis.summary, style: const TextStyle(fontSize: 14, color: Color(0xFF92400E), height: 1.6)),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Text('⚠️ AI 解读仅供参考,请以医生诊断为准', style: TextStyle(fontSize: 13, color: Color(0xFFD97706))),
),
]),
);
}
}