Updated the app's color scheme to implement a new "Slate" theme with refined dark and light variants. Changed background colors from #0A0E14 to #0B1120 for dark mode and updated surface layer colors to follow Material Design 3 specifications. Modified text colors and outline variants for better contrast and accessibility. Updated font sizes in transaction details screen from 11px to 12px for improved readability.
397 lines
11 KiB
Dart
397 lines
11 KiB
Dart
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 '../../../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;
|
||
|
||
/// 设计稿 radius-lg = 14
|
||
static const _designRadiusLg = 14.0;
|
||
|
||
@override
|
||
void dispose() {
|
||
_usernameController.dispose();
|
||
_passwordController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||
|
||
return Scaffold(
|
||
backgroundColor: isDark
|
||
? AppColorScheme.darkBackground
|
||
: AppColorScheme.lightSurface,
|
||
body: SafeArea(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.xl,
|
||
vertical: AppSpacing.xxl,
|
||
),
|
||
child: ShadForm(
|
||
key: formKey,
|
||
child: Column(
|
||
children: [
|
||
// 顶部品牌区域
|
||
_buildBrandSection(isDark),
|
||
const SizedBox(height: AppSpacing.xxl),
|
||
// 表单区域
|
||
_buildFormSection(isDark),
|
||
const SizedBox(height: AppSpacing.xl),
|
||
// 底部注册链接
|
||
_buildRegisterRow(isDark),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================
|
||
// 品牌区域 - Logo + 品牌名 + 标语
|
||
// ============================================
|
||
|
||
Widget _buildBrandSection(bool isDark) {
|
||
return Column(
|
||
children: [
|
||
// Logo 圆形:渐变 #1F2937 → #374151,内含 "M"
|
||
Container(
|
||
width: _logoCircleSize,
|
||
height: _logoCircleSize,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [Color(0xFF1F2937), Color(0xFF374151)],
|
||
),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(
|
||
'M',
|
||
style: TextStyle(
|
||
fontSize: 32,
|
||
fontWeight: FontWeight.w800,
|
||
color: isDark
|
||
? AppColorScheme.darkOnSurface
|
||
: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
// 品牌名 "MONISUO"
|
||
Text(
|
||
'MONISUO',
|
||
style: TextStyle(
|
||
fontSize: 28,
|
||
fontWeight: FontWeight.w800,
|
||
letterSpacing: 3,
|
||
color: isDark
|
||
? AppColorScheme.darkOnSurface
|
||
: AppColorScheme.lightOnSurface,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
// 标语
|
||
Text(
|
||
'虚拟货币模拟交易平台',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.normal,
|
||
color: isDark
|
||
? AppColorScheme.darkOnSurfaceVariant
|
||
: AppColorScheme.lightOnSurfaceVariant,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
// ============================================
|
||
// 表单区域 - 用户名 + 密码 + 登录按钮
|
||
// ============================================
|
||
|
||
Widget _buildFormSection(bool isDark) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
_buildUsernameField(isDark),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildPasswordField(isDark),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_buildLoginButton(isDark),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildUsernameField(bool isDark) {
|
||
final borderColor = isDark
|
||
? AppColorScheme.darkOutlineVariant
|
||
: AppColorScheme.lightOutlineVariant;
|
||
final cardColor = isDark
|
||
? AppColorScheme.darkSurfaceContainer
|
||
: AppColorScheme.lightSurfaceLowest;
|
||
final iconColor = isDark
|
||
? AppColorScheme.darkOnSurfaceMuted
|
||
: AppColorScheme.lightOnSurfaceMuted;
|
||
|
||
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: iconColor),
|
||
),
|
||
validator: _validateUsername,
|
||
controller: _usernameController,
|
||
decoration: ShadDecoration(
|
||
border: ShadBorder.all(
|
||
color: borderColor,
|
||
radius: BorderRadius.circular(_designRadiusLg),
|
||
),
|
||
),
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: isDark
|
||
? AppColorScheme.darkOnSurface
|
||
: AppColorScheme.lightOnSurface,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPasswordField(bool isDark) {
|
||
final borderColor = isDark
|
||
? AppColorScheme.darkOutlineVariant
|
||
: AppColorScheme.lightOutlineVariant;
|
||
final iconColor = isDark
|
||
? AppColorScheme.darkOnSurfaceMuted
|
||
: AppColorScheme.lightOnSurfaceMuted;
|
||
|
||
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: borderColor,
|
||
radius: BorderRadius.circular(_designRadiusLg),
|
||
),
|
||
),
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: isDark
|
||
? AppColorScheme.darkOnSurface
|
||
: AppColorScheme.lightOnSurface,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildLoginButton(bool isDark) {
|
||
// 设计稿: accent-primary = light:#1F2937 / dark:#D4AF37
|
||
final buttonColor = isDark
|
||
? AppColorScheme.darkSecondary
|
||
: const Color(0xFF1F2937);
|
||
final textColor = isDark
|
||
? AppColorScheme.darkBackground
|
||
: Colors.white;
|
||
|
||
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: BorderRadius.circular(_designRadiusLg),
|
||
),
|
||
),
|
||
child: auth.isLoading
|
||
? SizedBox.square(
|
||
dimension: _loadingIndicatorSize,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: textColor,
|
||
),
|
||
)
|
||
: Text(
|
||
'登录',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w700,
|
||
color: textColor,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
// ============================================
|
||
// 底部注册链接
|
||
// ============================================
|
||
|
||
Widget _buildRegisterRow(bool isDark) {
|
||
// gold-accent: light=#F59E0B / dark=#D4AF37
|
||
final goldColor = isDark
|
||
? AppColorScheme.darkSecondary
|
||
: const Color(0xFFF59E0B);
|
||
final secondaryTextColor = isDark
|
||
? AppColorScheme.darkOnSurfaceVariant
|
||
: AppColorScheme.lightOnSurfaceVariant;
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
'还没有账户?',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.normal,
|
||
color: secondaryTextColor,
|
||
),
|
||
),
|
||
const SizedBox(width: AppSpacing.xs),
|
||
GestureDetector(
|
||
onTap: _navigateToRegister,
|
||
child: Text(
|
||
'立即注册',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
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(),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|