2026-01-11 19:42:22 +08:00
|
|
|
|
import 'package:cc_ui_kit/cc_ui_kit.dart';
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
|
|
import 'package:go_router/go_router.dart';
|
2026-03-05 10:24:38 +08:00
|
|
|
|
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
|
|
|
|
|
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
2026-08-18 08:52:33 +08:00
|
|
|
|
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
2026-03-05 10:24:38 +08:00
|
|
|
|
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
2026-01-11 19:42:22 +08:00
|
|
|
|
|
|
|
|
|
|
import '../../../../core/router/route_paths.dart';
|
|
|
|
|
|
import '../bloc/login_cubit.dart';
|
|
|
|
|
|
import '../bloc/login_state.dart';
|
|
|
|
|
|
|
2026-01-18 20:21:14 +08:00
|
|
|
|
class LoginPage extends StatefulWidget {
|
|
|
|
|
|
const LoginPage({super.key});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<LoginPage> createState() => _LoginPageState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin {
|
2026-01-11 19:42:22 +08:00
|
|
|
|
final _userCtrl = TextEditingController();
|
|
|
|
|
|
final _pwdCtrl = TextEditingController();
|
2026-01-18 20:21:14 +08:00
|
|
|
|
bool _isAgreed = false; // 协议勾选状态
|
|
|
|
|
|
bool _obscurePwd = true; // 密码可见性
|
2026-01-11 19:42:22 +08:00
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
// 进场动画控制器
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 20:21:14 +08:00
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
2026-09-23 16:48:48 +08:00
|
|
|
|
_entranceController.dispose();
|
2026-01-18 20:21:14 +08:00
|
|
|
|
_userCtrl.dispose();
|
|
|
|
|
|
_pwdCtrl.dispose();
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
2026-01-11 19:42:22 +08:00
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
/// 生成一个错峰区间的动画(0.0 ~ 1.0)
|
|
|
|
|
|
Animation<double> _interval(double begin, double end) {
|
|
|
|
|
|
return CurvedAnimation(
|
|
|
|
|
|
parent: _entranceController,
|
|
|
|
|
|
curve: Interval(begin, end, curve: Curves.easeOut),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 19:42:22 +08:00
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return Scaffold(
|
2026-08-18 08:52:33 +08:00
|
|
|
|
backgroundColor: context.appColors.pageBackground,
|
2026-01-18 20:21:14 +08:00
|
|
|
|
// 保持 AppBar 简洁或直接去掉
|
2026-08-18 08:52:33 +08:00
|
|
|
|
appBar: AppBar(backgroundColor: context.appColors.pageBackground, elevation: 0),
|
2026-01-11 19:42:22 +08:00
|
|
|
|
body: BlocListener<LoginCubit, LoginState>(
|
|
|
|
|
|
listener: (context, state) {
|
2026-05-29 14:13:34 +08:00
|
|
|
|
debugPrint('🔍 [LOGIN] 登录状态变化: $state');
|
2026-01-11 19:42:22 +08:00
|
|
|
|
if (state is LoginSuccess) {
|
2026-05-29 14:13:34 +08:00
|
|
|
|
debugPrint('✅ [LOGIN] 登录成功,准备跳转到首页');
|
2026-05-13 10:34:07 +08:00
|
|
|
|
context.go(RoutePaths.home);
|
2026-05-29 14:13:34 +08:00
|
|
|
|
debugPrint('✅ [LOGIN] 已调用 context.go(RoutePaths.home)');
|
2026-01-18 20:21:14 +08:00
|
|
|
|
} else if (state is LoginFailure) {
|
|
|
|
|
|
// 可以在这里弹出简洁的黑白提示框
|
2026-03-05 10:24:38 +08:00
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败,${state.message}')));
|
2026-01-11 19:42:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-05-25 12:58:29 +08:00
|
|
|
|
child: LayoutBuilder(
|
|
|
|
|
|
builder: (context, constraints) {
|
|
|
|
|
|
return SingleChildScrollView(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
|
|
minHeight: constraints.maxHeight,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: IntrinsicHeight(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// 顶部区域:图片 + 认证信息 + 标题(压缩空间)
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
// ① 顶部居中图片和认证信息(缩放淡入)
|
|
|
|
|
|
_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,
|
|
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-09-23 16:48:48 +08:00
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-05-19 08:34:35 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-09-23 16:48:48 +08:00
|
|
|
|
|
2026-05-25 12:58:29 +08:00
|
|
|
|
const SizedBox(height: 16),
|
2026-09-23 16:48:48 +08:00
|
|
|
|
|
|
|
|
|
|
// ② 标题区(上滑淡入)
|
|
|
|
|
|
_Entrance(
|
|
|
|
|
|
animation: _interval(0.10, 0.45),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
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)),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
),
|
2026-09-23 16:48:48 +08:00
|
|
|
|
|
2026-05-25 12:58:29 +08:00
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
// ③ 表单区域
|
|
|
|
|
|
// 账号输入框(上滑淡入)
|
|
|
|
|
|
_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: "请输入用户名"),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
2026-05-25 12:58:29 +08:00
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
// 密码输入框(上滑淡入)
|
|
|
|
|
|
_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),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-05-25 12:58:29 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
2026-05-25 12:58:29 +08:00
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
// ④ 协议勾选区域(上滑淡入)
|
|
|
|
|
|
_Entrance(
|
|
|
|
|
|
animation: _interval(0.36, 0.71),
|
|
|
|
|
|
child: _buildProtocolSection(),
|
|
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
// ⑤ 底部区域:登录按钮(上滑淡入)
|
|
|
|
|
|
_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 ? '登录中...' : '登 录',
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
2026-09-23 16:48:48 +08:00
|
|
|
|
// 注册链接(上滑淡入,最后登场)
|
|
|
|
|
|
_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,
|
|
|
|
|
|
),
|
2026-04-20 08:54:27 +08:00
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-09-23 16:48:48 +08:00
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-05-13 10:34:07 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-05-13 10:34:07 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-25 12:58:29 +08:00
|
|
|
|
);
|
|
|
|
|
|
},
|
2026-01-18 20:21:14 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 执行登录逻辑
|
|
|
|
|
|
void _handleLogin() {
|
|
|
|
|
|
context.read<LoginCubit>().login(_userCtrl.text, _pwdCtrl.text, 2);
|
2026-03-05 10:24:38 +08:00
|
|
|
|
//final user = sl<AppUserCubit>().state.user;
|
|
|
|
|
|
//final devicesCubit = sl<DevicesCubit>();
|
|
|
|
|
|
//if (user != null) {
|
|
|
|
|
|
// devicesCubit.fetchAllDevices(user.username);
|
|
|
|
|
|
//}
|
2026-01-18 20:21:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 辅助组件:输入框标签
|
|
|
|
|
|
Widget _buildInputLabel(String label) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
2026-08-18 08:52:33 +08:00
|
|
|
|
child: Text(label, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: context.appColors.textSecondary)),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 辅助组件:输入框装饰
|
|
|
|
|
|
InputDecoration _inputDecoration({required String hint, Widget? suffixIcon}) {
|
|
|
|
|
|
return InputDecoration(
|
|
|
|
|
|
hintText: hint,
|
2026-08-18 08:52:33 +08:00
|
|
|
|
hintStyle: TextStyle(color: context.appColors.textTertiary, fontSize: 14),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
filled: true,
|
2026-08-18 08:52:33 +08:00
|
|
|
|
fillColor: context.appColors.fillBackground,
|
2026-01-18 20:21:14 +08:00
|
|
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
|
|
|
|
|
enabledBorder: OutlineInputBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
2026-08-18 08:52:33 +08:00
|
|
|
|
borderSide: BorderSide(color: context.appColors.divider),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
),
|
|
|
|
|
|
focusedBorder: OutlineInputBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
2026-08-18 08:52:33 +08:00
|
|
|
|
borderSide: BorderSide(color: context.appColors.primary, width: 1),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
),
|
|
|
|
|
|
suffixIcon: suffixIcon,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 辅助组件:协议勾选
|
|
|
|
|
|
Widget _buildProtocolSection() {
|
|
|
|
|
|
return Row(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: 24,
|
|
|
|
|
|
width: 24,
|
|
|
|
|
|
child: Checkbox(
|
|
|
|
|
|
value: _isAgreed,
|
2026-08-18 08:52:33 +08:00
|
|
|
|
activeColor: context.appColors.primary,
|
|
|
|
|
|
checkColor: Colors.white,
|
|
|
|
|
|
side: BorderSide(color: context.appColors.divider),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
onChanged: (val) => setState(() => _isAgreed = val!),
|
2026-03-05 10:24:38 +08:00
|
|
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Wrap(
|
|
|
|
|
|
children: [
|
2026-08-18 08:52:33 +08:00
|
|
|
|
Text("我已阅读并同意 ", style: TextStyle(fontSize: 13, color: context.appColors.textTertiary)),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
_protocolText("《用户协议》", () => _showProtocolDetail("用户协议")),
|
2026-08-18 08:52:33 +08:00
|
|
|
|
Text(" 和 ", style: TextStyle(fontSize: 13, color: context.appColors.textTertiary)),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
_protocolText("《隐私政策》", () => _showProtocolDetail("隐私政策")),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _protocolText(String text, VoidCallback onTap) {
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: onTap,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
text,
|
2026-08-18 08:52:33 +08:00
|
|
|
|
style: TextStyle(fontSize: 13, color: context.appColors.textPrimary, fontWeight: FontWeight.bold),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _showProtocolDetail(String title) {
|
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
isScrollControlled: true,
|
2026-08-18 08:52:33 +08:00
|
|
|
|
backgroundColor: context.appColors.cardBackground,
|
2026-03-05 10:24:38 +08:00
|
|
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
builder: (context) => Container(
|
|
|
|
|
|
padding: const EdgeInsets.all(24),
|
|
|
|
|
|
height: MediaQuery.of(context).size.height * 0.7,
|
2026-01-11 19:42:22 +08:00
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
2026-08-18 08:52:33 +08:00
|
|
|
|
Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: context.appColors.textPrimary)),
|
|
|
|
|
|
Divider(height: 32, color: context.appColors.divider),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
Expanded(
|
|
|
|
|
|
child: SingleChildScrollView(
|
2026-08-18 08:52:33 +08:00
|
|
|
|
child: Text("此处放置您的${title}详细内容...\n" * 20, style: TextStyle(color: context.appColors.textSecondary, height: 1.5)),
|
2026-01-18 20:21:14 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
child: BlocBuilder<LoginCubit, LoginState>(
|
|
|
|
|
|
builder: (context, state) {
|
2026-03-05 10:24:38 +08:00
|
|
|
|
return CCPrimaryButton(onPressed: () => Navigator.pop(context), text: "我已了解");
|
2026-01-18 20:21:14 +08:00
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-01-11 19:42:22 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-23 16:48:48 +08:00
|
|
|
|
|
|
|
|
|
|
/// 进场动画包装组件:淡入 + 上滑(默认)或淡入 + 缩放(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),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|