Browse Source

[new]增加头像选择

zk 5 months ago
parent
commit
24bcce387f

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

@@ -306,4 +306,5 @@
     <string name="dialog_net_error_title">网络已断开</string>
     <string name="dialog_net_error_desc">请检查您的网络连接并重试</string>
     <string name="dialog_net_error_again">重试</string>
+    <string name="mine_update_avatar_success">设置成功</string>
 </resources>

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

@@ -18,6 +18,7 @@ import 'package:location/data/api/request/submit_and_request_pay_request.dart';
 import 'package:location/data/api/request/subscription_check_request.dart';
 import 'package:location/data/api/request/subscription_resume_request.dart';
 import 'package:location/data/api/request/upload_client_id_request.dart';
+import 'package:location/data/api/request/user_avatar_update_request.dart';
 import 'package:location/data/api/response/configs_response.dart';
 import 'package:location/data/api/response/contact_list_response.dart';
 import 'package:location/data/api/response/contact_may_day_all_response.dart';
@@ -32,10 +33,12 @@ import 'package:location/data/api/response/query_track_response.dart';
 import 'package:location/data/api/response/request_friend_list_response.dart';
 import 'package:location/data/api/response/request_pay_response.dart';
 import 'package:location/data/api/response/subscription_check_response.dart';
+import 'package:location/data/api/response/user_avatar_response.dart';
 import 'package:retrofit/error_logger.dart';
 import 'package:retrofit/http.dart';
 import 'package:retrofit/dio.dart';
 import '../bean/user_info.dart';
+
 part 'atmob_api.g.dart';
 
 @RestApi()
@@ -166,9 +169,19 @@ abstract class AtmobApi {
 
   ///查询订阅状态
   @POST("/s/v1/subscription/check")
-  Future<BaseResponse<SubscriptionCheckResponse>> subscriptionCheck(@Body() SubscriptionCheckRequest request);
+  Future<BaseResponse<SubscriptionCheckResponse>> subscriptionCheck(
+      @Body() SubscriptionCheckRequest request);
 
   ///恢复订阅
   @POST("/s/v1/subscription/resume")
-  Future<BaseResponse> subscriptionresume(@Body() SubscriptionResumeRequest request);
+  Future<BaseResponse> subscriptionresume(
+      @Body() SubscriptionResumeRequest request);
+
+  @POST("/s/v1/user/avatar/list")
+  Future<BaseResponse<UserAvatarResponse>> userAvatarList(
+      @Body() AppBaseRequest request);
+
+  @POST("/s/v1/user/avatar/update")
+  Future<BaseResponse> userAvatarUpdate(
+      @Body() UserAvatarUpdateRequest request);
 }

+ 76 - 0
lib/data/api/atmob_api.g.dart

@@ -1318,6 +1318,82 @@ class _AtmobApi implements AtmobApi {
     return _value;
   }
 
