浏览代码

Merge branch 'v1.1.0' of git.atmob.com:Atmob-Flutter/location2025 into v1.1.0

# Conflicts:
#	lib/data/api/atmob_api.dart
#	lib/data/api/atmob_api.g.dart
#	lib/data/api/response/member_status_response.dart
#	lib/data/api/response/member_status_response.g.dart
zhoukun 5 月之前
父节点
当前提交
36367797cd

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

@@ -309,4 +309,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>

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

@@ -20,6 +20,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';
@@ -35,10 +36,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()
@@ -61,7 +64,7 @@ abstract class AtmobApi {
   Future<BaseResponse<FriendsListResponse>> friendList(
       @Body() FriendsListRequest request);
 
-  @POST("/s/v1/client/configs")
+  @POST("/s/v1/confs")
   Future<BaseResponse<ConfigsResponse>> getConfigs(
       @Body() ConfigsRequest request);
 
@@ -169,7 +172,8 @@ 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")
@@ -182,4 +186,12 @@ abstract class AtmobApi {
   ///试用期间上报查看轨迹次数-
   @POST("/s/v1/member/trial/track")
   Future<BaseResponse> memberTrailTrack(@Body() AppBaseRequest 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);
 }

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

@@ -187,7 +187,7 @@ class _AtmobApi implements AtmobApi {
     )
         .compose(
           _dio.options,
-          '/s/v1/client/configs',
+          '/s/v1/confs',
           queryParameters: queryParameters,
           data: _data,
         )
@@ -1395,6 +1395,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;

+ 4 - 3
lib/data/api/request/configs_request.dart

@@ -5,10 +5,11 @@ part 'configs_request.g.dart';
 
 @JsonSerializable()
 class ConfigsRequest extends AppBaseRequest {
-  @JsonKey(name: "ids")
-  List<int> ids;
+  @JsonKey(name: "confCodes")
+  List<String> confCodes;
 
-  ConfigsRequest({required this.ids});
+  ConfigsRequest(this.confCodes);
 
+  @override
   Map<String, dynamic> toJson() => _$ConfigsRequestToJson(this);
 }

+ 2 - 4
lib/data/api/request/configs_request.g.dart

@@ -8,9 +8,7 @@ part of 'configs_request.dart';
 
 ConfigsRequest _$ConfigsRequestFromJson(Map<String, dynamic> json) =>
     ConfigsRequest(
-      ids: (json['ids'] as List<dynamic>)
-          .map((e) => (e as num).toInt())
-          .toList(),
+      (json['confCodes'] as List<dynamic>).map((e) => e as String).toList(),
     )
       ..appPlatform = (json['appPlatform'] as num).toInt()
       ..os = json['os'] as String
@@ -67,5 +65,5 @@ Map<String, dynamic> _$ConfigsRequestToJson(ConfigsRequest instance) =>
       'locLng': instance.locLng,
       'locLat': instance.locLat,
       'authToken': instance.authToken,
-      'ids': instance.ids,
+      'confCodes': instance.confCodes,
     };

+ 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,
+    };

+ 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,
+    };

+ 5 - 5
lib/data/bean/config_bean.dart

