feat(auth): 优化登录页布局与字段反馈

This commit is contained in:
Zhou Weidong committed 2026-10-19 10:33:33 +08:00
1 parent e44fbdf6f2
commit 8a96f341c8
12 files changed
+355 -119

No files matched your search

+109 -56
View File
@@ -10,21 +10,40 @@ class _AuthFields extends StatelessWidget {
@override
Widget build(BuildContext context) {
final error = state.status.fieldError;
final emailError =
error == AuthFieldError.emailRequired ||
error == AuthFieldError.emailInvalid;
final passwordError =
error == AuthFieldError.passwordRequired ||
error == AuthFieldError.passwordLength ||
error == AuthFieldError.passwordWeak;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_fieldLabel(state.l10n.authEmailLabel),
const SizedBox(height: 8),
_emailField(),
_emailField(context, hasError: emailError),
const SizedBox(height: 16),
_fieldLabel(state.l10n.authPasswordLabel),
const SizedBox(height: 8),
_passwordField(),
_passwordField(context, hasError: passwordError),
if (state.status.signUpMode) ...[
const SizedBox(height: 8),
Text(
state.l10n.authPasswordHint,
key: const Key('auth-password-rule'),
style: const TextStyle(
color: AppColors.muted,
fontSize: AppDimens.caption,
),
),
],
],
);
}
Widget _emailField() {
Widget _emailField(BuildContext context, {required bool hasError}) {
final enabled = !state.status.busy;
return ListenableBuilder(
listenable: state.fields.emailFocus,
@@ -43,16 +62,18 @@ class _AuthFields extends StatelessWidget {
onSubmitted: (_) => state.fields.passwordFocus.requestFocus(),
style: const TextStyle(color: AppColors.onPrimary, fontSize: 15),
decoration: _decoration(
context: context,
hint: 'name@example.com',
icon: Icons.mail_outline_rounded,
focused: state.fields.emailFocus.hasFocus,
hasError: hasError,
),
),
),
);
}
Widget _passwordField() {
Widget _passwordField(BuildContext context, {required bool hasError}) {
final enabled = !state.status.busy;
final l10n = state.l10n;
return ListenableBuilder(
@@ -72,9 +93,11 @@ class _AuthFields extends StatelessWidget {
onSubmitted: (_) => callbacks.onSubmit(),
style: const TextStyle(color: AppColors.onPrimary, fontSize: 15),
decoration: _decoration(
context: context,
hint: l10n.authPasswordHint,
icon: Icons.lock_outline_rounded,
focused: state.fields.passwordFocus.hasFocus,
hasError: hasError,
suffix: IconButton(
key: const Key('auth-password-visibility'),
constraints: const BoxConstraints.tightFor(
@@ -108,15 +131,22 @@ class _AuthFields extends StatelessWidget {
);
InputDecoration _decoration({
required BuildContext context,
required String hint,
required IconData icon,
required bool focused,
required bool hasError,
Widget? suffix,
}) {
final border = OutlineInputBorder(
borderRadius: BorderRadius.circular(_fieldRadius),
borderSide: BorderSide.none,
);
final errorColor = Theme.of(context).colorScheme.error;
final errorBorder = OutlineInputBorder(
borderRadius: BorderRadius.circular(_fieldRadius),
borderSide: BorderSide(color: errorColor, width: _fieldBorderWidth),
);
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14),
@@ -144,6 +174,8 @@ class _AuthFields extends StatelessWidget {
width: _fieldBorderWidth,
),
),
errorBorder: errorBorder,
focusedErrorBorder: errorBorder,
);
}
}
@@ -175,7 +207,10 @@ class _AuthSubmitButton extends StatelessWidget {
return AppColors.blue;
}),
foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary),
elevation: const WidgetStatePropertyAll(0),
elevation: const WidgetStatePropertyAll(2),
shadowColor: WidgetStatePropertyAll(
AppColors.blue.withValues(alpha: 0.22),
),
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
@@ -267,65 +302,83 @@ class _ModeSegment extends StatelessWidget {
}
class _AuthBrand extends StatelessWidget {
const _AuthBrand({required this.tagline});
const _AuthBrand({required this.tagline, required this.compact});
final String tagline;
final bool compact;
@override
Widget build(BuildContext context) {
return Row(
final copy = _brandCopy(context, centered: !compact);
if (compact) {
return Row(
key: const Key('auth-brand'),
children: [
_brandMark(),
const SizedBox(width: 14),
Expanded(child: copy),
],
);
}
return Column(
key: const Key('auth-brand'),
children: [
Container(
key: const Key('auth-brand-mark'),
width: _brandMarkSize,
height: _brandMarkSize,
decoration: BoxDecoration(
color: AppColors.blue,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: AppColors.blue.withValues(alpha: 0.32),
blurRadius: 24,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.movie_filter_rounded,
color: AppColors.onPrimary,
size: 26,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.appName,
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 23,
fontWeight: FontWeight.w800,
height: 1.1,
),
),
const SizedBox(height: 4),
Text(
tagline,
style: const TextStyle(
color: AppColors.blueSoft,
fontSize: 13,
height: 1.35,
),
),
],
),
),
],
mainAxisSize: MainAxisSize.min,
children: [_brandMark(), const SizedBox(height: 14), copy],
);
}
Widget _brandMark() => Container(
key: const Key('auth-brand-mark'),
width: _brandMarkSize,
height: _brandMarkSize,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: AppColors.blue.withValues(alpha: 0.32),
blurRadius: 24,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Image.asset(
'assets/icon/splash_mark.png',
width: _brandMarkSize,
height: _brandMarkSize,
fit: BoxFit.cover,
),
),
);
Widget _brandCopy(BuildContext context, {required bool centered}) => Column(
crossAxisAlignment: centered
? CrossAxisAlignment.center
: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.appName,
textAlign: centered ? TextAlign.center : TextAlign.start,
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 26,
fontWeight: FontWeight.w800,
height: 1.1,
),
),
const SizedBox(height: 6),
Text(
tagline,
textAlign: centered ? TextAlign.center : TextAlign.start,
style: const TextStyle(
color: AppColors.blueSoft,
fontSize: 14,
height: 1.35,
),
),
],
);
}
class _AuthError extends StatelessWidget {