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

fix: 修复iOS下的logo显示问题,增加刷新组件

lilu 3 месяцев назад
Родитель
Сommit
e9321e2048
70 измененных файлов с 1191 добавлено и 62 удалено
  1. BIN
      android/app/src/main/res/drawable-hdpi/ic_launcher_background.png
  2. BIN
      android/app/src/main/res/drawable-mdpi/ic_launcher_background.png
  3. BIN
      android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png
  4. BIN
      android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png
  5. BIN
      android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png
  6. BIN
      android/app/src/main/res/mipmap-hdpi/launcher_icon.png
  7. BIN
      android/app/src/main/res/mipmap-mdpi/launcher_icon.png
  8. BIN
      android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
  9. BIN
      android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
  10. BIN
      android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
  11. BIN
      assets/icon.png
  12. BIN
      assets/icon_b.png
  13. 3 0
      assets/vectors/arrow_down_circle.svg
  14. 6 0
      assets/vectors/failed_circle.svg
  15. 3 0
      assets/vectors/refresh_circle.svg
  16. 6 0
      assets/vectors/success_circle.svg
  17. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  18. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  19. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  20. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  21. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  22. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  23. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  24. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  25. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  26. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  27. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  28. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  29. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  30. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  31. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  32. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  33. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  34. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  35. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  36. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  37. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]
  38. 5 0
      lib/app/api/core/api_core.dart
  39. 3 0
      lib/app/api/core/api_core_paths.dart
  40. 26 1
      lib/app/app.dart
  41. 5 0
      lib/app/constants/assets.dart
  42. 1 0
      lib/app/constants/enums.dart
  43. 177 2
      lib/app/controllers/api_controller.dart
  44. 21 13
      lib/app/dialog/all_dialog.dart
  45. 33 6
      lib/app/modules/forgotpwd/controllers/forgotpwd_controller.dart
  46. 2 2
      lib/app/modules/forgotpwd/views/forgotpwd_view.dart
  47. 30 0
      lib/app/modules/login/controllers/login_controller.dart
  48. 1 1
      lib/app/modules/login/views/login_view.dart
  49. 77 33
      lib/app/modules/node/widgets/node_list.dart
  50. 40 0
      lib/app/modules/setting/controllers/setting_controller.dart
  51. 8 3
      lib/app/modules/setting/views/setting_view.dart
  52. 32 0
      lib/app/modules/signup/controllers/signup_controller.dart
  53. 1 1
      lib/app/modules/signup/views/signup_view.dart
  54. 679 0
      lib/app/widgets/gradient_circle_header.dart
  55. 23 0
      lib/config/translations/strings_enum.dart
  56. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
  57. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
  58. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
  59. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
  60. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
  61. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
  62. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
  63. 8 0
      pubspec.lock
  64. 1 0
      pubspec.yaml
  65. BIN
      web/favicon.png
  66. BIN
      web/icons/Icon-192.png
  67. BIN
      web/icons/Icon-512.png
  68. BIN
      web/icons/Icon-maskable-192.png
  69. BIN
      web/icons/Icon-maskable-512.png
  70. BIN
      windows/runner/resources/app_icon.ico

BIN
android/app/src/main/res/drawable-hdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-mdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png


BIN
android/app/src/main/res/mipmap-hdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png


BIN
assets/icon.png


BIN
assets/icon_b.png


+ 3 - 0
assets/vectors/arrow_down_circle.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.7895 9.10105L12.1924 10.5039C12.3382 10.6497 12.5196 10.7226 12.7368 10.7226C12.954 10.7226 13.1355 10.6497 13.2813 10.5039C13.4271 10.3583 13.5 10.1746 13.5 9.95289C13.5 9.73096 13.4271 9.54833 13.2813 9.405L10.6558 6.77947C10.4656 6.58912 10.2437 6.49395 9.99 6.49395C9.73614 6.49395 9.51412 6.58912 9.32395 6.77947L6.69842 9.405C6.55263 9.54833 6.47974 9.73096 6.47974 9.95289C6.47974 10.1746 6.55263 10.3583 6.69842 10.5039C6.84421 10.6497 7.02798 10.7226 7.24974 10.7226C7.47149 10.7226 7.65412 10.6497 7.79763 10.5039L9.20053 9.10105L9.20053 13.1074C9.20053 13.3311 9.27614 13.5185 9.42737 13.6697C9.57877 13.8211 9.7664 13.8968 9.99026 13.8968C10.2139 13.8968 10.4031 13.8211 10.5576 13.6697C10.7122 13.5185 10.7895 13.3311 10.7895 13.1074L10.7895 9.10105ZM9.99816 -8.74389e-07C11.3813 -7.53469e-07 12.6814 0.262454 13.8984 0.787366C15.1154 1.31228 16.174 2.02465 17.0742 2.92447C17.9744 3.8243 18.6871 4.88245 19.2124 6.09895C19.7375 7.31544 20 8.61518 20 9.99816C20 11.3813 19.7375 12.6814 19.2126 13.8984C18.6877 15.1154 17.9754 16.174 17.0755 17.0742C16.1757 17.9744 15.1175 18.6871 13.9011 19.2124C12.6846 19.7375 11.3848 20 10.0018 20C8.61868 20 7.31859 19.7375 6.10158 19.2126C4.88456 18.6877 3.82596 17.9753 2.92579 17.0755C2.02561 16.1757 1.31289 15.1175 0.787631 13.9011C0.262543 12.6846 7.53163e-07 11.3848 8.74067e-07 10.0018C9.94986e-07 8.61868 0.262456 7.31859 0.787368 6.10158C1.31228 4.88456 2.02465 3.82596 2.92447 2.92579C3.8243 2.02561 4.88246 1.31289 6.09895 0.787629C7.31544 0.262542 8.61518 -9.95293e-07 9.99816 -8.74389e-07Z" fill="white" style="fill:white;fill-opacity:1;"/>
+</svg>

+ 6 - 0
assets/vectors/failed_circle.svg

@@ -0,0 +1,6 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M10 0C15.5229 0 20 4.47715 20 10C20 15.5229 15.5229 20 10 20C4.47715 20 0 15.5229 0 10C0 4.47715 4.47715 0 10 0ZM14.5303 5.46973C14.2374 5.17683 13.7626 5.17683 13.4697 5.46973L10 8.93945L6.53027 5.46973C6.23738 5.17683 5.76262 5.17683 5.46973 5.46973C5.17683 5.76262 5.17683 6.23738 5.46973 6.53027L8.93945 10L5.46973 13.4697C5.17683 13.7626 5.17683 14.2374 5.46973 14.5303C5.76263 14.8231 6.23741 14.8231 6.53027 14.5303L10 11.0605L13.4697 14.5303C13.7626 14.8231 14.2374 14.8231 14.5303 14.5303C14.8231 14.2374 14.8231 13.7626 14.5303 13.4697L11.0605 10L14.5303 6.53027C14.8231 6.23741 14.8231 5.76263 14.5303 5.46973Z"
+    fill="#FF9C98"
+  />
+</svg>

