Explorar o código

[new]完善好友设置页

zk hai 8 meses
pai
achega
485271ae77

BIN=BIN
assets/images/icon_friend_edit_arrow.webp


BIN=BIN
assets/images/icon_friend_setting_copy.webp


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

@@ -114,5 +114,20 @@
     <string name="examine_trace">查看轨迹</string>
     <string name="unopened_positioning">未开启定位</string>
     <string name="friend_location_time_unknown">未知</string>
-
+    <string name="friend_info_edit">好友编辑</string>
+    <string name="friend_edit_remark_title">备注</string>
+    <string name="friend_edit_phone_title">手机号</string>
+    <string name="friend_edit_block_title">不给Ta看</string>
+    <string name="blocked_friend">已屏蔽该好友</string>
+    <string name="block_friend_off">屏蔽好友已关闭</string>
+    <string name="friend_delete">删除好友</string>
+    <string name="kindly_reminder">温馨提示</string>
+    <string name="friend_delete_content">
+        互删好友后,双方将停止位置的分享,以及清除相关定位记录,是否确认互删?
+    </string>
+    <string name="delete_success">删除成功</string>
+    <string name="copy_phone_num_success">手机号复制成功</string>
+    <string name="friend_update_remark_title">修改备注</string>
+    <string name="friend_update_remark_hint">请输入备注</string>
+    <string name="remark_update_success">备注修改成功</string>
 </resources>

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

@@ -3,6 +3,7 @@ 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/friends_operation_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';
@@ -43,4 +44,15 @@ abstract class AtmobApi {
   @POST("/s/v1/friend/virtual")
   Future<BaseResponse<UserInfo>> getFriendVirtual(
       @Body() AppBaseRequest request);
+
+  @POST("/s/v1/friend/blocked")
+  Future<BaseResponse> updateFriendBlocked(
+      @Body() FriendsOperationRequest request);
+
+  @POST("/s/v1/friend/delete")
+  Future<BaseResponse> deleteFriend(@Body() FriendsOperationRequest request);
+
+  @POST("/s/v1/friend/remark")
+  Future<BaseResponse> updateFriendRemark(
+      @Body() FriendsOperationRequest request);
 }

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

@@ -1,283 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'atmob_api.dart';
-
-// **************************************************************************
-// RetrofitGenerator
-// **************************************************************************
-
-// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations
-
-class _AtmobApi implements AtmobApi {
-  _AtmobApi(
-    this._dio, {
-    this.baseUrl,
-    this.errorLogger,
-  });
-
-  final Dio _dio;
-
-  String? baseUrl;
-
-  final ParseErrorLogger? errorLogger;
-
-  @override
-  Future<BaseResponse<dynamic>> loginSendCode(SendCodeRequest 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/code',
-          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;
-  }
-
-  @override
-  Future<BaseResponse<LoginResponse>> loginUserLogin(
-      LoginRequest 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<LoginResponse>>(Options(
-      method: 'POST',
-      headers: _headers,
-      extra: _extra,
-    )
-        .compose(
-          _dio.options,
-          '/s/v1/user/login',
-          queryParameters: queryParameters,
-          data: _data,
-        )
-        .copyWith(
-            baseUrl: _combineBaseUrls(
-          _dio.options.baseUrl,
-          baseUrl,
-        )));
-    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
-    late BaseResponse<LoginResponse> _value;
-    try {
-      _value = BaseResponse<LoginResponse>.fromJson(
-        _result.data!,
-        (json) => LoginResponse.fromJson(json as Map<String, dynamic>),
-      );
-    } on Object catch (e, s) {
-      errorLogger?.logError(e, s, _options);
-      rethrow;
-    }
-    return _value;
-  }
-
-  @override
-  Future<BaseResponse<MemberStatusResponse?>> getMemberStatus(
-      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<MemberStatusResponse?>>(Options(
-      method: 'POST',
-      headers: _headers,
-      extra: _extra,
-    )
-            .compose(
-              _dio.options,
-              '/s/v1/user/member',
-              queryParameters: queryParameters,
-              data: _data,
-            )
-            .copyWith(
-                baseUrl: _combineBaseUrls(
-              _dio.options.baseUrl,
-              baseUrl,
-            )));
-    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
-    late BaseResponse<MemberStatusResponse?> _value;
-    try {
-      _value = BaseResponse<MemberStatusResponse?>.fromJson(
-        _result.data!,
-        (json) => json == null
-            ? null
-            : MemberStatusResponse.fromJson(json as Map<String, dynamic>),
-      );
-    } on Object catch (e, s) {
-      errorLogger?.logError(e, s, _options);
-      rethrow;
-    }
-    return _value;
-  }
-
-  @override
-  Future<BaseResponse<FriendsListResponse>> friendList(
-      FriendsListRequest 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<FriendsListResponse>>(Options(
-      method: 'POST',
-      headers: _headers,
-      extra: _extra,
-    )
-        .compose(
-          _dio.options,
-          '/s/v1/friend/list',
-          queryParameters: queryParameters,
-          data: _data,
-        )
-        .copyWith(
-            baseUrl: _combineBaseUrls(
-          _dio.options.baseUrl,
-          baseUrl,
-        )));
-    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
-    late BaseResponse<FriendsListResponse> _value;
-    try {
-      _value = BaseResponse<FriendsListResponse>.fromJson(
-        _result.data!,
-        (json) => FriendsListResponse.fromJson(json as Map<String, dynamic>),
-      );
-    } on Object catch (e, s) {
-      errorLogger?.logError(e, s, _options);
-      rethrow;
-    }
-    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 ||
-            requestOptions.responseType == ResponseType.stream)) {
-      if (T == String) {
-        requestOptions.responseType = ResponseType.plain;
-      } else {
-        requestOptions.responseType = ResponseType.json;
-      }
-    }
-    return requestOptions;
-  }
-
-  String _combineBaseUrls(
-    String dioBaseUrl,
-    String? baseUrl,
-  ) {
-    if (baseUrl == null || baseUrl.trim().isEmpty) {
-      return dioBaseUrl;
-    }
-
-    final url = Uri.parse(baseUrl);
-
-    if (url.isAbsolute) {
-      return url.toString();
-    }
-
-    return Uri.parse(dioBaseUrl).resolveUri(url).toString();
-  }
-}

