feat(auth): 优化登录页布局与字段反馈
This commit is contained in:
1 parent
e44fbdf6f2
commit
8a96f341c8
12 files changed
+355
-119
No files matched your search
+118
-57
@@ -33,6 +33,13 @@ const double _glowOpacity = 0.18;
|
||||
const double _errorOpacity = 0.10;
|
||||
const double _errorBorderOpacity = 0.28;
|
||||
const double _enterSlideDistance = 16;
|
||||
const double _topContentPadding = 64;
|
||||
const double _bottomContentPadding = 28;
|
||||
const double _compactTopContentPadding = 48;
|
||||
const double _compactHeightBreakpoint = 720;
|
||||
const double _compactBrandGap = 16;
|
||||
const double _warmGlowSize = 260;
|
||||
const double _warmGlowOpacity = 0.06;
|
||||
const Duration _enterDuration = Duration(milliseconds: 220);
|
||||
const Duration _segmentDuration = Duration(milliseconds: 160);
|
||||
|
||||
@@ -89,6 +96,7 @@ class _AuthPageState extends State<AuthPage> {
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_logic.busy) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final invalid = _logic.validate();
|
||||
if (invalid != null) {
|
||||
@@ -117,8 +125,10 @@ class _AuthPageState extends State<AuthPage> {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).maybePop();
|
||||
} on ApiException catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() => _logic.errorMessage = error.message);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _logic.errorMessage = l10n.authNetworkError);
|
||||
} finally {
|
||||
if (mounted) setState(() => _logic.busy = false);
|
||||
@@ -158,76 +168,120 @@ class _AuthPageState extends State<AuthPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: AutofillGroup(
|
||||
child: ListView(
|
||||
key: const Key('auth-scroll'),
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
12,
|
||||
AppDimens.pageHorizontal,
|
||||
28 + MediaQuery.viewInsetsOf(context).bottom,
|
||||
Positioned(
|
||||
top: 72,
|
||||
right: -80,
|
||||
width: _warmGlowSize,
|
||||
height: _warmGlowSize,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
key: const Key('auth-background-warm-glow'),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColors.gold.withValues(alpha: _warmGlowOpacity),
|
||||
AppColors.gold.withValues(alpha: 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Center(
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact =
|
||||
constraints.maxHeight < _compactHeightBreakpoint;
|
||||
final top = compact
|
||||
? _compactTopContentPadding
|
||||
: _topContentPadding;
|
||||
final bottom =
|
||||
_bottomContentPadding +
|
||||
MediaQuery.viewInsetsOf(context).bottom;
|
||||
final available = constraints.maxHeight - top - bottom;
|
||||
final minHeight = available > 0 ? available : 0.0;
|
||||
return AutofillGroup(
|
||||
child: SingleChildScrollView(
|
||||
key: const Key('auth-scroll'),
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
top,
|
||||
AppDimens.pageHorizontal,
|
||||
bottom,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: _contentMaxWidth,
|
||||
),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
key: const Key('auth-enter-animation'),
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: _enterDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, child) => Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
_enterSlideDistance * (1 - value),
|
||||
),
|
||||
child: child,
|
||||
constraints: BoxConstraints(minHeight: minHeight),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: _contentMaxWidth,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (Navigator.of(context).canPop())
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: IconButton(
|
||||
key: const Key('auth-back'),
|
||||
tooltip: MaterialLocalizations.of(
|
||||
context,
|
||||
).backButtonTooltip,
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(),
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_rounded,
|
||||
color: AppColors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
_AuthBrand(tagline: l10n.authBrandTagline),
|
||||
const SizedBox(height: 28),
|
||||
_authCard(l10n),
|
||||
],
|
||||
child: _content(l10n, compact: compact),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (Navigator.of(context).canPop())
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4, top: 4),
|
||||
child: IconButton(
|
||||
key: const Key('auth-back'),
|
||||
tooltip: MaterialLocalizations.of(
|
||||
context,
|
||||
).backButtonTooltip,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_rounded,
|
||||
color: AppColors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _content(AppLocalizations l10n, {required bool compact}) =>
|
||||
TweenAnimationBuilder<double>(
|
||||
key: const Key('auth-enter-animation'),
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: _enterDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, child) => Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _enterSlideDistance * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_AuthBrand(tagline: l10n.authBrandTagline, compact: compact),
|
||||
SizedBox(height: compact ? _compactBrandGap : 28),
|
||||
_authCard(l10n),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _authCard(AppLocalizations l10n) => _AuthCard(
|
||||
state: _AuthCardState(
|
||||
l10n: l10n,
|
||||
@@ -235,6 +289,7 @@ class _AuthPageState extends State<AuthPage> {
|
||||
signUpMode: _logic.signUpMode,
|
||||
busy: _logic.busy,
|
||||
errorMessage: _logic.errorMessage,
|
||||
fieldError: _logic.fieldError,
|
||||
obscurePassword: _obscurePassword,
|
||||
),
|
||||
fields: _AuthFieldControllers(
|
||||
@@ -246,8 +301,14 @@ class _AuthPageState extends State<AuthPage> {
|
||||
),
|
||||
callbacks: _AuthCardCallbacks(
|
||||
onModeSelected: _selectMode,
|
||||
onEmailChanged: (value) => _logic.email = value,
|
||||
onPasswordChanged: (value) => _logic.password = value,
|
||||
onEmailChanged: (value) => setState(() {
|
||||
_logic.email = value;
|
||||
_logic.clearFieldError();
|
||||
}),
|
||||
onPasswordChanged: (value) => setState(() {
|
||||
_logic.password = value;
|
||||
_logic.clearFieldError();
|
||||
}),
|
||||
onTogglePasswordVisibility: () =>
|
||||
setState(() => _obscurePassword = !_obscurePassword),
|
||||
onSubmit: _submit,
|
||||
|
||||
Reference in new issue
Block a user