Просмотр исходного кода

Merge branch 'develop' of afan/flutter_app_android into master

刘清 2 недель назад
Родитель
Сommit
58b652a51e

+ 3
- 0
.vscode/settings.json Просмотреть файл

1
+{
2
+    "cmake.ignoreCMakeListsMissing": true
3
+}

+ 2
- 0
lib/core/constants/api_constants.dart Просмотреть файл

8
   static const String login = '/api/v1/auth/login';
8
   static const String login = '/api/v1/auth/login';
9
   static const String register = '/api/v1/auth/register';
9
   static const String register = '/api/v1/auth/register';
10
   static const String logout = '/api/v1/auth/logout';
10
   static const String logout = '/api/v1/auth/logout';
11
+  static const String refresh = '/api/v1/auth/refresh';
11
   static const String getCurrentUser = '/api/v1/auth/me';
12
   static const String getCurrentUser = '/api/v1/auth/me';
12
   
13
   
13
   // 用户管理接口
14
   // 用户管理接口
18
   static String getLoginUrl() => '$baseUrl$login';
19
   static String getLoginUrl() => '$baseUrl$login';
19
   static String getRegisterUrl() => '$baseUrl$register';
20
   static String getRegisterUrl() => '$baseUrl$register';
20
   static String getLogoutUrl() => '$baseUrl$logout';
21
   static String getLogoutUrl() => '$baseUrl$logout';
22
+  static String getRefreshUrl() => '$baseUrl$refresh';
21
   static String getCurrentUserUrl() => '$baseUrl$getCurrentUser';
23
   static String getCurrentUserUrl() => '$baseUrl$getCurrentUser';
22
   static String getUserProfileUrl() => '$baseUrl$getUserProfile';
24
   static String getUserProfileUrl() => '$baseUrl$getUserProfile';
23
   static String getUpdateProfileUrl() => '$baseUrl$updateUserProfile';
25
   static String getUpdateProfileUrl() => '$baseUrl$updateUserProfile';

+ 16
- 6
lib/data/datasources/local/shared_prefs.dart Просмотреть файл

7
   SharedPrefs(this._prefs);
7
   SharedPrefs(this._prefs);
8
 
8
 
9
   // Token管理
9
   // Token管理
10
-  Future<bool> setAuthToken(String token) async {
11
-    return await _prefs.setString(AppConstants.authTokenKey, token);
10
+  Future<bool> setAccessToken(String token) async {
11
+    return await _prefs.setString('access_token', token);
12
   }
12
   }
13
 
13
 
14
-  String? getAuthToken() {
15
-    return _prefs.getString(AppConstants.authTokenKey);
14
+  Future<bool> setRefreshToken(String token) async {
15
+    return await _prefs.setString('refresh_token', token);
16
   }
16
   }
17
 
17
 
18
-  Future<bool> removeAuthToken() async {
19
-    return await _prefs.remove(AppConstants.authTokenKey);
18
+  String? getAccessToken() {
19
+    return _prefs.getString('access_token');
20
+  }
21
+
22
+  String? getRefreshToken() {
23
+    return _prefs.getString('refresh_token');
24
+  }
25
+
26
+  Future<bool> removeTokens() async {
27
+    await _prefs.remove('access_token');
28
+    await _prefs.remove('refresh_token');
29
+    return true;
20
   }
30
   }
21
 
31
 
22
   // 用户数据管理
32
   // 用户数据管理

+ 14
- 5
lib/data/datasources/remote/api_client.dart Просмотреть файл

78
     }
78
     }
79
   }
79
   }
80
   
80
   
81
-  // 保存token
82
-  Future<void> saveToken(String token) async {
83
-    await _prefs.setString('access_token', token);
81
+  // 保存tokens
82
+  Future<void> saveTokens(String accessToken, String? refreshToken) async {
83
+    await _prefs.setString('access_token', accessToken);
84
+    if (refreshToken != null && refreshToken.isNotEmpty) {
85
+      await _prefs.setString('refresh_token', refreshToken);
86
+    }
84
   }
87
   }
85
   
88
   
86
-  // 清除token
87
-  Future<void> clearToken() async {
89
+  // 清除所有token
90
+  Future<void> clearTokens() async {
88
     await _prefs.remove('access_token');
91
     await _prefs.remove('access_token');
92
+    await _prefs.remove('refresh_token');
93
+  }
94
+
95
+  // 获取refresh_token
96
+  String? getRefreshToken() {
97
+    return _prefs.getString('refresh_token');
89
   }
98
   }
90
   
99
   
91
   // 检查是否已登录
100
   // 检查是否已登录

+ 156
- 24
lib/data/repositories/auth_repository.dart Просмотреть файл

19
     required SharedPreferences prefs,
19
     required SharedPreferences prefs,
20
   }) : _apiClient = apiClient, _prefs = prefs;
20
   }) : _apiClient = apiClient, _prefs = prefs;
21
   
21
   
22
-  // 登录(需要使用安全登录替代)
22
+  // 登录(需要使用安全登录替代)保存双token
23
   Future<ApiResponse<User>> login(LoginRequest request) async {
23
   Future<ApiResponse<User>> login(LoginRequest request) async {
24
     try {
24
     try {
25
       final response = await _apiClient.post(
25
       final response = await _apiClient.post(
26
         ApiConstants.getLoginUrl(),
26
         ApiConstants.getLoginUrl(),
27
         request.toJson(),
27
         request.toJson(),
28
-        withAuth: false,
28
+        withAuth: false,  // 登录不需要认证头
29
       );
29
       );
30
       
30
       
31
       if (response.statusCode == 200) {
31
       if (response.statusCode == 200) {
33
           json.decode(response.body)
33
           json.decode(response.body)
34
         );
34
         );
35
         
35
         
36
-        // 保存token
37
-        await _apiClient.saveToken(tokenResponse.accessToken);
36
+        // 保存access_token和refresh_token
37
+        await _apiClient.saveTokens(
38
+          tokenResponse.accessToken,
39
+          tokenResponse.refreshToken,
40
+        );
41
+        
42
+        // 保存用户数据
43
+        await _prefs.setString(
44
+          'user_data',
45
+          json.encode(tokenResponse.user.toJson()),
46
+        );
38
         
47
         
39
         return ApiResponse<User>(
48
         return ApiResponse<User>(
40
           success: true,
49
           success: true,
56
     }
65
     }
57
   }
66
   }
58
   
67
   
59
-  // 注册(需要使用安全注册替代)
68
+  // 注册(需要使用安全注册替代)保存双token
60
   Future<ApiResponse<User>> register(RegisterRequest request) async {
69
   Future<ApiResponse<User>> register(RegisterRequest request) async {
61
     try {
70
     try {
62
       final response = await _apiClient.post(
71
       final response = await _apiClient.post(
70
           json.decode(response.body)
79
           json.decode(response.body)
71
         );
80
         );
72
         
81
         
73
-        // 保存token
74
-        await _apiClient.saveToken(tokenResponse.accessToken);
82
+        // 保存双token
83
+        await _apiClient.saveTokens(
84
+          tokenResponse.accessToken,
85
+          tokenResponse.refreshToken,
86
+        );
87
+        
88
+        // 保存用户数据
89
+        await _prefs.setString(
90
+          'user_data',
91
+          json.encode(tokenResponse.user.toJson()),
92
+        );
75
         
93
         
76
         return ApiResponse<User>(
94
         return ApiResponse<User>(
77
           success: true,
95
           success: true,
101
         username: request.username,
119
         username: request.username,
102
         password: request.password,
120
         password: request.password,
103
       );
121
       );
104
-
105
-      print(ApiConstants.getLoginUrl());
106
-      print(secureRequest.toJson());
107
       
122
       
108
       final response = await _apiClient.post(
123
       final response = await _apiClient.post(
109
         ApiConstants.getLoginUrl(),
124
         ApiConstants.getLoginUrl(),
110
         secureRequest.toJson(),
125
         secureRequest.toJson(),
111
         withAuth: false,
126
         withAuth: false,
112
       );
127
       );
113
-      print(response.statusCode);
114
       
128
       
115
       // ... 处理响应
129
       // ... 处理响应
116
       if (response.statusCode == 200) {
130
       if (response.statusCode == 200) {
118
           json.decode(response.body)
132
           json.decode(response.body)
119
         );
133
         );
120
         
134
         
121
-        // 保存token
122
-        await _apiClient.saveToken(tokenResponse.accessToken);
135
+        // 保存access_token和refresh_token
136
+        await _apiClient.saveTokens(
137
+          tokenResponse.accessToken,
138
+          tokenResponse.refreshToken,
139
+        );
140
+        
141
+        // 保存用户数据
142
+        await _prefs.setString(
143
+          'user_data',
144
+          json.encode(tokenResponse.user.toJson()),
145
+        );
123
         
146
         
124
         return ApiResponse<User>(
147
         return ApiResponse<User>(
125
           success: true,
148
           success: true,
169
           json.decode(response.body)
192
           json.decode(response.body)
170
         );
193
         );
171
         
194
         
172
-        // 保存token
173
-        await _apiClient.saveToken(tokenResponse.accessToken);
195
+        // 保存双token
196
+        await _apiClient.saveTokens(
197
+          tokenResponse.accessToken,
198
+          tokenResponse.refreshToken,
199
+        );
200
+        
201
+        // 保存用户数据
202
+        await _prefs.setString(
203
+          'user_data',
204
+          json.encode(tokenResponse.user.toJson()),
205
+        );
174
         
206
         
175
         return ApiResponse<User>(
207
         return ApiResponse<User>(
176
           success: true,
208
           success: true,
202
       
234
       
203
       if (response.statusCode == 200) {
235
       if (response.statusCode == 200) {
204
         final userData = json.decode(response.body);
236
         final userData = json.decode(response.body);
237
+        final user = User.fromJson(userData);
238
+        
239
+        // 更新本地用户数据
240
+        await _prefs.setString('user_data', json.encode(user.toJson()));
241
+        
205
         return ApiResponse<User>(
242
         return ApiResponse<User>(
206
           success: true,
243
           success: true,
207
           message: '获取成功',
244
           message: '获取成功',
208
-          data: User.fromJson(userData),
245
+          data: user,
209
         );
246
         );
247
+      } else if (response.statusCode == 401) {
248
+        // Token过期,尝试刷新
249
+        final refreshResult = await _refreshToken();
250
+        if (refreshResult) {
251
+          // 刷新成功,重新获取用户信息
252
+          return await getCurrentUser();
253
+        } else {
254
+          return ApiResponse<User>(
255
+            success: false,
256
+            message: '登录已过期,请重新登录',
257
+          );
258
+        }
210
       } else {
259
       } else {
211
         return ApiResponse<User>(
260
         return ApiResponse<User>(
212
           success: false,
261
           success: false,
220
       );
269
       );
221
     }
270
     }
222
   }
271
   }
223
-  
224
-  // 登出
225
-  Future<bool> logout() async {
272
+
273
+  // 刷新Token
274
+  Future<bool> _refreshToken() async {
226
     try {
275
     try {
276
+      final refreshToken = _apiClient.getRefreshToken();
277
+      
278
+      if (refreshToken == null || refreshToken.isEmpty) {
279
+        return false;
280
+      }
281
+      
227
       final response = await _apiClient.post(
282
       final response = await _apiClient.post(
228
-        ApiConstants.getLogoutUrl(),
229
-        {},
230
-        withAuth: true,
283
+        ApiConstants.getRefreshUrl(),  // 需要添加这个常量
284
+        {'refresh_token': refreshToken},
285
+        withAuth: false,
231
       );
286
       );
232
       
287
       
233
       if (response.statusCode == 200) {
288
       if (response.statusCode == 200) {
234
-        await _apiClient.clearToken();
289
+        final data = json.decode(response.body);
290
+        final newAccessToken = data['access_token'];
291
+        final newRefreshToken = data['refresh_token'];
292
+        
293
+        // 保存新的tokens
294
+        await _apiClient.saveTokens(newAccessToken, newRefreshToken);
235
         return true;
295
         return true;
236
       }
296
       }
297
+      
237
       return false;
298
       return false;
238
     } catch (e) {
299
     } catch (e) {
239
       return false;
300
       return false;
240
     }
301
     }
241
   }
302
   }
242
   
303
   
304
+  // 登出
305
+  Future<ApiResponse<bool>> logout() async {
306
+    try {
307
+      // 获取refresh_token
308
+      final refreshToken = _apiClient.getRefreshToken();
309
+      
310
+      if (refreshToken == null || refreshToken.isEmpty) {
311
+        // 没有refresh_token,只清除本地数据
312
+        await _clearLocalData();
313
+        return ApiResponse<bool>(
314
+          success: true,
315
+          message: '已退出登录',
316
+          data: true,
317
+        );
318
+      }
319
+      
320
+      // 发送登出请求到服务器
321
+      final response = await _apiClient.post(
322
+        ApiConstants.getLogoutUrl(),
323
+        {'refresh_token': refreshToken},
324
+        withAuth: true,  // 需要access_token认证头
325
+      );
326
+      
327
+      // 无论服务器响应如何,都清除本地数据
328
+      await _clearLocalData();
329
+      
330
+      if (response.statusCode == 200) {
331
+        return ApiResponse<bool>(
332
+          success: true,
333
+          message: '已成功退出登录',
334
+          data: true,
335
+        );
336
+      } else {
337
+        // 服务器登出失败,但本地已清除
338
+        return ApiResponse<bool>(
339
+          success: true,
340
+          message: '已退出登录(服务器通信失败)',
341
+          data: true,
342
+        );
343
+      }
344
+    } catch (e) {
345
+      // 网络异常,仍然清除本地数据
346
+      await _clearLocalData();
347
+      return ApiResponse<bool>(
348
+        success: true,
349
+        message: '已退出登录(网络异常)',
350
+        data: true,
351
+      );
352
+    }
353
+  }
354
+
355
+  // 清除本地数据
356
+  Future<void> _clearLocalData() async {
357
+    await _apiClient.clearTokens();
358
+    await _prefs.remove('user_data');
359
+  }
360
+  
243
   // 检查登录状态
361
   // 检查登录状态
244
   Future<bool> isLoggedIn() async {
362
   Future<bool> isLoggedIn() async {
245
-    return _apiClient.isLoggedIn();
363
+    // 检查是否有access_token
364
+    if (!_apiClient.isLoggedIn()) {
365
+      return false;
366
+    }
367
+    
368
+    // 可以进一步验证token是否有效
369
+    try {
370
+      final response = await _apiClient.get(
371
+        ApiConstants.getCurrentUserUrl(),
372
+        withAuth: true,
373
+      );
374
+      return response.statusCode == 200;
375
+    } catch (e) {
376
+      return false;
377
+    }
246
   }
378
   }
247
 }
379
 }

+ 4
- 3
lib/main.dart Просмотреть файл

1
 import 'package:flutter/material.dart';
1
 import 'package:flutter/material.dart';
2
 import 'package:provider/provider.dart';
2
 import 'package:provider/provider.dart';
3
+import 'core/constants/route_constants.dart';
3
 import 'presentation/providers/auth_provider.dart';
4
 import 'presentation/providers/auth_provider.dart';
4
 import 'presentation/providers/user_provider.dart';
5
 import 'presentation/providers/user_provider.dart';
5
 import 'presentation/navigation/app_router.dart';
6
 import 'presentation/navigation/app_router.dart';
7
 
8
 
8
 void main() async {
9
 void main() async {
9
   WidgetsFlutterBinding.ensureInitialized();
10
   WidgetsFlutterBinding.ensureInitialized();
10
-  await di.init();
11
+  await di.init();  // 初始化DI容器
11
   runApp(const MyApp());
12
   runApp(const MyApp());
12
 }
13
 }
13
 
14
 
23
         ChangeNotifierProvider(create: (_) => di.sl<UserProvider>()),
24
         ChangeNotifierProvider(create: (_) => di.sl<UserProvider>()),
24
       ],
25
       ],
25
       child: MaterialApp(
26
       child: MaterialApp(
26
-        title: 'Flutter App',
27
+        title: '中了么',
27
         theme: ThemeData(
28
         theme: ThemeData(
28
           primarySwatch: Colors.blue,
29
           primarySwatch: Colors.blue,
29
           // fontFamily: 'Roboto',
30
           // fontFamily: 'Roboto',
42
           ),
43
           ),
43
         ),
44
         ),
44
         debugShowCheckedModeBanner: false,
45
         debugShowCheckedModeBanner: false,
45
-        initialRoute: '/',
46
+        initialRoute: RouteConstants.splash,
46
         onGenerateRoute: AppRouter.onGenerateRoute,
47
         onGenerateRoute: AppRouter.onGenerateRoute,
47
       ),
48
       ),
48
     );
49
     );

+ 15
- 1
lib/presentation/navigation/app_router.dart Просмотреть файл

8
 import '../screens/profile/profile_detail_screen.dart';
8
 import '../screens/profile/profile_detail_screen.dart';
9
 import '../screens/splash_screen.dart';
9
 import '../screens/splash_screen.dart';
