Bläddra i källkod

[new]增加好友列表页面显示&虚拟好友显示

zk 8 månader sedan
förälder
incheckning
540d361eb7

assets/images/bg_check_locatin_permission.webp → assets/images/bg_check_location_permission.webp


BIN
assets/images/bg_friend_item.webp


BIN
assets/images/icon_friend_edit.webp


BIN
assets/images/icon_friend_news.webp


BIN
assets/images/icon_guard.webp


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

@@ -109,5 +109,10 @@
     <string name="dialog_record_location_not_request">暂不开启</string>
     <string name="dialog_record_location_request">立即开启</string>
     <string name="refresh_friend_data_success">好友位置刷新成功</string>
+    <string name="friend_title">我守护的人</string>
+    <string name="trace_free_experience">免费体验</string>
+    <string name="examine_trace">查看轨迹</string>
+    <string name="unopened_positioning">未开启定位</string>
+    <string name="friend_location_time_unknown">未知</string>
 
 </resources>

+ 12 - 0
lib/data/api/atmob_api.dart

@@ -1,15 +1,19 @@
 import 'package:dio/dio.dart';
 import 'package:location/base/app_base_request.dart';
 import 'package:location/base/base_response.dart';
+import 'package:location/data/api/request/configs_request.dart';
 import 'package:location/data/api/request/friends_list_request.dart';
 import 'package:location/data/api/request/login_request.dart';
 import 'package:location/data/api/request/send_code_request.dart';
+import 'package:location/data/api/response/configs_response.dart';
 import 'package:location/data/api/response/friends_list_response.dart';
 import 'package:location/data/api/response/login_response.dart';
 import 'package:location/data/api/response/member_status_response.dart';
 import 'package:retrofit/error_logger.dart';
 import 'package:retrofit/http.dart';
 
+import '../bean/user_info.dart';
+
 part 'atmob_api.g.dart';
 
 @RestApi()
@@ -31,4 +35,12 @@ abstract class AtmobApi {
   @POST("/s/v1/friend/list")
   Future<BaseResponse<FriendsListResponse>> friendList(
       @Body() FriendsListRequest request);
+
+  @POST("/s/v1/client/configs")
+  Future<BaseResponse<ConfigsResponse>> getConfigs(
+      @Body() ConfigsRequest request);
+
+  @POST("/s/v1/friend/virtual")
+  Future<BaseResponse<UserInfo>> getFriendVirtual(
+      @Body() AppBaseRequest request);
 }

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

@@ -175,6 +175,82 @@ class _AtmobApi implements AtmobApi {
     return _value;
   }
 
