Files
monisuo/flutter_monisuo/lib/ui/pages/auth/login_page.dart
sion123 7ed2435a4c refactor(theme): 迁移主题感知颜色至 ThemeExtension
- 创建 AppThemeColors ThemeExtension 类,统一管理主题感知颜色(涨跌色、卡片背景、渐变等)
- 从 AppColorScheme 移除主题感知辅助函数,仅保留静态颜色常量
- 在 AppTheme 中注册 ThemeExtension,支持深色/浅色主题工厂
- 重构所有 UI 组件使用 context.appColors 访问主题颜色,替代硬编码的 AppColorScheme 方法调用
- 移除组件中重复的 isDark 判断逻辑,简化颜色获取方式
- 保持向后兼容性,所有现有功能不变
2026-04-06 01:58:08 +08:00

349 lines
9.9 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:shadcn_ui/shadcn_ui.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 '../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<ShadFormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
static const _loadingIndicatorSize = 16.0;
static const _logoCircleSize = 80.0;
static const _inputHeight = 52.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: ShadForm(
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 SizedBox(
height: _inputHeight,
child: ShadInputFormField(
id: 'username',
placeholder: const Text('请输入用户名'),
leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.user, size: 18, color: context.appColors.onSurfaceMuted),
),
validator: _validateUsername,
controller: _usernameController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: context.colors.outlineVariant,
radius: AppRadius.radiusLg,
),
),
style: AppTextStyles.headlineMedium(context).copyWith(
color: context.colors.onSurface,
),
),
);
}
Widget _buildPasswordField() {
final iconColor = context.appColors.onSurfaceMuted;
return SizedBox(
height: _inputHeight,
child: ShadInputFormField(
id: 'password',
placeholder: const Text('请输入密码'),
obscureText: _obscurePassword,
leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.lock, size: 18, color: iconColor),
),
trailing: GestureDetector(
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
child: Icon(
_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye,
size: 18,
color: iconColor,
),
),
validator: _validatePassword,
controller: _passwordController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: context.colors.outlineVariant,
radius: AppRadius.radiusLg,
),
),
style: AppTextStyles.headlineMedium(context).copyWith(
color: context.colors.onSurface,
),
),
);
}
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: ShadButton(
onPressed: auth.isLoading ? null : () => _handleLogin(auth),
backgroundColor: buttonColor,
foregroundColor: textColor,
decoration: ShadDecoration(
border: ShadBorder.all(
radius: AppRadius.radiusLg,
),
),
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!.saveAndValidate()) return;
final values = formKey.currentState!.value;
final response = await auth.login(
values['username'],
values['password'],
);
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) {
showShadDialog(
context: context,
builder: (context) => ShadDialog.alert(
title: const Text('登录失败'),
description: Text(message),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
}