启动动画和登录页面的动画
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -18,19 +18,47 @@ class LoginPage extends StatefulWidget {
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
class _LoginPageState extends State<LoginPage> 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<double> _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<LoginPage> {
|
||||
// 顶部区域:图片 + 认证信息 + 标题(压缩空间)
|
||||
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<LoginCubit, LoginState>(
|
||||
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<LoginPage> {
|
||||
),
|
||||
),
|
||||
|
||||
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<LoginCubit, LoginState>(
|
||||
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<LoginPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 进场动画包装组件:淡入 + 上滑(默认)或淡入 + 缩放(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<double> 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<double>(begin: 0.85, end: 1.0).animate(curved);
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(scale: scaleAnim, child: child),
|
||||
);
|
||||
}
|
||||
|
||||
final slideAnim = Tween<Offset>(begin: slideOffset, end: Offset.zero).animate(curved);
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(position: slideAnim, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RouteBase>
|
||||
static List<RouteBase> routes = [
|
||||
GoRoute(
|
||||
path: RoutePaths.splash,
|
||||
builder: (context, state) => const SplashPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutePaths.login,
|
||||
builder: (context, state) => const LoginPage(),
|
||||
|
||||
Reference in New Issue
Block a user