+  @override
+  Future<BaseResponse<ConfigsResponse>> getConfigs(
+      ConfigsRequest 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<ConfigsResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/client/configs',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<ConfigsResponse> _value;
+    try {
+      _value = BaseResponse<ConfigsResponse>.fromJson(
+        _result.data!,
+        (json) => ConfigsResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<UserInfo>> getFriendVirtual(
+      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<UserInfo>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/friend/virtual',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<UserInfo> _value;
+    try {
+      _value = BaseResponse<UserInfo>.fromJson(
+        _result.data!,
+        (json) => UserInfo.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
   RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
     if (T != dynamic &&
         !(requestOptions.responseType == ResponseType.bytes ||

+ 13 - 0
lib/data/api/request/configs_request.dart

@@ -0,0 +1,13 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'configs_request.g.dart';
+
+@JsonSerializable()
+class ConfigsRequest {
+  @JsonKey(name: "ids")
+  List<int> ids;
+
+  ConfigsRequest({required this.ids});
+
+  Map<String, dynamic> toJson() => _$ConfigsRequestToJson(this);
+}

+ 19 - 0
lib/data/api/request/configs_request.g.dart

@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'configs_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ConfigsRequest _$ConfigsRequestFromJson(Map<String, dynamic> json) =>
+    ConfigsRequest(
+      ids: (json['ids'] as List<dynamic>)
+          .map((e) => (e as num).toInt())
+          .toList(),
+    );
+
+Map<String, dynamic> _$ConfigsRequestToJson(ConfigsRequest instance) =>
+    <String, dynamic>{
+      'ids': instance.ids,
+    };

+ 15 - 0
lib/data/api/response/configs_response.dart

@@ -0,0 +1,15 @@
+import 'package:json_annotation/json_annotation.dart';
+import '../../bean/config_bean.dart';
+
+part 'configs_response.g.dart';
+
+@JsonSerializable()
+class ConfigsResponse {
+  @JsonKey(name: "list")
+  List<ConfigBean>? list;
+
+  ConfigsResponse({this.list});
+
+  factory ConfigsResponse.fromJson(Map<String, dynamic> json) =>
+      _$ConfigsResponseFromJson(json);
+}

+ 17 - 0
lib/data/bean/config_bean.dart

@@ -0,0 +1,17 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'config_bean.g.dart';
+
+@JsonSerializable()
+class ConfigBean {
+  @JsonKey(name: "id")
+  int id;
+
+  @JsonKey(name: "cfg")
+  Map<String, dynamic>? cfg;
+
+  ConfigBean(this.id, this.cfg);
+
+  factory ConfigBean.fromJson(Map<String, dynamic> json) =>
+      _$ConfigBeanFromJson(json);
+}

+ 18 - 0
lib/data/bean/config_bean.g.dart

@@ -0,0 +1,18 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'config_bean.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ConfigBean _$ConfigBeanFromJson(Map<String, dynamic> json) => ConfigBean(
+      (json['id'] as num).toInt(),
+      json['cfg'] as Map<String, dynamic>?,
+    );
+
+Map<String, dynamic> _$ConfigBeanToJson(ConfigBean instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'cfg': instance.cfg,
+    };

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

@@ -0,0 +1,88 @@
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:location/data/api/atmob_api.dart';
+import 'package:location/data/api/request/configs_request.dart';
+import 'package:location/data/bean/config_bean.dart';
+import 'package:location/data/repositories/friends_repository.dart';
+import 'package:location/utils/async_util.dart';
+import 'package:location/utils/atmob_log.dart';
+import 'package:location/utils/http_handler.dart';
+
+import '../api/response/configs_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;
+
+  bool? _isShowVirtualFriend;
+
+  final Rxn<bool> isOpenFreeMember = Rxn<bool>();
+
+  final AtmobApi atmobApi;
+
+  final FriendsRepository friendsRepository;
+
+  ConfigRepository(this.atmobApi, this.friendsRepository) {
+    refreshConfig();
+  }
+
+  void refreshConfig() {
+    AsyncUtil.retry(() => requestConfigsData(), Duration(seconds: 3),
+            maxRetry: 100)
+        .then((configsResponse) {
+      final list = configsResponse.list;
+      if (list == null || list.isEmpty) {
+        return;
+      }
+      for (var item in list) {
+        final id = item.id;
+        switch (id) {
+          case configCustomId:
+            break;
+          case configVirtualFriendId:
+            _dealWithVirtualFriendSetting(item);
+            break;
+          case configAddFriendTipId:
+            break;
+          case configAccountLogoutId:
+            break;
+        }
+      }
+    });
+  }
+
+  Future<ConfigsResponse> requestConfigsData() {
+    return atmobApi
+        .getConfigs(ConfigsRequest(ids: [
+          configCustomId,
+          configVirtualFriendId,
+          configAddFriendTipId,
+          configAccountLogoutId
+        ]))
+        .then(HttpHandler.handle(true));
+  }
+
+  void _dealWithVirtualFriendSetting(ConfigBean item) {
+    final cfg = item.cfg;
+    if (cfg == null || cfg.isEmpty == true) {
+      return;
+    }
+    // "virtualFriendEnabled": true, 是否开启虚拟好友
+    if (cfg.containsKey("virtualFriendEnabled")) {
+      _isShowVirtualFriend = cfg["virtualFriendEnabled"];
+      if (_isShowVirtualFriend == true) {
+        friendsRepository.refreshVirtualFriend();
+      }
+    }
+    friendsRepository.refreshVirtualFriend();
+    //"freeMemberEnabled": false 是否开启免费会员体验
+    if (cfg.containsKey("freeMemberEnabled")) {
+      isOpenFreeMember.value = cfg["freeMemberEnabled"];
+    }
+  }
+}

+ 30 - 7
lib/data/repositories/friends_repository.dart

@@ -1,9 +1,11 @@
 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/friends_list_request.dart';
 import 'package:location/data/bean/location_info.dart';
 import 'package:location/di/get_it.dart';
+import 'package:location/utils/async_util.dart';
 import 'package:location/utils/http_handler.dart';
 import '../../socket/atmob_location_client.dart';
 import '../../utils/atmob_log.dart';
@@ -17,6 +19,10 @@ class FriendsRepository {
   bool refreshFriendsFlag = false;
   final RxList<UserInfo> friendsList = RxList();
 
+  final Rxn<UserInfo> virtualFriendInfo = Rxn<UserInfo>();
+
+  CancelableFuture? refreshVirtualFuture;
+
   FriendsRepository(this.atmobApi) {
     AtmobLog.d(tag, '$tag....init');
 
@@ -52,22 +58,39 @@ class FriendsRepository {
       return;
     }
     refreshFriendsFlag = true;
-    return friendList(0, 300).then((value) {
+    return _friendList(0, 300).then((value) {
       friendsList.value = value.friends;
     }).whenComplete(() {
       refreshFriendsFlag = false;
     });
   }
 
-  Future<FriendsListResponse> friendList(int offset, int limit) {
-    return atmobApi
-        .friendList(FriendsListRequest(offset: offset, limit: limit))
-        .then(HttpHandler.handle(true));
-  }
-
   static FriendsRepository getInstance() {
     return getIt.get<FriendsRepository>();
   }
 
   void refreshFriendRequestList() {}
+
+  void refreshVirtualFriend() {
+    refreshVirtualFuture?.cancel();
+    refreshVirtualFuture = AsyncUtil.retry(
+        () => _requestVirtualFriend(), Duration(seconds: 3),
+        maxRetry: 50);
+  }
+
+  Future<UserInfo> _requestVirtualFriend() {
+    return atmobApi
+        .getFriendVirtual(AppBaseRequest())
+        .then(HttpHandler.handle(true))
+        .then((value) {
+      virtualFriendInfo.value = value;
+      return value;
+    });
+  }
+
+  Future<FriendsListResponse> _friendList(int offset, int limit) {
+    return atmobApi
+        .friendList(FriendsListRequest(offset: offset, limit: limit))
+        .then(HttpHandler.handle(true));
+  }
 }

+ 15 - 4
lib/di/get_it.config.dart

@@ -14,11 +14,13 @@ import 'package:injectable/injectable.dart' as _i526;
 
 import '../data/api/atmob_api.dart' as _i243;
 import '../data/repositories/account_repository.dart' as _i20;
+import '../data/repositories/config_repository.dart' as _i825;
 import '../data/repositories/contact_repository.dart' as _i850;
 import '../data/repositories/friends_repository.dart' as _i1053;
 import '../data/repositories/message_repository.dart' as _i791;
 import '../module/add_friend/add_friend_dialog_controller.dart' as _i897;
 import '../module/browser/browser_controller.dart' as _i923;
+import '../module/friend/friend_controller.dart' as _i821;
 import '../module/login/login_controller.dart' as _i1008;
 import '../module/main/main_controller.dart' as _i731;
 import '../module/mine/mine_controller.dart' as _i732;
@@ -55,15 +57,24 @@ extension GetItInjectableX on _i174.GetIt {
         () => _i791.MessageRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i850.ContactRepository>(
         () => _i850.ContactRepository(gh<_i243.AtmobApi>()));
+    gh.factory<_i1008.LoginController>(
+        () => _i1008.LoginController(gh<_i20.AccountRepository>()));
+    gh.factory<_i732.MineController>(
+        () => _i732.MineController(gh<_i20.AccountRepository>()));
+    gh.lazySingleton<_i825.ConfigRepository>(() => _i825.ConfigRepository(
+          gh<_i243.AtmobApi>(),
+          gh<_i1053.FriendsRepository>(),
+        ));
+    gh.factory<_i821.FriendController>(() => _i821.FriendController(
+          gh<_i20.AccountRepository>(),
+          gh<_i1053.FriendsRepository>(),
+        ));
     gh.factory<_i731.MainController>(() => _i731.MainController(
           gh<_i1053.FriendsRepository>(),
           gh<_i20.AccountRepository>(),
           gh<_i220.AtmobLocationClient>(),
+          gh<_i825.ConfigRepository>(),
         ));
-    gh.factory<_i1008.LoginController>(
-        () => _i1008.LoginController(gh<_i20.AccountRepository>()));
-    gh.factory<_i732.MineController>(
-        () => _i732.MineController(gh<_i20.AccountRepository>()));
     return this;
   }
 }

+ 1 - 1
lib/dialog/check_loation_permission_dialog.dart

@@ -40,7 +40,7 @@ class CheckLocationPermissionView extends Dialog {
       height: 223.w,
       decoration: BoxDecoration(
         image: DecorationImage(
-          image: Assets.images.bgCheckLocatinPermission.provider(),
+          image: Assets.images.bgCheckLocationPermission.provider(),
           fit: BoxFit.fill,
         ),
       ),

+ 61 - 0
lib/module/friend/friend_controller.dart

@@ -0,0 +1,61 @@
+import 'dart:async';
+
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+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/repositories/account_repository.dart';
+import 'package:location/data/repositories/friends_repository.dart';
+
+import '../../data/bean/user_info.dart';
+
+@injectable
+class FriendController extends BaseController {
+  final AccountRepository accountRepository;
+  final FriendsRepository friendsRepository;
+
+  UserInfo get mineUserInfo => accountRepository.mineUserInfo.value;
+
+  UserInfo? get virtualFriendInfo => friendsRepository.virtualFriendInfo.value;
+
+  RxList<UserInfo> get friendsList => friendsRepository.friendsList;
+
+  final ScrollController scrollController = ScrollController();
+
+  final RxDouble _opacity = 0.0.obs;
+
+  double get opacity => _opacity.value;
+
+  final double _scrollThreshold = 80;
+
+  FriendController(this.accountRepository, this.friendsRepository);
+
+  @override
+  void onReady() {
+    super.onReady();
+    scrollController.addListener(_handleScroll);
+  }
+
+  void _handleScroll() {
+    final double offset = scrollController.offset;
+    if (offset <= _scrollThreshold) {
+      _opacity.value = 0.0;
+    } else {
+      double opacity = ((offset - _scrollThreshold) / 200).clamp(0.0, 1.0);
+      _opacity.value = opacity;
+    }
+  }
+
+  void back() {
+    Get.back();
+  }
+
+  void newsClick() {}
+
+  @override
+  void onClose() {
+    super.onClose();
+    scrollController.dispose();
+  }
+}

+ 134 - 0
lib/module/friend/friend_list_item.dart

@@ -0,0 +1,134 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/common_expand.dart';
+
+import '../../data/bean/user_info.dart';
+import '../../resource/assets.gen.dart';
+import '../../resource/colors.gen.dart';
+import '../../utils/date_util.dart';
+
+Widget buildFriendItem(UserInfo userInfo) {
+  return Container(
+    margin: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 10.w),
+    width: double.infinity,
+    decoration: BoxDecoration(
+        color: ColorName.white, borderRadius: BorderRadius.circular(14.w)),
+    child: Stack(
+      children: [
+        Positioned(
+            top: 0,
+            right: 0,
+            child: Assets.images.bgFriendItem.image(width: 190.w)),
+        Column(
+          children: [
+            SizedBox(height: 14.w),
+            Row(
+              children: [
+                SizedBox(width: 12.w),
+                Image.asset(
+                    userInfo.isMine == true
+                        ? Assets.images.iconDefaultMineAvatar.path
+                        : Assets.images.iconDefaultFriendAvatar.path,
+                    width: 48.w,
+                    height: 48.w),
+                SizedBox(width: 8.w),
+                Expanded(
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Row(
+                        crossAxisAlignment: CrossAxisAlignment.center,
+                        children: [
+                          Text((userInfo.getUserNickName()),
+                              style: TextStyle(
+                                  fontSize: 16.sp,
+                                  color: '#202020'.color,
+                                  fontWeight: FontWeight.bold)),
+                          SizedBox(width: 6.w),
+                          Visibility(
+                            visible: userInfo.virtual == true,
+                            child: Container(
+                              padding: EdgeInsets.symmetric(
+                                  horizontal: 5.w, vertical: 2.w),
+                              decoration: BoxDecoration(
+                                  gradient: LinearGradient(colors: [
+                                    '#FF4A4F'.color,
+                                    '#FF554A'.color
+                                  ]),
+                                  borderRadius: BorderRadius.only(
+                                      topLeft: Radius.circular(8.w),
+                                      bottomRight: Radius.circular(10.w),
+                                      topRight: Radius.circular(10.w),
+                                      bottomLeft: Radius.circular(2.w))),
+                              child: Text(StringName.traceFreeExperience,
+                                  style: TextStyle(
+                                    height: 1,
+                                    fontSize: 12.sp,
+                                    color: ColorName.white,
+                                  )),
+                            ),
+                          ),
+                          Spacer(),
+                          Visibility(
+                            visible: userInfo.isMine != true &&
+                                userInfo.virtual != true,
+                            child: Assets.images.iconFriendEdit
+                                .image(width: 22.w, height: 22.w),
+                          ),
+                          SizedBox(width: 16.w),
+                        ],
+                      ),
+                      Text(
+                          userInfo.lastLocation.value?.lastUpdateTime == null
+                              ? StringName.friendLocationTimeUnknown
+                              : DateUtil.fromMillisecondsSinceEpoch(
+                                  "yyyy-MM-dd HH:mm",
+                                  userInfo.lastLocation.value?.lastUpdateTime ??
+                                      0),
+                          style: TextStyle(
+                              fontSize: 13.sp, color: '#A7A7A7'.color)),
+                    ],
+                  ),
+                )
+              ],
+            ),
+            SizedBox(height: 12.w),
+            Row(
+              children: [
+                SizedBox(width: 12.w),
+                Expanded(
+                  child: Container(
+                    constraints: BoxConstraints(minHeight: 34.w),
+                    child: Align(
+                      alignment: Alignment.centerLeft,
+                      child: Text(
+                          userInfo.lastLocation.value?.address ??
+                              StringName.unopenedPositioning,
+                          style: TextStyle(
+                              fontSize: 13.sp, color: '#404040'.color)),
+                    ),
+                  ),
+                ),
+                SizedBox(width: 20.w),
+                Container(
+                  margin: EdgeInsets.only(right: 12.w),
+                  decoration: BoxDecoration(
+                      color: ColorName.colorPrimary,
+                      borderRadius: BorderRadius.circular(36.w)),
+                  padding:
+                      EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.w),
+                  child: Text(
+                    StringName.examineTrace,
+                    style: TextStyle(fontSize: 12.sp, color: ColorName.white),
+                  ),
+                ),
+              ],
+            ),
+            SizedBox(height: 16.w)
+          ],
+        )
+      ],
+    ),
+  );
+}

+ 114 - 0
lib/module/friend/friend_page.dart

@@ -0,0 +1,114 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:location/base/base_page.dart';
+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 '../../router/app_pages.dart';
+import '../../widget/common_view.dart';
+import 'friend_controller.dart';
+import 'friend_list_item.dart';
+
+class FriendPage extends BasePage<FriendController> {
+  const FriendPage({super.key});
+
+  static void start() {
+    Get.toNamed(RoutePath.friend);
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Color backgroundColor() {
+    return '#F7F7F7'.color;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        Assets.images.bgPageBackground.image(width: double.infinity),
+        Positioned(
+            top: 24.w,
+            right: 19.w,
+            child: SafeArea(child: Obx(() {
+              return Opacity(
+                  opacity: 1 - controller.opacity,
+                  child: Assets.images.iconGuard.image(width: 118.w));
+            }))),
+        SafeArea(
+          child: Column(
+            children: [
+              buildHeadView(),
+              Expanded(
+                child: Obx(() {
+                  return ListView(
+                    controller: controller.scrollController,
+                    children: [
+                      SizedBox(height: 18.w),
+                      Padding(
+                        padding: EdgeInsets.only(left: 12.w),
+                        child: Text(StringName.friendTitle,
+                            style: TextStyle(
+                                fontSize: 16.sp,
+                                color: ColorName.black90,
+                                fontWeight: FontWeight.bold)),
+                      ),
+                      SizedBox(height: 10.w),
+                      Obx(() {
+                        return buildFriendItem(controller.mineUserInfo);
+                      }),
+                      Obx(() {
+                        return controller.virtualFriendInfo == null
+                            ? SizedBox.shrink()
+                            : buildFriendItem(controller.virtualFriendInfo!);
+                      }),
+                      ...controller.friendsList.map((e) => Obx(() {
+                            return buildFriendItem(e);
+                          }))
+                    ],
+                  );
+                }),
+              )
+            ],
+          ),
+        )
+      ],
+    );
+  }
+
+  Widget buildHeadView() {
+    return Container(
+      margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w),
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.center,
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          GestureDetector(
+              onTap: controller.back, child: CommonView.getBackBtnView()),
+          Obx(() {
+            return Opacity(
+              opacity: controller.opacity,
+              child: Text(StringName.friendTitle,
+                  style: TextStyle(
+                      fontSize: 16.sp,
+                      color: ColorName.black90,
+                      fontWeight: FontWeight.bold)),
+            );
+          }),
+          GestureDetector(
+              onTap: controller.newsClick,
+              child:
+                  Assets.images.iconFriendNews.image(width: 24.w, height: 24.w))
+        ],
+      ),
+    );
+  }
+}

