feat: 三端抽屉重构 + 配色系统更新 + 后台管理页精调 + 键盘抬起组件
## 抽屉重构 - admin_drawer / doctor_drawer / health_drawer 三端抽屉全部重做 - drawer_shell 增强 ## 配色系统 - app_colors / app_module_visuals / app_theme 更新 - app.dart 启动流程调整 ## 后台管理页 - admin_doctors_page 大幅重构(+251) - admin_home / doctor_dashboard / doctor_reports / doctor_home / doctor_followups / doctor_settings 精调 ## 患者端 - home_page / chat_messages_view / medication_list / notification_center / remaining_pages / exercise_plan / device / diet / consultation 微调 ## 新增 - keyboard_lift.dart: 键盘抬起处理组件 - AGENTS.md: agent 指引文档 ## 其他 - api_client IP 适配 - app_future_view 增强 - data_providers 调整 - secondary_page_visuals_test 更新
This commit is contained in:
@@ -229,25 +229,30 @@ class _StatCard extends StatelessWidget {
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
Text(
|
||||
label,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -78,6 +78,7 @@ class _DoctorFollowUpEditPageState
|
||||
),
|
||||
body: BackofficeSurface(
|
||||
child: ListView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const Text(
|
||||
|
||||
@@ -62,22 +62,22 @@ class _DoctorFollowupsPageState extends ConsumerState<DoctorFollowupsPage> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
_Filt('全部', _filter == '', () => setState(() => _filter = '')),
|
||||
const SizedBox(width: 8),
|
||||
_Filt(
|
||||
'待完成',
|
||||
_filter == 'Upcoming',
|
||||
() => setState(() => _filter = 'Upcoming'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_Filt(
|
||||
'已完成',
|
||||
_filter == 'Completed',
|
||||
() => setState(() => _filter = 'Completed'),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: AppColors.primary),
|
||||
onPressed: () => pushRoute(ref, 'doctorFollowUpEdit'),
|
||||
|
||||
@@ -9,12 +9,41 @@ import 'doctor_consultations_page.dart';
|
||||
import 'doctor_reports_page.dart';
|
||||
import 'doctor_followups_page.dart';
|
||||
|
||||
class DoctorHomePage extends ConsumerWidget {
|
||||
class DoctorHomePage extends ConsumerStatefulWidget {
|
||||
const DoctorHomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<DoctorHomePage> createState() => _DoctorHomePageState();
|
||||
}
|
||||
|
||||
class _DoctorHomePageState extends ConsumerState<DoctorHomePage> {
|
||||
final List<Widget?> _pages = [
|
||||
const DoctorDashboardPage(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
];
|
||||
|
||||
Widget _createPage(int index) => switch (index) {
|
||||
1 => const DoctorPatientsPage(),
|
||||
2 => const DoctorConsultationsPage(),
|
||||
3 => const DoctorReportsPage(),
|
||||
4 => const DoctorFollowupsPage(),
|
||||
_ => const DoctorDashboardPage(),
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final page = ref.watch(doctorPageProvider);
|
||||
final pageIndex = switch (page) {
|
||||
'patients' => 1,
|
||||
'consultations' => 2,
|
||||
'reports' => 3,
|
||||
'followups' => 4,
|
||||
_ => 0,
|
||||
};
|
||||
_pages[pageIndex] ??= _createPage(pageIndex);
|
||||
|
||||
return PopScope(
|
||||
canPop: page == 'dashboard',
|
||||
@@ -39,20 +68,19 @@ class DoctorHomePage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
drawer: const DoctorDrawer(),
|
||||
body: BackofficeSurface(child: _bodyFor(page)),
|
||||
drawerEnableOpenDragGesture: true,
|
||||
body: BackofficeSurface(
|
||||
child: IndexedStack(
|
||||
index: pageIndex,
|
||||
children: _pages
|
||||
.map((page) => page ?? const SizedBox.shrink())
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bodyFor(String page) => switch (page) {
|
||||
'dashboard' => const DoctorDashboardPage(),
|
||||
'patients' => const DoctorPatientsPage(),
|
||||
'consultations' => const DoctorConsultationsPage(),
|
||||
'reports' => const DoctorReportsPage(),
|
||||
'followups' => const DoctorFollowupsPage(),
|
||||
_ => const DoctorDashboardPage(),
|
||||
};
|
||||
|
||||
String _titleFor(String page) => switch (page) {
|
||||
'dashboard' => '工作台',
|
||||
'patients' => '患者管理',
|
||||
|
||||
@@ -132,6 +132,7 @@ class _DoctorReportDetailPageState
|
||||
final fileUrl = data['fileUrl'] as String?;
|
||||
|
||||
return ListView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// 患者+分类信息
|
||||
|
||||
@@ -6,20 +6,13 @@ import '../../providers/auth_provider.dart';
|
||||
import '../../utils/backoffice_formatters.dart';
|
||||
import '../../widgets/backoffice_ui.dart';
|
||||
|
||||
final _reportsProvider =
|
||||
FutureProvider.family<List<Map<String, dynamic>>, String>((
|
||||
ref,
|
||||
status,
|
||||
) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final params = <String, dynamic>{};
|
||||
if (status.isNotEmpty && status != 'All') params['status'] = status;
|
||||
final res = await api.get(
|
||||
'/api/doctor/reports',
|
||||
queryParameters: params.isNotEmpty ? params : null,
|
||||
);
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
});
|
||||
final _reportsProvider = FutureProvider<List<Map<String, dynamic>>>((
|
||||
ref,
|
||||
) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/reports');
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
});
|
||||
|
||||
class DoctorReportsPage extends ConsumerStatefulWidget {
|
||||
const DoctorReportsPage({super.key});
|
||||
@@ -32,12 +25,14 @@ class _DoctorReportsPageState extends ConsumerState<DoctorReportsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reports = ref.watch(_reportsProvider(_filter));
|
||||
final reports = ref.watch(_reportsProvider);
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_FilterChip(
|
||||
'全部',
|
||||
@@ -45,14 +40,12 @@ class _DoctorReportsPageState extends ConsumerState<DoctorReportsPage> {
|
||||
_filter,
|
||||
() => setState(() => _filter = ''),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
'待审核',
|
||||
'PendingDoctor',
|
||||
_filter,
|
||||
() => setState(() => _filter = 'PendingDoctor'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
'已审核',
|
||||
'DoctorReviewed',
|
||||
@@ -66,57 +59,66 @@ class _DoctorReportsPageState extends ConsumerState<DoctorReportsPage> {
|
||||
child: reports.when(
|
||||
loading: () => const BackofficeLoadingState(message: '正在加载报告'),
|
||||
error: (_, _) => BackofficeErrorState(
|
||||
onRetry: () => ref.invalidate(_reportsProvider(_filter)),
|
||||
onRetry: () => ref.invalidate(_reportsProvider),
|
||||
),
|
||||
data: (items) => items.isEmpty
|
||||
? const BackofficeEmptyState(
|
||||
icon: Icons.description_outlined,
|
||||
title: '暂无报告',
|
||||
description: '患者上传的待审核报告会显示在这里',
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final r = items[i];
|
||||
return BackofficeSectionCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
data: (allItems) {
|
||||
final items = _filter.isEmpty
|
||||
? allItems
|
||||
: allItems
|
||||
.where((item) => item['status'] == _filter)
|
||||
.toList();
|
||||
return items.isEmpty
|
||||
? const BackofficeEmptyState(
|
||||
icon: Icons.description_outlined,
|
||||
title: '暂无报告',
|
||||
description: '患者上传的待审核报告会显示在这里',
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final r = items[i];
|
||||
return BackofficeSectionCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
color: AppColors.primary,
|
||||
title: Text(
|
||||
r['patientName'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${r['category'] ?? '未分类'} · '
|
||||
'${formatBackofficeDateTime(r['createdAt'])}',
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
onTap: () => pushRoute(
|
||||
ref,
|
||||
'doctorReportDetail',
|
||||
params: {'id': r['id']?.toString() ?? ''},
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
r['patientName'] ?? '',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${r['category'] ?? '未分类'} · '
|
||||
'${formatBackofficeDateTime(r['createdAt'])}',
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
onTap: () => pushRoute(
|
||||
ref,
|
||||
'doctorReportDetail',
|
||||
params: {'id': r['id']?.toString() ?? ''},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../../core/app_colors.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/backoffice_ui.dart';
|
||||
import '../../widgets/drawer_shell.dart';
|
||||
|
||||
class DoctorSettingsPage extends ConsumerWidget {
|
||||
const DoctorSettingsPage({super.key});
|
||||
@@ -56,6 +57,13 @@ class DoctorSettingsPage extends ConsumerWidget {
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
final confirmed = await confirmAccountAction(
|
||||
context,
|
||||
title: '退出登录',
|
||||
message: '确定退出当前医生账号吗?',
|
||||
confirmLabel: '退出登录',
|
||||
);
|
||||
if (!confirmed || !context.mounted) return;
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
goRoute(ref, 'login');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user