fix: 管理员系统 + 注册流程重构 + UI改版 + 全链路修复
- 新增管理员角色(手机号12345678910),管理员可增删医生、查看患者 - 注册页重构:去掉角色选择+审核码,改为选医生+填姓名 - 医生端点按DoctorId过滤患者,Patient↔Doctor关系建立 - Doctor/DoctorProfile/User实体新增关联字段 - JSON循环引用修复(IgnoreCycles) - /api/doctors改为公开接口 - 登录闪屏修复+原生Android启动页 - 输入框全局白底+灰框 - 蓝牙重连同步修复 - Web端(doctor_web+health_app/web)全部删除 - 全局UI改版:白底企业风,新配色和组件系统 - 新品牌图标和启动图
This commit is contained in:
@@ -5,13 +5,14 @@ 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 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
final reportProvider = NotifierProvider<ReportNotifier, ReportState>(ReportNotifier.new);
|
||||
final reportProvider = NotifierProvider<ReportNotifier, ReportState>(
|
||||
ReportNotifier.new,
|
||||
);
|
||||
|
||||
class ReportState {
|
||||
final List<ReportItem> reports;
|
||||
@@ -48,8 +49,8 @@ class ReportItem {
|
||||
final DateTime uploadedAt;
|
||||
final String? imagePath;
|
||||
final bool hasAnalysis;
|
||||
final String status; // PendingDoctor | DoctorReviewed
|
||||
final String? severity; // Normal | Abnormal | Severe | Critical
|
||||
final String status; // PendingDoctor | DoctorReviewed
|
||||
final String? severity; // Normal | Abnormal | Severe | Critical
|
||||
final String? doctorComment;
|
||||
final String? doctorRecommendation;
|
||||
final String? doctorName;
|
||||
@@ -117,14 +118,17 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
final m = r as Map<String, dynamic>;
|
||||
final rawTitle = m['title']?.toString() ?? '';
|
||||
final rawCat = m['category']?.toString() ?? '';
|
||||
final title = rawTitle.isNotEmpty && rawTitle != 'Other' && rawTitle != 'other'
|
||||
final title =
|
||||
rawTitle.isNotEmpty && rawTitle != 'Other' && rawTitle != 'other'
|
||||
? rawTitle
|
||||
: _catTitle(rawCat);
|
||||
return ReportItem(
|
||||
id: m['id']?.toString() ?? '',
|
||||
title: title,
|
||||
type: m['fileType']?.toString() ?? 'Image',
|
||||
uploadedAt: DateTime.tryParse(m['createdAt']?.toString() ?? '') ?? DateTime.now(),
|
||||
uploadedAt:
|
||||
DateTime.tryParse(m['createdAt']?.toString() ?? '') ??
|
||||
DateTime.now(),
|
||||
hasAnalysis: m['aiSummary'] != null,
|
||||
status: m['status']?.toString() ?? 'PendingDoctor',
|
||||
severity: m['severity']?.toString(),
|
||||
@@ -199,12 +203,17 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final file = File(path);
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(file.path, filename: file.path.split('/').last),
|
||||
'file': await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: file.path.split('/').last,
|
||||
),
|
||||
});
|
||||
// 直接上传到报告端点,后端自动触发 VLM+LLM 分析
|
||||
final createRes = await api.dio.post('/api/reports', data: formData);
|
||||
final data = createRes.data;
|
||||
final reportId = data is Map ? (data['data']?['id']?.toString() ?? '') : '';
|
||||
final reportId = data is Map
|
||||
? (data['data']?['id']?.toString() ?? '')
|
||||
: '';
|
||||
|
||||
state = state.copyWith(isAnalyzing: false, uploadingImage: null);
|
||||
loadReports();
|
||||
@@ -245,32 +254,42 @@ class ReportListPage extends ConsumerWidget {
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(title: const Text('报告管理')),
|
||||
body: const Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
CircularProgressIndicator(color: AppColors.primary),
|
||||
SizedBox(height: 16),
|
||||
Text('AI 正在分析报告...', style: TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||||
]),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(color: AppColors.primary),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'AI 正在分析报告...',
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
title: const Text('报告管理'),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: _buildUploadButton(context, ref),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async => ref.read(reportProvider.notifier).loadReports(),
|
||||
child: state.reports.isEmpty
|
||||
? ListView(children: [_buildEmptyState(context)])
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.reports.length,
|
||||
itemBuilder: (context, index) => _buildReportCard(context, ref, state.reports[index]),
|
||||
),
|
||||
),
|
||||
onRefresh: () async => ref.read(reportProvider.notifier).loadReports(),
|
||||
child: state.reports.isEmpty
|
||||
? ListView(children: [_buildEmptyState(context)])
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.reports.length,
|
||||
itemBuilder: (context, index) =>
|
||||
_buildReportCard(context, ref, state.reports[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,41 +306,68 @@ class ReportListPage extends ConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Wrap(children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt_outlined, color: AppColors.primary),
|
||||
title: const Text('拍照上传', style: TextStyle(fontSize: 17)),
|
||||
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_outlined, color: AppColors.primary),
|
||||
title: const Text('从相册选择', style: TextStyle(fontSize: 17)),
|
||||
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.picture_as_pdf_outlined, color: AppColors.primary),
|
||||
title: const Text('上传PDF文件', style: TextStyle(fontSize: 17)),
|
||||
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!);
|
||||
},
|
||||
),
|
||||
]),
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
title: const Text('拍照上传', style: TextStyle(fontSize: 17)),
|
||||
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_outlined,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
title: const Text('从相册选择', style: TextStyle(fontSize: 17)),
|
||||
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.picture_as_pdf_outlined,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
title: const Text('上传PDF文件', style: TextStyle(fontSize: 17)),
|
||||
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!);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -329,27 +375,55 @@ class ReportListPage extends ConsumerWidget {
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
width: 100, height: 100,
|
||||
decoration: BoxDecoration(color: AppColors.cardInner, borderRadius: BorderRadius.circular(50)),
|
||||
child: const Icon(Icons.description_outlined, size: 44, color: AppColors.textHint),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text('暂无检查报告', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, color: AppColors.textPrimary)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('点击右下角按钮上传报告', style: TextStyle(fontSize: 16, color: AppColors.textHint)),
|
||||
]),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardInner,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
size: 44,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'暂无检查报告',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'点击右下角按钮上传报告',
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReportCard(BuildContext context, WidgetRef ref, ReportItem report) {
|
||||
final displayTitle = (report.title == 'Other' || report.title == 'other') ? '检查报告' : report.title;
|
||||
Widget _buildReportCard(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ReportItem report,
|
||||
) {
|
||||
final displayTitle = (report.title == 'Other' || report.title == 'other')
|
||||
? '检查报告'
|
||||
: report.title;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Material(
|
||||
@@ -360,47 +434,117 @@ class ReportListPage extends ConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 48, height: 48,
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight)),
|
||||
child: Icon(Icons.description_outlined, size: 26, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(displayTitle, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
const SizedBox(height: 4),
|
||||
Text(_formatDate(report.uploadedAt), style: const TextStyle(fontSize: 14, color: AppColors.textHint)),
|
||||
]),
|
||||
),
|
||||
if (report.status == 'DoctorReviewed')
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(color: AppColors.successLight, borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text('已审核', style: TextStyle(fontSize: 13, color: AppColors.success, fontWeight: FontWeight.w500)),
|
||||
)
|
||||
else if (!report.hasAnalysis)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(color: AppColors.cardInner, borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text('分析中', style: TextStyle(fontSize: 13, color: AppColors.textHint, fontWeight: FontWeight.w500)),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(color: AppColors.warningLight, borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text('待审核', style: TextStyle(fontSize: 13, color: AppColors.warning, fontWeight: FontWeight.w500)),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
size: 26,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _confirmDelete(context, ref, report.id),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(Icons.delete_outline, size: 20, color: AppColors.textHint),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatDate(report.uploadedAt),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
if (report.status == 'DoctorReviewed')
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.successLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'已审核',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.success,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (!report.hasAnalysis)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardInner,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'分析中',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textHint,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warningLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'待审核',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.warning,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _confirmDelete(context, ref, report.id),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
size: 20,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -414,9 +558,15 @@ class ReportListPage extends ConsumerWidget {
|
||||
title: const Text('删除报告'),
|
||||
content: const Text('确定要删除这份报告吗?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
|
||||
TextButton(
|
||||
onPressed: () { Navigator.pop(ctx); ref.read(reportProvider.notifier).deleteReport(id); },
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
ref.read(reportProvider.notifier).deleteReport(id);
|
||||
},
|
||||
child: const Text('删除', style: TextStyle(color: AppColors.error)),
|
||||
),
|
||||
],
|
||||
@@ -443,4 +593,3 @@ String _catTitle(String c) => switch (c) {
|
||||
'Image' => '影像检查报告',
|
||||
_ => '检查报告',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user