Polish health app UI

This commit is contained in:
MingNian
2026-06-26 14:37:17 +08:00
parent 86c7957144
commit df37076c46
46 changed files with 717 additions and 514 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 KiB

View File

@@ -0,0 +1 @@

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -30,4 +30,16 @@ class BpReading {
'unit': 'mmHg', 'unit': 'mmHg',
'recordedAt': timestamp.toUtc().toIso8601String(), 'recordedAt': timestamp.toUtc().toIso8601String(),
}; };
Map<String, dynamic>? toHeartRateRecord() {
final value = pulse;
if (value == null || value <= 0) return null;
return {
'type': 'HeartRate',
'value': value,
'source': 'DeviceSync',
'unit': 'bpm',
'recordedAt': timestamp.toUtc().toIso8601String(),
};
}
} }

View File

@@ -12,8 +12,7 @@ class LoginPage extends ConsumerStatefulWidget {
} }
class _LoginPageState extends ConsumerState<LoginPage> { class _LoginPageState extends ConsumerState<LoginPage> {
static const _headerBg = 'assets/branding/ui_abstract_header_bg_v1.png'; static const _loginBg = 'assets/branding/login_background_v1.png';
static const _cornerAccent = 'assets/branding/ui_corner_accent_v1.png';
final _phoneCtrl = TextEditingController(); final _phoneCtrl = TextEditingController();
final _codeCtrl = TextEditingController(); final _codeCtrl = TextEditingController();
@@ -66,8 +65,12 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Future<void> _submit() async { Future<void> _submit() async {
if (!_agreed) { if (!_agreed) {
setState(() => _error = '请阅读并同意服务协议和隐私政策'); final accepted = await _showAgreementDialog();
return; if (!mounted || accepted != true) return;
setState(() {
_agreed = true;
_error = null;
});
} }
if (_phoneCtrl.text.trim().isEmpty || _codeCtrl.text.trim().isEmpty) { if (_phoneCtrl.text.trim().isEmpty || _codeCtrl.text.trim().isEmpty) {
setState(() => _error = '请输入手机号和验证码'); setState(() => _error = '请输入手机号和验证码');
@@ -132,6 +135,124 @@ class _LoginPageState extends ConsumerState<LoginPage> {
} }
} }
Future<bool?> _showAgreementDialog() {
return showDialog<bool>(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withValues(alpha: 0.54),
builder: (ctx) => Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 28),
backgroundColor: Colors.transparent,
child: Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 380),
padding: const EdgeInsets.fromLTRB(22, 28, 22, 22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.14),
blurRadius: 34,
offset: const Offset(0, 18),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'服务协议及隐私保护',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 21,
fontWeight: FontWeight.w900,
color: AppColors.textPrimary,
letterSpacing: 0,
),
),
const SizedBox(height: 18),
RichText(
textAlign: TextAlign.center,
text: const TextSpan(
style: TextStyle(
fontSize: 15,
height: 1.55,
fontWeight: FontWeight.w700,
color: AppColors.textSecondary,
),
children: [
TextSpan(text: '请阅读并同意'),
TextSpan(
text: '《服务协议》',
style: TextStyle(color: AppColors.primary),
),
TextSpan(text: ''),
TextSpan(
text: '《隐私政策》',
style: TextStyle(color: AppColors.primary),
),
],
),
),
const SizedBox(height: 26),
Row(
children: [
Expanded(
child: SizedBox(
height: 50,
child: OutlinedButton(
onPressed: () => Navigator.pop(ctx, false),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: BorderSide(
color: AppColors.primary.withValues(alpha: 0.62),
width: 1.2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
child: const Text('不同意'),
),
),
),
const SizedBox(width: 12),
Expanded(
child: GestureDetector(
onTap: () => Navigator.pop(ctx, true),
child: Container(
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: AppColors.doctorGradient,
borderRadius: BorderRadius.circular(18),
boxShadow: AppColors.buttonShadow,
),
child: const Text(
'同意',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
),
),
),
],
),
],
),
),
),
);
}
void _goHome() { void _goHome() {
ref.invalidate(latestHealthProvider); ref.invalidate(latestHealthProvider);
ref.invalidate(medicationListProvider); ref.invalidate(medicationListProvider);
@@ -250,34 +371,26 @@ class _LoginPageState extends ConsumerState<LoginPage> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: Stack( body: Stack(
children: [ children: [
Positioned.fill(
child: Image.asset(
_loginBg,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
Positioned.fill( Positioned.fill(
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration(gradient: AppColors.bgGradient), decoration: BoxDecoration(
), gradient: LinearGradient(
), begin: Alignment.topCenter,
Positioned( end: Alignment.bottomCenter,
top: 0, colors: [
left: 0, Colors.white.withValues(alpha: 0.10),
right: 0, Colors.white.withValues(alpha: 0.28),
height: 260, Colors.white.withValues(alpha: 0.54),
child: Opacity( ],
opacity: 0.92, stops: const [0.0, 0.52, 1.0],
child: Image.asset( ),
_headerBg,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
),
Positioned(
top: 18,
right: -44,
width: 180,
height: 180,
child: IgnorePointer(
child: Opacity(
opacity: 0.86,
child: Image.asset(_cornerAccent, fit: BoxFit.contain),
), ),
), ),
), ),
@@ -285,10 +398,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 22, 20, 24), padding: const EdgeInsets.fromLTRB(20, 28, 20, 24),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: constraints.maxHeight - 46, minHeight: (constraints.maxHeight - 52).clamp(
0.0,
double.infinity,
),
), ),
child: Center( child: Center(
child: ConstrainedBox( child: ConstrainedBox(
@@ -297,8 +413,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_LoginHero(isLogin: _isLogin), const _LoginHero(),
const SizedBox(height: 22), const SizedBox(height: 28),
_LoginFormCard( _LoginFormCard(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -328,7 +444,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
'手机号', '手机号',
TextInputType.phone, TextInputType.phone,
11, 11,
icon: Icons.phone_iphone_rounded, icon: Icons.local_phone_rounded,
gradientIcon: true,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
@@ -339,7 +456,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
'验证码', '验证码',
TextInputType.number, TextInputType.number,
6, 6,
icon: Icons.verified_outlined, icon: Icons.sms_rounded,
gradientIcon: true,
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -407,9 +525,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
} }
class _LoginHero extends StatelessWidget { class _LoginHero extends StatelessWidget {
final bool isLogin; const _LoginHero();
const _LoginHero({required this.isLogin});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -437,23 +553,6 @@ class _LoginHero extends StatelessWidget {
color: AppColors.textSecondary, color: AppColors.textSecondary,
), ),
), ),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.76),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: AppColors.borderLight),
),
child: Text(
isLogin ? '登录你的健康账户' : '创建新的健康账户',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
),
], ],
); );
} }
@@ -488,18 +587,15 @@ class _LoginFormCard extends StatelessWidget {
class _BrandMark extends StatelessWidget { class _BrandMark extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return SizedBox(
width: 72, width: 132,
height: 72, height: 132,
decoration: BoxDecoration( child: Transform.scale(
gradient: AppColors.primaryGradient, scale: 1.08,
borderRadius: BorderRadius.circular(24), child: Image.asset(
boxShadow: AppColors.buttonShadow, 'assets/branding/health_icon_master.png',
), fit: BoxFit.cover,
child: const Icon( ),
Icons.monitor_heart_outlined,
color: Colors.white,
size: 34,
), ),
); );
} }
@@ -511,8 +607,16 @@ class _TextField extends StatelessWidget {
final TextInputType type; final TextInputType type;
final int maxLen; final int maxLen;
final IconData? icon; final IconData? icon;
final bool gradientIcon;
const _TextField(this.ctrl, this.hint, this.type, this.maxLen, {this.icon}); const _TextField(
this.ctrl,
this.hint,
this.type,
this.maxLen, {
this.icon,
this.gradientIcon = false,
});
@override @override
Widget build(BuildContext context) => TextField( Widget build(BuildContext context) => TextField(
@@ -529,6 +633,8 @@ class _TextField extends StatelessWidget {
counterText: '', counterText: '',
prefixIcon: icon == null prefixIcon: icon == null
? null ? null
: gradientIcon
? _GradientPrefixIcon(icon: icon!)
: Icon(icon, size: 20, color: AppColors.textSecondary), : Icon(icon, size: 20, color: AppColors.textSecondary),
filled: true, filled: true,
fillColor: const Color(0xFFF8FAFC), fillColor: const Color(0xFFF8FAFC),
@@ -545,6 +651,20 @@ class _TextField extends StatelessWidget {
); );
} }
class _GradientPrefixIcon extends StatelessWidget {
final IconData icon;
const _GradientPrefixIcon({required this.icon});
@override
Widget build(BuildContext context) {
return ShaderMask(
shaderCallback: AppColors.doctorGradient.createShader,
blendMode: BlendMode.srcIn,
child: Icon(icon, size: 23, color: Colors.white),
);
}
}
class _DoctorSelector extends StatelessWidget { class _DoctorSelector extends StatelessWidget {
final String? name; final String? name;
final VoidCallback onTap; final VoidCallback onTap;
@@ -762,7 +882,7 @@ class _PrimaryButton extends StatelessWidget {
height: 52, height: 52,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: AppColors.primaryGradient, gradient: AppColors.doctorGradient,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
boxShadow: AppColors.buttonShadow, boxShadow: AppColors.buttonShadow,
), ),

View File

@@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
import '../../services/in_app_notification_service.dart'; import '../../services/in_app_notification_service.dart';
@@ -147,7 +146,7 @@ class _NotificationCenterPageState
final unread = _history?.unreadCount ?? 0; final unread = _history?.unreadCount ?? 0;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8FAFF), backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
@@ -248,19 +247,19 @@ class _NotificationCenterPageState
return ListView( return ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), padding: const EdgeInsets.fromLTRB(16, 14, 16, 32),
children: [ children: [
if (unread > 0) _UnreadHint(unreadCount: unread), if (unread > 0) _UnreadHint(unreadCount: unread),
const SizedBox(height: 8), const SizedBox(height: 8),
if (today.isNotEmpty) ...[ if (today.isNotEmpty) ...[
const _SectionTitle('今天'), const _SectionTitle('今天'),
const SizedBox(height: 8), const SizedBox(height: 10),
...today.map(_buildDismissible), ...today.map(_buildDismissible),
], ],
if (earlier.isNotEmpty) ...[ if (earlier.isNotEmpty) ...[
if (today.isNotEmpty) const SizedBox(height: 14), if (today.isNotEmpty) const SizedBox(height: 18),
const _SectionTitle('更早'), const _SectionTitle('更早'),
const SizedBox(height: 8), const SizedBox(height: 10),
...earlier.map(_buildDismissible), ...earlier.map(_buildDismissible),
], ],
], ],
@@ -268,7 +267,7 @@ class _NotificationCenterPageState
} }
Widget _buildDismissible(InAppNotification item) => Padding( Widget _buildDismissible(InAppNotification item) => Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 12),
child: Dismissible( child: Dismissible(
key: ValueKey(item.id), key: ValueKey(item.id),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
@@ -281,7 +280,7 @@ class _NotificationCenterPageState
end: Alignment.centerRight, end: Alignment.centerRight,
colors: [AppColors.error.withValues(alpha: 0.6), AppColors.error], colors: [AppColors.error.withValues(alpha: 0.6), AppColors.error],
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(28),
), ),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -312,15 +311,21 @@ class _UnreadHint extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), margin: const EdgeInsets.fromLTRB(0, 4, 0, 12),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFFFFBEB),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0xFFFDE68A)),
),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 6, width: 8,
height: 6, height: 8,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xFFEF4444), color: Color(0xFFF97316),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
@@ -329,8 +334,8 @@ class _UnreadHint extends StatelessWidget {
'你有 $unreadCount 条未读消息', '你有 $unreadCount 条未读消息',
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
color: AppColors.textSecondary, color: Color(0xFF92400E),
), ),
), ),
], ],
@@ -344,15 +349,21 @@ class _SectionTitle extends StatelessWidget {
const _SectionTitle(this.text); const _SectionTitle(this.text);
@override @override
Widget build(BuildContext context) => Padding( Widget build(BuildContext context) => Container(
padding: const EdgeInsets.fromLTRB(2, 12, 2, 6), margin: const EdgeInsets.fromLTRB(0, 12, 0, 2),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0xFFE5E7EB)),
),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 4, width: 8,
height: 16, height: 8,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primary, gradient: AppColors.doctorGradient,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
), ),
), ),
@@ -366,8 +377,18 @@ class _SectionTitle extends StatelessWidget {
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
const Expanded( Expanded(
child: Divider(height: 1, thickness: 1, color: AppColors.divider), child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFE5E7EB),
const Color(0xFFE5E7EB).withValues(alpha: 0),
],
),
),
),
), ),
], ],
), ),
@@ -385,26 +406,32 @@ class _NotificationCard extends StatelessWidget {
final visual = _NotificationVisual.of(item); final visual = _NotificationVisual.of(item);
return Material( return Material(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(28),
elevation: 0,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(28),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(28),
border: Border.all( border: Border.all(
color: item.isRead color: item.isRead
? AppColors.border ? const Color(0xFFF1F5F9)
: visual.color.withValues(alpha: 0.34), : visual.color.withValues(alpha: 0.24),
width: item.isRead ? 1 : 1.2, width: 1,
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: const Color(0xFF101828).withValues(alpha: 0.03), color: const Color(0xFF0F172A).withValues(alpha: 0.08),
blurRadius: 8, blurRadius: 22,
offset: const Offset(0, 2), offset: const Offset(0, 10),
),
BoxShadow(
color: visual.color.withValues(alpha: item.isRead ? 0 : 0.08),
blurRadius: 18,
offset: const Offset(0, 8),
), ),
], ],
), ),
@@ -416,21 +443,24 @@ class _NotificationCard extends StatelessWidget {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Container( Container(
width: 44, width: 48,
height: 44, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: visual.lightColor, color: visual.lightColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(18),
border: Border.all(
color: visual.color.withValues(alpha: 0.12),
),
), ),
child: Icon(visual.icon, size: 22, color: visual.color), child: Icon(visual.icon, size: 23, color: visual.color),
), ),
if (!item.isRead) if (!item.isRead)
Positioned( Positioned(
top: -2, top: -2,
right: -2, right: -2,
child: Container( child: Container(
width: 9, width: 10,
height: 9, height: 10,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEF4444), color: const Color(0xFFEF4444),
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -440,7 +470,7 @@ class _NotificationCard extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(width: 12), const SizedBox(width: 13),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -463,25 +493,36 @@ class _NotificationCard extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 8),
Text( Container(
_formatTime(item.createdAt), padding: const EdgeInsets.symmetric(
style: const TextStyle( horizontal: 8,
fontSize: 12, vertical: 4,
fontWeight: FontWeight.w600, ),
color: AppColors.textSecondary, decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(999),
),
child: Text(
_formatTime(item.createdAt),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppColors.textSecondary,
),
), ),
), ),
], ],
), ),
const SizedBox(height: 5), const SizedBox(height: 6),
Text( Text(
item.message, item.message,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
height: 1.45, height: 1.5,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary, color: AppColors.textSecondary,
), ),
), ),