+ 3 - 0
assets/vectors/refresh_circle.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 15C11.2667 15 12.3667 14.5833 13.3 13.75C14.2333 12.9167 14.7833 11.8833 14.95 10.65C14.9833 10.4667 14.925 10.3125 14.775 10.1875C14.625 10.0625 14.45 10 14.25 10C14.0667 10 13.8958 10.0583 13.7375 10.175C13.5792 10.2917 13.4833 10.4417 13.45 10.625C13.3 11.4417 12.9042 12.125 12.2625 12.675C11.6208 13.225 10.8667 13.5 10 13.5C9.03333 13.5 8.20833 13.1583 7.525 12.475C6.84167 11.7917 6.5 10.9667 6.5 10C6.5 9.03333 6.84167 8.20833 7.525 7.525C8.20833 6.84167 9.03333 6.5 10 6.5H10.075L9.375 7.225C9.24167 7.375 9.17083 7.55 9.1625 7.75C9.15417 7.95 9.225 8.125 9.375 8.275C9.525 8.425 9.70417 8.5 9.9125 8.5C10.1208 8.5 10.3 8.425 10.45 8.275L12.55 6.175C12.65 6.075 12.7 5.95833 12.7 5.825C12.7 5.69167 12.65 5.575 12.55 5.475L10.425 3.35C10.275 3.2 10.1 3.125 9.9 3.125C9.7 3.125 9.525 3.2 9.375 3.35C9.225 3.5 9.15 3.67917 9.15 3.8875C9.15 4.09583 9.225 4.275 9.375 4.425L9.925 5C8.55833 5.03333 7.39583 5.53333 6.4375 6.5C5.47917 7.46667 5 8.63333 5 10C5 11.3833 5.4875 12.5625 6.4625 13.5375C7.4375 14.5125 8.61667 15 10 15ZM10 20C8.61667 20 7.31667 19.7375 6.1 19.2125C4.88333 18.6875 3.825 17.975 2.925 17.075C2.025 16.175 1.3125 15.1167 0.7875 13.9C0.2625 12.6833 0 11.3833 0 10C0 8.61667 0.2625 7.31667 0.7875 6.1C1.3125 4.88333 2.025 3.825 2.925 2.925C3.825 2.025 4.88333 1.3125 6.1 0.7875C7.31667 0.2625 8.61667 0 10 0C11.3833 0 12.6833 0.2625 13.9 0.7875C15.1167 1.3125 16.175 2.025 17.075 2.925C17.975 3.825 18.6875 4.88333 19.2125 6.1C19.7375 7.31667 20 8.61667 20 10C20 11.3833 19.7375 12.6833 19.2125 13.9C18.6875 15.1167 17.975 16.175 17.075 17.075C16.175 17.975 15.1167 18.6875 13.9 19.2125C12.6833 19.7375 11.3833 20 10 20Z" fill="white" style="fill:white;fill-opacity:1;"/>
+</svg>

+ 6 - 0
assets/vectors/success_circle.svg

@@ -0,0 +1,6 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M10 0C12.7614 0 15.2616 1.11907 17.0713 2.92871C18.8809 4.73838 20 7.23861 20 10C20 12.7614 18.8809 15.2616 17.0713 17.0713C15.2616 18.8809 12.7614 20 10 20C7.23861 20 4.73838 18.8809 2.92871 17.0713C1.11907 15.2616 0 12.7614 0 10C0 7.2386 1.11907 4.73838 2.92871 2.92871C4.73838 1.11907 7.2386 0 10 0ZM15.0752 6.74219C14.7823 6.44963 14.3074 6.44941 14.0146 6.74219L9.08984 11.666L6.89355 9.46973C6.60064 9.17708 6.12582 9.17692 5.83301 9.46973C5.5404 9.76256 5.54043 10.2374 5.83301 10.5303L8.56055 13.2578C8.70109 13.3981 8.89221 13.4765 9.09082 13.4766C9.28954 13.4765 9.48052 13.3983 9.62109 13.2578L15.0752 7.80273C15.3681 7.50984 15.3681 7.03508 15.0752 6.74219Z"
+    fill="#5CF38C"
+  />
+</svg>

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]


+ 5 - 0
lib/app/api/core/api_core.dart

@@ -277,6 +277,11 @@ class ApiCore extends BaseApi {
     return post(ApiCorePaths.logout, data: data);
   }
 
