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:
@@ -12,99 +12,138 @@ class AdminDrawer extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentPage = ref.watch(adminPageProvider);
|
||||
final pages = ref.read(adminPageProvider.notifier);
|
||||
final routes = ref.read(routeStackProvider.notifier);
|
||||
final authNotifier = ref.read(authProvider.notifier);
|
||||
|
||||
return DrawerShell(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.88),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.86),
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
margin: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: AppColors.buttonShadow,
|
||||
color: Colors.white.withValues(alpha: 0.88),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.86),
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.admin_panel_settings,
|
||||
size: 28,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'系统管理员',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: AppColors.buttonShadow,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.admin_panel_settings,
|
||||
size: 28,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3),
|
||||
Text(
|
||||
'医生与患者管理',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'系统管理员',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3),
|
||||
Text(
|
||||
'医生与患者管理',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_MenuItem(
|
||||
icon: Icons.local_hospital,
|
||||
label: '医生管理',
|
||||
selected: currentPage == 'doctors',
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => pages.set('doctors'),
|
||||
),
|
||||
),
|
||||
_MenuItem(
|
||||
icon: Icons.people_outline,
|
||||
label: '患者列表',
|
||||
selected: currentPage == 'patients',
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => pages.set('patients'),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Divider(height: 1, color: AppColors.divider),
|
||||
const SizedBox(height: 8),
|
||||
_MenuItem(
|
||||
icon: Icons.delete_forever_outlined,
|
||||
label: '删除账号',
|
||||
danger: true,
|
||||
onTap: () async {
|
||||
final confirmed = await confirmAccountAction(
|
||||
context,
|
||||
title: '删除账号',
|
||||
message: '此操作会永久删除当前管理员账号及相关数据,删除后无法恢复。',
|
||||
confirmLabel: '确认删除',
|
||||
);
|
||||
if (!confirmed || !context.mounted) return;
|
||||
await runAfterDrawerClose(context, () async {
|
||||
await ref
|
||||
.read(apiClientProvider)
|
||||
.delete('/api/user/account');
|
||||
await authNotifier.logout();
|
||||
routes.replace('login');
|
||||
});
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: Icons.logout,
|
||||
label: '退出登录',
|
||||
danger: true,
|
||||
onTap: () async {
|
||||
final confirmed = await confirmAccountAction(
|
||||
context,
|
||||
title: '退出登录',
|
||||
message: '确定退出当前管理员账号吗?',
|
||||
confirmLabel: '退出登录',
|
||||
);
|
||||
if (!confirmed || !context.mounted) return;
|
||||
await runAfterDrawerClose(context, () async {
|
||||
await authNotifier.logout();
|
||||
routes.replace('login');
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_MenuItem(
|
||||
icon: Icons.local_hospital,
|
||||
label: '医生管理',
|
||||
selected: currentPage == 'doctors',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ref.read(adminPageProvider.notifier).set('doctors');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: Icons.people_outline,
|
||||
label: '患者列表',
|
||||
selected: currentPage == 'patients',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ref.read(adminPageProvider.notifier).set('patients');
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
const Divider(height: 1, color: AppColors.divider),
|
||||
const SizedBox(height: 8),
|
||||
_MenuItem(
|
||||
icon: Icons.logout,
|
||||
label: '退出登录',
|
||||
danger: true,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
goRoute(ref, 'login');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/app_colors.dart';
|
||||
import 'app_error_state.dart';
|
||||
|
||||
@@ -49,3 +50,35 @@ class AppFutureView<T> extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Riverpod 异步数据视图。Provider 会保留最近一次成功数据,刷新时不清空页面。
|
||||
class AppAsyncValueView<T> extends StatelessWidget {
|
||||
final AsyncValue<T> value;
|
||||
final Widget Function(BuildContext context, T data) onData;
|
||||
final VoidCallback? onRetry;
|
||||
final Widget? loading;
|
||||
final String? errorTitle;
|
||||
|
||||
const AppAsyncValueView({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onData,
|
||||
this.onRetry,
|
||||
this.loading,
|
||||
this.errorTitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => value.when(
|
||||
skipLoadingOnRefresh: true,
|
||||
skipLoadingOnReload: true,
|
||||
data: (data) => onData(context, data),
|
||||
error: (_, _) =>
|
||||
AppErrorState(title: errorTitle ?? '加载失败', onRetry: onRetry),
|
||||
loading: () =>
|
||||
loading ??
|
||||
const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ Future<void> showBleSyncDialog(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppModuleVisuals.device.gradient,
|
||||
color: AppModuleVisuals.device.color,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Icon(
|
||||
|
||||
@@ -14,90 +14,108 @@ class DoctorDrawer extends ConsumerWidget {
|
||||
final auth = ref.watch(authProvider);
|
||||
final currentPage = ref.watch(doctorPageProvider);
|
||||
final name = auth.user?.name ?? '医生';
|
||||
final pages = ref.read(doctorPageProvider.notifier);
|
||||
final routes = ref.read(routeStackProvider.notifier);
|
||||
final authNotifier = ref.read(authProvider.notifier);
|
||||
|
||||
return DrawerShell(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_DrawerHeader(
|
||||
name: name,
|
||||
subtitle: '点击完善医生信息',
|
||||
icon: Icons.medical_services_outlined,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
pushRoute(ref, 'doctorProfile');
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_DrawerHeader(
|
||||
name: name,
|
||||
subtitle: '点击完善医生信息',
|
||||
icon: Icons.medical_services_outlined,
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => routes.push('doctorProfile'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DrawerItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
label: '工作台',
|
||||
selected: currentPage == 'dashboard',
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => pages.set('dashboard'),
|
||||
),
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.people_outline,
|
||||
label: '患者管理',
|
||||
selected: currentPage == 'patients',
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => pages.set('patients'),
|
||||
),
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.chat_outlined,
|
||||
label: '问诊列表',
|
||||
selected: currentPage == 'consultations',
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => pages.set('consultations'),
|
||||
),
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.description_outlined,
|
||||
label: '报告审核',
|
||||
selected: currentPage == 'reports',
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => pages.set('reports'),
|
||||
),
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.event_note_outlined,
|
||||
label: '复查随访',
|
||||
selected: currentPage == 'followups',
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => pages.set('followups'),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Divider(height: 1, color: AppColors.divider),
|
||||
const SizedBox(height: 8),
|
||||
_DrawerItem(
|
||||
icon: Icons.settings_outlined,
|
||||
label: '设置',
|
||||
selected: false,
|
||||
onTap: () => runAfterDrawerClose(
|
||||
context,
|
||||
() => routes.push('doctorSettings'),
|
||||
),
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.logout,
|
||||
label: '退出登录',
|
||||
selected: false,
|
||||
danger: true,
|
||||
onTap: () async {
|
||||
final confirmed = await confirmAccountAction(
|
||||
context,
|
||||
title: '退出登录',
|
||||
message: '确定退出当前医生账号吗?',
|
||||
confirmLabel: '退出登录',
|
||||
);
|
||||
if (!confirmed || !context.mounted) return;
|
||||
await runAfterDrawerClose(context, () async {
|
||||
await authNotifier.logout();
|
||||
routes.replace('login');
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DrawerItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
label: '工作台',
|
||||
selected: currentPage == 'dashboard',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ref.read(doctorPageProvider.notifier).set('dashboard');
|
||||
},
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.people_outline,
|
||||
label: '患者管理',
|
||||
selected: currentPage == 'patients',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ref.read(doctorPageProvider.notifier).set('patients');
|
||||
},
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.chat_outlined,
|
||||
label: '问诊列表',
|
||||
selected: currentPage == 'consultations',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ref.read(doctorPageProvider.notifier).set('consultations');
|
||||
},
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.description_outlined,
|
||||
label: '报告审核',
|
||||
selected: currentPage == 'reports',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ref.read(doctorPageProvider.notifier).set('reports');
|
||||
},
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.event_note_outlined,
|
||||
label: '复查随访',
|
||||
selected: currentPage == 'followups',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
ref.read(doctorPageProvider.notifier).set('followups');
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
const Divider(height: 1, color: AppColors.divider),
|
||||
const SizedBox(height: 8),
|
||||
_DrawerItem(
|
||||
icon: Icons.settings_outlined,
|
||||
label: '设置',
|
||||
selected: false,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
pushRoute(ref, 'doctorSettings');
|
||||
},
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.logout,
|
||||
label: '退出登录',
|
||||
selected: false,
|
||||
danger: true,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
goRoute(ref, 'login');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> runAfterDrawerClose(
|
||||
BuildContext context,
|
||||
FutureOr<void> Function() action,
|
||||
) async {
|
||||
Navigator.of(context).pop();
|
||||
await action();
|
||||
}
|
||||
|
||||
Future<bool> confirmAccountAction(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
required String confirmLabel,
|
||||
}) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(
|
||||
confirmLabel,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
class DrawerShell extends StatelessWidget {
|
||||
final double widthFactor;
|
||||
final Widget child;
|
||||
|
||||
@@ -213,11 +213,7 @@ class _HealthDashboard extends StatelessWidget {
|
||||
onTap: () => pushRoute(ref, 'trend'),
|
||||
),
|
||||
titleColor: Colors.white,
|
||||
backgroundGradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF89F7FE), Color(0xFF66A6FF)],
|
||||
),
|
||||
backgroundPainter: const _HealthDashboardBackgroundPainter(),
|
||||
child: latestHealth.when(
|
||||
data: (data) => _DashboardMetrics(data: data, ref: ref),
|
||||
loading: () => const SizedBox(
|
||||
@@ -392,7 +388,7 @@ class _NavigationSection extends StatelessWidget {
|
||||
colors: AppColors.medicationGradient.colors,
|
||||
),
|
||||
_NavItem(
|
||||
icon: LucideIcons.utensils,
|
||||
icon: AppModuleVisuals.diet.icon,
|
||||
title: '饮食',
|
||||
route: 'dietRecords',
|
||||
colors: AppColors.dietGradient.colors,
|
||||
@@ -476,6 +472,7 @@ class _NavTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDevice = item.route == 'devices';
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
@@ -488,12 +485,16 @@ class _NavTile extends StatelessWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: _colors,
|
||||
),
|
||||
color: isDevice ? AppColors.device : null,
|
||||
gradient: isDevice
|
||||
? null
|
||||
: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: _colors,
|
||||
),
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
border: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _colors.last.withValues(alpha: 0.10),
|
||||
@@ -567,60 +568,96 @@ class _Panel extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
final Widget? trailing;
|
||||
final LinearGradient? backgroundGradient;
|
||||
final CustomPainter? backgroundPainter;
|
||||
final Color titleColor;
|
||||
const _Panel({
|
||||
required this.title,
|
||||
required this.child,
|
||||
this.trailing,
|
||||
this.backgroundGradient,
|
||||
this.backgroundPainter,
|
||||
this.titleColor = AppColors.textPrimary,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
backgroundGradient ??
|
||||
LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0.92),
|
||||
const Color(0xFFF8F4FF).withValues(alpha: 0.84),
|
||||
],
|
||||
),
|
||||
color: backgroundPainter == null ? null : const Color(0xFFBAE6FD),
|
||||
gradient: backgroundPainter != null
|
||||
? null
|
||||
: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0.92),
|
||||
const Color(0xFFF8F4FF).withValues(alpha: 0.84),
|
||||
],
|
||||
),
|
||||
borderRadius: AppRadius.xlBorder,
|
||||
boxShadow: AppShadows.soft,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
child: CustomPaint(
|
||||
painter: backgroundPainter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: titleColor,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: titleColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
?trailing,
|
||||
],
|
||||
),
|
||||
?trailing,
|
||||
const SizedBox(height: 12),
|
||||
child,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HealthDashboardBackgroundPainter extends CustomPainter {
|
||||
const _HealthDashboardBackgroundPainter();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
canvas.drawRect(rect, Paint()..color = const Color(0xFFBAE6FD));
|
||||
|
||||
void drawGlow(Alignment center, Color color) {
|
||||
final paint = Paint()
|
||||
..shader = RadialGradient(
|
||||
center: center,
|
||||
radius: 1,
|
||||
colors: [color, color.withValues(alpha: 0)],
|
||||
stops: const [0, 0.8],
|
||||
).createShader(rect);
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
|
||||
drawGlow(Alignment.bottomRight, const Color(0xFF818CF8));
|
||||
drawGlow(const Alignment(0, 0.6), const Color(0xFF60A5FA));
|
||||
drawGlow(const Alignment(0.6, -0.6), const Color(0xFF6366F1));
|
||||
drawGlow(const Alignment(-0.6, -0.6), const Color(0xFF38BDF8));
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_HealthDashboardBackgroundPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _TextAction extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
17
health_app/lib/widgets/keyboard_lift.dart
Normal file
17
health_app/lib/widgets/keyboard_lift.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 只让输入区响应键盘高度,避免整棵页面随 MediaQuery 反复重建。
|
||||
class KeyboardLift extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const KeyboardLift({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: bottom),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user