Files
monisuo/flutter_monisuo/lib/ui/pages/auth/login_page.dart
sion 1793fb727e 统一弹窗风格:Material Design 3 规范,消除颜色不一致
- 所有 AlertDialog 替换为 ModernDialog
- ConfirmDialog/AssetDialogs 去掉 GlassPanel,统一 surfaceContainer 背景
- 按钮统一 FilledButton + TextButton
- 修复 import 路径
2026-04-16 11:47:17 +08:00

307 lines
8.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_theme_extension.dart';
import '../../../providers/auth_provider.dart';
import '../../components/material_input.dart';
import '../../shared/modern_dialog.dart';
import '../main/main_page.dart';
import 'register_page.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
static const _loadingIndicatorSize = 16.0;
static const _logoCircleSize = 80.0;
static const _buttonHeight = 52.0;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: context.colors.surface,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: AppSpacing.xxl,
),
child: Form(
key: _formKey,
child: Column(
children: [
// 頂部品牌區域
_buildBrandSection(),
const SizedBox(height: AppSpacing.xxl),
// 表單區域
_buildFormSection(),
const SizedBox(height: AppSpacing.xl),
// 底部註冊鏈接
_buildRegisterRow(),
],
),
),
),
),
);
}
// ============================================
// 品牌區域 - Logo + 品牌名 + 標語
// ============================================
Widget _buildBrandSection() {
return Column(
children: [
// Logo 圓形:漸變 #1F2937 → #374151內含 "M"
Container(
width: _logoCircleSize,
height: _logoCircleSize,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColorScheme.darkSurfaceContainerHigh, AppColorScheme.darkOutline],
),
),
alignment: Alignment.center,
child: Text(
'M',
style: AppTextStyles.displayLarge(context).copyWith(
fontSize: 32,
fontWeight: FontWeight.w800,
color: context.colors.onPrimary,
),
),
),
const SizedBox(height: AppSpacing.md),
// 品牌名 "MONISUO"
Text(
'MONISUO',
style: AppTextStyles.displayLarge(context).copyWith(
letterSpacing: 3,
color: context.colors.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
// 標語
Text(
'虛擬貨幣模擬交易平臺',
style: AppTextStyles.bodyLarge(context).copyWith(
color: context.colors.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
);
}
// ============================================
// 表單區域 - 用戶名 + 密碼 + 登錄按鈕
// ============================================
Widget _buildFormSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildUsernameField(),
const SizedBox(height: AppSpacing.md),
_buildPasswordField(),
const SizedBox(height: AppSpacing.sm),
_buildLoginButton(),
],
);
}
Widget _buildUsernameField() {
return MaterialInput(
controller: _usernameController,
labelText: '用戶名',
hintText: '請輸入用戶名',
prefixIcon: Icons.person_outline,
validator: _validateUsername,
);
}
Widget _buildPasswordField() {
return MaterialPasswordInput(
controller: _passwordController,
labelText: '密碼',
hintText: '請輸入密碼',
prefixIcon: Icons.lock_outline,
validator: _validatePassword,
);
}
Widget _buildLoginButton() {
// 設計稿: accent-primary = light:#1F2937 / dark:#D4AF37
final buttonColor = context.appColors.accentPrimary;
final textColor = context.colors.onPrimary;
return Consumer<AuthProvider>(
builder: (context, auth, _) {
return SizedBox(
height: _buttonHeight,
child: ElevatedButton(
onPressed: auth.isLoading ? null : () => _handleLogin(auth),
style: ElevatedButton.styleFrom(
backgroundColor: buttonColor,
foregroundColor: textColor,
disabledBackgroundColor: buttonColor.withValues(alpha: 0.5),
disabledForegroundColor: textColor.withValues(alpha: 0.5),
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: AppSpacing.md,
),
),
child: auth.isLoading
? SizedBox.square(
dimension: _loadingIndicatorSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: textColor,
),
)
: Text(
'登錄',
style: AppTextStyles.headlineLarge(context).copyWith(
fontWeight: FontWeight.w700,
color: textColor,
),
),
),
);
},
);
}
// ============================================
// 底部註冊鏈接
// ============================================
Widget _buildRegisterRow() {
// gold-accent: light=#F59E0B / dark=#D4AF37
final goldColor = context.appColors.accentPrimary;
final secondaryTextColor = context.colors.onSurfaceVariant;
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'還沒有賬戶?',
style: AppTextStyles.bodyLarge(context).copyWith(
color: secondaryTextColor,
),
),
const SizedBox(width: AppSpacing.xs),
GestureDetector(
onTap: _navigateToRegister,
child: Text(
'立即註冊',
style: AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w600,
color: goldColor,
),
),
),
],
),
);
}
// ============================================
// Validators
// ============================================
String? _validateUsername(String? value) {
if (value == null || value.isEmpty) {
return '請輸入用戶名';
}
if (value.length < 3) {
return '用戶名至少 3 個字符';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return '請輸入密碼';
}
if (value.length < 6) {
return '密碼至少 6 個字符';
}
return null;
}
// ============================================
// Actions
// ============================================
Future<void> _handleLogin(AuthProvider auth) async {
if (!_formKey.currentState!.validate()) return;
final response = await auth.login(
_usernameController.text.trim(),
_passwordController.text,
);
if (!mounted) return;
if (response.success) {
_navigateToMainPage();
} else {
_showErrorDialog(response.message ?? '用戶名或密碼錯誤');
}
}
void _navigateToMainPage() {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const MainPage()),
(route) => false,
);
}
void _navigateToRegister() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const RegisterPage()),
);
}
void _showErrorDialog(String message) {
ModernDialog.info(
context: context,
title: '登錄失敗',
description: message,
);
}
}