+  @override
+  Future<BaseResponse<UserAvatarResponse>> userAvatarList(
+      AppBaseRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<UserAvatarResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/user/avatar/list',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<UserAvatarResponse> _value;
+    try {
+      _value = BaseResponse<UserAvatarResponse>.fromJson(
+        _result.data!,
+        (json) => UserAvatarResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<dynamic>> userAvatarUpdate(
+      UserAvatarUpdateRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<dynamic>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/user/avatar/update',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<dynamic> _value;
+    try {
+      _value = BaseResponse<dynamic>.fromJson(
+        _result.data!,
+        (json) => json as dynamic,
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
   RequestOptions newRequestOptions(Object? options) {
     if (options is RequestOptions) {
       return options as RequestOptions;

+ 15 - 0
lib/data/api/request/user_avatar_update_request.dart

@@ -0,0 +1,15 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
+
+part 'user_avatar_update_request.g.dart';
+
+@JsonSerializable()
+class UserAvatarUpdateRequest extends AppBaseRequest {
+  @JsonKey(name: 'avatar')
+  final String avatar;
+
+  UserAvatarUpdateRequest(this.avatar);
+
+  @override
+  Map<String, dynamic> toJson() => _$UserAvatarUpdateRequestToJson(this);
+}

+ 71 - 0
lib/data/api/request/user_avatar_update_request.g.dart

@@ -0,0 +1,71 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'user_avatar_update_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+UserAvatarUpdateRequest _$UserAvatarUpdateRequestFromJson(
+        Map<String, dynamic> json) =>
+    UserAvatarUpdateRequest(
+      json['avatar'] as String,
+    )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$UserAvatarUpdateRequestToJson(
+        UserAvatarUpdateRequest instance) =>
+    <String, dynamic>{
+      'appPlatform': instance.appPlatform,
+      'os': instance.os,
+      'osVersion': instance.osVersion,
+      'packageName': instance.packageName,
+      'appVersionName': instance.appVersionName,
+      'appVersionCode': instance.appVersionCode,
+      'channelName': instance.channelName,
+      'appId': instance.appId,
+      'tgPlatform': instance.tgPlatform,
+      'oaid': instance.oaid,
+      'aaid': instance.aaid,
+      'androidId': instance.androidId,
+      'imei': instance.imei,
+      'simImei0': instance.simImei0,
+      'simImei1': instance.simImei1,
+      'mac': instance.mac,
+      'idfa': instance.idfa,
+      'idfv': instance.idfv,
+      'machineId': instance.machineId,
+      'brand': instance.brand,
+      'model': instance.model,
+      'wifiName': instance.wifiName,
+      'region': instance.region,
+      'locLng': instance.locLng,
+      'locLat': instance.locLat,
+      'authToken': instance.authToken,
+      'avatar': instance.avatar,
+    };

+ 4 - 0
lib/data/api/response/member_status_response.dart

@@ -28,6 +28,9 @@ class MemberStatusResponse {
   @JsonKey(name: 'deviceId')
   final String deviceId;
 
+  @JsonKey(name: 'avatar')
+  final String? avatar;
+
   MemberStatusResponse({
     required this.userId,
     required this.level,
@@ -37,6 +40,7 @@ class MemberStatusResponse {
     required this.expired,
     required this.permanent,
     required this.deviceId,
+    required this.avatar,
   });
 
   // 反序列化:从 JSON 到 Dart 对象

+ 2 - 0
lib/data/api/response/member_status_response.g.dart

@@ -17,6 +17,7 @@ MemberStatusResponse _$MemberStatusResponseFromJson(
       expired: json['expired'] as bool,
       permanent: json['permanent'] as bool,
       deviceId: json['deviceId'] as String,
+      avatar: json['avatar'] as String?,
     );
 
 Map<String, dynamic> _$MemberStatusResponseToJson(
@@ -30,4 +31,5 @@ Map<String, dynamic> _$MemberStatusResponseToJson(
       'expired': instance.expired,
       'permanent': instance.permanent,
       'deviceId': instance.deviceId,
+      'avatar': instance.avatar,
     };

+ 14 - 0
lib/data/api/response/user_avatar_response.dart

@@ -0,0 +1,14 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'user_avatar_response.g.dart';
+
+@JsonSerializable()
+class UserAvatarResponse {
+  @JsonKey(name: 'list')
+  List<String>? list;
+
+  UserAvatarResponse({this.list});
+
+  factory UserAvatarResponse.fromJson(Map<String, dynamic> json) =>
+      _$UserAvatarResponseFromJson(json);
+}

+ 17 - 0
lib/data/api/response/user_avatar_response.g.dart

@@ -0,0 +1,17 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'user_avatar_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+UserAvatarResponse _$UserAvatarResponseFromJson(Map<String, dynamic> json) =>
+    UserAvatarResponse(
+      list: (json['list'] as List<dynamic>?)?.map((e) => e as String).toList(),
+    );
+
+Map<String, dynamic> _$UserAvatarResponseToJson(UserAvatarResponse instance) =>
+    <String, dynamic>{
+      'list': instance.list,
+    };

+ 4 - 0
lib/data/bean/user_info.dart

@@ -31,6 +31,9 @@ class UserInfo {
   @JsonKey(name: 'virtual')
   final bool? virtual;
 
+  @JsonKey(name: 'avatar')
+  String? avatar;
+
   final bool? isMine;
 
   UserInfo({
@@ -42,6 +45,7 @@ class UserInfo {
     this.blockedMe,
     this.virtual,
     this.isMine,
+    this.avatar,
   });
 
   factory UserInfo.fromJson(Map<String, dynamic> json) {

+ 2 - 0
lib/data/bean/user_info.g.dart

@@ -15,6 +15,7 @@ UserInfo _$UserInfoFromJson(Map<String, dynamic> json) => UserInfo(
       blockedMe: json['blockedMe'] as bool?,
       virtual: json['virtual'] as bool?,
       isMine: json['isMine'] as bool?,
+      avatar: json['avatar'] as String?,
     );
 
 Map<String, dynamic> _$UserInfoToJson(UserInfo instance) => <String, dynamic>{
@@ -25,5 +26,6 @@ Map<String, dynamic> _$UserInfoToJson(UserInfo instance) => <String, dynamic>{
       'blockedHim': instance.blockedHim,
       'blockedMe': instance.blockedMe,
       'virtual': instance.virtual,
+      'avatar': instance.avatar,
       'isMine': instance.isMine,
     };

+ 19 - 2
lib/data/repositories/account_repository.dart

@@ -23,6 +23,7 @@ import 'package:location/utils/http_handler.dart';
 import 'package:location/utils/mmkv_util.dart';
 
 import '../../sdk/map/map_helper.dart';
+import '../api/request/user_avatar_update_request.dart';
 import '../api/response/login_response.dart';
 import '../api/response/member_status_response.dart';
 
@@ -75,6 +76,10 @@ class AccountRepository {
     });
   }
 
+  static AccountRepository getInstance() {
+    return getIt.get<AccountRepository>();
+  }
+
   Future<void> loginSendCode(String phoneNum) {
     final currentTime = DateTime.now().millisecondsSinceEpoch;
 
@@ -169,6 +174,7 @@ class AccountRepository {
         .then(HttpHandler.handle(false))
         .then((response) {
       refreshMemberHandler?.cancel();
+      updateAvatar(response.avatar);
       KVUtil.putString(keyAccountLoginUserId, response.deviceId);
       if (!response.permanent && !response.expired) {
         refreshMemberHandler = Timer(
@@ -188,8 +194,19 @@ class AccountRepository {
     });
   }
 
-  static AccountRepository getInstance() {
-    return getIt.get<AccountRepository>();
+  void updateAvatar(String? avatar) {
+    mineUserInfo.value.avatar = avatar;
+    mineUserInfo.refresh();
+  }
+
+  Future<void> userAvatarUpdate(String avatar) {
+    return atmobApi
+        .userAvatarUpdate(UserAvatarUpdateRequest(avatar))
+        .then(HttpHandler.handle(true))
+        .then((_) {
+      updateAvatar(avatar);
+      AtmobLog.d(tag, "userAvatarUpdate success: $avatar");
+    });
   }
 
   bool memberIsExpired() {

+ 21 - 0
lib/data/repositories/config_repository.dart

@@ -1,5 +1,6 @@
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
+import 'package:location/base/app_base_request.dart';
 import 'package:location/data/api/atmob_api.dart';
 import 'package:location/data/api/request/configs_request.dart';
 import 'package:location/data/api/request/upload_client_id_request.dart';
@@ -10,6 +11,7 @@ import 'package:location/utils/async_util.dart';
 import 'package:location/utils/http_handler.dart';
 
 import '../api/response/configs_response.dart';
+import '../api/response/user_avatar_response.dart';
 
 @lazySingleton
 class ConfigRepository {
@@ -21,6 +23,8 @@ class ConfigRepository {
 
   final Rxn<bool> isOpenFreeMember = Rxn<bool>();
 
+  List<String>? userAvatarList;
+
   final AtmobApi atmobApi;
 
   final FriendsRepository friendsRepository;
@@ -81,4 +85,21 @@ class ConfigRepository {
         .uploadClientId(UploadClientIdRequest(clientId))
         .then(HttpHandler.handle(true));
   }
+
+  Future<List<String>> requestUserAvatarList() {
+    if (userAvatarList != null && userAvatarList!.isNotEmpty) {
+      return Future.value(userAvatarList);
+    }
+    return atmobApi
+        .userAvatarList(AppBaseRequest())
+        .then(HttpHandler.handle(false))
+        .then((response) {
+      final list = response.list;
+      userAvatarList = list;
+      if (list == null) {
+        throw Exception("User avatar list is null");
+      }
+      return list;
+    });
+  }
 }

+ 113 - 0
lib/dialog/user_avatar_dialog.dart

@@ -0,0 +1,113 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:location/resource/colors.gen.dart';
+
+typedef AvatarSelectedCallback = void Function(String avatarUrl);
+
+class UserAvatarDialog {
+  static const String _tag = 'UserAvatarDialog';
+
+  static void show(List<String> avatarList, String? selectedAvatarUrl,
+      {required AvatarSelectedCallback onAvatarSelected}) {
+    SmartDialog.show(
+        builder: (_) =>
+            _UserAvatarView(avatarList, onAvatarSelected, selectedAvatarUrl),
+        tag: _tag);
+  }
+
+  static void dismiss() {
+    SmartDialog.dismiss(tag: _tag);
+  }
+}
+
+class _UserAvatarView extends StatelessWidget {
+  final List<String> avatarList;
+  final RxnString selectedAvatar = RxnString();
+  final AvatarSelectedCallback onAvatarSelected;
+
+  _UserAvatarView(
+      this.avatarList, this.onAvatarSelected, String? selectedAvatarUrl) {
+    selectedAvatar.value = selectedAvatarUrl;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      color: Colors.white,
+      width: 278.w,
+      child: IntrinsicHeight(
+        child: Column(
+          children: [
+            buildSelectAvatarView(),
+            buildUserAvatarListView(),
+            SizedBox(height: 30.h),
+            GestureDetector(
+              onTap: onSelectSureClick,
+              child: Container(
+                  margin: EdgeInsets.symmetric(horizontal: 25.w),
+                  decoration: BoxDecoration(
+                    color: ColorName.colorPrimary,
+                    borderRadius: BorderRadius.circular(25.w),
+                  ),
+                  width: 328.w,
+                  height: 40.w,
+                  child: Center(child: Text('确认'))),
+            ),
+            SizedBox(height: 20.h),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildUserAvatarListView() {
+    return Wrap(
+      spacing: 10,
+      runSpacing: 10,
+      children: avatarList.map((url) {
+        return GestureDetector(
+          onTap: () {
+            onSelectAvatarClick(url);
+          },
+          child: ClipOval(
+            child: CachedNetworkImage(
+              width: 50.w,
+              height: 50.w,
+              imageUrl: url,
+              fit: BoxFit.cover,
+            ),
+          ),
+        );
+      }).toList(),
+    );
+  }
+
+  Widget buildSelectAvatarView() {
+    return ClipOval(
+      child: Obx(() {
+        return CachedNetworkImage(
+          width: 50.w,
+          height: 50.w,
+          imageUrl: selectedAvatar.value ?? '',
+          fit: BoxFit.cover,
+        );
+      }),
+    );
+  }
+
+  void onSelectAvatarClick(String avatarUrl) {
+    selectedAvatar.value = avatarUrl;
+  }
+
+  void onSelectSureClick() {
+    final avatar = selectedAvatar.value;
+    if (avatar == null || avatar.isEmpty) {
+      return;
+    }
+    onAvatarSelected(avatar);
+  }
+}

+ 24 - 0
lib/module/mine/mine_controller.dart

@@ -5,6 +5,7 @@ import 'package:get/get_core/src/get_main.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
 import 'package:location/data/bean/member_status_info.dart';
+import 'package:location/data/bean/user_info.dart';
 import 'package:location/data/consts/error_code.dart';
 import 'package:location/data/repositories/config_repository.dart';
 import 'package:location/handler/error_handler.dart';
@@ -18,6 +19,7 @@ import 'package:location/utils/http_handler.dart';
 import '../../data/repositories/account_repository.dart';
 import '../../data/repositories/member_repository.dart';
 import '../../dialog/common_alert_dialog_impl.dart';
+import '../../dialog/user_avatar_dialog.dart';
 import '../../sdk/wechat/wechat_share_util.dart';
 import '../../utils/app_info_util.dart';
 import '../../utils/toast_util.dart';
@@ -38,6 +40,8 @@ class MineController extends BaseController {
 
   bool? get isOpenFreeMember => configRepository.isOpenFreeMember.value;
 
+  UserInfo get mineInfo => accountRepository.mineUserInfo.value;
+
   MemberStatusInfo? get memberStatusInfo =>
       accountRepository.memberStatusInfo.value;
 
@@ -106,11 +110,31 @@ class MineController extends BaseController {
 
   onLoginClick() {
     if (isLogin) {
+      editUserAvatar();
       return;
     }
     LoginPage.start();
   }
 
+  void editUserAvatar() {
+    configRepository.requestUserAvatarList().then((list) {
+      UserAvatarDialog.show(
+          list, AccountRepository.getInstance().mineUserInfo.value.avatar,
+          onAvatarSelected: onAvatarSelected);
+    }).catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+
+  void onAvatarSelected(String avatar) {
+    accountRepository.userAvatarUpdate(avatar).then((_) {
+      ToastUtil.show(StringName.mineUpdateAvatarSuccess);
+      UserAvatarDialog.dismiss();
+    }).catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+
   onUrgentContactClick() {
     UrgentContactPage.start();
   }

+ 21 - 2
lib/module/mine/mine_page.dart

@@ -1,3 +1,4 @@
+import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -47,8 +48,10 @@ class MinePage extends BasePage<MineController> {
                     SizedBox(width: 12.w),
                     Obx(() {
                       return controller.isLogin
-                          ? Assets.images.iconMineLogged
-                              .image(width: 54.w, height: 54.w)
+                          ? (controller.mineInfo.avatar != null
+                              ? buildAvatarView(controller.mineInfo.avatar!)
+                              : Assets.images.iconMineLogged
+                                  .image(width: 54.w, height: 54.w))
                           : Assets.images.iconMineNoLogin
                               .image(width: 54.w, height: 54.w);
                     }),
@@ -374,4 +377,20 @@ class MinePage extends BasePage<MineController> {
       ),
     );
   }
+
+  Widget buildAvatarView(String avatar) {
+    return Container(
+      decoration: BoxDecoration(
+        shape: BoxShape.circle,
+        border: Border.all(
+          color: '#E8E1FF'.color,
+          width: 1.w,
+        ),
+      ),
+      child: ClipOval(
+        child: CachedNetworkImage(
+            width: 54.w, height: 54.w, imageUrl: avatar, fit: BoxFit.cover),
+      ),
+    );
+  }
 }

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

@@ -360,6 +360,8 @@ class StringName {
   static String get dialogNetErrorDesc =>
       'dialog_net_error_desc'.tr; // 请检查您的网络连接并重试
   static String get dialogNetErrorAgain => 'dialog_net_error_again'.tr; // 重试
+  static String get mineUpdateAvatarSuccess =>
+      'mine_update_avatar_success'.tr; // 设置成功
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -615,6 +617,7 @@ class StringMultiSource {
       'dialog_net_error_title': '网络已断开',
       'dialog_net_error_desc': '请检查您的网络连接并重试',
       'dialog_net_error_again': '重试',
+      'mine_update_avatar_success': '设置成功',
     },
   };
 }

+ 3 - 0
pubspec.yaml

@@ -127,6 +127,9 @@ dependencies:
   #网络连接情况
   internet_connection_checker: ^3.0.1
 
+  #图片缓存
+  cached_network_image: ^3.4.1
+
   ######################地图########################
   flutter_map:
     path: plugins/map