+  /// 删除账户
+  Future<ApiResult> deleteAccount(dynamic data) async {
+    return post(ApiCorePaths.deleteAccount, data: data);
+  }
+
   /// 修改密码
   Future<ApiResult> changePassword(dynamic data) async {
     return post(ApiCorePaths.changePassword, data: data);

+ 3 - 0
lib/app/api/core/api_core_paths.dart

@@ -20,6 +20,9 @@ class ApiCorePaths {
   /// 退出登录
   static const String logout = '$_ver/user/logout';
 
+  /// 删除账户
+  static const String deleteAccount = '$_ver/user/delAccount';
+
   /// 修改密码
   static const String changePassword = '$_ver/user/changePassword';
 

+ 26 - 1
lib/app/app.dart

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:get/get.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
+import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
 import '../config/theme/ix_theme.dart';
 import '../config/translations/localization_service.dart';
 import '../config/translations/strings_enum.dart';
@@ -16,6 +17,7 @@ import 'routes/app_pages.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 
 import 'widgets/click_opacity.dart';
+import 'widgets/gradient_circle_header.dart';
 import 'widgets/triple_tap_detector.dart';
 
 class App extends StatelessWidget {
@@ -29,7 +31,30 @@ class App extends StatelessWidget {
       splitScreenMode: true,
       useInheritedMediaQuery: true,
       rebuildFactor: (old, data) => true,
-      builder: (context, widget) => _buildMaterialApp(),
+      builder: (context, widget) => RefreshConfiguration(
+        headerBuilder: () =>
+            GradientCircleHeader(), // Configure the default header indicator. If you have the same header indicator for each page, you need to set this
+        footerBuilder: () =>
+            ClassicFooter(), // Configure default bottom indicator
+        headerTriggerDistance: 100.0, // header trigger refresh trigger distance
+        springDescription: SpringDescription(
+          mass: 1,
+          stiffness: 1000,
+          damping: 100,
+        ), // custom spring back animate,the props meaning see the flutter api
+        maxOverScrollExtent:
+            100, //The maximum dragging range of the head. Set this property if a rush out of the view area occurs
+        maxUnderScrollExtent: 0, // Maximum dragging range at the bottom
+        enableScrollWhenRefreshCompleted:
+            true, //This property is incompatible with PageView and TabBarView. If you need TabBarView to slide left and right, you need to set it to true.
+        enableLoadingWhenFailed:
+            true, //In the case of load failure, users can still trigger more loads by gesture pull-up.
+        hideFooterWhenNotFull:
+            false, // Disable pull-up to load more functionality when Viewport is less than one screen
+        enableBallisticLoad:
+            true, // trigger load more by BallisticScrollActivity
+        child: _buildMaterialApp(),
+      ),
     );
   }
 

+ 5 - 0
lib/app/constants/assets.dart

@@ -97,4 +97,9 @@ class Assets {
   static const String bannerTest = 'assets/images/banner_test.png';
   static const String subscriptionBg = 'assets/images/subscription_bg.mp4';
   static const String mediaBg = 'assets/images/media_bg.jpg';
+
+  static const String arrowDownCircle = 'assets/vectors/arrow_down_circle.svg';
+  static const String refreshCircle = 'assets/vectors/refresh_circle.svg';
+  static const String successCircle = 'assets/vectors/success_circle.svg';
+  static const String failedCircle = 'assets/vectors/failed_circle.svg';
 }

+ 1 - 0
lib/app/constants/enums.dart

@@ -52,6 +52,7 @@ enum FirebaseEvent {
   register,
   login,
   logout,
+  deleteAccount,
   startBoost,
   cancelBoost,
   errorBoost,

+ 177 - 2
lib/app/controllers/api_controller.dart

@@ -438,8 +438,6 @@ class ApiController extends GetxService {
 
         // 保存app配置
         await IXSP.saveAppConfig(launchData.appConfig!);
-        // 保存vpn配置
-        // await IXSP.saveVpnConfig(launchData.vpnConfig!);
 
         return launchData;
       } on ApiException catch (_) {
@@ -473,4 +471,181 @@ class ApiController extends GetxService {
       }
     }
   }
+
+  Future<Launch> register(Map<String, dynamic> params) async {
+    while (true) {
+      try {
+        ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
+        final request = fp.toJson();
+        request.addAll(params);
+        final result = await ApiCore().register(request);
+        if (!result.success) {
+          throw Failure(
+            code: result.errorCode ?? '',
+            message: result.errorMessage ?? '',
+          );
+        }
+        final launchData = Launch.fromJson(result.data);
+
+        // 注册成功后上报firebase注册事件
+        sendAnalytics(FirebaseEvent.register);
+
+        // 保存 Launch 数据
+        await IXSP.saveLaunch(launchData);
+
+        // 初始化Launch
+        await initData(launchData);
+
+        return launchData;
+      } on ApiException catch (_) {
+        rethrow;
+      } on Failure catch (_) {
+        rethrow;
+      } on DioException catch (e) {
+        if (e.response?.statusCode == Errors.eRegionNotAvailable ||
+            e.response?.statusCode == Errors.eUserDisabled ||
+            e.response?.statusCode == Errors.eTokenExpired) {
+          rethrow;
+        } else {
+          if (await NetworkHelper.instance.isNetworkAvailable()) {
+            final url = await ApiDomains.instance.getNextApiUrl();
+            log(TAG, 'Launch request failed for URL $url: $e');
+            if (url.isEmpty) {
+              rethrow;
+            }
+            ApiCore().setbaseUrl(url);
+          } else {
+            rethrow;
+          }
+        }
+      } catch (e) {
+        final url = await ApiDomains.instance.getNextApiUrl();
+        log(TAG, 'Launch request failed for URL $url: $e');
+        if (url.isEmpty) {
+          rethrow;
+        }
+        ApiCore().setbaseUrl(url);
+      }
+    }
+  }
+
+  Future<Launch> login(Map<String, dynamic> params) async {
+    while (true) {
+      try {
+        ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
+        final request = fp.toJson();
+        request.addAll(params);
+        final result = await ApiCore().login(request);
+        if (!result.success) {
+          throw Failure(
+            code: result.errorCode ?? '',
+            message: result.errorMessage ?? '',
+          );
+        }
+        final launchData = Launch.fromJson(result.data);
+
+        // 注册成功后上报firebase注册事件
+        sendAnalytics(FirebaseEvent.login);
+
+        // 保存 Launch 数据
+        await IXSP.saveLaunch(launchData);
+
+        // 初始化Launch
+        await initData(launchData);
+
+        return launchData;
+      } on ApiException catch (_) {
+        rethrow;
+      } on Failure catch (_) {
+        rethrow;
+      } on DioException catch (e) {
+        if (e.response?.statusCode == Errors.eRegionNotAvailable ||
+            e.response?.statusCode == Errors.eUserDisabled ||
+            e.response?.statusCode == Errors.eTokenExpired) {
+          rethrow;
+        } else {
+          if (await NetworkHelper.instance.isNetworkAvailable()) {
+            final url = await ApiDomains.instance.getNextApiUrl();
+            log(TAG, 'Launch request failed for URL $url: $e');
+            if (url.isEmpty) {
+              rethrow;
+            }
+            ApiCore().setbaseUrl(url);
+          } else {
+            rethrow;
+          }
+        }
+      } catch (e) {
+        final url = await ApiDomains.instance.getNextApiUrl();
+        log(TAG, 'Launch request failed for URL $url: $e');
+        if (url.isEmpty) {
+          rethrow;
+        }
+        ApiCore().setbaseUrl(url);
+      }
+    }
+  }
+
+  Future<Launch> logout() async {
+    try {
+      final request = fp.toJson();
+      final result = await ApiCore().logout(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+      final launchData = Launch.fromJson(result.data);
+
+      // 登出成功后上报firebase登出事件
+      sendAnalytics(FirebaseEvent.logout);
+
+      // 保存 Launch 数据
+      await IXSP.saveLaunch(launchData);
+      return launchData;
+    } catch (e) {
+      rethrow;
+    }
+  }
+
+  Future<Launch> deleteAccount() async {
+    try {
+      final request = fp.toJson();
+      final result = await ApiCore().deleteAccount(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+      final launchData = Launch.fromJson(result.data);
+
+      // 登出成功后上报firebase登出事件
+      sendAnalytics(FirebaseEvent.deleteAccount);
+
+      // 保存 Launch 数据
+      await IXSP.saveLaunch(launchData);
+      return launchData;
+    } catch (e) {
+      rethrow;
+    }
+  }
+
+  Future<String> changePassword(Map<String, dynamic> params) async {
+    try {
+      final request = fp.toJson();
+      request.addAll(params);
+      final result = await ApiCore().changePassword(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+      return result.errorMessage ?? '';
+    } catch (e) {
+      rethrow;
+    }
+  }
 }

+ 21 - 13
lib/app/dialog/all_dialog.dart

@@ -3,7 +3,6 @@ import 'package:get/get.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/translations/strings_enum.dart';
 import '../constants/iconfont/iconfont.dart';
-import '../routes/app_pages.dart';
 import 'custom_dialog.dart';
 
 /// 弹窗使用示例
@@ -18,7 +17,6 @@ class AllDialog {
       iconColor: const Color(0xFFFF9500),
       onPressed: () {
         // 处理激活成功后的逻辑
-        print('Premium activated successfully');
         Navigator.of(Get.context!).pop();
       },
     );
@@ -34,7 +32,6 @@ class AllDialog {
       iconColor: Colors.white,
       onPressed: () {
         // 处理邮件发送成功后的逻辑
-        print('Email sent successfully');
         Navigator.of(Get.context!).pop();
       },
     );
@@ -53,19 +50,17 @@ class AllDialog {
       errorCode: '',
       onPressed: () {
         // 处理重试逻辑
-        print('Retry network connection');
         Navigator.of(Get.context!).pop();
       },
       onCancel: () {
         // 处理取消逻辑
-        print('Cancel network retry');
         Navigator.of(Get.context!).pop();
       },
     );
   }
 
   /// 显示退出登录确认弹窗
-  static void showLogoutConfirm() {
+  static void showLogoutConfirm(Function() onLogout) {
     CustomDialog.showError(
       title: Strings.logOut.tr,
       message: Strings.logOutConfirmMessage.tr,
@@ -76,14 +71,11 @@ class AllDialog {
       confirmButtonColor: const Color(0xFFFF3B30),
       onPressed: () {
         // 处理退出登录逻辑
-        print('User confirmed logout');
-        // 这里可以调用退出登录的API
         Navigator.of(Get.context!).pop();
-        Get.toNamed(Routes.LOGIN);
+        onLogout();
       },
       onCancel: () {
         // 处理取消退出逻辑
-        print('User cancelled logout');
         Navigator.of(Get.context!).pop();
       },
     );
@@ -99,7 +91,6 @@ class AllDialog {
       iconColor: Get.theme.textTheme.bodyLarge!.color,
       onPressed: () {
         // 处理邮件发送成功后的逻辑
-        print('Feedback submitted successfully');
         Navigator.of(Get.context!).pop();
       },
     );
@@ -129,12 +120,29 @@ class AllDialog {
       buttonText: Strings.invalidAuthorizationCodeButton.tr,
       onPressed: () {
         // 处理重试逻辑
-        print('Retry authorization code');
         Navigator.of(Get.context!).pop();
       },
       onCancel: () {
         // 处理取消逻辑
-        print('Cancel authorization code retry');
+        Navigator.of(Get.context!).pop();
+      },
+    );
+  }
+
+  // 显示删除账户确认弹窗
+  static void showDeleteAccountConfirm(Function() onDeleteAccount) {
+    CustomDialog.showError(
+      title: Strings.deleteAccount.tr,
+      message: Strings.deleteAccountConfirmMessage.tr,
+      buttonText: Strings.deleteAccount.tr,
+      cancelText: Strings.cancel.tr,
+      onPressed: () {
+        // 处理删除账户逻辑
+        Navigator.of(Get.context!).pop();
+        onDeleteAccount();
+      },
+      onCancel: () {
+        // 处理取消删除账户逻辑
         Navigator.of(Get.context!).pop();
       },
     );

+ 33 - 6
lib/app/modules/forgotpwd/controllers/forgotpwd_controller.dart

@@ -1,15 +1,20 @@
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 
+import '../../../../config/translations/strings_enum.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../dialog/loading/loading_dialog.dart';
+
 class ForgotpwdController extends GetxController {
+  final _apiController = Get.find<ApiController>();
   final usernameController = TextEditingController();
   final passwordController = TextEditingController();
   final usernameFocusNode = FocusNode();
   final passwordFocusNode = FocusNode();
-  final _isLogin = false.obs;
-  bool get isLogin => _isLogin.value;
-  set isLogin(bool value) {
-    _isLogin.value = value;
+  final _isChangePassword = false.obs;
+  bool get isChangePassword => _isChangePassword.value;
+  set isChangePassword(bool value) {
+    _isChangePassword.value = value;
   }
 
   @override
@@ -32,9 +37,9 @@ class ForgotpwdController extends GetxController {
     if (validatorInputValue(username) &&
         validatorInputValue(password) &&
         username == password) {
-      isLogin = true;
+      isChangePassword = true;
     } else {
-      isLogin = false;
+      isChangePassword = false;
     }
   }
 
@@ -50,4 +55,26 @@ class ForgotpwdController extends GetxController {
     // 只允许字母和数字并且是6-20位
     return value == usernameController.text;
   }
+
+  // 处理修改密码
+  Future<void> handleChangePassword() async {
+    if (!isChangePassword) {
+      return;
+    }
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: Strings.changingPassword.tr,
+      successText: Strings.changePasswordSuccessful.tr,
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.changePassword({
+          "newPassword": passwordController.text,
+        });
+      },
+      onSuccess: () {
+        // 成功后的操作
+        Get.back();
+      },
+    );
+  }
 }

+ 2 - 2
lib/app/modules/forgotpwd/views/forgotpwd_view.dart

@@ -82,8 +82,8 @@ class ForgotpwdView extends BaseView<ForgotpwdController> {
               156.verticalSpaceFromWidth,
               SubmitButton(
                 text: Strings.yes.tr,
-                enabled: controller.isLogin,
-                onPressed: () {},
+                enabled: controller.isChangePassword,
+                onPressed: controller.handleChangePassword,
               ),
             ],
           ),

+ 30 - 0
lib/app/modules/login/controllers/login_controller.dart

@@ -1,7 +1,13 @@
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 
+import '../../../../config/translations/strings_enum.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../dialog/loading/loading_dialog.dart';
+import '../../../routes/app_pages.dart';
+
 class LoginController extends GetxController {
+  final _apiController = Get.find<ApiController>();
   final usernameController = TextEditingController();
   final passwordController = TextEditingController();
   final usernameFocusNode = FocusNode();
@@ -43,4 +49,28 @@ class LoginController extends GetxController {
         value.length <= 20 &&
         RegExp(r'^[a-zA-Z0-9]+$').hasMatch(value);
   }
+
+  // 处理登录
+  Future<void> handleSignUp() async {
+    if (!isLogin) {
+      return;
+    }
+    final params = {
+      "account": usernameController.text.trim(),
+      "password": passwordController.text,
+    };
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: Strings.loggingIn.tr,
+      successText: Strings.loginSuccessful.tr,
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.login(params);
+      },
+      onSuccess: () {
+        // 成功后的操作
+        Get.offAllNamed(Routes.HOME);
+      },
+    );
+  }
 }

+ 1 - 1
lib/app/modules/login/views/login_view.dart

@@ -85,7 +85,7 @@ class LoginView extends BaseView<LoginController> {
               SubmitButton(
                 text: Strings.loginButton.tr,
                 enabled: controller.isLogin,
-                onPressed: () {},
+                onPressed: controller.handleSignUp,
               ),
               10.verticalSpaceFromWidth,
               // 底部注册链接

+ 77 - 33
lib/app/modules/node/widgets/node_list.dart

@@ -1,3 +1,4 @@
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_sticky_header/flutter_sticky_header.dart';
@@ -5,10 +6,12 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
+import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
 
 import '../../../constants/iconfont/iconfont.dart';
 import '../../../data/models/launch/groups.dart';
 import '../../../widgets/country_icon.dart';
+import '../../../widgets/gradient_circle_header.dart';
 import '../../home/controllers/home_controller.dart';
 import '../controllers/node_controller.dart';
 
@@ -43,6 +46,20 @@ class _NodeListState extends State<NodeList>
     });
   }
 
