Browse Source

[feat]完成号码不规则功能

云天逵 1 năm trước cách đây
mục cha
commit
519e802ff7

+ 3 - 0
android/app/src/main/AndroidManifest.xml

@@ -1,6 +1,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
     <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+<!--    读写联系人-->
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
     <application
         android:label="clean"
         android:name="${applicationName}"

+ 149 - 1
lib/module/contact/incomplete/controller.dart

@@ -1,5 +1,153 @@
 import 'package:clean/base/base_controller.dart';
+import 'package:clean/module/contact/contact_state.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_contacts/contact.dart';
+import 'package:get/get.dart';
+
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
+
+import '../../../utils/toast_util.dart';
 
 class ContactInCompleteController extends BaseController {
+  // 存储不符合要求的联系人
+  final RxList<Contact> filteredContacts = <Contact>[].obs;
+
+  // 存储联系人按首字母分组
+  final RxMap<String, List<Contact>> groupedContacts =
+      <String, List<Contact>>{}.obs;
+
+  // 存储联系人首字母
+  final RxList<String> initials = <String>[].obs;
+
+  // 是否全选
+  final RxBool isAllSelected = false.obs;
+
+  // 存储选中的联系人
+  final RxSet<String> selectedContacts = <String>{}.obs;
+
+  final ItemScrollController itemScrollController = ItemScrollController();
+
+  final ItemPositionsListener itemPositionsListener =
+      ItemPositionsListener.create();
+
+  // isEdit
+  final RxBool isEdit = false.obs;
+
+  @override
+  void onInit()async {
+    super.onInit();
+
+  }
+
+  @override
+  void onReady() {
+    // TODO: implement onReady
+    super.onReady();
+
+
+    filterContacts();
+    groupContacts();
+  }
+
+  // 过滤联系人
+  void filterContacts() {
+    filteredContacts.value = ContactState.contactList.where((contact) {
+      final hasValidPhone =
+          contact.phones.any((p) => isValidPhoneNumber(p.number ?? ''));
+
+      return contact.phones.isEmpty || !hasValidPhone;
+    }).toList();
+  }
+
+  bool isValidPhoneNumber(String phone) {
+
+    // 如果电话号码为空,则返回false
+    if (phone.isEmpty) return false;
+    // 如果电话号码不是数字、空格、+号,则返回false
+    return RegExp(r'^\+?[0-9 -]+$').hasMatch(phone);
+  }
+
+  Future<void> deleteBtnClick() async {
+    print("deleteBtnClick selectedContacts: $selectedContacts");
+    final contactToDelete = ContactState.contactList
+        .where((contact) => selectedContacts.contains(contact.id))
+        .toList();
+
+    for (var contact in contactToDelete) {
+      await contact.delete(); // 删除操作必须 `await`
+    }
+
+    ToastUtil.show("删除成功");
+
+    exitEditMode();
+
+    await ContactState.loadContacts(); // 确保联系人列表更新
+
+    filterContacts(); // 重新筛选联系人
+    groupContacts(); // 重新分组
+
+    debugPrint("filteredContacts.length: ${filteredContacts.length}");
+  }
+
+  // 全选/取消全选
+  void toggleSelectAll() {
+    if (isAllSelected.value) {
+      selectedContacts.clear();
+    } else {
+      selectedContacts
+          .addAll(filteredContacts.map((contact) => contact.id));
+    }
+    isAllSelected.value = !isAllSelected.value;
+  }
+
+  // 退出编辑模式时清空选择
+  void exitEditMode() {
+    isEdit.value = false;
+    selectedContacts.clear();
+    isAllSelected.value = false;
+  }
+
+  // 滚动到指定首字母
+  void groupContacts() {
+    final Map<String, List<Contact>> map = {};
+    for (var contact in filteredContacts) {
+      final initial = contact.displayName.isNotEmpty == true
+          ? contact.displayName[0].toUpperCase()
+          : '#';
+      map.putIfAbsent(initial, () => []).add(contact);
+    }
+
+    final sortedKeys = map.keys.toList()..sort();
+    groupedContacts.value =
+        Map.fromEntries(sortedKeys.map((key) => MapEntry(key, map[key]!)));
+    initials.value = sortedKeys;
+  }
+
+  void scrollToInitial(String initial) {
+    int index = ContactState.initials.indexOf(initial);
+    if (index != -1) {
+
+      final positions = itemPositionsListener.itemPositions.value;
+      final isVisible = positions.any((position) => position.index == index);
+
+      if (!isVisible) {
+        itemScrollController.scrollTo(
+          index: index,
+          duration: Duration(milliseconds: 300),
+        );
+      }
+    }
+  }
+
+  void toggleSelectContact(Contact selectContact) {
+    print("selectContact: ${selectContact.displayName}");
+    if (selectedContacts.contains(selectContact.id)) {
+      selectedContacts.remove(selectContact.id);
+    } else {
+      selectedContacts.add(selectContact.id);
+    }
+    isAllSelected.value = selectedContacts.length == filteredContacts.length;
+  }
+
 
-}
+}

