feat: 切换邮箱认证并重构前后端启动与门禁

This commit is contained in:
qzl
2026-04-02 18:39:35 +08:00
parent 92cdfd9fca
commit 31594558eb
116 changed files with 5608 additions and 628 deletions
+125
View File
@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import '../core/auth/session_store.dart';
import '../data/network/api_client.dart';
import '../data/storage/local_kv_store.dart';
import '../features/auth/data/apis/auth_api.dart';
import '../features/auth/data/repositories/auth_repository.dart';
import '../features/auth/presentation/bloc/auth_bloc.dart';
import '../features/auth/presentation/bloc/auth_state.dart';
import '../features/auth/presentation/screens/login_screen.dart';
import '../features/home/presentation/screens/home_screen.dart';
import '../l10n/app_localizations.dart';
import '../shared/widgets/app_loading_indicator.dart';
import 'app_theme.dart';
import 'di/injection.dart';
class EryaoApp extends StatefulWidget {
const EryaoApp({super.key});
@override
State<EryaoApp> createState() => _EryaoAppState();
}
class _EryaoAppState extends State<EryaoApp> {
final SessionStore _sessionStore = SessionStore(LocalKvStore());
late final AuthBloc _authBloc;
Locale _locale = const Locale('zh');
@override
void initState() {
super.initState();
final apiClient = ApiClient(
baseUrl: appDependencies.backendUrl,
tokenProvider: _sessionStore.getToken,
onUnauthorized: () {
return _authBloc.handleUnauthorized401();
},
);
final authApi = AuthApi(apiClient: apiClient);
final authRepository = AuthRepositoryImpl(
authApi: authApi,
sessionStore: _sessionStore,
);
_authBloc = AuthBloc(repository: authRepository);
_bootstrap();
}
@override
void dispose() {
_authBloc.dispose();
super.dispose();
}
Future<void> _bootstrap() async {
final localeCode = await _sessionStore.getLocaleCode();
if (mounted) {
setState(() {
_locale = localeCode == 'en' ? const Locale('en') : const Locale('zh');
});
}
await _authBloc.start();
}
Future<void> _handleLocaleChanged(Locale locale) async {
await _sessionStore.saveLocaleCode(locale.languageCode);
if (!mounted) {
return;
}
setState(() {
_locale = locale;
});
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _authBloc,
builder: (context, _) {
return MaterialApp(
debugShowCheckedModeBanner: false,
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
locale: _locale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
theme: AppTheme.light(),
home: _buildHomeByAuthState(_authBloc.state),
);
},
);
}
Widget _buildHomeByAuthState(AuthState state) {
if (state.status == AuthStatus.initial ||
state.status == AuthStatus.loading) {
return const Scaffold(
body: Center(
child: AppLoadingIndicator(variant: AppLoadingVariant.surface),
),
);
}
if (state.status == AuthStatus.authenticated && state.user != null) {
return HomeScreen(
account: state.user!.email,
sessionStore: _sessionStore,
onLogout: _authBloc.logout,
);
}
return LoginScreen(
currentLocale: _locale,
onLocaleChanged: _handleLocaleChanged,
onRequestOtp: _authBloc.sendOtp,
onLoginWithOtp: (email, otp) {
return _authBloc.loginWithOtp(email: email, otp: otp);
},
);
}
}
+76
View File
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import '../shared/theme/app_color_palette.dart';
class AppTheme {
static const Color _primary = Color(0xFF673AB7);
static const Color _accent = Color(0xFF9C27B0);
static const Color _scaffold = Color(0xFFF8F8F8);
static const Color _textHigh = Color(0xFF333333);
static const Color _textMid = Color(0xFF666666);
static const Color _textLow = Color(0xFF999999);
static ThemeData light() {
const colorScheme = ColorScheme.light(
primary: _primary,
onPrimary: Color(0xFFFFFFFF),
secondary: _accent,
onSecondary: Color(0xFFFFFFFF),
surface: Color(0xFFFFFFFF),
onSurface: _textHigh,
error: Color(0xFFB00020),
onError: Color(0xFFFFFFFF),
outline: Color(0xFFDDDDDD),
surfaceContainerHighest: Color(0xFFF0E6FF),
surfaceContainerHigh: Color(0xFFF4F5F7),
surfaceContainerLow: Color(0xFFFAFAFA),
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: _scaffold,
textTheme: const TextTheme(
headlineMedium: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: _textHigh,
),
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: _textHigh,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _textHigh,
),
bodyLarge: TextStyle(fontSize: 16, color: _textMid),
bodyMedium: TextStyle(fontSize: 14, color: _textMid),
bodySmall: TextStyle(fontSize: 12, color: _textLow),
),
extensions: const <ThemeExtension<dynamic>>[
AppColorPalette(
accentPurple: _accent,
historyGoldBg: Color(0xFFFFF8E1),
historyGoldText: Color(0xFFFFB300),
historyBlueBg: Color(0xFFE6F7FF),
historyBlueText: Color(0xFF1890FF),
historyGrayBg: Color(0xFFF5F5F5),
historyGrayText: Color(0xFF9E9E9E),
categoryCareerBg: Color(0xFFF0E6FF),
categoryCareerText: Color(0xFF673AB7),
categoryLoveBg: Color(0xFFFFF3E0),
categoryLoveText: Color(0xFFFF9800),
categoryMoneyBg: Color(0xFFE8F5E9),
categoryMoneyText: Color(0xFF4CAF50),
notificationDot: Color(0xFFE53935),
warning: Color(0xFFF57C00),
warningContainer: Color(0xFFFFF3E0),
onWarningContainer: Color(0xFF8A4B00),
),
],
);
}
}
+17
View File
@@ -0,0 +1,17 @@
import '../../core/config/env.dart';
class AppDependencies {
const AppDependencies({required this.backendUrl});
final String backendUrl;
}
AppDependencies? _appDependencies;
AppDependencies get appDependencies {
return _appDependencies ?? AppDependencies(backendUrl: Env.backendUrl);
}
Future<void> configureDependencies() async {
_appDependencies = AppDependencies(backendUrl: Env.backendUrl);
}