+  final RefreshController _refreshController = RefreshController(
+    initialRefresh: false,
+  );
+
+  void _onRefresh() async {
+    await Future.delayed(Duration(milliseconds: 2000));
+    _refreshController.refreshCompleted();
+  }
+
+  void _onLoading() async {
+    await Future.delayed(Duration(milliseconds: 1000));
+    _refreshController.loadComplete();
+  }
+
   @override
   Widget build(BuildContext context) {
     super.build(context);
@@ -79,43 +96,70 @@ class _NodeListState extends State<NodeList>
     final sortedTags = data.tags!.toList()
       ..sort((a, b) => (a.sort ?? 0).compareTo(b.sort ?? 0));
 
-    return CustomScrollView(
-      slivers: [
-        for (var tag in sortedTags)
-          if (groupedData.containsKey(tag.id) &&
-              groupedData[tag.id]!.isNotEmpty)
-            SliverStickyHeader(
-              header: Container(
-                color: Get.reactiveTheme.scaffoldBackgroundColor,
-                padding: const EdgeInsets.all(16),
-                child: Text(
-                  tag.name ?? '',
-                  style: TextStyle(
-                    color: Get.reactiveTheme.hintColor,
-                    fontSize: 16,
-                    fontWeight: FontWeight.bold,
+    return SmartRefresher(
+      enablePullDown: true,
+      enablePullUp: true,
+      header: GradientCircleHeader(),
+      physics: const ClampingScrollPhysics(),
+      footer: CustomFooter(
+        builder: (BuildContext context, LoadStatus? mode) {
+          Widget body;
+          if (mode == LoadStatus.idle) {
+            body = Text("pull up load");
+          } else if (mode == LoadStatus.loading) {
+            body = CupertinoActivityIndicator();
+          } else if (mode == LoadStatus.failed) {
+            body = Text("Load Failed!Click retry!");
+          } else if (mode == LoadStatus.canLoading) {
+            body = Text("release to load more");
+          } else {
+            body = Text("No more Data");
+          }
+          return SizedBox(height: 55.0, child: Center(child: body));
+        },
+      ),
+      controller: _refreshController,
+      onRefresh: _onRefresh,
+      onLoading: _onLoading,
+      child: CustomScrollView(
+        physics: const ClampingScrollPhysics(),
+        slivers: [
+          for (var tag in sortedTags)
+            if (groupedData.containsKey(tag.id) &&
+                groupedData[tag.id]!.isNotEmpty)
+              SliverStickyHeader(
+                header: Container(
+                  color: Get.reactiveTheme.scaffoldBackgroundColor,
+                  padding: const EdgeInsets.all(16),
+                  child: Text(
+                    tag.name ?? '',
+                    style: TextStyle(
+                      color: Get.reactiveTheme.hintColor,
+                      fontSize: 16,
+                      fontWeight: FontWeight.bold,
+                    ),
                   ),
                 ),
-              ),
-              sliver: SliverList(
-                delegate: SliverChildBuilderDelegate((context, i) {
-                  final locationList = groupedData[tag.id]![i];
-                  final countryCode = locationList.icon ?? '';
+                sliver: SliverList(
+                  delegate: SliverChildBuilderDelegate((context, i) {
+                    final locationList = groupedData[tag.id]![i];
+                    final countryCode = locationList.icon ?? '';
 
-                  return _CountrySection(
-                    locationList: locationList,
-                    // 传递展开状态
-                    expanded: _getExpandedState(countryCode),
-                    // 展开状态变化回调
-                    onExpandedChanged: (expanded) {
-                      _setExpandedState(countryCode, expanded);
-                    },
-                  );
-                }, childCount: groupedData[tag.id]!.length),
+                    return _CountrySection(
+                      locationList: locationList,
+                      // 传递展开状态
+                      expanded: _getExpandedState(countryCode),
+                      // 展开状态变化回调
+                      onExpandedChanged: (expanded) {
+                        _setExpandedState(countryCode, expanded);
+                      },
+                    );
+                  }, childCount: groupedData[tag.id]!.length),
+                ),
               ),
-            ),
-        SliverSafeArea(sliver: SliverToBoxAdapter(child: 0.verticalSpace)),
-      ],
+          SliverSafeArea(sliver: SliverToBoxAdapter(child: 0.verticalSpace)),
+        ],
+      ),
     );
   }
 }

+ 40 - 0
lib/app/modules/setting/controllers/setting_controller.dart

@@ -1,6 +1,12 @@
 import 'package:get/get.dart';
 
+import '../../../../config/translations/strings_enum.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../dialog/loading/loading_dialog.dart';
+
 class SettingController extends GetxController {
+  final _apiController = Get.find<ApiController>();
+
   // 自动重连开关
   final autoReconnect = true.obs;
 
@@ -10,4 +16,38 @@ class SettingController extends GetxController {
   void onInit() {
     super.onInit();
   }
+
+  // 处理退出登录
+  Future<void> handleLogout() async {
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: Strings.loggingOut.tr,
+      successText: Strings.logoutSuccessful.tr,
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.logout();
+      },
+      onSuccess: () {
+        // 成功后的操作
+        Get.back();
+      },
+    );
+  }
+
+  // 处理删除账户
+  Future<void> handleDeleteAccount() async {
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: Strings.deletingAccount.tr,
+      successText: Strings.deleteAccountSuccessful.tr,
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.deleteAccount();
+      },
+      onSuccess: () {
+        // 成功后的操作
+        Get.back();
+      },
+    );
+  }
 }

