youhua
This commit is contained in:
74
flutter_monisuo/lib/core/network/api_response.dart
Normal file
74
flutter_monisuo/lib/core/network/api_response.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
/// API 响应状态码
|
||||
class ResponseCode {
|
||||
static const String success = '0000';
|
||||
static const String unauthorized = '0002';
|
||||
}
|
||||
|
||||
/// API 响应模型
|
||||
class ApiResponse<T> {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final T? data;
|
||||
final String? code;
|
||||
|
||||
ApiResponse({
|
||||
required this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
this.code,
|
||||
});
|
||||
|
||||
factory ApiResponse.success(T data, [String? message]) {
|
||||
return ApiResponse(
|
||||
success: true,
|
||||
data: data,
|
||||
message: message,
|
||||
code: ResponseCode.success,
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.fail(String message, [String? code]) {
|
||||
return ApiResponse(
|
||||
success: false,
|
||||
message: message,
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.unauthorized(String message) {
|
||||
return ApiResponse(
|
||||
success: false,
|
||||
message: message,
|
||||
code: ResponseCode.unauthorized,
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(dynamic)? fromJsonT,
|
||||
) {
|
||||
final code = json['code'] as String? ?? '';
|
||||
final msg = json['msg'] as String? ?? '';
|
||||
|
||||
return switch (code) {
|
||||
ResponseCode.success => _parseSuccess(json, msg, fromJsonT),
|
||||
ResponseCode.unauthorized => ApiResponse.unauthorized(msg),
|
||||
_ => ApiResponse.fail(msg, code),
|
||||
};
|
||||
}
|
||||
|
||||
static ApiResponse<T> _parseSuccess<T>(
|
||||
Map<String, dynamic> json,
|
||||
String msg,
|
||||
T Function(dynamic)? fromJsonT,
|
||||
) {
|
||||
final data = json['data'];
|
||||
if (fromJsonT != null && data != null) {
|
||||
return ApiResponse.success(fromJsonT(data), msg);
|
||||
}
|
||||
return ApiResponse.success(data as T, msg);
|
||||
}
|
||||
|
||||
bool get isSuccess => success;
|
||||
bool get isUnauthorized => code == ResponseCode.unauthorized;
|
||||
}
|
||||
@@ -1,65 +1,13 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../storage/local_storage.dart';
|
||||
import 'api_exception.dart';
|
||||
import 'api_response.dart';
|
||||
|
||||
/// API 响应模型
|
||||
class ApiResponse<T> {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final T? data;
|
||||
final String? code;
|
||||
|
||||
ApiResponse({
|
||||
required this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
this.code,
|
||||
});
|
||||
|
||||
factory ApiResponse.success(T data, [String? message]) {
|
||||
return ApiResponse(
|
||||
success: true,
|
||||
data: data,
|
||||
message: message,
|
||||
code: '0000',
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.fail(String message, [String? code]) {
|
||||
return ApiResponse(
|
||||
success: false,
|
||||
message: message,
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.unauthorized(String message) {
|
||||
return ApiResponse(
|
||||
success: false,
|
||||
message: message,
|
||||
code: '0002',
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(dynamic)? fromJsonT,
|
||||
) {
|
||||
final code = json['code'] as String? ?? '';
|
||||
final msg = json['msg'] as String? ?? '';
|
||||
|
||||
if (code == '0000') {
|
||||
final data = json['data'];
|
||||
if (fromJsonT != null && data != null) {
|
||||
return ApiResponse.success(fromJsonT(data), msg);
|
||||
}
|
||||
return ApiResponse.success(data as T, msg);
|
||||
} else if (code == '0002') {
|
||||
return ApiResponse.unauthorized(msg);
|
||||
} else {
|
||||
return ApiResponse.fail(msg, code);
|
||||
}
|
||||
}
|
||||
/// 网络配置常量
|
||||
class NetworkConfig {
|
||||
static const String baseUrl = 'http://localhost:5010';
|
||||
static const Duration connectTimeout = Duration(seconds: 30);
|
||||
static const Duration receiveTimeout = Duration(seconds: 30);
|
||||
}
|
||||
|
||||
/// Dio 网络客户端
|
||||
@@ -67,23 +15,30 @@ class DioClient {
|
||||
late final Dio _dio;
|
||||
|
||||
DioClient() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: 'http://localhost:5010',
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
));
|
||||
_dio = _createDio();
|
||||
_setupInterceptors();
|
||||
}
|
||||
|
||||
_dio.interceptors.add(_AuthInterceptor());
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestHeader: false,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
Dio _createDio() {
|
||||
return Dio(BaseOptions(
|
||||
baseUrl: NetworkConfig.baseUrl,
|
||||
connectTimeout: NetworkConfig.connectTimeout,
|
||||
receiveTimeout: NetworkConfig.receiveTimeout,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
));
|
||||
}
|
||||
|
||||
void _setupInterceptors() {
|
||||
_dio.interceptors.addAll([
|
||||
_AuthInterceptor(),
|
||||
LogInterceptor(
|
||||
requestHeader: false,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// GET 请求
|
||||
Future<ApiResponse<T>> get<T>(
|
||||
String path, {
|
||||
@@ -112,7 +67,6 @@ class DioClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理响应
|
||||
ApiResponse<T> _handleResponse<T>(
|
||||
Response response,
|
||||
T Function(dynamic)? fromJson,
|
||||
@@ -124,13 +78,39 @@ class DioClient {
|
||||
return ApiResponse.fail('响应数据格式错误');
|
||||
}
|
||||
|
||||
/// 处理错误
|
||||
ApiResponse<T> _handleError<T>(DioException e) {
|
||||
if (e.response?.statusCode == 401) {
|
||||
LocalStorage.clearUserData();
|
||||
if (_isUnauthorized(e)) {
|
||||
_clearUserData();
|
||||
return ApiResponse.unauthorized('登录已过期,请重新登录');
|
||||
}
|
||||
return ApiResponse.fail(e.message ?? '网络请求失败');
|
||||
|
||||
final message = _getErrorMessage(e);
|
||||
return ApiResponse.fail(message);
|
||||
}
|
||||
|
||||
bool _isUnauthorized(DioException e) {
|
||||
return e.response?.statusCode == 401;
|
||||
}
|
||||
|
||||
void _clearUserData() {
|
||||
LocalStorage.clearUserData();
|
||||
}
|
||||
|
||||
String _getErrorMessage(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return '连接超时,请检查网络';
|
||||
case DioExceptionType.sendTimeout:
|
||||
return '发送超时,请重试';
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return '响应超时,请重试';
|
||||
case DioExceptionType.connectionError:
|
||||
return '网络连接失败';
|
||||
case DioExceptionType.badResponse:
|
||||
return '服务器错误 (${e.response?.statusCode})';
|
||||
default:
|
||||
return e.message ?? '网络请求失败';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +119,7 @@ class _AuthInterceptor extends Interceptor {
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
final token = LocalStorage.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
if (token?.isNotEmpty == true) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
super.onRequest(options, handler);
|
||||
|
||||
@@ -19,12 +19,9 @@ import 'ui/pages/main/main_page.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 禁用 Provider 类型检查
|
||||
Provider.debugCheckInvalidValueType = null;
|
||||
|
||||
// 初始化本地存储
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await SharedPreferences.getInstance();
|
||||
await LocalStorage.init();
|
||||
|
||||
runApp(const MyApp());
|
||||
@@ -36,84 +33,117 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
// 服务
|
||||
Provider<DioClient>(create: (_) => DioClient()),
|
||||
ProxyProvider<DioClient, UserService>(
|
||||
create: (_) => UserService(DioClient()),
|
||||
update: (_, client, previous) => previous ?? UserService(client),
|
||||
providers: _buildProviders(),
|
||||
child: AuthNavigator(
|
||||
child: ShadApp.custom(
|
||||
themeMode: ThemeMode.dark,
|
||||
darkTheme: ShadThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: const ShadSlateColorScheme.dark(),
|
||||
),
|
||||
appBuilder: _buildMaterialApp,
|
||||
),
|
||||
ProxyProvider<DioClient, MarketService>(
|
||||
create: (_) => MarketService(DioClient()),
|
||||
update: (_, client, previous) => previous ?? MarketService(client),
|
||||
),
|
||||
ProxyProvider<DioClient, TradeService>(
|
||||
create: (_) => TradeService(DioClient()),
|
||||
update: (_, client, previous) => previous ?? TradeService(client),
|
||||
),
|
||||
ProxyProvider<DioClient, AssetService>(
|
||||
create: (_) => AssetService(DioClient()),
|
||||
update: (_, client, previous) => previous ?? AssetService(client),
|
||||
),
|
||||
ProxyProvider<DioClient, FundService>(
|
||||
create: (_) => FundService(DioClient()),
|
||||
update: (_, client, previous) => previous ?? FundService(client),
|
||||
),
|
||||
// 状态管理
|
||||
ProxyProvider2<UserService, DioClient, AuthProvider>(
|
||||
create: (_) => AuthProvider(UserService(DioClient())),
|
||||
update: (_, userService, __, previous) =>
|
||||
previous ?? AuthProvider(userService),
|
||||
),
|
||||
ProxyProvider<MarketService, MarketProvider>(
|
||||
create: (_) => MarketProvider(MarketService(DioClient())),
|
||||
update: (_, service, previous) =>
|
||||
previous ?? MarketProvider(service),
|
||||
),
|
||||
ProxyProvider2<AssetService, FundService, AssetProvider>(
|
||||
create: (_) =>
|
||||
AssetProvider(AssetService(DioClient()), FundService(DioClient())),
|
||||
update: (_, assetService, fundService, previous) =>
|
||||
previous ?? AssetProvider(assetService, fundService),
|
||||
),
|
||||
],
|
||||
child: ShadApp.custom(
|
||||
themeMode: ThemeMode.dark,
|
||||
darkTheme: ShadThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: const ShadSlateColorScheme.dark(),
|
||||
),
|
||||
appBuilder: (context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: Theme.of(context),
|
||||
localizationsDelegates: const [
|
||||
GlobalShadLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
builder: (context, child) {
|
||||
return ShadAppBuilder(child: child!);
|
||||
},
|
||||
home: Consumer<AuthProvider>(
|
||||
builder: (context, auth, _) {
|
||||
if (auth.isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (auth.isLoggedIn) {
|
||||
return const MainPage();
|
||||
}
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<SingleChildWidget> _buildProviders() {
|
||||
final dioClient = DioClient();
|
||||
|
||||
return [
|
||||
// Services
|
||||
Provider<DioClient>.value(value: dioClient),
|
||||
Provider<UserService>(create: (_) => UserService(dioClient)),
|
||||
Provider<MarketService>(create: (_) => MarketService(dioClient)),
|
||||
Provider<TradeService>(create: (_) => TradeService(dioClient)),
|
||||
Provider<AssetService>(create: (_) => AssetService(dioClient)),
|
||||
Provider<FundService>(create: (_) => FundService(dioClient)),
|
||||
// State Management
|
||||
ChangeNotifierProvider<AuthProvider>(
|
||||
create: (ctx) => AuthProvider(ctx.read<UserService>()),
|
||||
),
|
||||
ChangeNotifierProvider<MarketProvider>(
|
||||
create: (ctx) => MarketProvider(ctx.read<MarketService>()),
|
||||
),
|
||||
ChangeNotifierProvider<AssetProvider>(
|
||||
create: (ctx) => AssetProvider(
|
||||
ctx.read<AssetService>(),
|
||||
ctx.read<FundService>(),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildMaterialApp(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: Theme.of(context),
|
||||
localizationsDelegates: const [
|
||||
GlobalShadLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
builder: (context, child) => ShadAppBuilder(child: child!),
|
||||
home: _buildHome(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHome() {
|
||||
return Consumer<AuthProvider>(
|
||||
builder: (context, auth, _) {
|
||||
if (auth.isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return auth.isLoggedIn ? const MainPage() : const LoginPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 认证路由守卫 - 监听认证状态并自动导航
|
||||
class AuthNavigator extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const AuthNavigator({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<AuthNavigator> createState() => _AuthNavigatorState();
|
||||
}
|
||||
|
||||
class _AuthNavigatorState extends State<AuthNavigator> {
|
||||
bool? _wasLoggedIn;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final isLoggedIn = context.watch<AuthProvider>().isLoggedIn;
|
||||
|
||||
if (_wasLoggedIn == null) {
|
||||
_wasLoggedIn = isLoggedIn;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_wasLoggedIn != isLoggedIn) {
|
||||
_wasLoggedIn = isLoggedIn;
|
||||
_navigateToAuthPage(isLoggedIn);
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToAuthPage(bool isLoggedIn) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => isLoggedIn ? const MainPage() : const LoginPage(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/network/dio_client.dart';
|
||||
import '../core/storage/local_storage.dart';
|
||||
@@ -15,7 +14,7 @@ class AuthProvider extends ChangeNotifier {
|
||||
String? _token;
|
||||
|
||||
AuthProvider(this._userService) {
|
||||
_checkAuth();
|
||||
_initAuth();
|
||||
}
|
||||
|
||||
// Getters
|
||||
@@ -24,100 +23,81 @@ class AuthProvider extends ChangeNotifier {
|
||||
bool get isLoading => _isLoading;
|
||||
String? get token => _token;
|
||||
|
||||
/// 检查登录状态
|
||||
Future<void> _checkAuth() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
/// 初始化认证状态
|
||||
Future<void> _initAuth() async {
|
||||
_token = LocalStorage.getToken();
|
||||
_isLoggedIn = _token != null && _token!.isNotEmpty;
|
||||
_isLoggedIn = _token?.isNotEmpty == true;
|
||||
|
||||
if (_isLoggedIn) {
|
||||
final userJson = LocalStorage.getUserInfo();
|
||||
if (userJson != null) {
|
||||
_user = User.fromJson(userJson);
|
||||
}
|
||||
_user = _loadUserFromStorage();
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
User? _loadUserFromStorage() {
|
||||
final userJson = LocalStorage.getUserInfo();
|
||||
return userJson != null ? User.fromJson(userJson) : null;
|
||||
}
|
||||
|
||||
/// 登录
|
||||
Future<ApiResponse<User>> login(String username, String password) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await _userService.login(username, password);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_token = response.data!['token'] as String?;
|
||||
final userJson = response.data!['user'] as Map<String, dynamic>? ??
|
||||
response.data!['userInfo'] as Map<String, dynamic>?;
|
||||
|
||||
if (_token != null) {
|
||||
await LocalStorage.saveToken(_token!);
|
||||
}
|
||||
if (userJson != null) {
|
||||
await LocalStorage.saveUserInfo(userJson);
|
||||
_user = User.fromJson(userJson);
|
||||
}
|
||||
|
||||
_isLoggedIn = true;
|
||||
notifyListeners();
|
||||
return ApiResponse.success(_user!, response.message);
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.fail(response.message ?? '登录失败');
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.fail('登录失败: $e');
|
||||
}
|
||||
Future<ApiResponse<User>> login(String username, String password) {
|
||||
return _authenticate(() => _userService.login(username, password));
|
||||
}
|
||||
|
||||
/// 注册
|
||||
Future<ApiResponse<User>> register(String username, String password) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
Future<ApiResponse<User>> register(String username, String password) {
|
||||
return _authenticate(() => _userService.register(username, password));
|
||||
}
|
||||
|
||||
/// 统一认证处理
|
||||
Future<ApiResponse<User>> _authenticate(
|
||||
Future<ApiResponse<Map<String, dynamic>>> Function() action,
|
||||
) async {
|
||||
_setLoading(true);
|
||||
|
||||
try {
|
||||
final response = await _userService.register(username, password);
|
||||
final response = await action();
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_token = response.data!['token'] as String?;
|
||||
final userJson = response.data!['userInfo'] as Map<String, dynamic>?;
|
||||
|
||||
if (_token != null) {
|
||||
await LocalStorage.saveToken(_token!);
|
||||
}
|
||||
if (userJson != null) {
|
||||
await LocalStorage.saveUserInfo(userJson);
|
||||
_user = User.fromJson(userJson);
|
||||
}
|
||||
|
||||
_isLoggedIn = true;
|
||||
notifyListeners();
|
||||
return ApiResponse.success(_user!, response.message);
|
||||
if (!response.success || response.data == null) {
|
||||
return ApiResponse.fail(response.message ?? '操作失败');
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.fail(response.message ?? '注册失败');
|
||||
return _handleAuthSuccess(response.data!, response.message);
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.fail('注册失败: $e');
|
||||
return ApiResponse.fail('操作失败: $e');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理认证成功
|
||||
ApiResponse<User> _handleAuthSuccess(
|
||||
Map<String, dynamic> data,
|
||||
String? message,
|
||||
) {
|
||||
_token = data['token'] as String?;
|
||||
final userJson = data['user'] as Map<String, dynamic>? ??
|
||||
data['userInfo'] as Map<String, dynamic>?;
|
||||
|
||||
if (_token != null) {
|
||||
LocalStorage.saveToken(_token!);
|
||||
}
|
||||
|
||||
if (userJson != null) {
|
||||
LocalStorage.saveUserInfo(userJson);
|
||||
_user = User.fromJson(userJson);
|
||||
}
|
||||
|
||||
_isLoggedIn = true;
|
||||
|
||||
return _user != null
|
||||
? ApiResponse.success(_user!, message)
|
||||
: ApiResponse.fail('用户信息获取失败');
|
||||
}
|
||||
|
||||
/// 退出登录
|
||||
Future<void> logout() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
_setLoading(true);
|
||||
|
||||
try {
|
||||
await _userService.logout();
|
||||
@@ -125,12 +105,15 @@ class AuthProvider extends ChangeNotifier {
|
||||
// 忽略退出登录的接口错误
|
||||
}
|
||||
|
||||
await LocalStorage.clearUserData();
|
||||
_clearAuthState();
|
||||
_setLoading(false);
|
||||
}
|
||||
|
||||
void _clearAuthState() {
|
||||
LocalStorage.clearUserData();
|
||||
_user = null;
|
||||
_token = null;
|
||||
_isLoggedIn = false;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 刷新用户信息
|
||||
@@ -148,4 +131,9 @@ class AuthProvider extends ChangeNotifier {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import '../main/main_page.dart';
|
||||
import 'register_page.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
@@ -15,6 +16,10 @@ class LoginPage extends StatefulWidget {
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
|
||||
static const _maxFormWidth = 400.0;
|
||||
static const _logoSize = 64.0;
|
||||
static const _loadingIndicatorSize = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
@@ -22,7 +27,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
constraints: const BoxConstraints(maxWidth: _maxFormWidth),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ShadForm(
|
||||
@@ -31,135 +36,15 @@ class _LoginPageState extends State<LoginPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo 和标题
|
||||
Icon(
|
||||
LucideIcons.trendingUp,
|
||||
size: 64,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'模拟所',
|
||||
style: theme.textTheme.h1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'虚拟货币模拟交易平台',
|
||||
style: theme.textTheme.muted,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
_buildHeader(theme),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// 用户名输入
|
||||
ShadInputFormField(
|
||||
id: 'username',
|
||||
label: const Text('用户名'),
|
||||
placeholder: const Text('请输入用户名'),
|
||||
leading: const Icon(LucideIcons.user),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入用户名';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return '用户名至少 3 个字符';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
_buildUsernameField(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 密码输入
|
||||
ShadInputFormField(
|
||||
id: 'password',
|
||||
label: const Text('密码'),
|
||||
placeholder: const Text('请输入密码'),
|
||||
obscureText: true,
|
||||
leading: const Icon(LucideIcons.lock),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入密码';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return '密码至少 6 个字符';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
_buildPasswordField(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 登录按钮
|
||||
Consumer<AuthProvider>(
|
||||
builder: (context, auth, _) {
|
||||
return ShadButton(
|
||||
onPressed: auth.isLoading
|
||||
? null
|
||||
: () async {
|
||||
if (formKey.currentState!.saveAndValidate()) {
|
||||
final values = formKey.currentState!.value;
|
||||
final response = await auth.login(
|
||||
values['username'],
|
||||
values['password'],
|
||||
);
|
||||
|
||||
// 登录成功后,Provider 会自动更新状态
|
||||
// MaterialApp 的 Consumer 会自动切换到 MainPage
|
||||
if (!response.success && mounted) {
|
||||
// 只在失败时显示错误
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog.alert(
|
||||
title: const Text('登录失败'),
|
||||
description: Text(
|
||||
response.message ?? '用户名或密码错误',
|
||||
),
|
||||
actions: [
|
||||
ShadButton(
|
||||
child: const Text('确定'),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: auth.isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('登录'),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildLoginButton(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 注册链接
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'还没有账号?',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
ShadButton.link(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RegisterPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('立即注册'),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildRegisterLink(theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -168,4 +53,154 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ShadThemeData theme) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.trendingUp,
|
||||
size: _logoSize,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'模拟所',
|
||||
style: theme.textTheme.h1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'虚拟货币模拟交易平台',
|
||||
style: theme.textTheme.muted,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUsernameField() {
|
||||
return ShadInputFormField(
|
||||
id: 'username',
|
||||
label: const Text('用户名'),
|
||||
placeholder: const Text('请输入用户名'),
|
||||
leading: const Icon(LucideIcons.user),
|
||||
validator: _validateUsername,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordField() {
|
||||
return ShadInputFormField(
|
||||
id: 'password',
|
||||
label: const Text('密码'),
|
||||
placeholder: const Text('请输入密码'),
|
||||
obscureText: true,
|
||||
leading: const Icon(LucideIcons.lock),
|
||||
validator: _validatePassword,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginButton() {
|
||||
return Consumer<AuthProvider>(
|
||||
builder: (context, auth, _) {
|
||||
return ShadButton(
|
||||
onPressed: auth.isLoading ? null : () => _handleLogin(auth),
|
||||
child: auth.isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: _loadingIndicatorSize,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('登录'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegisterLink(ShadThemeData theme) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'还没有账号?',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
ShadButton.link(
|
||||
onPressed: _navigateToRegister,
|
||||
child: const Text('立即注册'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 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: (_) => 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import '../auth/login_page.dart';
|
||||
|
||||
/// 我的页面 - 使用 shadcn_ui 现代化设计
|
||||
class MinePage extends StatefulWidget {
|
||||
@@ -275,8 +276,14 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
await auth.logout();
|
||||
// 登出成功,直接导航到登录页
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const LoginPage(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user