835 lines
25 KiB
Dart
835 lines
25 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import '../theme/app_colors.dart';
|
|
import '../widgets/gradient_button.dart';
|
|
|
|
class LoginPage extends StatefulWidget {
|
|
const LoginPage({super.key});
|
|
|
|
@override
|
|
State<LoginPage> createState() => _LoginPageState();
|
|
}
|
|
|
|
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
|
// State
|
|
bool _agreed = false;
|
|
bool _isLoading = false;
|
|
bool _showSmsView = false;
|
|
|
|
// SMS Login State
|
|
final TextEditingController _phoneController = TextEditingController();
|
|
final TextEditingController _codeController = TextEditingController();
|
|
int _countdown = 0;
|
|
Timer? _countdownTimer;
|
|
bool _isSmsSubmitting = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_phoneController.dispose();
|
|
_codeController.dispose();
|
|
_countdownTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
// ========== Agreement Dialog ==========
|
|
void _showAgreementDialog({required String action}) {
|
|
showDialog(
|
|
context: context,
|
|
barrierColor: Colors.black.withOpacity(0.5),
|
|
builder: (context) => _buildAgreementModal(action),
|
|
);
|
|
}
|
|
|
|
Widget _buildAgreementModal(String action) {
|
|
return Dialog(
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
child: Container(
|
|
width: 320,
|
|
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Title
|
|
Text(
|
|
'服务协议',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Content
|
|
Text.rich(
|
|
TextSpan(
|
|
text: '请先阅读并同意',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
color: const Color(0xFF6B7280),
|
|
height: 1.6,
|
|
),
|
|
children: [
|
|
TextSpan(
|
|
text: '《用户协议》',
|
|
style: TextStyle(fontFamily: 'Inter', color: const Color(0xFF6366F1)),
|
|
),
|
|
const TextSpan(text: '和'),
|
|
TextSpan(
|
|
text: '《隐私政策》',
|
|
style: TextStyle(fontFamily: 'Inter', color: const Color(0xFF6366F1)),
|
|
),
|
|
const TextSpan(text: ',以便为您提供更好的服务。'),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 24),
|
|
// Buttons
|
|
Row(
|
|
children: [
|
|
// Cancel
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => Navigator.pop(context),
|
|
child: Container(
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF3F4F6),
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
'再想想',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF6B7280),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// Confirm
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() => _agreed = true);
|
|
Navigator.pop(context);
|
|
if (action == 'oneclick') {
|
|
_doOneClickLogin();
|
|
} else if (action == 'sms') {
|
|
setState(() => _showSmsView = true);
|
|
}
|
|
},
|
|
child: Container(
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
|
|
),
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
'同意并继续',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ========== One-Click Login ==========
|
|
void _handleOneClickLogin() {
|
|
if (!_agreed) {
|
|
_showAgreementDialog(action: 'oneclick');
|
|
return;
|
|
}
|
|
_doOneClickLogin();
|
|
}
|
|
|
|
void _doOneClickLogin() {
|
|
setState(() => _isLoading = true);
|
|
_showToast('正在获取本机号码...');
|
|
|
|
Future.delayed(const Duration(milliseconds: 1500), () {
|
|
if (mounted) {
|
|
_showToast('登录成功');
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
Navigator.of(context).pushReplacementNamed('/home');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ========== SMS Login ==========
|
|
void _handleSmsLinkTap() {
|
|
if (!_agreed) {
|
|
_showAgreementDialog(action: 'sms');
|
|
return;
|
|
}
|
|
setState(() => _showSmsView = true);
|
|
}
|
|
|
|
bool _isValidPhone(String phone) {
|
|
return RegExp(r'^1[3-9]\d{9}$').hasMatch(phone);
|
|
}
|
|
|
|
bool get _canSubmitSms {
|
|
return _isValidPhone(_phoneController.text) &&
|
|
_codeController.text.length == 6;
|
|
}
|
|
|
|
void _sendCode() {
|
|
if (!_isValidPhone(_phoneController.text)) {
|
|
_showToast('请输入正确的手机号');
|
|
return;
|
|
}
|
|
|
|
setState(() => _countdown = 60);
|
|
_showToast('验证码已发送');
|
|
|
|
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
if (_countdown <= 1) {
|
|
timer.cancel();
|
|
if (mounted) setState(() => _countdown = 0);
|
|
} else {
|
|
if (mounted) setState(() => _countdown--);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _submitSmsLogin() {
|
|
if (!_canSubmitSms) return;
|
|
|
|
setState(() => _isSmsSubmitting = true);
|
|
|
|
Future.delayed(const Duration(milliseconds: 1500), () {
|
|
if (mounted) {
|
|
_showToast('登录成功');
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
if (mounted) {
|
|
setState(() => _isSmsSubmitting = false);
|
|
Navigator.of(context).pushReplacementNamed('/home');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ========== Toast ==========
|
|
void _showToast(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
duration: const Duration(seconds: 2),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ========== Build ==========
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
resizeToAvoidBottomInset: true,
|
|
body: Stack(
|
|
children: [
|
|
// Background
|
|
_buildGradientBackground(),
|
|
// Main Login View
|
|
_buildMainLoginView(),
|
|
// SMS View (overlay)
|
|
if (_showSmsView) _buildSmsView(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ========== Gradient Background ==========
|
|
Widget _buildGradientBackground() {
|
|
final size = MediaQuery.of(context).size;
|
|
return Positioned.fill(
|
|
child: Stack(
|
|
children: [
|
|
// Layer 1 - Pink (bottom-left)
|
|
Positioned(
|
|
bottom: -size.width * 0.5,
|
|
left: -size.width * 0.5,
|
|
width: size.width * 2,
|
|
height: size.width * 2,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
const Color(0xFFFFC8DC).withOpacity(0.6),
|
|
Colors.transparent,
|
|
],
|
|
radius: 0.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Layer 2 - Cyan (top-right)
|
|
Positioned(
|
|
top: -size.width * 0.5,
|
|
right: -size.width * 0.5,
|
|
width: size.width * 2,
|
|
height: size.width * 2,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
const Color(0xFFB4F0F0).withOpacity(0.5),
|
|
Colors.transparent,
|
|
],
|
|
radius: 0.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Layer 3 - Lavender (center)
|
|
Positioned(
|
|
top: size.height * 0.2,
|
|
left: size.width * 0.1,
|
|
width: size.width * 1.2,
|
|
height: size.width * 1.2,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
const Color(0xFFE6D2FA).withOpacity(0.45),
|
|
Colors.transparent,
|
|
],
|
|
radius: 0.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ========== Main Login View ==========
|
|
Widget _buildMainLoginView() {
|
|
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
|
final topPadding = MediaQuery.of(context).padding.top;
|
|
|
|
return SafeArea(
|
|
child: Column(
|
|
children: [
|
|
// Logo - padding-top: calc(env(safe-area-inset-top) + 60px)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 60),
|
|
child: Text(
|
|
'Airhub',
|
|
style: GoogleFonts.pressStart2p(
|
|
fontSize: 26,
|
|
color: const Color(0xFF4B2E83),
|
|
letterSpacing: 2,
|
|
shadows: [
|
|
Shadow(
|
|
offset: const Offset(0, 2),
|
|
blurRadius: 10,
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.3),
|
|
),
|
|
Shadow(
|
|
offset: const Offset(0, 0),
|
|
blurRadius: 40,
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.15),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Mascot - flex: 1, centered
|
|
Expanded(child: Center(child: _FloatingMascot())),
|
|
|
|
// Bottom Form
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(32, 0, 32, bottomPadding + 40),
|
|
child: Column(
|
|
children: [
|
|
// Primary Button - height: 56px, border-radius: 28px
|
|
GradientButton(
|
|
text: '本机号码一键登录',
|
|
onPressed: _handleOneClickLogin,
|
|
isLoading: _isLoading,
|
|
height: 56,
|
|
),
|
|
|
|
// SMS Link - margin-top: 20px, font-size: 14px
|
|
const SizedBox(height: 20),
|
|
GestureDetector(
|
|
onTap: _handleSmsLinkTap,
|
|
child: Text(
|
|
'使用验证码登录',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
color: const Color(0xFF4B2E83).withOpacity(0.7),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Agreement - margin-top: 28px
|
|
const SizedBox(height: 28),
|
|
_buildAgreementCheckbox(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ========== Agreement Checkbox ==========
|
|
Widget _buildAgreementCheckbox() {
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _agreed = !_agreed),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Checkbox - 18x18, border-radius: 5px
|
|
Container(
|
|
width: 18,
|
|
height: 18,
|
|
margin: const EdgeInsets.only(top: 1), // Fine-tune alignment
|
|
decoration: BoxDecoration(
|
|
gradient: _agreed
|
|
? const LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
|
|
)
|
|
: null,
|
|
color: _agreed ? null : const Color(0x99FFFFFF),
|
|
borderRadius: BorderRadius.circular(5),
|
|
border: Border.all(
|
|
color: _agreed
|
|
? Colors.transparent
|
|
: const Color(0xFF4B2E83).withOpacity(0.3),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: _agreed
|
|
? const Center(
|
|
child: Text(
|
|
'✓',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
),
|
|
const SizedBox(width: 10), // gap: 10px
|
|
// Text - font-size: 12px, line-height: 1.6
|
|
Flexible(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
text: '我已阅读并同意',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
color: const Color(0xFF4B2E83).withOpacity(0.6),
|
|
height: 1.6,
|
|
),
|
|
children: [
|
|
TextSpan(
|
|
text: '《用户协议》',
|
|
style: TextStyle(fontFamily: 'Inter', color: const Color(0xFF6366F1)),
|
|
),
|
|
const TextSpan(text: '和'),
|
|
TextSpan(
|
|
text: '《隐私政策》',
|
|
style: TextStyle(fontFamily: 'Inter', color: const Color(0xFF6366F1)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ========== SMS View ==========
|
|
Widget _buildSmsView() {
|
|
final topPadding = MediaQuery.of(context).padding.top;
|
|
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
|
|
|
return Positioned.fill(
|
|
child: Container(
|
|
color: Colors.white,
|
|
child: Stack(
|
|
children: [
|
|
// Background
|
|
_buildGradientBackground(),
|
|
// Content
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header - padding-top: 60px (fixed)
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 60, 24, 16),
|
|
child: _buildBackButton(),
|
|
),
|
|
|
|
// Body
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.fromLTRB(
|
|
32,
|
|
60,
|
|
32,
|
|
bottomPadding + 40,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Heading - font-size: 32px, font-weight: 700
|
|
Text(
|
|
'欢迎使用 Airhub',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.w700,
|
|
color: const Color(0xFF4B2E83),
|
|
letterSpacing: -0.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Subheading - font-size: 15px
|
|
Text(
|
|
'请输入您的手机号验证登录',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w400,
|
|
color: const Color(0xFF4B2E83).withOpacity(0.6),
|
|
),
|
|
),
|
|
const SizedBox(height: 48),
|
|
|
|
// Phone Input
|
|
_buildPhoneInput(),
|
|
const SizedBox(height: 24),
|
|
|
|
// Code Input
|
|
_buildCodeInput(),
|
|
const SizedBox(height: 48),
|
|
|
|
// Submit Button - height: 60px, border-radius: 30px
|
|
_buildSmsSubmitButton(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBackButton() {
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _showSmsView = false),
|
|
child: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: const Color(0x66FFFFFF),
|
|
border: Border.all(color: const Color(0x99FFFFFF)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
offset: const Offset(0, 4),
|
|
blurRadius: 12,
|
|
),
|
|
],
|
|
),
|
|
child: const Center(
|
|
child: Icon(Icons.arrow_back, size: 22, color: Color(0xFF4B2E83)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPhoneInput() {
|
|
return Container(
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0x8CFFFFFF),
|
|
border: Border.all(color: const Color(0xCCFFFFFF)),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.03),
|
|
offset: const Offset(0, 2),
|
|
blurRadius: 10,
|
|
),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Row(
|
|
children: [
|
|
// Prefix
|
|
Container(
|
|
padding: const EdgeInsets.only(right: 16),
|
|
margin: const EdgeInsets.only(right: 16),
|
|
decoration: const BoxDecoration(
|
|
border: Border(
|
|
right: BorderSide(color: Color(0x1A4B2E83), width: 1),
|
|
),
|
|
),
|
|
child: Text(
|
|
'+86',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: const Color(0xFF4B2E83),
|
|
),
|
|
),
|
|
),
|
|
// Input
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _phoneController,
|
|
keyboardType: TextInputType.phone,
|
|
maxLength: 11,
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
decoration: InputDecoration(
|
|
border: InputBorder.none,
|
|
hintText: '请输入手机号',
|
|
hintStyle: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w400,
|
|
color: const Color(0xFF4B2E83).withOpacity(0.35),
|
|
),
|
|
counterText: '',
|
|
),
|
|
cursorColor: const Color(0xFF8B5CF6),
|
|
onChanged: (_) => setState(() {}),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCodeInput() {
|
|
return Container(
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0x8CFFFFFF),
|
|
border: Border.all(color: const Color(0xCCFFFFFF)),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.03),
|
|
offset: const Offset(0, 2),
|
|
blurRadius: 10,
|
|
),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Row(
|
|
children: [
|
|
// Input
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _codeController,
|
|
keyboardType: TextInputType.number,
|
|
maxLength: 6,
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
decoration: InputDecoration(
|
|
border: InputBorder.none,
|
|
hintText: '输入验证码',
|
|
hintStyle: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w400,
|
|
color: const Color(0xFF4B2E83).withOpacity(0.35),
|
|
),
|
|
counterText: '',
|
|
),
|
|
cursorColor: const Color(0xFF8B5CF6),
|
|
onChanged: (_) => setState(() {}),
|
|
),
|
|
),
|
|
// Send Button
|
|
Container(
|
|
padding: const EdgeInsets.only(left: 14),
|
|
margin: const EdgeInsets.only(left: 10),
|
|
decoration: const BoxDecoration(
|
|
border: Border(
|
|
left: BorderSide(color: Color(0x1A4B2E83), width: 1),
|
|
),
|
|
),
|
|
child: GestureDetector(
|
|
onTap: _countdown > 0 ? null : _sendCode,
|
|
child: Text(
|
|
_countdown > 0 ? '${_countdown}s' : '获取验证码',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: _countdown > 0
|
|
? const Color(0xFF9CA3AF)
|
|
: const Color(0xFF6366F1),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSmsSubmitButton() {
|
|
final bool enabled = _canSubmitSms && !_isSmsSubmitting;
|
|
|
|
return GestureDetector(
|
|
onTap: enabled ? _submitSmsLogin : null,
|
|
child: AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 200),
|
|
opacity: enabled ? 1.0 : 0.6,
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
gradient: AppColors.btnPrimaryGradient,
|
|
borderRadius: BorderRadius.circular(30),
|
|
boxShadow: enabled
|
|
? [
|
|
BoxShadow(
|
|
color: const Color(0xFF6366F1).withOpacity(0.3),
|
|
offset: const Offset(0, 10),
|
|
blurRadius: 30,
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
alignment: Alignment.center,
|
|
child: _isSmsSubmitting
|
|
? const SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2.5,
|
|
),
|
|
)
|
|
: Text(
|
|
'立即登录',
|
|
style: TextStyle(fontFamily: 'Inter',
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ========== Floating Mascot Widget ==========
|
|
class _FloatingMascot extends StatefulWidget {
|
|
@override
|
|
State<_FloatingMascot> createState() => _FloatingMascotState();
|
|
}
|
|
|
|
class _FloatingMascotState extends State<_FloatingMascot>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(seconds: 5),
|
|
vsync: this,
|
|
)..repeat(reverse: true);
|
|
|
|
_animation = Tween<double>(
|
|
begin: 0,
|
|
end: -15,
|
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Transform.translate(
|
|
offset: Offset(0, _animation.value),
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
width: 220,
|
|
height: 220,
|
|
decoration: BoxDecoration(
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.25),
|
|
offset: const Offset(0, 20),
|
|
blurRadius: 40,
|
|
),
|
|
],
|
|
),
|
|
child: Image.asset(
|
|
'assets/www/mascot.png',
|
|
width: 220,
|
|
height: 220,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|