+ 8 - 3
lib/app/modules/setting/views/setting_view.dart

@@ -489,7 +489,10 @@ class SettingView extends BaseView<SettingController> {
               ),
               title: Strings.deleteAccount.tr,
               onTap: () {
-                // TODO: 删除账户
+                AllDialog.showDeleteAccountConfirm(() {
+                  // 退出登录
+                  controller.handleDeleteAccount();
+                });
               },
             ),
             _buildDivider(),
@@ -508,8 +511,10 @@ class SettingView extends BaseView<SettingController> {
               title: Strings.logout.tr,
               titleColor: const Color(0xFFEF0000),
               onTap: () {
-                // TODO: 退出登录
-                AllDialog.showLogoutConfirm();
+                AllDialog.showLogoutConfirm(() {
+                  // 退出登录
+                  controller.handleLogout();
+                });
               },
             ),
           ],

+ 32 - 0
lib/app/modules/signup/controllers/signup_controller.dart

@@ -1,7 +1,14 @@
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 
+import '../../../../config/translations/strings_enum.dart';
+import '../../../constants/enums.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../dialog/loading/loading_dialog.dart';
+import '../../../routes/app_pages.dart';
+
 class SignupController extends GetxController {
+  final _apiController = Get.find<ApiController>();
   final usernameController = TextEditingController();
   final passwordController = TextEditingController();
   final usernameFocusNode = FocusNode();
@@ -43,4 +50,29 @@ class SignupController extends GetxController {
         value.length <= 20 &&
         RegExp(r'^[a-zA-Z0-9]+$').hasMatch(value);
   }
+
+  // 处理注册
+  Future<void> handleSignUp() async {
+    if (!isSignup) {
+      return;
+    }
+    final params = {
+      "account": usernameController.text.trim(),
+      "password": passwordController.text,
+      "registMode": RegisterMode.manual.value,
+    };
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: Strings.signingUp.tr,
+      successText: Strings.signUpSuccessful.tr,
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.register(params);
+      },
+      onSuccess: () {
+        // 成功后的操作
+        Get.offAllNamed(Routes.HOME);
+      },
+    );
+  }
 }

+ 1 - 1
lib/app/modules/signup/views/signup_view.dart

