Parcourir la source

[feat]实现手机号登录页

云天逵 il y a 7 mois
Parent
commit
8fa5c9d9cd
44 fichiers modifiés avec 1953 ajouts et 98 suppressions
  1. BIN
      assets/images/bg_login.webp
  2. BIN
      assets/images/bg_login_dialog.webp
  3. BIN
      assets/images/icon_login_agree_privacy.webp
  4. BIN
      assets/images/icon_login_dialog_close.webp
  5. BIN
      assets/images/icon_login_dialog_phone_logo.webp
  6. BIN
      assets/images/icon_login_dialog_wechat_logo_white.webp
  7. BIN
      assets/images/icon_user_info_arrow_left.webp
  8. BIN
      assets/images/icon_user_info_logout.webp
  9. BIN
      assets/images/icon_wechat_logo_black.webp
  10. 32 0
      assets/string/base/string.xml
  11. 2 1
      lib/data/api/atmob_api.dart
  12. 1 1
      lib/data/api/atmob_api.g.dart
  13. 3 3
      lib/data/api/request/character_custom_delete_request.dart
  14. 2 2
      lib/data/api/request/character_custom_delete_request.g.dart
  15. 6 0
      lib/data/repository/account_repository.dart
  16. 1 1
      lib/data/repository/characters_repository.dart
  17. 37 2
      lib/data/repository/keyboard_repository.dart
  18. 18 1
      lib/di/get_it.config.dart
  19. 124 0
      lib/dialog/common_alert_dialog.dart
  20. 34 0
      lib/dialog/common_alert_dialog_impl.dart
  21. 137 0
      lib/dialog/deprecate_diolog.dart
  22. 23 0
      lib/dialog/login/login_dialog.dart
  23. 81 0
      lib/dialog/login/login_dialog_controller.dart
  24. 224 0
      lib/dialog/login/login_dialog_view.dart
  25. 160 0
      lib/dialog/privacy_agreement_dialog.dart
  26. 121 0
      lib/handler/wechat_login_service.dart
  27. 3 1
      lib/module/character/content/character_group_content_view.dart
  28. 2 1
      lib/module/character_custom/detail/character_custom_detail_controller.dart
  29. 167 4
      lib/module/login/login_controller.dart
  30. 328 2
      lib/module/login/login_page.dart
  31. 35 3
      lib/module/main/main_controller.dart
  32. 17 46
      lib/module/mine/mine_controller.dart
  33. 5 3
      lib/module/mine/mine_view.dart
  34. 8 4
      lib/module/profile/profile_page.dart
  35. 10 1
      lib/module/store/store_controller.dart
  36. 2 1
      lib/module/store/store_page.dart
  37. 53 0
      lib/module/user_info/user_info_controller.dart
  38. 184 0
      lib/module/user_info/user_info_page.dart
  39. 46 0
      lib/resource/assets.gen.dart
  40. 50 0
      lib/resource/string.gen.dart
  41. 8 0
      lib/router/app_pages.dart
  42. 1 0
      lib/utils/styles.dart
  43. 27 20
      lib/widget/pargress_bar.dart
  44. 1 1
      pubspec.yaml

BIN
assets/images/bg_login.webp


BIN
assets/images/bg_login_dialog.webp


BIN
assets/images/icon_login_agree_privacy.webp


BIN
assets/images/icon_login_dialog_close.webp


BIN
assets/images/icon_login_dialog_phone_logo.webp


BIN
assets/images/icon_login_dialog_wechat_logo_white.webp


BIN
assets/images/icon_user_info_arrow_left.webp


BIN
assets/images/icon_user_info_logout.webp


BIN
assets/images/icon_wechat_logo_black.webp


+ 32 - 0
assets/string/base/string.xml

@@ -16,6 +16,7 @@
     <string name="mine_account_no_login">游客,去登陆</string>
 
 
+
     <string name="direct_send">生成内容直接发送</string>
     <string name="direct_send_desc">内容生成后,点击发送按钮,直接发送</string>
     <string name="open_float">打开悬浮窗</string>
@@ -37,12 +38,30 @@
 
     <string name="account_no_login">账号未登录</string>
     <string name="login_account">登录账号</string>
+    <string name="login_agree_privacy">请先阅读并同意《隐私权政策》和《服务条款》</string>
     <string name="login_request_code_frequently_toast">请求过于频繁,请稍后再试</string>
     <string name="login_verification_code_request_failed_toast">验证码发送失败,请重试</string>
     <string name="login_success">登录成功</string>
     <string name="login_too_often_toast">登录过于频繁,请稍后再试</string>
     <string name="login_failed_toast">登录失败</string>
     <string name="login_verification_code_error_toast">验证码输入错误,请重新输入</string>
+    <string name="login">登录</string>
+    <string name="login_print_verification_code">请输入验证码</string>
+    <string name="login_print_phone_verification">请输入正确格式的手机号码</string>
+    <string name="login_send_verification_code">发送验证码</string>
+    <string name="login_retransmission_code">s</string>
+    <string name="login_et_phone_hint">请输入手机号</string>
+    <string name="login_resend_code">重新发送</string>
+    <string name="login_other_login">其他登录方式</string>
+    <string name="wechat">微信</string>
+    <string name="login_delete_account">注销账号</string>
+    <string name="login_delete_account_desc">确认注销后您将失去以下权益 \n1.无法登录本账号 \n2.您的个人相关数据将被清空,人设设置与个人档案等相关数据将无法恢复。 \n3.会员在有效期内将放弃本账号所有权益</string>
+    <string name="login_delete_account_confirm">确认注销</string>
+    <string name="login_delete_account_cancel">再想想</string>
+    <string name="user_info">用户信息</string>
+    <string name="personal_info">个人信息</string>
+    <string name="logout">退出登录</string>
+
 
 
     <!-- 反馈页面-->
@@ -152,6 +171,7 @@
     <string name="text_span_service_terms">《服务条款》</string>
 
 
+
     <string name="member_continue_pay">继续支付</string>
     <string name="member_please_choice_goods">请选择支付商品</string>
     <string name="member_please_choice_payment">请选择支付方式</string>
@@ -298,5 +318,17 @@
 
     <string name="add_hobbies">添加爱好</string>
     <string name="save">保存</string>
+    <string name="wechat_login">微信登录</string>
+    <string name="phone_login">手机号登录</string>
+    <string name="tip">提示</string>
+    <string name="privacy_dialog_cancel">不同意</string>
+    <string name="privacy_dialog_confirm">同意</string>
+    <string name="logout_dialog_cancel">取消</string>
+    <string name="logout_dialog_confirm">确认</string>
+    <string name="logout_dialog_desc">确定退出登录吗?</string>
+
+
+
+
 
 </resources>

+ 2 - 1
lib/data/api/atmob_api.dart