View File

@@ -2885,6 +2885,44 @@ class StaticTextPage extends ConsumerWidget {
### 六、联系我们 ### 六、联系我们
如有任何关于隐私的问题,请联系: 如有任何关于隐私的问题,请联系:
邮箱privacy@healthbutler.com 邮箱privacy@healthbutler.com
电话400-xxx-xxxx''',
'terms': '''## 服务协议
更新日期2026年6月26日
欢迎使用小脉健康。请您在注册、登录和使用本应用前,仔细阅读并理解本服务协议。您点击同意或继续使用本应用,即表示您已阅读、理解并同意本协议。
### 一、服务内容
小脉健康为用户提供健康数据记录、用药管理、报告管理、运动计划、健康日历、AI 健康建议及在线医生咨询等功能。具体服务内容会根据产品版本和实际开通情况进行调整。
### 二、账号使用
- 您应使用真实、准确、有效的信息完成注册和登录。
- 您应妥善保管账号、验证码及设备信息,不得将账号转让、出租或出借给他人使用。
- 因您主动泄露账号信息或操作不当造成的损失,由您自行承担。
### 三、健康服务说明
- 本应用提供的 AI 分析、健康提醒和运动饮食建议仅作为健康管理参考,不替代医生面对面的诊断、治疗或处方。
- 如您出现胸痛、呼吸困难、意识异常、严重头痛等紧急情况,应立即拨打急救电话或前往医疗机构就诊。
- 医生咨询、报告解读等内容应结合您的实际病史、检查结果和线下诊疗意见综合判断。
### 四、用户行为规范
您在使用本应用时不得:
- 提交虚假、违法、侵权或误导性信息;
- 干扰应用正常运行,或尝试未经授权访问系统、数据和接口;
- 利用本应用从事违法违规、损害他人权益或扰乱医疗服务秩序的行为。
### 五、数据与隐私
我们会按照《隐私政策》收集、使用、存储和保护您的个人信息及健康数据。您可在 App 内「设置」中查看隐私政策,并依法行使查询、更正、删除和注销账号等权利。
### 六、服务变更与中止
为提升服务质量或满足合规要求,我们可能对功能、页面、规则或服务范围进行调整。若因维护、升级、网络故障、不可抗力或法律法规要求导致服务暂时中断,我们会尽力降低对您的影响。
### 七、责任限制
在法律允许范围内,小脉健康不对因第三方服务、网络故障、设备异常、用户误操作或不可抗力造成的服务中断、数据延迟或损失承担超出法定范围的责任。
### 八、联系我们
如您对本协议或服务使用有任何疑问,请通过以下方式联系我们:
邮箱support@healthbutler.com
电话400-xxx-xxxx''', 电话400-xxx-xxxx''',
'about': '''## 关于小脉健康 'about': '''## 关于小脉健康
@@ -2914,56 +2952,76 @@ class StaticTextPage extends ConsumerWidget {
© 2025-2026 小脉健康团队。保留所有权利。 © 2025-2026 小脉健康团队。保留所有权利。
本软件受中华人民共和国著作权法保护。''', 本软件受中华人民共和国著作权法保护。''',
}; };
return GradientScaffold( return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
scrolledUnderElevation: 0.5,
surfaceTintColor: Colors.transparent,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.chevron_left), icon: const Icon(Icons.chevron_left, color: AppColors.textPrimary),
onPressed: () => popRoute(ref), onPressed: () => popRoute(ref),
), ),
title: Text( title: Text(
titles[type] ?? '', titles[type] ?? '',
style: TextStyle( style: const TextStyle(
fontSize: 19,
color: AppColors.textPrimary, color: AppColors.textPrimary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
), ),
), ),
centerTitle: true, centerTitle: true,
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.fromLTRB(22, 18, 22, 36),
child: MarkdownBody( child: Center(
data: contents[type] ?? '内容加载中...', child: ConstrainedBox(
selectable: true, constraints: const BoxConstraints(maxWidth: 680),
styleSheet: MarkdownStyleSheet( child: MarkdownBody(
p: const TextStyle( data: contents[type] ?? '内容加载中...',
fontSize: 17, selectable: true,
height: 1.8, styleSheet: MarkdownStyleSheet(
color: AppColors.textPrimary, textAlign: WrapAlignment.start,
), p: const TextStyle(
h1: const TextStyle( fontSize: 15,
fontSize: 22, height: 1.85,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w500,
color: AppColors.textPrimary, color: AppColors.textPrimary,
height: 1.5, ),
), h1Align: WrapAlignment.center,
h2: const TextStyle( h1Padding: const EdgeInsets.only(bottom: 18),
fontSize: 20, h1: const TextStyle(
fontWeight: FontWeight.w700, fontSize: 24,
color: AppColors.textPrimary, fontWeight: FontWeight.w900,
height: 1.5, color: AppColors.textPrimary,
), height: 1.35,
h3: const TextStyle( letterSpacing: 0,
fontSize: 18, ),
fontWeight: FontWeight.w700, h2Padding: const EdgeInsets.only(top: 18, bottom: 8),
color: AppColors.textPrimary, h2: const TextStyle(
height: 1.5, fontSize: 19,
), fontWeight: FontWeight.w900,
listBullet: const TextStyle( color: AppColors.textPrimary,
fontSize: 17, height: 1.45,
height: 1.8, letterSpacing: 0,
color: AppColors.textPrimary, ),
h3Padding: const EdgeInsets.only(top: 16, bottom: 6),
h3: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
height: 1.45,
letterSpacing: 0,
),
listBullet: const TextStyle(
fontSize: 15,
height: 1.85,
color: AppColors.textPrimary,
),
blockSpacing: 10,
listIndent: 22,
),
), ),
), ),
), ),

