- 后端新增 GET /api/medications/reminders 接口 - 前端任务卡片区显示真实用药提醒 - 移除 DoctorListPage/DoctorChatPage 路由 - 移除"找医生"面板按钮 - 医生端另做 Web 页面
535 lines
18 KiB
Dart
535 lines
18 KiB
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';
|
||
|
||
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(0xFF635BFF)),
|
||
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(0xFF635BFF),
|
||
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(0xFFF5F3FF),
|
||
borderRadius: BorderRadius.circular(60),
|
||
),
|
||
child: const Icon(Icons.file_open, size: 48, color: Color(0xFF635BFF)),
|
||
),
|
||
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(0xFF635BFF).withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
|
||
),
|
||
child: ListTile(
|
||
leading: Container(
|
||
width: 48,
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF5F3FF),
|
||
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(0xFF635BFF)),
|
||
'心电图': const Icon(Icons.monitor_heart, size: 24, color: Color(0xFF635BFF)),
|
||
'超声检查': const Icon(Icons.image, size: 24, color: Color(0xFF635BFF)),
|
||
'影像报告': const Icon(Icons.image, size: 24, color: Color(0xFF635BFF)),
|
||
'PDF文档': const Icon(Icons.picture_as_pdf, size: 24, color: Color(0xFF635BFF)),
|
||
};
|
||
return icons[type] ?? const Icon(Icons.description, size: 24, color: Color(0xFF635BFF));
|
||
}
|
||
|
||
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(0xFF635BFF),
|
||
side: const BorderSide(color: Color(0xFF635BFF)),
|
||
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(0xFF635BFF), 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(0xFFF5F3FF),
|
||
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(0xFFE8E6FF), 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))),
|
||
),
|
||
]),
|
||
);
|
||
}
|
||
} |