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:
MingNian
2026-07-19 19:11:30 +08:00
parent ae94ced2d5
commit 0d4fd88ce7
35 changed files with 1022 additions and 644 deletions

View File

@@ -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,
),
),
),
],
],
),
),
],
),

View File

@@ -78,6 +78,7 @@ class _DoctorFollowUpEditPageState
),
body: BackofficeSurface(
child: ListView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: const EdgeInsets.all(16),
children: [
const Text(

View File

@@ -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'),

View File

@@ -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' => '患者管理',

View File

@@ -132,6 +132,7 @@ class _DoctorReportDetailPageState
final fileUrl = data['fileUrl'] as String?;
return ListView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: const EdgeInsets.all(16),
children: [
// 患者+分类信息

View File

@@ -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() ?? ''},
),
),
);
},
),
);
},
);
},
),
),
],

View File

@@ -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');
},