@@ -4,13 +4,13 @@ part 'config_bean.g.dart';
 
 @JsonSerializable()
 class ConfigBean {
-  @JsonKey(name: "id")
-  int id;
+  @JsonKey(name: "confCode")
+  String confCode;
 
-  @JsonKey(name: "cfg")
-  Map<String, dynamic>? cfg;
+  @JsonKey(name: "value")
+  Map<String, dynamic>? value;
 
-  ConfigBean(this.id, this.cfg);
+  ConfigBean(this.confCode, this.value);
 
   factory ConfigBean.fromJson(Map<String, dynamic> json) =>
       _$ConfigBeanFromJson(json);

+ 4 - 4
lib/data/bean/config_bean.g.dart

@@ -7,12 +7,12 @@ part of 'config_bean.dart';
 // **************************************************************************
 
 ConfigBean _$ConfigBeanFromJson(Map<String, dynamic> json) => ConfigBean(
-      (json['id'] as num).toInt(),
-      json['cfg'] as Map<String, dynamic>?,
+      json['confCode'] as String,
+      json['value'] as Map<String, dynamic>?,
     );
 
 Map<String, dynamic> _$ConfigBeanToJson(ConfigBean instance) =>
     <String, dynamic>{
-      'id': instance.id,
-      'cfg': instance.cfg,
+      'confCode': instance.confCode,
+      'value': instance.value,
     };

+ 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(
@@ -192,8 +198,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() {

+ 29 - 22
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,20 +11,20 @@ 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 {
   final String tag = 'ConfigRepository';
 
-  static const int configCustomId = 2;
-  static const int configVirtualFriendId = 4;
-  static const int configAddFriendTipId = 5;
-  static const int configAccountLogoutId = 12;
+  static const String keyVirtualFriend = 'virtual_friend';
 
   bool? _isShowVirtualFriend;
 
   final Rxn<bool> isOpenFreeMember = Rxn<bool>();
 
+  List<String>? userAvatarList;
+
   final AtmobApi atmobApi;
 
   final FriendsRepository friendsRepository;
@@ -45,17 +46,11 @@ class ConfigRepository {
         return;
       }
       for (var item in list) {
-        final id = item.id;
-        switch (id) {
-          case configCustomId:
-            break;
-          case configVirtualFriendId:
+        final confCode = item.confCode;
+        switch (confCode) {
+          case keyVirtualFriend:
             _dealWithVirtualFriendSetting(item);
             break;
-          case configAddFriendTipId:
-            break;
-          case configAccountLogoutId:
-            break;
         }
       }
     });
@@ -63,28 +58,23 @@ class ConfigRepository {
 
   Future<ConfigsResponse> requestConfigsData() {
     return atmobApi
-        .getConfigs(ConfigsRequest(ids: [
-          configCustomId,
-          configVirtualFriendId,
-          configAddFriendTipId,
-          configAccountLogoutId
-        ]))
+        .getConfigs(ConfigsRequest([keyVirtualFriend]))
         .then(HttpHandler.handle(true));
   }
 
   void _dealWithVirtualFriendSetting(ConfigBean item) {
-    final cfg = item.cfg;
+    final cfg = item.value;
     if (cfg == null || cfg.isEmpty == true) {
       return;
     }
-    // "virtualFriendEnabled": true, 是否开启虚拟好友
+    // virtualFriendEnabled 是否开启虚拟好友
     if (cfg.containsKey("virtualFriendEnabled")) {
       _isShowVirtualFriend = cfg["virtualFriendEnabled"];
       if (_isShowVirtualFriend == true) {
         friendsRepository.refreshVirtualFriend();
       }
     }
-    //"freeMemberEnabled": false 是否开启免费会员体验
+    //freeMemberEnabled 是否开启免费会员体验
     if (cfg.containsKey("freeMemberEnabled")) {
       isOpenFreeMember.value = cfg["freeMemberEnabled"];
     }
@@ -95,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;
+    });
+  }
 }

+ 21 - 0
lib/dialog/account_replace_dialog.dart

@@ -0,0 +1,21 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+
+class AccountReplaceDialog {
+  static const String _tag = 'AccountReplaceDialog';
+
+  static void show() {
+    SmartDialog.show(builder: (_) => _AccountReplaceView(), tag: _tag);
+  }
+
+  static void dismiss() {
+    SmartDialog.dismiss(tag: _tag);
+  }
+}
+
+class _AccountReplaceView extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Container(child: Text('账号已在其他设备登录,请重新登录'));
+  }
+}

+ 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);
+  }
+}

+ 165 - 129
lib/module/main/main_page.dart

@@ -10,6 +10,7 @@ import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/common_expand.dart';
+import 'package:sliding_sheet2/sliding_sheet2.dart';
 
 import '../../router/app_pages.dart';
 import 'main_friend_item.dart';
