From d230279f4fbe1938001701a1ef5c6327b2ed71a2 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Wed, 23 Sep 2026 16:48:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=8A=A8=E7=94=BB=E5=92=8C?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/router/app_router.dart | 8 +- .../auth/presentation/pages/login_page.dart | 331 ++++++++++++------ .../auth/presentation/routes/auth_routes.dart | 5 + 3 files changed, 229 insertions(+), 115 deletions(-) diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index fa500c85..b6192254 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -30,13 +30,19 @@ GoRouter createRouter(AuthCubit authCubit) { return GoRouter( navigatorKey: navigatorKey, // 🔥 绑定全局 navigatorKey,使异地登录弹窗能获取 context - initialLocation: RoutePaths.login, + initialLocation: RoutePaths.splash, refreshListenable: GoRouterRefreshStream(authCubit.stream), redirect: (context, state) { final loggedIn = authCubit.state is AuthAuthenticated; + final onSplash = state.matchedLocation == RoutePaths.splash; final loggingIn = state.matchedLocation == RoutePaths.login; final registering = state.matchedLocation == RoutePaths.register; + // 启动屏:交给 SplashPage 自己根据就绪信号跳转,路由层不干预 + if (onSplash) { + return null; + } + // 如果未登录,只允许访问 login 和 register 页面 if (!loggedIn && !loggingIn && !registering) { return RoutePaths.login; diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index d285ee67..d2a241b5 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -18,19 +18,47 @@ class LoginPage extends StatefulWidget { State createState() => _LoginPageState(); } -class _LoginPageState extends State { +class _LoginPageState extends State with SingleTickerProviderStateMixin { final _userCtrl = TextEditingController(); final _pwdCtrl = TextEditingController(); bool _isAgreed = false; // 协议勾选状态 bool _obscurePwd = true; // 密码可见性 + // 进场动画控制器 + late final AnimationController _entranceController; + + @override + void initState() { + super.initState(); + _entranceController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + ); + // 等首帧渲染 + 图片解码完成后再播放,避开启动初始化争抢导致的掉帧 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + Future.delayed(const Duration(milliseconds: 120), () { + if (mounted) _entranceController.forward(); + }); + }); + } + @override void dispose() { + _entranceController.dispose(); _userCtrl.dispose(); _pwdCtrl.dispose(); super.dispose(); } + /// 生成一个错峰区间的动画(0.0 ~ 1.0) + Animation _interval(double begin, double end) { + return CurvedAnimation( + parent: _entranceController, + curve: Interval(begin, end, curve: Curves.easeOut), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -64,130 +92,94 @@ class _LoginPageState extends State { // 顶部区域:图片 + 认证信息 + 标题(压缩空间) const SizedBox(height: 10), - // 顶部居中图片和认证信息 - Center( - child: Column( - children: [ - Image.asset( - 'assets/images/huawei.png', - width: 80, - height: 80, - fit: BoxFit.contain, - ), - const SizedBox(height: 6), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Text( - "通过华为云生态技术认证,请放心使用,证书编号C202605301", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 10, - color: context.appColors.textTertiary, - height: 1.3, + // ① 顶部居中图片和认证信息(缩放淡入) + _Entrance( + animation: _interval(0.00, 0.35), + scale: true, + child: Center( + child: Column( + children: [ + Image.asset( + 'assets/images/huawei.png', + width: 80, + height: 80, + fit: BoxFit.contain, + ), + const SizedBox(height: 6), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + "通过华为云生态技术认证,请放心使用,证书编号C202605301", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10, + color: context.appColors.textTertiary, + height: 1.3, + ), ), ), - ), - ], - ), - ), - - const SizedBox(height: 16), - Text( - "账号登录", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: context.appColors.textPrimary), - ), - const SizedBox(height: 8), - Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: context.appColors.textTertiary)), - const SizedBox(height: 24), - - // 表单区域 - // 账号输入框 - _buildInputLabel("账号"), - TextField( - controller: _userCtrl, - style: TextStyle(fontSize: 14, color: context.appColors.textPrimary), - cursorColor: context.appColors.textPrimary, - decoration: _inputDecoration(hint: "请输入用户名"), - ), - - const SizedBox(height: 20), - - // 密码输入框 - _buildInputLabel("密码"), - TextField( - controller: _pwdCtrl, - obscureText: _obscurePwd, - style: TextStyle(fontSize: 14, color: context.appColors.textPrimary), - cursorColor: context.appColors.textPrimary, - decoration: _inputDecoration( - hint: "请输入密码", - suffixIcon: IconButton( - icon: Icon(_obscurePwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: context.appColors.textTertiary, size: 20), - onPressed: () => setState(() => _obscurePwd = !_obscurePwd), + ], ), ), ), const SizedBox(height: 16), - // 协议勾选区域 - _buildProtocolSection(), - - const SizedBox(height: 30), - - // 底部区域:登录按钮 + 注册链接 - SizedBox( - width: double.infinity, - child: BlocBuilder( - builder: (context, state) { - bool isLoading = state is LoginLoading; - - return CCPrimaryButton( - onPressed: isLoading - ? null - : () { - if (!_isAgreed) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('请先阅读并同意用户协议'), - duration: Duration(seconds: 1), - ), - ); - return; - } - _handleLogin(); - }, - text: isLoading ? '登录中...' : '登 录', - ); - }, - ), - ), - const SizedBox(height: 16), - - Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + // ② 标题区(上滑淡入) + _Entrance( + animation: _interval(0.10, 0.45), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "没有账号吗?", - style: TextStyle(fontSize: 14, color: context.appColors.textTertiary), + "账号登录", + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: context.appColors.textPrimary), ), - InkWell( - onTap: () { - debugPrint(' 点击了立即注册'); - debugPrint('👉 跳转路径: ${RoutePaths.register}'); - context.go(RoutePaths.register); - }, - borderRadius: BorderRadius.circular(4), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Text( - "立即注册", - style: TextStyle( - fontSize: 14, - color: context.appColors.textPrimary, - fontWeight: FontWeight.bold, - ), + const SizedBox(height: 8), + Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: context.appColors.textTertiary)), + ], + ), + ), + + const SizedBox(height: 24), + + // ③ 表单区域 + // 账号输入框(上滑淡入) + _Entrance( + animation: _interval(0.20, 0.55), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInputLabel("账号"), + TextField( + controller: _userCtrl, + style: TextStyle(fontSize: 14, color: context.appColors.textPrimary), + cursorColor: context.appColors.textPrimary, + decoration: _inputDecoration(hint: "请输入用户名"), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // 密码输入框(上滑淡入) + _Entrance( + animation: _interval(0.28, 0.63), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInputLabel("密码"), + TextField( + controller: _pwdCtrl, + obscureText: _obscurePwd, + style: TextStyle(fontSize: 14, color: context.appColors.textPrimary), + cursorColor: context.appColors.textPrimary, + decoration: _inputDecoration( + hint: "请输入密码", + suffixIcon: IconButton( + icon: Icon(_obscurePwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: context.appColors.textTertiary, size: 20), + onPressed: () => setState(() => _obscurePwd = !_obscurePwd), ), ), ), @@ -195,6 +187,83 @@ class _LoginPageState extends State { ), ), + const SizedBox(height: 16), + + // ④ 协议勾选区域(上滑淡入) + _Entrance( + animation: _interval(0.36, 0.71), + child: _buildProtocolSection(), + ), + + const SizedBox(height: 30), + + // ⑤ 底部区域:登录按钮(上滑淡入) + _Entrance( + animation: _interval(0.45, 0.82), + child: SizedBox( + width: double.infinity, + child: BlocBuilder( + builder: (context, state) { + bool isLoading = state is LoginLoading; + + return CCPrimaryButton( + onPressed: isLoading + ? null + : () { + if (!_isAgreed) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('请先阅读并同意用户协议'), + duration: Duration(seconds: 1), + ), + ); + return; + } + _handleLogin(); + }, + text: isLoading ? '登录中...' : '登 录', + ); + }, + ), + ), + ), + const SizedBox(height: 16), + + // 注册链接(上滑淡入,最后登场) + _Entrance( + animation: _interval(0.55, 0.95), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "没有账号吗?", + style: TextStyle(fontSize: 14, color: context.appColors.textTertiary), + ), + InkWell( + onTap: () { + debugPrint(' 点击了立即注册'); + debugPrint('👉 跳转路径: ${RoutePaths.register}'); + context.go(RoutePaths.register); + }, + borderRadius: BorderRadius.circular(4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + "立即注册", + style: TextStyle( + fontSize: 14, + color: context.appColors.textPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), ], ), @@ -320,3 +389,37 @@ class _LoginPageState extends State { ); } } + +/// 进场动画包装组件:淡入 + 上滑(默认)或淡入 + 缩放(scale=true) +class _Entrance extends StatelessWidget { + const _Entrance({ + required this.animation, + required this.child, + this.scale = false, + this.slideOffset = const Offset(0, 0.15), + }); + + final Animation animation; + final Widget child; + final bool scale; + final Offset slideOffset; + + @override + Widget build(BuildContext context) { + final curved = CurvedAnimation(parent: animation, curve: Curves.easeOutCubic); + + if (scale) { + final scaleAnim = Tween(begin: 0.85, end: 1.0).animate(curved); + return FadeTransition( + opacity: animation, + child: ScaleTransition(scale: scaleAnim, child: child), + ); + } + + final slideAnim = Tween(begin: slideOffset, end: Offset.zero).animate(curved); + return FadeTransition( + opacity: animation, + child: SlideTransition(position: slideAnim, child: child), + ); + } +} diff --git a/lib/features/auth/presentation/routes/auth_routes.dart b/lib/features/auth/presentation/routes/auth_routes.dart index 8dc121d6..d0b72fda 100644 --- a/lib/features/auth/presentation/routes/auth_routes.dart +++ b/lib/features/auth/presentation/routes/auth_routes.dart @@ -3,10 +3,15 @@ import 'package:maibu_satabot_v2/core/router/route_paths.dart'; import 'package:maibu_satabot_v2/features/auth/presentation/pages/login_page.dart'; import '../pages/register_page.dart'; +import '../pages/splash_page.dart'; class AuthRoutes { // 返回一个 List static List routes = [ + GoRoute( + path: RoutePaths.splash, + builder: (context, state) => const SplashPage(), + ), GoRoute( path: RoutePaths.login, builder: (context, state) => const LoginPage(),