@@ -75,7 +75,8 @@ abstract class AtmobApi {
 
   // 注销账号
   @POST("/central/open/v1/user/deprecate")
-  Future<BaseResponse> logoutUser(@Body() AppBaseRequest request);
+  Future<BaseResponse> deprecate(@Body() AppBaseRequest request);
+
 
   // 意见反馈
   @POST("/project/keyboard/v1/complaint/submit")

+ 1 - 1
lib/data/api/atmob_api.g.dart

@@ -82,7 +82,7 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
-  Future<BaseResponse<dynamic>> logoutUser(AppBaseRequest request) async {
+  Future<BaseResponse<dynamic>> deprecate(AppBaseRequest request) async {
     final _extra = <String, dynamic>{};
     final queryParameters = <String, dynamic>{};
     final _headers = <String, dynamic>{};

+ 3 - 3
lib/data/api/request/character_custom_delete_request.dart

@@ -7,10 +7,10 @@ part 'character_custom_delete_request.g.dart';
 @JsonSerializable()
 class CharacterCustomDeleteRequest extends AppBaseRequest {
   // 人设id
-  @JsonKey(name: "id")
-  String id;
+  @JsonKey(name: "characterId")
+  String characterId;
 
-  CharacterCustomDeleteRequest({required this.id});
+  CharacterCustomDeleteRequest({required this.characterId});
 
   @override
   Map<String, dynamic> toJson() => _$CharacterCustomDeleteRequestToJson(this);

+ 2 - 2
lib/data/api/request/character_custom_delete_request.g.dart

@@ -9,7 +9,7 @@ part of 'character_custom_delete_request.dart';
 CharacterCustomDeleteRequest _$CharacterCustomDeleteRequestFromJson(
   Map<String, dynamic> json,
 ) =>
-    CharacterCustomDeleteRequest(id: json['id'] as String)
+    CharacterCustomDeleteRequest(characterId: json['characterId'] as String)
       ..appPlatform = (json['appPlatform'] as num).toInt()
       ..os = json['os'] as String
       ..osVersion = json['osVersion'] as String
@@ -66,5 +66,5 @@ Map<String, dynamic> _$CharacterCustomDeleteRequestToJson(
   'locLng': instance.locLng,
   'locLat': instance.locLat,
   'authToken': instance.authToken,
-  'id': instance.id,
+  'characterId': instance.characterId,
 };

+ 6 - 0
lib/data/repository/account_repository.dart

@@ -100,6 +100,12 @@ class AccountRepository {
           throw error;
         });
   }
+  Future<void> deprecateAccount() {
+
+      return atmobApi.deprecate(AppBaseRequest()).then(HttpHandler.handle(true));
+
+
+  }
 
   void refreshUserInfo() {
     userInfoFuture?.cancel();

+ 1 - 1
lib/data/repository/characters_repository.dart

@@ -180,7 +180,7 @@ class CharactersRepository {
   // 删除定制人设
   Future<void> deleteCustomCharacter({required String characterId}) {
     return atmobApi
-        .deleteCustomCharacter(CharacterCustomDeleteRequest(id: characterId))
+        .deleteCustomCharacter(CharacterCustomDeleteRequest(characterId: characterId))
         .then(HttpHandler.handle(false));
   }
 

+ 37 - 2
lib/data/repository/keyboard_repository.dart

@@ -10,6 +10,7 @@ import 'package:keyboard/data/api/response/keyboard_prologue_list_response.dart'
 import 'package:keyboard/utils/atmob_log.dart';
 import 'dart:convert';
 import '../../di/get_it.dart';
+import '../../utils/async_util.dart';
 import '../../utils/http_handler.dart';
 import '../api/atmob_api.dart';
 import '../api/request/keyboard_character_list_request.dart';
@@ -19,6 +20,7 @@ import '../api/response/keyboard_character_list_response.dart';
 import '../api/response/keyboard_home_info_response.dart';
 import '../api/response/keyboard_list_response.dart';
 import '../bean/keyboard_info.dart';
+import '../consts/error_code.dart';
 
 @lazySingleton
 class KeyboardRepository {
@@ -39,6 +41,9 @@ class KeyboardRepository {
 
   Rxn<KeyboardHomeInfoResponse> get homeInfo => _homeInfo;
 
+  CancelableFuture? homeInfoFuture;
+  CancelableFuture? homeLoveIndexFuture;
+
   KeyboardRepository(this.atmobApi) {
     print('$tag....init');
     refreshData();
@@ -49,10 +54,40 @@ class KeyboardRepository {
 
     await Future.delayed(const Duration(milliseconds: 500));
     // 延迟为了保证首页数据能够正常获取,不然保存的时候,获取太快了,导致还是拉到旧的数值
-    await getKeyboardHomeInfo();
-    getKeyboardLoveIndex();
+    refreshUserInfo();
+    refreshLoveIndex();
+  }
+
+  void refreshUserInfo() {
+    homeInfoFuture?.cancel();
+    homeInfoFuture = AsyncUtil.retryWithExponentialBackoff(
+          () => getKeyboardHomeInfo(),
+      10,
+      predicate: (error) {
+        if (error is ServerErrorException) {
+          return error.code != ErrorCode.noLoginError;
+        }
+        return true;
+      },
+    );
   }
 
+  void refreshLoveIndex() {
+    homeLoveIndexFuture?.cancel();
+    homeLoveIndexFuture = AsyncUtil.retryWithExponentialBackoff(
+          () => getKeyboardLoveIndex(),
+      10,
+      predicate: (error) {
+        if (error is ServerErrorException) {
+          return error.code != ErrorCode.noLoginError;
+        }
+        return true;
+      },
+    );
+  }
+
+
+
   Future cleanData() async {
     _keyboardInfoList.clear();
     getKeyboardHomeInfo();

+ 18 - 1
lib/di/get_it.config.dart

@@ -28,6 +28,8 @@ import '../dialog/content/character_add_tab_controller.dart' as _i991;
 import '../dialog/content/character_tab_group_content_controller.dart' as _i293;
 import '../dialog/custom_character/custom_character_add_controller.dart'
     as _i122;
+import '../dialog/login/login_dialog_controller.dart' as _i798;
+import '../handler/wechat_login_service.dart' as _i495;
 import '../module/about/about_controller.dart' as _i256;
 import '../module/browser/browser_controller.dart' as _i923;
 import '../module/change/birthday/change_birthday_controller.dart' as _i1057;
@@ -68,6 +70,7 @@ import '../module/profile/profile_controller.dart' as _i244;
 import '../module/store/discount/discount_controller.dart' as _i333;
 import '../module/store/store_controller.dart' as _i344;
 import '../module/store/suprise/goods_surprise_controller.dart' as _i935;
+import '../module/user_info/user_info_controller.dart' as _i866;
 import '../plugins/keyboard_method_handler.dart' as _i415;
 import '../utils/payment_status_manager.dart' as _i779;
 import 'network_module.dart' as _i567;
@@ -112,12 +115,14 @@ extension GetItInjectableX on _i174.GetIt {
     gh.factory<_i248.KeyboardGuidePageController>(
       () => _i248.KeyboardGuidePageController(),
     );
-    gh.factory<_i1008.LoginController>(() => _i1008.LoginController());
     gh.factory<_i731.MainController>(() => _i731.MainController());
     gh.factory<_i333.DiscountController>(() => _i333.DiscountController());
     gh.factory<_i415.KeyboardMethodHandler>(
       () => _i415.KeyboardMethodHandler(),
     );
+    gh.lazySingleton<_i495.WechatLoginService>(
+      () => _i495.WechatLoginService(),
+    );
     gh.singleton<_i361.Dio>(
       () => networkModule.createStreamDio(),
       instanceName: 'streamDio',
@@ -131,6 +136,9 @@ extension GetItInjectableX on _i174.GetIt {
         gh<_i361.Dio>(instanceName: 'streamDio'),
       ),
     );
+    gh.factory<_i798.LoginDialogController>(
+      () => _i798.LoginDialogController(gh<_i495.WechatLoginService>()),
+    );
     gh.singleton<_i243.AtmobApi>(
       () => networkModule.provideAtmobApi(
         gh<_i361.Dio>(instanceName: 'defaultDio'),
@@ -214,6 +222,9 @@ extension GetItInjectableX on _i174.GetIt {
     gh.factory<_i732.MineController>(
       () => _i732.MineController(gh<_i83.AccountRepository>()),
     );
+    gh.factory<_i866.UserInfoController>(
+      () => _i866.UserInfoController(gh<_i83.AccountRepository>()),
+    );
     gh.factoryParam<
       _i991.CharacterAddTabController,
       _i497.KeyboardInfo,
@@ -263,6 +274,12 @@ extension GetItInjectableX on _i174.GetIt {
         gh<_i283.IntimacyAnalyzeRepository>(),
       ),
     );
+    gh.factory<_i1008.LoginController>(
+      () => _i1008.LoginController(
+        gh<_i83.AccountRepository>(),
+        gh<_i495.WechatLoginService>(),
+      ),
+    );
     gh.factory<_i1059.CharacterCustomListController>(
       () => _i1059.CharacterCustomListController(
         gh<_i421.CharactersRepository>(),

+ 124 - 0
lib/dialog/common_alert_dialog.dart

@@ -0,0 +1,124 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:keyboard/utils/common_expand.dart';
+
+
+import '../resource/colors.gen.dart';
+import '../utils/styles.dart';
+
+class CommonAlertDialog {
+  static const String _tag = "CommonAlertDialog";
+
+  static void show(
+      {required Widget titleWidget,
+      required Widget descWidget,
+      required String cancelText,
+      required String confirmText,
+      required VoidCallback cancelOnTap,
+      required VoidCallback confirmOnTap,
+      String tag = _tag}) {
+    SmartDialog.show(
+        tag: _tag,
+        builder: (_) {
+          return _CommonAlertDialog(
+              titleWidget: titleWidget,
+              descWidget: descWidget,
+              cancelText: cancelText,
+              confirmText: confirmText,
+              cancelOnTap: cancelOnTap,
+              confirmOnTap: confirmOnTap);
+        });
+  }
+
+  static void dismiss({String tag = _tag}) {
+    SmartDialog.dismiss(tag: _tag);
+  }
+}
+
+class _CommonAlertDialog extends Dialog {
+  final Widget titleWidget;
+  final Widget descWidget;
+  final String cancelText;
+  final String confirmText;
+  final VoidCallback cancelOnTap;
+  final VoidCallback confirmOnTap;
+
+  const _CommonAlertDialog({
+    required this.titleWidget,
+    required this.descWidget,
+    required this.cancelText,
+    required this.confirmText,
+    required this.cancelOnTap,
+    required this.confirmOnTap,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: EdgeInsets.symmetric(horizontal: 33.w),
+      decoration: ShapeDecoration(
+        color: Colors.white,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(20.r),
+        ),
+
+      ),
+      padding: EdgeInsets.symmetric(
+        horizontal: 16.w,
+        vertical: 24.w,
+      ),
+      child: IntrinsicHeight(
+        child: Column(
+          children: [
+            titleWidget,
+            SizedBox(height: 20.h),
+             descWidget,
+            SizedBox(height: 20.h),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                GestureDetector(
+                  onTap: () => cancelOnTap(),
+                  child: Container(
+                    height: 40.h,
+                    width: 128.w,
+                    alignment: Alignment.center,
+                    decoration: ShapeDecoration(
+                      color: const Color(0xFFF5F4F9),
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(50.r),
+                      ),
+                    ),
+                    child: Center(
+                      child: Text(cancelText,
+                            style: Styles.getTextStyleBlack102W500(16.sp),
+                  ),
+                ),
+                  ),
+                ),
+
+                GestureDetector(
+                  onTap: () => confirmOnTap(),
+                  child: Container(
+                    height: 40.h,
+                    width: 128.r,
+                    alignment: Alignment.center,
+                    decoration: Styles.getActivateButtonDecoration(
+                      50.r,
+                    ),
+                    child: Center(
+                      child: Text(confirmText,
+                          style:  Styles.getTextStyleWhiteW500(16.sp),),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 34 - 0
lib/dialog/common_alert_dialog_impl.dart

@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../resource/string.gen.dart';
+import '../utils/styles.dart';
+import 'common_alert_dialog.dart';
+
+void logoutAccountDialog({required VoidCallback confirmOnTap}) {
+  final tag = 'logoutAccountDialog';
+  CommonAlertDialog.show(
+    titleWidget: Text(
+      StringName.tip,
+      style: Styles.getTextStyleBlack204W500(16.sp),
+    ),
+    descWidget: Text(
+      StringName.logoutDialogDesc,
+      style: TextStyle(
+        color: Colors.black.withAlpha(204),
+        fontSize: 14.sp,
+        fontWeight: FontWeight.w400,
+      ),
+    ),
+    cancelText: StringName.logoutDialogCancel,
+    confirmText: StringName.logoutDialogConfirm,
+    cancelOnTap: () {
+      CommonAlertDialog.dismiss(tag: tag);
+    },
+    confirmOnTap: () {
+      confirmOnTap();
+      CommonAlertDialog.dismiss(tag: tag);
+    },
+    tag: tag,
+  );
+}

+ 137 - 0
lib/dialog/deprecate_diolog.dart

@@ -0,0 +1,137 @@
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:flutter/material.dart';
+import '../resource/assets.gen.dart';
+import '../resource/colors.gen.dart';
+import '../resource/string.gen.dart';
+import '../utils/styles.dart';
+
+class DeprecateDialog {
+  static const String tag = "DeprecateDialog";
+
+  static void show({Function? btnCancel, Function? btnConfirm}) {
+    SmartDialog.show(
+      tag: tag,
+      backType: SmartBackType.block,
+      clickMaskDismiss: true,
+      maskColor: ColorName.black70,
+      builder: (_) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.center,
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Container(
+              margin: EdgeInsets.symmetric(horizontal: 31.w),
+              decoration: ShapeDecoration(
+                color: Colors.white,
+                shape: RoundedRectangleBorder(
+                  borderRadius: BorderRadius.circular(20.r),
+                ),
+              ),
+              child: Stack(
+                children: [
+                  Container(
+                    padding: EdgeInsets.symmetric(
+                      horizontal: 16.w,
+                      vertical: 24.h,
+                    ),
+                    child: Column(
+                      mainAxisSize: MainAxisSize.min,
+                      crossAxisAlignment: CrossAxisAlignment.center,
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Text(
+                          StringName.loginDeleteAccount,
+                          style: Styles.getTextStyleBlack204W500(16.sp),
+                        ),
+                        SizedBox(height: 12.h),
+                        Text(
+                          StringName.loginDeleteAccountDesc,
+                          textAlign: TextAlign.center,
+                          style: TextStyle(
+                            color: Colors.black.withAlpha(204),
+                            fontSize: 14.sp,
+                            fontWeight: FontWeight.w400,
+                          ),
+                        ),
+                        SizedBox(height: 20.h),
+                        Row(
+                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                          children: [
+                            GestureDetector(
+                              onTap: () {
+                                if (btnConfirm != null) {
+                                  btnConfirm();
+                                }
+                                SmartDialog.dismiss(tag: tag);
+                              },
+                              child: Container(
+                                height: 40.h,
+                                width: 128.r,
+                                alignment: Alignment.center,
+                                decoration: ShapeDecoration(
+                                  color: const Color(0xFFFFE5E3),
+                                  shape: RoundedRectangleBorder(
+                                    borderRadius: BorderRadius.circular(50.r),
+                                  ),
+                                ),
+                                child: Text(
+                                  StringName.loginDeleteAccountConfirm,
+                                  style: TextStyle(
+                                    color: const Color(0xFFF44E45),
+                                    fontSize: 16.sp,
+                                    fontWeight: FontWeight.w500,
+                                  ),
+                                ),
+                              ),
+                            ),
+                            GestureDetector(
+                              onTap: () {
+                                if (btnCancel != null) {
+                                  btnCancel();
+                                }
+                                SmartDialog.dismiss();
+                              },
+                              child: Container(
+                                height: 40.h,
+                                width: 128.w,
+                                alignment: Alignment.center,
+                                decoration: ShapeDecoration(
+                                  color: const Color(0xFFF5F4F9),
+                                  shape: RoundedRectangleBorder(
+                                    borderRadius: BorderRadius.circular(50.r),
+                                  ),
+                                ),
+                                child: Text(
+                                  StringName.loginDeleteAccountCancel,
+                                  style: Styles.getTextStyleBlack102W500(16.sp),
+                                ),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ],
+                    ),
+                  ),
+                  Positioned(
+                    right: 14.w,
+                    top: 14.h,
+                    child: GestureDetector(
+                      onTap: () {
+                        SmartDialog.dismiss();
+                      },
+                      child: Assets.images.iconCustomDialogClose.image(
+                        width: 24.w,
+                        height: 24.h,
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 23 - 0
lib/dialog/login/login_dialog.dart

@@ -0,0 +1,23 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:keyboard/dialog/login/login_dialog_controller.dart';
+import '../../module/login/login_controller.dart';
+import 'login_dialog_view.dart';
+
+class LoginDialog {
+  static const String TAG = "LoginDialog";
+
+  static void show() {
+    SmartDialog.show(
+      backType: SmartBackType.block,
+      clickMaskDismiss: true,
+      alignment: Alignment.bottomCenter,
+      animationType: SmartAnimationType.centerScale_otherSlide,
+      tag: TAG,
+      keepSingle: true,
+      // onDismiss: () => Get.delete<LoginDialogController>(),
+      builder: (_) => const LoginDialogView(),
+    );
+  }
+}

+ 81 - 0
lib/dialog/login/login_dialog_controller.dart

@@ -0,0 +1,81 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/base/base_controller.dart';
+import 'package:keyboard/module/login/login_page.dart';
+import 'package:keyboard/utils/atmob_log.dart';
+import 'package:keyboard/utils/toast_util.dart';
+
+import '../../handler/wechat_login_service.dart';
+import '../member_agreement_dialog.dart';
+import '../privacy_agreement_dialog.dart';
+import 'login_dialog.dart';
+
+@injectable
+class LoginDialogController extends BaseController {
+  final tag = "LoginDialogController";
+
+  final WechatLoginService wechatLoginService;
+
+  LoginDialogController(this.wechatLoginService);
+
+  final RxBool _isAgree = false.obs;
+
+  bool get isAgree => _isAgree.value;
+
+  clickAgree() {
+    _isAgree.value = !_isAgree.value;
+  }
+
+  void clickWxLogin() async {
+    if (!_isAgree.value) {
+      PrivacyAgreementDialog.show(
+        btnConfirm: () async {
+          _isAgree.value = true;
+          clickWxLogin();
+        },
+      );
+
+      return;
+    }
+    wechatLoginService.login(
+      onSuccess: (code) {
+        debugPrint("登录成功 code: $code");
+        // TODO: 通过 code 获取后端 token
+      },
+      onError: (code, msg) {
+        ToastUtil.show("微信登录失败:$msg");
+      },
+      onCancel: () {
+        ToastUtil.show("用户取消登录");
+      },
+    );
+  }
+
+  void clickClose() {
+    AtmobLog.d(tag, "clickClose");
+    SmartDialog.dismiss(tag: LoginDialog.TAG);
+  }
+
+  void clickPhoneLogin() {
+    AtmobLog.d(tag, "clickPhoneLogin");
+
+    LoginPage.start();
+
+    SmartDialog.dismiss(tag: LoginDialog.TAG);
+  }
+
+  @override
+  void onInit() {
+    super.onInit();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+  }
+}

+ 224 - 0
lib/dialog/login/login_dialog_view.dart

@@ -0,0 +1,224 @@
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:keyboard/base/base_view.dart';
+import 'package:flutter/material.dart';
+import 'package:keyboard/module/login/login_controller.dart';
+import 'package:keyboard/dialog/login/login_dialog_controller.dart';
+import 'package:get/get.dart';
+import '../../data/consts/web_url.dart';
+import '../../resource/assets.gen.dart';
+import '../../resource/string.gen.dart';
+import '../../widget/click_text_span.dart';
+
+class LoginDialogView extends BaseView<LoginDialogController> {
+  const LoginDialogView({super.key});
+
+  static var TAG = "LoginDialog";
+
+  @override
+  backgroundColor() => Colors.transparent;
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.only(
+          topLeft: Radius.circular(24.r),
+          topRight: Radius.circular(24.r),
+        ),
+      ),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          Stack(
+            children: [
+              Container(
+                width: 360.w,
+                decoration: BoxDecoration(
+                  image: DecorationImage(
+                    image: Assets.images.bgLoginDialog.provider(),
+                    fit: BoxFit.contain,
+                    alignment: Alignment.topCenter,
+                  ),
+                ),
+                child: Container(
+                  child: Column(
+                    children: [
+                      SizedBox(height: 131.h),
+                      _buildWeChatButton(),
+                      SizedBox(height: 31.h),
+                      _buildPhoneLoginButton(),
+                      SizedBox(height: 10.h),
+                      _buildPrivacy(),
+                    ],
+                  ),
+                ),
+              ),
+              Positioned(
+                  right: 13.w,
+                  top: 13.w,
+                  child: InkWell(onTap: controller.clickClose,
+                    child: Assets.images.iconLoginDialogClose.image(
+                      width: 24.w,
+                      height: 24.w,
+                    ),)
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPhoneLoginButton() {
+    return InkWell(
+      onTap: () {
+        controller.clickPhoneLogin();
+      },
+      child: Column(
+        children: [
+          Container(
+            width: 44.w,
+            height: 44.w,
+            decoration: ShapeDecoration(
+              color: const Color(0xFFF5F4F9),
+              shape: OvalBorder(),
+            ),
+            child: Center(
+              child: Assets.images.iconLoginDialogPhoneLogo.image(
+                width: 20.w,
+                height: 20.w,
+              ),
+            ),
+          ),
+          Text(
+            '手机号登录',
+            textAlign: TextAlign.center,
+            style: TextStyle(
+              color: Colors.black.withValues(alpha: 128),
+              fontSize: 12,
+              fontFamily: 'Source Han Sans CN',
+              fontWeight: FontWeight.w400,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildWeChatButton() {
+    return InkWell(
+      onTap: () {
+        controller.clickWxLogin();
+      },
+      child: Container(
+        width: 312.w,
+        height: 48.h,
+        decoration: ShapeDecoration(
+          color: const Color(0xFF31DB78),
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(50.r),
+          ),
+        ),
+        child: Row(
+          mainAxisSize: MainAxisSize.min,
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          spacing: 4,
+          children: [
+            Assets.images.iconLoginDialogWechatLogoWhite.image(
+              width: 26.w,
+              height: 26.w,
+            ),
+            Text(
+              StringName.wechatLogin,
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Colors.white,
+                fontSize: 16.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildPrivacy() {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        Obx(() {
+          return GestureDetector(
+            behavior: HitTestBehavior.opaque,
+            onTap: () {
+              controller.clickAgree();
+            },
+            child: Padding(
+              padding: EdgeInsets.symmetric(vertical: 10.w),
+              child:
+                  controller.isAgree
+                      ? Assets.images.iconLoginAgreePrivacy.image(
+                        width: 14.w,
+                        height: 14.w,
+                      )
+                      : Container(
+                        padding: EdgeInsets.all(1.w),
+                        width: 12.w,
+                        height: 12.w,
+                        child: Container(
+                          decoration: BoxDecoration(
+                            shape: BoxShape.circle,
+                            border: Border.all(
+                              color: Colors.black.withAlpha(153),
+                              width: 1.w,
+                            ),
+                          ),
+                        ),
+                      ),
+            ),
+          );
+        }),
+        Text.rich(
+          TextSpan(
+            children: [
+              TextSpan(
+                text: StringName.textSpanIHaveReadAndAgree,
+                style: TextStyle(
+                  color: Colors.black.withAlpha(128),
+                  fontSize: 12.sp,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+              ClickTextSpan(
+                text: StringName.textSpanPrivacyPolicy,
+                url: WebUrl.privacyPolicy,
+                fontSize: 12.sp,
+                color: Colors.black.withAlpha(204),
+              ),
+
+              TextSpan(
+                text: StringName.textSpanAnd,
+                style: TextStyle(
+                  color: Colors.black.withAlpha(128),
+                  fontSize: 12.sp,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+
+              ClickTextSpan(
+                text: StringName.textSpanServiceTerms,
+                url: WebUrl.serviceAgreement,
+                color: Colors.black.withAlpha(204),
+                fontSize: 12.sp,
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 160 - 0
lib/dialog/privacy_agreement_dialog.dart

@@ -0,0 +1,160 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:keyboard/resource/string.gen.dart';
+
+import '../data/consts/web_url.dart';
+import '../resource/assets.gen.dart';
+import '../resource/colors.gen.dart';
+import '../utils/styles.dart';
+import '../widget/click_text_span.dart';
+
+class PrivacyAgreementDialog {
+  static const String tag = 'PrivacyAgreementDialog';
+
+  static void show({Function? btnCancel, Function? btnConfirm}) {
+    SmartDialog.show(
+      tag: tag,
+      backType: SmartBackType.block,
+
+      clickMaskDismiss: true,
+      maskColor: ColorName.black70,
+      builder: (_) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.center,
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Container(
+              margin: EdgeInsets.symmetric(horizontal: 33.w),
+              decoration: ShapeDecoration(
+                color: Colors.white,
+                shape: RoundedRectangleBorder(
+                  borderRadius: BorderRadius.circular(20.r),
+                ),
+              ),
+              child: Stack(
+                children: [
+                  Container(
+                    padding: EdgeInsets.symmetric(
+                      horizontal: 16.w,
+                      vertical: 24.w,
+                    ),
+                    child: Column(
+                      mainAxisSize: MainAxisSize.min,
+                      crossAxisAlignment: CrossAxisAlignment.center,
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Text(
+                          StringName.tip,
+                          style: Styles.getTextStyleBlack204W500(16.sp),
+                        ),
+                        SizedBox(height: 20.h),
+
+                        SizedBox(
+                          child: Text.rich(
+                            textAlign: TextAlign.center,
+                            TextSpan(
+                              children: [
+                                TextSpan(
+                                  text: StringName.textSpanIHaveReadAndAgree,
+                                  style: TextStyle(
+                                    color: Colors.black.withAlpha(153),
+                                    fontSize: 14.sp,
+                                    fontWeight: FontWeight.w400,
+                                  ),
+                                ),
+                                TextSpan(
+                                  text: "\n",
+                                  style: TextStyle(
+                                    color: Colors.black.withAlpha(153),
+                                    fontSize: 14.sp,
+                                    fontWeight: FontWeight.w400,
+                                  ),
+                                ),
+                                ClickTextSpan(
+                                  fontSize: 14.sp,
+                                  text: StringName.textSpanPrivacyPolicy,
+                                  url: WebUrl.privacyPolicy,
+                                  color: Color(0xff374BFF),
+                                ),
+
+                                TextSpan(
+                                  text: StringName.textSpanAnd,
+                                  style: TextStyle(
+                                    color: Colors.black.withAlpha(153),
+                                    fontSize: 14.sp,
+                                    fontWeight: FontWeight.w400,
+                                  ),
+                                ),
+
+                                ClickTextSpan(
+                                  fontSize: 14.sp,
+                                  text: StringName.textSpanServiceTerms,
+                                  url: WebUrl.serviceAgreement,
+                                  color: Color(0xff374BFF),
+                                ),
+                              ],
+                            ),
+                          ),
+                        ),
+                        SizedBox(height: 20.h),
+                        Row(
+                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                          children: [
+                            GestureDetector(
+                              onTap: () {
+                                if (btnCancel != null) {
+                                  btnCancel();
+                                }
+                                SmartDialog.dismiss();
+                              },
+                              child: Container(
+                                height: 40.h,
+                                width: 128.w,
+                                alignment: Alignment.center,
+                                decoration: ShapeDecoration(
+                                  color: const Color(0xFFF5F4F9),
+                                  shape: RoundedRectangleBorder(
+                                    borderRadius: BorderRadius.circular(50.r),
+                                  ),
+                                ),
+                                child: Text(
+                                  StringName.privacyDialogCancel,
+                                  style: Styles.getTextStyleBlack102W500(16.sp),
+                                ),
+                              ),
+                            ),
+                            GestureDetector(
+                              onTap: () {
+                                if (btnConfirm != null) {
+                                  btnConfirm();
+                                }
+                                SmartDialog.dismiss(tag: tag);
+                              },
+                              child: Container(
+                                height: 40.h,
+                                width: 128.r,
+                                alignment: Alignment.center,
+                                decoration: Styles.getActivateButtonDecoration(
+                                  50.r,
+                                ),
+                                child: Text(
+                                  StringName.privacyDialogConfirm,
+                                  style: Styles.getTextStyleWhiteW500(16.sp),
+                                ),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 121 - 0
lib/handler/wechat_login_service.dart

@@ -0,0 +1,121 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:injectable/injectable.dart';
+import 'package:wechat_kit/wechat_kit.dart';
+
+import '../di/get_it.dart';
+import '../utils/atmob_log.dart';
+
+@lazySingleton
+class WechatLoginService {
+  final String tag = "WechatLoginService";
+  final String _appId = "wx21272929e8fd33e9"; //AppID
+  final String? _universalLink = null; // universalLink
+
+  StreamSubscription<WechatResp>? _respSub;
+
+  void Function(String code)? _onSuccess;
+  void Function(int code, String msg)? _onError;
+  VoidCallback? _onCancel;
+
+  String? lastCode;
+
+  WechatLoginService() {
+    AtmobLog.d(tag, '$tag....init');
+    _init();
+  }
+
+  void _init() {
+    // 注册 App 并监听回调
+    WechatKitPlatform.instance.registerApp(
+      appId: _appId,
+      universalLink: _universalLink,
+    );
+    _respSub = WechatKitPlatform.instance.respStream().listen(_handleWechatResp);
+  }
+
+  void login({
+    required void Function(String code) onSuccess,
+    void Function(int code, String msg)? onError,
+    VoidCallback? onCancel,
+  }) async {
+    try {
+      final isInstalled = await WechatKitPlatform.instance.isInstalled();
+      if (!isInstalled) {
+        onError?.call(
+          WechatResp.kErrorCodeUnsupport,
+          _getErrorMsg(WechatResp.kErrorCodeUnsupport),
+        );
+        return;
+      }
+
+      // 保存回调函数
+      _onSuccess = onSuccess;
+      _onError = onError;
+      _onCancel = onCancel;
+
+      // 发送授权请求
+      WechatKitPlatform.instance.auth(
+        scope: <String>[WechatScope.kSNSApiUserInfo],
+        state: 'wechat_login',
+      );
+    } catch (e) {
+      onError?.call(-999, '微信初始化失败: ${e.toString()}');
+    }
+  }
+
+  void _handleWechatResp(WechatResp resp) {
+    if (resp is WechatAuthResp) {
+      debugPrint("【WechatLoginService】收到微信响应: ${resp.errorCode} ${resp.errorMsg}");
+
+      switch (resp.errorCode) {
+        case WechatResp.kErrorCodeSuccess:
+          if (resp.code != null) {
+            lastCode = resp.code;
+            _onSuccess?.call(resp.code!);
+          } else {
+            _onError?.call(-998, '未获取到授权 code');
+          }
+          break;
+        case WechatResp.kErrorCodeUserCancel:
+          _onCancel?.call();
+          break;
+        default:
+          _onError?.call(resp.errorCode, _getErrorMsg(resp.errorCode));
+      }
+
+      // 一次登录完成后清空回调
+      _clearCallbacks();
+    }
+  }
+
+  void _clearCallbacks() {
+    _onSuccess = null;
+    _onError = null;
+    _onCancel = null;
+  }
+
+  String _getErrorMsg(int code) {
+    switch (code) {
+      case WechatResp.kErrorCodeCommon:
+        return '普通错误,可能是网络问题';
+      case WechatResp.kErrorCodeUserCancel:
+        return '用户取消了登录';
+      case WechatResp.kErrorCodeSentFail:
+        return '发送请求失败';
+      case WechatResp.kErrorCodeAuthDeny:
+        return '授权失败';
+      case WechatResp.kErrorCodeUnsupport:
+        return '未检测到微信客户端';
+      default:
+        return '未知错误: $code';
+    }
+  }
+
+  void dispose() {
+    _respSub?.cancel();
+  }
+
+  static WechatLoginService getInstance() => getIt.get<WechatLoginService>();
+}

+ 3 - 1
lib/module/character/content/character_group_content_view.dart

@@ -19,7 +19,9 @@ class CharacterGroupContentView
 
   @override
   Widget buildBody(BuildContext context) {
-    return Column(
+    return
+
+      Column(
       children: [
         Expanded(
           child: Obx(() {

+ 2 - 1
lib/module/character_custom/detail/character_custom_detail_controller.dart

@@ -296,13 +296,14 @@ class CharacterCustomDetailController extends BaseController {
         birthday: _currentBirthday.value,
         imageUrl: _avatarUrl.value,
       );
+      ToastUtil.show("生成成功");
     } catch (error) {
       if (error is ServerErrorException && error.code == 1005) {
         ToastUtil.show('请开通会员解锁权益~');
         StorePage.start();
       }
       if (error is ServerErrorException) {
-        ErrorHandler.toastError(error);
+        ToastUtil.show(error.message);
       }
     }
   }

+ 167 - 4
lib/module/login/login_controller.dart

@@ -1,9 +1,20 @@
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/base/base_controller.dart';
-import 'package:get/get.dart';
-@injectable
-class LoginController extends BaseController{
 
+import '../../data/consts/error_code.dart';
+import '../../data/repository/account_repository.dart';
+import '../../dialog/privacy_agreement_dialog.dart';
+import '../../handler/wechat_login_service.dart';
+import '../../resource/string.gen.dart';
+import '../../utils/http_handler.dart';
+import '../../utils/toast_util.dart';
+
+@injectable
+class LoginController extends BaseController {
+  final AccountRepository accountRepository;
+  final WechatLoginService wechatLoginService;
   final RxString _phone = ''.obs;
   final RxString _code = ''.obs;
 
@@ -11,9 +22,161 @@ class LoginController extends BaseController{
 
   String get code => _code.value;
 
+  final int _countDownTime = 60;
+
+  final RxnInt _countDown = RxnInt();
+
+  int? get countDown => _countDown.value;
+
+  final RxBool _isAgree = false.obs;
+
+  bool get isAgree => _isAgree.value;
+
+  final RxBool _isFirstSend = true.obs;
+
+  bool get isFirstSend => _isFirstSend.value;
+
+  LoginController(this.accountRepository, this.wechatLoginService);
 
   @override
   void onInit() {
     super.onInit();
   }
-}
+
+  void onPhoneChanged(String value) {
+    _phone.value = value;
+  }
+
+  void onCodeChanged(String value) {
+    _code.value = value;
+  }
+
+  void onBackClick() {
+    Get.back();
+  }
+
+  void clickAgree() {
+    _isAgree.value = !_isAgree.value;
+  }
+
+  void onSendVerificationCode() async {
+    if (_countDown.value != null) {
+      return;
+    }
+    if (!RegExp(r'^1\d{10}$').hasMatch(phone)) {
+      ToastUtil.show(StringName.loginPrintPhoneVerification);
+      return;
+    }
+    if (!_isAgree.value) {
+      PrivacyAgreementDialog.show(
+        btnConfirm: () async {
+          _isAgree.value = true;
+          onSendVerificationCode();
+        },
+      );
+      return;
+    }
+
+    try {
+      await accountRepository.loginSendCode(phone);
+      _countDown.value = _countDownTime;
+      _startCountDown();
+    } catch (error) {
+      if (error is RequestCodeTooOftenException) {
+        ToastUtil.show(StringName.loginRequestCodeFrequentlyToast);
+      } else if (error is ServerErrorException) {
+        ToastUtil.show(error.message);
+      } else {
+        ToastUtil.show(StringName.loginVerificationCodeRequestFailedToast);
+      }
+    }
+  }
+
+  void onLoginClick() {
+    if (!RegExp(r'^1\d{10}$').hasMatch(phone)) {
+      ToastUtil.show(StringName.loginPrintPhoneVerification);
+      return;
+    }
+    if (!_isAgree.value) {
+      PrivacyAgreementDialog.show(
+        btnConfirm: () async {
+          _isAgree.value = true;
+          onLoginClick();
+        },
+      );
+      ToastUtil.show(StringName.loginAgreePrivacy);
+      return;
+    }
+    if (code.isEmpty) {
+      ToastUtil.show(StringName.loginPrintVerificationCode);
+      return;
+    }
+    accountRepository
+        .loginUserLogin(phone, code)
+        .then((data) {
+          Get.back();
+          ToastUtil.show(StringName.loginSuccess);
+        })
+        .catchError((error) {
+          if (error is LoginTooOftenException) {
+            ToastUtil.show(StringName.loginTooOftenToast);
+            return;
+          }
+          if (error is ServerErrorException) {
+            if (error.code == ErrorCode.verificationCodeError) {
+              ToastUtil.show(StringName.loginVerificationCodeErrorToast);
+            } else {
+              ToastUtil.show(error.message);
+            }
+          } else {
+            ToastUtil.show(StringName.loginFailedToast);
+          }
+        });
+  }
+
+  void _startCountDown() {
+    Future.delayed(Duration(seconds: 1), () {
+      int? time = _countDown.value;
+      _isFirstSend.value = false;
+      if (time != null) {
+        _countDown.value = time - 1;
+        if (time > 0) {
+          _startCountDown();
+        } else {
+          _countDown.value = null;
+        }
+      }
+    });
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    _countDown.value = null;
+  }
+
+  void clickWxLogin() async {
+    if (!_isAgree.value) {
+      PrivacyAgreementDialog.show(
+        btnConfirm: () async {
+          _isAgree.value = true;
+          clickWxLogin();
+        },
+      );
+      return;
+    }
+    wechatLoginService.login(
+      onSuccess: (code) {
+        debugPrint("登录成功 code: $code");
+      },
+      onError: (code, msg) {
+        ToastUtil.show("微信登录失败:$msg");
+      },
+      onCancel: () {
+        ToastUtil.show("用户取消登录");
+      },
+    );
+  }
+
+
+}

+ 328 - 2
lib/module/login/login_page.dart

@@ -1,10 +1,19 @@
-
 import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:keyboard/base/base_page.dart';
 import 'package:keyboard/module/login/login_controller.dart';
 import 'package:get/get.dart';
 import 'package:keyboard/router/app_pages.dart';
+import 'package:keyboard/utils/common_expand.dart';
+
+import '../../data/consts/web_url.dart';
+import '../../resource/assets.gen.dart';
+import '../../resource/colors.gen.dart';
+import '../../resource/string.gen.dart';
+import '../../utils/styles.dart';
+import '../../widget/click_text_span.dart';
 
 class LoginPage extends BasePage<LoginController> {
   const LoginPage({super.key});
@@ -14,9 +23,326 @@ class LoginPage extends BasePage<LoginController> {
   }
 
   @override
+  Color backgroundColor() {
+    return const Color(0xFFF6F5FA);
+  }
+
+  @override
+  immersive() {
+    return true;
+  }
+
+  @override
   Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        Assets.images.bgLogin.image(fit: BoxFit.contain, width: 360.w),
+        SafeArea(
+          child: Column(
+            children: [
+              SizedBox(height: 225.w),
+              buildPhoneTextFiled(),
+              SizedBox(height: 16.w),
+              buildCodeTextFiled(),
+              SizedBox(height: 46.w),
+              buildLoginButton(),
+              SizedBox(height: 25.w),
+              _buildPrivacy(),
+              SizedBox(height: 100.w),
+              buildOtherLogin(),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget buildPhoneTextFiled() {
+    return Container(
+      height: 48.w,
+      margin: EdgeInsets.symmetric(horizontal: 20.w),
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(100.w),
+      ),
+      child: Row(
+        children: [
+          Expanded(
+            child: TextField(
+              cursorHeight: 20.w,
+              style: TextStyle(
+                fontSize: 14.sp,
+                color: Colors.black.withAlpha( 204),
+                fontWeight: FontWeight.w500,
+              ),
+              maxLines: 1,
+              maxLength: 11,
+              keyboardType: TextInputType.phone,
+              textAlignVertical: TextAlignVertical.center,
+              textInputAction: TextInputAction.next,
+              decoration: InputDecoration(
+                hintText: StringName.loginEtPhoneHint,
+                counterText: '',
+                hintStyle: TextStyle(
+                  fontSize: 14.sp,
+                  color: Colors.black.withAlpha(61),
+                  fontWeight: FontWeight.normal,
+                ),
+                labelStyle: TextStyle(
+                  fontSize: 14.sp,
+                  color: ColorName.primaryTextColor,
+                ),
+                contentPadding: const EdgeInsets.all(0),
+                border: const OutlineInputBorder(borderSide: BorderSide.none),
+                enabled: true,
+              ),
+              onChanged: controller.onPhoneChanged,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget buildCodeTextFiled() {
     return Container(
-      child:  Text('Login Page')
+      height: 48.w,
+      margin: EdgeInsets.symmetric(horizontal: 20.w),
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(100.w),
+      ),
+      child: Row(
+        children: [
+          Expanded(
+            child: TextField(
+              cursorHeight: 20.w,
+              style: TextStyle(
+                fontSize: 14.sp,
+                color: Colors.black.withAlpha( 204),
+                fontWeight: FontWeight.w500,
+              ),
+              maxLines: 1,
+              maxLength: 4,
+              keyboardType: TextInputType.phone,
+              textAlignVertical: TextAlignVertical.center,
+              textInputAction: TextInputAction.next,
+              decoration: InputDecoration(
+                hintText: StringName.loginPrintVerificationCode,
+                counterText: '',
+                hintStyle: TextStyle(
+                  fontSize: 14.sp,
+                  color: Colors.black.withAlpha(61),
+                  fontWeight: FontWeight.normal,
+                ),
+                labelStyle: TextStyle(
+                  fontSize: 14.sp,
+                  color: ColorName.primaryTextColor,
+                ),
+                contentPadding: const EdgeInsets.all(0),
+                border: const OutlineInputBorder(borderSide: BorderSide.none),
+                enabled: true,
+              ),
+              onChanged: controller.onCodeChanged,
+            ),
+          ),
+          buildVerificationCodeSendBtn(),
+        ],
+      ),
+    );
+  }
+
+  Widget buildVerificationCodeSendBtn() {
+    return GestureDetector(
+      onTap: controller.onSendVerificationCode,
+      child: Container(
+        margin: EdgeInsets.only(left: 12.w, right: 8.w),
+        child: Obx(() {
+          String txt = "";
+
+          if (controller.countDown != null) {
+            txt =
+                '${controller.countDown}${StringName.loginRetransmissionCode}';
+          } else {
+            if (controller.isFirstSend) {
+              txt = StringName.loginSendVerificationCode;
+            } else {
+              txt = StringName.loginResendCode;
+            }
+
+          }
+          return Text(
+            txt,
+            style: TextStyle(
+              fontSize: 14.sp,
+              color: controller.isFirstSend
+                  ? Colors.black.withAlpha(204)
+                  : Color(0xCC7D46FC)
+            ),
+          );
+        }),
+      ),
+    );
+  }
+
+  Widget buildLoginButton() {
+    return GestureDetector(
+      onTap: () {
+        controller.onLoginClick();
+      },
+      child: Container(
+        height: 48.w,
+        margin: EdgeInsets.symmetric(horizontal: 24.w),
+        child: Row(
+          children: [
+            Expanded(
+              child: Obx(() {
+                return Container(
+                  height: 48.w,
+                  decoration:
+                      controller.phone.length == 11 &&
+                              controller.code.isNotEmpty &&
+                              controller.isAgree
+                          ? Styles.getActivateButtonDecoration(50.r)
+                          : Styles.getInactiveButtonDecoration(50.r),
+                  child: Center(
+                    child: Text(
+                      StringName.login,
+                      style: TextStyle(color: Colors.white, fontSize: 16.sp),
+                    ),
+                  ),
+                );
+              }),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildPrivacy() {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        Obx(() {
+          return GestureDetector(
+            behavior: HitTestBehavior.opaque,
+            onTap: () {
+              controller.clickAgree();
+            },
+            child: Padding(
+              padding: EdgeInsets.symmetric(vertical: 10.w),
+              child:
+                  controller.isAgree
+                      ? Assets.images.iconLoginAgreePrivacy.image(
+                        width: 14.w,
+                        height: 14.w,
+                      )
+                      : Container(
+                        padding: EdgeInsets.all(1.w),
+                        width: 14.w,
+                        height: 14.w,
+                        child: Container(
+                          decoration: BoxDecoration(
+                            shape: BoxShape.circle,
+                            border: Border.all(
+                              color: Colors.black.withAlpha(153),
+                              width: 1.w,
+                            ),
+                          ),
+                        ),
+                      ),
+            ),
+          );
+        }),
+        Text.rich(
+          TextSpan(
+            children: [
+              TextSpan(
+                text: StringName.textSpanIHaveReadAndAgree,
+                style: TextStyle(
+                  color: Colors.black.withAlpha(128),
+                  fontSize: 12.sp,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+              ClickTextSpan(
+                text: StringName.textSpanPrivacyPolicy,
+                url: WebUrl.privacyPolicy,
+                fontSize: 12.sp,
+                color: Colors.black.withAlpha(204),
+              ),
+
+              TextSpan(
+                text: StringName.textSpanAnd,
+                style: TextStyle(
+                  color: Colors.black.withAlpha(128),
+                  fontSize: 12.sp,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+
+              ClickTextSpan(
+                text: StringName.textSpanServiceTerms,
+                url: WebUrl.serviceAgreement,
+                color: Colors.black.withAlpha(204),
+                fontSize: 12.sp,
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget buildOtherLogin() {
+    return Container(
+      child: Column(
+        children: [
+          Text(
+            StringName.loginOtherlogin,
+            style: TextStyle(
+              color: Colors.black.withValues(alpha: 0.5),
+              fontSize: 12.sp,
+              height: 0,
+            ),
+          ),
+          SizedBox(height: 18.h),
+          Material(
+            color: Colors.white,
+            shape: const CircleBorder(),
+            child: InkWell(
+              customBorder: const CircleBorder(), // 保证点击区域是圆的
+              onTap: () {
+                controller.clickWxLogin();
+              },
+              child: SizedBox(
+                width: 44.w,
+                height: 44.w,
+                child: Center(
+                  child: Assets.images.iconWechatLogoBlack.image(
+                    width: 22.w,
+                    height: 22.w,
+                  ),
+                ),
+              ),
+            ),
+          ),
+          SizedBox(height: 6.w),
+          Text(
+            StringName.wechat,
+            textAlign: TextAlign.center,
+            style: TextStyle(
+              color: Colors.black.withValues(alpha: 0.5),
+              fontSize: 12.sp,
+
+              height: 0,
+            ),
+          ),
+        ],
+      ),
     );
   }
 }

+ 35 - 3
lib/module/main/main_controller.dart

@@ -22,19 +22,51 @@ class MainController extends BaseController {
       StringName.mainTabKeyboard,
       Assets.images.iconTabKeyboardUnselect.path,
       Assets.anim.animTabKeyboardSelectedData,
-      KeyBoardView(),
+      AnimatedSwitcher(
+        duration: Duration(milliseconds: 300),
+        transitionBuilder: (Widget child, Animation<double> animation) {
+          return FadeTransition(
+            opacity: animation,
+            child: child,
+          );
+        },
+        child: KeyBoardView(),
+      ),
     ),
     TabBean(
       StringName.mainTabCharacter,
       Assets.images.iconTabCharacterUnselect.path,
       Assets.anim.animTabCharacterSelectedData,
-      CharacterView(),
+
+      AnimatedSwitcher(
+        duration: Duration(milliseconds: 300),
+
+        transitionBuilder: (Widget child, Animation<double> animation) {
+          return FadeTransition(
+            opacity: animation,
+            child: child,
+          );
+        },
+        child: CharacterView(),
+
+      ),
     ),
     TabBean(
       StringName.mainTabMine,
       Assets.images.iconTabMineUnselect.path,
       Assets.anim.animTabMineSelectedData,
-      MineView(),
+
+      AnimatedSwitcher(
+        duration: Duration(milliseconds: 300),
+        transitionBuilder: (Widget child, Animation<double> animation) {
+          return FadeTransition(
+            opacity: animation,
+            child: child,
+          );
+        },
+
+        child: MineView(),
+      ),
     ),
   ];
 

+ 17 - 46
lib/module/mine/mine_controller.dart

@@ -8,11 +8,13 @@ import 'package:keyboard/base/base_controller.dart';
 import 'package:keyboard/module/about/about_page.dart';
 import 'package:keyboard/module/feedback/feedback_page.dart';
 import 'package:keyboard/module/store/discount/discount_view.dart';
+import 'package:keyboard/module/user_info/user_info_page.dart';
 import 'package:keyboard/utils/atmob_log.dart';
 
 import '../../data/consts/build_config.dart';
 import '../../data/consts/error_code.dart';
 import '../../data/repository/account_repository.dart';
+import '../../dialog/login/login_dialog.dart';
 import '../../plugins/keyboard_android_platform.dart';
 import '../../plugins/keyboard_method_handler.dart';
 import '../../resource/colors.gen.dart';
@@ -52,68 +54,37 @@ class MineController extends BaseController {
   }
 
   longClickVip() {
-    if (BuildConfig.isDebug) {
-      KeyboardAndroidPlatform.enableFloatingWindow(true);
-      KeyboardAndroidPlatform.openInputMethodSettings();
-    }
+
+    KeyboardAndroidPlatform.enableFloatingWindow(true);
+    KeyboardAndroidPlatform.openInputMethodSettings();
   }
-  bool isTest = false;
+
   clickOnlineCustomerService() {
     debugPrint('clickOnlineCustomerService');
-    isTest?
-    accountRepository
-        .loginUserLogin("11223344551", "1122")
-        .then((data) {
-          Get.back();
-          ToastUtil.show(StringName.loginSuccess);
-
-        })
-        .catchError((error) {
-          if (error is LoginTooOftenException) {
-            ToastUtil.show(StringName.loginTooOftenToast);
-            return;
-          }
-          if (error is ServerErrorException) {
-            if (error.code == ErrorCode.verificationCodeError) {
-              ToastUtil.show(StringName.loginVerificationCodeErrorToast);
-            } else {
-              ToastUtil.show(error.message);
-            }
-          } else {
-            ToastUtil.show(StringName.loginFailedToast);
-          }
-        }):
-        accountRepository.logout();
-    isTest=!isTest;
-
-    AtmobLog.d("MineController", 'clickOnlineCustomerService $isLogin');
+
+  }
+
+  clickUserCard(){
+    if (isLogin) {
+      UserInfoPage.start();
+    } else {
+      LoginDialog.show();
+    }
   }
 
+
   clickTutorials() {
     debugPrint('clickTutorials');
 
   }
 
+
   longClickTutorials() {
 
   }
 
   clickPersonalProfile() {
     debugPrint('clickPersonalProfile');
-    // SurpriseDialog.show(clickConfirm: StorePage.start);
-    // DiscountTicketDialog.show();
-    // SmartDialog.show(
-    //
-    //   maskColor: ColorName.black70,
-    //   backType: SmartBackType.block,
-    //   clickMaskDismiss: true,
-    //   alignment: Alignment.bottomCenter,
-    //   keepSingle: true,
-    //   tag: "discountDialog",
-    //   onDismiss: () => Get.delete<DiscountController>(),
-    //
-    //   builder: (_) => DiscountView(),
-    // );
     ProfilePage.start();
 
   }

+ 5 - 3
lib/module/mine/mine_view.dart

@@ -88,11 +88,13 @@ class MineView extends BaseView<MineController> {
   // 用户信息卡片
   Widget userCard() {
     return Obx(() {
-      return Row(
+      return GestureDetector(
+        onTap: controller.clickUserCard,
+      child: Row(
         children: [
           controller.isLogin
               ? Assets.images.iconMineUserLogged.image(
-              width: 56.r, height: 56.r,fit: BoxFit.contain,)
+            width: 56.r, height: 56.r,fit: BoxFit.contain,)
               : Assets.images.iconMineUserLogged.image(
               width: 56.r, height: 56.r,fit: BoxFit.contain),
           SizedBox(width: 12.r),
@@ -109,7 +111,7 @@ class MineView extends BaseView<MineController> {
 
           Assets.images.iconMineLoginArrow.image(width: 16.r, height: 16.r),
         ],
-      );
+      ),);
     });
   }
 

+ 8 - 4
lib/module/profile/profile_page.dart

@@ -37,8 +37,8 @@ class ProfilePage extends BasePage<ProfileController> {
     return Stack(
       children: [
         SafeArea(
+
           child: CustomScrollView(
-            physics: const BouncingScrollPhysics(), // 更流畅的滚动
             slivers: [
               SliverToBoxAdapter(
                 child: Column(
@@ -46,7 +46,6 @@ class ProfilePage extends BasePage<ProfileController> {
                   children: [_buildTitle(), SizedBox(height: 26.h)],
                 ),
               ),
-
               // 键盘列表
               Obx(() {
                 if (controller.keyboardInfoList.isEmpty) {
@@ -59,7 +58,7 @@ class ProfilePage extends BasePage<ProfileController> {
                 return SliverList(
                   delegate: SliverChildBuilderDelegate((context, index) {
                     KeyboardInfo keyboardInfo =
-                        controller.keyboardInfoList[index];
+                    controller.keyboardInfoList[index];
                     return Obx(() {
                       return _buildKeyboardListItem(
                         keyboardInfo: keyboardInfo,
@@ -70,8 +69,13 @@ class ProfilePage extends BasePage<ProfileController> {
                   }, childCount: controller.keyboardInfoList.length),
                 );
               }),
+
+              SliverToBoxAdapter(
+                child: SizedBox(height: 110.h),
+              ),
             ],
-          ),
+
+          )
         ),
         Positioned(
           bottom: 20.h,

+ 10 - 1
lib/module/store/store_controller.dart

@@ -9,6 +9,7 @@ import 'package:keyboard/data/bean/pay_way_info.dart';
 import 'package:keyboard/data/bean/goods_info.dart';
 import 'package:keyboard/data/repository/account_repository.dart';
 import 'package:keyboard/data/repository/store_repository.dart';
+import 'package:keyboard/dialog/login/login_dialog.dart';
 import 'package:keyboard/dialog/payment_fail_dialog.dart';
 import 'package:keyboard/dialog/payment_success_dialog.dart';
 import 'package:keyboard/module/store/store_banner_bean.dart';
@@ -273,7 +274,7 @@ class StoreController extends BaseController implements PaymentStatusCallback {
               ToastUtil.show(error.message);
             } else if (error.code == ErrorCode.noLoginError) {
               ToastUtil.show(StringName.accountNoLogin);
-              LoginPage.start();
+            LoginDialog.show();
             } else {
               ToastUtil.show(error.message);
               paymentFail();
@@ -471,4 +472,12 @@ class StoreController extends BaseController implements PaymentStatusCallback {
       },
     );
   }
+
+  String getUserName() {
+    if (isLogin || phone != null && phone!.length > 4) {
+      return '${StringName.mineAccountLoggedDesc}${phone!.substring(phone!.length - 4)}';
+    } else {
+      return "";
+    }
+  }
 }

+ 2 - 1
lib/module/store/store_page.dart

@@ -119,6 +119,7 @@ class StorePage extends BasePage<StoreController> {
 
   // 会员状态文字逻辑提取
   List<TextSpan> _getMemberStatusText() {
+    // 未登录
     if (!controller.isLogin) {
       return [
         TextSpan(
@@ -161,7 +162,7 @@ class StorePage extends BasePage<StoreController> {
 
     // 登录但不是会员
     return [
-      TextSpan(text: username, style: _vipTextStyle(isHighlight: true)),
+      TextSpan(text: controller.userInfo?.name ?? controller.getUserName(), style: _vipTextStyle(isHighlight: true)),
       TextSpan(text: StringName.memberCardNoVipDesc, style: _vipTextStyle()),
     ];
   }

+ 53 - 0
lib/module/user_info/user_info_controller.dart

@@ -0,0 +1,53 @@
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/dialog/deprecate_diolog.dart';
+
+import '../../base/base_controller.dart';
+import '../../data/api/response/user_info_response.dart';
+import '../../data/repository/account_repository.dart';
+import 'package:get/get.dart';
+
+import '../../dialog/common_alert_dialog_impl.dart';
+import '../../utils/error_handler.dart';
+
+@injectable
+class UserInfoController extends BaseController {
+  final AccountRepository accountRepository;
+
+  Rxn<UserInfoResponse> get _userInfo => accountRepository.userInfo;
+
+  UserInfoResponse? get userInfo => _userInfo.value;
+
+  UserInfoController(this.accountRepository);
+
+  @override
+  void onInit() {
+    super.onInit();
+  }
+
+  void clickLogout() {
+    logoutAccountDialog(
+      confirmOnTap: () {
+        accountRepository.logout();
+        Get.back();
+      },
+    );
+  }
+
+  void clickDeprecate() {
+    DeprecateDialog.show(
+      btnConfirm: () async {
+        try {
+          await accountRepository.deprecateAccount();
+          accountRepository.logout();
+          Get.back();
+        } catch (error) {
+          ErrorHandler.toastError(error);
+        }
+      },
+    );
+  }
+
+  void clickBack() {
+    Get.back();
+  }
+}

+ 184 - 0
lib/module/user_info/user_info_page.dart

@@ -0,0 +1,184 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:keyboard/module/user_info/user_info_controller.dart';
+import 'package:get/get.dart';
+import 'package:keyboard/resource/string.gen.dart';
+import 'package:keyboard/utils/styles.dart';
+import '../../base/base_page.dart';
+import '../../resource/assets.gen.dart';
+import '../../router/app_pages.dart';
+import '../../widget/commonAppBar.dart';
+
+class UserInfoPage extends BasePage<UserInfoController> {
+  const UserInfoPage({super.key});
+
+  /// 跳转
+  static start() {
+    Get.toNamed(RoutePath.userInfo);
+  }
+
+  @override
+  bool immersive() {
+    return false;
+  }
+
+  @override
+  statusBarDarkFont() {
+    return true;
+  }
+
+  @override
+  Color backgroundColor() {
+    return const Color(0xFFF6F5FA);
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Scaffold(
+      backgroundColor: backgroundColor(),
+      appBar: CommonAppBar(
+        title: StringName.userInfo,
+        backgroundColor: () {
+          return Colors.transparent;
+        },
+        onBack: () {
+          controller.clickBack();
+        },
+      ),
+      body: Column(
+        children: [
+          Container(
+            margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 20.w),
+            padding: EdgeInsets.all(16.w),
+            decoration: ShapeDecoration(
+              color: Colors.white,
+              shape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(12.r),
+              ),
+            ),
+            child: Column(
+              mainAxisSize: MainAxisSize.min,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  StringName.personalInfo,
+                  style: TextStyle(
+                    color: const Color(0xFFBBBCCC),
+                    fontSize: 14.sp,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+                SizedBox(height: 24.h),
+                Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    Text(
+                      StringName.loginAccount,
+                      style: Styles.getTextStyleBlack204W400(14.sp),
+                    ),
+                    Text(
+                      controller.userInfo?.phone ?? "",
+                      style: TextStyle(
+                        color: Colors.black.withAlpha(102),
+                        fontSize: 14.sp,
+                        fontWeight: FontWeight.w400,
+                      ),
+                    ),
+                  ],
+                ),
+              ],
+            ),
+          ),
+          SizedBox(height: 8.h),
+
+          _buildDeprecateButton(),
+          const Spacer(),
+
+          _buildLogoutButton(),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildLogoutButton() {
+    return Row(
+      children: [
+        Expanded(
+          child: Container(
+            margin: EdgeInsets.only(bottom: 20.w, left: 16.w, right: 16.w),
+            height: 48.h,
+            child: Material(
+              color: const Color(0xFFEDE8FF),
+              borderRadius: BorderRadius.circular(50.r),
+              child: InkWell(
+                splashColor: Color(0xFFB983FF),
+                borderRadius: BorderRadius.circular(50.r),
+                onTap: () {
+                  controller.clickLogout();
+                },
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    Container(
+                      width: 22.w,
+                      height: 22.w,
+                      margin: EdgeInsets.only(right: 6.w),
+                      child: Assets.images.iconUserInfoLogout.image(
+                        width: 22.w,
+                        height: 22.w,
+                        fit: BoxFit.contain,
+                      ),
+                    ),
+                    Text(
+                      StringName.logout,
+                      textAlign: TextAlign.center,
+                      style: TextStyle(
+                        color: const Color(0xFF7D46FC),
+                        fontSize: 16.sp,
+                        fontWeight: FontWeight.w500,
+                        height: 1.25,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildDeprecateButton() {
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 16.w),
+      child: Material(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(12.r),
+        child: InkWell(
+          borderRadius: BorderRadius.circular(12.r),
+          onTap: () {
+            controller.clickDeprecate();
+          },
+          child: Padding(
+            padding: EdgeInsets.all(16.w),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Text(
+                  StringName.loginDeleteAccount,
+                  style: Styles.getTextStyleBlack204W400(14.sp),
+                ),
+                Assets.images.iconUserInfoArrowLeft.image(
+                  width: 20.w,
+                  height: 20.w,
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 46 - 0
lib/resource/assets.gen.dart

@@ -161,6 +161,14 @@ class $AssetsImagesGen {
   AssetGenImage get bgKeyboardScreenshotReply =>
       const AssetGenImage('assets/images/bg_keyboard_screenshot_reply.webp');
 
+  /// File path: assets/images/bg_login.webp
+  AssetGenImage get bgLogin =>
+      const AssetGenImage('assets/images/bg_login.webp');
+
+  /// File path: assets/images/bg_login_dialog.webp
+  AssetGenImage get bgLoginDialog =>
+      const AssetGenImage('assets/images/bg_login_dialog.webp');
+
   /// File path: assets/images/bg_mine.webp
   AssetGenImage get bgMine => const AssetGenImage('assets/images/bg_mine.webp');
 
@@ -669,6 +677,23 @@ class $AssetsImagesGen {
   AssetGenImage get iconLock =>
       const AssetGenImage('assets/images/icon_lock.webp');
 
+  /// File path: assets/images/icon_login_agree_privacy.webp
+  AssetGenImage get iconLoginAgreePrivacy =>
+      const AssetGenImage('assets/images/icon_login_agree_privacy.webp');
+
+  /// File path: assets/images/icon_login_dialog_close.webp
+  AssetGenImage get iconLoginDialogClose =>
+      const AssetGenImage('assets/images/icon_login_dialog_close.webp');
+
+  /// File path: assets/images/icon_login_dialog_phone_logo.webp
+  AssetGenImage get iconLoginDialogPhoneLogo =>
+      const AssetGenImage('assets/images/icon_login_dialog_phone_logo.webp');
+
+  /// File path: assets/images/icon_login_dialog_wechat_logo_white.webp
+  AssetGenImage get iconLoginDialogWechatLogoWhite => const AssetGenImage(
+    'assets/images/icon_login_dialog_wechat_logo_white.webp',
+  );
+
   /// File path: assets/images/icon_member_retain_close.webp
   AssetGenImage get iconMemberRetainClose =>
       const AssetGenImage('assets/images/icon_member_retain_close.webp');
@@ -907,6 +932,14 @@ class $AssetsImagesGen {
   AssetGenImage get iconUploading =>
       const AssetGenImage('assets/images/icon_uploading.webp');
 
+  /// File path: assets/images/icon_user_info_arrow_left.webp
+  AssetGenImage get iconUserInfoArrowLeft =>
+      const AssetGenImage('assets/images/icon_user_info_arrow_left.webp');
+
+  /// File path: assets/images/icon_user_info_logout.webp
+  AssetGenImage get iconUserInfoLogout =>
+      const AssetGenImage('assets/images/icon_user_info_logout.webp');
+
   /// File path: assets/images/icon_virgo.webp
   AssetGenImage get iconVirgo =>
       const AssetGenImage('assets/images/icon_virgo.webp');
@@ -915,6 +948,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconWechat =>
       const AssetGenImage('assets/images/icon_wechat.webp');
 
+  /// File path: assets/images/icon_wechat_logo_black.webp
+  AssetGenImage get iconWechatLogoBlack =>
+      const AssetGenImage('assets/images/icon_wechat_logo_black.webp');
+
   /// File path: assets/images/icon_wechat_payment.webp
   AssetGenImage get iconWechatPayment =>
       const AssetGenImage('assets/images/icon_wechat_payment.webp');
@@ -956,6 +993,8 @@ class $AssetsImagesGen {
     bgKeyboardManage,
     bgKeyboardManageIntimacy,
     bgKeyboardScreenshotReply,
+    bgLogin,
+    bgLoginDialog,
     bgMine,
     bgMineVipCard,
     bgProfileEditIntimacy,
@@ -1076,6 +1115,10 @@ class $AssetsImagesGen {
     iconLeo,
     iconLibra,
     iconLock,
+    iconLoginAgreePrivacy,
+    iconLoginDialogClose,
+    iconLoginDialogPhoneLogo,
+    iconLoginDialogWechatLogoWhite,
     iconMemberRetainClose,
     iconMineAbout,
     iconMineArrow,
@@ -1135,8 +1178,11 @@ class $AssetsImagesGen {
     iconUploadFail,
     iconUploadScreenshotSampleImage,
     iconUploading,
+    iconUserInfoArrowLeft,
+    iconUserInfoLogout,
     iconVirgo,
     iconWechat,
+    iconWechatLogoBlack,
     iconWechatPayment,
     iconWechatScanPayment,
     iconWhiteBackArrow,

+ 50 - 0
lib/resource/string.gen.dart

@@ -28,12 +28,29 @@ class StringName {
   static final String networkError = 'network_error'.tr; // 网络异常
   static final String accountNoLogin = 'account_no_login'.tr; // 账号未登录
   static final String loginAccount = 'login_account'.tr; // 登录账号
+  static final String loginAgreePrivacy = 'login_agree_privacy'.tr; // 请先阅读并同意《隐私权政策》和《服务条款》
   static final String loginRequestCodeFrequentlyToast = 'login_request_code_frequently_toast'.tr; // 请求过于频繁,请稍后再试
   static final String loginVerificationCodeRequestFailedToast = 'login_verification_code_request_failed_toast'.tr; // 验证码发送失败,请重试
   static final String loginSuccess = 'login_success'.tr; // 登录成功
   static final String loginTooOftenToast = 'login_too_often_toast'.tr; // 登录过于频繁,请稍后再试
   static final String loginFailedToast = 'login_failed_toast'.tr; // 登录失败
   static final String loginVerificationCodeErrorToast = 'login_verification_code_error_toast'.tr; // 验证码输入错误,请重新输入
+  static final String login = 'login'.tr; // 登录
+  static final String loginPrintVerificationCode = 'login_print_verification_code'.tr; // 请输入验证码
+  static final String loginPrintPhoneVerification = 'login_print_phone_verification'.tr; // 请输入正确格式的手机号码
+  static final String loginSendVerificationCode = 'login_send_verification_code'.tr; // 发送验证码
+  static final String loginRetransmissionCode = 'login_retransmission_code'.tr; // s
+  static final String loginEtPhoneHint = 'login_et_phone_hint'.tr; // 请输入手机号
+  static final String loginResendCode = 'login_resend_code'.tr; // 重新发送
+  static final String loginOtherlogin = 'login_other_login'.tr; // 其他登录方式
+  static final String wechat = 'wechat'.tr; // 微信
+  static final String loginDeleteAccount = 'login_delete_account'.tr; // 注销账号
+  static final String loginDeleteAccountDesc = 'login_delete_account_desc'.tr; // 确认注销后您将失去以下权益 \n1.无法登录本账号 \n2.您的个人相关数据将被清空,人设设置与个人档案等相关数据将无法恢复。 \n3.会员在有效期内将放弃本账号所有权益
+  static final String loginDeleteAccountConfirm = 'login_delete_account_confirm'.tr; // 确认注销
+  static final String loginDeleteAccountCancel = 'login_delete_account_cancel'.tr; // 再想想
+  static final String userInfo = 'user_info'.tr; // 用户信息
+  static final String personalInfo = 'personal_info'.tr; // 个人信息
+  static final String logout = 'logout'.tr; // 退出登录
   static final String feedbackContentTitle = 'feedback_content_title'.tr; // 问题描述
   static final String feedbackContentHint = 'feedback_content_hint'.tr; // 请描述您的问题或建议,或者您可以联系我们在线客服
   static final String feedbackPhone = 'feedback_phone'.tr; // 联系电话
@@ -210,6 +227,14 @@ class StringName {
   static final String recently = 'recently'.tr; // 最近
   static final String addHobbies = 'add_hobbies'.tr; // 添加爱好
   static final String save = 'save'.tr; // 保存
+  static final String wechatLogin = 'wechat_login'.tr; // 微信登录
+  static final String phoneLogin = 'phone_login'.tr; // 手机号登录
+  static final String tip = 'tip'.tr; // 提示
+  static final String privacyDialogCancel = 'privacy_dialog_cancel'.tr; // 不同意
+  static final String privacyDialogConfirm = 'privacy_dialog_confirm'.tr; // 同意
+  static final String logoutDialogCancel = 'logout_dialog_cancel'.tr; // 取消
+  static final String logoutDialogConfirm = 'logout_dialog_confirm'.tr; // 确认
+  static final String logoutDialogDesc = 'logout_dialog_desc'.tr; // 确定退出登录吗?
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -241,12 +266,29 @@ class StringMultiSource {
       'network_error': '网络异常',
       'account_no_login': '账号未登录',
       'login_account': '登录账号',
+      'login_agree_privacy': '请先阅读并同意《隐私权政策》和《服务条款》',
       'login_request_code_frequently_toast': '请求过于频繁,请稍后再试',
       'login_verification_code_request_failed_toast': '验证码发送失败,请重试',
       'login_success': '登录成功',
       'login_too_often_toast': '登录过于频繁,请稍后再试',
       'login_failed_toast': '登录失败',
       'login_verification_code_error_toast': '验证码输入错误,请重新输入',
+      'login': '登录',
+      'login_print_verification_code': '请输入验证码',
+      'login_print_phone_verification': '请输入正确格式的手机号码',
+      'login_send_verification_code': '发送验证码',
+      'login_retransmission_code': 's',
+      'login_et_phone_hint': '请输入手机号',
+      'login_resend_code': '重新发送',
+      'login_other_login': '其他登录方式',
+      'wechat': '微信',
+      'login_delete_account': '注销账号',
+      'login_delete_account_desc': '确认注销后您将失去以下权益 \n1.无法登录本账号 \n2.您的个人相关数据将被清空,人设设置与个人档案等相关数据将无法恢复。 \n3.会员在有效期内将放弃本账号所有权益',
+      'login_delete_account_confirm': '确认注销',
+      'login_delete_account_cancel': '再想想',
+      'user_info': '用户信息',
+      'personal_info': '个人信息',
+      'logout': '退出登录',
       'feedback_content_title': '问题描述',
       'feedback_content_hint': '请描述您的问题或建议,或者您可以联系我们在线客服',
       'feedback_phone': '联系电话',
@@ -423,6 +465,14 @@ class StringMultiSource {
       'recently': '最近',
       'add_hobbies': '添加爱好',
       'save': '保存',
+      'wechat_login': '微信登录',
+      'phone_login': '手机号登录',
+      'tip': '提示',
+      'privacy_dialog_cancel': '不同意',
+      'privacy_dialog_confirm': '同意',
+      'logout_dialog_cancel': '取消',
+      'logout_dialog_confirm': '确认',
+      'logout_dialog_desc': '确定退出登录吗?',
     },
   };
 }

+ 8 - 0
lib/router/app_pages.dart

@@ -1,4 +1,5 @@
 import 'package:get/get.dart';
+import 'package:keyboard/dialog/login/login_dialog_controller.dart';
 import 'package:keyboard/module/about/about_controller.dart';
 import 'package:keyboard/module/browser/browser_controller.dart';
 import 'package:keyboard/module/change/birthday/change_birthday_controller.dart';
@@ -23,6 +24,8 @@ import 'package:keyboard/module/profile/profile_controller.dart';
 import 'package:keyboard/module/profile/profile_page.dart';
 import 'package:keyboard/module/store/store_controller.dart';
 import 'package:keyboard/module/store/store_page.dart';
+import 'package:keyboard/module/user_info/user_info_controller.dart';
+import 'package:keyboard/module/user_info/user_info_page.dart';
 
 import '../di/get_it.dart';
 import '../module/about/about_page.dart';
@@ -88,6 +91,7 @@ abstract class RoutePath {
   static const changeBirthday = '/changeBirthday';
   static const changeHobbies = '/changeHobbies';
   static const changeCharacters = '/changeCharacters';
+  static const userInfo = '/userInfo';
 
   // 图片预览页
   static const imageViewer = '/imageViewerPage';
@@ -126,7 +130,9 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<ScanImageReplyController>());
     lazyPut(() => getIt.get<ChangeHobbiesController>());
     lazyPut(() => getIt.get<ChangeCharacterController>());
+    lazyPut(() => getIt.get<LoginDialogController>());
     lazyPut(() => getIt.get<ImageViewerController>());
+    lazyPut(() => getIt.get<UserInfoController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -171,4 +177,6 @@ final generalPages = [
 
   // 图片预览页
   GetPage(name: RoutePath.imageViewer, page: () => ImageViewerPage()),
+
+  GetPage(name: RoutePath.userInfo, page: () => UserInfoPage()),
 ];

+ 1 - 0
lib/utils/styles.dart

@@ -8,6 +8,7 @@ class Styles {
       gradient: LinearGradient(
         begin: Alignment.centerLeft,
         end: Alignment.centerRight,
+          transform: GradientRotation(0.5),
         colors: [const Color(0xFF7D46FC), const Color(0xFFBC87FF)],
       ),
 

+ 27 - 20
lib/widget/pargress_bar.dart

@@ -1,9 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 
-import 'package:flutter/material.dart';
-import 'package:flutter_screenutil/flutter_screenutil.dart';
-
 class ProgressBar extends StatefulWidget {
   final String title;
   final int? value;
@@ -31,14 +28,9 @@ class _ProgressBarState extends State<ProgressBar>
     super.initState();
     _animationController = AnimationController(
       vsync: this,
-      duration: Duration(seconds: 1),
-    );
-
-    _valueTween = Tween<double>(begin: 0.0, end: _getTargetProgress());
-    _progressAnimation = _valueTween.animate(
-      CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
+      duration: const Duration(seconds: 2),
     );
-
+    _setupAnimation();
     _startAnimation();
   }
 
@@ -47,15 +39,27 @@ class _ProgressBarState extends State<ProgressBar>
     super.didUpdateWidget(oldWidget);
 
     if (oldWidget.value != widget.value) {
+      _setupAnimation();
+      _startAnimation();
+    }
+  }
+
+  /// 设置动画曲线和范围
+
+  void _setupAnimation() {
+    if (widget.value == null) {
+      _valueTween = Tween<double>(begin: 0.0, end: 1.0);
+    } else {
+
       _valueTween = Tween<double>(
-        begin: _progressAnimation.value,
+        begin: 0,
         end: _getTargetProgress(),
       );
-      _progressAnimation = _valueTween.animate(
-        CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
-      );
-      _startAnimation();
     }
+
+    _progressAnimation = _valueTween.animate(
+      CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
+    );
   }
 
   double _getTargetProgress() {
@@ -63,12 +67,13 @@ class _ProgressBarState extends State<ProgressBar>
   }
 
   void _startAnimation() {
+    _animationController.stop();
     if (widget.value == null) {
-      _animationController.repeat(); // 加载中,持续动画
+      _animationController.repeat();
     } else {
       _animationController
         ..reset()
-        ..forward(); // 有值,从 0 -> value 动画
+        ..forward();
     }
   }
 
@@ -88,13 +93,15 @@ class _ProgressBarState extends State<ProgressBar>
         Expanded(
           child: Stack(
             children: [
+              // 背景条
               Container(
                 height: 11.h,
                 decoration: BoxDecoration(
-                  color: widget.color.withValues(alpha: 0.3),
+                  color: widget.color.withOpacity(0.3),
                   borderRadius: BorderRadius.circular(53.r),
                 ),
               ),
+              // 动画进度条
               AnimatedBuilder(
                 animation: _progressAnimation,
                 builder: (_, __) {
@@ -111,6 +118,7 @@ class _ProgressBarState extends State<ProgressBar>
                   );
                 },
               ),
+              // 中间文字
               Positioned.fill(
                 child: Center(
                   child: Text(
@@ -123,7 +131,7 @@ class _ProgressBarState extends State<ProgressBar>
                       fontWeight: FontWeight.w500,
                       shadows: [
                         Shadow(
-                          color: Colors.black.withValues(alpha: 0.6),
+                          color: Colors.black.withOpacity(0.6),
                           offset: Offset(1, 1),
                           blurRadius: 3.r,
                         ),
@@ -139,4 +147,3 @@ class _ProgressBarState extends State<ProgressBar>
     );
   }
 }
-

+ 1 - 1
pubspec.yaml

@@ -177,5 +177,5 @@ flutter:
 
 wechat_kit:
   ios: no_pay # 默认 pay
-  app_id: wx
+  app_id: wx21272929e8fd33e9
   universal_link: https://flutter.dev