rtc_prd/airhub_app/lib/pages/login_page.dart
2026-02-05 11:46:42 +08:00

833 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: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: const Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
// Content
Text.rich(
TextSpan(
text: '请先阅读并同意',
style: GoogleFonts.inter(
fontSize: 14,
color: const Color(0xFF6B7280),
height: 1.6,
),
children: [
TextSpan(
text: '《用户协议》',
style: GoogleFonts.inter(color: const Color(0xFF6366F1)),
),
const TextSpan(text: ''),
TextSpan(
text: '《隐私政策》',
style: GoogleFonts.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: GoogleFonts.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: GoogleFonts.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) + 20px)
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
'Airhub',
style: GoogleFonts.pressStart2p(
fontSize: 26, // Exact match
color: const Color(0xFF4B2E83),
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: GoogleFonts.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: GoogleFonts.inter(
fontSize: 12,
color: const Color(0xFF4B2E83).withOpacity(0.6),
height: 1.6,
),
children: [
TextSpan(
text: '《用户协议》',
style: GoogleFonts.inter(color: const Color(0xFF6366F1)),
),
const TextSpan(text: ''),
TextSpan(
text: '《隐私政策》',
style: GoogleFonts.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: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w700,
color: const Color(0xFF4B2E83),
letterSpacing: -0.5,
),
),
const SizedBox(height: 12),
// Subheading - font-size: 15px
Text(
'请输入您的手机号验证登录',
style: GoogleFonts.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: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: const Color(0xFF4B2E83),
),
),
),
// Input
Expanded(
child: TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
maxLength: 11,
style: GoogleFonts.inter(
fontSize: 17,
fontWeight: FontWeight.w500,
color: const Color(0xFF1F2937),
),
decoration: InputDecoration(
border: InputBorder.none,
hintText: '请输入手机号',
hintStyle: GoogleFonts.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: GoogleFonts.inter(
fontSize: 17,
fontWeight: FontWeight.w500,
color: const Color(0xFF1F2937),
),
decoration: InputDecoration(
border: InputBorder.none,
hintText: '输入验证码',
hintStyle: GoogleFonts.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: GoogleFonts.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: GoogleFonts.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,
),
),
);
}
}