@@ -39,111 +40,111 @@ class MainPage extends BasePage<MainController> {
       },
       child: Stack(
         children: [
-          Padding(
-            padding: EdgeInsets.only(bottom: 50.h),
-            child: SizedBox(
-                width: double.infinity,
-                child: MapWidget(
-                  controller: controller.mapController,
-                  onMarkerTap: controller.onMarkerTap,
-                )),
-          ),
-          Align(
-            alignment: Alignment.bottomCenter,
-            child: IntrinsicHeight(
-              child: Column(
-                children: [
-                  Row(
-                    crossAxisAlignment: CrossAxisAlignment.end,
-                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                    children: [
-                      Visibility(
-                        visible: false,
-                        child: Container(
-                            margin: EdgeInsets.only(bottom: 24.w, left: 12.w),
-                            child: Assets.images.iconMainMapClock
-                                .image(width: 50.w)),
-                      ),
-                      Align(
-                        alignment: Alignment.bottomRight,
-                        child: Column(
-                          children: [
-                            GestureDetector(
-                              onTap: controller.onRefreshFriendLocationClick,
-                              child: Container(
-                                  margin: EdgeInsets.only(right: 12.w),
-                                  child: Assets
-                                      .images.iconMainRefreshFriendLocation
-                                      .image(width: 42.w, height: 42.w)),
-                            ),
-                            SizedBox(height: 14.w),
-                            GestureDetector(
-                              onTap: controller.onCurrentLocationClick,
-                              child: Container(
-                                  margin: EdgeInsets.only(right: 12.w),
-                                  child: Assets
-                                      .images.iconMainRefreshMineLocation
-                                      .image(width: 42.w, height: 42.w)),
-                            ),
-                            SizedBox(height: 20.w)
-                          ],
-                        ),
-                      )
-                    ],
-                  ),
-                  Container(
-                    decoration: BoxDecoration(
-                      color: '#F9F9F9'.color,
-                      borderRadius: BorderRadius.only(
-                        topLeft: Radius.circular(20.w),
-                        topRight: Radius.circular(20.w),
-                      ),
-                    ),
-                    child: Column(
-                      children: [
-                        SizedBox(height: 5.w),
-                        Container(
-                          width: 32.w,
-                          height: 3.w,
-                          decoration: BoxDecoration(
-                              color: '#D9D9D9'.color,
-                              borderRadius:
-                                  BorderRadius.all(Radius.circular(49.w))),
-                        ),
-                        SizedBox(height: 12.w),
-                        buildSelectFriendInfoView(),
-                        Obx(() {
-                          return Visibility(
-                              visible: controller.selectedFriend != null,
-                              child: SizedBox(height: 13.w));
-                        }),
-                        buildTabContainer()
-                      ],
-                    ),
-                  )
-                ],
-              ),
-            ),
+          buildMapView(),
+          buildMapFunView(),
+          buildMainBottomView(),
+          buildFriendListView(),
+        ],
+      ),
+    );
+  }
+
+  Widget buildMapView() {
+    return Padding(
+      padding: EdgeInsets.only(bottom: 50.h),
+      child: SizedBox(
+          width: double.infinity,
+          child: MapWidget(
+            controller: controller.mapController,
+            onMarkerTap: controller.onMarkerTap,
+          )),
+    );
+  }
+
+  Widget buildMainBottomView() {
+    return SlidingSheet(
+      color: '#F9F9F9'.color,
+      elevation: 10,
+      shadowColor: Colors.black.withOpacity(0.1),
+      cornerRadius: 18.w,
+      snapSpec: SnapSpec(
+        initialSnap: SnapSpec.headerFooterSnap,
+        // Enable snapping. This is true by default.
+        snap: true,
+        // Set custom snapping points.
+        snappings: [SnapSpec.headerFooterSnap, SnapSpec.expanded],
+        // Define to what the snappings relate to. In this case,
+        // the total available space that the sheet can expand to.
+        positioning: SnapPositioning.relativeToAvailableSpace,
+      ),
+      // headerBuilder: (context, state) {
+      //   return Container(
+      //     height: 56,
+      //     width: double.infinity,
+      //     color: Colors.green,
+      //     alignment: Alignment.center,
+      //     child: Text(
+      //       'This is the header',
+      //     ),
+      //   );
+      // },
+      footerBuilder: buildFooterBuilder,
+      headerBuilder: buildHeaderBuilder,
+      builder: buildTrackEntranceBuilder,
+    );
+  }
+
+  Widget buildTrackEntranceBuilder(BuildContext context, SheetState state) {
+    return Container(
+      height: 200.w,
+      width: 1.sw,
+    );
+  }
+
+  Widget buildFriendListView() {
+    return SafeArea(
+      child: Container(
+        margin: EdgeInsets.only(top: 26.w),
+        child: Row(
+          children: [
+            Expanded(child: buildMainFriendList()),
+            GestureDetector(
+              onTap: () {
+                controller.onAddFriendClick();
+              },
+              child: Container(
+                  margin: EdgeInsets.only(right: 16.w, left: 8.w),
+                  child: Assets.images.iconMainAddFriend
+                      .image(width: 60.w, height: 60.w)),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildMapFunView() {
+    return Positioned(
+      right: 0.w,
+      bottom: 230.w,
+      child: Column(
+        children: [
+          GestureDetector(
+            onTap: controller.onRefreshFriendLocationClick,
+            child: Container(
+                margin: EdgeInsets.only(right: 12.w),
+                child: Assets.images.iconMainRefreshFriendLocation
+                    .image(width: 42.w, height: 42.w)),
           ),
-          SafeArea(
+          SizedBox(height: 14.w),
+          GestureDetector(
+            onTap: controller.onCurrentLocationClick,
             child: Container(
-              margin: EdgeInsets.only(top: 26.w),
-              child: Row(
-                children: [
-                  Expanded(child: buildMainFriendList()),
-                  GestureDetector(
-                    onTap: () {
-                      controller.onAddFriendClick();
-                    },
-                    child: Container(
-                        margin: EdgeInsets.only(right: 16.w, left: 8.w),
-                        child: Assets.images.iconMainAddFriend
-                            .image(width: 60.w, height: 60.w)),
-                  )
-                ],
-              ),
-            ),
-          )
+                margin: EdgeInsets.only(right: 12.w),
+                child: Assets.images.iconMainRefreshMineLocation
+                    .image(width: 42.w, height: 42.w)),
+          ),
+          SizedBox(height: 20.w)
         ],
       ),
     );
@@ -157,6 +158,13 @@ class MainPage extends BasePage<MainController> {
             topLeft: Radius.circular(20.w),
             topRight: Radius.circular(20.w),
           ),
+          boxShadow: [
+            BoxShadow(
+              color: ColorName.black.withOpacity(0.01),
+              blurRadius: 10,
+              offset: const Offset(0, -2), // changes position of shadow
+            ),
+          ],
         ),
         padding: EdgeInsets.only(top: 13.w, bottom: 23.w),
         child: buildMainFunList());
@@ -191,33 +199,35 @@ class MainPage extends BasePage<MainController> {
       {bool? isShowDot}) {
     return GestureDetector(
       onTap: onTap,
-      child: Column(
-        children: [
-          Stack(children: [
-            SizedBox(
-                width: 44.w, height: 44.w, child: Image(image: imgProvider)),
-            Visibility(
-              visible: isShowDot ?? false,
-              child: Positioned(
-                top: 6.w,
-                right: 6.w,
-                child: Container(
-                  width: 10.w,
-                  height: 10.w,
-                  decoration: BoxDecoration(
-                    shape: BoxShape.circle,
-                    color: '#FF333D'.color, // 背景颜色
+      child: IntrinsicHeight(
+        child: Column(
+          children: [
+            Stack(children: [
+              SizedBox(
+                  width: 44.w, height: 44.w, child: Image(image: imgProvider)),
+              Visibility(
+                visible: isShowDot ?? false,
+                child: Positioned(
+                  top: 6.w,
+                  right: 6.w,
+                  child: Container(
+                    width: 10.w,
+                    height: 10.w,
+                    decoration: BoxDecoration(
+                      shape: BoxShape.circle,
+                      color: '#FF333D'.color, // 背景颜色
+                    ),
                   ),
                 ),
-              ),
-            )
-          ]),
-          Text(title,
-              style: TextStyle(
-                  fontSize: 12.sp,
-                  color: ColorName.black70,
-                  fontWeight: FontWeight.bold))
-        ],
+              )
+            ]),
+            Text(title,
+                style: TextStyle(
+                    fontSize: 12.sp,
+                    color: ColorName.black70,
+                    fontWeight: FontWeight.bold))
+          ],
+        ),
       ),
     );
   }
@@ -226,7 +236,9 @@ class MainPage extends BasePage<MainController> {
     return Obx(() {
       UserInfo? userInfo = controller.selectedFriend;
       if (userInfo == null) {
-        return SizedBox.shrink();
+        return SizedBox(
+          height: 86.w,
+        );
       }
       return mainSelectedFriendItem(
           userInfo,
@@ -259,4 +271,28 @@ class MainPage extends BasePage<MainController> {
       }),
     );
   }
+
+  Widget buildFooterBuilder(BuildContext context, SheetState state) {
+    return buildTabContainer();
+  }
+
+  Widget buildHeaderBuilder(BuildContext context, SheetState state) {
+    return IntrinsicHeight(
+      child: Column(
+        children: [
+          SizedBox(height: 5.w),
+          Container(
+            width: 32.w,
+            height: 3.w,
+            decoration: BoxDecoration(
+                color: '#D9D9D9'.color,
+                borderRadius: BorderRadius.all(Radius.circular(49.w))),
+          ),
+          SizedBox(height: 12.w),
+          buildSelectFriendInfoView(),
+          SizedBox(height: 13.w)
+        ],
+      ),
+    );
+  }
 }

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

@@ -6,6 +6,7 @@ import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
 import 'package:location/data/api/response/member_trial_info_response.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';
@@ -19,6 +20,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';
@@ -40,6 +42,8 @@ class MineController extends BaseController {
 
   bool? get isOpenFreeMember => configRepository.isOpenFreeMember.value;
 
+  UserInfo get mineInfo => accountRepository.mineUserInfo.value;
+
   MemberStatusInfo? get memberStatusInfo =>
       accountRepository.memberStatusInfo.value;
 
@@ -108,11 +112,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';
@@ -49,8 +50,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);
                     }),
@@ -439,4 +442,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),
+      ),
+    );
+  }
 }

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

