style: 重构登录页视觉布局

- 使用品牌抽象背景和角落装饰素材
- 调整登录/注册卡片布局、品牌信息和表单层级
- 保留验证码、医生选择、协议勾选和登录注册流程
This commit is contained in:
MingNian
2026-06-25 22:19:10 +08:00
parent 18df2f3285
commit 86c7957144

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/app_colors.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
import '../../providers/data_providers.dart';
@@ -13,6 +12,9 @@ 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';
final _phoneCtrl = TextEditingController();
final _codeCtrl = TextEditingController();
final _nameCtrl = TextEditingController();
@@ -245,127 +247,253 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}
return Scaffold(
body: AppBackground(
safeArea: true,
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Container(
padding: const EdgeInsets.fromLTRB(22, 24, 22, 22),
decoration: BoxDecoration(
gradient: AppColors.softGlassGradient,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.borderLight),
boxShadow: AppColors.cardShadow,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_BrandMark(),
const SizedBox(height: 16),
const Text(
'小脉健康',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w900,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
const Text(
'血管病患者的 AI 健康管理助手',
style: TextStyle(fontSize: 13, color: AppColors.textSecondary),
),
const SizedBox(height: 8),
Text(
_isLogin ? '登录你的健康账户' : '创建新的健康账户',
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 26),
if (_successMsg != null)
_NoticeBanner(text: _successMsg!, success: true),
if (!_isLogin) ...[
_DoctorSelector(
name: _selectedDoctorName,
onTap: _showDoctorPicker,
),
const SizedBox(height: 12),
_TextField(_nameCtrl, '姓名', TextInputType.name, 20),
const SizedBox(height: 12),
],
_TextField(_phoneCtrl, '手机号', TextInputType.phone, 11),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _TextField(
_codeCtrl,
'验证码',
TextInputType.number,
6,
),
),
const SizedBox(width: 10),
_SmsButton(
sending: _sending,
countdown: _countdown,
onTap: (_countdown > 0 || _sending) ? null : _sendSms,
),
],
),
const SizedBox(height: 14),
_Agreement(
agreed: _agreed,
onTap: () => setState(() => _agreed = !_agreed),
),
if (_error != null) ...[
const SizedBox(height: 12),
_NoticeBanner(text: _error!, success: false),
],
const SizedBox(height: 22),
_PrimaryButton(
loading: _loading,
label: _isLogin ? '登录' : '注册',
onTap: _loading ? null : _submit,
),
const SizedBox(height: 16),
GestureDetector(
onTap: () => setState(() {
_isLogin = !_isLogin;
_error = null;
_successMsg = null;
}),
child: Text(
_isLogin ? '没有账号?去注册' : '已有账号?去登录',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
),
],
backgroundColor: Colors.white,
body: Stack(
children: [
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),
),
),
),
SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 22, 20, 24),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 46,
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 430),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_LoginHero(isLogin: _isLogin),
const SizedBox(height: 22),
_LoginFormCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_successMsg != null)
_NoticeBanner(
text: _successMsg!,
success: true,
),
if (!_isLogin) ...[
_DoctorSelector(
name: _selectedDoctorName,
onTap: _showDoctorPicker,
),
const SizedBox(height: 12),
_TextField(
_nameCtrl,
'姓名',
TextInputType.name,
20,
icon: Icons.person_outline_rounded,
),
const SizedBox(height: 12),
],
_TextField(
_phoneCtrl,
'手机号',
TextInputType.phone,
11,
icon: Icons.phone_iphone_rounded,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _TextField(
_codeCtrl,
'验证码',
TextInputType.number,
6,
icon: Icons.verified_outlined,
),
),
const SizedBox(width: 10),
_SmsButton(
sending: _sending,
countdown: _countdown,
onTap: (_countdown > 0 || _sending)
? null
: _sendSms,
),
],
),
const SizedBox(height: 14),
_Agreement(
agreed: _agreed,
onTap: () =>
setState(() => _agreed = !_agreed),
),
if (_error != null) ...[
const SizedBox(height: 12),
_NoticeBanner(
text: _error!,
success: false,
),
],
const SizedBox(height: 22),
_PrimaryButton(
loading: _loading,
label: _isLogin ? '登录' : '注册',
onTap: _loading ? null : _submit,
),
const SizedBox(height: 16),
GestureDetector(
onTap: () => setState(() {
_isLogin = !_isLogin;
_error = null;
_successMsg = null;
}),
child: Text(
_isLogin ? '没有账号?去注册' : '已有账号?去登录',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: AppColors.primary,
),
),
),
],
),
),
],
),
),
),
),
);
},
),
),
],
),
);
}
}
class _LoginHero extends StatelessWidget {
final bool isLogin;
const _LoginHero({required this.isLogin});
@override
Widget build(BuildContext context) {
return Column(
children: [
_BrandMark(),
const SizedBox(height: 16),
const Text(
'小脉健康',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.w900,
color: AppColors.textPrimary,
letterSpacing: 0,
),
),
const SizedBox(height: 6),
const Text(
'血管病患者的 AI 健康管理助手',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.45,
fontWeight: FontWeight.w700,
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,
),
),
),
],
);
}
}
class _LoginFormCard extends StatelessWidget {
final Widget child;
const _LoginFormCard({required this.child});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 20),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.94),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: AppColors.border, width: 1.1),
boxShadow: [
BoxShadow(
color: const Color(0xFF475467).withValues(alpha: 0.10),
blurRadius: 30,
offset: const Offset(0, 16),
),
],
),
child: child,
);
}
}
class _BrandMark extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 68,
height: 68,
width: 72,
height: 72,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: BorderRadius.circular(22),
borderRadius: BorderRadius.circular(24),
boxShadow: AppColors.buttonShadow,
),
child: const Icon(
@@ -382,15 +510,38 @@ class _TextField extends StatelessWidget {
final String hint;
final TextInputType type;
final int maxLen;
const _TextField(this.ctrl, this.hint, this.type, this.maxLen);
final IconData? icon;
const _TextField(this.ctrl, this.hint, this.type, this.maxLen, {this.icon});
@override
Widget build(BuildContext context) => TextField(
controller: ctrl,
keyboardType: type,
maxLength: maxLen,
style: const TextStyle(fontSize: 15, color: AppColors.textPrimary),
decoration: InputDecoration(hintText: hint, counterText: ''),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
decoration: InputDecoration(
hintText: hint,
counterText: '',
prefixIcon: icon == null
? null
: Icon(icon, size: 20, color: AppColors.textSecondary),
filled: true,
fillColor: const Color(0xFFF8FAFC),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 15),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: AppColors.border, width: 1.1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: AppColors.primary, width: 1.2),
),
),
);
}
@@ -407,17 +558,24 @@ class _DoctorSelector extends StatelessWidget {
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.border),
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.border, width: 1.1),
),
child: Row(
children: [
const Icon(
Icons.medical_services_outlined,
size: 20,
color: AppColors.textSecondary,
),
const SizedBox(width: 12),
Expanded(
child: Text(
name ?? '请选择医生',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: name != null
? AppColors.textPrimary
: AppColors.textHint,
@@ -452,10 +610,11 @@ class _SmsButton extends StatelessWidget {
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: disabled ? AppColors.cardInner : AppColors.primary,
borderRadius: BorderRadius.circular(12),
gradient: disabled ? null : AppColors.doctorGradient,
color: disabled ? AppColors.cardInner : null,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: disabled ? AppColors.border : AppColors.primary,
color: disabled ? AppColors.border : Colors.transparent,
),
),
child: Text(
@@ -485,12 +644,12 @@ class _Agreement extends StatelessWidget {
return GestureDetector(
onTap: onTap,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 18,
height: 18,
margin: const EdgeInsets.only(right: 6),
margin: const EdgeInsets.only(right: 7, top: 1),
decoration: BoxDecoration(
color: agreed ? AppColors.primary : Colors.white,
borderRadius: BorderRadius.circular(5),
@@ -554,6 +713,11 @@ class _NoticeBanner extends StatelessWidget {
decoration: BoxDecoration(
color: success ? AppColors.successLight : AppColors.errorLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: success
? AppColors.success.withValues(alpha: 0.16)
: AppColors.error.withValues(alpha: 0.16),
),
),
child: Row(
children: [