+ 0 - 1
lib/module/login/login_controller.dart

@@ -3,7 +3,6 @@ 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/consts/error_code.dart';
-import 'package:location/handler/error_handler.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/http_handler.dart';
 import 'package:location/utils/toast_util.dart';

+ 15 - 5
lib/module/main/main_controller.dart

@@ -11,10 +11,12 @@ import 'package:location/data/repositories/account_repository.dart';
 import 'package:location/data/repositories/friends_repository.dart';
 import 'package:flutter_map/flutter_map.dart';
 import 'package:location/handler/error_handler.dart';
+import 'package:location/module/friend/friend_page.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/sdk/map/map_helper.dart';
 import 'package:location/utils/base_expand.dart';
 import 'package:location/utils/toast_util.dart';
+import '../../data/repositories/config_repository.dart';
 import '../../socket/atmob_location_client.dart';
 import '../../dialog/add_friend_dialog.dart';
 import '../../dialog/check_loation_permission_dialog.dart';
@@ -29,12 +31,15 @@ class MainController extends BaseController {
   final FriendsRepository friendsRepository;
   final AccountRepository accountRepository;
 
-  RxList<UserInfo> get friendsList => friendsRepository.friendsList;
+  RxList<UserInfo> get _friendsList => friendsRepository.friendsList;
+
+  RxList<UserInfo> integrateList = RxList();
 
   UserInfo get mineUserInfo => accountRepository.mineUserInfo.value;
 
   MainController(this.friendsRepository, this.accountRepository,
-      AtmobLocationClient atmobLocationClient);
+      AtmobLocationClient atmobLocationClient,
+      ConfigRepository configRepository);
 
   final Rxn<UserInfo> _selectedFriend = Rxn<UserInfo>();
 