@@ -255,6 +255,7 @@ class StringName {
   static String get dialogNetErrorTitle => 'dialog_net_error_title'.tr; // 网络已断开
   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._();
@@ -513,6 +514,7 @@ class StringMultiSource {
       'dialog_net_error_title': '网络已断开',
       'dialog_net_error_desc': '请检查您的网络连接并重试',
       'dialog_net_error_again': '重试',
+      'mine_update_avatar_success': '设置成功',
     },
   };
 }

+ 5 - 0
lib/socket/atmob_location_client.dart

@@ -15,6 +15,7 @@ import '../data/bean/location_info.dart';
 import '../data/repositories/account_repository.dart';
 import '../data/repositories/contact_repository.dart';
 import '../data/repositories/message_repository.dart';
+import '../dialog/account_replace_dialog.dart';
 import 'location_message.dart';
 
 typedef OnLocationChangeListener = void Function(List<LocationInfo> data);
@@ -133,6 +134,10 @@ class AtmobLocationClient {
             case SocketConstants.refreshMember:
               AccountRepository.getInstance().refreshMemberStatus();
               break;
+            case SocketConstants.refreshUserLogin:
+              AccountReplaceDialog.show();
+              AccountRepository.getInstance().logout();
+              break;
           }
           return SocketConstants.receiveFriendBatchLocation == message.cmd;
         })

