Polish health app UI
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 39 KiB |
BIN
health_app/assets/branding/drawer_background_v1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 906 KiB After Width: | Height: | Size: 1.5 MiB |
BIN
health_app/assets/branding/login_background_v1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 920 KiB |
|
Before Width: | Height: | Size: 823 KiB |
|
Before Width: | Height: | Size: 844 KiB |
|
Before Width: | Height: | Size: 912 KiB |
1
health_app/assets/images/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 752 KiB |
|
Before Width: | Height: | Size: 985 B After Width: | Height: | Size: 951 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
@@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -251,33 +372,25 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(gradient: AppColors.bgGradient),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: 260,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: 0.92,
|
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
_headerBg,
|
_loginBg,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.white.withValues(alpha: 0.10),
|
||||||
|
Colors.white.withValues(alpha: 0.28),
|
||||||
|
Colors.white.withValues(alpha: 0.54),
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.52, 1.0],
|
||||||
),
|
),
|
||||||
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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF8FAFC),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
_formatTime(item.createdAt),
|
_formatTime(item.createdAt),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.textSecondary,
|
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,57 +2952,77 @@ 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: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 680),
|
||||||
child: MarkdownBody(
|
child: MarkdownBody(
|
||||||
data: contents[type] ?? '内容加载中...',
|
data: contents[type] ?? '内容加载中...',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(
|
||||||
|
textAlign: WrapAlignment.start,
|
||||||
p: const TextStyle(
|
p: const TextStyle(
|
||||||
fontSize: 17,
|
fontSize: 15,
|
||||||
height: 1.8,
|
height: 1.85,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
|
h1Align: WrapAlignment.center,
|
||||||
|
h1Padding: const EdgeInsets.only(bottom: 18),
|
||||||
h1: const TextStyle(
|
h1: const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w900,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
height: 1.5,
|
height: 1.35,
|
||||||
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
|
h2Padding: const EdgeInsets.only(top: 18, bottom: 8),
|
||||||
h2: const TextStyle(
|
h2: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 19,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w900,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
height: 1.5,
|
height: 1.45,
|
||||||
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
|
h3Padding: const EdgeInsets.only(top: 16, bottom: 6),
|
||||||
h3: const TextStyle(
|
h3: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w800,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
height: 1.5,
|
height: 1.45,
|
||||||
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
listBullet: const TextStyle(
|
listBullet: const TextStyle(
|
||||||
fontSize: 17,
|
fontSize: 15,
|
||||||
height: 1.8,
|
height: 1.85,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
|
blockSpacing: 10,
|
||||||
|
listIndent: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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',
|
|
||||||
params: {'type': 'privacy'},
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_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,13 +199,10 @@ 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,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -202,19 +210,6 @@ class _SettingsTile extends StatelessWidget {
|
|||||||
color: AppColors.textPrimary,
|
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(
|
||||||
Icons.arrow_forward_ios_rounded,
|
Icons.arrow_forward_ios_rounded,
|
||||||
|
|||||||
@@ -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,10 +13,7 @@ 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: Container(
|
|
||||||
decoration: const BoxDecoration(gradient: AppColors.drawerGradient),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -23,19 +21,22 @@ class AdminDrawer extends ConsumerWidget {
|
|||||||
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: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 52,
|
width: 56,
|
||||||
height: 52,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: AppColors.primaryGradient,
|
gradient: AppColors.doctorGradient,
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: AppColors.buttonShadow,
|
boxShadow: AppColors.buttonShadow,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -107,7 +108,6 @@ class AdminDrawer extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,10 +15,7 @@ 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: Container(
|
|
||||||
decoration: const BoxDecoration(gradient: AppColors.drawerGradient),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -103,7 +101,6 @@ class DoctorDrawer extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
48
health_app/lib/widgets/drawer_shell.dart
Normal 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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,19 +14,8 @@ 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,
|
|
||||||
elevation: 0,
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFF87CEEB), Color(0xFFFFFFFF)],
|
|
||||||
stops: [0.0, 0.35],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
@@ -45,7 +35,6 @@ class HealthDrawer extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||