@@ -65,8 +70,9 @@ class MainController extends BaseController {
         _selectedFriend.value = null;
       }
     });
-    friendsListSubscription = friendsList.listen((list) {
-      list.insert(0, mineUserInfo);
+    friendsListSubscription = _friendsList.listen((list) {
+      integrateList.assignAll(list);
+      integrateList.insert(0, mineUserInfo);
       mapController.replaceAllMarkers(
           Location2MarkerUtil.userInfoList2MarkerList(list, selectedFriend));
     });
@@ -142,7 +148,7 @@ class MainController extends BaseController {
       return;
     }
     UserInfo? userInfo =
-        friendsList.firstWhereOrNull((element) => element.id == id);
+        integrateList.firstWhereOrNull((element) => element.id == id);
     if (userInfo == null) {
       return;
     }
@@ -205,6 +211,10 @@ class MainController extends BaseController {
     });
   }
 
+  onFriendClick() {
+    FriendPage.start();
+  }
+
   @override
   void onClose() {
     mineLocationSubscription?.cancel();

+ 0 - 39
lib/module/main/main_friend_item.dart

@@ -137,45 +137,6 @@ Widget mainSelectedFriendItem(UserInfo userInfo) {
               ),
             ),
           )
-          // Column(
-          //   children: [
-          //     Row(
-          //       children: [
-          //         Text(
-          //           userInfo.getUserNickName(),
-          //           style: TextStyle(
-          //               fontWeight: FontWeight.bold,
-          //               fontSize: 16.sp,
-          //               color: '#202020'.color),
-          //         ),
-          //         SizedBox(width: 7.w),
-          //         Text('1分钟前',
-          //             style:
-          //                 TextStyle(fontSize: 12.sp, color: '#A7A7A7'.color)),
-          //         // Spacer(),
-          //         Container(
-          //             margin: EdgeInsets.only(right: 17.w),
-          //             decoration: getPrimaryBtnDecoration(32.w),
-          //             padding: EdgeInsets.symmetric(
-          //                 horizontal: 21.w, vertical: 5.w),
-          //             child: Text(StringName.locationTrace,
-          //                 style: TextStyle(
-          //                     fontSize: 15.sp, color: Colors.white)))
-          //       ],
-          //     ),
-          //     SizedBox(height: 10.w),
-          //
-          //     Container(
-          //       width: 250.w,
-          //       height: 50.w,
-          //       child: MarqueeText.marquee(
-          //           text: '广东省广州市天河区XX街街XX街区XX村XXasdasdadadasdadadad',
-          //           containerWidth: 250.w,
-          //           textStyle:
-          //           TextStyle(fontSize: 13.sp, color: ColorName.black50)),
-          //     )
-          //   ],
-          // )
         ],
       ));
 }