+ 1 - 0
lib/socket/socket_constants.dart

@@ -10,6 +10,7 @@ class SocketConstants {
       'd.refresh.friend.message'; // 刷新好友消息
   static const String refreshContact = 'd.refresh.contact'; // 刷新联系人
   static const String refreshMember = 'd.refresh.member'; // 刷新会员信息
+  static const String refreshUserLogin = 'd.refresh.user.login'; // 刷新用户登录
 
   static const String receiveFriendBatchLocation =
       'd.location.batch'; //批量接收好友位置信息

+ 104 - 0
pubspec.lock

@@ -188,6 +188,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "8.10.1"
+  cached_network_image:
+    dependency: "direct main"
+    description:
+      name: cached_network_image
+      sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.4.1"
+  cached_network_image_platform_interface:
+    dependency: transitive
+    description:
+      name: cached_network_image_platform_interface
+      sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.1"
+  cached_network_image_web:
+    dependency: transitive
+    description:
+      name: cached_network_image_web
+      sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.1"
   characters:
     dependency: transitive
     description:
@@ -385,6 +409,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.1.0"
+  flutter_cache_manager:
+    dependency: transitive
+    description:
+      name: flutter_cache_manager
+      sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.4.1"
   flutter_contacts:
     dependency: "direct main"
     description:
@@ -887,6 +919,14 @@ packages:
       url: "http://git.atmob.com:28999/Atmob-Flutter/Oaid.git"
     source: git
     version: "0.0.1"
+  octo_image:
+    dependency: transitive
+    description:
+      name: octo_image
+      sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   package_config:
     dependency: transitive
     description:
@@ -1143,6 +1183,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "9.1.5"
+  rxdart:
+    dependency: transitive
+    description:
+      name: rxdart
+      sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.28.0"
   shelf:
     dependency: transitive
     description:
@@ -1196,6 +1244,54 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.10.1"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  sqflite:
+    dependency: transitive
+    description:
+      name: sqflite
+      sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.2"
+  sqflite_android:
+    dependency: transitive
+    description:
+      name: sqflite_android
+      sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
+  sqflite_common:
+    dependency: transitive
+    description:
+      name: sqflite_common
+      sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.5"
+  sqflite_darwin:
+    dependency: transitive
+    description:
+      name: sqflite_darwin
+      sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.2"
+  sqflite_platform_interface:
+    dependency: transitive
+    description:
+      name: sqflite_platform_interface
+      sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
   stack_trace:
     dependency: transitive
     description:
@@ -1349,6 +1445,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.1.4"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.5.1"
   vector_graphics_codec:
     dependency: transitive
     description:

+ 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