@@ -85,7 +85,7 @@ class SignupView extends BaseView<SignupController> {
               SubmitButton(
                 text: Strings.signupButton.tr,
                 enabled: controller.isSignup,
-                onPressed: () {},
+                onPressed: controller.handleSignUp,
               ),
               10.verticalSpaceFromWidth,
               // 底部注册链接

+ 679 - 0
lib/app/widgets/gradient_circle_header.dart

@@ -0,0 +1,679 @@
+import 'dart:async';
+import 'package:flutter/material.dart'
+    hide RefreshIndicatorState, RefreshIndicator;
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
+import '../constants/assets.dart';
+
+/// 自定义渐变圆形到圆柱形刷新头部
+class GradientCircleHeader extends RefreshIndicator {
+  /// refreshing content
+  final Widget? refresh;
+
+  /// complete content
+  final Widget? complete;
+
+  /// failed content
+  final Widget? failed;
+
+  /// idle Icon center in circle
+  final Widget idleIcon;
+
+  GradientCircleHeader({
+    super.key,
+    this.refresh,
+    this.complete,
+    super.completeDuration = const Duration(milliseconds: 600),
+    this.failed,
+    Widget? idleIcon,
+  }) : idleIcon =
+           idleIcon ??
+           SvgPicture.asset(Assets.arrowDownCircle, width: 20, height: 20),
+       super(height: 60.0, refreshStyle: RefreshStyle.UnFollow);
+
+  @override
+  State<StatefulWidget> createState() {
+    return _GradientCircleHeaderState();
+  }
+}
+
+class _GradientCircleHeaderState
+    extends RefreshIndicatorState<GradientCircleHeader>
+    with TickerProviderStateMixin {
+  AnimationController? _animationController;
+  late AnimationController _dismissCtl;
+  late AnimationController _rotateCtl; // 旋转动画控制器
+  late AnimationController _colorCtl; // 颜色过渡动画控制器
+  double _currentOffset = 0.0; // 跟踪当前下拉距离
+
+  // 默认状态的背景渐变
+  final Color _defaultBgStartColor = const Color(0xA6BABABA);
+  final Color _defaultBgEndColor = const Color(0x33404040);
+
+  // 默认状态的边框渐变
+  final List<Color> _defaultBorderColors = const [
+    Color(0xFFDFDFDF),
+    Color(0xFFA2A2A2),
+    Color(0x4F6B6B6B),
+  ];
+
+  // 当前状态的背景渐变颜色
+  Color _currentBgStartColor = const Color(0xA6BABABA);
+  Color _currentBgEndColor = const Color(0x33404040);
+
+  // 当前状态的边框渐变颜色
+  List<Color> _currentBorderColors = [
+    const Color(0xFFDFDFDF),
+    const Color(0xFFA2A2A2),
+    const Color(0x4F6B6B6B),
+  ];
+
+  // 成功状态的背景渐变
+  final Color _successBgStartColor = const Color(0xFF0D2116);
+  final Color _successBgEndColor = const Color(0xFF228643);
+
+  // 成功状态的边框渐变
+  final List<Color> _successBorderColors = [
+    const Color(0xFF253B2E),
+    const Color(0xFF41905A),
+    const Color(0xFF7BF4AB),
+  ];
+
+  // 失败状态的背景渐变
+  final Color _failedBgStartColor = const Color(0xFF1E1215);
+  final Color _failedBgEndColor = const Color(0xFFAA4140);
+
+  // 失败状态的边框渐变
+  final List<Color> _failedBorderColors = [
+    const Color(0xFF4B3033),
+    const Color(0xFFDC7A7F),
+    const Color(0xFFFFBDBB),
+  ];
+
+  @override
+  void onOffsetChange(double offset) {
+    // 圆形完全显示需要的距离:topMargin(12) + diameter(30) = 42
+    const double circleFullyVisibleOffset = 42.0;
+
+    // 更新当前下拉距离(但在特定状态下固定为完整圆形的距离)
+    if (mounted) {
+      setState(() {
+        // 如果正在刷新、成功、失败状态,固定 offset 为完整圆形的距离
+        if (mode == RefreshStatus.refreshing ||
+            mode == RefreshStatus.completed ||
+            mode == RefreshStatus.failed) {
+          _currentOffset = circleFullyVisibleOffset;
+        } else {
+          _currentOffset = offset;
+        }
+      });
+    }
+
+    // 如果正在刷新、成功、失败状态,保持圆形状态但不强制设置值
+    // 让 readyToRefresh 的动画自然完成
+    if (mode == RefreshStatus.refreshing ||
+        mode == RefreshStatus.completed ||
+        mode == RefreshStatus.failed) {
+      // 不在这里强制设置值,避免打断动画造成闪烁
+      return;
+    }
+
+    // 如果动画正在执行(收缩动画),不打断它
+    if (_animationController!.isAnimating) {
+      return;
+    }
+
+    // 跟随 offset 变化
+    if (offset <= circleFullyVisibleOffset) {
+      // 第一阶段:圆形逐渐显示,不变形
+      _animationController!.value = 0.0;
+    } else {
+      // 第二阶段:圆形完全显示后,继续下拉变成圆柱
+      final double cylinderOffset = offset - circleFullyVisibleOffset;
+      _animationController!.value = cylinderOffset.clamp(0.0, 50.0);
+    }
+  }
+
+  @override
+  Future<void> readyToRefresh() {
+    // 使用短时间的快速动画收缩到圆形
+    return _animationController!.animateTo(
+      0.0,
+      duration: Duration(milliseconds: 150), // 快速收缩
+      curve: Curves.easeOut,
+    );
+  }
+
+  @override
+  void initState() {
+    _dismissCtl = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 400),
+      value: 1.0,
+    );
+    _animationController = AnimationController(
+      vsync: this,
+      lowerBound: 0.0,
+      upperBound: 50.0,
+      duration: Duration(milliseconds: 150), // 缩短动画时长以匹配快速回弹
+    );
+    _rotateCtl = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 1000),
+    );
+    _colorCtl = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 300),
+    );
+    super.initState();
+  }
+
+  // 颜色过渡方法
+  void _animateToColor({
+    required Color bgStartColor,
+    required Color bgEndColor,
+    required List<Color> borderColors,
+  }) {
+    // 如果已经是目标颜色,不需要动画
+    if (_currentBgStartColor == bgStartColor &&
+        _currentBgEndColor == bgEndColor) {
+      return;
+    }
+
+    final ColorTween bgStartTween = ColorTween(
+      begin: _currentBgStartColor,
+      end: bgStartColor,
+    );
+    final ColorTween bgEndTween = ColorTween(
+      begin: _currentBgEndColor,
+      end: bgEndColor,
+    );
+
+    void listener() {
+      if (mounted) {
+        setState(() {
+          _currentBgStartColor = bgStartTween.evaluate(_colorCtl)!;
+          _currentBgEndColor = bgEndTween.evaluate(_colorCtl)!;
+          _currentBorderColors = borderColors; // 直接设置边框颜色
+        });
+      }
+    }
+
+    _colorCtl.removeListener(listener);
+    _colorCtl.addListener(listener);
+
+    _colorCtl.reset();
+    _colorCtl.forward().then((_) {
+      _colorCtl.removeListener(listener);
+    });
+  }
+
+  @override
+  bool needReverseAll() {
+    return false;
+  }
+
+  @override
+  Widget buildContent(BuildContext context, RefreshStatus? mode) {
+    Widget? child;
+    if (mode == RefreshStatus.refreshing) {
+      // 刷新状态:启动旋转动画
+      if (!_rotateCtl.isAnimating) {
+        _rotateCtl.repeat();
+      }
+
+      // 确保可见
+      if (_dismissCtl.value != 1.0) {
+        _dismissCtl.value = 1.0;
+      }
+
+      // 确保动画值是0(圆形状态),但不强制设置避免闪烁
+      // 如果动画还在执行,让它完成
+      if (!_animationController!.isAnimating &&
+          _animationController!.value != 0.0) {
+        _animationController!.value = 0.0;
+      }
+
+      // 和 idle 状态一样显示圆形边框,但图标是旋转的 refresh
+      const double circleRadius = 15.0;
+      const double topMargin = 12.0;
+      final double topCircleCenterY = topMargin + circleRadius;
+      final double cylinderHeight = _animationController?.value ?? 0.0;
+      final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
+      final double arrowTopPosition =
+          bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
+
+      return widget.refresh ??
+          SizedBox(
+            height: 60.0,
+            child: Stack(
+              clipBehavior: Clip.none,
+              children: <Widget>[
+                // 绘制圆形边框
+                CustomPaint(
+                  painter: _GradientCircleToCylinderPainter(
+                    listener: _animationController,
+                    bgStartColor: _currentBgStartColor,
+                    bgEndColor: _currentBgEndColor,
+                    borderColors: _currentBorderColors,
+                    currentOffset: _currentOffset,
+                    isDefaultStyle: true, // 刷新状态使用默认样式渐变方向
+                  ),
+                  child: Container(height: 60.0),
+                ),
+                // 旋转的刷新图标
+                Positioned(
+                  left: 0,
+                  right: 0,
+                  top: arrowTopPosition,
+                  child: RotationTransition(
+                    turns: _rotateCtl,
+                    child: SvgPicture.asset(
+                      Assets.refreshCircle,
+                      width: 20,
+                      height: 20,
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          );
+    } else if (mode == RefreshStatus.completed) {
+      // 停止旋转动画,启动颜色过渡动画
+      _rotateCtl.stop();
+      _animateToColor(
+        bgStartColor: _successBgStartColor,
+        bgEndColor: _successBgEndColor,
+        borderColors: _successBorderColors,
+      );
+
+      // 重置 dismiss 控制器,确保可见
+      if (_dismissCtl.value != 1.0) {
+        _dismissCtl.value = 1.0;
+      }
+
+      // 确保动画值是0(圆形状态)
+      if (!_animationController!.isAnimating &&
+          _animationController!.value != 0.0) {
+        _animationController!.value = 0.0;
+      }
+
+      // 和刷新状态一样显示圆形边框,但颜色变绿,图标变成成功图标
+      const double circleRadius = 15.0;
+      const double topMargin = 12.0;
+      final double topCircleCenterY = topMargin + circleRadius;
+      final double cylinderHeight = _animationController?.value ?? 0.0;
+      final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
+      final double arrowTopPosition =
+          bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
+
+      return widget.complete ??
+          FadeTransition(
+            opacity: _dismissCtl,
+            child: SizedBox(
+              height: 60.0,
+              child: Stack(
+                clipBehavior: Clip.none,
+                children: <Widget>[
+                  // 绘制圆形边框(绿色渐变)
+                  CustomPaint(
+                    painter: _GradientCircleToCylinderPainter(
+                      listener: _animationController,
+                      bgStartColor: _currentBgStartColor,
+                      bgEndColor: _currentBgEndColor,
+                      borderColors: _currentBorderColors,
+                      currentOffset: _currentOffset,
+                    ),
+                    child: Container(height: 60.0),
+                  ),
+                  // 成功图标
+                  Positioned(
+                    left: 0,
+                    right: 0,
+                    top: arrowTopPosition,
+                    child: SvgPicture.asset(
+                      Assets.successCircle,
+                      width: 20,
+                      height: 20,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          );
+    } else if (mode == RefreshStatus.failed) {
+      // 停止旋转动画,启动颜色过渡动画
+      _rotateCtl.stop();
+      _animateToColor(
+        bgStartColor: _failedBgStartColor,
+        bgEndColor: _failedBgEndColor,
+        borderColors: _failedBorderColors,
+      );
+
+      // 重置 dismiss 控制器,确保可见
+      if (_dismissCtl.value != 1.0) {
+        _dismissCtl.value = 1.0;
+      }
+
+      // 确保动画值是0(圆形状态)
+      if (!_animationController!.isAnimating &&
+          _animationController!.value != 0.0) {
+        _animationController!.value = 0.0;
+      }
+
+      // 和刷新状态一样显示圆形边框,但颜色变红,图标变成失败图标
+      const double circleRadius = 15.0;
+      const double topMargin = 12.0;
+      final double topCircleCenterY = topMargin + circleRadius;
+      final double cylinderHeight = _animationController?.value ?? 0.0;
+      final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
+      final double arrowTopPosition =
+          bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
+
+      return widget.failed ??
+          FadeTransition(
+            opacity: _dismissCtl,
+            child: SizedBox(
+              height: 60.0,
+              child: Stack(
+                clipBehavior: Clip.none,
+                children: <Widget>[
+                  // 绘制圆形边框(红色渐变)
+                  CustomPaint(
+                    painter: _GradientCircleToCylinderPainter(
+                      listener: _animationController,
+                      bgStartColor: _currentBgStartColor,
+                      bgEndColor: _currentBgEndColor,
+                      borderColors: _currentBorderColors,
+                      currentOffset: _currentOffset,
+                    ),
+                    child: Container(height: 60.0),
+                  ),
+                  // 失败图标
+                  Positioned(
+                    left: 0,
+                    right: 0,
+                    top: arrowTopPosition,
+                    child: SvgPicture.asset(
+                      Assets.failedCircle,
+                      width: 20,
+                      height: 20,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          );
+    } else if (mode == RefreshStatus.idle || mode == RefreshStatus.canRefresh) {
+      // 停止旋转动画,恢复原始颜色
+      _rotateCtl.stop();
+      _animateToColor(
+        bgStartColor: _defaultBgStartColor,
+        bgEndColor: _defaultBgEndColor,
+        borderColors: _defaultBorderColors,
+      );
+
+      // 重置 dismiss 控制器,确保可见
+      if (_dismissCtl.value != 1.0) {
+        _dismissCtl.value = 1.0;
+      }
+
+      // 计算箭头位置 - 始终在底部圆(30x30)的中心
+      const double circleRadius = 15.0;
+      const double topMargin = 12.0;
+      final double topCircleCenterY = topMargin + circleRadius; // 顶部圆心Y坐标 = 27
+
+      // 圆柱高度
+      final double cylinderHeight = _animationController?.value ?? 0.0;
+
+      // 箭头在底部圆的中心:顶部圆心 + 圆柱延伸高度
+      // 当cylinderHeight=0时,箭头在顶部圆中心
+      // 当cylinderHeight>0时,箭头在底部圆中心(底部圆心 = 顶部圆心 + 圆柱高度)
+      final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
+      final double arrowTopPosition =
+          bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2),让图标居中
+
+      return FadeTransition(
+        opacity: _dismissCtl,
+        child: SizedBox(
+          height: 60.0,
+          child: Stack(
+            clipBehavior: Clip.none,
+            children: <Widget>[
+              // 绘制圆形到圆柱形的渐变边框
+              CustomPaint(
+                painter: _GradientCircleToCylinderPainter(
+                  listener: _animationController,
+                  bgStartColor: _currentBgStartColor,
+                  bgEndColor: _currentBgEndColor,
+                  borderColors: _currentBorderColors,
+                  currentOffset: _currentOffset,
+                  isDefaultStyle: true, // idle/canRefresh 状态使用默认样式渐变方向
+                ),
+                child: Container(height: 60.0),
+              ),
+              // 箭头在底部圆(30x30)的中心
+              Positioned(
+                left: 0,
+                right: 0,
+                top: arrowTopPosition,
+                child: widget.idleIcon,
+              ),
+            ],
+          ),
+        ),
+      );
+    }
+    return SizedBox(height: 60.0, child: Center(child: child));
+  }
+
+  @override
+  void resetValue() {
+    _animationController!.reset();
+    _dismissCtl.value = 1.0;
+    _currentOffset = 0.0;
+    _rotateCtl.reset();
+    _colorCtl.reset();
+    // 重置为原始颜色
+    _currentBgStartColor = _defaultBgStartColor;
+    _currentBgEndColor = _defaultBgEndColor;
+    _currentBorderColors = List.from(_defaultBorderColors);
+  }
+
+  @override
+  void dispose() {
+    _dismissCtl.dispose();
+    _animationController!.dispose();
+    _rotateCtl.dispose();
+    _colorCtl.dispose();
+    super.dispose();
+  }
+}
+
+/// 绘制从圆形到圆柱形的渐变边框效果
+class _GradientCircleToCylinderPainter extends CustomPainter {
+  final Animation<double>? listener;
+  final Color bgStartColor;
+  final Color bgEndColor;
+  final List<Color> borderColors;
+  final double currentOffset; // 当前下拉距离
+  final bool isDefaultStyle; // 是否为默认/刷新状态的样式
+
+  double get value => listener!.value;
+
+  _GradientCircleToCylinderPainter({
+    this.listener,
+    required this.bgStartColor,
+    required this.bgEndColor,
+    required this.borderColors,
+    required this.currentOffset,
+    this.isDefaultStyle = false,
+  }) : super(repaint: listener);
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final double middleW = size.width / 2;
+    final double circleRadius = 15.0; // 圆的半径(改为15)
+    final double strokeWidth = 2.0; // 边框宽度
+    final double topMargin = 12.0; // 顶部边距
+    const double circleFullyVisibleOffset = 42.0; // 圆形完全显示需要的距离(12 + 30 = 42)
+
+    // 圆心位置
+    final double circleCenterY = topMargin + circleRadius;
+
+    // 计算下拉的距离,控制圆柱的高度
+    final double pullDistance = value;
+
+    // 背景渐变色
+    final Gradient backgroundGradient = LinearGradient(
+      colors: [bgStartColor, bgEndColor],
+      // 默认/刷新状态:从右到左,其他状态:从上到下
+      begin: isDefaultStyle ? Alignment.centerRight : Alignment.topCenter,
+      end: isDefaultStyle ? Alignment.centerLeft : Alignment.bottomCenter,
+    );
+
+    // 边框渐变色(支持多个颜色)
+    final Gradient borderGradient = LinearGradient(
+      colors: borderColors,
+      // 默认/刷新状态:从下到上(反转),其他状态:从上到下
+      begin: isDefaultStyle ? Alignment.bottomCenter : Alignment.topCenter,
+      end: isDefaultStyle ? Alignment.topCenter : Alignment.bottomCenter,
+    );
+
+    // 阴影效果
+    final Paint shadowPaint = Paint()
+      ..color = Colors.black.withOpacity(0.1)
+      ..maskFilter = MaskFilter.blur(BlurStyle.normal, 2);
+
+    // 第一阶段:圆形逐渐显示(使用裁剪)
+    if (currentOffset < circleFullyVisibleOffset) {
+      canvas.save();
+      // 裁剪显示区域,让圆形从顶部逐渐出现
+      canvas.clipRect(Rect.fromLTWH(0, 0, size.width, currentOffset));
+
+      // 绘制阴影
+      canvas.drawCircle(
+        Offset(middleW, circleCenterY + 1),
+        circleRadius,
+        shadowPaint,
+      );
+
+      // 绘制背景
+      final Rect bgRect = Rect.fromCircle(
+        center: Offset(middleW, circleCenterY),
+        radius: circleRadius,
+      );
+      final Paint bgPaint = Paint()
+        ..shader = backgroundGradient.createShader(bgRect)
+        ..style = PaintingStyle.fill;
+      canvas.drawCircle(Offset(middleW, circleCenterY), circleRadius, bgPaint);
+
+      // 绘制边框(渐变)
+      final Paint borderPaint = Paint()
+        ..shader = borderGradient.createShader(bgRect)
+        ..style = PaintingStyle.stroke
+        ..strokeWidth = strokeWidth
+        ..strokeCap = StrokeCap.round;
+      canvas.drawCircle(
+        Offset(middleW, circleCenterY),
+        circleRadius,
+        borderPaint,
+      );
+      canvas.restore();
+    } else {
+      // 第二阶段:绘制圆柱形效果(包括 pullDistance = 0 的情况)
+      if (pullDistance <= 0.1) {
+        // pullDistance很小时,只绘制圆形
+        // 绘制阴影
+        canvas.drawCircle(
+          Offset(middleW, circleCenterY + 1),
+          circleRadius,
+          shadowPaint,
+        );
+
+        // 绘制背景
+        final Rect bgRect = Rect.fromCircle(
+          center: Offset(middleW, circleCenterY),
+          radius: circleRadius,
+        );
+        final Paint bgPaint = Paint()
+          ..shader = backgroundGradient.createShader(bgRect)
+          ..style = PaintingStyle.fill;
+        canvas.drawCircle(
+          Offset(middleW, circleCenterY),
+          circleRadius,
+          bgPaint,
+        );
+
+        // 绘制边框(渐变)
+        final Paint borderPaint = Paint()
+          ..shader = borderGradient.createShader(bgRect)
+          ..style = PaintingStyle.stroke
+          ..strokeWidth = strokeWidth
+          ..strokeCap = StrokeCap.round;
+        canvas.drawCircle(
+          Offset(middleW, circleCenterY),
+          circleRadius,
+          borderPaint,
+        );
+      } else {
+        // 绘制圆柱
+        Path path = Path();
+
+        // 绘制顶部半圆(上半部分)
+        path.addArc(
+          Rect.fromCircle(
+            center: Offset(middleW, circleCenterY),
+            radius: circleRadius,
+          ),
+          -3.14159, // π (左侧)
+          3.14159, // π (到右侧)
+        );
+
+        // 右侧垂直线
+        path.lineTo(middleW + circleRadius, circleCenterY + pullDistance);
+
+        // 绘制底部半圆
+        path.addArc(
+          Rect.fromCircle(
+            center: Offset(middleW, circleCenterY + pullDistance),
+            radius: circleRadius,
+          ),
+          0, // 0 (右侧)
+          3.14159, // π (到左侧)
+        );
+
+        // 左侧垂直线(回到起点)
+        path.lineTo(middleW - circleRadius, circleCenterY);
+
+        // 绘制阴影
+        canvas.drawPath(path.shift(Offset(0, 1)), shadowPaint);
+
+        // 绘制背景
+        final Rect bgRect = Rect.fromLTWH(
+          middleW - circleRadius,
+          circleCenterY - circleRadius,
+          circleRadius * 2,
+          circleRadius * 2 + pullDistance,
+        );
+        final Paint bgPaint = Paint()
+          ..shader = backgroundGradient.createShader(bgRect)
+          ..style = PaintingStyle.fill;
+        canvas.drawPath(path, bgPaint);
+
+        // 绘制边框(渐变)
+        final Paint borderPaint = Paint()
+          ..shader = borderGradient.createShader(bgRect)
+          ..style = PaintingStyle.stroke
+          ..strokeWidth = strokeWidth
+          ..strokeCap = StrokeCap.round;
+        canvas.drawPath(path, borderPaint);
+      }
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) {
+    return true;
+  }
+}

+ 23 - 0
lib/config/translations/strings_enum.dart

@@ -397,4 +397,27 @@ class Strings {
   static const String confirmPasswordMustBeTheSame =
       'The passwords entered twice are inconsistent';
   static const String yes = 'Yes';
+
+  // Signup
+  static const String signingUp = 'Signing up...';
+  static const String signUpSuccessful = 'Sign up successful';
+
+  // login
+  static const String loggingIn = 'Logging in...';
+  static const String loginSuccessful = 'Login successful';
+
+  // logout
+  static const String loggingOut = 'Logging out...';
+  static const String logoutSuccessful = 'Logout successful';
+
+  // change password
+  static const String changingPassword = 'Changing password...';
+  static const String changePasswordSuccessful = 'Change password successful';
+
+  // delete account
+  static const String deletingAccount = 'Deleting account...';
+  static const String deleteAccountSuccessful = 'Delete account successful';
+  static const String deleteAccountConfirmMessage =
+      'Deleting your account will permanently remove your data and membership information. This action cannot be undone.';
+  static const String deleteAccountConfirmButton = 'Delete';
 }

BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png


BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png


BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png


BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png


BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png


BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png


BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png


+ 8 - 0
pubspec.lock

@@ -1208,6 +1208,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.5.0"
+  pull_to_refresh_flutter3:
+    dependency: "direct main"
+    description:
+      name: pull_to_refresh_flutter3
+      sha256: "37a88d901cca9a46dbdd46523de8e7b35a3e58634a0e775b1a5904981f69b353"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.2"
   rename_app:
     dependency: "direct dev"
     description:

+ 1 - 0
pubspec.yaml

@@ -82,6 +82,7 @@ dependencies:
   video_player: ^2.10.1 # 视频播放器
   in_app_purchase: ^3.2.3 # 内购
   carousel_slider: ^5.1.1 # 轮播图
+  pull_to_refresh_flutter3: ^2.0.2 # 下拉刷新
 
 dev_dependencies:
   flutter_test:

BIN
web/favicon.png


BIN
web/icons/Icon-192.png


BIN
web/icons/Icon-512.png


BIN
web/icons/Icon-maskable-192.png


BIN
web/icons/Icon-maskable-512.png


BIN
windows/runner/resources/app_icon.ico