+ 3 - 2
lib/module/main/main_page.dart

@@ -161,7 +161,8 @@ class MainPage extends BasePage<MainController> {
       children: [
         Expanded(
             child: buildFunItem(Assets.images.iconMainFriendGuard.provider(),
-                StringName.mainFriendListTab, () {})),
+                StringName.mainFriendListTab,
+                () => controller.onFriendClick())),
         Expanded(
             child: buildFunItem(Assets.images.iconMainNews.provider(),
                 StringName.mainNewsTab, () {},
@@ -231,7 +232,7 @@ class MainPage extends BasePage<MainController> {
           padding: EdgeInsets.only(left: 12.w),
           scrollDirection: Axis.horizontal,
           children: [
-            for (UserInfo userInfo in controller.friendsList)
+            for (UserInfo userInfo in controller.integrateList)
               Obx(() {
                 return mainFriendItem(
                     userInfo, controller.selectedFriend?.id == userInfo.id,

+ 24 - 4
lib/resource/assets.gen.dart

@@ -16,14 +16,18 @@ class $AssetsImagesGen {
   AssetGenImage get bgAddFriendDialog =>
       const AssetGenImage('assets/images/bg_add_friend_dialog.webp');
 
-  /// File path: assets/images/bg_check_locatin_permission.webp
-  AssetGenImage get bgCheckLocatinPermission =>
-      const AssetGenImage('assets/images/bg_check_locatin_permission.webp');
+  /// File path: assets/images/bg_check_location_permission.webp
+  AssetGenImage get bgCheckLocationPermission =>
+      const AssetGenImage('assets/images/bg_check_location_permission.webp');
 
   /// File path: assets/images/bg_dialog_location_permission_ios.webp
   AssetGenImage get bgDialogLocationPermissionIos => const AssetGenImage(
       'assets/images/bg_dialog_location_permission_ios.webp');
 
+  /// File path: assets/images/bg_friend_item.webp
+  AssetGenImage get bgFriendItem =>
+      const AssetGenImage('assets/images/bg_friend_item.webp');
+
   /// File path: assets/images/bg_login_head_container.webp
   AssetGenImage get bgLoginHeadContainer =>
       const AssetGenImage('assets/images/bg_login_head_container.webp');
@@ -72,6 +76,18 @@ class $AssetsImagesGen {
   AssetGenImage get iconExperiment =>
       const AssetGenImage('assets/images/icon_experiment.webp');
 
+  /// File path: assets/images/icon_friend_edit.webp
+  AssetGenImage get iconFriendEdit =>
+      const AssetGenImage('assets/images/icon_friend_edit.webp');
+
+  /// File path: assets/images/icon_friend_news.webp
+  AssetGenImage get iconFriendNews =>
+      const AssetGenImage('assets/images/icon_friend_news.webp');
+
+  /// File path: assets/images/icon_guard.webp
+  AssetGenImage get iconGuard =>
+      const AssetGenImage('assets/images/icon_guard.webp');
+
   /// File path: assets/images/icon_login_address_book.webp
   AssetGenImage get iconLoginAddressBook =>
       const AssetGenImage('assets/images/icon_login_address_book.webp');
@@ -207,8 +223,9 @@ class $AssetsImagesGen {
   /// List of all assets
   List<AssetGenImage> get values => [
         bgAddFriendDialog,
-        bgCheckLocatinPermission,
+        bgCheckLocationPermission,
         bgDialogLocationPermissionIos,
+        bgFriendItem,
         bgLoginHeadContainer,
         bgMineMemberCard,
         bgPageBackground,
@@ -221,6 +238,9 @@ class $AssetsImagesGen {
         iconDialogAddFriend,
         iconDialogClose,
         iconExperiment,
+        iconFriendEdit,
+        iconFriendNews,
+        iconGuard,
         iconLoginAddressBook,
         iconLoginClose,
         iconLoginGoWxArrow,

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

@@ -124,6 +124,12 @@ class StringName {
       'dialog_record_location_request'.tr; // 立即开启
   static final String refreshFriendDataSuccess =
       'refresh_friend_data_success'.tr; // 好友位置刷新成功
+  static final String friendTitle = 'friend_title'.tr; // 我守护的人
+  static final String traceFreeExperience = 'trace_free_experience'.tr; // 免费体验
+  static final String examineTrace = 'examine_trace'.tr; // 查看轨迹
+  static final String unopenedPositioning = 'unopened_positioning'.tr; // 未开启定位
+  static final String friendLocationTimeUnknown =
+      'friend_location_time_unknown'.tr; // 未知
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -218,6 +224,11 @@ class StringMultiSource {
       'dialog_record_location_not_request': '暂不开启',
       'dialog_record_location_request': '立即开启',
       'refresh_friend_data_success': '好友位置刷新成功',
+      'friend_title': '我守护的人',
+      'trace_free_experience': '免费体验',
+      'examine_trace': '查看轨迹',
+      'unopened_positioning': '未开启定位',
+      'friend_location_time_unknown': '未知',
     },
   };
 }

+ 5 - 1
lib/router/app_pages.dart

@@ -2,6 +2,8 @@ import 'package:get/get.dart';
 import 'package:location/di/get_it.dart';
 import 'package:location/module/browser/browser_controller.dart';
 import 'package:location/module/browser/browser_view.dart';
+import 'package:location/module/friend/friend_controller.dart';
+import 'package:location/module/friend/friend_page.dart';
 import 'package:location/module/login/login_controller.dart';
 import 'package:location/module/main/main_page.dart';
 import 'package:location/module/mine/mine_page.dart';
@@ -23,6 +25,7 @@ abstract class RoutePath {
   static const login = '/login';
   static const mine = '/mine';
   static const browser = '/browser';
+  static const friend = '/friend';
 }
 
 class AppBinding extends Bindings {
@@ -33,6 +36,7 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<LoginController>());
     lazyPut(() => getIt.get<MineController>());
     lazyPut(() => getIt.get<BrowserController>());
+    lazyPut(() => getIt.get<FriendController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -45,5 +49,5 @@ final generalPages = [
   GetPage(name: RoutePath.mainTab, page: () => MainPage()),
   GetPage(name: RoutePath.login, page: () => LoginPage()),
   GetPage(name: RoutePath.mine, page: () => MinePage()),
-  GetPage(name: RoutePath.browser, page: () => BrowserPage()),
+  GetPage(name: RoutePath.friend, page: () => FriendPage()),
 ];