+ 2 - 1
lib/data/api/request/configs_request.dart

@@ -1,9 +1,10 @@
 import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
 
 part 'configs_request.g.dart';
 
 @JsonSerializable()
-class ConfigsRequest {
+class ConfigsRequest extends AppBaseRequest {
   @JsonKey(name: "ids")
   List<int> ids;
 

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

@@ -1,19 +0,0 @@
-// 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,
-    };

+ 24 - 0
lib/data/api/request/friends_operation_request.dart

@@ -0,0 +1,24 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
+
+part 'friends_operation_request.g.dart';
+
+@JsonSerializable()
+class FriendsOperationRequest extends AppBaseRequest {
+  @JsonKey(name: "friendId")
+  final String friendId;
+
+  @JsonKey(name: "remark")
+  String? remark;
+
+  @JsonKey(name: "blocked")
+  bool? blocked;
+
+  FriendsOperationRequest({required this.friendId, this.blocked, this.remark});
+
+  factory FriendsOperationRequest.fromJson(Map<String, dynamic> json) =>
+      _$FriendsOperationRequestFromJson(json);
+
+  @override
+  Map<String, dynamic> toJson() => _$FriendsOperationRequestToJson(this);
+}

+ 0 - 1
lib/data/api/request/login_request.dart

@@ -1,6 +1,5 @@
 import 'package:json_annotation/json_annotation.dart';
 import 'package:location/base/app_base_request.dart';
-import 'package:location/base/base_request.dart';
 
 part 'login_request.g.dart';
 

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

@@ -20,7 +20,7 @@ class UserInfo {
   final int? timestamp;
 
   @JsonKey(name: 'blockedHim')
-  final bool? blockedHim;
+  bool? blockedHim;
 
   @JsonKey(name: 'blockedMe')
   final bool? blockedMe;
@@ -55,7 +55,7 @@ class UserInfo {
   }
 
   String getUserNickName() {
-    return remark ?? phoneNumber;
+    return (remark?.isNotEmpty == true ? remark : phoneNumber) ?? '';
   }
 
   void updateLocation(LocationInfo location) {

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

@@ -79,7 +79,6 @@ class ConfigRepository {
         friendsRepository.refreshVirtualFriend();
       }
     }
-    friendsRepository.refreshVirtualFriend();
     //"freeMemberEnabled": false 是否开启免费会员体验
     if (cfg.containsKey("freeMemberEnabled")) {
       isOpenFreeMember.value = cfg["freeMemberEnabled"];

+ 22 - 0
lib/data/repositories/friends_repository.dart

@@ -9,6 +9,7 @@ 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';
+import '../api/request/friends_operation_request.dart';
 import '../api/response/friends_list_response.dart';
 import '../bean/user_info.dart';
 
@@ -88,6 +89,27 @@ class FriendsRepository {
     });
   }
 
+  Future<void> updateFriendBlocked(
+      {required String friendId, required bool blocked}) {
+    return atmobApi
+        .updateFriendBlocked(
+            FriendsOperationRequest(friendId: friendId, blocked: blocked))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<void> deleteFriend(String friendId) {
+    return atmobApi
+        .deleteFriend(FriendsOperationRequest(friendId: friendId))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<void> updateFriendRemark(String friendId, String remark) {
+    return atmobApi
+        .updateFriendRemark(
+            FriendsOperationRequest(friendId: friendId, remark: remark))
+        .then(HttpHandler.handle(true));
+  }
+
   Future<FriendsListResponse> _friendList(int offset, int limit) {
     return atmobApi
         .friendList(FriendsListRequest(offset: offset, limit: limit))

+ 5 - 2
lib/di/get_it.config.dart

@@ -21,6 +21,7 @@ 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/friend/setting/friend_setting_controller.dart' as _i492;
 import '../module/login/login_controller.dart' as _i1008;
 import '../module/main/main_controller.dart' as _i731;
 import '../module/mine/mine_controller.dart' as _i732;
@@ -51,12 +52,12 @@ extension GetItInjectableX on _i174.GetIt {
         () => networkModule.provideAtmobApi(gh<_i361.Dio>()));
     gh.lazySingleton<_i20.AccountRepository>(
         () => _i20.AccountRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i850.ContactRepository>(
+        () => _i850.ContactRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i1053.FriendsRepository>(
         () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i791.MessageRepository>(
         () => _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>(
@@ -75,6 +76,8 @@ extension GetItInjectableX on _i174.GetIt {
           gh<_i220.AtmobLocationClient>(),
           gh<_i825.ConfigRepository>(),
         ));
+    gh.factory<_i492.FriendSettingController>(
+        () => _i492.FriendSettingController(gh<_i1053.FriendsRepository>()));
     return this;
   }
 }

+ 1 - 1
lib/dialog/common_alert_dialog.dart

@@ -28,7 +28,7 @@ class CommonAlertDialog {
         });
   }
 
-  static void dismiss() {
+  static void dismiss({String tag = _tag}) {
     SmartDialog.dismiss(tag: _tag);
   }
 }

+ 30 - 2
lib/dialog/common_alert_dialog_impl.dart

@@ -6,6 +6,7 @@ import 'package:location/utils/common_expand.dart';
 import 'common_alert_dialog.dart';
 
 void exitAccountDialog({required VoidCallback confirmOnTap}) {
+  final tag = 'exitAccountDialog';
   CommonAlertDialog.show(
       titleWidget: Text(
         StringName.dialogExitAccountTitle,
@@ -21,10 +22,37 @@ void exitAccountDialog({required VoidCallback confirmOnTap}) {
       cancelText: StringName.dialogCancel,
       confirmText: StringName.dialogSure,
       cancelOnTap: () {
-        CommonAlertDialog.dismiss();
+        CommonAlertDialog.dismiss(tag: tag);
       },
       confirmOnTap: () {
         confirmOnTap();
-        CommonAlertDialog.dismiss();
+        CommonAlertDialog.dismiss(tag: tag);
+      },
+      tag: tag);
+}
+
+void deleteFriendDialog({required VoidCallback confirmOnTap}) {
+  final tag = 'deleteFriendDialog';
+  CommonAlertDialog.show(
+      tag: tag,
+      titleWidget: Text(
+        StringName.kindlyReminder,
+        style: TextStyle(
+            fontSize: 17.sp,
+            color: '#333333'.color,
+            fontWeight: FontWeight.bold),
+      ),
+      descWidget: Text(
+        StringName.friendDeleteContent,
+        style: TextStyle(fontSize: 15.sp, color: '#404040'.color),
+      ),
+      cancelText: StringName.dialogCancel,
+      confirmText: StringName.dialogSure,
+      cancelOnTap: () {
+        CommonAlertDialog.dismiss(tag: tag);
+      },
+      confirmOnTap: () {
+        confirmOnTap();
+        CommonAlertDialog.dismiss(tag: tag);
       });
 }

+ 154 - 0
lib/dialog/friend_update_remark_dialog.dart

@@ -0,0 +1,154 @@
+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/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:location/utils/common_style.dart';
+import 'package:location/utils/toast_util.dart';
+
+typedef FriendUpdateRemarkCallback = void Function(String remark);
+
+class FriendUpdateRemarkDialog {
+  static const String _tag = "FriendUpdateRemarkDialog";
+
+  static void show(
+      {String? remark, required FriendUpdateRemarkCallback callback}) {
+    SmartDialog.show(
+        builder: (_) {
+          return FriendUpdateRemarkView(
+            remark: remark,
+            callback: callback,
+          );
+        },
+        tag: _tag);
+  }
+
+  static void dismiss() {
+    SmartDialog.dismiss(tag: _tag);
+  }
+}
+
+class FriendUpdateRemarkView extends Dialog {
+  final FriendUpdateRemarkCallback callback;
+  final String? remark;
+
+  final Rx<String> _remark = Rx<String>('');
+  final TextEditingController editingController = TextEditingController();
+
+  FriendUpdateRemarkView({super.key, required this.callback, this.remark}) {
+    editingController.addListener(() {
+      _remark.value = editingController.text;
+    });
+    editingController.text = remark ?? '';
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return IntrinsicHeight(
+      child: Container(
+        width: 307.w,
+        decoration: BoxDecoration(
+          color: Colors.white,
+          borderRadius: BorderRadius.circular(20.w),
+        ),
+        child: Stack(
+          children: [
+            Positioned(
+                top: 12.w,
+                right: 12.w,
+                child: GestureDetector(
+                    onTap: () {
+                      FriendUpdateRemarkDialog.dismiss();
+                    },
+                    child: Assets.images.iconDialogClose
+                        .image(width: 24.w, height: 24.w))),
+            Column(
+              children: [
+                SizedBox(height: 27.w),
+                Text(StringName.friendUpdateRemarkTitle,
+                    style: TextStyle(
+                        fontSize: 18.sp,
+                        color: ColorName.black90,
+                        fontWeight: FontWeight.bold)),
+                SizedBox(height: 20.w),
+                Container(
+                  padding: EdgeInsets.symmetric(horizontal: 12.w),
+                  decoration: BoxDecoration(
+                      color: '#F9F9F9'.color,
+                      borderRadius: BorderRadius.circular(10.w)),
+                  margin: EdgeInsets.symmetric(horizontal: 22.w),
+                  child: Row(
+                    children: [
+                      Expanded(
+                          child: TextField(
+                              controller: editingController,
+                              style: TextStyle(
+                                  fontSize: 14.sp,
+                                  color: ColorName.primaryTextColor),
+                              maxLines: 1,
+                              maxLength: 11,
+                              keyboardType: TextInputType.text,
+                              textAlignVertical: TextAlignVertical.center,
+                              textInputAction: TextInputAction.next,
+                              decoration: InputDecoration(
+                                  hintText: StringName.friendUpdateRemarkHint,
+                                  counterText: '',
+                                  hintStyle: TextStyle(
+                                      fontSize: 14, color: "#AAAAAA".toColor()),
+                                  labelStyle: const TextStyle(
+                                    fontSize: 14,
+                                    color: ColorName.primaryTextColor,
+                                  ),
+                                  contentPadding: const EdgeInsets.all(0),
+                                  border: const OutlineInputBorder(
+                                      borderSide: BorderSide.none)))),
+                      Obx(() {
+                        return Text(
+                            _remark.value.isEmpty
+                                ? '0'
+                                : _remark.value.length.toString(),
+                            style: TextStyle(
+                                fontSize: 14.sp,
+                                color: _remark.value.isNotEmpty
+                                    ? ColorName.black90
+                                    : '#AAAAAA'.color));
+                      }),
+                      Text('/',
+                          style: TextStyle(
+                              fontSize: 14.sp, color: '#AAAAAA'.color)),
+                      Text('11',
+                          style: TextStyle(
+                              fontSize: 14.sp, color: '#AAAAAA'.color))
+                    ],
+                  ),
+                ),
+                SizedBox(height: 20.w),
+                GestureDetector(
+                  onTap: () {
+                    callback(_remark.value);
+                  },
+                  child: Container(
+                    width: 229.w,
+                    height: 43.w,
+                    decoration: getPrimaryBtnDecoration(46.w),
+                    child: Center(
+                      child: Text(StringName.dialogSure,
+                          style: TextStyle(
+                              fontSize: 14.sp,
+                              color: Colors.white,
+                              fontWeight: FontWeight.bold)),
+                    ),
+                  ),
+                ),
+                SizedBox(height: 20.w)
+              ],
+            )
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 0 - 1
lib/dialog/location_permission_dialog.dart

@@ -5,7 +5,6 @@ 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/assets.gen.dart';
-import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/common_expand.dart';
 

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

@@ -7,6 +7,7 @@ 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 'package:location/module/friend/setting/friend_setting_page.dart';
 
 import '../../data/bean/user_info.dart';
 
@@ -58,4 +59,8 @@ class FriendController extends BaseController {
     super.onClose();
     scrollController.dispose();
   }
+
+  void onEditClick(UserInfo e) {
+    FriendSettingPage.start(e);
+  }
 }

+ 8 - 3
lib/module/friend/friend_list_item.dart

@@ -8,7 +8,7 @@ import '../../resource/assets.gen.dart';
 import '../../resource/colors.gen.dart';
 import '../../utils/date_util.dart';
 
-Widget buildFriendItem(UserInfo userInfo) {
+Widget buildFriendItem(UserInfo userInfo, {VoidCallback? onEditClick}) {
   return Container(
     margin: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 10.w),
     width: double.infinity,
@@ -73,8 +73,13 @@ Widget buildFriendItem(UserInfo userInfo) {
                           Visibility(
                             visible: userInfo.isMine != true &&
                                 userInfo.virtual != true,
-                            child: Assets.images.iconFriendEdit
-                                .image(width: 22.w, height: 22.w),
+                            child: GestureDetector(
+                              onTap: () {
+                                onEditClick?.call();
+                              },
+                              child: Assets.images.iconFriendEdit
+                                  .image(width: 22.w, height: 22.w),
+                            ),
                           ),
                           SizedBox(width: 16.w),
                         ],

+ 2 - 1
lib/module/friend/friend_page.dart

@@ -71,7 +71,8 @@ class FriendPage extends BasePage<FriendController> {
                             : buildFriendItem(controller.virtualFriendInfo!);
                       }),
                       ...controller.friendsList.map((e) => Obx(() {
-                            return buildFriendItem(e);
+                            return buildFriendItem(e,
+                                onEditClick: () => controller.onEditClick(e));
                           }))
                     ],
                   );

+ 98 - 0
lib/module/friend/setting/friend_setting_controller.dart

@@ -0,0 +1,98 @@
+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/bean/user_info.dart';
+import 'package:location/data/repositories/friends_repository.dart';
+import 'package:location/handler/error_handler.dart';
+import 'package:location/resource/string.gen.dart';
+
+import '../../../dialog/common_alert_dialog_impl.dart';
+import '../../../dialog/friend_update_remark_dialog.dart';
+import '../../../utils/common_util.dart';
+import '../../../utils/toast_util.dart';
+
+@injectable
+class FriendSettingController extends BaseController {
+  final Rxn<UserInfo> _userInfo = Rxn<UserInfo>();
+
+  UserInfo? get userInfo => _userInfo.value;
+
+  final FriendsRepository friendsRepository;
+
+  FriendSettingController(this.friendsRepository);
+
+  @override
+  void onInit() {
+    final param = Get.arguments;
+    if (param is UserInfo) {
+      _userInfo.value = param;
+    }
+  }
+
+  void back() {
+    Get.back();
+  }
+
+  void editRemarkClick() {
+    FriendUpdateRemarkDialog.show(
+        remark: userInfo?.remark,
+        callback: (remark) {
+          _updateRemark(remark);
+        });
+  }
+
+  void _updateRemark(String remark) {
+    friendsRepository.updateFriendRemark(userInfo?.id ?? '', remark).then((_) {
+      ToastUtil.show(StringName.remarkUpdateSuccess);
+      FriendUpdateRemarkDialog.dismiss();
+      userInfo?.remark = remark;
+      _userInfo.refresh();
+    }).catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+
+  void copyPhoneClick() {
+    copyToClipboard(userInfo?.phoneNumber ?? '');
+    ToastUtil.show(StringName.copyPhoneNumSuccess);
+  }
+
+  Future<bool> blockHimFuture(bool isChecked) async {
+    if (userInfo == null) {
+      return false;
+    }
+    bool isBlocked = userInfo?.blockedHim ?? false;
+    try {
+      await Future.delayed(Duration(seconds: 3));
+      await friendsRepository.updateFriendBlocked(
+          friendId: userInfo?.id ?? '', blocked: !isBlocked);
+      if (!isBlocked) {
+        ToastUtil.show(StringName.blockedFriend);
+      } else {
+        ToastUtil.show(StringName.blockFriendOff);
+      }
+      userInfo?.blockedHim = !isBlocked;
+      return !isBlocked;
+    } catch (e) {
+      ErrorHandler.toastError(e);
+      return isBlocked;
+    }
+  }
+
+  void deleteFriendClick() {
+    deleteFriendDialog(confirmOnTap: () {
+      _deleteFriendFuture();
+    });
+  }
+
+  void _deleteFriendFuture() {
+    final friendId = userInfo?.id ?? '';
+    friendsRepository.deleteFriend(friendId).then((data) {
+      ToastUtil.show(StringName.deleteSuccess);
+      Get.back();
+    }).catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+}

+ 182 - 0
lib/module/friend/setting/friend_setting_page.dart

@@ -0,0 +1,182 @@
+import 'package:animated_toggle_switch/animated_toggle_switch.dart';
+import 'package:flutter/cupertino.dart';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:location/base/base_page.dart';
+import 'package:location/data/bean/user_info.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/router/app_pages.dart';
+import 'package:location/utils/common_expand.dart';
+import 'package:location/widget/load_switch.dart';
+
+import '../../../resource/colors.gen.dart';
+import '../../../resource/string.gen.dart';
+import '../../../widget/common_view.dart';
+import 'friend_setting_controller.dart';
+
+class FriendSettingPage extends BasePage<FriendSettingController> {
+  const FriendSettingPage({super.key});
+
+  static void start(UserInfo userInfo) {
+    Get.toNamed(RoutePath.friendSetting, arguments: userInfo);
+  }
+
+  @override
+  Color backgroundColor() {
+    return '#F7F7F7'.color;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Column(
+      children: [
+        buildHeadView(),
+        Expanded(
+            child: Padding(
+          padding: EdgeInsets.symmetric(horizontal: 12.w),
+          child: Column(
+            children: [
+              SizedBox(height: 15.w),
+              Container(
+                padding: EdgeInsets.symmetric(horizontal: 12.w),
+                width: double.infinity,
+                decoration: _getSettingCardDecoration(),
+                child: Column(
+                  children: [
+                    Obx(() {
+                      return _buildInfoEditType(
+                          StringName.friendEditRemarkTitle,
+                          controller.userInfo?.remark ?? '',
+                          Assets.images.iconFriendEditArrow.provider(),
+                          onTap: controller.editRemarkClick);
+                    }),
+                    Container(
+                        width: double.infinity,
+                        height: 1.w,
+                        color: '#F8F8F8'.color),
+                    _buildInfoEditType(
+                        StringName.friendEditPhoneTitle,
+                        controller.userInfo?.phoneNumber ?? '',
+                        Assets.images.iconFriendSettingCopy.provider(),
+                        onTap: controller.copyPhoneClick),
+                  ],
+                ),
+              ),
+              SizedBox(height: 10.w),
+              Obx(() {
+                return buildInfoSwitchType(StringName.friendEditBlockTitle,
+                    controller.userInfo?.blockedHim ?? false,
+                    future: (value) => controller.blockHimFuture(value));
+              }),
+              SizedBox(height: 16.w),
+              GestureDetector(
+                onTap: controller.deleteFriendClick,
+                child: Container(
+                    height: 54.w,
+                    decoration: _getSettingCardDecoration(),
+                    child: Center(
+                        child: Text(StringName.friendDelete,
+                            style: TextStyle(
+                                fontSize: 15.sp,
+                                color: '#E1261A'.color,
+                                fontWeight: FontWeight.bold)))),
+              )
+            ],
+          ),
+        ))
+      ],
+    );
+  }
+
+  Widget buildHeadView() {
+    return Container(
+      width: double.infinity,
+      padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w),
+      child: Stack(
+        children: [
+          GestureDetector(
+              onTap: controller.back, child: CommonView.getBackBtnView()),
+          Positioned(
+            left: 0,
+            right: 0,
+            top: 0,
+            bottom: 0,
+            child: Center(
+              child: Text(StringName.friendInfoEdit,
+                  style: TextStyle(
+                      height: 1,
+                      fontSize: 16.sp,
+                      color: ColorName.black90,
+                      fontWeight: FontWeight.bold)),
+            ),
+          )
+        ],
+      ),
+    );
+  }
+
+  BoxDecoration _getSettingCardDecoration() {
+    return BoxDecoration(
+        color: ColorName.white, borderRadius: BorderRadius.circular(8.w));
+  }
+
+  Widget _buildInfoEditType(
+      String title, String content, ImageProvider imageProvider,
+      {VoidCallback? onTap}) {
+    return GestureDetector(
+      behavior: HitTestBehavior.translucent,
+      onTap: onTap,
+      child: SizedBox(
+        height: 50.w,
+        child: Row(
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: [
+            Text(
+              title,
+              style: TextStyle(
+                  fontSize: 15.sp,
+                  color: '#202020'.color,
+                  fontWeight: FontWeight.bold),
+            ),
+            Spacer(),
+            Text(content,
+                style: TextStyle(fontSize: 14.sp, color: '#666666'.color)),
+            SizedBox(width: 5.w),
+            Image(image: imageProvider, width: 20.w, height: 20.w)
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildInfoSwitchType(String title, bool isSwitchOn,
+      {required LoadFutureCallback future}) {
+    return Container(
+      height: 54.w,
+      padding: EdgeInsets.symmetric(horizontal: 12.w),
+      decoration: _getSettingCardDecoration(),
+      child: Row(children: [
+        Text(
+          title,
+          style: TextStyle(
+              fontSize: 15.sp,
+              color: '#202020'.color,
+              fontWeight: FontWeight.bold),
+        ),
+        Spacer(),
+        LoadSwitch(
+            value: isSwitchOn,
+            future: future,
+            height: 24.w,
+            borderWidth: 4.w,
+            loadingColor: ColorName.colorPrimary,
+            selectedColor: ColorName.colorPrimary,
+            unselectedColor: '#EBEBEB'.color)
+      ]),
+    );
+  }
+}

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

@@ -80,10 +80,18 @@ class $AssetsImagesGen {
   AssetGenImage get iconFriendEdit =>
       const AssetGenImage('assets/images/icon_friend_edit.webp');
 
+  /// File path: assets/images/icon_friend_edit_arrow.webp
+  AssetGenImage get iconFriendEditArrow =>
+      const AssetGenImage('assets/images/icon_friend_edit_arrow.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_friend_setting_copy.webp
+  AssetGenImage get iconFriendSettingCopy =>
+      const AssetGenImage('assets/images/icon_friend_setting_copy.webp');
+
   /// File path: assets/images/icon_guard.webp
   AssetGenImage get iconGuard =>
       const AssetGenImage('assets/images/icon_guard.webp');
@@ -239,7 +247,9 @@ class $AssetsImagesGen {
         iconDialogClose,
         iconExperiment,
         iconFriendEdit,
+        iconFriendEditArrow,
         iconFriendNews,
+        iconFriendSettingCopy,
         iconGuard,
         iconLoginAddressBook,
         iconLoginClose,

+ 62 - 68
lib/resource/string.gen.dart

@@ -16,10 +16,8 @@ class StringName {
   static final String termOfService = 'term_of_service'.tr; // 《服务条款》
   static final String friendAddTitle = 'friend_add_title'.tr; // 添加好友
   static final String friendAddDesc = 'friend_add_desc'.tr; // 查看实时定位,开启轨迹守护
-  static final String friendAddPhoneEtHint =
-      'friend_add_phone_et_hint'.tr; // 请输入手机号
-  static final String friendAddAddressBook =
-      'friend_add_address_book'.tr; // 通讯录
+  static final String friendAddPhoneEtHint = 'friend_add_phone_et_hint'.tr; // 请输入手机号
+  static final String friendAddAddressBook = 'friend_add_address_book'.tr; // 通讯录
   static final String friendAddFromWx = 'friend_add_from_wx'.tr; // 通过微信添加
   static final String friendAddRule = 'friend_add_rule'.tr; // 定位功能需要双方同意方可使用
   static final String mainFriendListTab = 'main_friend_list_tab'.tr; // 好友守护
@@ -27,8 +25,7 @@ class StringName {
   static final String mainHelpTab = 'main_help_tab'.tr; // 一键求助
   static final String mainMineTab = 'main_mine_tab'.tr; // 个人中心
   static final String mineAccountGoLogin = 'mine_account_go_login'.tr; // 点击登录
-  static final String mineAccountLoggedDesc =
-      'mine_account_logged_desc'.tr; // 用户
+  static final String mineAccountLoggedDesc = 'mine_account_logged_desc'.tr; // 用户
   static final String mineNotLoginDesc = 'mine_not_login_desc'.tr; // 登录后可体验更多服务
   static final String mineOpenVip = 'mine_open_vip'.tr; // 开通VIP可体验更多服务
   static final String mineVip = 'mine_vip'.tr; // 您好,尊贵的VIP用户
@@ -42,94 +39,77 @@ class StringName {
   static final String memberLevel3660000 = 'member_level_3660000'.tr; // 终身会员
   static final String memberLevelUndefined = 'member_level_undefined'.tr; // 未知
   static final String memberTryOut = 'member_try_out'.tr; // 会员试用
-  static final String memberCardNoLoginDesc =
-      'member_card_no_login_desc'.tr; // 升级VIP会员,享受更多权益
-  static final String memberCardNoVipDesc =
-      'member_card_no_vip_desc'.tr; // 开通VIP会员,享受更多权益
-  static final String memberCardExpirationDesc =
-      'member_card_expiration_desc'.tr; // 到期
-  static final String memberCardPermanentVipDesc =
-      'member_card_permanent_vip_desc'.tr; // 您已是尊贵的永久会员
+  static final String memberCardNoLoginDesc = 'member_card_no_login_desc'.tr; // 升级VIP会员,享受更多权益
+  static final String memberCardNoVipDesc = 'member_card_no_vip_desc'.tr; // 开通VIP会员,享受更多权益
+  static final String memberCardExpirationDesc = 'member_card_expiration_desc'.tr; // 到期
+  static final String memberCardPermanentVipDesc = 'member_card_permanent_vip_desc'.tr; // 您已是尊贵的永久会员
   static final String memberVipUnlock = 'member_vip_unlock'.tr; // 立即解锁
   static final String memberVipRenew = 'member_vip_renew'.tr; // 立即续费
   static final String memberVipPermanent = 'member_vip_permanent'.tr; // 永久会员
-  static final String memberExperienceVip =
-      'member_experience_vip'.tr; // 免费VIP体验礼包
-  static final String memberExperienceVipReceive =
-      'member_experience_vip_receive'.tr; // 去领取
+  static final String memberExperienceVip = 'member_experience_vip'.tr; // 免费VIP体验礼包
+  static final String memberExperienceVipReceive = 'member_experience_vip_receive'.tr; // 去领取
   static final String mineFunShare = 'mine_fun_share'.tr; // 邀请好友
-  static final String mineFunCustomerService =
-      'mine_fun_customer_service'.tr; // 专属客服
-  static final String mineFunPermissionSetting =
-      'mine_fun_permission_setting'.tr; // 权限设置
-  static final String mineFunAccountFeedback =
-      'mine_fun_account_feedback'.tr; // 用户反馈
+  static final String mineFunCustomerService = 'mine_fun_customer_service'.tr; // 专属客服
+  static final String mineFunPermissionSetting = 'mine_fun_permission_setting'.tr; // 权限设置
+  static final String mineFunAccountFeedback = 'mine_fun_account_feedback'.tr; // 用户反馈
   static final String mineFunAbout = 'mine_fun_about'.tr; // 关于我们
-  static final String mineFunLogoutAccount =
-      'mine_fun_logout_account'.tr; // 注销账号
+  static final String mineFunLogoutAccount = 'mine_fun_logout_account'.tr; // 注销账号
   static final String mineFunExitAccount = 'mine_fun_exit_account'.tr; // 退出账号
   static final String login = 'login'.tr; // 登录
-  static final String loginSendVerificationCode =
-      'login_send_verification_code'.tr; // 发送验证码
-  static final String loginRetransmissionCode =
-      'login_retransmission_code'.tr; // s后重发
+  static final String loginSendVerificationCode = 'login_send_verification_code'.tr; // 发送验证码
+  static final String loginRetransmissionCode = 'login_retransmission_code'.tr; // s后重发
   static final String loginEtPhoneHint = 'login_et_phone_hint'.tr; // 请输入11位手机号码
   static final String loginEtPrivacyRead = 'login_et_privacy_read'.tr; // 已阅读并同意
   static final String loginEtPrivacyAnd = 'login_et_privacy_and'.tr; // 和
-  static final String loginPrintVerificationCode =
-      'login_print_verification_code'.tr; // 请输入验证码
-  static final String loginPrintPhoneVerification =
-      'login_print_phone_verification'.tr; // 请输入正确格式的手机号码
-  static final String loginAgreePrivacy =
-      'login_agree_privacy'.tr; // 请先阅读并同意《隐私权政策》和《服务条款》
-  static final String loginVerificationCodeErrorToast =
-      'login_verification_code_error_toast'.tr; // 验证码输入错误,请重新输入
+  static final String loginPrintVerificationCode = 'login_print_verification_code'.tr; // 请输入验证码
+  static final String loginPrintPhoneVerification = 'login_print_phone_verification'.tr; // 请输入正确格式的手机号码
+  static final String loginAgreePrivacy = 'login_agree_privacy'.tr; // 请先阅读并同意《隐私权政策》和《服务条款》
+  static final String loginVerificationCodeErrorToast = 'login_verification_code_error_toast'.tr; // 验证码输入错误,请重新输入
   static final String accountNoLogin = 'account_no_login'.tr; // 账号未登录
-  static final String loginRequestCodeFrequentlyToast =
-      'login_request_code_frequently_toast'.tr; // 请求过于频繁,请稍后再试
-  static final String loginVerificationCodeRequestFailedToast =
-      'login_verification_code_request_failed_toast'.tr; // 验证码发送失败,请重试
+  static final String loginRequestCodeFrequentlyToast = 'login_request_code_frequently_toast'.tr; // 请求过于频繁,请稍后再试
+  static final String loginVerificationCodeRequestFailedToast = 'login_verification_code_request_failed_toast'.tr; // 验证码发送失败,请重试
   static final String loginSuccess = 'login_success'.tr; // 登录成功
-  static final String loginTooOftenToast =
-      'login_too_often_toast'.tr; // 登录过于频繁,请稍后再试
+  static final String loginTooOftenToast = 'login_too_often_toast'.tr; // 登录过于频繁,请稍后再试
   static final String loginFailedToast = 'login_failed_toast'.tr; // 登录失败
   static final String privacyTitle = 'privacy_title'.tr; // 隐私权政策与服务条款
   static final String privacyDisagree = 'privacy_disagree'.tr; // 不同意
-  static final String privacyDisagreeAndExit =
-      'privacy_disagree_and_exit'.tr; // 不同意并退出
+  static final String privacyDisagreeAndExit = 'privacy_disagree_and_exit'.tr; // 不同意并退出
   static final String privacyAgree = 'privacy_agree'.tr; // 同意
   static final String locationMine = 'location_mine'.tr; // 我
   static final String locationTrace = 'location_trace'.tr; // 轨迹
   static final String dialogCancel = 'dialog_cancel'.tr; // 取消
   static final String dialogSure = 'dialog_sure'.tr; // 确定
-  static final String dialogExitAccountTitle =
-      'dialog_exit_account_title'.tr; // 提示
-  static final String dialogExitAccountDesc =
-      'dialog_exit_account_desc'.tr; // 确定退出登录吗?
+  static final String dialogExitAccountTitle = 'dialog_exit_account_title'.tr; // 提示
+  static final String dialogExitAccountDesc = 'dialog_exit_account_desc'.tr; // 确定退出登录吗?
   static final String nextStep = 'next_step'.tr; // 下一步
-  static final String locationBackgroundAlwaysDesc =
-      'location_background_always_desc'.tr; // 为保位置展示在地图上,请按照图中指引操作
-  static final String dialogAddFriendTitle =
-      'dialog_add_friend_title'.tr; // 实时定位轨迹
-  static final String dialogAddFriendDesc =
-      'dialog_add_friend_desc'.tr; // 去添加Ta的手机号码
+  static final String locationBackgroundAlwaysDesc = 'location_background_always_desc'.tr; // 为保位置展示在地图上,请按照图中指引操作
+  static final String dialogAddFriendTitle = 'dialog_add_friend_title'.tr; // 实时定位轨迹
+  static final String dialogAddFriendDesc = 'dialog_add_friend_desc'.tr; // 去添加Ta的手机号码
   static final String dialogAddFriendBtn = 'dialog_add_friend_btn'.tr; // 立即添加
-  static final String dialogRecordLocation =
-      'dialog_record_location'.tr; // 记录轨迹
-  static final String dialogRecordLocationHasPermission =
-      'dialog_record_location_has_permission'.tr; // 开启定位权限
-  static final String dialogRecordLocationNotRequest =
-      'dialog_record_location_not_request'.tr; // 暂不开启
-  static final String dialogRecordLocationRequest =
-      'dialog_record_location_request'.tr; // 立即开启
-  static final String refreshFriendDataSuccess =
-      'refresh_friend_data_success'.tr; // 好友位置刷新成功
+  static final String dialogRecordLocation = 'dialog_record_location'.tr; // 记录轨迹
+  static final String dialogRecordLocationHasPermission = 'dialog_record_location_has_permission'.tr; // 开启定位权限
+  static final String dialogRecordLocationNotRequest = 'dialog_record_location_not_request'.tr; // 暂不开启
+  static final String dialogRecordLocationRequest = '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; // 未知
+  static final String friendLocationTimeUnknown = 'friend_location_time_unknown'.tr; // 未知
+  static final String friendInfoEdit = 'friend_info_edit'.tr; // 好友编辑
+  static final String friendEditRemarkTitle = 'friend_edit_remark_title'.tr; // 备注
+  static final String friendEditPhoneTitle = 'friend_edit_phone_title'.tr; // 手机号
+  static final String friendEditBlockTitle = 'friend_edit_block_title'.tr; // 不给Ta看
+  static final String blockedFriend = 'blocked_friend'.tr; // 已屏蔽该好友
+  static final String blockFriendOff = 'block_friend_off'.tr; // 屏蔽好友已关闭
+  static final String friendDelete = 'friend_delete'.tr; // 删除好友
+  static final String kindlyReminder = 'kindly_reminder'.tr; // 温馨提示
+  static final String friendDeleteContent = 'friend_delete_content'.tr; // 互删好友后,双方将停止位置的分享,以及清除相关定位记录,是否确认互删?
+  static final String deleteSuccess = 'delete_success'.tr; // 删除成功
+  static final String copyPhoneNumSuccess = 'copy_phone_num_success'.tr; // 手机号复制成功
+  static final String friendUpdateRemarkTitle = 'friend_update_remark_title'.tr; // 修改备注
+  static final String friendUpdateRemarkHint = 'friend_update_remark_hint'.tr; // 请输入备注
+  static final String remarkUpdateSuccess = 'remark_update_success'.tr; // 备注修改成功
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -229,6 +209,20 @@ class StringMultiSource {
       'examine_trace': '查看轨迹',
       'unopened_positioning': '未开启定位',
       'friend_location_time_unknown': '未知',
+      'friend_info_edit': '好友编辑',
+      'friend_edit_remark_title': '备注',
+      'friend_edit_phone_title': '手机号',
+      'friend_edit_block_title': '不给Ta看',
+      'blocked_friend': '已屏蔽该好友',
+      'block_friend_off': '屏蔽好友已关闭',
+      'friend_delete': '删除好友',
+      'kindly_reminder': '温馨提示',
+      'friend_delete_content': '互删好友后,双方将停止位置的分享,以及清除相关定位记录,是否确认互删?',
+      'delete_success': '删除成功',
+      'copy_phone_num_success': '手机号复制成功',
+      'friend_update_remark_title': '修改备注',
+      'friend_update_remark_hint': '请输入备注',
+      'remark_update_success': '备注修改成功',
     },
   };
 }

+ 5 - 0
lib/router/app_pages.dart

@@ -4,6 +4,8 @@ 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/friend/setting/friend_setting_controller.dart';
+import 'package:location/module/friend/setting/friend_setting_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';
@@ -26,6 +28,7 @@ abstract class RoutePath {
   static const mine = '/mine';
   static const browser = '/browser';
   static const friend = '/friend';
+  static const friendSetting = '/friendSetting';
 }
 
 class AppBinding extends Bindings {
@@ -37,6 +40,7 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<MineController>());
     lazyPut(() => getIt.get<BrowserController>());
     lazyPut(() => getIt.get<FriendController>());
+    lazyPut(() => getIt.get<FriendSettingController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -50,4 +54,5 @@ final generalPages = [
   GetPage(name: RoutePath.login, page: () => LoginPage()),
   GetPage(name: RoutePath.mine, page: () => MinePage()),
   GetPage(name: RoutePath.friend, page: () => FriendPage()),
+  GetPage(name: RoutePath.friendSetting, page: () => FriendSettingPage()),
 ];

+ 6 - 0
lib/utils/common_util.dart

@@ -1,3 +1,5 @@
+import 'package:flutter/services.dart';
+
 /// 截取后几位
 String stringExpand(String phone) {
   if (phone.length < 4) {
@@ -60,3 +62,7 @@ String addressCheck(String? address) {
   }
   return address;
 }
+
+void copyToClipboard(String content) {
+  Clipboard.setData(ClipboardData(text: content));
+}

+ 20 - 0
lib/utils/de_bounce.dart

@@ -0,0 +1,20 @@
+class Debounce {
+  // 设定的时间间隔,单位为毫秒
+  final int debounceTime;
+
+  // 记录上次点击的时间
+  DateTime? _lastClickTime;
+
+  Debounce({this.debounceTime = 300});
+
+  // 点击事件处理方法
+  void onClick(Function action) {
+    DateTime now = DateTime.now();
+    if (_lastClickTime == null ||
+        now.difference(_lastClickTime!) >
+            Duration(milliseconds: debounceTime)) {
+      _lastClickTime = now;
+      action();
+    }
+  }
+}

+ 90 - 0
lib/widget/load_switch.dart

@@ -0,0 +1,90 @@
+import 'package:animated_toggle_switch/animated_toggle_switch.dart';
+import 'package:flutter/material.dart';
+
+typedef LoadFutureCallback = Future<bool> Function(bool value);
+
+class LoadSwitch extends StatefulWidget {
+  final bool value;
+  final double height;
+  final double borderWidth;
+  final Color loadingColor;
+  final Color selectedColor;
+  final Color unselectedColor;
+
+  final LoadFutureCallback future;
+
+  const LoadSwitch(
+      {super.key,
+      required this.value,
+      required this.height,
+      required this.future,
+      this.borderWidth = 5,
+      this.loadingColor = Colors.blue,
+      this.selectedColor = Colors.green,
+      this.unselectedColor = Colors.red});
+
+  @override
+  State<LoadSwitch> createState() => _LoadSwitchState();
+}
+
+class _LoadSwitchState extends State<LoadSwitch> {
+  bool isSelected = false;
+
+  @override
+  void initState() {
+    // TODO: implement initState
+    super.initState();
+    isSelected = widget.value;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return CustomAnimatedToggleSwitch(
+      height: widget.height,
+      indicatorSize: Size.square(widget.height),
+      current: isSelected,
+      values: const [false, true],
+      onChanged: (value) async {
+        bool newValue = await widget.future(value);
+        setState(() {
+          isSelected = newValue;
+        });
+      },
+      animationDuration: const Duration(milliseconds: 350),
+      // iconArrangement: IconArrangement.overlap,
+      // spacing: -16.0,
+      wrapperBuilder: (context, global, child) => DecoratedBox(
+          decoration: BoxDecoration(
+              color: Color.lerp(
+                  Color.lerp(widget.unselectedColor, widget.selectedColor,
+                      global.position),
+                  Colors.grey,
+                  global.loadingAnimationValue),
+              borderRadius:
+                  BorderRadius.all(Radius.circular(widget.height / 2))),
+          child: child),
+      foregroundIndicatorBuilder: (context, global) {
+        return Stack(
+          fit: StackFit.expand,
+          children: [
+            Padding(
+              padding: EdgeInsets.all(widget.borderWidth),
+              child: const DecoratedBox(
+                  decoration: BoxDecoration(
+                      shape: BoxShape.circle, color: Colors.white)),
+            ),
+            Padding(
+              padding: EdgeInsets.all(widget.borderWidth / 2),
+              child: CircularProgressIndicator(
+                strokeWidth: widget.borderWidth,
+                color: widget.loadingColor
+                    .withOpacity(global.loadingAnimationValue),
+              ),
+            ),
+          ],
+        );
+      },
+      iconBuilder: (context, local, global) => const SizedBox(),
+    );
+  }
+}

+ 8 - 0
pubspec.lock

@@ -30,6 +30,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.4.0"
+  animated_toggle_switch:
+    dependency: "direct main"
+    description:
+      name: animated_toggle_switch
+      sha256: f2e021b1bbbaa76c664b29f6932ed9a37fbd75870da19053a0cc6cd9f790d83a
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.8.4"
   app_tracking_transparency:
     dependency: "direct main"
     description:

+ 3 - 0
pubspec.yaml

@@ -84,6 +84,9 @@ dependencies:
   #socket连接
   web_socket_channel: 3.0.2
 
+  #switch
+  animated_toggle_switch: ^0.8.4
+
   ######################地图########################
   flutter_map:
     path: plugins/map