View File

@@ -19,10 +19,7 @@ class SettingsPage extends ConsumerWidget {
scrolledUnderElevation: 0.5, scrolledUnderElevation: 0.5,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
leading: IconButton( leading: IconButton(
icon: const Icon( icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary),
Icons.arrow_back,
color: AppColors.textPrimary,
),
onPressed: () => popRoute(ref), onPressed: () => popRoute(ref),
), ),
title: const Text( title: const Text(
@@ -44,21 +41,18 @@ class SettingsPage extends ConsumerWidget {
_SettingsTile( _SettingsTile(
icon: LucideIcons.bluetooth, icon: LucideIcons.bluetooth,
title: '蓝牙设备', title: '蓝牙设备',
colors: const [Color(0xFF38BDF8), Color(0xFF2563EB)],
onTap: () => pushRoute(ref, 'devices'), onTap: () => pushRoute(ref, 'devices'),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_SettingsTile( _SettingsTile(
icon: LucideIcons.bell, icon: LucideIcons.bell,
title: '消息通知', title: '消息通知',
colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
onTap: () => pushRoute(ref, 'notificationPrefs'), onTap: () => pushRoute(ref, 'notificationPrefs'),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_SettingsTile( _SettingsTile(
icon: LucideIcons.info, icon: LucideIcons.info,
title: '关于小脉健康', title: '关于小脉健康',
colors: const [Color(0xFF60A5FA), Color(0xFF0891B2)],
onTap: () => onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'about'}), pushRoute(ref, 'staticText', params: {'type': 'about'}),
), ),
@@ -66,12 +60,15 @@ class SettingsPage extends ConsumerWidget {
_SettingsTile( _SettingsTile(
icon: LucideIcons.shield, icon: LucideIcons.shield,
title: '隐私协议', title: '隐私协议',
colors: const [Color(0xFFF472B6), Color(0xFFDB2777)], onTap: () =>
onTap: () => pushRoute( pushRoute(ref, 'staticText', params: {'type': 'privacy'}),
ref, ),
'staticText', const SizedBox(height: 8),
params: {'type': 'privacy'}, _SettingsTile(
), icon: Icons.description_outlined,
title: '服务协议',
onTap: () =>
pushRoute(ref, 'staticText', params: {'type': 'terms'}),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
OutlinedButton.icon( OutlinedButton.icon(
@@ -80,11 +77,18 @@ class SettingsPage extends ConsumerWidget {
label: const Text('删除账号'), label: const Text('删除账号'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.error, foregroundColor: AppColors.error,
side: BorderSide(color: AppColors.error.withValues(alpha: 0.34)), side: BorderSide(
color: AppColors.error.withValues(alpha: 0.34),
),
backgroundColor: Colors.white, backgroundColor: Colors.white,
minimumSize: const Size.fromHeight(52), minimumSize: const Size.fromHeight(52),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800), textStyle: const TextStyle(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), fontSize: 16,
fontWeight: FontWeight.w800,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -96,7 +100,10 @@ class SettingsPage extends ConsumerWidget {
backgroundColor: AppColors.error, backgroundColor: AppColors.error,
foregroundColor: Colors.white, foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(52), minimumSize: const Size.fromHeight(52),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800), textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
@@ -113,12 +120,20 @@ class SettingsPage extends ConsumerWidget {
final ok = await showDialog<bool>( final ok = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.rXl),
),
title: const Text('删除账号', style: TextStyle(color: AppColors.error)), title: const Text('删除账号', style: TextStyle(color: AppColors.error)),
content: const Text('此操作不可恢复,将删除所有健康数据、用药记录、饮食记录、报告等,确定继续?'), content: const Text('此操作不可恢复,将删除所有健康数据、用药记录、饮食记录、报告等,确定继续?'),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确认删除', style: TextStyle(color: AppColors.error))), onPressed: () => Navigator.pop(ctx, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('确认删除', style: TextStyle(color: AppColors.error)),
),
], ],
), ),
); );
@@ -160,15 +175,11 @@ class SettingsPage extends ConsumerWidget {
class _SettingsTile extends StatelessWidget { class _SettingsTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String title; final String title;
final String? subtitle;
final List<Color> colors;
final VoidCallback onTap; final VoidCallback onTap;
const _SettingsTile({ const _SettingsTile({
required this.icon, required this.icon,
required this.title, required this.title,
required this.colors,
required this.onTap, required this.onTap,
this.subtitle,
}); });
@override @override
@@ -188,32 +199,16 @@ class _SettingsTile extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
Icon(icon, color: const Color(0xFF64748B), size: 22), Icon(icon, color: Colors.black, size: 22),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded( Expanded(
child: Column( child: Text(
crossAxisAlignment: CrossAxisAlignment.start, title,
children: [ style: const TextStyle(
Text( fontSize: 16,
title, fontWeight: FontWeight.w600,
style: const TextStyle( color: AppColors.textPrimary,
fontSize: 16, ),
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 3),
Text(
subtitle!,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textHint,
),
),
],
],
), ),
), ),
const Icon( const Icon(

View File

@@ -4,6 +4,7 @@ import '../core/app_colors.dart';
import '../core/navigation_provider.dart'; import '../core/navigation_provider.dart';
import '../pages/admin/admin_home_page.dart' show adminPageProvider; import '../pages/admin/admin_home_page.dart' show adminPageProvider;
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import 'drawer_shell.dart';
class AdminDrawer extends ConsumerWidget { class AdminDrawer extends ConsumerWidget {
const AdminDrawer({super.key}); const AdminDrawer({super.key});
@@ -12,100 +13,99 @@ class AdminDrawer extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentPage = ref.watch(adminPageProvider); final currentPage = ref.watch(adminPageProvider);
return Drawer( return DrawerShell(
width: MediaQuery.of(context).size.width * 0.8, child: SafeArea(
child: Container( child: Column(
decoration: const BoxDecoration(gradient: AppColors.drawerGradient), children: [
child: SafeArea( Container(
child: Column( margin: const EdgeInsets.all(14),
children: [ padding: const EdgeInsets.all(16),
Container( decoration: BoxDecoration(
margin: const EdgeInsets.all(14), color: Colors.white.withValues(alpha: 0.88),
padding: const EdgeInsets.all(16), borderRadius: BorderRadius.circular(22),
decoration: BoxDecoration( border: Border.all(
gradient: AppColors.softGlassGradient, color: Colors.white.withValues(alpha: 0.86),
borderRadius: BorderRadius.circular(20), width: 1.2,
border: Border.all(color: AppColors.borderLight),
boxShadow: AppColors.cardShadow,
), ),
child: Row( boxShadow: AppColors.cardShadowLight,
children: [ ),
Container( child: Row(
width: 52, children: [
height: 52, Container(
decoration: BoxDecoration( width: 56,
gradient: AppColors.primaryGradient, height: 56,
borderRadius: BorderRadius.circular(18), decoration: BoxDecoration(
boxShadow: AppColors.buttonShadow, gradient: AppColors.doctorGradient,
), borderRadius: BorderRadius.circular(20),
child: const Icon( boxShadow: AppColors.buttonShadow,
Icons.admin_panel_settings,
size: 28,
color: Colors.white,
),
), ),
const SizedBox(width: 12), child: const Icon(
const Expanded( Icons.admin_panel_settings,
child: Column( size: 28,
crossAxisAlignment: CrossAxisAlignment.start, color: Colors.white,
children: [
Text(
'系统管理员',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
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.w800,
color: AppColors.textPrimary,
),
),
SizedBox(height: 3),
Text(
'医生与患者管理',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
],
), ),
const SizedBox(height: 8), ),
_MenuItem( const SizedBox(height: 8),
icon: Icons.local_hospital, _MenuItem(
label: '医生管理', icon: Icons.local_hospital,
selected: currentPage == 'doctors', label: '医生管理',
onTap: () { selected: currentPage == 'doctors',
Navigator.pop(context); onTap: () {
ref.read(adminPageProvider.notifier).set('doctors'); Navigator.pop(context);
}, ref.read(adminPageProvider.notifier).set('doctors');
), },
_MenuItem( ),
icon: Icons.people_outline, _MenuItem(
label: '患者列表', icon: Icons.people_outline,
selected: currentPage == 'patients', label: '患者列表',
onTap: () { selected: currentPage == 'patients',
Navigator.pop(context); onTap: () {
ref.read(adminPageProvider.notifier).set('patients'); Navigator.pop(context);
}, ref.read(adminPageProvider.notifier).set('patients');
), },
const Spacer(), ),
const Divider(height: 1, color: AppColors.divider), const Spacer(),
const SizedBox(height: 8), const Divider(height: 1, color: AppColors.divider),
_MenuItem( const SizedBox(height: 8),
icon: Icons.logout, _MenuItem(
label: '退出登录', icon: Icons.logout,
danger: true, label: '退出登录',
onTap: () async { danger: true,
Navigator.pop(context); onTap: () async {
await ref.read(authProvider.notifier).logout(); Navigator.pop(context);
goRoute(ref, 'login'); await ref.read(authProvider.notifier).logout();
}, goRoute(ref, 'login');
), },
const SizedBox(height: 16), ),
], const SizedBox(height: 16),
), ],
), ),
), ),
); );
@@ -131,7 +131,7 @@ class _MenuItem extends StatelessWidget {
final color = danger final color = danger
? AppColors.error ? AppColors.error
: selected : selected
? AppColors.primary ? const Color(0xFF7C5CFF)
: AppColors.textPrimary; : AppColors.textPrimary;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
@@ -145,13 +145,14 @@ class _MenuItem extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: selected gradient: selected
? AppColors.primaryGradient ? AppColors.doctorGradient
: AppColors.surfaceGradient, : AppColors.surfaceGradient,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all( border: Border.all(
color: selected color: selected
? AppColors.primaryLight ? Colors.white.withValues(alpha: 0.80)
: AppColors.borderLight, : AppColors.borderLight,
width: 1.1,
), ),
boxShadow: selected boxShadow: selected
? AppColors.buttonShadow ? AppColors.buttonShadow

View File

@@ -4,6 +4,7 @@ import '../core/app_colors.dart';
import '../core/navigation_provider.dart'; import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../pages/doctor/doctor_home_page.dart' show doctorPageProvider; import '../pages/doctor/doctor_home_page.dart' show doctorPageProvider;
import 'drawer_shell.dart';
class DoctorDrawer extends ConsumerWidget { class DoctorDrawer extends ConsumerWidget {
const DoctorDrawer({super.key}); const DoctorDrawer({super.key});
@@ -14,94 +15,90 @@ class DoctorDrawer extends ConsumerWidget {
final currentPage = ref.watch(doctorPageProvider); final currentPage = ref.watch(doctorPageProvider);
final name = auth.user?.name ?? '医生'; final name = auth.user?.name ?? '医生';
return Drawer( return DrawerShell(
width: MediaQuery.of(context).size.width * 0.8, child: SafeArea(
child: Container( child: Column(
decoration: const BoxDecoration(gradient: AppColors.drawerGradient), children: [
child: SafeArea( _DrawerHeader(
child: Column( name: name,
children: [ subtitle: '点击完善医生信息',
_DrawerHeader( icon: Icons.medical_services_outlined,
name: name, onTap: () {
subtitle: '点击完善医生信息', Navigator.pop(context);
icon: Icons.medical_services_outlined, pushRoute(ref, 'doctorProfile');
onTap: () { },
Navigator.pop(context); ),
pushRoute(ref, 'doctorProfile'); const SizedBox(height: 8),
}, _DrawerItem(
), icon: Icons.dashboard_outlined,
const SizedBox(height: 8), label: '工作台',
_DrawerItem( selected: currentPage == 'dashboard',
icon: Icons.dashboard_outlined, onTap: () {
label: '工作台', Navigator.pop(context);
selected: currentPage == 'dashboard', ref.read(doctorPageProvider.notifier).set('dashboard');
onTap: () { },
Navigator.pop(context); ),
ref.read(doctorPageProvider.notifier).set('dashboard'); _DrawerItem(
}, icon: Icons.people_outline,
), label: '患者管理',
_DrawerItem( selected: currentPage == 'patients',
icon: Icons.people_outline, onTap: () {
label: '患者管理', Navigator.pop(context);
selected: currentPage == 'patients', ref.read(doctorPageProvider.notifier).set('patients');
onTap: () { },
Navigator.pop(context); ),
ref.read(doctorPageProvider.notifier).set('patients'); _DrawerItem(
}, icon: Icons.chat_outlined,
), label: '问诊列表',
_DrawerItem( selected: currentPage == 'consultations',
icon: Icons.chat_outlined, onTap: () {
label: '问诊列表', Navigator.pop(context);
selected: currentPage == 'consultations', ref.read(doctorPageProvider.notifier).set('consultations');
onTap: () { },
Navigator.pop(context); ),
ref.read(doctorPageProvider.notifier).set('consultations'); _DrawerItem(
}, icon: Icons.description_outlined,
), label: '报告审核',
_DrawerItem( selected: currentPage == 'reports',
icon: Icons.description_outlined, onTap: () {
label: '报告审核', Navigator.pop(context);
selected: currentPage == 'reports', ref.read(doctorPageProvider.notifier).set('reports');
onTap: () { },
Navigator.pop(context); ),
ref.read(doctorPageProvider.notifier).set('reports'); _DrawerItem(
}, icon: Icons.event_note_outlined,
), label: '复查随访',
_DrawerItem( selected: currentPage == 'followups',
icon: Icons.event_note_outlined, onTap: () {
label: '复查随访', Navigator.pop(context);
selected: currentPage == 'followups', ref.read(doctorPageProvider.notifier).set('followups');
onTap: () { },
Navigator.pop(context); ),
ref.read(doctorPageProvider.notifier).set('followups'); const Spacer(),
}, const Divider(height: 1, color: AppColors.divider),
), const SizedBox(height: 8),
const Spacer(), _DrawerItem(
const Divider(height: 1, color: AppColors.divider), icon: Icons.settings_outlined,
const SizedBox(height: 8), label: '设置',
_DrawerItem( selected: false,
icon: Icons.settings_outlined, onTap: () {
label: '设置', Navigator.pop(context);
selected: false, pushRoute(ref, 'doctorSettings');
onTap: () { },
Navigator.pop(context); ),
pushRoute(ref, 'doctorSettings'); _DrawerItem(
}, icon: Icons.logout,
), label: '退出登录',
_DrawerItem( selected: false,
icon: Icons.logout, danger: true,
label: '退出登录', onTap: () async {
selected: false, Navigator.pop(context);
danger: true, await ref.read(authProvider.notifier).logout();
onTap: () async { goRoute(ref, 'login');
Navigator.pop(context); },
await ref.read(authProvider.notifier).logout(); ),
goRoute(ref, 'login'); const SizedBox(height: 16),
}, ],
),
const SizedBox(height: 16),
],
),
), ),
), ),
); );
@@ -126,10 +123,13 @@ class _DrawerHeader extends StatelessWidget {
margin: const EdgeInsets.all(14), margin: const EdgeInsets.all(14),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: AppColors.softGlassGradient, color: Colors.white.withValues(alpha: 0.88),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(22),
border: Border.all(color: AppColors.borderLight), border: Border.all(
boxShadow: AppColors.cardShadow, color: Colors.white.withValues(alpha: 0.86),
width: 1.2,
),
boxShadow: AppColors.cardShadowLight,
), ),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
@@ -137,11 +137,11 @@ class _DrawerHeader extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 52, width: 56,
height: 52, height: 56,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: AppColors.doctorGradient, gradient: AppColors.doctorGradient,
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(20),
boxShadow: AppColors.buttonShadow, boxShadow: AppColors.buttonShadow,
), ),
child: Icon(icon, color: Colors.white, size: 26), child: Icon(icon, color: Colors.white, size: 26),
@@ -198,7 +198,7 @@ class _DrawerItem extends StatelessWidget {
final color = danger final color = danger
? AppColors.error ? AppColors.error
: selected : selected
? AppColors.primary ? const Color(0xFF7C5CFF)
: AppColors.textPrimary; : AppColors.textPrimary;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
@@ -212,13 +212,14 @@ class _DrawerItem extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: selected gradient: selected
? AppColors.primaryGradient ? AppColors.doctorGradient
: AppColors.surfaceGradient, : AppColors.surfaceGradient,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all( border: Border.all(
color: selected color: selected
? AppColors.primaryLight ? Colors.white.withValues(alpha: 0.80)
: AppColors.borderLight, : AppColors.borderLight,
width: 1.1,
), ),
boxShadow: selected boxShadow: selected
? AppColors.buttonShadow ? AppColors.buttonShadow

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
class DrawerShell extends StatelessWidget {
final double widthFactor;
final Widget child;
const DrawerShell({super.key, required this.child, this.widthFactor = 0.84});
@override
Widget build(BuildContext context) {
return Drawer(
width: MediaQuery.of(context).size.width * widthFactor,
backgroundColor: Colors.transparent,
elevation: 0,
child: ClipRRect(
borderRadius: const BorderRadius.horizontal(right: Radius.circular(28)),
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/branding/drawer_background_v1.png',
fit: BoxFit.cover,
alignment: Alignment.centerLeft,
),
),
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withValues(alpha: 0.12),
Colors.white.withValues(alpha: 0.42),
Colors.white.withValues(alpha: 0.72),
],
stops: const [0.0, 0.48, 1.0],
),
),
),
),
child,
],
),
),
);
}
}

View File

@@ -4,6 +4,7 @@ import '../core/app_colors.dart';
import '../core/navigation_provider.dart'; import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../providers/data_providers.dart'; import '../providers/data_providers.dart';
import 'drawer_shell.dart';
class HealthDrawer extends ConsumerWidget { class HealthDrawer extends ConsumerWidget {
const HealthDrawer({super.key}); const HealthDrawer({super.key});
@@ -13,37 +14,25 @@ class HealthDrawer extends ConsumerWidget {
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final latestHealth = ref.watch(latestHealthProvider); final latestHealth = ref.watch(latestHealthProvider);
return Drawer( return DrawerShell(
width: MediaQuery.of(context).size.width * 0.9, widthFactor: 0.9,
backgroundColor: Colors.transparent, child: SafeArea(
elevation: 0, child: CustomScrollView(
child: DecoratedBox( physics: const BouncingScrollPhysics(),
decoration: const BoxDecoration( slivers: [
gradient: LinearGradient( SliverPadding(
begin: Alignment.topCenter, padding: const EdgeInsets.fromLTRB(18, 18, 18, 24),
end: Alignment.bottomCenter, sliver: SliverList.list(
colors: [Color(0xFF87CEEB), Color(0xFFFFFFFF)], children: [
stops: [0.0, 0.35], _AccountHeader(user: auth.user, ref: ref),
), const SizedBox(height: 18),
), _HealthDashboard(latestHealth: latestHealth, ref: ref),
child: SafeArea( const SizedBox(height: 18),
child: CustomScrollView( _NavigationSection(ref: ref),
physics: const BouncingScrollPhysics(), ],
slivers: [
SliverPadding(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 24),
sliver: SliverList.list(
children: [
_AccountHeader(user: auth.user, ref: ref),
const SizedBox(height: 18),
_HealthDashboard(latestHealth: latestHealth, ref: ref),
const SizedBox(height: 18),
_NavigationSection(ref: ref),
],
),
), ),
], ),
), ],
), ),
), ),
); );
@@ -75,12 +64,12 @@ class _AccountHeader extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFE9EEF5)), border: Border.all(color: Colors.white, width: 2),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: const Color(0xFF101828).withValues(alpha: 0.06), color: const Color(0xFF7C5CFF).withValues(alpha: 0.16),
blurRadius: 14, blurRadius: 20,
offset: const Offset(0, 6), offset: const Offset(0, 10),
), ),
], ],
), ),
@@ -152,9 +141,9 @@ class _HealthDashboard extends StatelessWidget {
), ),
titleColor: Colors.white, titleColor: Colors.white,
backgroundGradient: const LinearGradient( backgroundGradient: const LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topLeft,
end: Alignment.bottomCenter, end: Alignment.bottomRight,
colors: [Color(0xFF89F7FE), Color(0xFF66A6FF)], colors: [Color(0xFF60A5FA), Color(0xFF8B5CF6), Color(0xFFF472B6)],
), ),
child: latestHealth.when( child: latestHealth.when(
data: (data) => _DashboardMetrics(data: data, ref: ref), data: (data) => _DashboardMetrics(data: data, ref: ref),
@@ -282,7 +271,11 @@ class _MetricTile extends StatelessWidget {
if (info.unit != null) if (info.unit != null)
Text( Text(
info.unit!, info.unit!,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xE0FFFFFF)), style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xE0FFFFFF),
),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
@@ -311,7 +304,7 @@ class _NavigationSection extends StatelessWidget {
icon: Icons.folder_shared_rounded, icon: Icons.folder_shared_rounded,
title: '档案', title: '档案',
route: 'healthArchive', route: 'healthArchive',
colors: const [Color(0xFF6EE7B7), Color(0xFF14B8A6)], colors: const [Color(0xFF93C5FD), Color(0xFF60A5FA)],
), ),
_NavItem( _NavItem(
icon: Icons.description_rounded, icon: Icons.description_rounded,
@@ -329,13 +322,13 @@ class _NavigationSection extends StatelessWidget {
icon: Icons.restaurant_rounded, icon: Icons.restaurant_rounded,
title: '饮食', title: '饮食',
route: 'dietRecords', route: 'dietRecords',
colors: const [Color(0xFFFED7AA), Color(0xFFFDBA74)], colors: const [Color(0xFFFBCFE8), Color(0xFFF472B6)],
), ),
_NavItem( _NavItem(
icon: Icons.calendar_month_rounded, icon: Icons.calendar_month_rounded,
title: '日历', title: '日历',
route: 'calendar', route: 'calendar',
colors: const [Color(0xFFA7F3D0), Color(0xFF86EFAC)], colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)],
), ),
_NavItem( _NavItem(
icon: Icons.event_available_rounded, icon: Icons.event_available_rounded,
@@ -354,9 +347,9 @@ class _NavigationSection extends StatelessWidget {
return _Panel( return _Panel(
title: '功能入口', title: '功能入口',
backgroundGradient: const LinearGradient( backgroundGradient: const LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topLeft,
end: Alignment.bottomCenter, end: Alignment.bottomRight,
colors: [Color(0xFFEAF4FB), Color(0xFFF4F9FD)], colors: [Color(0xFAFFFFFF), Color(0xF5EFF6FF)],
), ),
child: GridView.builder( child: GridView.builder(
itemCount: items.length, itemCount: items.length,
@@ -394,13 +387,13 @@ class _NavTile extends StatelessWidget {
}; };
List<Color> get _colors => switch (item.route) { List<Color> get _colors => switch (item.route) {
'healthArchive' => const [Color(0xFF34D399), Color(0xFF059669)], 'healthArchive' => const [Color(0xFF93C5FD), Color(0xFF60A5FA)],
'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)], 'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)],
'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)], 'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
'dietRecords' => const [Color(0xFFF6D365), Color(0xFFF97316)], 'dietRecords' => const [Color(0xFFFBCFE8), Color(0xFFF472B6)],
'calendar' => const [Color(0xFF34D399), Color(0xFF059669)], 'calendar' => const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)],
'followups' => const [Color(0xFFF472B6), Color(0xFFDB2777)], 'followups' => const [Color(0xFFF472B6), Color(0xFFDB2777)],
'exercisePlan' => const [Color(0xFF22D3EE), Color(0xFF0891B2)], 'exercisePlan' => const [Color(0xFF7DD3FC), Color(0xFF60A5FA)],
_ => item.colors, _ => item.colors,
}; };
@@ -413,7 +406,8 @@ class _NavTile extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white, width: 1.2), border: Border.all(color: AppColors.borderLight, width: 1.1),
boxShadow: AppColors.cardShadowLight,
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -479,7 +473,10 @@ class _Panel extends StatelessWidget {
], ],
), ),
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
border: Border.all(color: Colors.white, width: 1.4), border: Border.all(
color: Colors.white.withValues(alpha: 0.82),
width: 1.2,
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: const Color(0xFF6D5DF6).withValues(alpha: 0.08), color: const Color(0xFF6D5DF6).withValues(alpha: 0.08),

View File

@@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:health_app/core/app_theme.dart';
import 'package:health_app/pages/auth/login_page.dart';
void main() {
testWidgets('主题颜色正确', (tester) async {
expect(AppTheme.primary, const Color(0xFF6366F1));
expect(AppTheme.primaryLight, const Color(0xFFEEF2FF));
expect(AppTheme.bg, AppTheme.bg);
expect(AppTheme.error, const Color(0xFFF56C6C));
expect(AppTheme.success, const Color(0xFF6ECF8A));
});
testWidgets('登录页渲染正常', (tester) async {
await tester.pumpWidget(const ProviderScope(child: MaterialApp(home: LoginPage())));
await tester.pumpAndSettle();
expect(find.text('小脉健康'), findsOneWidget);
expect(find.text('登 录'), findsOneWidget);
});
testWidgets('获取验证码按钮存在', (tester) async {
await tester.pumpWidget(const ProviderScope(child: MaterialApp(home: LoginPage())));
await tester.pumpAndSettle();
expect(find.text('获取验证码'), findsOneWidget);
});
testWidgets('AI 气泡样式', (tester) async {
await tester.pumpWidget(MaterialApp(
theme: AppTheme.lightTheme,
home: Scaffold(body: Align(
alignment: Alignment.centerLeft,
child: Container(
constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: const Border(left: BorderSide(color: AppTheme.primaryLight, width: 3)),
),
child: const Text('收到!已记录', style: TextStyle(fontSize: 16)),
),
)),
));
await tester.pumpAndSettle();
expect(find.text('收到!已记录'), findsOneWidget);
});
testWidgets('用户气泡样式', (tester) async {
await tester.pumpWidget(MaterialApp(
theme: AppTheme.lightTheme,
home: Scaffold(body: Align(
alignment: Alignment.centerRight,
child: Container(
constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(16),
),
child: const Text('血压 135/85', style: TextStyle(fontSize: 16, color: Colors.white)),
),
)),
));
await tester.pumpAndSettle();
expect(find.text('血压 135/85'), findsOneWidget);
});
}