+ 316 - 2
lib/module/contact/incomplete/view.dart

@@ -1,8 +1,12 @@
 import 'package:clean/base/base_page.dart';
 import 'package:clean/module/contact/incomplete/controller.dart';
 import 'package:clean/resource/assets.gen.dart';
+import 'package:clean/utils/expand.dart';
 import 'package:flutter/Material.dart';
+import 'package:flutter_contacts/contact.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
+import 'package:get/get.dart';
 
 class ContactIncompletePage extends BasePage<ContactInCompleteController> {
   const ContactIncompletePage({super.key});
@@ -19,7 +23,7 @@ class ContactIncompletePage extends BasePage<ContactInCompleteController> {
   Widget buildBody(BuildContext context) {
     return Stack(
       children: [
-        // buildMain(context),
+        buildMain(context),
         IgnorePointer(
           child: Assets.images.bgHome.image(
             width: 360.w,
@@ -28,4 +32,314 @@ class ContactIncompletePage extends BasePage<ContactInCompleteController> {
       ],
     );
   }
-}
+
+  Widget buildMain(BuildContext context) {
+    return SafeArea(
+      child: Container(
+        padding: EdgeInsets.only(left: 16.w, top: 14.h, right: 16.w),
+        child: Obx(() {
+          return Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              !controller.isEdit.value
+                  ? Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        GestureDetector(
+                          onTap: () {
+                            Get.back();
+                          },
+                          child: Assets.images.iconCommonBack
+                              .image(width: 28.w, height: 28.w),
+                        ),
+                        GestureDetector(
+                          onTap: () {
+                            controller.isEdit.value = true;
+                          },
+                          child: Container(
+                            width: 71.w,
+                            height: 30.h,
+                            decoration: BoxDecoration(
+                              color: "#1F2D3F".color,
+                              borderRadius: BorderRadius.all(
+                                Radius.circular(15.h),
+                              ),
+                            ),
+                            child: Center(
+                              child: Text(
+                                "Select",
+                                style: TextStyle(
+                                  color: Colors.white,
+                                  fontSize: 14.sp,
+                                ),
+                              ),
+                            ),
+                          ),
+                        ),
+                      ],
+                    )
+                  : Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        GestureDetector(
+                          onTap: () {
+                            controller.isEdit.value = false;
+                          },
+                          child: Container(
+                            width: 71.w,
+                            height: 30.h,
+                            decoration: BoxDecoration(
+                              color: "#1F2D3F".color,
+                              borderRadius: BorderRadius.all(
+                                Radius.circular(15.h),
+                              ),
+                            ),
+                            child: Center(
+                              child: Text(
+                                "Cancel",
+                                style: TextStyle(
+                                  color: Colors.white,
+                                  fontSize: 14.sp,
+                                ),
+                              ),
+                            ),
+                          ),
+                        ),
+                        GestureDetector(
+                          onTap: () {
+                            controller.toggleSelectAll();
+                          },
+                          child: Text(
+                            controller.isAllSelected.value
+                                ? "Deselect all"
+                                : "Select All",
+                            style: TextStyle(
+                              color: Colors.white.withValues(alpha: 0.65),
+                              fontSize: 14.sp,
+                            ),
+                          ),
+                        ),
+                      ],
+                    ),
+              SizedBox(
+                height: 12.h,
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Text(
+                    "Incomplete Contacts",
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontWeight: FontWeight.w700,
+                      fontSize: 24.sp,
+                    ),
+                  ),
+                ],
+              ),
+              Expanded(
+                child: Row(
+                  children: [
+                    Expanded(
+                      child: Obx(() {
+                        return ScrollablePositionedList.builder(
+                          itemCount: controller.initials.length,
+                          itemScrollController: controller.itemScrollController,
+                          itemPositionsListener:
+                              controller.itemPositionsListener,
+                          itemBuilder: (context, index) {
+                            if (controller.initials.isEmpty) {
+                              return Container();
+                            }
+                            String initial = controller.initials[index];
+                            // 当前首字母对应的联系人列表
+                                controller.groupedContacts[initial];
+                            return Column(
+                              crossAxisAlignment: CrossAxisAlignment.start,
+                              children: [
+                                SizedBox(
+                                  height: 12.h,
+                                ),
+                                Padding(
+                                  padding: EdgeInsets.symmetric(vertical: 8.h),
+                                  child: Text(
+                                    initial,
+                                    style: TextStyle(
+                                      color: Colors.white.withOpacity(0.7),
+                                      fontSize: 14.sp,
+                                      fontWeight: FontWeight.w500,
+                                    ),
+                                  ),
+                                ),
+                                ...controller.groupedContacts[initial]!
+                                    .asMap()
+                                    .entries
+                                    .map((entry) {
+                                  int index = entry.key; // 当前联系人的索引
+                                  Contact contact = entry.value; // 当前联系人
+                                  bool isFirst = index == 0; // 是否是第一个
+                                  bool isLast = index ==
+                                      (controller.groupedContacts[initial]
+                                                  ?.length ??
+                                              0) -
+                                          1; // 是否是最后一个
+                                  return Container(
+                                    padding: EdgeInsets.all(10.w),
+                                    width: double.infinity,
+                                    // height: 62.h,
+                                    decoration: BoxDecoration(
+                                      borderRadius: BorderRadius.vertical(
+                                        top: isFirst
+                                            ? Radius.circular(12)
+                                            : Radius.zero, // 第一个设置上圆角
+                                        bottom: isLast
+                                            ? Radius.circular(12)
+                                            : Radius.zero, // 最后一个设置下圆角
+                                      ),
+                                      color: Colors.white.withValues(alpha: 0.12),
+                                    ),
+                                    child: Row(
+                                      mainAxisAlignment:
+                                          MainAxisAlignment.spaceBetween,
+                                      children: [
+                                        Column(
+                                          mainAxisAlignment:
+                                              MainAxisAlignment.start,
+                                          crossAxisAlignment:
+                                              CrossAxisAlignment.start,
+                                          children: [
+                                            Text(
+                                              contact.displayName ?? '未命名',
+                                              style: TextStyle(
+                                                color: Colors.white,
+                                                fontSize: 14.sp,
+                                                fontWeight: FontWeight.w500,
+                                              ),
+                                            ),
+                                            SizedBox(
+                                              height: 5.h,
+                                            ),
+                                            Text(
+                                              contact.phones.isEmpty
+                                                  ? ''
+                                                  : contact.phones.first.number,
+                                              style: TextStyle(
+                                                color: Colors.white,
+                                                fontSize: 14.sp,
+                                                fontWeight: FontWeight.w500,
+                                              ),
+                                            ),
+                                          ],
+                                        ),
+                                        // 删除按钮
+                                        Visibility(
+                                          visible: controller.isEdit.value,
+                                          child: GestureDetector(
+                                            onTap: () {
+                                              controller
+                                                  .toggleSelectContact(contact);
+                                            },
+                                            child: Container(
+                                              child: controller.selectedContacts
+                                                      .contains(contact.id)
+                                                  ? Center(
+                                                      child: Assets
+                                                          .images.iconSelected
+                                                          .image(
+                                                        width: 16.w,
+                                                        height: 16.h,
+                                                      ),
+                                                    )
+                                                  : Center(
+                                                      child: Assets
+                                                          .images.iconUnselected
+                                                          .image(
+                                                        width: 16.w,
+                                                        height: 16.h,
+                                                      ),
+                                                    ),
+                                            ),
+                                          ),
+                                        ),
+                                      ],
+                                    ),
+                                  );
+                                }),
+                                SizedBox(
+                                  height: 12.h,
+                                ),
+                              ],
+                            );
+                          },
+                        );
+                      }),
+                    ),
+                    Container(
+                      width: 30,
+                      child: ListView.builder(
+                        // physics: NeverScrollableScrollPhysics(),
+                        itemCount: controller.initials.length,
+                        itemBuilder: (context, index) {
+                          return GestureDetector(
+                            onTap: () => controller
+                                .scrollToInitial(controller.initials[index]),
+                            child: Padding(
+                              padding: EdgeInsets.symmetric(vertical: 2),
+                              child: Text(
+                                controller.initials[index],
+                                textAlign: TextAlign.center,
+                                style: TextStyle(
+                                  fontSize: 14,
+                                  color: "#0279FB".color,
+                                  fontWeight: FontWeight.bold,
+                                ),
+                              ),
+                            ),
+                          );
+                        },
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              Visibility(
+                visible: controller.isEdit.value,
+                child: GestureDetector(
+                  onTap: () {
+                    controller.deleteBtnClick();
+                  },
+                  child: Container(
+                    width: 328.w,
+                    height: 48.h,
+                    decoration: BoxDecoration(
+                      color: "#0279FB".color,
+                      borderRadius: BorderRadius.all(
+                        Radius.circular(10.r),
+                      ),
+                    ),
+                    child: Center(
+                      child: Row(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          Text(
+                            "Delete",
+                            style: TextStyle(
+                              color: Colors.white,
+                              fontSize: 16.sp,
+                              fontWeight: FontWeight.w500,
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+                ),
+              )
+            ],
+          );
+        }),
+      ),
+    );
+  }
+}