10
 import '../../core/constants/route_constants.dart';
10
 import '../../core/constants/route_constants.dart';
11
+import 'route_guards.dart';
11
 
12
 
12
 class AppRouter {
13
 class AppRouter {
13
   static Route<dynamic> onGenerateRoute(RouteSettings settings) {
14
   static Route<dynamic> onGenerateRoute(RouteSettings settings) {
14
     switch (settings.name) {
15
     switch (settings.name) {
15
       case RouteConstants.splash:
16
       case RouteConstants.splash:
16
         return MaterialPageRoute(builder: (_) => const SplashScreen());
17
         return MaterialPageRoute(builder: (_) => const SplashScreen());
18
+      
17
       case RouteConstants.home:
19
       case RouteConstants.home:
18
         return MaterialPageRoute(builder: (_) => const HomeScreen());
20
         return MaterialPageRoute(builder: (_) => const HomeScreen());
21
+      
19
       case RouteConstants.login:
22
       case RouteConstants.login:
20
         return MaterialPageRoute(builder: (_) => const LoginScreen());
23
         return MaterialPageRoute(builder: (_) => const LoginScreen());
24
+      
21
       case RouteConstants.register:
25
       case RouteConstants.register:
22
         return MaterialPageRoute(builder: (_) => const RegisterScreen());
26
         return MaterialPageRoute(builder: (_) => const RegisterScreen());
27
+      
23
       case RouteConstants.news:
28
       case RouteConstants.news:
24
         return MaterialPageRoute(builder: (_) => const NewsScreen());
29
         return MaterialPageRoute(builder: (_) => const NewsScreen());
30
+      
25
       case RouteConstants.services:
31
       case RouteConstants.services:
26
         return MaterialPageRoute(builder: (_) => const ServicesScreen());
32
         return MaterialPageRoute(builder: (_) => const ServicesScreen());
33
+      
27
       case RouteConstants.profile:
34
       case RouteConstants.profile:
28
         return MaterialPageRoute(builder: (_) => const ProfileScreen());
35
         return MaterialPageRoute(builder: (_) => const ProfileScreen());
36
+      
29
       case RouteConstants.profileDetail:
37
       case RouteConstants.profileDetail:
30
-        return MaterialPageRoute(builder: (_) => const ProfileDetailScreen());
38
+        return MaterialPageRoute(
39
+          builder: (_) => ProtectedRoute(
40
+            routeName: RouteConstants.profileDetail,
41
+            child: const ProfileDetailScreen(),
42
+          ),
43
+        );
44
+      
31
       default:
45
       default:
32
         return MaterialPageRoute(
46
         return MaterialPageRoute(
33
           builder: (_) => Scaffold(
47
           builder: (_) => Scaffold(

+ 1
- 10
lib/presentation/navigation/bottom_nav_bar.dart Просмотреть файл

1
 import 'package:flutter/material.dart';
1
 import 'package:flutter/material.dart';
2
-import 'package:provider/provider.dart';
3
 import '../../core/constants/route_constants.dart';
2
 import '../../core/constants/route_constants.dart';
4
-import '../../presentation/providers/auth_provider.dart';
5
 
3
 
6
 class BottomNavBar extends StatefulWidget {
4
 class BottomNavBar extends StatefulWidget {
7
   final int initialIndex;
5
   final int initialIndex;
25
   }
23
   }
26
   
24
   
27
   void _onItemTapped(int index) {
25
   void _onItemTapped(int index) {
28
-    final authProvider = Provider.of<AuthProvider>(context, listen: false);
29
-    
30
-    // 如果点击"我的"但未登录,跳转到登录页
31
-    if (index == 3 && !authProvider.isAuthenticated) {
32
-      Navigator.of(context).pushNamed(RouteConstants.login);
33
-      return;
34
-    }
35
     
26
     
36
     setState(() {
27
     setState(() {
37
       _selectedIndex = index;
28
       _selectedIndex = index;
73
         BottomNavigationBarItem(
64
         BottomNavigationBarItem(
74
           icon: Icon(Icons.newspaper_outlined),
65
           icon: Icon(Icons.newspaper_outlined),
75
           activeIcon: Icon(Icons.newspaper),
66
           activeIcon: Icon(Icons.newspaper),
76
-          label: '资讯',
67
+          label: '推荐',
77
         ),
68
         ),
78
         BottomNavigationBarItem(
69
         BottomNavigationBarItem(
79
           icon: Icon(Icons.work_outline),
70
           icon: Icon(Icons.work_outline),

+ 15
- 4
lib/presentation/navigation/route_guards.dart Просмотреть файл

5
 
5
 
6
 class ProtectedRoute extends StatelessWidget {
6
 class ProtectedRoute extends StatelessWidget {
7
   final Widget child;
7
   final Widget child;
8
+  final String? routeName;
8
   
9
   
9
-  const ProtectedRoute({Key? key, required this.child}) : super(key: key);
10
+  const ProtectedRoute({
11
+    super.key,
12
+    required this.child,
13
+    this.routeName,
14
+  });
10
   
15
   
11
   @override
16
   @override
12
   Widget build(BuildContext context) {
17
   Widget build(BuildContext context) {
17
     } else {
22
     } else {
18
       return LoginScreen(
23
       return LoginScreen(
19
         onSuccess: () {
24
         onSuccess: () {
20
-          Navigator.of(context).pushReplacement(
21
-            MaterialPageRoute(builder: (_) => child),
22
-          );
25
+          // 如果有routeName,可以使用pushReplacementNamed
26
+          if (routeName != null) {
27
+            Navigator.of(context).pushReplacementNamed(routeName!);
28
+          } else {
29
+            // 否则使用原来的方式
30
+            Navigator.of(context).pushReplacement(
31
+              MaterialPageRoute(builder: (_) => child),
32
+            );
33
+          }
23
         },
34
         },
24
       );
35
       );
25
     }
36
     }

+ 60
- 5
lib/presentation/providers/auth_provider.dart Просмотреть файл

21
   String? get error => _error;
21
   String? get error => _error;
22
   
22
   
23
   Future<void> _loadCurrentUser() async {
23
   Future<void> _loadCurrentUser() async {
24
-    final user = await authRepository.getCurrentUser();
25
-    if (user != null) {
26
-      _user = user.data;
24
+    final response = await authRepository.getCurrentUser();
25
+    if (response.success && response.data != null) {
26
+      _user = response.data;
27
       notifyListeners();
27
       notifyListeners();
28
     }
28
     }
29
   }
29
   }
76
     }
76
     }
77
   }
77
   }
78
   
78
   
79
+  Future<void> modifyPassword(String oldPassword, String newPassword) async {
80
+    _isLoading = true;
81
+    _error = null;
82
+    notifyListeners();
83
+    
84
+    try {
85
+      // 1. 调用API验证旧密码
86
+      // final isValidOldPassword = await authRepository.validatePassword(oldPassword);
87
+
88
+      // if (!isValidOldPassword) {
89
+      //   _error = '当前密码不正确';
90
+      //   return;
91
+      // }
92
+
93
+      // 2. 调用API更新密码
94
+      // final response = await authRepository.updatePassword(
95
+      //   LoginRequest(username: oldPassword, password: newPassword),
96
+      // );
97
+      
98
+      // if (response.success && response.data != null) {
99
+      //   // 可选:清除本地存储的token,让用户重新登录
100
+      //   // await _authService.logout();
101
+      //   _error = null;
102
+      // } else {
103
+      //   _error = response.message;
104
+      // }
105
+    } catch (e) {
106
+      _error = '密码修改失败: $e';
107
+    } finally {
108
+      _isLoading = false;
109
+      notifyListeners();
110
+    }
111
+  }
112
+  
79
   Future<void> logout() async {
113
   Future<void> logout() async {
80
-    await authRepository.logout();
81
-    _user = null;
114
+    _isLoading = true;
115
+    _error = null;
82
     notifyListeners();
116
     notifyListeners();
117
+    
118
+    try {
119
+      final response = await authRepository.logout();
120
+      
121
+      if (response.success) {
122
+        _user = null;
123
+        _error = null;
124
+      } else {
125
+        _error = response.message;
126
+      }
127
+    } catch (e) {
128
+      _error = '登出失败: $e';
129
+    } finally {
130
+      _isLoading = false;
131
+      notifyListeners();
132
+    }
83
   }
133
   }
84
   
134
   
85
   Future<void> checkAuthStatus() async {
135
   Future<void> checkAuthStatus() async {
96
     _error = null;
146
     _error = null;
97
     notifyListeners();
147
     notifyListeners();
98
   }
148
   }
149
+
150
+  void updateUser(User? user) {
151
+    _user = user;
152
+    notifyListeners();
153
+  }
99
 }
154
 }

+ 5
- 0
lib/presentation/providers/user_provider.dart Просмотреть файл

59
     _error = null;
59
     _error = null;
60
     notifyListeners();
60
     notifyListeners();
61
   }
61
   }
62
+
63
+  void updateUser(User? user) {
64
+    _user = user;
65
+    notifyListeners();
66
+  }
62
 }
67
 }

+ 13
- 4
lib/presentation/screens/auth/login_screen.dart Просмотреть файл

2
 import 'package:provider/provider.dart';
2
 import 'package:provider/provider.dart';
3
 import '../../providers/auth_provider.dart';
3
 import '../../providers/auth_provider.dart';
4
 import '../../../core/constants/route_constants.dart';
4
 import '../../../core/constants/route_constants.dart';
5
+import '../../providers/user_provider.dart';
5
 import '../../widgets/common/app_button.dart';
6
 import '../../widgets/common/app_button.dart';
6
 import '../../widgets/common/app_text_field.dart';
7
 import '../../widgets/common/app_text_field.dart';
7
 import '../../widgets/common/loading_indicator.dart';
8
 import '../../widgets/common/loading_indicator.dart';
9
 class LoginScreen extends StatefulWidget {
10
 class LoginScreen extends StatefulWidget {
10
   final VoidCallback? onSuccess;
11
   final VoidCallback? onSuccess;
11
   
12
   
12
-  const LoginScreen({super.key, this.onSuccess});
13
+  const LoginScreen({
14
+    super.key,
15
+    this.onSuccess,
16
+  });
13
   
17
   
14
   @override
18
   @override
15
   State<LoginScreen> createState() => _LoginScreenState();
19
   State<LoginScreen> createState() => _LoginScreenState();
17
 
21
 
18
 class _LoginScreenState extends State<LoginScreen> {
22
 class _LoginScreenState extends State<LoginScreen> {
19
   final _formKey = GlobalKey<FormState>();
23
   final _formKey = GlobalKey<FormState>();
20
-  final _emailController = TextEditingController(text: 'test@example.com');
21
-  final _passwordController = TextEditingController(text: '123456');
24
+  final _emailController = TextEditingController(text: 'aaa');
25
+  final _passwordController = TextEditingController(text: 'Heweidabangzi77!');
22
   
26
   
23
   @override
27
   @override
24
   void dispose() {
28
   void dispose() {
117
                           _passwordController.text,
121
                           _passwordController.text,
118
                         );
122
                         );
119
                         if (authProvider.isAuthenticated) {
123
                         if (authProvider.isAuthenticated) {
120
-                          widget.onSuccess?.call();
124
+                          // ✅ 同时更新 userProvider
125
+                          final userProvider = Provider.of<UserProvider>(context, listen: false);
126
+                          userProvider.updateUser(authProvider.user);
127
+
128
+                          // widget.onSuccess?.call();
121
                           Navigator.of(context).pushReplacementNamed(RouteConstants.home);
129
                           Navigator.of(context).pushReplacementNamed(RouteConstants.home);
130
+                          // Navigator.of(context).pushNamedAndRemoveUntil(RouteConstants.home);
122
                         }
131
                         }
123
                       }
132
                       }
124
                     },
133
                     },

+ 7
- 24
lib/presentation/screens/home/home_screen.dart Просмотреть файл

2
 import 'package:provider/provider.dart';
2
 import 'package:provider/provider.dart';
3
 import '../../navigation/bottom_nav_bar.dart';
3
 import '../../navigation/bottom_nav_bar.dart';
4
 import '../../providers/auth_provider.dart';
4
 import '../../providers/auth_provider.dart';
5
-import '../../widgets/common/app_button.dart';
6
 
5
 
7
 class HomeScreen extends StatelessWidget {
6
 class HomeScreen extends StatelessWidget {
8
   const HomeScreen({super.key});
7
   const HomeScreen({super.key});
18
           if (authProvider.isAuthenticated)
17
           if (authProvider.isAuthenticated)
19
             IconButton(
18
             IconButton(
20
               icon: const Icon(Icons.notifications_none),
19
               icon: const Icon(Icons.notifications_none),
21
-              onPressed: () {},
20
+              onPressed: () {
21
+                // 进入消息中心界面
22
+                // Navigator.of(context).pushNamed('/login');
23
+              },
22
             ),
24
             ),
23
         ],
25
         ],
24
       ),
26
       ),
42
                     children: [
44
                     children: [
43
                       Row(
45
                       Row(
44
                         children: [
46
                         children: [
45
-                          CircleAvatar(
46
-                            radius: 30,
47
-                            backgroundColor: Colors.blue[100],
48
-                            child: Icon(
49
-                              Icons.person,
50
-                              size: 30,
51
-                              color: Colors.blue,
52
-                            ),
53
-                          ),
54
-                          const SizedBox(width: 16),
55
                           Expanded(
47
                           Expanded(
56
                             child: Column(
48
                             child: Column(
57
                               crossAxisAlignment: CrossAxisAlignment.start,
49
                               crossAxisAlignment: CrossAxisAlignment.start,
68
                                 const SizedBox(height: 4),
60
                                 const SizedBox(height: 4),
69
                                 Text(
61
                                 Text(
70
                                   authProvider.isAuthenticated
62
                                   authProvider.isAuthenticated
71
-                                      ? '欢迎回来!'
63
+                                      ? '这里会展示一些有趣的东西!'
72
                                       : '请登录以使用完整功能',
64
                                       : '请登录以使用完整功能',
73
                                   style: TextStyle(
65
                                   style: TextStyle(
74
                                     fontSize: 14,
66
                                     fontSize: 14,
80
                           ),
72
                           ),
81
                         ],
73
                         ],
82
                       ),
74
                       ),
83
-                      const SizedBox(height: 20),
84
-                      if (!authProvider.isAuthenticated)
85
-                        AppButton(
86
-                          text: '立即登录',
87
-                          onPressed: () {
88
-                            Navigator.of(context).pushNamed('/login');
89
-                          },
90
-                          backgroundColor: Colors.blue,
91
-                          height: 45,
92
-                        ),
75
+                      const SizedBox(height: 100),
93
                     ],
76
                     ],
94
                   ),
77
                   ),
95
                 ),
78
                 ),
113
                 children: [
96
                 children: [
114
                   _buildFeatureCard(
97
                   _buildFeatureCard(
115
                     icon: Icons.newspaper,
98
                     icon: Icons.newspaper,
116
-                    title: '资讯',
99
+                    title: '推荐',
117
                     color: Colors.green,
100
                     color: Colors.green,
118
                     onTap: () {
101
                     onTap: () {
119
                       Navigator.of(context).pushNamed('/news');
102
                       Navigator.of(context).pushNamed('/news');

+ 80
- 52
lib/presentation/screens/news/news_screen.dart Просмотреть файл

1
 import 'package:flutter/material.dart';
1
 import 'package:flutter/material.dart';
2
 import '../../navigation/bottom_nav_bar.dart';
2
 import '../../navigation/bottom_nav_bar.dart';
3
+import '../../widgets/common/app_search_bar.dart';
3
 
4
 
4
 class NewsScreen extends StatelessWidget {
5
 class NewsScreen extends StatelessWidget {
5
   const NewsScreen({super.key});
6
   const NewsScreen({super.key});
8
   Widget build(BuildContext context) {
9
   Widget build(BuildContext context) {
9
     return Scaffold(
10
     return Scaffold(
10
       appBar: AppBar(
11
       appBar: AppBar(
11
-        title: const Text('资讯'),
12
-        actions: [
13
-          IconButton(
14
-            icon: const Icon(Icons.search),
15
-            onPressed: () {},
16
-          ),
17
-        ],
12
+        title: const Text('推荐'),
18
       ),
13
       ),
19
-      body: ListView.builder(
20
-        padding: const EdgeInsets.all(16),
21
-        itemCount: 10,
22
-        itemBuilder: (context, index) {
23
-          return _buildNewsCard(index);
24
-        },
14
+      body: SingleChildScrollView(
15
+        padding: const EdgeInsets.all(20),
16
+        child: Column(
17
+          crossAxisAlignment: CrossAxisAlignment.start,
18
+          children: [
19
+            // 使用 StatefulWidget 搜索框
20
+            SearchBarCustom(
21
+              hintText: '搜索...',
22
+              onSearch: (query) {
23
+                // 处理搜索逻辑
24
+                // print('搜索: $query');
25
+              },
26
+              onChanged: (query) {
27
+                // 实时搜索
28
+                // print('输入变化: $query');
29
+              },
30
+              onClear: () {
31
+                // print('清除搜索');
32
+              },
33
+            ),
34
+            const SizedBox(height: 24),
35
+
36
+            // 推荐列表
37
+            ListView.builder(
38
+              shrinkWrap: true,
39
+              physics: const NeverScrollableScrollPhysics(),
40
+              itemCount: 10,
41
+              itemBuilder: (context, index) {
42
+                return _buildNewsCard(index);
43
+              },
44
+            ),
45
+          ],
46
+        ),
25
       ),
47
       ),
26
       bottomNavigationBar: const BottomNavBar(initialIndex: 1),
48
       bottomNavigationBar: const BottomNavBar(initialIndex: 1),
27
     );
49
     );
29
   
51
   
30
   Widget _buildNewsCard(int index) {
52
   Widget _buildNewsCard(int index) {
31
     final titles = [
53
     final titles = [
32
-      'Flutter 3.0 新特性详解',
33
-      'Dart 语言最新更新',
34
-      '移动开发趋势分析',
35
-      '前端框架对比',
36
-      '用户体验设计原则',
37
-      '后端架构最佳实践',
38
-      '数据库性能优化',
39
-      '云原生技术解析',
40
-      '人工智能在移动端的应用',
41
-      '跨平台开发方案比较',
54
+      '体彩 大乐透 20260001期',
55
+      '福彩 双色球 20260002期',
56
+      '福彩 双色球 20260003期',
57
+      '体彩 大乐透 20260004期',
58
+      '福彩 双色球 20260005期',
59
+      '福彩 双色球 20260007期',
60
+      '体彩 大乐透 20260006期',
61
+      '福彩 双色球 20260008期',
62
+      '福彩 双色球 20260009期',
63
+      '福彩 快乐八 20260010期',
42
     ];
64
     ];
43
     
65
     
44
     final subtitles = [
66
     final subtitles = [
45
-      '深入了解Flutter最新版本的重要更新和改进',
46
-      'Dart语言的最新特性和优化方向',
47
-      '2024年移动开发的重要趋势和技术方向',
48
-      '主流前端框架的优缺点对比分析',
49
-      '提升应用用户体验的关键设计原则',
50
-      '构建高性能后端服务的最佳实践',
51
-      '数据库查询优化和性能调优技巧',
52
-      '云原生技术在微服务中的应用',
53
-      'AI技术在移动应用中的创新应用',
54
-      '各种跨平台开发方案的对比和选择',
67
+      '预测号码:4 5 8 6 25 8 9 6',
68
+      '预测号码:4 5 8 6 25 8 9 6',
69
+      '预测号码:4 5 8 6 25 8 9 6',
70
+      '预测号码:4 5 8 6 25 8 9 6',
71
+      '预测号码:4 5 8 6 25 8 9 6',
72
+      '预测号码:4 5 8 6 25 8 9 6',
73
+      '预测号码:4 5 8 6 25 8 9 6',
74
+      '预测号码:4 5 8 6 25 8 9 6',
75
+      '预测号码:4 5 8 6 25 8 9 6',
76
+      '预测号码:4 5 8 6 25 8 9 6',
55
     ];
77
     ];
56
     
78
     
57
     final times = [
79
     final times = [
58
-      '2小时前',
59
-      '5小时前',
60
-      '昨天',
61
-      '2天前',
62
-      '3天前',
63
-      '1周前',
64
-      '1周前',
65
-      '2周前',
66
-      '2周前',
67
-      '1个月前',
80
+      '2小时前 :aaa',
81
+      '5小时前 :aaa',
82
+      '昨天 :aaa',
83
+      '2天前 :aaa',
84
+      '3天前 :aaa',
85
+      '1周前 :aaa',
86
+      '1周前 :aaa',
87
+      '2周前 :aaa',
88
+      '2周前 :aaa',
89
+      '1个月前 :aaa',
68
     ];
90
     ];
69
     
91
     
70
     return Card(
92
     return Card(
73
         borderRadius: BorderRadius.circular(12),
95
         borderRadius: BorderRadius.circular(12),
74
       ),
96
       ),
75
       child: InkWell(
97
       child: InkWell(
76
-        onTap: () {},
98
+        onTap: () {
99
+          print(index);
100
+        },
77
         borderRadius: BorderRadius.circular(12),
101
         borderRadius: BorderRadius.circular(12),
78
         child: Padding(
102
         child: Padding(
79
           padding: const EdgeInsets.all(16),
103
           padding: const EdgeInsets.all(16),
91
                     ),
115
                     ),
92
                     child: Center(
116
                     child: Center(
93
                       child: Text(
117
                       child: Text(
94
-                        '新闻',
118
+                        '推号',
95
                         style: TextStyle(
119
                         style: TextStyle(
96
                           color: Colors.white,
120
                           color: Colors.white,
97
                           fontWeight: FontWeight.bold,
121
                           fontWeight: FontWeight.bold,
107
                         Text(
131
                         Text(
108
                           titles[index],
132
                           titles[index],
109
                           style: const TextStyle(
133
                           style: const TextStyle(
110
-                            fontSize: 16,
134
+                            fontSize: 18,
111
                             fontWeight: FontWeight.bold,
135
                             fontWeight: FontWeight.bold,
112
                           ),
136
                           ),
113
                           maxLines: 2,
137
                           maxLines: 2,
117
                         Text(
141
                         Text(
118
                           subtitles[index],
142
                           subtitles[index],
119
                           style: TextStyle(
143
                           style: TextStyle(
120
-                            fontSize: 14,
144
+                            fontSize: 16,
121
                             color: Colors.grey[600],
145
                             color: Colors.grey[600],
122
                           ),
146
                           ),
123
                           maxLines: 2,
147
                           maxLines: 2,
128
                   ),
152
                   ),
129
                 ],
153
                 ],
130
               ),
154
               ),
131
-              const SizedBox(height: 12),
155
+              const SizedBox(height: 4),
132
               Row(
156
               Row(
133
                 children: [
157
                 children: [
134
                   Icon(
158
                   Icon(
135
                     Icons.schedule,
159
                     Icons.schedule,
136
-                    size: 14,
160
+                    size: 16,
137
                     color: Colors.grey[500],
161
                     color: Colors.grey[500],
138
                   ),
162
                   ),
139
                   const SizedBox(width: 4),
163
                   const SizedBox(width: 4),
140
                   Text(
164
                   Text(
141
                     times[index],
165
                     times[index],
142
                     style: TextStyle(
166
                     style: TextStyle(
143
-                      fontSize: 12,
167
+                      fontSize: 14,
144
                       color: Colors.grey[500],
168
                       color: Colors.grey[500],
145
                     ),
169
                     ),
146
                   ),
170
                   ),
150
                       Icons.bookmark_border,
174
                       Icons.bookmark_border,
151
                       color: Colors.grey[500],
175
                       color: Colors.grey[500],
152
                     ),
176
                     ),
153
-                    onPressed: () {},
177
+                    onPressed: () {
178
+                      // 添加书签
179
+                    },
154
                     iconSize: 20,
180
                     iconSize: 20,
155
                   ),
181
                   ),
156
                   IconButton(
182
                   IconButton(
158
                       Icons.share,
184
                       Icons.share,
159
                       color: Colors.grey[500],
185
                       color: Colors.grey[500],
160
                     ),
186
                     ),
161
-                    onPressed: () {},
187
+                    onPressed: () {
188
+                      // 分享
189
+                    },
162
                     iconSize: 20,
190
                     iconSize: 20,
163
                   ),
191
                   ),
164
                 ],
192
                 ],

+ 715
- 0
lib/presentation/screens/profile/profile_about_screen.dart Просмотреть файл

1
+import 'package:flutter/material.dart';
2
+import 'package:url_launcher/url_launcher.dart';
3
+import 'package:package_info_plus/package_info_plus.dart';
4
+
5
+class AboutUsScreen extends StatefulWidget {
6
+  const AboutUsScreen({super.key});
7
+
8
+  @override
9
+  State<AboutUsScreen> createState() => _AboutUsScreenState();
10
+}
11
+
12
+class _AboutUsScreenState extends State<AboutUsScreen> {
13
+  PackageInfo _packageInfo = PackageInfo(
14
+    appName: '未知',
15
+    packageName: '未知',
16
+    version: '未知',
17
+    buildNumber: '未知',
18
+    buildSignature: '未知',
19
+  );
20
+  bool _isLoading = false;
21
+
22
+  @override
23
+  void initState() {
24
+    super.initState();
25
+    // _initPackageInfo();
26
+  }
27
+
28
+  Future<void> _initPackageInfo() async {
29
+    final info = await PackageInfo.fromPlatform();
30
+    setState(() {
31
+      _packageInfo = info;
32
+      _isLoading = false;
33
+    });
34
+  }
35
+
36
+  Future<void> _launchURL(String url) async {
37
+    final Uri uri = Uri.parse(url);
38
+    if (await canLaunchUrl(uri)) {
39
+      await launchUrl(uri);
40
+    } else {
41
+      ScaffoldMessenger.of(context).showSnackBar(
42
+        const SnackBar(
43
+          content: Text('无法打开链接'),
44
+          backgroundColor: Colors.red,
45
+        ),
46
+      );
47
+    }
48
+  }
49
+
50
+  void _showVersionInfo() {
51
+    showDialog(
52
+      context: context,
53
+      builder: (context) => AlertDialog(
54
+        title: const Text('版本信息'),
55
+        content: Column(
56
+          mainAxisSize: MainAxisSize.min,
57
+          crossAxisAlignment: CrossAxisAlignment.start,
58
+          children: [
59
+            _buildInfoItem('应用名称', _packageInfo.appName),
60
+            _buildInfoItem('版本号', _packageInfo.version),
61
+            _buildInfoItem('构建号', _packageInfo.buildNumber),
62
+            _buildInfoItem('包名', _packageInfo.packageName),
63
+          ],
64
+        ),
65
+        actions: [
66
+          TextButton(
67
+            onPressed: () => Navigator.of(context).pop(),
68
+            child: const Text('确定'),
69
+          ),
70
+        ],
71
+      ),
72
+    );
73
+  }
74
+
75
+  Widget _buildInfoItem(String label, String value) {
76
+    return Padding(
77
+      padding: const EdgeInsets.symmetric(vertical: 6),
78
+      child: Row(
79
+        crossAxisAlignment: CrossAxisAlignment.start,
80
+        children: [
81
+          SizedBox(
82
+            width: 80,
83
+            child: Text(
84
+              '$label:',
85
+              style: const TextStyle(
86
+                fontWeight: FontWeight.bold,
87
+                color: Colors.grey,
88
+              ),
89
+            ),
90
+          ),
91
+          Expanded(
92
+            child: Text(
93
+              value,
94
+              style: const TextStyle(color: Colors.black87),
95
+            ),
96
+          ),
97
+        ],
98
+      ),
99
+    );
100
+  }
101
+
102
+  @override
103
+  Widget build(BuildContext context) {
104
+    return Scaffold(
105
+      appBar: AppBar(
106
+        title: const Text('关于我们'),
107
+        centerTitle: true,
108
+      ),
109
+      body: _isLoading
110
+          ? const Center(child: CircularProgressIndicator())
111
+          : SingleChildScrollView(
112
+              child: Column(
113
+                children: [
114
+                  // 应用Logo和名称区域
115
+                  Container(
116
+                    padding: const EdgeInsets.symmetric(vertical: 40),
117
+                    decoration: BoxDecoration(
118
+                      gradient: LinearGradient(
119
+                        begin: Alignment.topCenter,
120
+                        end: Alignment.bottomCenter,
121
+                        colors: [
122
+                          Colors.red[50]!,
123
+                          Colors.orange[50]!,
124
+                        ],
125
+                      ),
126
+                    ),
127
+                    child: Column(
128
+                      children: [
129
+                        // 应用Logo
130
+                        Container(
131
+                          width: 120,
132
+                          height: 120,
133
+                          decoration: BoxDecoration(
134
+                            color: Colors.white,
135
+                            borderRadius: BorderRadius.circular(30),
136
+                            boxShadow: [
137
+                              BoxShadow(
138
+                                color: Colors.red.withOpacity(0.2),
139
+                                blurRadius: 20,
140
+                                offset: const Offset(0, 10),
141
+                              ),
142
+                            ],
143
+                          ),
144
+                          child: const Icon(
145
+                            Icons.emoji_events,
146
+                            size: 60,
147
+                            color: Colors.red,
148
+                          ),
149
+                        ),
150
+                        const SizedBox(height: 20),
151
+                        // 应用名称
152
+                        const Text(
153
+                          '中了么',
154
+                          style: TextStyle(
155
+                            fontSize: 32,
156
+                            fontWeight: FontWeight.bold,
157
+                            color: Colors.red,
158
+                          ),
159
+                        ),
160
+                        const SizedBox(height: 8),
161
+                        // 宣传语
162
+                        const Text(
163
+                          '专业彩票资讯,开启幸运之门',
164
+                          style: TextStyle(
165
+                            fontSize: 16,
166
+                            color: Colors.orange,
167
+                            fontStyle: FontStyle.italic,
168
+                          ),
169
+                        ),
170
+                        const SizedBox(height: 16),
171
+                        // 版本信息
172
+                        GestureDetector(
173
+                          onTap: _showVersionInfo,
174
+                          child: Container(
175
+                            padding: const EdgeInsets.symmetric(
176
+                              horizontal: 16,
177
+                              vertical: 6,
178
+                            ),
179
+                            decoration: BoxDecoration(
180
+                              color: Colors.white.withOpacity(0.8),
181
+                              borderRadius: BorderRadius.circular(20),
182
+                            ),
183
+                            child: Text(
184
+                              '版本 ${_packageInfo.version}',
185
+                              style: const TextStyle(
186
+                                color: Colors.red,
187
+                                fontWeight: FontWeight.w500,
188
+                              ),
189
+                            ),
190
+                          ),
191
+                        ),
192
+                      ],
193
+                    ),
194
+                  ),
195
+
196
+                  // 应用介绍卡片
197
+                  _buildSectionCard(
198
+                    title: '应用介绍',
199
+                    icon: Icons.info_outline,
200
+                    color: Colors.blue,
201
+                    child: const Column(
202
+                      crossAxisAlignment: CrossAxisAlignment.start,
203
+                      children: [
204
+                        Text(
205
+                          '「中了么」是一款专业的彩票资讯服务平台,致力于为用户提供全面、及时、准确的彩票信息。我们不是彩票销售平台,而是您获取彩票资讯、分析数据和社区交流的得力助手。',
206
+                          style: TextStyle(
207
+                            height: 1.6,
208
+                            color: Colors.black87,
209
+                          ),
210
+                        ),
211
+                        SizedBox(height: 12),
212
+                        Row(
213
+                          children: [
214
+                            Icon(Icons.check_circle, color: Colors.green, size: 16),
215
+                            SizedBox(width: 8),
216
+                            Expanded(child: Text('合法合规:严格遵守国家法律法规')),
217
+                          ],
218
+                        ),
219
+                        SizedBox(height: 6),
220
+                        Row(
221
+                          children: [
222
+                            Icon(Icons.check_circle, color: Colors.green, size: 16),
223
+                            SizedBox(width: 8),
224
+                            Expanded(child: Text('信息透明:所有数据公开可查')),
225
+                          ],
226
+                        ),
227
+                        SizedBox(height: 6),
228
+                        Row(
229
+                          children: [
230
+                            Icon(Icons.check_circle, color: Colors.green, size: 16),
231
+                            SizedBox(width: 8),
232
+                            Expanded(child: Text('理性推荐:倡导健康购彩理念')),
233
+                          ],
234
+                        ),
235
+                      ],
236
+                    ),
237
+                  ),
238
+
239
+                  // 核心功能
240
+                  _buildSectionCard(
241
+                    title: '核心功能',
242
+                    icon: Icons.stars,
243
+                    color: Colors.amber,
244
+                    child: Column(
245
+                      children: [
246
+                        _buildFeatureItem(
247
+                          icon: Icons.notifications_active,
248
+                          title: '开奖通知',
249
+                          description: '第一时间推送最新开奖结果',
250
+                          color: Colors.red,
251
+                        ),
252
+                        _buildFeatureItem(
253
+                          icon: Icons.analytics,
254
+                          title: '数据统计',
255
+                          description: '历史数据深度分析和趋势预测',
256
+                          color: Colors.blue,
257
+                        ),
258
+                        _buildFeatureItem(
259
+                          icon: Icons.article,
260
+                          title: '资讯快报',
261
+                          description: '行业动态和玩法技巧专业解读',
262
+                          color: Colors.green,
263
+                        ),
264
+                        _buildFeatureItem(
265
+                          icon: Icons.people,
266
+                          title: '彩民社区',
267
+                          description: '与千万彩民交流经验心得',
268
+                          color: Colors.purple,
269
+                        ),
270
+                        _buildFeatureItem(
271
+                          icon: Icons.security,
272
+                          title: '风险提示',
273
+                          description: '购彩风险提示和理性建议',
274
+                          color: Colors.orange,
275
+                        ),
276
+                      ],
277
+                    ),
278
+                  ),
279
+
280
+                  // 开发团队
281
+                  _buildSectionCard(
282
+                    title: '开发团队',
283
+                    icon: Icons.group,
284
+                    color: Colors.purple,
285
+                    child: Column(
286
+                      children: [
287
+                        _buildTeamMember(
288
+                          name: '技术团队',
289
+                          description: '来自一线互联网公司的技术专家,拥有丰富的移动开发经验',
290
+                          avatarColor: Colors.blue,
291
+                        ),
292
+                        _buildTeamMember(
293
+                          name: '数据分析团队',
294
+                          description: '统计学和数据分析专业人士,提供精准的数据支持',
295
+                          avatarColor: Colors.green,
296
+                        ),
297
+                        _buildTeamMember(
298
+                          name: '内容团队',
299
+                          description: '资深彩票行业编辑,确保资讯的专业性和时效性',
300
+                          avatarColor: Colors.orange,
301
+                        ),
302
+                        _buildTeamMember(
303
+                          name: '风控团队',
304
+                          description: '法律和风控专家,确保平台合规运营',
305
+                          avatarColor: Colors.red,
306
+                        ),
307
+                      ],
308
+                    ),
309
+                  ),
310
+
311
+                  // 联系我们
312
+                  _buildSectionCard(
313
+                    title: '联系我们',
314
+                    icon: Icons.contact_support,
315
+                    color: Colors.teal,
316
+                    child: Column(
317
+                      children: [
318
+                        _buildContactItem(
319
+                          icon: Icons.email,
320
+                          label: '商务合作',
321
+                          value: 'business@zhongleme.com',
322
+                          onTap: () => _launchURL('mailto:business@zhongleme.com'),
323
+                        ),
324
+                        _buildContactItem(
325
+                          icon: Icons.email,
326
+                          label: '用户反馈',
327
+                          value: 'feedback@zhongleme.com',
328
+                          onTap: () => _launchURL('mailto:feedback@zhongleme.com'),
329
+                        ),
330
+                        _buildContactItem(
331
+                          icon: Icons.phone,
332
+                          label: '客服热线',
333
+                          value: '400-888-8888',
334
+                          onTap: () => _launchURL('tel:4008888888'),
335
+                        ),
336
+                        _buildContactItem(
337
+                          icon: Icons.language,
338
+                          label: '官方网站',
339
+                          value: 'https://www.zhongleme.com',
340
+                          onTap: () => _launchURL('https://www.zhongleme.com'),
341
+                        ),
342
+                        const SizedBox(height: 16),
343
+                        const Text(
344
+                          '工作时间:周一至周五 9:00-18:00',
345
+                          style: TextStyle(
346
+                            color: Colors.grey,
347
+                            fontSize: 13,
348
+                          ),
349
+                        ),
350
+                      ],
351
+                    ),
352
+                  ),
353
+
354
+                  // 法律声明
355
+                  _buildSectionCard(
356
+                    title: '法律声明',
357
+                    icon: Icons.gavel,
358
+                    color: Colors.grey,
359
+                    child: const Column(
360
+                      crossAxisAlignment: CrossAxisAlignment.start,
361
+                      children: [
362
+                        Text(
363
+                          '1. 本应用提供的所有资讯仅供参考,不构成任何投注建议。',
364
+                          style: TextStyle(fontSize: 13, color: Colors.grey),
365
+                        ),
366
+                        SizedBox(height: 6),
367
+                        Text(
368
+                          '2. 彩票有风险,请理性购彩。未满18周岁不得购买彩票。',
369
+                          style: TextStyle(fontSize: 13, color: Colors.grey),
370
+                        ),
371
+                        SizedBox(height: 6),
372
+                        Text(
373
+                          '3. 我们严格遵守《互联网信息服务管理办法》等相关法律法规。',
374
+                          style: TextStyle(fontSize: 13, color: Colors.grey),
375
+                        ),
376
+                        SizedBox(height: 6),
377
+                        Text(
378
+                          '4. 用户在使用过程中应遵守当地法律法规,对自己的行为负责。',
379
+                          style: TextStyle(fontSize: 13, color: Colors.grey),
380
+                        ),
381
+                        SizedBox(height: 12),
382
+                        Center(
383
+                          child: Text(
384
+                            '© 2023 中了么 版权所有',
385
+                            style: TextStyle(
386
+                              fontSize: 12,
387
+                              color: Colors.grey,
388
+                              fontWeight: FontWeight.bold,
389
+                            ),
390
+                          ),
391
+                        ),
392
+                      ],
393
+                    ),
394
+                  ),
395
+
396
+                  // 社交平台
397
+                  Container(
398
+                    padding: const EdgeInsets.all(20),
399
+                    child: Column(
400
+                      children: [
401
+                        const Text(
402
+                          '关注我们',
403
+                          style: TextStyle(
404
+                            fontSize: 18,
405
+                            fontWeight: FontWeight.bold,
406
+                          ),
407
+                        ),
408
+                        const SizedBox(height: 16),
409
+                        Row(
410
+                          mainAxisAlignment: MainAxisAlignment.center,
411
+                          children: [
412
+                            _buildSocialButton(
413
+                              icon: Icons.wechat,
414
+                              label: '微信',
415
+                              onTap: () => _showWechatQRCode(context),
416
+                            ),
417
+                            _buildSocialButton(
418
+                              icon: Icons.camera_alt,
419
+                              label: '微博',
420
+                              onTap: () => _launchURL('https://weibo.com/zhongleme'),
421
+                            ),
422
+                            _buildSocialButton(
423
+                              icon: Icons.videocam,
424
+                              label: '抖音',
425
+                              onTap: () => _launchURL('https://www.douyin.com/zhongleme'),
426
+                            ),
427
+                          ],
428
+                        ),
429
+                      ],
430
+                    ),
431
+                  ),
432
+
433
+                  const SizedBox(height: 40),
434
+                ],
435
+              ),
436
+            ),
437
+    );
438
+  }
439
+
440
+  Widget _buildSectionCard({
441
+    required String title,
442
+    required IconData icon,
443
+    required Color color,
444
+    required Widget child,
445
+  }) {
446
+    return Container(
447
+      margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
448
+      padding: const EdgeInsets.all(20),
449
+      decoration: BoxDecoration(
450
+        color: Colors.white,
451
+        borderRadius: BorderRadius.circular(16),
452
+        boxShadow: [
453
+          BoxShadow(
454
+            color: Colors.grey.withOpacity(0.1),
455
+            blurRadius: 10,
456
+            offset: const Offset(0, 5),
457
+          ),
458
+        ],
459
+      ),
460
+      child: Column(
461
+        crossAxisAlignment: CrossAxisAlignment.start,
462
+        children: [
463
+          Row(
464
+            children: [
465
+              Container(
466
+                padding: const EdgeInsets.all(8),
467
+                decoration: BoxDecoration(
468
+                  color: color.withOpacity(0.1),
469
+                  borderRadius: BorderRadius.circular(10),
470
+                ),
471
+                child: Icon(icon, color: color, size: 24),
472
+              ),
473
+              const SizedBox(width: 12),
474
+              Text(
475
+                title,
476
+                style: TextStyle(
477
+                  fontSize: 20,
478
+                  fontWeight: FontWeight.bold,
479
+                  color: color,
480
+                ),
481
+              ),
482
+            ],
483
+          ),
484
+          const SizedBox(height: 16),
485
+          child,
486
+        ],
487
+      ),
488
+    );
489
+  }
490
+
491
+  Widget _buildFeatureItem({
492
+    required IconData icon,
493
+    required String title,
494
+    required String description,
495
+    required Color color,
496
+  }) {
497
+    return Container(
498
+      margin: const EdgeInsets.only(bottom: 12),
499
+      child: Row(
500
+        crossAxisAlignment: CrossAxisAlignment.start,
501
+        children: [
502
+          Container(
503
+            width: 40,
504
+            height: 40,
505
+            decoration: BoxDecoration(
506
+              color: color.withOpacity(0.1),
507
+              borderRadius: BorderRadius.circular(10),
508
+            ),
509
+            child: Icon(icon, color: color, size: 22),
510
+          ),
511
+          const SizedBox(width: 12),
512
+          Expanded(
513
+            child: Column(
514
+              crossAxisAlignment: CrossAxisAlignment.start,
515
+              children: [
516
+                Text(
517
+                  title,
518
+                  style: const TextStyle(
519
+                    fontWeight: FontWeight.bold,
520
+                    fontSize: 16,
521
+                  ),
522
+                ),
523
+                const SizedBox(height: 4),
524
+                Text(
525
+                  description,
526
+                  style: TextStyle(
527
+                    color: Colors.grey[700],
528
+                    fontSize: 14,
529
+                  ),
530
+                ),
531
+              ],
532
+            ),
533
+          ),
534
+        ],
535
+      ),
536
+    );
537
+  }
538
+
539
+  Widget _buildTeamMember({
540
+    required String name,
541
+    required String description,
542
+    required Color avatarColor,
543
+  }) {
544
+    return Container(
545
+      margin: const EdgeInsets.only(bottom: 12),
546
+      child: Row(
547
+        crossAxisAlignment: CrossAxisAlignment.start,
548
+        children: [
549
+          CircleAvatar(
550
+            backgroundColor: avatarColor.withOpacity(0.1),
551
+            radius: 20,
552
+            child: Icon(
553
+              Icons.person,
554
+              color: avatarColor,
555
+              size: 20,
556
+            ),
557
+          ),
558
+          const SizedBox(width: 12),
559
+          Expanded(
560
+            child: Column(
561
+              crossAxisAlignment: CrossAxisAlignment.start,
562
+              children: [
563
+                Text(
564
+                  name,
565
+                  style: const TextStyle(
566
+                    fontWeight: FontWeight.bold,
567
+                    fontSize: 16,
568
+                  ),
569
+                ),
570
+                const SizedBox(height: 4),
571
+                Text(
572
+                  description,
573
+                  style: TextStyle(
574
+                    color: Colors.grey[700],
575
+                    fontSize: 14,
576
+                  ),
577
+                ),
578
+              ],
579
+            ),
580
+          ),
581
+        ],
582
+      ),
583
+    );
584
+  }
585
+
586
+  Widget _buildContactItem({
587
+    required IconData icon,
588
+    required String label,
589
+    required String value,
590
+    required VoidCallback onTap,
591
+  }) {
592
+    return GestureDetector(
593
+      onTap: onTap,
594
+      child: Container(
595
+        margin: const EdgeInsets.only(bottom: 12),
596
+        child: Row(
597
+          children: [
598
+            Container(
599
+              width: 40,
600
+              height: 40,
601
+              decoration: BoxDecoration(
602
+                color: Colors.blue.withOpacity(0.1),
603
+                borderRadius: BorderRadius.circular(10),
604
+              ),
605
+              child: Icon(icon, color: Colors.blue, size: 22),
606
+            ),
607
+            const SizedBox(width: 12),
608
+            Expanded(
609
+              child: Column(
610
+                crossAxisAlignment: CrossAxisAlignment.start,
611
+                children: [
612
+                  Text(
613
+                    label,
614
+                    style: const TextStyle(
615
+                      fontSize: 14,
616
+                      color: Colors.grey,
617
+                    ),
618
+                  ),
619
+                  Text(
620
+                    value,
621
+                    style: const TextStyle(
622
+                      fontWeight: FontWeight.w500,
623
+                      fontSize: 16,
624
+                    ),
625
+                  ),
626
+                ],
627
+              ),
628
+            ),
629
+            const Icon(Icons.chevron_right, color: Colors.grey),
630
+          ],
631
+        ),
632
+      ),
633
+    );
634
+  }
635
+
636
+  Widget _buildSocialButton({
637
+    required IconData icon,
638
+    required String label,
639
+    required VoidCallback onTap,
640
+  }) {
641
+    return GestureDetector(
642
+      onTap: onTap,
643
+      child: Container(
644
+        margin: const EdgeInsets.symmetric(horizontal: 12),
645
+        child: Column(
646
+          children: [
647
+            Container(
648
+              width: 60,
649
+              height: 60,
650
+              decoration: BoxDecoration(
651
+                color: Colors.red.withOpacity(0.1),
652
+                borderRadius: BorderRadius.circular(30),
653
+              ),
654
+              child: Icon(icon, color: Colors.red, size: 30),
655
+            ),
656
+            const SizedBox(height: 8),
657
+            Text(label, style: const TextStyle(fontSize: 14)),
658
+          ],
659
+        ),
660
+      ),
661
+    );
662
+  }
663
+
664
+  void _showWechatQRCode(BuildContext context) {
665
+    showDialog(
666
+      context: context,
667
+      builder: (context) => Dialog(
668
+        shape: RoundedRectangleBorder(
669
+          borderRadius: BorderRadius.circular(20),
670
+        ),
671
+        child: Container(
672
+          padding: const EdgeInsets.all(24),
673
+          child: Column(
674
+            mainAxisSize: MainAxisSize.min,
675
+            children: [
676
+              const Text(
677
+                '关注微信公众号',
678
+                style: TextStyle(
679
+                  fontSize: 18,
680
+                  fontWeight: FontWeight.bold,
681
+                ),
682
+              ),
683
+              const SizedBox(height: 16),
684
+              Container(
685
+                width: 200,
686
+                height: 200,
687
+                decoration: BoxDecoration(
688
+                  color: Colors.white,
689
+                  borderRadius: BorderRadius.circular(10),
690
+                  border: Border.all(color: Colors.grey[300]!),
691
+                ),
692
+                child: const Icon(
693
+                  Icons.qr_code_scanner,
694
+                  size: 100,
695
+                  color: Colors.grey,
696
+                ),
697
+              ),
698
+              const SizedBox(height: 16),
699
+              const Text(
700
+                '扫描二维码关注「中了么」公众号',
701
+                textAlign: TextAlign.center,
702
+                style: TextStyle(color: Colors.grey),
703
+              ),
704
+              const SizedBox(height: 20),
705
+              TextButton(
706
+                onPressed: () => Navigator.of(context).pop(),
707
+                child: const Text('关闭'),
708
+              ),
709
+            ],
710
+          ),
711
+        ),
712
+      ),
713
+    );
714
+  }
715
+}

+ 9
- 2
lib/presentation/screens/profile/profile_detail_screen.dart Просмотреть файл

1
 import 'package:flutter/material.dart';
1
 import 'package:flutter/material.dart';
2
 import 'package:provider/provider.dart';
2
 import 'package:provider/provider.dart';
3
 import '../../../data/models/user.dart';
3
 import '../../../data/models/user.dart';
4
+import '../../providers/auth_provider.dart';
4
 import '../../providers/user_provider.dart';
5
 import '../../providers/user_provider.dart';
5
 import '../../widgets/common/app_button.dart';
6
 import '../../widgets/common/app_button.dart';
6
 import '../../widgets/common/app_text_field.dart';
7
 import '../../widgets/common/app_text_field.dart';
24
     super.initState();
25
     super.initState();
25
     final userProvider = Provider.of<UserProvider>(context, listen: false);
26
     final userProvider = Provider.of<UserProvider>(context, listen: false);
26
     _currentUser = userProvider.user ?? User(id: -1, email: '', username: '', createdAt: DateTime.now());
27
     _currentUser = userProvider.user ?? User(id: -1, email: '', username: '', createdAt: DateTime.now());
27
-    _nameController = TextEditingController(text: _currentUser.username);
28
+    _nameController = TextEditingController(text: _currentUser.fullName);
28
     _emailController = TextEditingController(text: _currentUser.email);
29
     _emailController = TextEditingController(text: _currentUser.email);
29
     _phoneController = TextEditingController(text: _currentUser.phone ?? '');
30
     _phoneController = TextEditingController(text: _currentUser.phone ?? '');
30
   }
31
   }
183
                     
184
                     
184
                     await userProvider.updateUserProfile(updatedUser);
185
                     await userProvider.updateUserProfile(updatedUser);
185
                     
186
                     
186
-                    if (userProvider.error == null) {
187
+                    if (userProvider.error == null && mounted) {
188
+                      // ✅ 同时更新 AuthProvider
189
+                      final authProvider = Provider.of<AuthProvider>(context, listen: false);
190
+                      authProvider.updateUser(userProvider.user);  // 使用接口返回的完整用户数据
191
+
187
                       ScaffoldMessenger.of(context).showSnackBar(
192
                       ScaffoldMessenger.of(context).showSnackBar(
188
                         const SnackBar(
193
                         const SnackBar(
189
                           content: Text('个人信息已更新'),
194
                           content: Text('个人信息已更新'),
190
                           duration: Duration(seconds: 2),
195
                           duration: Duration(seconds: 2),
191
                         ),
196
                         ),
192
                       );
197
                       );
198
+
199
+                      // ✅ 直接返回 - 数据已同步更新
193
                       Navigator.of(context).pop();
200
                       Navigator.of(context).pop();
194
                     }
201
                     }
195
                   }
202
                   }

+ 538
- 0
lib/presentation/screens/profile/profile_help_screen.dart Просмотреть файл

1
+import 'package:flutter/material.dart';
2
+import 'package:url_launcher/url_launcher.dart';
3
+
4
+class HelpCenterScreen extends StatefulWidget {
5
+  const HelpCenterScreen({super.key});
6
+
7
+  @override
8
+  State<HelpCenterScreen> createState() => _HelpCenterScreenState();
9
+}
10
+
11
+class _HelpCenterScreenState extends State<HelpCenterScreen> {
12
+  final TextEditingController _searchController = TextEditingController();
13
+  final List<FAQItem> _allFAQs = [];
14
+  List<FAQItem> _filteredFAQs = [];
15
+  List<FAQCategory> _categories = [];
16
+  bool _isSearching = false;
17
+  String _selectedCategoryId = 'all';
18
+
19
+  @override
20
+  void initState() {
21
+    super.initState();
22
+    _initializeData();
23
+    _searchController.addListener(_onSearchChanged);
24
+  }
25
+
26
+  @override
27
+  void dispose() {
28
+    _searchController.dispose();
29
+    super.dispose();
30
+  }
31
+
32
+  void _initializeData() {
33
+    // 初始化分类数据
34
+    _categories = [
35
+      FAQCategory(id: 'all', name: '全部', icon: Icons.all_inclusive, color: Colors.blue),
36
+      FAQCategory(id: 'account', name: '账户问题', icon: Icons.person, color: Colors.purple),
37
+      FAQCategory(id: 'payment', name: '支付相关', icon: Icons.payment, color: Colors.green),
38
+      FAQCategory(id: 'order', name: '订单问题', icon: Icons.shopping_cart, color: Colors.orange),
39
+      FAQCategory(id: 'app', name: '应用使用', icon: Icons.phone_iphone, color: Colors.red),
40
+      FAQCategory(id: 'privacy', name: '隐私安全', icon: Icons.security, color: Colors.teal),
41
+      FAQCategory(id: 'other', name: '其他问题', icon: Icons.help_outline, color: Colors.grey),
42
+    ];
43
+
44
+    // 初始化FAQ数据
45
+    _allFAQs.addAll([
46
+      FAQItem(
47
+        id: '1',
48
+        question: '如何修改个人资料?',
49
+        answer: '您可以在"我的"页面点击头像或个人信息进入编辑页面进行修改。修改完成后记得点击保存按钮。',
50
+        categoryId: 'account',
51
+        isExpanded: false,
52
+      ),
53
+      FAQItem(
54
+        id: '2',
55
+        question: '忘记密码怎么办?',
56
+        answer: '在登录页面点击"忘记密码",按照提示输入注册邮箱或手机号,系统将发送重置密码链接到您的邮箱或手机。',
57
+        categoryId: 'account',
58
+        isExpanded: false,
59
+      ),
60
+      FAQItem(
61
+        id: '3',
62
+        question: '如何绑定/解绑第三方登录?',
63
+        answer: '进入"设置" -> "账户与安全" -> "第三方账号绑定",在这里可以绑定或解绑微信、Apple ID等第三方账号。',
64
+        categoryId: 'account',
65
+        isExpanded: false,
66
+      ),
67
+      FAQItem(
68
+        id: '4',
69
+        question: '支付方式有哪些?',
70
+        answer: '我们支持微信支付、支付宝、银联卡等多种支付方式。在支付时可以选择您方便的支付方式进行付款。',
71
+        categoryId: 'payment',
72
+        isExpanded: false,
73
+      ),
74
+      FAQItem(
75
+        id: '5',
76
+        question: '支付遇到问题怎么办?',
77
+        answer: '1. 检查网络连接\n2. 确认支付账户余额充足\n3. 检查银行卡是否过期\n4. 如仍无法解决,请联系客服提供订单号',
78
+        categoryId: 'payment',
79
+        isExpanded: false,
80
+      ),
81
+      FAQItem(
82
+        id: '6',
83
+        question: '如何查看订单状态?',
84
+        answer: '进入"我的订单"页面,可以查看所有订单的详细状态,包括待支付、进行中、已完成、已取消等状态。',
85
+        categoryId: 'order',
86
+        isExpanded: false,
87
+      ),
88
+      FAQItem(
89
+        id: '7',
90
+        question: '如何取消订单?',
91
+        answer: '在订单详情页面,如果订单状态允许取消,会出现"取消订单"按钮。请注意,部分订单在特定状态下不可取消。',
92
+        categoryId: 'order',
93
+        isExpanded: false,
94
+      ),
95
+      FAQItem(
96
+        id: '8',
97
+        question: '应用出现闪退怎么办?',
98
+        answer: '1. 尝试重启应用\n2. 检查应用是否为最新版本\n3. 清理手机缓存\n4. 重启手机设备\n5. 如问题持续,请反馈给我们',
99
+        categoryId: 'app',
100
+        isExpanded: false,
101
+      ),
102
+      FAQItem(
103
+        id: '9',
104
+        question: '如何更新应用到最新版本?',
105
+        answer: 'iOS用户:前往App Store搜索应用并更新\nAndroid用户:前往应用商店或应用内检查更新',
106
+        categoryId: 'app',
107
+        isExpanded: false,
108
+      ),
109
+      FAQItem(
110
+        id: '10',
111
+        question: '如何保护我的隐私信息?',
112
+        answer: '我们采用加密技术保护您的数据,不会向第三方泄露您的个人信息。您可以在隐私设置中管理数据权限。',
113
+        categoryId: 'privacy',
114
+        isExpanded: false,
115
+      ),
116
+      FAQItem(
117
+        id: '11',
118
+        question: '如何联系客服?',
119
+        answer: '1. 拨打电话:400-123-4567(工作日 9:00-18:00)\n2. 发送邮件:support@example.com\n3. 在线客服:应用内"我的客服"',
120
+        categoryId: 'other',
121
+        isExpanded: false,
122
+      ),
123
+      FAQItem(
124
+        id: '12',
125
+        question: '服务时间是什么时候?',
126
+        answer: '在线客服:每天 8:00-22:00\n电话客服:工作日 9:00-18:00\n节假日服务时间可能调整',
127
+        categoryId: 'other',
128
+        isExpanded: false,
129
+      ),
130
+    ]);
131
+
132
+    _filteredFAQs = List.from(_allFAQs);
133
+  }
134
+
135
+  void _onSearchChanged() {
136
+    final query = _searchController.text.trim();
137
+    setState(() {
138
+      _isSearching = query.isNotEmpty;
139
+      _filteredFAQs = _allFAQs.where((faq) {
140
+        final matchesSearch = faq.question.toLowerCase().contains(query.toLowerCase()) ||
141
+            (faq.answer?.toLowerCase().contains(query.toLowerCase()) ?? false);
142
+        
143
+        final matchesCategory = _selectedCategoryId == 'all' || 
144
+            faq.categoryId == _selectedCategoryId;
145
+        
146
+        return matchesSearch && matchesCategory;
147
+      }).toList();
148
+    });
149
+  }
150
+
151
+  void _selectCategory(String categoryId) {
152
+    setState(() {
153
+      _selectedCategoryId = categoryId;
154
+      _filteredFAQs = _allFAQs.where((faq) {
155
+        final matchesSearch = _isSearching
156
+            ? (faq.question.toLowerCase().contains(_searchController.text.toLowerCase()) ||
157
+                (faq.answer?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false))
158
+            : true;
159
+        
160
+        final matchesCategory = categoryId == 'all' || faq.categoryId == categoryId;
161
+        
162
+        return matchesSearch && matchesCategory;
163
+      }).toList();
164
+    });
165
+  }
166
+
167
+  void _toggleFAQ(int index) {
168
+    setState(() {
169
+      _filteredFAQs[index].isExpanded = !_filteredFAQs[index].isExpanded;
170
+    });
171
+  }
172
+
173
+  Future<void> _contactSupport(String method) async {
174
+    switch (method) {
175
+      case 'phone':
176
+        final Uri telUri = Uri.parse('tel:4001234567');
177
+        if (await canLaunchUrl(telUri)) {
178
+          await launchUrl(telUri);
179
+        }
180
+        break;
181
+      case 'email':
182
+        final Uri emailUri = Uri(
183
+          scheme: 'mailto',
184
+          path: 'support@example.com',
185
+          queryParameters: {'subject': '用户咨询'},
186
+        );
187
+        if (await canLaunchUrl(emailUri)) {
188
+          await launchUrl(emailUri);
189
+        }
190
+        break;
191
+    }
192
+  }
193
+
194
+  void _showFeedbackDialog(BuildContext context) {
195
+    showDialog(
196
+      context: context,
197
+      builder: (context) => AlertDialog(
198
+        title: const Text('问题反馈'),
199
+        content: const Text('您可以将遇到的问题详细描述并发送给我们,我们会尽快处理。'),
200
+        actions: [
201
+          TextButton(
202
+            onPressed: () => Navigator.of(context).pop(),
203
+            child: const Text('取消'),
204
+          ),
205
+          TextButton(
206
+            onPressed: () {
207
+              Navigator.of(context).pop();
208
+              _contactSupport('email');
209
+            },
210
+            child: const Text('去反馈'),
211
+          ),
212
+        ],
213
+      ),
214
+    );
215
+  }
216
+
217
+  @override
218
+  Widget build(BuildContext context) {
219
+    return Scaffold(
220
+      appBar: AppBar(
221
+        title: const Text('帮助中心'),
222
+        actions: [
223
+          IconButton(
224
+            icon: const Icon(Icons.feedback_outlined),
225
+            tooltip: '问题反馈',
226
+            onPressed: () => _showFeedbackDialog(context),
227
+          ),
228
+        ],
229
+      ),
230
+      body: Column(
231
+        children: [
232
+          // 搜索栏
233
+          Padding(
234
+            padding: const EdgeInsets.all(16),
235
+            child: Container(
236
+              decoration: BoxDecoration(
237
+                color: Colors.grey[100],
238
+                borderRadius: BorderRadius.circular(12),
239
+                border: Border.all(color: Colors.grey[300]!),
240
+              ),
241
+              child: TextField(
242
+                controller: _searchController,
243
+                decoration: InputDecoration(
244
+                  hintText: '搜索问题或关键词...',
245
+                  prefixIcon: const Icon(Icons.search, color: Colors.grey),
246
+                  border: InputBorder.none,
247
+                  suffixIcon: _searchController.text.isNotEmpty
248
+                      ? IconButton(
249
+                          icon: const Icon(Icons.clear, color: Colors.grey),
250
+                          onPressed: () {
251
+                            _searchController.clear();
252
+                            _onSearchChanged();
253
+                          },
254
+                        )
255
+                      : null,
256
+                  contentPadding: const EdgeInsets.symmetric(vertical: 14),
257
+                ),
258
+              ),
259
+            ),
260
+          ),
261
+
262
+          // 分类导航
263
+          SizedBox(
264
+            height: 100,
265
+            child: ListView.builder(
266
+              scrollDirection: Axis.horizontal,
267
+              padding: const EdgeInsets.symmetric(horizontal: 16),
268
+              itemCount: _categories.length,
269
+              itemBuilder: (context, index) {
270
+                final category = _categories[index];
271
+                final isSelected = _selectedCategoryId == category.id;
272
+                
273
+                return Padding(
274
+                  padding: const EdgeInsets.only(right: 12),
275
+                  child: Column(
276
+                    children: [
277
+                      GestureDetector(
278
+                        onTap: () => _selectCategory(category.id),
279
+                        child: Container(
280
+                          width: 70,
281
+                          height: 70,
282
+                          decoration: BoxDecoration(
283
+                            color: isSelected 
284
+                                ? category.color.withOpacity(0.2)
285
+                                : Colors.grey[50],
286
+                            borderRadius: BorderRadius.circular(16),
287
+                            border: Border.all(
288
+                              color: isSelected 
289
+                                  ? category.color 
290
+                                  : Colors.transparent,
291
+                              width: 2,
292
+                            ),
293
+                          ),
294
+                          child: Column(
295
+                            mainAxisAlignment: MainAxisAlignment.center,
296
+                            children: [
297
+                              Icon(category.icon, color: category.color, size: 28),
298
+                              const SizedBox(height: 4),
299
+                            ],
300
+                          ),
301
+                        ),
302
+                      ),
303
+                      const SizedBox(height: 4),
304
+                      Text(
305
+                        category.name,
306
+                        style: TextStyle(
307
+                          fontSize: 12,
308
+                          fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
309
+                          color: isSelected ? category.color : Colors.grey[700],
310
+                        ),
311
+                      ),
312
+                    ],
313
+                  ),
314
+                );
315
+              },
316
+            ),
317
+          ),
318
+
319
+          // 问题列表
320
+          Expanded(
321
+            child: _filteredFAQs.isEmpty
322
+                ? Center(
323
+                    child: Column(
324
+                      mainAxisAlignment: MainAxisAlignment.center,
325
+                      children: [
326
+                        Icon(
327
+                          Icons.search_off,
328
+                          size: 60,
329
+                          color: Colors.grey[400],
330
+                        ),
331
+                        const SizedBox(height: 16),
332
+                        Text(
333
+                          _isSearching
334
+                              ? '没有找到相关问题的解答'
335
+                              : '暂无问题',
336
+                          style: TextStyle(
337
+                            color: Colors.grey[600],
338
+                            fontSize: 16,
339
+                          ),
340
+                        ),
341
+                        if (_isSearching) ...[
342
+                          const SizedBox(height: 8),
343
+                          TextButton(
344
+                            onPressed: () => _showFeedbackDialog(context),
345
+                            child: const Text('反馈问题给我们'),
346
+                          ),
347
+                        ],
348
+                      ],
349
+                    ),
350
+                  )
351
+                : ListView.builder(
352
+                    padding: const EdgeInsets.all(16),
353
+                    itemCount: _filteredFAQs.length,
354
+                    itemBuilder: (context, index) {
355
+                      final faq = _filteredFAQs[index];
356
+                      final category = _categories.firstWhere(
357
+                        (cat) => cat.id == faq.categoryId,
358
+                        orElse: () => _categories.last,
359
+                      );
360
+                      
361
+                      return Card(
362
+                        margin: const EdgeInsets.only(bottom: 12),
363
+                        elevation: 1,
364
+                        shape: RoundedRectangleBorder(
365
+                          borderRadius: BorderRadius.circular(12),
366
+                          side: BorderSide(color: Colors.grey[200]!),
367
+                        ),
368
+                        child: ExpansionTile(
369
+                          key: Key(faq.id),
370
+                          initiallyExpanded: faq.isExpanded,
371
+                          onExpansionChanged: (_) => _toggleFAQ(index),
372
+                          leading: Container(
373
+                            width: 36,
374
+                            height: 36,
375
+                            decoration: BoxDecoration(
376
+                              color: category.color.withOpacity(0.1),
377
+                              borderRadius: BorderRadius.circular(8),
378
+                            ),
379
+                            child: Icon(
380
+                              category.icon,
381
+                              color: category.color,
382
+                              size: 20,
383
+                            ),
384
+                          ),
385
+                          title: Text(
386
+                            faq.question,
387
+                            style: const TextStyle(
388
+                              fontWeight: FontWeight.w500,
389
+                              fontSize: 15,
390
+                            ),
391
+                          ),
392
+                          children: [
393
+                            Padding(
394
+                              padding: const EdgeInsets.fromLTRB(68, 8, 16, 16),
395
+                              child: Column(
396
+                                crossAxisAlignment: CrossAxisAlignment.start,
397
+                                children: [
398
+                                  Text(
399
+                                    faq.answer ?? '',
400
+                                    style: TextStyle(
401
+                                      color: Colors.grey[700],
402
+                                      fontSize: 14,
403
+                                      height: 1.5,
404
+                                    ),
405
+                                  ),
406
+                                  if (faq.id == '11') ...[
407
+                                    const SizedBox(height: 16),
408
+                                    const Text(
409
+                                      '快速联系:',
410
+                                      style: TextStyle(
411
+                                        fontWeight: FontWeight.bold,
412
+                                        fontSize: 14,
413
+                                      ),
414
+                                    ),
415
+                                    const SizedBox(height: 8),
416
+                                    Row(
417
+                                      children: [
418
+                                        ElevatedButton.icon(
419
+                                          onPressed: () => _contactSupport('phone'),
420
+                                          icon: const Icon(Icons.phone, size: 16),
421
+                                          label: const Text('电话联系'),
422
+                                          style: ElevatedButton.styleFrom(
423
+                                            backgroundColor: Colors.green[50],
424
+                                            foregroundColor: Colors.green,
425
+                                            elevation: 0,
426
+                                          ),
427
+                                        ),
428
+                                        const SizedBox(width: 12),
429
+                                        ElevatedButton.icon(
430
+                                          onPressed: () => _contactSupport('email'),
431
+                                          icon: const Icon(Icons.email, size: 16),
432
+                                          label: const Text('发送邮件'),
433
+                                          style: ElevatedButton.styleFrom(
434
+                                            backgroundColor: Colors.blue[50],
435
+                                            foregroundColor: Colors.blue,
436
+                                            elevation: 0,
437
+                                          ),
438
+                                        ),
439
+                                      ],
440
+                                    ),
441
+                                  ],
442
+                                ],
443
+                              ),
444
+                            ),
445
+                          ],
446
+                        ),
447
+                      );
448
+                    },
449
+                  ),
450
+          ),
451
+
452
+          // 底部联系栏
453
+          Container(
454
+            padding: const EdgeInsets.all(16),
455
+            decoration: BoxDecoration(
456
+              color: Colors.grey[50],
457
+              border: Border(top: BorderSide(color: Colors.grey[300]!)),
458
+            ),
459
+            child: Column(
460
+              children: [
461
+                const Text(
462
+                  '没有找到您需要的答案?',
463
+                  style: TextStyle(
464
+                    fontWeight: FontWeight.w600,
465
+                    fontSize: 15,
466
+                  ),
467
+                ),
468
+                const SizedBox(height: 8),
469
+                const Text(
470
+                  '我们的客服团队随时为您提供帮助',
471
+                  style: TextStyle(color: Colors.grey, fontSize: 13),
472
+                ),
473
+                const SizedBox(height: 16),
474
+                Row(
475
+                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
476
+                  children: [
477
+                    Expanded(
478
+                      child: OutlinedButton.icon(
479
+                        onPressed: () => _contactSupport('phone'),
480
+                        icon: const Icon(Icons.phone, size: 18),
481
+                        label: const Text('电话客服'),
482
+                        style: OutlinedButton.styleFrom(
483
+                          padding: const EdgeInsets.symmetric(vertical: 12),
484
+                        ),
485
+                      ),
486
+                    ),
487
+                    const SizedBox(width: 12),
488
+                    Expanded(
489
+                      child: ElevatedButton.icon(
490
+                        onPressed: () => _showFeedbackDialog(context),
491
+                        icon: const Icon(Icons.chat, size: 18),
492
+                        label: const Text('在线反馈'),
493
+                        style: ElevatedButton.styleFrom(
494
+                          padding: const EdgeInsets.symmetric(vertical: 12),
495
+                        ),
496
+                      ),
497
+                    ),
498
+                  ],
499
+                ),
500
+              ],
501
+            ),
502
+          ),
503
+        ],
504
+      ),
505
+    );
506
+  }
507
+}
508
+
509
+// 数据模型
510
+class FAQItem {
511
+  final String id;
512
+  final String question;
513
+  final String? answer;
514
+  final String categoryId;
515
+  bool isExpanded;
516
+
517
+  FAQItem({
518
+    required this.id,
519
+    required this.question,
520
+    this.answer,
521
+    required this.categoryId,
522
+    required this.isExpanded,
523
+  });
524
+}
525
+
526
+class FAQCategory {
527
+  final String id;
528
+  final String name;
529
+  final IconData icon;
530
+  final Color color;
531
+
532
+  FAQCategory({
533
+    required this.id,
534
+    required this.name,
535
+    required this.icon,
536
+    required this.color,
537
+  });
538
+}

+ 486
- 0
lib/presentation/screens/profile/profile_modify_password_screen.dart Просмотреть файл

1
+import 'package:flutter/material.dart';
2
+import 'package:provider/provider.dart';
3
+import '../../providers/auth_provider.dart';
4
+import '../../widgets/common/app_button.dart';
5
+import '../../widgets/common/app_text_field.dart';
6
+
7
+class ChangePasswordScreen extends StatefulWidget {
8
+  const ChangePasswordScreen({super.key});
9
+
10
+  @override
11
+  State<ChangePasswordScreen> createState() => _ChangePasswordScreenState();
12
+}
13
+
14
+class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
15
+  // 密码控制器
16
+  late TextEditingController _oldPasswordController;
17
+  late TextEditingController _newPasswordController;
18
+  late TextEditingController _confirmPasswordController;
19
+  
20
+  final _formKey = GlobalKey<FormState>();
21
+
22
+  // 密码是否可见
23
+  bool _isOldPasswordVisible = false;
24
+  bool _isNewPasswordVisible = false;
25
+  bool _isConfirmPasswordVisible = false;
26
+  
27
+  // 密码强度相关
28
+  String _passwordStrength = '';
29
+  Color _strengthColor = Colors.grey;
30
+  double _strengthValue = 0.0;
31
+  
32
+  @override
33
+  void initState() {
34
+    super.initState();
35
+    _oldPasswordController = TextEditingController();
36
+    _newPasswordController = TextEditingController();
37
+    _confirmPasswordController = TextEditingController();
38
+    
39
+    // 监听新密码输入,实时评估强度
40
+    _newPasswordController.addListener(_evaluatePasswordStrength);
41
+  }
42
+  
43
+  @override
44
+  void dispose() {
45
+    _oldPasswordController.dispose();
46
+    _newPasswordController.dispose();
47
+    _confirmPasswordController.dispose();
48
+    super.dispose();
49
+  }
50
+  
51
+  // 评估密码强度
52
+  void _evaluatePasswordStrength() {
53
+    final password = _newPasswordController.text;
54
+    
55
+    if (password.isEmpty) {
56
+      setState(() {
57
+        _passwordStrength = '';
58
+        _strengthValue = 0.0;
59
+        _strengthColor = Colors.grey;
60
+      });
61
+      return;
62
+    }
63
+    
64
+    // 强度评估逻辑
65
+    int score = 0;
66
+    if (password.length >= 8) score++;
67
+    if (RegExp(r'[A-Z]').hasMatch(password)) score++;
68
+    if (RegExp(r'[a-z]').hasMatch(password)) score++;
69
+    if (RegExp(r'[0-9]').hasMatch(password)) score++;
70
+    if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) score++;
71
+    
72
+    setState(() {
73
+      _strengthValue = score / 5.0;
74
+      
75
+      switch (score) {
76
+        case 0:
77
+        case 1:
78
+          _passwordStrength = '弱';
79
+          _strengthColor = Colors.red;
80
+          break;
81
+        case 2:
82
+        case 3:
83
+          _passwordStrength = '中';
84
+          _strengthColor = Colors.orange;
85
+          break;
86
+        case 4:
87
+        case 5:
88
+          _passwordStrength = '强';
89
+          _strengthColor = Colors.green;
90
+          break;
91
+        default:
92
+          _passwordStrength = '';
93
+          _strengthColor = Colors.grey;
94
+      }
95
+    });
96
+  }
97
+  
98
+  // 切换密码可见性
99
+  void _togglePasswordVisibility(String field) {
100
+    setState(() {
101
+      switch (field) {
102
+        case 'old':
103
+          _isOldPasswordVisible = !_isOldPasswordVisible;
104
+          break;
105
+        case 'new':
106
+          _isNewPasswordVisible = !_isNewPasswordVisible;
107
+          break;
108
+        case 'confirm':
109
+          _isConfirmPasswordVisible = !_isConfirmPasswordVisible;
110
+          break;
111
+      }
112
+    });
113
+  }
114
+  
115
+  // 提交修改密码
116
+  Future<void> _submitChangePassword(BuildContext context) async {
117
+    if (!_formKey.currentState!.validate()) {
118
+      return;
119
+    }
120
+    
121
+    final authProvider = Provider.of<AuthProvider>(context, listen: false);
122
+    
123
+    // 调用Provider中的修改密码方法
124
+    await authProvider.modifyPassword(
125
+      _oldPasswordController.text,
126
+      _newPasswordController.text,
127
+    );
128
+    
129
+    if (authProvider.error == null && mounted) {
130
+      // 成功:显示提示并返回
131
+      ScaffoldMessenger.of(context).showSnackBar(
132
+        const SnackBar(
133
+          content: Text('密码修改成功'),
134
+          backgroundColor: Colors.green,
135
+          duration: Duration(seconds: 2),
136
+        ),
137
+      );
138
+      
139
+      // 清空表单
140
+      _oldPasswordController.clear();
141
+      _newPasswordController.clear();
142
+      _confirmPasswordController.clear();
143
+      
144
+      // 延迟返回,让用户看到成功提示
145
+      Future.delayed(const Duration(milliseconds: 1500), () {
146
+        if (mounted) {
147
+          Navigator.of(context).pop();
148
+        }
149
+      });
150
+    } else if (mounted) {
151
+      // 失败:错误信息已在Provider中设置,这里会通过Consumer显示
152
+      ScaffoldMessenger.of(context).showSnackBar(
153
+        SnackBar(
154
+          content: Text(authProvider.error ?? '密码修改失败'),
155
+          backgroundColor: Colors.red,
156
+          duration: const Duration(seconds: 3),
157
+        ),
158
+      );
159
+    }
160
+  }
161
+
162
+  @override
163
+  Widget build(BuildContext context) {
164
+    return Scaffold(
165
+      appBar: AppBar(
166
+        title: const Text('修改密码'),
167
+        leading: IconButton(
168
+          icon: const Icon(Icons.arrow_back),
169
+          onPressed: () {
170
+            // 如果有未保存的更改,提示用户
171
+            if (_oldPasswordController.text.isNotEmpty ||
172
+                _newPasswordController.text.isNotEmpty ||
173
+                _confirmPasswordController.text.isNotEmpty) {
174
+              _showUnsavedChangesDialog(context);
175
+            } else {
176
+              Navigator.of(context).pop();
177
+            }
178
+          },
179
+        ),
180
+      ),
181
+      body: Consumer<AuthProvider>(
182
+        builder: (context, authProvider, _) {
183
+          return SingleChildScrollView(
184
+            padding: const EdgeInsets.all(20),
185
+            child: Form(
186
+              key: _formKey,
187
+              child: Column(
188
+                crossAxisAlignment: CrossAxisAlignment.start,
189
+                children: [
190
+                  // 提示信息
191
+                  Container(
192
+                    padding: const EdgeInsets.all(12),
193
+                    decoration: BoxDecoration(
194
+                      color: Colors.blue[50],
195
+                      borderRadius: BorderRadius.circular(8),
196
+                      border: Border.all(color: Colors.blue[100]!),
197
+                    ),
198
+                    child: Row(
199
+                      children: [
200
+                        Icon(Icons.info_outline, color: Colors.blue[700]),
201
+                        const SizedBox(width: 10),
202
+                        Expanded(
203
+                          child: Text(
204
+                            '为了账户安全,请定期修改密码',
205
+                            style: TextStyle(
206
+                              color: Colors.blue[800],
207
+                              fontSize: 14,
208
+                            ),
209
+                          ),
210
+                        ),
211
+                      ],
212
+                    ),
213
+                  ),
214
+                  const SizedBox(height: 30),
215
+                  
216
+                  // 旧密码输入
217
+                  AppTextField(
218
+                    controller: _oldPasswordController,
219
+                    labelText: '当前密码',
220
+                    hintText: '请输入当前使用的密码',
221
+                    obscureText: !_isOldPasswordVisible,
222
+                    suffixIcon: IconButton(
223
+                      icon: Icon(
224
+                        _isOldPasswordVisible
225
+                            ? Icons.visibility_off
226
+                            : Icons.visibility,
227
+                        color: Colors.grey[600],
228
+                      ),
229
+                      onPressed: () => _togglePasswordVisibility('old'),
230
+                    ),
231
+                    validator: (value) {
232
+                      if (value == null || value.isEmpty) {
233
+                        return '请输入当前密码';
234
+                      }
235
+                      if (value.length < 6) {
236
+                        return '密码长度至少6位';
237
+                      }
238
+                      return null;
239
+                    },
240
+                  ),
241
+                  const SizedBox(height: 20),
242
+                  
243
+                  // 新密码输入
244
+                  AppTextField(
245
+                    controller: _newPasswordController,
246
+                    labelText: '新密码',
247
+                    hintText: '请输入新密码',
248
+                    obscureText: !_isNewPasswordVisible,
249
+                    suffixIcon: IconButton(
250
+                      icon: Icon(
251
+                        _isNewPasswordVisible
252
+                            ? Icons.visibility_off
253
+                            : Icons.visibility,
254
+                        color: Colors.grey[600],
255
+                      ),
256
+                      onPressed: () => _togglePasswordVisibility('new'),
257
+                    ),
258
+                    validator: (value) {
259
+                      if (value == null || value.isEmpty) {
260
+                        return '请输入新密码';
261
+                      }
262
+                      if (value.length < 8) {
263
+                        return '密码长度至少8位';
264
+                      }
265
+                      if (value == _oldPasswordController.text) {
266
+                        return '新密码不能与旧密码相同';
267
+                      }
268
+                      return null;
269
+                    },
270
+                  ),
271
+                  
272
+                  // 密码强度指示器
273
+                  if (_newPasswordController.text.isNotEmpty) ...[
274
+                    const SizedBox(height: 8),
275
+                    Column(
276
+                      crossAxisAlignment: CrossAxisAlignment.start,
277
+                      children: [
278
+                        Row(
279
+                          children: [
280
+                            Text(
281
+                              '密码强度: ',
282
+                              style: TextStyle(
283
+                                fontSize: 13,
284
+                                color: Colors.grey[700],
285
+                              ),
286
+                            ),
287
+                            Text(
288
+                              _passwordStrength,
289
+                              style: TextStyle(
290
+                                fontSize: 13,
291
+                                fontWeight: FontWeight.bold,
292
+                                color: _strengthColor,
293
+                              ),
294
+                            ),
295
+                          ],
296
+                        ),
297
+                        const SizedBox(height: 4),
298
+                        LinearProgressIndicator(
299
+                          value: _strengthValue,
300
+                          backgroundColor: Colors.grey[200],
301
+                          color: _strengthColor,
302
+                          minHeight: 6,
303
+                          borderRadius: BorderRadius.circular(3),
304
+                        ),
305
+                        const SizedBox(height: 4),
306
+                        Text(
307
+                          '建议使用字母、数字和特殊符号的组合',
308
+                          style: TextStyle(
309
+                            fontSize: 12,
310
+                            color: Colors.grey[600],
311
+                          ),
312
+                        ),
313
+                      ],
314
+                    ),
315
+                  ],
316
+                  const SizedBox(height: 20),
317
+                  
318
+                  // 确认新密码输入
319
+                  AppTextField(
320
+                    controller: _confirmPasswordController,
321
+                    labelText: '确认新密码',
322
+                    hintText: '请再次输入新密码',
323
+                    obscureText: !_isConfirmPasswordVisible,
324
+                    suffixIcon: IconButton(
325
+                      icon: Icon(
326
+                        _isConfirmPasswordVisible
327
+                            ? Icons.visibility_off
328
+                            : Icons.visibility,
329
+                        color: Colors.grey[600],
330
+                      ),
331
+                      onPressed: () => _togglePasswordVisibility('confirm'),
332
+                    ),
333
+                    validator: (value) {
334
+                      if (value == null || value.isEmpty) {
335
+                        return '请确认新密码';
336
+                      }
337
+                      if (value != _newPasswordController.text) {
338
+                        return '两次输入的密码不一致';
339
+                      }
340
+                      return null;
341
+                    },
342
+                  ),
343
+                  const SizedBox(height: 30),
344
+                  
345
+                  // 密码要求说明
346
+                  Container(
347
+                    padding: const EdgeInsets.all(15),
348
+                    decoration: BoxDecoration(
349
+                      color: Colors.grey[50],
350
+                      borderRadius: BorderRadius.circular(8),
351
+                      border: Border.all(color: Colors.grey[300]!),
352
+                    ),
353
+                    child: Column(
354
+                      crossAxisAlignment: CrossAxisAlignment.start,
355
+                      children: [
356
+                        Text(
357
+                          '密码要求:',
358
+                          style: TextStyle(
359
+                            fontWeight: FontWeight.bold,
360
+                            color: Colors.grey[800],
361
+                          ),
362
+                        ),
363
+                        const SizedBox(height: 8),
364
+                        _buildRequirementItem('至少8个字符', true),
365
+                        _buildRequirementItem('包含大小写字母', false),
366
+                        _buildRequirementItem('包含数字', false),
367
+                        _buildRequirementItem('可包含特殊符号(如 ! @ # \$)', false),
368
+                      ],
369
+                    ),
370
+                  ),
371
+                  const SizedBox(height: 40),
372
+                  
373
+                  // 错误信息显示
374
+                  if (authProvider.error != null)
375
+                    Padding(
376
+                      padding: const EdgeInsets.only(bottom: 16),
377
+                      child: Container(
378
+                        padding: const EdgeInsets.all(12),
379
+                        decoration: BoxDecoration(
380
+                          color: Colors.red[50],
381
+                          borderRadius: BorderRadius.circular(8),
382
+                          border: Border.all(color: Colors.red[200]!),
383
+                        ),
384
+                        child: Row(
385
+                          children: [
386
+                            Icon(Icons.error_outline, color: Colors.red[700]),
387
+                            const SizedBox(width: 10),
388
+                            Expanded(
389
+                              child: Text(
390
+                                authProvider.error!,
391
+                                style: TextStyle(color: Colors.red[700]),
392
+                              ),
393
+                            ),
394
+                          ],
395
+                        ),
396
+                      ),
397
+                    ),
398
+                  
399
+                  // 保存按钮
400
+                  AppButton(
401
+                    text: '确认修改密码',
402
+                    isLoading: authProvider.isLoading,
403
+                    enabled: !authProvider.isLoading,
404
+                    onPressed: () => _submitChangePassword(context),
405
+                  ),
406
+                  const SizedBox(height: 20),
407
+                  
408
+                  // 取消按钮
409
+                  if (!authProvider.isLoading)
410
+                    Center(
411
+                      child: TextButton(
412
+                        onPressed: () {
413
+                          if (_oldPasswordController.text.isNotEmpty ||
414
+                              _newPasswordController.text.isNotEmpty ||
415
+                              _confirmPasswordController.text.isNotEmpty) {
416
+                            _showUnsavedChangesDialog(context);
417
+                          } else {
418
+                            Navigator.of(context).pop();
419
+                          }
420
+                        },
421
+                        child: const Text(
422
+                          '取消',
423
+                          style: TextStyle(color: Colors.grey),
424
+                        ),
425
+                      ),
426
+                    ),
427
+                ],
428
+              ),
429
+            ),
430
+          );
431
+        },
432
+      ),
433
+    );
434
+  }
435
+  
436
+  // 构建密码要求项
437
+  Widget _buildRequirementItem(String text, bool isRequired) {
438
+    return Padding(
439
+      padding: const EdgeInsets.only(bottom: 4),
440
+      child: Row(
441
+        children: [
442
+          Icon(
443
+            isRequired ? Icons.check_circle : Icons.info_outline,
444
+            size: 16,
445
+            color: isRequired ? Colors.green : Colors.grey[600],
446
+          ),
447
+          const SizedBox(width: 8),
448
+          Text(
449
+            text,
450
+            style: TextStyle(
451
+              fontSize: 13,
452
+              color: Colors.grey[700],
453
+            ),
454
+          ),
455
+        ],
456
+      ),
457
+    );
458
+  }
459
+  
460
+  // 显示未保存更改的对话框
461
+  void _showUnsavedChangesDialog(BuildContext context) {
462
+    showDialog(
463
+      context: context,
464
+      builder: (context) => AlertDialog(
465
+        title: const Text('放弃修改?'),
466
+        content: const Text('您有未保存的密码修改,确定要离开吗?'),
467
+        actions: [
468
+          TextButton(
469
+            onPressed: () => Navigator.of(context).pop(),
470
+            child: const Text('取消'),
471
+          ),
472
+          TextButton(
473
+            onPressed: () {
474
+              Navigator.of(context).pop(); // 关闭对话框
475
+              Navigator.of(context).pop(); // 返回上一页
476
+            },
477
+            child: const Text(
478
+              '确定',
479
+              style: TextStyle(color: Colors.red),
480
+            ),
481
+          ),
482
+        ],
483
+      ),
484
+    );
485
+  }
486
+}

+ 300
- 165
lib/presentation/screens/profile/profile_screen.dart Просмотреть файл

1
 import 'package:flutter/material.dart';
1
 import 'package:flutter/material.dart';
2
 import 'package:provider/provider.dart';
2
 import 'package:provider/provider.dart';
3
+import '../../../core/constants/route_constants.dart';
3
 import '../../providers/auth_provider.dart';
4
 import '../../providers/auth_provider.dart';
4
-import '../../providers/user_provider.dart';
5
 import '../../navigation/bottom_nav_bar.dart';
5
 import '../../navigation/bottom_nav_bar.dart';
6
+import '../../widgets/common/app_button.dart';
7
+import 'profile_about_screen.dart';
6
 import 'profile_detail_screen.dart';
8
 import 'profile_detail_screen.dart';
7
-import '../../widgets/custom/protected_widget.dart';
9
+import 'profile_help_screen.dart';
10
+import 'profile_modify_password_screen.dart';
8
 
11
 
9
 class ProfileScreen extends StatelessWidget {
12
 class ProfileScreen extends StatelessWidget {
10
   const ProfileScreen({super.key});
13
   const ProfileScreen({super.key});
11
 
14
 
12
   @override
15
   @override
13
   Widget build(BuildContext context) {
16
   Widget build(BuildContext context) {
14
-    return ProtectedWidget(
15
-      child: _ProfileContent(),
16
-      loadingWidget: const Center(
17
-        child: CircularProgressIndicator(),
18
-      ),
19
-    );
17
+    // 直接显示内容,认证由路由层处理
18
+    return _ProfileContent();
20
   }
19
   }
21
 }
20
 }
22
 
21
 
26
 }
25
 }
27
 
26
 
28
 class _ProfileContentState extends State<_ProfileContent> {
27
 class _ProfileContentState extends State<_ProfileContent> {
29
-  @override
30
-  void initState() {
31
-    super.initState();
32
-    WidgetsBinding.instance.addPostFrameCallback((_) {
33
-      final userProvider = Provider.of<UserProvider>(context, listen: false);
34
-      userProvider.loadUserProfile();
35
-    });
36
-  }
28
+  // @override
29
+  // void initState() {
30
+  //   super.initState();
31
+  //   WidgetsBinding.instance.addPostFrameCallback((_) {
32
+  //     final userProvider = Provider.of<UserProvider>(context, listen: false);
33
+  //     userProvider.loadUserProfile();
34
+  //   });
35
+  // }
37
 
36
 
38
   @override
37
   @override
39
   Widget build(BuildContext context) {
38
   Widget build(BuildContext context) {
40
     final authProvider = Provider.of<AuthProvider>(context);
39
     final authProvider = Provider.of<AuthProvider>(context);
41
-    // final userProvider = Provider.of<UserProvider>(context);
42
     
40
     
43
     return Scaffold(
41
     return Scaffold(
44
       appBar: AppBar(
42
       appBar: AppBar(
55
       body: SingleChildScrollView(
53
       body: SingleChildScrollView(
56
         child: Column(
54
         child: Column(
57
           children: [
55
           children: [
58
-            // 用户信息卡片
59
-            Card(
60
-              margin: const EdgeInsets.all(16),
61
-              shape: RoundedRectangleBorder(
62
-                borderRadius: BorderRadius.circular(16),
63
-              ),
64
-              elevation: 2,
65
-              child: Padding(
66
-                padding: const EdgeInsets.all(20),
67
-                child: Column(
68
-                  children: [
69
-                    GestureDetector(
70
-                      onTap: () {
71
-                        // 点击头像
72
-                      },
73
-                      child: Stack(
56
+            if (!authProvider.isAuthenticated) ...[
57
+              // 欢迎区域
58
+              Card(
59
+                margin: const EdgeInsets.all(16),
60
+                shape: RoundedRectangleBorder(
61
+                  borderRadius: BorderRadius.circular(16),
62
+                ),
63
+                // color: Colors.blue[50],
64
+                elevation: 2,
65
+                child: Padding(
66
+                  padding: const EdgeInsets.all(20),
67
+                  child: Column(
68
+                    crossAxisAlignment: CrossAxisAlignment.start,
69
+                    children: [
70
+                      Row(
74
                         children: [
71
                         children: [
75
                           CircleAvatar(
72
                           CircleAvatar(
76
-                            radius: 50,
73
+                            radius: 30,
77
                             backgroundColor: Colors.blue[100],
74
                             backgroundColor: Colors.blue[100],
78
-                            backgroundImage: authProvider.user?.avatar != null
79
-                                ? NetworkImage(authProvider.user!.avatar!)
80
-                                : null,
81
-                            child: authProvider.user?.avatar == null
82
-                                ? const Icon(
83
-                                    Icons.person,
84
-                                    size: 60,
85
-                                    color: Colors.blue,
86
-                                  )
87
-                                : null,
75
+                            child: Icon(
76
+                              Icons.person,
77
+                              size: 30,
78
+                              color: Colors.blue,
79
+                            ),
88
                           ),
80
                           ),
89
-                          Positioned(
90
-                            bottom: 0,
91
-                            right: 0,
92
-                            child: Container(
93
-                              padding: const EdgeInsets.all(6),
94
-                              decoration: BoxDecoration(
95
-                                color: Colors.blue,
96
-                                borderRadius: BorderRadius.circular(20),
97
-                                border: Border.all(
98
-                                  color: Colors.white,
99
-                                  width: 2,
81
+                          const SizedBox(width: 16),
82
+                          Expanded(
83
+                            child: Column(
84
+                              crossAxisAlignment: CrossAxisAlignment.start,
85
+                              children: [
86
+                                Text(
87
+                                  authProvider.isAuthenticated
88
+                                      ? '您好,${authProvider.user?.fullName}'
89
+                                      : '您好,游客',
90
+                                  style: const TextStyle(
91
+                                    fontSize: 18,
92
+                                    fontWeight: FontWeight.bold,
93
+                                  ),
100
                                 ),
94
                                 ),
101
-                              ),
102
-                              child: const Icon(
103
-                                Icons.edit,
104
-                                size: 16,
105
-                                color: Colors.white,
106
-                              ),
95
+                                const SizedBox(height: 4),
96
+                                Text(
97
+                                  authProvider.isAuthenticated
98
+                                      ? '欢迎回来!'
99
+                                      : '请登录以使用完整功能',
100
+                                  style: TextStyle(
101
+                                    fontSize: 14,
102
+                                    color: Colors.grey[600],
103
+                                  ),
104
+                                ),
105
+                              ],
107
                             ),
106
                             ),
108
                           ),
107
                           ),
109
                         ],
108
                         ],
110
                       ),
109
                       ),
111
-                    ),
112
-                    const SizedBox(height: 16),
113
-                    Text(
114
-                      authProvider.user?.fullName ?? '用户',
115
-                      style: const TextStyle(
116
-                        fontSize: 22,
117
-                        fontWeight: FontWeight.bold,
110
+                      const SizedBox(height: 20),
111
+                      if (!authProvider.isAuthenticated)
112
+                        AppButton(
113
+                          text: '立即登录',
114
+                          onPressed: () {
115
+                            Navigator.of(context).pushNamed('/login');
116
+                          },
117
+                          backgroundColor: Colors.blue,
118
+                          height: 45,
119
+                        ),
120
+                    ],
121
+                  ),
122
+                ),
123
+              ),
124
+            ] else ...[
125
+              // 用户信息卡片
126
+              Card(
127
+                margin: const EdgeInsets.all(16),
128
+                shape: RoundedRectangleBorder(
129
+                  borderRadius: BorderRadius.circular(16),
130
+                ),
131
+                // color: Colors.blue[50],
132
+                elevation: 2,
133
+                child: Padding(
134
+                  padding: const EdgeInsets.all(20),
135
+                  child: Column(
136
+                    children: [
137
+                      GestureDetector(
138
+                        onTap: () {
139
+                          // 点击头像
140
+                        },
141
+                        child: Stack(
142
+                          children: [
143
+                            CircleAvatar(
144
+                              radius: 50,
145
+                              backgroundColor: Colors.blue[100],
146
+                              backgroundImage: authProvider.user?.avatar != null
147
+                                  ? NetworkImage(authProvider.user!.avatar!)
148
+                                  : null,
149
+                              child: authProvider.user?.avatar == null
150
+                                  ? const Icon(
151
+                                      Icons.person,
152
+                                      size: 60,
153
+                                      color: Colors.blue,
154
+                                    )
155
+                                  : null,
156
+                            ),
157
+                            Positioned(
158
+                              bottom: 0,
159
+                              right: 0,
160
+                              child: Container(
161
+                                padding: const EdgeInsets.all(6),
162
+                                decoration: BoxDecoration(
163
+                                  color: Colors.blue,
164
+                                  borderRadius: BorderRadius.circular(20),
165
+                                  border: Border.all(
166
+                                    color: Colors.white,
167
+                                    width: 2,
168
+                                  ),
169
+                                ),
170
+                              ),
171
+                            ),
172
+                          ],
173
+                        ),
118
                       ),
174
                       ),
119
-                    ),
120
-                    const SizedBox(height: 8),
121
-                    Text(
122
-                      authProvider.user?.email ?? '',
123
-                      style: TextStyle(
124
-                        fontSize: 14,
125
-                        color: Colors.grey[600],
175
+                      const SizedBox(height: 16),
176
+                      Text(
177
+                        authProvider.user?.fullName ?? '用户',
178
+                        style: const TextStyle(
179
+                          fontSize: 22,
180
+                          fontWeight: FontWeight.bold,
181
+                        ),
126
                       ),
182
                       ),
127
-                    ),
128
-                    if (authProvider.user?.phone != null)
129
-                      Padding(
130
-                        padding: const EdgeInsets.only(top: 4),
131
-                        child: Text(
132
-                          authProvider.user!.phone!,
133
-                          style: TextStyle(
134
-                            fontSize: 14,
135
-                            color: Colors.grey[600],
183
+                      const SizedBox(height: 8),
184
+                      Text(
185
+                        authProvider.user?.email ?? '',
186
+                        style: TextStyle(
187
+                          fontSize: 14,
188
+                          color: Colors.grey[600],
189
+                        ),
190
+                      ),
191
+                      if (authProvider.user?.phone != null)
192
+                        Padding(
193
+                          padding: const EdgeInsets.only(top: 4),
194
+                          child: Text(
195
+                            authProvider.user!.phone!,
196
+                            style: TextStyle(
197
+                              fontSize: 14,
198
+                              color: Colors.grey[600],
199
+                            ),
136
                           ),
200
                           ),
137
                         ),
201
                         ),
202
+                      const SizedBox(height: 16),
203
+                      Row(
204
+                        mainAxisAlignment: MainAxisAlignment.center,
205
+                        children: [
206
+                          _buildStatItem('关注', '0'),
207
+                          _buildVerticalDivider(),
208
+                          _buildStatItem('粉丝', '0'),
209
+                          _buildVerticalDivider(),
210
+                          _buildStatItem('积分', '0'),
211
+                        ],
138
                       ),
212
                       ),
139
-                    const SizedBox(height: 16),
140
-                    Row(
141
-                      mainAxisAlignment: MainAxisAlignment.center,
142
-                      children: [
143
-                        _buildStatItem('关注', '128'),
144
-                        _buildVerticalDivider(),
145
-                        _buildStatItem('粉丝', '256'),
146
-                        _buildVerticalDivider(),
147
-                        _buildStatItem('积分', '1024'),
148
-                      ],
149
-                    ),
150
-                  ],
213
+                    ],
214
+                  ),
151
                 ),
215
                 ),
152
               ),
216
               ),
153
-            ),
154
-            
217
+            ],
155
             // 菜单项
218
             // 菜单项
156
             Padding(
219
             Padding(
157
               padding: const EdgeInsets.symmetric(horizontal: 16),
220
               padding: const EdgeInsets.symmetric(horizontal: 16),
158
               child: Column(
221
               child: Column(
159
                 children: [
222
                 children: [
160
-                  _buildMenuCard(
161
-                    title: '账户设置',
162
-                    items: [
163
-                      _buildMenuItem(
164
-                        icon: Icons.person_outline,
165
-                        title: '个人信息',
166
-                        subtitle: '查看和编辑个人信息',
167
-                        onTap: () {
168
-                          Navigator.of(context).push(
169
-                            MaterialPageRoute(
170
-                              builder: (_) => const ProfileDetailScreen(),
171
-                            ),
172
-                          );
173
-                        },
174
-                      ),
175
-                      _buildMenuItem(
176
-                        icon: Icons.lock_outline,
177
-                        title: '账号安全',
178
-                        subtitle: '修改密码和安全设置',
179
-                        onTap: () {},
180
-                      ),
181
-                      _buildMenuItem(
182
-                        icon: Icons.notifications_none,
183
-                        title: '消息通知',
184
-                        subtitle: '管理通知偏好设置',
185
-                        onTap: () {},
186
-                      ),
187
-                    ],
188
-                  ),
189
-                  const SizedBox(height: 16),
190
-                  _buildMenuCard(
191
-                    title: '我的内容',
192
-                    items: [
193
-                      _buildMenuItem(
194
-                        icon: Icons.bookmark_border,
195
-                        title: '我的收藏',
196
-                        subtitle: '查看收藏的内容',
197
-                        onTap: () {},
198
-                      ),
199
-                      _buildMenuItem(
200
-                        icon: Icons.history,
201
-                        title: '浏览历史',
202
-                        subtitle: '查看最近浏览记录',
203
-                        onTap: () {},
204
-                      ),
205
-                      _buildMenuItem(
206
-                        icon: Icons.download,
207
-                        title: '我的下载',
208
-                        subtitle: '管理下载的文件',
209
-                        onTap: () {},
210
-                      ),
211
-                    ],
212
-                  ),
213
-                  const SizedBox(height: 16),
223
+                  if (authProvider.isAuthenticated) ... [
224
+                    _buildMenuCard(
225
+                      title: '账户设置',
226
+                      items: [
227
+                        _buildMenuItem(
228
+                          icon: Icons.person_outline,
229
+                          title: '个人信息',
230
+                          subtitle: '查看和编辑个人信息',
231
+                          onTap: () {
232
+                            Navigator.of(context).push(
233
+                              MaterialPageRoute(
234
+                                builder: (_) => const ProfileDetailScreen(),
235
+                              ),
236
+                            );
237
+                          },
238
+                        ),
239
+                        _buildMenuItem(
240
+                          icon: Icons.lock_outline,
241
+                          title: '账号安全',
242
+                          subtitle: '修改密码',
243
+                          onTap: () {
244
+                            Navigator.of(context).push(
245
+                              MaterialPageRoute(
246
+                                builder: (_) => const ChangePasswordScreen(),
247
+                              ),
248
+                            );
249
+                          },
250
+                        ),
251
+                        _buildMenuItem(
252
+                          icon: Icons.notifications_none,
253
+                          title: '消息通知',
254
+                          subtitle: '管理通知偏好设置',
255
+                          onTap: () {},
256
+                        ),
257
+                      ],
258
+                    ),
259
+                    const SizedBox(height: 16),
260
+                    _buildMenuCard(
261
+                      title: '我的内容',
262
+                      items: [
263
+                        _buildMenuItem(
264
+                          icon: Icons.bookmark_border,
265
+                          title: '我的收藏',
266
+                          subtitle: '查看收藏的内容',
267
+                          onTap: () {},
268
+                        ),
269
+                        _buildMenuItem(
270
+                          icon: Icons.history,
271
+                          title: '浏览历史',
272
+                          subtitle: '查看最近浏览记录',
273
+                          onTap: () {},
274
+                        ),
275
+                        _buildMenuItem(
276
+                          icon: Icons.download,
277
+                          title: '我的下载',
278
+                          subtitle: '管理下载的文件',
279
+                          onTap: () {},
280
+                        ),
281
+                      ],
282
+                    ),
283
+                    const SizedBox(height: 16),
284
+                  ],
214
                   _buildMenuCard(
285
                   _buildMenuCard(
215
                     title: '其他',
286
                     title: '其他',
216
                     items: [
287
                     items: [
218
                         icon: Icons.help_outline,
289
                         icon: Icons.help_outline,
219
                         title: '帮助中心',
290
                         title: '帮助中心',
220
                         subtitle: '常见问题和帮助文档',
291
                         subtitle: '常见问题和帮助文档',
221
-                        onTap: () {},
292
+                        onTap: () {
293
+                          Navigator.of(context).push(
294
+                              MaterialPageRoute(
295
+                                builder: (_) => const HelpCenterScreen(),
296
+                              ),
297
+                            );
298
+                        },
222
                       ),
299
                       ),
223
                       _buildMenuItem(
300
                       _buildMenuItem(
224
                         icon: Icons.info_outline,
301
                         icon: Icons.info_outline,
225
                         title: '关于我们',
302
                         title: '关于我们',
226
                         subtitle: '了解应用信息',
303
                         subtitle: '了解应用信息',
227
-                        onTap: () {},
228
-                      ),
229
-                      _buildMenuItem(
230
-                        icon: Icons.logout,
231
-                        title: '退出登录',
232
-                        subtitle: '安全退出当前账号',
233
-                        onTap: () async {
234
-                          await authProvider.logout();
235
-                          if (mounted) {
236
-                            Navigator.of(context).pushReplacementNamed('/');
237
-                          }
304
+                        onTap: () {
305
+                          Navigator.of(context).push(
306
+                              MaterialPageRoute(
307
+                                builder: (_) => const AboutUsScreen(),
308
+                              ),
309
+                            );
238
                         },
310
                         },
239
                       ),
311
                       ),
312
+                      if (authProvider.isAuthenticated) ... [
313
+                        _buildMenuItem(
314
+                          icon: Icons.logout,
315
+                          title: '退出登录',
316
+                          subtitle: '安全退出当前账号',
317
+                          onTap: () async {
318
+                            // 防止重复点击
319
+                            if (authProvider.isLoading) return;
320
+
321
+                            final shouldLogout = await showDialog<bool>(
322
+                              context: context,
323
+                              builder: (context) {
324
+                                final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
325
+                                
326
+                                if (isIOS) {
327
+                                  // iOS风格:确定在右边,取消在左边
328
+                                  return AlertDialog(
329
+                                    title: const Text('确认退出'),
330
+                                    content: const Text('确定要退出登录吗?'),
331
+                                    actionsAlignment: MainAxisAlignment.spaceBetween,
332
+                                    actions: [
333
+                                      TextButton(
334
+                                        onPressed: () => Navigator.of(context).pop(true),
335
+                                        child: const Text('确定'),
336
+                                      ),
337
+                                      TextButton(
338
+                                        onPressed: () => Navigator.of(context).pop(false),
339
+                                        child: const Text('取消'),
340
+                                      ),
341
+                                    ],
342
+                                  );
343
+                                } else {
344
+                                  // Android/Material风格:取消在左边,确定在右边
345
+                                  return AlertDialog(
346
+                                    title: const Text('确认退出'),
347
+                                    content: const Text('确定要退出登录吗?'),
348
+                                    actionsAlignment: MainAxisAlignment.spaceBetween,
349
+                                    actions: [
350
+                                      TextButton(
351
+                                        onPressed: () => Navigator.of(context).pop(false),
352
+                                        child: const Text('取消'),
353
+                                      ),
354
+                                      TextButton(
355
+                                        onPressed: () => Navigator.of(context).pop(true),
356
+                                        child: const Text('确定'),
357
+                                      ),
358
+                                    ],
359
+                                  );
360
+                                }
361
+                              },
362
+                            );
363
+                            
364
+                            if (shouldLogout == true) {
365
+                              await authProvider.logout();
366
+
367
+                              // 登出后,确保在组件仍然存在时进行导航
368
+                              if (mounted) {
369
+                                // 使用pushReplacementNamed清空导航栈
370
+                                Navigator.of(context).pushReplacementNamed(RouteConstants.home);
371
+                              }
372
+                            }
373
+                          },
374
+                        ),
375
+                      ],
240
                     ],
376
                     ],
241
                   ),
377
                   ),
242
                   const SizedBox(height: 20),
378
                   const SizedBox(height: 20),
243
-                  
244
                   // 版本信息
379
                   // 版本信息
245
                   Padding(
380
                   Padding(
246
                     padding: const EdgeInsets.symmetric(vertical: 20),
381
                     padding: const EdgeInsets.symmetric(vertical: 20),

+ 0
- 20
lib/presentation/screens/services/services_screen.dart Просмотреть файл

15
         child: Column(
15
         child: Column(
16
           crossAxisAlignment: CrossAxisAlignment.start,
16
           crossAxisAlignment: CrossAxisAlignment.start,
17
           children: [
17
           children: [
18
-            // 搜索框
19
-            Container(
20
-              decoration: BoxDecoration(
21
-                color: Colors.grey[100],
22
-                borderRadius: BorderRadius.circular(25),
23
-              ),
24
-              child: TextField(
25
-                decoration: InputDecoration(
26
-                  hintText: '搜索服务...',
27
-                  border: InputBorder.none,
28
-                  prefixIcon: const Icon(Icons.search),
29
-                  contentPadding: const EdgeInsets.symmetric(
30
-                    vertical: 15,
31
-                    horizontal: 20,
32
-                  ),
33
-                ),
34
-              ),
35
-            ),
36
-            const SizedBox(height: 30),
37
-            
38
             // 服务分类
18
             // 服务分类
39
             const Text(
19
             const Text(
40
               '服务分类',
20
               '服务分类',

+ 1
- 1
lib/presentation/screens/splash_screen.dart Просмотреть файл

47
             ),
47
             ),
48
             const SizedBox(height: 20),
48
             const SizedBox(height: 20),
49
             const Text(
49
             const Text(
50
-              '采油会',
50
+              '中了么',
51
               style: TextStyle(
51
               style: TextStyle(
52
                 fontSize: 32,
52
                 fontSize: 32,
53
                 fontWeight: FontWeight.bold,
53
                 fontWeight: FontWeight.bold,

+ 72
- 0
lib/presentation/widgets/common/app_search_bar.dart Просмотреть файл

1
+// 独立的搜索框组件
2
+import 'package:flutter/material.dart';
3
+
4
+class SearchBarCustom extends StatefulWidget {
5
+  final ValueChanged<String>? onSearch;
6
+  final ValueChanged<String>? onChanged;
7
+  final VoidCallback? onClear;
8
+  final String hintText;
9
+  
10
+  const SearchBarCustom({
11
+    super.key,
12
+    this.onSearch,
13
+    this.onChanged,
14
+    this.onClear,
15
+    this.hintText = '搜索...',
16
+  });
17
+  
18
+  @override
19
+  State<SearchBarCustom> createState() => _SearchBarCustomState();
20
+}
21
+
22
+class _SearchBarCustomState extends State<SearchBarCustom> {
23
+  final TextEditingController _controller = TextEditingController();
24
+  final FocusNode _focusNode = FocusNode();
25
+  
26
+  @override
27
+  void dispose() {
28
+    _controller.dispose();
29
+    _focusNode.dispose();
30
+    super.dispose();
31
+  }
32
+  
33
+  @override
34
+  Widget build(BuildContext context) {
35
+    return Container(
36
+      decoration: BoxDecoration(
37
+        color: Colors.grey[100],
38
+        borderRadius: BorderRadius.circular(15),
39
+      ),
40
+      child: TextField(
41
+        controller: _controller,
42
+        focusNode: _focusNode,
43
+        decoration: InputDecoration(
44
+          hintText: widget.hintText,
45
+          border: InputBorder.none,
46
+          prefixIcon: const Icon(Icons.search),
47
+          suffixIcon: _controller.text.isNotEmpty
48
+              ? IconButton(
49
+                  icon: const Icon(Icons.clear),
50
+                  onPressed: () {
51
+                    _controller.clear();
52
+                    widget.onClear?.call();
53
+                  },
54
+                )
55
+              : null,
56
+          contentPadding: const EdgeInsets.symmetric(
57
+            vertical: 15,
58
+            horizontal: 20,
59
+          ),
60
+        ),
61
+        onChanged: (value) {
62
+          setState(() {}); // 更新清除按钮显示
63
+          widget.onChanged?.call(value);
64
+        },
65
+        onSubmitted: (value) {
66
+          widget.onSearch?.call(value);
67
+          _focusNode.unfocus();
68
+        },
69
+      ),
70
+    );
71
+  }
72
+}

+ 3
- 0
lib/presentation/widgets/custom/protected_widget.dart Просмотреть файл

18
   Widget build(BuildContext context) {
18
   Widget build(BuildContext context) {
19
     final authProvider = Provider.of<AuthProvider>(context);
19
     final authProvider = Provider.of<AuthProvider>(context);
20
     
20
     
21
+    // 如果正在加载,显示loading
21
     if (authProvider.isLoading) {
22
     if (authProvider.isLoading) {
22
       return loadingWidget ?? const LoadingIndicator();
23
       return loadingWidget ?? const LoadingIndicator();
23
     }
24
     }
24
     
25
     
26
+    // 如果未认证,显示登录页
25
     if (!authProvider.isAuthenticated) {
27
     if (!authProvider.isAuthenticated) {
26
       return LoginScreen(
28
       return LoginScreen(
27
         onSuccess: () {
29
         onSuccess: () {
30
       );
32
       );
31
     }
33
     }
32
     
34
     
35
+    // 已认证,显示子组件
33
     return child;
36
     return child;
34
   }
37
   }
35
 }
38
 }

+ 92
- 4
pubspec.lock Просмотреть файл

148
     dependency: "direct main"
148
     dependency: "direct main"
149
     description:
149
     description:
150
       name: get_it
150
       name: get_it
151
-      sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
151
+      sha256: "1d648d2dd2047d7f7450d5727ca24ee435f240385753d90b49650e3cdff32e56"
152
       url: "https://pub.flutter-io.cn"
152
       url: "https://pub.flutter-io.cn"
153
     source: hosted
153
     source: hosted
154
-    version: "7.7.0"
154
+    version: "9.2.0"
155
   http:
155
   http:
156
     dependency: "direct main"
156
     dependency: "direct main"
157
     description:
157
     description:
172
     dependency: "direct main"
172
     dependency: "direct main"
173
     description:
173
     description:
174
       name: intl
174
       name: intl
175
-      sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
175
+      sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
176
       url: "https://pub.flutter-io.cn"
176
       url: "https://pub.flutter-io.cn"
177
     source: hosted
177
     source: hosted
178
-    version: "0.18.1"
178
+    version: "0.20.2"
179
   js:
179
   js:
180
     dependency: transitive
180
     dependency: transitive
181
     description:
181
     description:
248
       url: "https://pub.flutter-io.cn"
248
       url: "https://pub.flutter-io.cn"
249
     source: hosted
249
     source: hosted
250
     version: "1.0.0"
250
     version: "1.0.0"
251
+  package_info_plus:
252
+    dependency: "direct main"
253
+    description:
254
+      name: package_info_plus
255
+      sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
256
+      url: "https://pub.flutter-io.cn"
257
+    source: hosted
258
+    version: "4.2.0"
259
+  package_info_plus_platform_interface:
260
+    dependency: transitive
261
+    description:
262
+      name: package_info_plus_platform_interface
263
+      sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
264
+      url: "https://pub.flutter-io.cn"
265
+    source: hosted
266
+    version: "2.0.1"
251
   path:
267
   path:
252
     dependency: transitive
268
     dependency: transitive
253
     description:
269
     description:
445
       url: "https://pub.flutter-io.cn"
461
       url: "https://pub.flutter-io.cn"
446
     source: hosted
462
     source: hosted
447
     version: "1.4.0"
463
     version: "1.4.0"
464
+  url_launcher:
465
+    dependency: "direct main"
466
+    description:
467
+      name: url_launcher
468
+      sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
469
+      url: "https://pub.flutter-io.cn"
470
+    source: hosted
471
+    version: "6.3.2"
472
+  url_launcher_android:
473
+    dependency: transitive
474
+    description:
475
+      name: url_launcher_android
476
+      sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
477
+      url: "https://pub.flutter-io.cn"
478
+    source: hosted
479
+    version: "6.3.28"
480
+  url_launcher_ios:
481
+    dependency: transitive
482
+    description:
483
+      name: url_launcher_ios
484
+      sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
485
+      url: "https://pub.flutter-io.cn"
486
+    source: hosted
487
+    version: "6.3.6"
488
+  url_launcher_linux:
489
+    dependency: transitive
490
+    description:
491
+      name: url_launcher_linux
492
+      sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
493
+      url: "https://pub.flutter-io.cn"
494
+    source: hosted
495
+    version: "3.2.2"
496
+  url_launcher_macos:
497
+    dependency: transitive
498
+    description:
499
+      name: url_launcher_macos
500
+      sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
501
+      url: "https://pub.flutter-io.cn"
502
+    source: hosted
503
+    version: "3.2.5"
504
+  url_launcher_platform_interface:
505
+    dependency: transitive
506
+    description:
507
+      name: url_launcher_platform_interface
508
+      sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
509
+      url: "https://pub.flutter-io.cn"
510
+    source: hosted
511
+    version: "2.3.2"
512
+  url_launcher_web:
513
+    dependency: transitive
514
+    description:
515
+      name: url_launcher_web
516
+      sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
517
+      url: "https://pub.flutter-io.cn"
518
+    source: hosted
519
+    version: "2.4.1"
520
+  url_launcher_windows:
521
+    dependency: transitive
522
+    description:
523
+      name: url_launcher_windows
524
+      sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
525
+      url: "https://pub.flutter-io.cn"
526
+    source: hosted
527
+    version: "3.1.5"
448
   vector_graphics:
528
   vector_graphics:
449
     dependency: transitive
529
     dependency: transitive
450
     description:
530
     description:
493
       url: "https://pub.flutter-io.cn"
573
       url: "https://pub.flutter-io.cn"
494
     source: hosted
574
     source: hosted
495
     version: "1.1.1"
575
     version: "1.1.1"
576
+  win32:
577
+    dependency: transitive
578
+    description:
579
+      name: win32
580
+      sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
581
+      url: "https://pub.flutter-io.cn"
582
+    source: hosted
583
+    version: "5.15.0"
496
   xdg_directories:
584
   xdg_directories:
497
     dependency: transitive
585
     dependency: transitive
498
     description:
586
     description:

+ 4
- 2
pubspec.yaml Просмотреть файл

14
   http: ^1.1.0
14
   http: ^1.1.0
15
   shared_preferences: ^2.2.2
15
   shared_preferences: ^2.2.2
16
   flutter_svg: ^2.0.9
16
   flutter_svg: ^2.0.9
17
-  intl: ^0.18.1
18
-  get_it: ^7.6.4
17
+  intl: ^0.20.2
18
+  get_it: ^9.2.0
19
+  url_launcher: ^6.1.0
20
+  package_info_plus: ^4.0.1
19
   # 安全相关依赖
21
   # 安全相关依赖
20
   crypto: ^3.0.3        # SHA256等哈希算法
22
   crypto: ^3.0.3        # SHA256等哈希算法
21
   encrypt: ^5.0.1       # AES加密
23
   encrypt: ^5.0.1       # AES加密