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

View File

@@ -12,8 +12,7 @@ class LoginPage extends ConsumerStatefulWidget {
}
class _LoginPageState extends ConsumerState<LoginPage> {
static const _headerBg = 'assets/branding/ui_abstract_header_bg_v1.png';
static const _cornerAccent = 'assets/branding/ui_corner_accent_v1.png';
static const _loginBg = 'assets/branding/login_background_v1.png';
final _phoneCtrl = TextEditingController();
final _codeCtrl = TextEditingController();
@@ -66,8 +65,12 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Future<void> _submit() async {
if (!_agreed) {
setState(() => _error = '请阅读并同意服务协议和隐私政策');
return;
final accepted = await _showAgreementDialog();
if (!mounted || accepted != true) return;
setState(() {
_agreed = true;
_error = null;
});
}
if (_phoneCtrl.text.trim().isEmpty || _codeCtrl.text.trim().isEmpty) {
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() {
ref.invalidate(latestHealthProvider);
ref.invalidate(medicationListProvider);
@@ -250,34 +371,26 @@ class _LoginPageState extends ConsumerState<LoginPage> {
backgroundColor: Colors.white,
body: Stack(
children: [
Positioned.fill(
child: Image.asset(
_loginBg,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
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(
_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),
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],
),
),
),
),
@@ -285,10 +398,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 22, 20, 24),
padding: const EdgeInsets.fromLTRB(20, 28, 20, 24),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 46,
minHeight: (constraints.maxHeight - 52).clamp(
0.0,
double.infinity,
),
),
child: Center(
child: ConstrainedBox(
@@ -297,8 +413,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_LoginHero(isLogin: _isLogin),
const SizedBox(height: 22),
const _LoginHero(),
const SizedBox(height: 28),
_LoginFormCard(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -328,7 +444,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
'手机号',
TextInputType.phone,
11,
icon: Icons.phone_iphone_rounded,
icon: Icons.local_phone_rounded,
gradientIcon: true,
),
const SizedBox(height: 12),
Row(
@@ -339,7 +456,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
'验证码',
TextInputType.number,
6,
icon: Icons.verified_outlined,
icon: Icons.sms_rounded,
gradientIcon: true,
),
),
const SizedBox(width: 10),
@@ -407,9 +525,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}
class _LoginHero extends StatelessWidget {
final bool isLogin;
const _LoginHero({required this.isLogin});
const _LoginHero();
@override
Widget build(BuildContext context) {
@@ -437,23 +553,6 @@ class _LoginHero extends StatelessWidget {
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 {
@override
Widget build(BuildContext context) {
return Container(
width: 72,
height: 72,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: BorderRadius.circular(24),
boxShadow: AppColors.buttonShadow,
),
child: const Icon(
Icons.monitor_heart_outlined,
color: Colors.white,
size: 34,
return SizedBox(
width: 132,
height: 132,
child: Transform.scale(
scale: 1.08,
child: Image.asset(
'assets/branding/health_icon_master.png',
fit: BoxFit.cover,
),
),
);
}
@@ -511,8 +607,16 @@ class _TextField extends StatelessWidget {
final TextInputType type;
final int maxLen;
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
Widget build(BuildContext context) => TextField(
@@ -529,6 +633,8 @@ class _TextField extends StatelessWidget {
counterText: '',
prefixIcon: icon == null
? null
: gradientIcon
? _GradientPrefixIcon(icon: icon!)
: Icon(icon, size: 20, color: AppColors.textSecondary),
filled: true,
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 {
final String? name;
final VoidCallback onTap;
@@ -762,7 +882,7 @@ class _PrimaryButton extends StatelessWidget {
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
gradient: AppColors.doctorGradient,
borderRadius: BorderRadius.circular(14),
boxShadow: AppColors.buttonShadow,
),