Browse Source

[feat]init

Destiny 11 months ago
commit
9ce9bae4c9
100 changed files with 17092 additions and 0 deletions
  1. BIN
      .DS_Store
  2. 73 0
      .gitignore
  3. 48 0
      Podfile
  4. 1335 0
      QuickSearchLocation.xcodeproj/project.pbxproj
  5. 56 0
      QuickSearchLocation/Base.lproj/LaunchScreen.storyboard
  6. BIN
      QuickSearchLocation/Classes/.DS_Store
  7. 49 0
      QuickSearchLocation/Classes/Category/Bundle+Extension.swift
  8. 50 0
      QuickSearchLocation/Classes/Category/CAGradientLayer+Extension.swift
  9. 639 0
      QuickSearchLocation/Classes/Category/Date+Extension.swift
  10. 18 0
      QuickSearchLocation/Classes/Category/DateFormatter+Extension.swift
  11. 68 0
      QuickSearchLocation/Classes/Category/NSAttributedString+Extension.swift
  12. 90 0
      QuickSearchLocation/Classes/Category/NSMutableAttributedString+Extension.swift
  13. 47 0
      QuickSearchLocation/Classes/Category/NSObject+Extension.swift
  14. 220 0
      QuickSearchLocation/Classes/Category/String+Extension.swift
  15. 49 0
      QuickSearchLocation/Classes/Category/UIApplication+Extension.swift
  16. 303 0
      QuickSearchLocation/Classes/Category/UIButton+Extension.swift
  17. 80 0
      QuickSearchLocation/Classes/Category/UICollectionView+Extension.swift
  18. 136 0
      QuickSearchLocation/Classes/Category/UIColor+Extension.swift
  19. 97 0
      QuickSearchLocation/Classes/Category/UIFont+Extension.swift
  20. 136 0
      QuickSearchLocation/Classes/Category/UIImage+Extension.swift
  21. 130 0
      QuickSearchLocation/Classes/Category/UILabel+Extension.swift
  22. 60 0
      QuickSearchLocation/Classes/Category/UITabBarController+Extension.swift
  23. 40 0
      QuickSearchLocation/Classes/Category/UITableView+Extension.swift
  24. 82 0
      QuickSearchLocation/Classes/Category/UITextField+Extension.swift
  25. 501 0
      QuickSearchLocation/Classes/Category/UIView+Extension.swift
  26. 139 0
      QuickSearchLocation/Classes/Category/UIViewController+Extension.swift
  27. 42 0
      QuickSearchLocation/Classes/Category/UIVisualEffectView+Extension.swift
  28. 53 0
      QuickSearchLocation/Classes/Common/Controller/QSLBaseController.swift
  29. 65 0
      QuickSearchLocation/Classes/Common/Controller/QSLBaseNavController.swift
  30. 179 0
      QuickSearchLocation/Classes/Common/Controller/QSLWebController.swift
  31. 318 0
      QuickSearchLocation/Classes/Common/LoadingViewController.swift
  32. 26 0
      QuickSearchLocation/Classes/Common/Model/QSLContactModel.swift
  33. 42 0
      QuickSearchLocation/Classes/Common/Model/QSLGoodModel.swift
  34. 29 0
      QuickSearchLocation/Classes/Common/Model/QSLMapMessageModel.swift
  35. 21 0
      QuickSearchLocation/Classes/Common/Model/QSLMapPointModel.swift
  36. 36 0
      QuickSearchLocation/Classes/Common/Model/QSLMapTrackModel.swift
  37. 74 0
      QuickSearchLocation/Classes/Common/Model/QSLMemberModel.swift
  38. 35 0
      QuickSearchLocation/Classes/Common/Model/QSLMessageModel.swift
  39. 30 0
      QuickSearchLocation/Classes/Common/Model/QSLOrderModel.swift
  40. 41 0
      QuickSearchLocation/Classes/Common/Model/QSLRequestModel.swift
  41. 42 0
      QuickSearchLocation/Classes/Common/Model/QSLUserModel.swift
  42. 316 0
      QuickSearchLocation/Classes/Common/PhotoClassifier.swift
  43. 86 0
      QuickSearchLocation/Classes/Common/Tool/QSLCacheManager.swift
  44. 55 0
      QuickSearchLocation/Classes/Common/Tool/QSLGravityManager.swift
  45. 68 0
      QuickSearchLocation/Classes/Common/Tool/QSLJumpManager.swift
  46. 27 0
      QuickSearchLocation/Classes/Common/Tool/QSLLoading.swift
  47. 176 0
      QuickSearchLocation/Classes/Common/Tool/QSLSocketManager.swift
  48. 242 0
      QuickSearchLocation/Classes/Common/View/QSLAlertView.swift
  49. 263 0
      QuickSearchLocation/Classes/Common/View/QSLPopView.swift
  50. 44 0
      QuickSearchLocation/Classes/Common/View/QSLPopViewCell.swift
  51. 230 0
      QuickSearchLocation/Classes/Common/View/QSLPrivacyAlertView.swift
  52. 36 0
      QuickSearchLocation/Classes/Main/AppDelegate.swift
  53. 24 0
      QuickSearchLocation/Classes/Main/Base.lproj/Main.storyboard
  54. 112 0
      QuickSearchLocation/Classes/Main/CustomTabBarController.swift
  55. 233 0
      QuickSearchLocation/Classes/Main/QSLBaseManager.swift
  56. 49 0
      QuickSearchLocation/Classes/Main/SceneDelegate.swift
  57. 76 0
      QuickSearchLocation/Classes/Main/ViewController.swift
  58. 1 0
      QuickSearchLocation/Classes/Main/zh-Hans.lproj/Main.strings
  59. 279 0
      QuickSearchLocation/Classes/Network/QSLNetwork.swift
  60. 350 0
      QuickSearchLocation/Classes/Pages/QSLAdd/Controller/QSLAddController.swift
  61. 284 0
      QuickSearchLocation/Classes/Pages/QSLAlert/View/QSLFriendAddAlertView.swift
  62. 185 0
      QuickSearchLocation/Classes/Pages/QSLContact/Cell/QSLContactCell.swift
  63. 55 0
      QuickSearchLocation/Classes/Pages/QSLContact/Cell/QSLContactFailCell.swift
  64. 397 0
      QuickSearchLocation/Classes/Pages/QSLContact/Controller/QSLContactController.swift
  65. 285 0
      QuickSearchLocation/Classes/Pages/QSLContact/View/QSLContactAddAlertView.swift
  66. 220 0
      QuickSearchLocation/Classes/Pages/QSLContact/View/QSLContactSendFailAlertView.swift
  67. 247 0
      QuickSearchLocation/Classes/Pages/QSLFriend/Cell/QSLFriendTableViewCell.swift
  68. 355 0
      QuickSearchLocation/Classes/Pages/QSLFriend/Controller/QSLFriendController.swift
  69. 221 0
      QuickSearchLocation/Classes/Pages/QSLFriend/View/QSLFriendRemarkAlertView.swift
  70. 139 0
      QuickSearchLocation/Classes/Pages/QSLGuide/QSLGuideController.swift
  71. 248 0
      QuickSearchLocation/Classes/Pages/QSLHome/Cell/QSLHomeFriendTableViewCell.swift
  72. 692 0
      QuickSearchLocation/Classes/Pages/QSLHome/Controller/QSLHomeController.swift
  73. 51 0
      QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeAnnotatinView.swift
  74. 68 0
      QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeAuthHeaderView.swift
  75. 65 0
      QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeButtonView.swift
  76. 49 0
      QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeCallOutView.swift
  77. 258 0
      QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeEmptyView.swift
  78. 97 0
      QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeFriendFooterView.swift
  79. 458 0
      QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeFriendView.swift
  80. 50 0
      QuickSearchLocation/Classes/Pages/QSLHome/ViewModel/QSLHomeViewModel.swift
  81. 423 0
      QuickSearchLocation/Classes/Pages/QSLLogin/Controller/QSLLoginViewController.swift
  82. 191 0
      QuickSearchLocation/Classes/Pages/QSLMessage/Cell/QSLMessageTableViewCell.swift
  83. 260 0
      QuickSearchLocation/Classes/Pages/QSLMessage/Controller/QSLMessageController.swift
  84. 179 0
      QuickSearchLocation/Classes/Pages/QSLMessage/QSLRequest/QSLRequestCell.swift
  85. 158 0
      QuickSearchLocation/Classes/Pages/QSLMessage/QSLRequest/QSLRequestController.swift
  86. 203 0
      QuickSearchLocation/Classes/Pages/QSLMessage/View/QSLMessageHeaderView.swift
  87. 85 0
      QuickSearchLocation/Classes/Pages/QSLMine/Cell/QSLMineFuncCollectionViewCell.swift
  88. 237 0
      QuickSearchLocation/Classes/Pages/QSLMine/Controller/QSLMineController.swift
  89. 165 0
      QuickSearchLocation/Classes/Pages/QSLMine/QSLAppInfo/QSLAppInfoController.swift
  90. 105 0
      QuickSearchLocation/Classes/Pages/QSLMine/View/QSLMineFuncView.swift
  91. 79 0
      QuickSearchLocation/Classes/Pages/QSLMine/View/QSLMineInfoView.swift
  92. 149 0
      QuickSearchLocation/Classes/Pages/QSLMine/View/QSLMineVipView.swift
  93. 59 0
      QuickSearchLocation/Classes/Pages/QSLMine/ViewModel/QSLMineViewModel.swift
  94. 613 0
      QuickSearchLocation/Classes/Pages/QSLRoad/Controller/QSLRoadController.swift
  95. 311 0
      QuickSearchLocation/Classes/Pages/QSLRoad/View/QSLRoadMainView.swift
  96. 71 0
      QuickSearchLocation/Classes/Pages/QSLVip/Cell/QSLVipCommentCellView.swift
  97. 173 0
      QuickSearchLocation/Classes/Pages/QSLVip/Cell/QSLVipGoodCollectionViewCell.swift
  98. 709 0
      QuickSearchLocation/Classes/Pages/QSLVip/Controller/QSLVipController.swift
  99. 352 0
      QuickSearchLocation/Classes/Pages/QSLVip/QSLVipManager.swift
  100. 0 0
      QuickSearchLocation/Frameworks/GravityEngineSDK.framework/.DS_Store

BIN
.DS_Store


+ 73 - 0
.gitignore

@@ -0,0 +1,73 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## AppCode
+.idea
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+Pods
+Podfile.lock
+
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+*.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+#Carthage/Build/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+#fastlane/report.xml
+#fastlane/Preview.html
+#fastlane/screenshots/**/*.png
+#fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+#iOSInjectionProject/

+ 48 - 0
Podfile

@@ -0,0 +1,48 @@
+# Uncomment the next line to define a global platform for your project
+# platform :ios, '9.0'
+
+target 'QuickSearchLocation' do
+  # Comment the next line if you don't want to use dynamic frameworks
+  use_frameworks!
+
+  # Pods for QuickSearchLocation
+
+    pod 'SnapKit'
+    pod 'Kingfisher'
+    pod 'Alamofire'
+    pod 'Moya'
+    pod 'MoyaMapper'
+    pod 'SwiftyJSON'
+    pod 'HandyJSON'
+    pod 'YYText'
+
+    pod 'IQKeyboardManagerSwift'
+    pod 'SnapKit'
+    pod 'Toast-Swift'
+    pod 'ProgressHUD'
+    pod 'MarqueeLabel'
+    
+    pod 'CRRefresh'
+    
+    pod 'WMZDropDownMenu'
+    
+    pod 'SocketRocket'
+    
+    pod 'GKCycleScrollView'
+    
+    pod 'BRPickerView'
+
+    # 高德
+    pod 'AMap3DMap'
+    pod 'AMapLocation'
+    pod 'AMapSearch'
+
+    pod 'LookinServer', :configurations => ['Debug']
+end
+
+post_install do |installer|
+  installer.pods_project.build_configurations.each do |config|
+    config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
+  end
+end
+

File diff suppressed because it is too large
+ 1335 - 0
QuickSearchLocation.xcodeproj/project.pbxproj


+ 56 - 0
QuickSearchLocation/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <device id="retina6_12" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="launch_bottom_bg" translatesAutoresizingMaskIntoConstraints="NO" id="d8e-gw-cy9">
+                                <rect key="frame" x="0.0" y="721" width="393" height="131"/>
+                            </imageView>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="launch_icon" translatesAutoresizingMaskIntoConstraints="NO" id="KRc-qV-WiP">
+                                <rect key="frame" x="156.66666666666666" y="189" width="80" height="80"/>
+                            </imageView>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="launch_title" translatesAutoresizingMaskIntoConstraints="NO" id="qVj-8G-b1M">
+                                <rect key="frame" x="96" y="715.33333333333337" width="201" height="62.666666666666629"/>
+                                <constraints>
+                                    <constraint firstAttribute="width" secondItem="qVj-8G-b1M" secondAttribute="height" multiplier="501:156" id="YNr-ry-XnS"/>
+                                </constraints>
+                            </imageView>
+                        </subviews>
+                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <constraints>
+                            <constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="qVj-8G-b1M" secondAttribute="bottom" constant="40" id="0wO-Fy-VaD"/>
+                            <constraint firstItem="KRc-qV-WiP" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="130" id="Bqy-Mr-Rx9"/>
+                            <constraint firstItem="KRc-qV-WiP" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="NXs-nV-10X"/>
+                            <constraint firstItem="d8e-gw-cy9" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="P0s-Fb-yw9"/>
+                            <constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="qVj-8G-b1M" secondAttribute="trailing" constant="96" id="eQX-iH-6vl"/>
+                            <constraint firstItem="qVj-8G-b1M" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="96" id="rrc-8E-gWh"/>
+                            <constraint firstAttribute="bottom" secondItem="d8e-gw-cy9" secondAttribute="bottom" id="vud-Qw-r4Y"/>
+                            <constraint firstAttribute="trailing" secondItem="d8e-gw-cy9" secondAttribute="trailing" id="yNc-Sx-bYw"/>
+                        </constraints>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+    <resources>
+        <image name="launch_bottom_bg" width="360" height="131"/>
+        <image name="launch_icon" width="80" height="80"/>
+        <image name="launch_title" width="167" height="52"/>
+    </resources>
+</document>

BIN
QuickSearchLocation/Classes/.DS_Store


+ 49 - 0
QuickSearchLocation/Classes/Category/Bundle+Extension.swift

@@ -0,0 +1,49 @@
+//
+//  Bundle+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+// MARK: - App的基本信息
+public extension Bundle {
+    
+    // MARK: 2.1、App命名空间
+    /// App命名空间
+    static var namespace: String {
+        guard let namespace =  Bundle.main.infoDictionary?["CFBundleExecutable"] as? String else { return "" }
+        return  namespace
+    }
+    
+    // MARK: 2.2、项目/app 的名字
+    /// 项目/app 的名字
+    static var bundleName: String {
+        return (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) ?? ""
+    }
+    
+    // MARK: 2.3、获取app的版本号
+    /// 获取app的版本号
+    static var appVersion: String {
+        return (Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? ""
+    }
+    
+    // MARK: 2.4、获取app的 Build ID
+    /// 获取app的 Build ID
+    static var appBuild: String {
+        return (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) ?? ""
+    }
+    
+    // MARK: 2.5、获取app的 Bundle Identifier
+    /// 获取app的 Bundle Identifier
+    static var appBundleIdentifier: String {
+        return Bundle.main.bundleIdentifier ?? ""
+    }
+    
+    // MARK: 2.7、App 名称
+    /// App 名称
+    static var appDisplayName: String {
+        return (Bundle.main.infoDictionary!["CFBundleDisplayName"] as? String) ?? ""
+    }
+}

+ 50 - 0
QuickSearchLocation/Classes/Category/CAGradientLayer+Extension.swift

@@ -0,0 +1,50 @@
+//
+//  CAGradientLayer+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import UIKit
+
+public enum ViewGradientDirection {
+    
+    /// 水平从左到右
+    case horizontal
+    ///  垂直从上到下
+    case vertical
+    
+    public func point() -> (CGPoint, CGPoint) {
+        switch self {
+        case .horizontal:
+            return (CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 0))
+        case .vertical:
+            return (CGPoint(x: 0, y: 0), CGPoint(x: 0, y: 1))
+        }
+    }
+}
+
+public extension CAGradientLayer {
+    
+    // MARK: 1.1、设置渐变色图层
+    /// 设置渐变色图层
+    /// - Parameters:
+    ///   - direction: 渐变方向
+    ///   - gradientColors: 渐变的颜色数组(颜色的数组)
+    ///   - gradientLocations: 设置渐变颜色的终止位置,这些值必须是递增的,数组的长度和 colors 的长度最好一致
+    func gradientLayer(_ direction: ViewGradientDirection = .horizontal, _ gradientColors: [Any], _ gradientLocations: [NSNumber]? = nil, _ transform: CATransform3D? = nil) -> CAGradientLayer {
+       
+        // 设置渐变的颜色数组
+        self.colors = gradientColors
+        // 设置渐变颜色的终止位置,这些值必须是递增的,数组的长度和 colors 的长度最好一致
+        self.locations = gradientLocations
+        // 设置渲染的起始结束位置(渐变方向设置)
+        self.startPoint = direction.point().0
+        self.endPoint = direction.point().1
+        if let weakTransform = transform {
+            self.transform = weakTransform
+        }
+        
+        return self
+    }
+}

File diff suppressed because it is too large
+ 639 - 0
QuickSearchLocation/Classes/Category/Date+Extension.swift


+ 18 - 0
QuickSearchLocation/Classes/Category/DateFormatter+Extension.swift

@@ -0,0 +1,18 @@
+//
+//  DateFormatter+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/15.
+//
+
+import Foundation
+
+let qsl_formatter = DateFormatter()
+
+public extension DateFormatter {
+    
+    convenience init(format: String) {
+        self.init()
+        dateFormat = format
+    }
+}

+ 68 - 0
QuickSearchLocation/Classes/Category/NSAttributedString+Extension.swift

@@ -0,0 +1,68 @@
+//
+//  NSAttributedString+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/27.
+//
+
+import UIKit
+
+extension NSAttributedString {
+    
+    func setRangeFontText(font: UIFont, range: NSRange) -> NSAttributedString {
+        return setSpecificRangeTextMoreAttributes(attributes: [NSAttributedString.Key.font : font], range: range)
+    }
+    
+    func setSpecificTextColor(_ text: String, color: UIColor) -> NSAttributedString {
+        return setSpecificTextMoreAttributes(text, attributes: [NSAttributedString.Key.foregroundColor : color])
+    }
+    
+    func setSpecificRangeTextMoreAttributes(attributes: Dictionary<NSAttributedString.Key, Any>, range: NSRange) -> NSAttributedString {
+        let mutableAttributedString = NSMutableAttributedString(attributedString: self)
+        for name in attributes.keys {
+            mutableAttributedString.addAttribute(name, value: attributes[name] ?? "", range: range)
+        }
+        return mutableAttributedString
+    }
+    
+    func setSpecificTextColorFont(_ text: String, color: UIColor, font: UIFont) -> NSAttributedString {
+        return setSpecificTextMoreAttributes(text, attributes: [NSAttributedString.Key.foregroundColor : color, NSAttributedString.Key.font : font])
+    }
+    
+    func setSpecificTextMoreAttributes(_ text: String, attributes: Dictionary<NSAttributedString.Key, Any>) -> NSAttributedString {
+        let mutableAttributedString = NSMutableAttributedString(attributedString: self)
+        let rangeArray = getStringRangeArray(with: [text])
+        if !rangeArray.isEmpty {
+            for name in attributes.keys {
+                for range in rangeArray {
+                    mutableAttributedString.addAttribute(name, value: attributes[name] ?? "", range: range)
+                }
+            }
+        }
+        return mutableAttributedString
+    }
+    
+    private func getStringRangeArray(with textArray: Array<String>) -> Array<NSRange> {
+        var rangeArray = Array<NSRange>()
+        // 遍历
+        for str in textArray {
+            if self.string.contains(str) {
+                let subStrArr = self.string.components(separatedBy: str)
+                var subStrIndex = 0
+                for i in 0 ..< (subStrArr.count - 1) {
+                    let subDivisionStr = subStrArr[i]
+                    if i == 0 {
+                        subStrIndex += (subDivisionStr.lengthOfBytes(using: .unicode) / 2)
+                    } else {
+                        subStrIndex += (subDivisionStr.lengthOfBytes(using: .unicode) / 2 + str.lengthOfBytes(using: .unicode) / 2)
+                    }
+                    let newRange = NSRange(location: subStrIndex, length: str.count)
+                    rangeArray.append(newRange)
+                }
+            }
+        }
+        return rangeArray
+    }
+}
+
+

+ 90 - 0
QuickSearchLocation/Classes/Category/NSMutableAttributedString+Extension.swift

@@ -0,0 +1,90 @@
+//
+//  NSMutableAttributedString+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/26.
+//
+
+import Foundation
+import UIKit
+import YYText
+
+// MARK: - 一、基本的链式编程 扩展
+public extension NSMutableAttributedString {
+ 
+    // MARK: 1.1、设置 删除线
+    /// 设置 删除线
+    /// - Returns: 返回自身
+    @discardableResult
+    func strikethrough() -> Self {
+        let range = NSMakeRange(0, length)
+        addAttributes([.strikethroughStyle: NSUnderlineStyle.single.rawValue], range: range)
+        return self
+    }
+    
+    // MARK: 1.2、设置富文本文字的颜色
+    /// 设置富文本文字的颜色
+    /// - Parameter color: 富文本文字的颜色
+    /// - Returns: 返回自身
+    @discardableResult
+    func color(_ color: UIColor) -> Self {
+        let range = NSMakeRange(0, length)
+        addAttributes([.foregroundColor: color], range: range)
+        return self
+    }
+    
+    // MARK: 1.3、设置富文本文字的颜色(十六进制字符串颜色)
+    /// 设置富文本文字的颜色(十六进制字符串颜色)
+    /// - Parameter hexString: (十六进制字符串颜
+    /// - Returns: 返回自身
+    @discardableResult
+    func color(_ hexString: String) -> Self {
+        let range = NSMakeRange(0, length)
+        addAttributes([.foregroundColor: UIColor.hexStringColor(hexString: hexString)], range: range)
+        return self
+    }
+    
+    // MARK: 1.4、设置富文本文字的大小
+    /// 设置富文本文字的大小
+    /// - Parameter font: 字体的大小
+    /// - Returns: 返回自身
+    @discardableResult
+    func font(_ font: CGFloat) -> Self {
+        let range = NSMakeRange(0, length)
+        addAttributes([.font: UIFont.textF(font)], range: range)
+        return self
+    }
+    
+    // MARK: 1.5、设置富文本文字的 UIFont
+    /// 设置富文本文字的 UIFont
+    /// - Parameter font: UIFont
+    /// - Returns: 返回自身
+    @discardableResult
+    func font(_ font: UIFont) -> Self {
+        let range = NSMakeRange(0, length)
+        addAttributes([.font: font], range: range)
+        return self
+    }
+    
+    // MARK: 1.6、设置富文本文字的间距
+    /// 设置富文本文字的间距
+    /// - Parameter wordSpaceing: 字体之间的间距
+    /// - Returns: 返回自身
+    @discardableResult
+    func kern(_ wordSpaceing: CGFloat) -> Self {
+        let range = NSMakeRange(0, length)
+        addAttributes([.kern: wordSpaceing], range: range)
+        return self
+    }
+    
+    // MARK: 1.7、设置段落的样式
+    /// 设置段落的样式
+    /// - Parameter style: 样式
+    /// - Returns: 返回自身
+    @discardableResult
+    func paragraphStyle(_ style: NSMutableParagraphStyle) -> Self {
+        let range = NSMakeRange(0, length)
+        addAttributes([.paragraphStyle: style], range: range)
+        return self
+    }
+}

+ 47 - 0
QuickSearchLocation/Classes/Category/NSObject+Extension.swift

@@ -0,0 +1,47 @@
+//
+//  NSObject+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/12.
+//
+
+import UIKit
+
+// MARK: - NSObject 属性的扩展
+public extension NSObject {
+    
+    // MARK: 类名(对象方法)
+    /// 类名
+    var className: String {
+        return type(of: self).className
+    }
+    
+    // MARK: 类名(类方法)
+    /// 类名
+    static var className: String {
+        return String(describing: self)
+    }
+    
+    func rootViewController() -> UIViewController? {
+        guard let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first, let rootVC = window.rootViewController  else {
+            return nil
+        }
+        return NSObject.top(rootVC: rootVC)
+    }
+    
+    private static func top(rootVC: UIViewController?) -> UIViewController? {
+        if let presentedVC = rootVC?.presentedViewController {
+            return top(rootVC: presentedVC)
+        }
+        if let nav = rootVC as? UINavigationController,
+            let lastVC = nav.viewControllers.last {
+            return top(rootVC: lastVC)
+        }
+        if let tab = rootVC as? UITabBarController,
+            let selectedVC = tab.selectedViewController {
+            return top(rootVC: selectedVC)
+        }
+        return rootVC
+    }
+}
+

+ 220 - 0
QuickSearchLocation/Classes/Category/String+Extension.swift

@@ -0,0 +1,220 @@
+//
+//  String+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/15.
+//
+
+import Foundation
+import UIKit
+
+// MARK: - 五、字符串UI的处理
+extension String {
+    
+    // MARK: 9.1、判断是否全是空白,包括空白字符和换行符号,长度为0返回true
+    /// 判断是否全是空白,包括空白字符和换行符号,长度为0返回true
+    public var isBlank: Bool {
+        return self.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) == ""
+    }
+    
+    // MARK: 5.1、对字符串(多行)指定出字体大小和最大的 Size,获取 (Size)
+    /// 对字符串(多行)指定出字体大小和最大的 Size,获取展示的 Size
+    /// - Parameters:
+    ///   - font: 字体大小
+    ///   - size: 字符串的最大宽和高
+    /// - Returns: 按照 font 和 Size 的字符的Size
+    public func rectSize(font: UIFont, size: CGSize) -> CGSize {
+        let attributes = [NSAttributedString.Key.font: font]
+        /**
+         usesLineFragmentOrigin: 整个文本将以每行组成的矩形为单位计算整个文本的尺寸
+         usesFontLeading:
+         usesDeviceMetrics:
+         @available(iOS 6.0, *)
+         truncatesLastVisibleLine:
+         */
+        let option = NSStringDrawingOptions.usesLineFragmentOrigin
+        let rect: CGRect = self.boundingRect(with: size, options: option, attributes: attributes, context: nil)
+        return rect.size
+    }
+    
+    // MARK: 5.2、对字符串(多行)指定字体及Size,获取 (高度)
+    /// 对字符串指定字体及Size,获取 (高度)
+    /// - Parameters:
+    ///   - font: 字体的大小
+    ///   - size: 字体的size
+    /// - Returns: 返回对应字符串的高度
+    public func rectHeight(font: UIFont, size: CGSize) -> CGFloat {
+        return rectSize(font: font, size: size).height
+    }
+    
+    // MARK: 5.3、对字符串(多行)指定字体及Size,获取 (宽度)
+    /// 对字符串指定字体及Size,获取 (宽度)
+    /// - Parameters:
+    ///   - font: 字体的大小
+    ///   - size: 字体的size
+    /// - Returns: 返回对应字符串的宽度
+    public func rectWidth(font: UIFont, size: CGSize) -> CGFloat {
+        return rectSize(font: font, size: size).width
+    }
+    
+    // MARK: 5.4、对字符串(单行)指定字体,获取 (Size)
+    /// 对字符串(单行)指定字体,获取 (Size)
+    /// - Parameter font: 字体的大小
+    /// - Returns: 返回单行字符串的 size
+    public func singleLineSize(font: UIFont) -> CGSize {
+        let attrs = [NSAttributedString.Key.font: font]
+        return self.size(withAttributes: attrs as [NSAttributedString.Key: Any])
+    }
+    
+    // MARK: 5.5、对字符串(单行)指定字体,获取 (width)
+    /// 对字符串(单行)指定字体,获取 (width)
+    /// - Parameter font: 字体的大小
+    /// - Returns: 返回单行字符串的 width
+    public func singleLineWidth(font: UIFont) -> CGFloat {
+        let attrs = [NSAttributedString.Key.font: font]
+        return self.size(withAttributes: attrs as [NSAttributedString.Key: Any]).width
+    }
+    
+    // MARK: 5.6、对字符串(单行)指定字体,获取 (Height)
+    /// 对字符串(单行)指定字体,获取 (height)
+    /// - Parameter font: 字体的大小
+    /// - Returns: 返回单行字符串的 height
+    public func singleLineHeight(font: UIFont) -> CGFloat {
+        let attrs = [NSAttributedString.Key.font: font]
+        return self.size(withAttributes: attrs as [NSAttributedString.Key: Any]).height
+    }
+    
+    // MARK: 5.7、字符串通过 label 根据高度&字体 —> Size
+    /// 字符串通过 label 根据高度&字体 ——> Size
+    /// - Parameters:
+    ///   - height: 字符串最大的高度
+    ///   - font: 字体大小
+    /// - Returns: 返回Size
+    public func sizeAccording(width: CGFloat, height: CGFloat = CGFloat(MAXFLOAT), font: UIFont) -> CGSize {
+        if self.isBlank {return CGSize(width: 0, height: 0)}
+        let rect = CGRect(x: 0, y: 0, width: width, height: height)
+        let label = UILabel(frame: rect).font(font).text(self).line(0)
+        return label.sizeThatFits(rect.size)
+    }
+    
+    // MARK: 5.8、字符串通过 label 根据高度&字体 —> Width
+    /// 字符串通过 label 根据高度&字体 ——> Width
+    /// - Parameters:
+    ///   - height: 字符串最大高度
+    ///   - font: 字体大小
+    /// - Returns: 返回宽度大小
+    public func widthAccording(width: CGFloat, height: CGFloat = CGFloat(MAXFLOAT), font: UIFont) -> CGFloat {
+        if self.isBlank {return 0}
+        let rect = CGRect(x: 0, y: 0, width: width, height: height)
+        let label = UILabel(frame: rect).font(font).text(self).line(0)
+        return label.sizeThatFits(rect.size).width
+    }
+    
+    // MARK: 5.9、字符串通过 label 根据宽度&字体 —> height
+    /// 字符串通过 label 根据宽度&字体 ——> height
+    /// - Parameters:
+    ///   - width: 字符串最大宽度
+    ///   - font: 字体大小
+    /// - Returns: 返回高度大小
+    public func heightAccording(width: CGFloat, height: CGFloat = CGFloat(MAXFLOAT), font: UIFont) -> CGFloat {
+        if self.isBlank {return 0}
+        let rect = CGRect(x: 0, y: 0, width: width, height: height)
+        let label = UILabel(frame: rect).font(font).text(self).line(0)
+        return label.sizeThatFits(rect.size).height
+    }
+    
+    // MARK: 5.10、字符串根据宽度 & 字体 & 行间距 —> Size
+    /// 字符串根据宽度 & 字体 & 行间距 ——> Size
+    /// - Parameters:
+    ///   - width: 字符串最大的宽度
+    ///   - heiht: 字符串最大的高度
+    ///   - font: 字体的大小
+    ///   - lineSpacing: 行间距
+    /// - Returns: 返回对应的size
+    public func sizeAccording(width: CGFloat, height: CGFloat = CGFloat(MAXFLOAT), font: UIFont, lineSpacing: CGFloat) -> CGSize {
+        if self.isBlank {return CGSize(width: 0, height: 0)}
+        let rect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
+        let label = UILabel(frame: rect).font(font).text(self).line(0)
+        let attrStr = NSMutableAttributedString(string: self)
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineSpacing = lineSpacing
+        attrStr.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, self.count))
+        label.attributedText = attrStr
+        return label.sizeThatFits(rect.size)
+    }
+    
+    // MARK: 5.11、字符串根据宽度 & 字体 & 行间距 —> width
+    /// 字符串根据宽度 & 字体 & 行间距 ——> width
+    /// - Parameters:
+    ///   - width: 字符串最大的宽度
+    ///   - heiht: 字符串最大的高度
+    ///   - font: 字体的大小
+    ///   - lineSpacing: 行间距
+    /// - Returns: 返回对应的 width
+    public func widthAccording(width: CGFloat = CGFloat(MAXFLOAT), height: CGFloat, font: UIFont, lineSpacing: CGFloat) -> CGFloat {
+        if self.isBlank {return 0}
+        let rect = CGRect(x: 0, y: 0, width: width, height: height)
+        let label = UILabel(frame: rect).font(font).text(self).line(0)
+        let attrStr = NSMutableAttributedString(string: self)
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineSpacing = lineSpacing
+        attrStr.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, self.count))
+        label.attributedText = attrStr
+        return label.sizeThatFits(rect.size).width
+    }
+    
+    // MARK: 5.12、字符串根据宽度 & 字体 & 行间距 —> height
+    /// 字符串根据宽度 & 字体 & 行间距 ——> height
+    /// - Parameters:
+    ///   - width: 字符串最大的宽度
+    ///   - heiht: 字符串最大的高度
+    ///   - font: 字体的大小
+    ///   - lineSpacing: 行间距
+    /// - Returns: 返回对应的 height
+    public func heightAccording(width: CGFloat, height: CGFloat = CGFloat(MAXFLOAT), font: UIFont, lineSpacing: CGFloat) -> CGFloat {
+        if self.isBlank {return 0}
+        let rect = CGRect(x: 0, y: 0, width: width, height: height)
+        let label = UILabel(frame: rect).font(font).text(self).line(0)
+        let attrStr = NSMutableAttributedString(string: self)
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineSpacing = lineSpacing
+        attrStr.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, self.count))
+        label.attributedText = attrStr
+        return label.sizeThatFits(rect.size).height
+    }
+}
+
+// MARK: - Base64 编解码
+extension String {
+    
+    var decode: String {
+        get {
+            guard let decodeResult = self.base64String(encode: false) else { return "" }
+            return decodeResult
+        }
+    }
+    
+    //  Base64 编解码
+    /// Base64 编解码
+    /// - Parameter encode: true:编码 false:解码
+    /// - Returns: 编解码结果
+    func base64String(encode: Bool) -> String? {
+        guard encode else {
+            // 1.解码
+            guard let decryptionData = Data(base64Encoded: self, options: .ignoreUnknownCharacters) else {
+                return nil
+            }
+            return String(data: decryptionData, encoding: .utf8)
+        }
+        // 2.编码
+        guard let codingData = self.data(using: .utf8) else {
+            return nil
+        }
+        return codingData.base64EncodedString()
+    }
+    
+//    func decode() -> String? {
+//        
+//        return self.base64String(encode: false)
+//    }
+}

+ 49 - 0
QuickSearchLocation/Classes/Category/UIApplication+Extension.swift

@@ -0,0 +1,49 @@
+//
+//  UIApplication+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import Foundation
+import UIKit
+
+public extension UIApplication {
+    
+    //MARK: 1.1、获取当前的keyWindow
+    /// 获取当前的keyWindow
+    static var keyWindow: UIWindow? {
+        if #available(iOS 13, *) {
+            return UIApplication.shared.windows.filter { $0.isKeyWindow }.first
+        } else {
+            return UIApplication.keyWindow
+        }
+    }
+    
+    // MARK: 1.2、获取根控制器
+    /// 获取根控制器
+    /// - Parameter base: 哪个控制器为基准
+    /// - Returns: 返回 UIViewController
+    static func topViewController(_ base: UIViewController? = UIApplication.keyWindow?.rootViewController) -> UIViewController? {
+        if let nav = base as? UINavigationController {
+            return topViewController(nav.visibleViewController)
+        }
+        if let tab = base as? UITabBarController {
+            if let selected = tab.selectedViewController {
+                return topViewController(selected)
+            }
+        }
+        if let presented = base?.presentedViewController {
+            return topViewController(presented)
+        }
+        return base
+    }
+    
+    // MARK: 1.3、网络状态是否可用
+    /// 网络状态是否可用
+    static func reachable() -> Bool {
+        let data = NSData(contentsOf: URL(string: "https://www.baidu.com/")!)
+        return (data != nil)
+    }
+    
+}

+ 303 - 0
QuickSearchLocation/Classes/Category/UIButton+Extension.swift

@@ -0,0 +1,303 @@
+//
+//  UIButton+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import UIKit
+
+extension UIButton {
+    
+    enum ThemeButtonType {
+        case mainTheme
+    }
+    
+    // MARK: 1.1、创建一个带颜色的 Button, 默认为主题色,默认高度44
+    /// 创建一个带颜色的 Button
+    /// - Parameters:
+    ///   - type: 类型
+    ///   - height: 高度
+    /// - Returns: 返回自身
+    @discardableResult
+    static func normal() -> UIButton {
+        let normalColor: UIColor
+        let disabledColor: UIColor
+        let titleColorNormal: UIColor
+        let titleColorDisable: UIColor
+        
+        normalColor = QSLColor.themeMainColor
+        disabledColor = QSLColor.buttonDisabledColor
+        titleColorNormal = .white
+        titleColorDisable = .white
+        
+        let btn = UIButton(type: .custom).font(.systemFont(ofSize: 16))
+        btn.setTitleColor(titleColorNormal, for: .normal)
+        btn.setTitleColor(titleColorDisable, for: .disabled)
+        btn.setBackgroundColor(normalColor, forState: .normal)
+        btn.setBackgroundColor(disabledColor, forState: .disabled)
+        return btn
+    }
+    
+    //MARK: 1.3、设置背景色
+    /// 设置背景色
+    /// - Parameters:
+    ///   - color: 背景色
+    ///   - forState: 状态
+    func setBackgroundColor(_ color: UIColor, forState: UIControl.State) {
+        self.setBackgroundImage(backgroundImage(color), for: forState)
+    }
+    
+    private func backgroundImage(_ color: UIColor) -> UIImage? {
+        UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
+        UIGraphicsGetCurrentContext()?.setFillColor(color.cgColor)
+        UIGraphicsGetCurrentContext()?.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
+        let colorImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return colorImage
+    }
+}
+
+// MARK: - 二、链式调用
+public extension UIButton {
+    
+    // MARK: 2.1、设置title
+    /// 设置title
+    /// - Parameters:
+    ///   - text: 文字
+    ///   - state: 状态
+    /// - Returns: 返回自身
+    @discardableResult
+    func title(_ text: String, _ state: UIControl.State = .normal) -> Self {
+        setTitle(text, for: state)
+        return self
+    }
+    
+    // MARK: 2.2、设置文字颜色
+    /// 设置文字颜色
+    /// - Parameters:
+    ///   - color: 文字颜色
+    ///   - state: 状态
+    /// - Returns: 返回自身
+    @discardableResult
+    func textColor(_ color: UIColor, _ state: UIControl.State = .normal) -> Self {
+        setTitleColor(color, for: state)
+        return self
+    }
+    
+    // MARK: 2.3、设置字体大小(UIFont)
+    /// 设置字体大小
+    /// - Parameter font: 字体 UIFont
+    /// - Returns: 返回自身
+    @discardableResult
+    func font(_ font: UIFont) -> Self {
+        titleLabel?.font = font
+        return self
+    }
+    
+    // MARK: 2.4、设置字体大小(CGFloat)
+    /// 设置字体大小(CGFloat)
+    /// - Parameter fontSize: 字体的大小
+    /// - Returns: 返回自身
+    @discardableResult
+    func font(_ fontSize: CGFloat) -> Self {
+        titleLabel?.font = UIFont.textF(fontSize)
+        return self
+    }
+    
+    // MARK: 2.5、设置字体中等粗(CGFloat)
+    /// 设置字体大小(CGFloat)
+    /// - Parameter fontSize: 字体的大小
+    /// - Returns: 返回自身
+    @discardableResult
+    func mediumFont(_ fontSize: CGFloat) -> Self {
+        titleLabel?.font = UIFont.textM(fontSize)
+        return self
+    }
+    
+    // MARK: 2.6、设置字体粗体
+    /// 设置粗体
+    /// - Parameter fontSize: 设置字体粗体
+    /// - Returns: 返回自身
+    @discardableResult
+    func boldFont(_ fontSize: CGFloat) -> Self {
+        titleLabel?.font = UIFont.textB(fontSize)
+        return self
+    }
+    
+    // MARK: 2.7、设置图片
+    /// 设置图片
+    /// - Parameters:
+    ///   - image: 图片
+    ///   - state: 状态
+    /// - Returns: 返回自身
+    @discardableResult
+    func image(_ image: UIImage?, _ state: UIControl.State = .normal) -> Self {
+        setImage(image, for: state)
+        return self
+    }
+    
+}
+
+// MARK: - UIButton 图片 与 title 位置关系
+extension UIButton {
+    
+    /// 图片 和 title 的布局样式
+    enum ImageTitleLayout {
+        case imgTop
+        case imgBottom
+        case imgLeft
+        case imgRight
+    }
+    
+    // MARK: 3.1、设置图片和 title 的位置关系(提示:title和image要在设置布局关系之前设置)
+    /// 设置图片和 title 的位置关系(提示:title和image要在设置布局关系之前设置)
+    /// - Parameters:
+    ///   - layout: 布局
+    ///   - spacing: 间距
+    /// - Returns: 返回自身
+    @discardableResult
+    func setImageTitleLayout(_ layout: ImageTitleLayout, spacing: CGFloat = 0) -> UIButton {
+        switch layout {
+        case .imgLeft:
+            alignHorizontal(spacing: spacing, imageFirst: true)
+        case .imgRight:
+            alignHorizontal(spacing: spacing, imageFirst: false)
+        case .imgTop:
+            alignVertical(spacing: spacing, imageTop: true)
+        case .imgBottom:
+            alignVertical(spacing: spacing, imageTop: false)
+        }
+        return self
+    }
+    
+    /// 水平方向
+    /// - Parameters:
+    ///   - spacing: 间距
+    ///   - imageFirst: 图片是否优先
+    private func alignHorizontal(spacing: CGFloat, imageFirst: Bool) {
+        let edgeOffset = spacing / 2
+        imageEdgeInsets = UIEdgeInsets(top: 0, left: -edgeOffset,
+                                            bottom: 0,right: edgeOffset)
+        titleEdgeInsets = UIEdgeInsets(top: 0, left: edgeOffset,
+                                            bottom: 0, right: -edgeOffset)
+        if !imageFirst {
+            transform = CGAffineTransform(scaleX: -1, y: 1)
+            imageView?.transform = CGAffineTransform(scaleX: -1, y: 1)
+            titleLabel?.transform = CGAffineTransform(scaleX: -1, y: 1)
+        }
+        contentEdgeInsets = UIEdgeInsets(top: 0, left: edgeOffset, bottom: 0, right: edgeOffset)
+    }
+    
+    /// 垂直方向
+    /// - Parameters:
+    ///   - spacing: 间距
+    ///   - imageTop: 图片是不是在顶部
+    private func alignVertical(spacing: CGFloat, imageTop: Bool) {
+        
+        guard let imageWidth = self.imageView?.qsl_width,
+              let imageHeight = self.imageView?.qsl_height,
+              let text = self.titleLabel?.text,
+              let font = self.titleLabel?.font
+        else {
+            return
+        }
+        
+        let labelString = NSString(string: text)
+        let titleSize = labelString.size(withAttributes: [NSAttributedString.Key.font: font])
+        let titleHeight = titleSize.height
+        let titleWidth = titleSize.width
+        let insetAmount = spacing / 2
+        
+        if imageTop {
+            
+            imageEdgeInsets = UIEdgeInsets(top: -titleHeight - insetAmount,
+                                           left: (self.qsl_width - imageWidth) / 2,
+                                           bottom: 0,
+                                           right: (self.qsl_width - imageWidth) / 2 - titleWidth)
+            titleEdgeInsets = UIEdgeInsets(top: 0,
+                                           left: -imageWidth,
+                                           bottom: -imageWidth - insetAmount,
+                                           right: 0)
+        } else {
+            
+            imageEdgeInsets = UIEdgeInsets(top: 0,
+                                           left: (self.qsl_width - imageWidth) / 2,
+                                           bottom: -titleHeight - insetAmount,
+                                           right: (self.qsl_width - imageWidth) / 2 - titleWidth)
+            titleEdgeInsets = UIEdgeInsets(top: -imageHeight - insetAmount,
+                                           left: -imageWidth,
+                                           bottom: 0,
+                                           right: 0)
+        }
+    }
+    
+}
+
+extension UIButton {
+    
+    //倒计时
+    func countDown(_ timeOut: Int){
+        //倒计时时间
+        var timeout = timeOut
+        let queue:DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
+        let _timer:DispatchSource = DispatchSource.makeTimerSource(flags: [], queue: queue) as! DispatchSource
+        _timer.schedule(wallDeadline: DispatchWallTime.now(), repeating: .seconds(1))
+            //每秒执行
+        _timer.setEventHandler(handler: { () -> Void in
+            if(timeout<=0){ //倒计时结束,关闭
+                _timer.cancel();
+                DispatchQueue.main.sync(execute: { () -> Void in
+                    self.setTitle("重新发送", for: .normal)
+                    self.isEnabled = true
+                })
+            }else{//正在倒计时
+                let seconds = timeout
+                DispatchQueue.main.sync(execute: { () -> Void in
+                    let str = String(describing: seconds)
+                    self.setTitle("\(str)s", for: .normal)
+                    self.isEnabled = false
+                })
+                timeout -= 1;
+            }
+        })
+        _timer.resume()
+    }
+}
+
+// MARK: - 六、Button扩大点击事件
+private var UIButtonExpandSizeKey = UnsafeRawPointer("UIButtonExpandSizeKey".withCString { $0 })
+
+public extension UIButton {
+    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
+        if self.touchExtendInset == .zero || isHidden || !isEnabled {
+            return super.point(inside: point, with: event)
+        }
+        var hitFrame = bounds.inset(by: self.touchExtendInset)
+        hitFrame.size.width = max(hitFrame.size.width, 0)
+        hitFrame.size.height = max(hitFrame.size.height, 0)
+        return hitFrame.contains(point)
+    }
+}
+
+public extension UIButton {
+    
+    // MARK: 6.1、扩大UIButton的点击区域,向四周扩展10像素的点击范围
+    /// 扩大按钮点击区域 如UIEdgeInsets(top: -50, left: -50, bottom: -50, right: -50)将点击区域上下左右各扩充50
+    ///
+    /// 提示:theView 扩展点击相应区域时,其扩展的区域不能超过 superView 的 frame ,否则不会相应改点击事件;如果需要响应点击事件,需要对其 superView 进行和 theView 进行同样的处理
+    var touchExtendInset: UIEdgeInsets {
+        get {
+            if let value = objc_getAssociatedObject(self, &UIButtonExpandSizeKey) {
+                var edgeInsets: UIEdgeInsets = UIEdgeInsets.zero
+                (value as AnyObject).getValue(&edgeInsets)
+                return edgeInsets
+            } else {
+                return UIEdgeInsets.zero
+            }
+        }
+        set {
+            objc_setAssociatedObject(self, &UIButtonExpandSizeKey, NSValue(uiEdgeInsets: newValue), .OBJC_ASSOCIATION_COPY_NONATOMIC)
+        }
+    }
+}

+ 80 - 0
QuickSearchLocation/Classes/Category/UICollectionView+Extension.swift

@@ -0,0 +1,80 @@
+//
+//  UICollectionView+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/16.
+//
+
+import UIKit
+
+// MARK: - 二、滚动和注册
+public extension UICollectionView {
+    
+    // MARK: 2.1、是否滚动到顶部
+    /// 是否滚动到顶部
+    /// - Parameter animated: 是否要动画
+    func scrollToTop(animated: Bool) {
+        setContentOffset(CGPoint(x: 0, y: 0), animated: animated)
+    }
+    
+    // MARK: 2.2、是否滚动到底部
+    /// 是否滚动到底部
+    /// - Parameter animated: 是否要动画
+    func scrollToBottom(animated: Bool) {
+        let y = contentSize.height - frame.size.height
+        if y < 0 { return }
+        setContentOffset(CGPoint(x: 0, y: y), animated: animated)
+    }
+    
+    // MARK: 2.3、滚动到什么位置(CGPoint)
+    /// 滚动到什么位置(CGPoint)
+    /// - Parameter animated: 是否要动画
+    func scrollToOffset(offsetX: CGFloat = 0, offsetY: CGFloat = 0, animated: Bool) {
+        setContentOffset(CGPoint(x: offsetX, y: offsetY), animated: animated)
+    }
+    
+    // MARK: 2.4、注册自定义cell
+    /// 注册自定义cell
+    /// - Parameter cellClass: UICollectionViewCell类型
+    func register(cellClass: UICollectionViewCell.Type) {
+        register(cellClass.self, forCellWithReuseIdentifier: cellClass.className)
+    }
+    
+    // MARK: 2.5、注册Xib自定义cell
+    /// 注册Xib自定义cell
+    /// - Parameter nib: nib description
+    func register(nib: UINib) {
+        register(nib, forCellWithReuseIdentifier: nib.className)
+    }
+    
+    // MARK: 2.6、创建UICollectionViewCell(注册后使用该方法)
+    /// 创建UICollectionViewCell(注册后使用该方法)
+    /// - Parameters:
+    ///   - cellType: UICollectionViewCell类型
+    ///   - indexPath: indexPath description
+    /// - Returns: 返回UICollectionViewCell类型
+    func dequeueReusableCell<T: UICollectionViewCell>(cellType: T.Type, cellForRowAt indexPath: IndexPath) -> T {
+        return dequeueReusableCell(withReuseIdentifier: cellType.className, for: indexPath) as! T
+    }
+    
+    // MARK: 2.7、注册自定义: Section 的Header或者Footer
+    /// 注册自定义: Section 的Header或者Footer
+    /// - Parameters:
+    ///   - reusableView: UICollectionReusableView类
+    ///   - elementKind: elementKind: header:UICollectionView.elementKindSectionHeader  还是 footer:UICollectionView.elementKindSectionFooter
+    func registerCollectionReusableView(reusableView: UICollectionReusableView.Type, forSupplementaryViewOfKind elementKind: String) {
+        register(reusableView.self, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: reusableView.className)
+    }
+    
+    // MARK: 2.8、创建Section 的Header或者Footer(注册后使用该方法)
+    /// 创建Section 的Header或者Footer(注册后使用该方法)
+    /// - Parameters:
+    ///   - reusableView: UICollectionReusableView类
+    ///   - collectionView: collectionView
+    ///   - elementKind:  header:UICollectionView.elementKindSectionHeader  还是 footer:UICollectionView.elementKindSectionFooter
+    ///   - indexPath: indexPath description
+    /// - Returns: 返回UICollectionReusableView类型
+    func dequeueReusableSupplementaryView<T: UICollectionReusableView>(reusableView: T.Type, in collectionView: UICollectionView, ofKind elementKind: String, for indexPath: IndexPath) -> T {
+        return collectionView.dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: reusableView.className, for: indexPath) as! T
+    }
+}

+ 136 - 0
QuickSearchLocation/Classes/Category/UIColor+Extension.swift

@@ -0,0 +1,136 @@
+//
+//  UIColor+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+// MARK: - 二、使用方法设置颜色
+public extension UIColor {
+    
+    // MARK: 2.1、根据RGBA的颜色(方法)
+    /// 根据RGBA的颜色(方法)
+    /// - Parameters:
+    ///   - r: red 颜色值
+    ///   - g: green颜色值
+    ///   - b: blue颜色值
+    ///   - alpha: 透明度
+    /// - Returns: 返回 UIColor
+    static func color(r: CGFloat, g: CGFloat, b: CGFloat, alpha: CGFloat = 1.0) -> UIColor {
+        return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: alpha)
+    }
+    
+    // MARK: 2.2、十六进制字符串设置颜色(方法)
+    static func hexStringColor(hexString: String, alpha: CGFloat = 1.0) -> UIColor {
+        let newColor = hexStringToColorRGB(hexString: hexString)
+        guard let r = newColor.r, let g = newColor.g, let b = newColor.b else {
+            assert(false, "颜色值有误")
+            return .white
+        }
+        return color(r: r, g: g, b: b, alpha: alpha)
+    }
+    
+    // MARK: 2.3、十六进制 Int 颜色的使用(方法)
+    /// 十六进制颜色的使用
+    /// - Parameters:
+    ///   - color: 16进制 Int 颜色 0x999999
+    ///   - alpha: 透明度
+    /// - Returns: 返回一个 UIColor
+    static func hexIntColor(hexInt: Int, alpha: CGFloat = 1) -> UIColor {
+        let redComponet: Float = Float(hexInt >> 16)
+        let greenComponent: Float = Float((hexInt & 0xFF00) >> 8)
+        let blueComponent: Float = Float(hexInt & 0xFF)
+        return UIColor(red: CGFloat(redComponet / 255.0), green: CGFloat(greenComponent / 255.0), blue: CGFloat(blueComponent / 255.0), alpha: alpha)
+    }
+}
+
+private extension UIColor {
+    
+    // MARK: 3.1、根据 十六进制字符串 颜色获取 RGB,如:#3CB371 或者 ##3CB371 -> 60,179,113
+    /// 根据 十六进制字符串 颜色获取 RGB
+    /// - Parameter hexString: 十六进制颜色的字符串,如:#3CB371 或者 ##3CB371 -> 60,179,113
+    /// - Returns: 返回 RGB
+    static func hexStringToColorRGB(hexString: String) -> (r: CGFloat?, g: CGFloat?, b: CGFloat?) {
+        // 1、判断字符串的长度是否符合
+        guard hexString.count >= 6 else {
+            return (nil, nil, nil)
+        }
+        // 2、将字符串转成大写
+        var tempHex = hexString.uppercased()
+        // 检查字符串是否拥有特定前缀
+        // hasPrefix(prefix: String)
+        // 检查字符串是否拥有特定后缀。
+        // hasSuffix(suffix: String)
+        // 3、判断开头: 0x/#/##
+        if tempHex.hasPrefix("0x") || tempHex.hasPrefix("0X") || tempHex.hasPrefix("##") {
+            tempHex = String(tempHex[tempHex.index(tempHex.startIndex, offsetBy: 2)..<tempHex.endIndex])
+        }
+        if tempHex.hasPrefix("#") {
+            tempHex = String(tempHex[tempHex.index(tempHex.startIndex, offsetBy: 1)..<tempHex.endIndex])
+        }
+        // 4、分别取出 RGB
+        // FF --> 255
+        var range = NSRange(location: 0, length: 2)
+        let rHex = (tempHex as NSString).substring(with: range)
+        range.location = 2
+        let gHex = (tempHex as NSString).substring(with: range)
+        range.location = 4
+        let bHex = (tempHex as NSString).substring(with: range)
+        // 5、将十六进制转成 255 的数字
+        var r: UInt64 = 0, g: UInt64 = 0, b: UInt64 = 0
+        Scanner(string: rHex).scanHexInt64(&r)
+        Scanner(string: gHex).scanHexInt64(&g)
+        Scanner(string: bHex).scanHexInt64(&b)
+        return (r: CGFloat(r), g: CGFloat(g), b: CGFloat(b))
+    }
+    
+    // MARK: 3.2、根据 十六进制值 颜色获取 RGB, 如:0x3CB371 -> 60,179,113
+    /// 根据 十六进制值 颜色获取 RGB, 如:0x3CB371 -> 60,179,113
+    /// - Parameter hexInt: 十六进制值,如:0x3CB37
+    /// - Returns: 返回 RGB
+    static func hexIntToColorRGB(hexInt: Int) -> (r: CGFloat, g: CGFloat, b: CGFloat) {
+        let red: CGFloat = CGFloat(hexInt >> 16)
+        let green: CGFloat = CGFloat((hexInt & 0xFF00) >> 8)
+        let blue: CGFloat = CGFloat(hexInt & 0xFF)
+        return (red, green, blue)
+    }
+}
+
+extension UIColor {
+    
+    enum ColorGradientDirection {
+        case Vertical
+        case Horizontal
+    }
+    
+//    func gradientColor(fromColor: UIColor, toColor: UIColor, distance:CGFloat, direction:ColorGradientDirection) -> UIColor {
+//        
+//        var size = CGSize(width: distance, height: 1)
+//        if direction == .Vertical {
+//            size = CGSize(width: 1, height: distance)
+//        }
+//        UIGraphicsBeginImageContextWithOptions(size, false, 0)
+//        guard let context = UIGraphicsGetCurrentContext() else { return UIColor() }
+//        let colorspace = CGColorSpaceCreateDeviceRGB()
+//        
+//        let colors = NSArray(objects: fromColor.cgColor, toColor.cgColor, (Any).self)
+//        
+//        guard let gradient = CGGradient(colorsSpace: colorspace, colors: colors, locations: nil) else { return UIColor()
+//        }
+//        if direction == .Vertical {
+//            context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPointMake(0, size.height), options: CGGradientDrawingOptions())
+//        } else {
+//            context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPointMake(0, size.height), options: CGGradientDrawingOptions())
+//        }
+//        
+//        let image = UIGraphicsGetImageFromCurrentImageContext()
+//        
+//        CGGradientRelease(gradient);
+//        CGColorSpaceRelease(colorspace);
+//        UIGraphicsEndImageContext();
+//        
+//        return
+//    }
+}

+ 97 - 0
QuickSearchLocation/Classes/Category/UIFont+Extension.swift

@@ -0,0 +1,97 @@
+//
+//  UIFont+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+// MARK: - 常用的系统基本字体扩展
+extension UIFont {
+    
+    // MARK: 1.1、默认字体
+    /// 默认字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textF(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .regular)
+    }
+    
+    // MARK: 1.2、常规字体
+    /// 常规字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textR(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .regular)
+    }
+    
+    // MARK: 1.3、中等的字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textM(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .medium)
+    }
+    
+    // MARK: 1.4、加粗的字体
+    /// 加粗的字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textB(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .bold)
+    }
+    
+    // MARK: 1.5、半粗体的字体
+    /// 半粗体的字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textSB(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .semibold)
+    }
+    
+    // MARK: 1.6、超细的字体
+    /// 超细的字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textUltraLight(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .ultraLight)
+    }
+    
+    // MARK: 1.7、纤细的字体
+    /// 纤细的字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textThin(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .thin)
+    }
+    
+    // MARK: 1.8、亮字体
+    /// 亮字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textLight(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .light)
+    }
+    
+    // MARK: 1.9、介于Bold和Black之间
+    /// 介于Bold和Black之间
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textHeavy(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .heavy)
+    }
+    
+    // MARK: 1.10、最粗字体
+    /// 最粗字体
+    /// - Parameter ofSize: 字体大小
+    /// - Returns: 字体
+    static func textBlack(_ ofSize: CGFloat) -> UIFont {
+        return text(ofSize, W: .black)
+    }
+    
+    /// 文字字体
+    fileprivate static func text(_ ofSize: CGFloat, W Weight: UIFont.Weight) -> UIFont {
+        let scaleSize = ofSize.rpx
+        return UIFont.systemFont(ofSize: scaleSize, weight: Weight)
+    }
+}

+ 136 - 0
QuickSearchLocation/Classes/Category/UIImage+Extension.swift

@@ -0,0 +1,136 @@
+//
+//  UIImage+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+public enum ImageGradientDirection {
+    /// 水平从左到右
+    case horizontal
+    /// 垂直从上到下
+    case vertical
+    /// 左上到右下
+    case leftOblique
+    /// 右上到左下
+    case rightOblique
+    /// 自定义
+    case other(CGPoint, CGPoint)
+    
+    public func point(size: CGSize) -> (CGPoint, CGPoint) {
+        switch self {
+        case .horizontal:
+            return (CGPoint(x: 0, y: 0), CGPoint(x: size.width, y: 0))
+        case .vertical:
+            return (CGPoint(x: 0, y: 0), CGPoint(x: 0, y: size.height))
+        case .leftOblique:
+            return (CGPoint(x: 0, y: 0), CGPoint(x: size.width, y: size.height))
+        case .rightOblique:
+            return (CGPoint(x: size.width, y: 0), CGPoint(x: 0, y: size.height))
+        case .other(let stat, let end):
+            return (stat, end)
+        }
+    }
+}
+
+extension UIImage {
+    
+    // MARK: 2.1、生成指定尺寸的纯色图像
+    /// 生成指定尺寸的纯色图像
+    /// - Parameters:
+    ///   - color: 图片颜色
+    ///   - size: 图片尺寸
+    /// - Returns: 返回对应的图片
+    static func image(color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0)) -> UIImage? {
+        return image(color: color, size: size, corners: .allCorners, radius: 0)
+    }
+
+    // MARK: 2.2、生成指定尺寸和圆角的纯色图像
+    /// 生成指定尺寸和圆角的纯色图像
+    /// - Parameters:
+    ///   - color: 图片颜色
+    ///   - size: 图片尺寸
+    ///   - corners: 剪切的方式
+    ///   - round: 圆角大小
+    /// - Returns: 返回对应的图片
+    static func image(color: UIColor, size: CGSize, corners: UIRectCorner, radius: CGFloat) -> UIImage? {
+        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
+        // 防止size:(0, 0)崩溃
+        var drawSize = size
+        if drawSize.width <= 0 || drawSize.height <= 0 {
+            drawSize = CGSize(width: 1, height: 1)
+        }
+        UIGraphicsBeginImageContextWithOptions(drawSize, false, UIScreen.main.scale)
+        let context = UIGraphicsGetCurrentContext()
+        if radius > 0 {
+            let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
+            color.setFill()
+            path.fill()
+        } else {
+            context?.setFillColor(color.cgColor)
+            context?.fill(rect)
+        }
+        let img = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return img
+    }
+    
+    // MARK: 2.3、生成渐变色的图片 ["#B0E0E6", "#00CED1", "#2E8B57"]
+    /// 生成渐变色的图片 ["#B0E0E6", "#00CED1", "#2E8B57"]
+    /// - Parameters:
+    ///   - hexsString: 十六进制字符数组
+    ///   - size: 图片大小
+    ///   - locations: locations 数组
+    ///   - direction: 渐变的方向
+    /// - Returns: 渐变的图片
+    static func gradient(_ hexsString: [String], size: CGSize = CGSize(width: 1, height: 1), locations:[CGFloat]? = nil, direction: ImageGradientDirection = .horizontal) -> UIImage? {
+        return gradient(hexsString.map{ UIColor.hexStringColor(hexString: $0) }, size: size, locations: locations, direction: direction)
+    }
+    
+    // MARK: 2.4、生成渐变色的图片 [UIColor, UIColor, UIColor]
+    /// 生成渐变色的图片 [UIColor, UIColor, UIColor]
+    /// - Parameters:
+    ///   - colors: UIColor 数组
+    ///   - size: 图片大小
+    ///   - locations: locations 数组
+    ///   - direction: 渐变的方向
+    /// - Returns: 渐变的图片
+    static func gradient(_ colors: [UIColor], size: CGSize = CGSize(width: 10, height: 10), locations:[CGFloat]? = nil, direction: ImageGradientDirection = .horizontal) -> UIImage? {
+        return gradient(colors, size: size, radius: 0, locations: locations, direction: direction)
+    }
+    
+    // MARK: 2.5、生成带圆角渐变色的图片 [UIColor, UIColor, UIColor]
+    /// 生成带圆角渐变色的图片 [UIColor, UIColor, UIColor]
+    /// - Parameters:
+    ///   - colors: UIColor 数组
+    ///   - size: 图片大小
+    ///   - radius: 圆角
+    ///   - locations: locations 数组
+    ///   - direction: 渐变的方向
+    /// - Returns: 带圆角的渐变的图片
+    static func gradient(_ colors: [UIColor],
+                         size: CGSize = CGSize(width: 10, height: 10),
+                         radius: CGFloat,
+                         locations:[CGFloat]? = nil,
+                         direction: ImageGradientDirection = .horizontal) -> UIImage? {
+        if colors.count == 0 { return nil }
+        if colors.count == 1 {
+            return image(color: colors[0])
+        }
+        UIGraphicsBeginImageContext(size)
+        let context = UIGraphicsGetCurrentContext()
+        let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height), cornerRadius: radius)
+        path.addClip()
+        context?.addPath(path.cgPath)
+        guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors.map{$0.cgColor} as CFArray, locations: locations?.map { CGFloat($0) }) else { return nil
+        }
+        let directionPoint = direction.point(size: size)
+        context?.drawLinearGradient(gradient, start: directionPoint.0, end: directionPoint.1, options: .drawsBeforeStartLocation)
+        
+        let image = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return image
+    }
+}

+ 130 - 0
QuickSearchLocation/Classes/Category/UILabel+Extension.swift

@@ -0,0 +1,130 @@
+//
+//  UILabel+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import UIKit
+
+extension UILabel {
+    
+    // MARK: 设置文字
+    /// 设置文字
+    /// - Parameter text: 文字内容
+    /// - Returns: 返回自身
+    @discardableResult
+    func text(_ text: String) -> Self {
+        self.text = text
+        return self
+    }
+    
+    // MARK: 设置文字行数
+    /// 设置文字行数
+    /// - Parameter number: 行数
+    /// - Returns: 返回自身
+    @discardableResult
+    func line(_ number: Int) -> Self {
+        numberOfLines = number
+        return self
+    }
+    
+    // MARK: 设置文本颜色(十六进制字符串)
+    /// 设置文本颜色(十六进制字符串)
+    /// - Parameter hex: 十六进制字符串
+    /// - Returns: 返回自身
+    @discardableResult
+    func color(_ hex: String) -> Self {
+        textColor = UIColor.hexStringColor(hexString: hex)
+        return self
+    }
+    
+    // MARK: 设置字体的大小
+    /// 设置字体的大小
+    /// - Parameter fontSize: 字体的大小
+    /// - Returns: 返回自身
+    @discardableResult
+    func font(_ font: UIFont) -> Self {
+        self.font = font
+        return self
+    }
+    
+    // MARK: 设置字体的大小
+    /// 设置字体的大小
+    /// - Parameter fontSize: 字体的大小
+    /// - Returns: 返回自身
+    @discardableResult
+    func font(_ fontSize: CGFloat) -> Self {
+        font = UIFont.textF(fontSize)
+        return self
+    }
+    
+    // MARK: 设置字体的大小(中等)
+    /// 设置字体的大小(中等)
+    /// - Parameter fontSize: 字体的大小
+    /// - Returns: 返回自身
+    @discardableResult
+    func mediumFont(_ fontSize: CGFloat) -> Self {
+        font = UIFont.textM(fontSize)
+        return self
+    }
+    
+    // MARK: 设置字体的大小(粗体)
+    /// 设置字体的大小(粗体)
+    /// - Parameter fontSize: 字体的大小
+    /// - Returns: 返回自身
+    @discardableResult
+    func boldFont(_ fontSize: CGFloat) -> Self {
+        font = UIFont.textB(fontSize)
+        return self
+    }
+    
+    // MARK: 改变行间距
+    /// 改变行间距
+    /// - Parameter space: 行间距大小
+    func changeLineSpace(space: CGFloat) {
+        if self.text == nil || self.text == "" {
+            return
+        }
+        let text = self.text
+        let attributedString = NSMutableAttributedString(string: text!)
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineSpacing = space
+        paragraphStyle.alignment = self.textAlignment
+        attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: .init(location: 0, length: text!.count))
+        self.attributedText = attributedString
+        self.sizeToFit()
+    }
+    
+    // MARK: label添加中划线
+    /// label添加中划线
+    /// - Parameters:
+    ///   - lineValue: value 越大,划线越粗
+    ///   - underlineColor: 中划线的颜色
+    func centerLineText(lineValue: Int = 1, underlineColor: UIColor = .black) {
+        guard let content = self.text else {
+            return
+        }
+        let arrText = NSMutableAttributedString(string: content)
+        arrText.addAttributes([NSAttributedString.Key.strikethroughStyle: lineValue, NSAttributedString.Key.strikethroughColor: underlineColor], range: NSRange(location: 0, length: arrText.length))
+        self.attributedText = arrText
+    }
+}
+
+extension UILabel {
+    
+    func setRangeFontText(font: UIFont, range: NSRange) {
+        let attributedString = self.attributedText?.setRangeFontText(font: font, range: range)
+        self.attributedText = attributedString
+    }
+    
+    func setSpecificTextColor(_ text: String, color: UIColor) {
+        let attributedString = self.attributedText?.setSpecificTextColor(text, color: color)
+        self.attributedText = attributedString
+    }
+    
+    func setSpecificTextColorFont(_ text: String, color: UIColor, font: UIFont) {
+        let attributedString = self.attributedText?.setSpecificTextColorFont(text, color: color, font: font)
+        self.attributedText = attributedString
+    }
+}

+ 60 - 0
QuickSearchLocation/Classes/Category/UITabBarController+Extension.swift

@@ -0,0 +1,60 @@
+//
+//  UITabBarController+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+extension UITabBarController {
+    
+    /// 设置 tabbar 的样式
+    func addChildViewController(childVc: UIViewController,
+                                title: String,
+                                image: String,
+                                selectedImage: String,
+                                imageInsets: UIEdgeInsets,
+                                titlePosition: UIOffset,
+                                index: Int)
+    {
+        self .configureChildViewController(childVc: childVc, title: title, image: image, selectedImage: selectedImage, imageInsets: imageInsets, titlePosition: titlePosition, index: index)
+        let nav:QSLBaseNavController = QSLBaseNavController.init(rootViewController: childVc)
+        self.addChild(nav)
+    }
+    
+    func configureChildViewController(childVc: UIViewController,
+                                      title: String,
+                                      image: String,
+                                      selectedImage: String,
+                                      imageInsets: UIEdgeInsets,
+                                      titlePosition: UIOffset,
+                                      index: Int) {
+        
+        childVc.tabBarItem.tag = index
+        // 同时设置tabbar和navigationBar的文字
+        childVc.title = title
+        childVc.tabBarItem.title = title
+        // 设置子控制器的图片
+        childVc.tabBarItem.image = UIImage(named: image)
+        //声明显示图片的原始式样 不要渲染
+        childVc.tabBarItem.selectedImage = UIImage(named: selectedImage)?.withRenderingMode(.alwaysOriginal)
+        childVc.tabBarItem.imageInsets = imageInsets
+        childVc.tabBarItem.titlePositionAdjustment = titlePosition
+        
+        // 设置文字颜色
+        var selectedDict:[NSAttributedString.Key : Any] = [NSAttributedString.Key : Any]()
+        selectedDict[NSAttributedString.Key.foregroundColor] = QSLColor.themeMainColor;
+        childVc.tabBarItem.setTitleTextAttributes(selectedDict, for: .selected)
+        self.tabBar.tintColor = QSLColor.themeMainColor;
+        
+        var normalDict:[NSAttributedString.Key : Any] = [NSAttributedString.Key : Any]()
+        normalDict[NSAttributedString.Key.foregroundColor] = QSLColor.Color_888
+        normalDict[NSAttributedString.Key.font] = UIFont.textF(10)
+        childVc.tabBarItem.setTitleTextAttributes(normalDict, for: .normal)
+
+        if #available(iOS 13.0, *) {
+            childVc.tabBarController?.tabBar.unselectedItemTintColor = UIColor.clear
+        }
+    }
+}

+ 40 - 0
QuickSearchLocation/Classes/Category/UITableView+Extension.swift

@@ -0,0 +1,40 @@
+//
+//  UITableView+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/12.
+//
+
+import UIKit
+
+// MARK: - 基础扩展
+extension UITableView {
+    
+    // MARK: tableView 在 iOS 11 上的适配
+    /// tableView 在 iOS 11 上的适配
+    func tableViewNeverAdjustContentInset() {
+        if #available(iOS 11, *) {
+            self.estimatedRowHeight = 0
+            self.estimatedSectionFooterHeight = 0
+            self.estimatedSectionHeaderHeight = 0
+            self.contentInsetAdjustmentBehavior = .never
+        }
+    }
+    
+    // MARK: 注册自定义cell
+    /// 注册自定义cell
+    /// - Parameter cellClass: UITableViewCell类型
+    func register(cellClass: UITableViewCell.Type) {
+        self.register(cellClass.self, forCellReuseIdentifier: cellClass.className)
+    }
+    
+    // MARK: 创建UITableViewCell(注册后使用该方法)
+    /// 创建UITableViewCell(注册后使用该方法)
+    /// - Parameters:
+    ///   - cellType: UITableViewCell类型
+    ///   - indexPath: indexPath description
+    /// - Returns: 返回UITableViewCell类型
+    func dequeueReusableCell<T: UITableViewCell>(cellType: T.Type, cellForRowAt indexPath: IndexPath) -> T {
+        return self.dequeueReusableCell(withIdentifier: cellType.className, for: indexPath) as! T
+    }
+}

+ 82 - 0
QuickSearchLocation/Classes/Category/UITextField+Extension.swift

@@ -0,0 +1,82 @@
+//
+//  UITextField+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/12.
+//
+
+import UIKit
+
+extension UITextField {
+    
+    // MARK: 是否都是数字
+    /// 是否都是数字
+    /// - Returns: 返回结果
+    func validateDigits() -> Bool {
+        let digitsRegEx = "[0-9]*"
+        let digitsTest = NSPredicate(format: "SELF MATCHES %@", digitsRegEx)
+        return digitsTest.evaluate(with: self.text)
+    }
+    
+    
+    // MARK: 设置富文本的占位符
+    /// 设置富文本的占位符
+    /// - Parameters:
+    ///   - font: 字体的大小
+    ///   - color: 字体的颜色
+    func setPlaceholderAttribute(font: UIFont, color: UIColor = .black) {
+        let arrStr = NSMutableAttributedString(string: self.placeholder ?? "", attributes: [NSAttributedString.Key.foregroundColor: color, NSAttributedString.Key.font: font])
+        self.attributedPlaceholder = arrStr
+    }
+}
+
+/// 默认最大输入字数为15
+var maxTextNumberDefault = 11
+// MARK: 设置最大输入字数
+extension UITextField {
+    
+        /// 以runtime的形式UITextField添加最大输入字数属性
+        public var maxTextNumber: Int {
+            set {
+                objc_setAssociatedObject(self, &maxTextNumberDefault, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
+                self.addChangeTextTarget()
+            }
+            get {
+                if let rs = objc_getAssociatedObject(self, &maxTextNumberDefault) as? Int {
+                    return rs
+                }
+                return 15
+            }
+        }
+    
+        /// 添加限制最大输入字数target
+        public func addChangeTextTarget(){
+            self.addTarget(self, action: #selector(changeText), for: .editingChanged)
+        }
+    
+        @objc private func changeText(){
+            //判断是不是在拼音状态,拼音状态不截取文本
+            if let positionRange = self.markedTextRange{
+                guard self.position(from: positionRange.start, offset: 0) != nil else {
+                    checkTextFieldText()
+                    return
+                }
+            }else {
+                checkTextFieldText()
+            }
+        }
+    
+        /// 检测如果输入数高于设置最大输入数则截取
+        private func checkTextFieldText(){
+            guard (self.text?.utf16.count)! <= maxTextNumber  else {
+                guard let text = self.text else {
+                    return
+                }
+                /// emoji的utf16.count是2,所以不能以maxTextNumber进行截取,改用text.count-1
+                let sIndex = text.index(text
+                    .startIndex, offsetBy: text.count-1)
+                self.text = String(text[..<sIndex])
+                return
+            }
+        }
+}

+ 501 - 0
QuickSearchLocation/Classes/Category/UIView+Extension.swift

@@ -0,0 +1,501 @@
+//
+//  UIView+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import Foundation
+import UIKit
+import Toast_Swift
+
+// MARK: - 屏幕尺寸常用的常量
+public extension UIView {
+    
+    // MARK: 1.1、是否是缺口屏幕(刘海屏)或者灵动岛的屏幕
+    // 是否是缺口屏幕(刘海屏)或者灵动岛的屏幕
+    var qsl_isIPhoneNotch: Bool {
+        if #available(iOS 11.0, *) {
+            if let window = UIApplication.keyWindow {
+                return window.safeAreaInsets.bottom > 0
+            } else {
+                return false
+            }
+        } else {
+            return UIApplication.shared.statusBarFrame.height > 20
+        }
+    }
+
+    // MARK: 2.1、屏幕的宽
+    /// 屏幕的宽
+    var qsl_kScreenW: CGFloat { return UIScreen.main.bounds.width }
+
+    // MARK: 2.2、屏幕的高
+    /// 屏幕的高
+    var qsl_kScreenH: CGFloat { return UIScreen.main.bounds.height }
+
+    // MARK: 2.3、获取statusBar的高度
+    /// 获取statusBar的高度
+    var qsl_kStatusBarFrameH: CGFloat {
+        if #available(iOS 13.0, *) {
+            let window: UIWindow? = UIApplication.shared.windows.first
+            let statusBarHeight = (window?.windowScene?.statusBarManager?.statusBarFrame.height) ?? 0
+            return statusBarHeight
+        } else {
+            // 防止界面没有出来获取为0的情况
+            return UIApplication.shared.statusBarFrame.height > 0 ? UIApplication.shared.statusBarFrame.height : 44
+        }
+    }
+
+    // MARK: 2.4、获取导航栏的高度
+    /// 获取导航栏的高度
+    var qsl_kNavFrameH: CGFloat { return 44 + qsl_kStatusBarFrameH }
+        
+    // MARK: 2.5、屏幕底部Tabbar高度
+    /// 屏幕底部Tabbar高度
+    var qsl_kTabbarFrameH: CGFloat { return qsl_isIPhoneNotch ? 83 : 49 }
+
+    // MARK: 2.6、屏幕底部刘海高度
+    /// 屏幕底部刘海高度
+    var qsl_kTabbarBottom: CGFloat { return qsl_isIPhoneNotch ? 34 : 0 }
+
+    // MARK: 2.7、屏幕比例
+    /// 屏幕比例
+    var qsl_kPixel: CGFloat { return  1.0 / UIScreen.main.scale }
+    
+    var qsl_kScale: CGFloat { return qsl_kScreenW / CGFloat(375.0) }
+    
+}
+
+// MARK: - UIView 有关 Frame 的扩展
+public extension UIView {
+    // MARK: 3.1、x 的位置
+    /// x 的位置
+    var qsl_x: CGFloat {
+        get {
+            return frame.origin.x
+        }
+        set(newValue) {
+            var tempFrame: CGRect = frame
+            tempFrame.origin.x = newValue
+            frame = tempFrame
+        }
+    }
+    // MARK: 3.2、y 的位置
+    /// y 的位置
+    var qsl_y: CGFloat {
+        get {
+            return frame.origin.y
+        }
+        set(newValue) {
+            var tempFrame: CGRect = frame
+            tempFrame.origin.y = newValue
+            frame = tempFrame
+        }
+    }
+    
+    // MARK: 3.3、height: 视图的高度
+    /// height: 视图的高度
+    var qsl_height: CGFloat {
+        get {
+            return frame.size.height
+        }
+        set(newValue) {
+            var tempFrame: CGRect = frame
+            tempFrame.size.height = newValue
+            frame = tempFrame
+        }
+    }
+    
+    // MARK: 3.4、width: 视图的宽度
+    /// width: 视图的宽度
+    var qsl_width: CGFloat {
+        get {
+            return frame.size.width
+        }
+        set(newValue) {
+            var tempFrame: CGRect = frame
+            tempFrame.size.width = newValue
+            frame = tempFrame
+        }
+    }
+    
+    // MARK: 3.5、size: 视图的zize
+    /// size: 视图的zize
+    var qsl_size: CGSize {
+        get {
+            return frame.size
+        }
+        set(newValue) {
+            var tempFrame: CGRect = frame
+            tempFrame.size = newValue
+            frame = tempFrame
+        }
+    }
+    
+    // MARK: 3.6、centerX: 视图的X中间位置
+    /// centerX: 视图的X中间位置
+    var qsl_centerX: CGFloat {
+        get {
+            return center.x
+        }
+        set(newValue) {
+            var tempCenter: CGPoint = center
+            tempCenter.x = newValue
+            center = tempCenter
+        }
+    }
+    
+    // MARK: 3.7、centerY: 视图的Y中间位置
+    /// centerY: 视图Y的中间位置
+    var qsl_centerY: CGFloat {
+        get {
+            return center.y
+        }
+        set(newValue) {
+            var tempCenter: CGPoint = center
+            tempCenter.y = newValue
+            center = tempCenter
+        }
+    }
+    
+    // MARK: 3.9、top 上端横坐标(y)
+    /// top 上端横坐标(y)
+    var qsl_top: CGFloat {
+        get {
+            return frame.origin.y
+        }
+        set(newValue) {
+            var tempFrame: CGRect = frame
+            tempFrame.origin.y = newValue
+            frame = tempFrame
+        }
+    }
+    
+    // MARK: 3.10、left 左端横坐标(x)
+    /// left 左端横坐标(x)
+    var qsl_left: CGFloat {
+        get {
+            return frame.origin.x
+        }
+        set(newValue) {
+            var tempFrame: CGRect = frame
+            tempFrame.origin.x = newValue
+            frame = tempFrame
+        }
+    }
+    
+    // MARK: 3.11、bottom 底端纵坐标 (y + height)
+    /// bottom 底端纵坐标 (y + height)
+    var qsl_bottom: CGFloat {
+        get {
+            return frame.origin.y + frame.size.height
+        }
+        set(newValue) {
+            frame.origin.y = newValue - frame.size.height
+        }
+    }
+    
+    // MARK: 3.12、right 底端纵坐标 (x + width)
+    /// right 底端纵坐标 (x + width)
+    var qsl_right: CGFloat {
+        get {
+            return frame.origin.x + frame.size.width
+        }
+        set(newValue) {
+            frame.origin.x = newValue - frame.size.width
+        }
+    }
+    
+    // MARK: 3.13、origin 点
+    /// origin 点
+    var qsl_origin: CGPoint {
+        get {
+            return frame.origin
+        }
+        set(newValue) {
+            var tempOrigin: CGPoint = frame.origin
+            tempOrigin = newValue
+            frame.origin = tempOrigin
+        }
+    }
+}
+
+// MARK: - 关于UIView的 圆角、阴影、边框、虚线 的设置
+public extension UIView {
+    
+    // MARK: 5.1、添加圆角
+    /// 添加圆角
+    /// - Parameters:
+    ///   - radius: 圆角的大小
+    func addRadius(radius:CGFloat) {
+        
+        self.layer.cornerRadius = radius
+        self.clipsToBounds = true
+    }
+    
+    // MARK: 5.1、添加圆角
+    /// 添加圆角
+    /// - Parameters:
+    ///   - conrners: 具体哪个圆角
+    ///   - radius: 圆角的大小
+    func addCorner(conrners: UIRectCorner , radius: CGFloat) {
+        let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: conrners, cornerRadii: CGSize(width: radius, height: radius))
+        let maskLayer = CAShapeLayer()
+        maskLayer.frame = self.bounds
+        maskLayer.path = maskPath.cgPath
+        self.layer.mask = maskLayer
+    }
+    
+    // MARK: 5.2、添加圆角和边框
+    /// 添加圆角和边框
+    /// - Parameters:
+    ///   - conrners: 具体哪个圆角
+    ///   - radius: 圆角的大小
+    ///   - borderWidth: 边框的宽度
+    ///   - borderColor: 边框的颜色
+    func addCorner(conrners: UIRectCorner , radius: CGFloat, borderWidth: CGFloat, borderColor: UIColor) {
+        let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: conrners, cornerRadii: CGSize(width: radius, height: radius))
+        let maskLayer = CAShapeLayer()
+        maskLayer.frame = self.bounds
+        maskLayer.path = maskPath.cgPath
+        self.layer.mask = maskLayer
+        
+        // Add border
+        let borderLayer = CAShapeLayer()
+        borderLayer.path = maskLayer.path
+        borderLayer.fillColor = UIColor.clear.cgColor
+        borderLayer.strokeColor = borderColor.cgColor
+        borderLayer.lineWidth = borderWidth
+        borderLayer.frame =  self.bounds
+        self.layer.addSublayer(borderLayer)
+    }
+    
+    // MARK: 5.3、给继承于view的类添加阴影
+    /// 给继承于view的类添加阴影
+    /// - Parameters:
+    ///   - shadowColor: 阴影的颜色
+    ///   - shadowOffset: 阴影的偏移度:CGSizeMake(X[正的右偏移,负的左偏移], Y[正的下偏移,负的上偏移])
+    ///   - shadowOpacity: 阴影的透明度
+    ///   - shadowRadius: 阴影半径,默认 3
+    func addShadow(shadowColor: UIColor, shadowOffset: CGSize, shadowOpacity: Float, shadowRadius: CGFloat = 3) {
+        // 设置阴影颜色
+        layer.shadowColor = shadowColor.cgColor
+        // 设置透明度
+        layer.shadowOpacity = shadowOpacity
+        // 设置阴影半径
+        layer.shadowRadius = shadowRadius
+        // 设置阴影偏移量
+        layer.shadowOffset = shadowOffset
+    }
+    
+    // MARK: 5.4、添加阴影和圆角并存
+    /// 添加阴影和圆角并存
+    ///
+    /// - Parameter superview: 父视图
+    /// - Parameter conrners: 具体哪个圆角
+    /// - Parameter radius: 圆角大小
+    /// - Parameter shadowColor: 阴影的颜色
+    /// - Parameter shadowOffset: 阴影的偏移度:CGSizeMake(X[正的右偏移,负的左偏移], Y[正的下偏移,负的上偏移])
+    /// - Parameter shadowOpacity: 阴影的透明度
+    /// - Parameter shadowRadius: 阴影半径,默认 3
+    ///
+    /// - Note1: 如果在异步布局(如:SnapKit布局)中使用,要在布局后先调用 layoutIfNeeded,再使用该方法
+    /// - Note2: 如果在添加阴影的视图被移除,底部插入的父视图的layer是不会被移除的⚠️
+    func addCornerAndShadow(superview: UIView, conrners: UIRectCorner , radius: CGFloat = 3, shadowColor: UIColor, shadowOffset: CGSize, shadowOpacity: Float, shadowRadius: CGFloat = 3) {
+        
+        let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: conrners, cornerRadii: CGSize(width: radius, height: radius))
+        let maskLayer = CAShapeLayer()
+        maskLayer.frame = self.bounds
+        maskLayer.path = maskPath.cgPath
+        self.layer.mask = maskLayer
+        
+        let subLayer = CALayer()
+        let fixframe = self.frame
+        subLayer.frame = fixframe
+        subLayer.cornerRadius = radius
+        subLayer.backgroundColor = shadowColor.cgColor
+        subLayer.masksToBounds = false
+        // shadowColor阴影颜色
+        subLayer.shadowColor = shadowColor.cgColor
+        // shadowOffset阴影偏移,x向右偏移3,y向下偏移2,默认(0, -3),这个跟shadowRadius配合使用
+        subLayer.shadowOffset = shadowOffset
+        // 阴影透明度,默认0
+        subLayer.shadowOpacity = shadowOpacity
+        // 阴影半径,默认3
+        subLayer.shadowRadius = shadowRadius
+        subLayer.shadowPath = maskPath.cgPath
+        superview.layer.insertSublayer(subLayer, below: self.layer)
+    }
+    
+    // MARK: 5.5、通过贝塞尔曲线View添加阴影和圆角
+    /// 通过贝塞尔曲线View添加阴影和圆角
+    ///
+    /// - Parameter conrners: 具体哪个圆角(暂时只支持:allCorners)
+    /// - Parameter radius: 圆角大小
+    /// - Parameter shadowColor: 阴影的颜色
+    /// - Parameter shadowOffset: 阴影的偏移度:CGSizeMake(X[正的右偏移,负的左偏移], Y[正的下偏移,负的上偏移])
+    /// - Parameter shadowOpacity: 阴影的透明度
+    /// - Parameter shadowRadius: 阴影半径,默认 3
+    ///
+    /// - Note: 提示:如果在异步布局(如:SnapKit布局)中使用,要在布局后先调用 layoutIfNeeded,再使用该方法或者在override func layoutSublayers(of layer: CALayer) {} 里面调用,也要使用 layoutIfNeeded
+    func addViewCornerAndShadow(conrners: UIRectCorner , radius: CGFloat = 3, shadowColor: UIColor, shadowOffset: CGSize, shadowOpacity: Float, shadowRadius: CGFloat = 3) {
+        // 切圆角
+        layer.shadowColor = shadowColor.cgColor
+        layer.shadowOffset = shadowOffset
+        layer.shadowOpacity = shadowOpacity
+        layer.shadowRadius = shadowRadius
+        layer.cornerRadius = radius
+       
+        // 路径阴影
+        let path = UIBezierPath.init(roundedRect: bounds, byRoundingCorners: conrners, cornerRadii: CGSize.init(width: radius, height: radius))
+        layer.shadowPath = path.cgPath
+    }
+    
+    // MARK: 5.6、添加边框
+    /// 添加边框
+    /// - Parameters:
+    ///   - width: 边框宽度
+    ///   - color: 边框颜色
+    func addBorder(borderWidth: CGFloat, borderColor: UIColor) {
+        layer.borderWidth = borderWidth
+        layer.borderColor = borderColor.cgColor
+        layer.masksToBounds = true
+    }
+    
+    // MARK: 5.7、毛玻璃效果
+    /// 毛玻璃效果
+    /// - Parameters:
+    ///   - alpha: 可设置模糊的程度(0-1),越大模糊程度越大
+    ///   - size: 毛玻璃的size
+    ///   - style: 模糊效果
+    func effectViewWithAlpha(alpha: CGFloat = 1.0, size: CGSize? = nil, style: UIBlurEffect.Style = .light) {
+        // 模糊视图的大小
+        var visualEffectViewSize = CGSize(width: 0, height: 0)
+        if let weakSize = size {
+            visualEffectViewSize = weakSize
+        } else {
+            visualEffectViewSize = self.qsl_size
+        }
+        let visualEffectView = UIVisualEffectView.visualEffectView(size: visualEffectViewSize, alpha: alpha, style: style, isAddVibrancy: false)
+        self.addSubview(visualEffectView)
+    }
+    
+    // 设置渐变颜色
+    func gradientBackgroundColor(color1: UIColor, color2: UIColor, width: CGFloat, height: CGFloat, direction: ImageGradientDirection) {
+        
+        if let image = UIImage.gradient([color1, color2], size: CGSize(width: width, height: height), locations: [0, 1], direction: direction) {
+            self.backgroundColor = UIColor(patternImage: image)
+        }
+    }
+}
+
+extension UIView {
+    
+    func addFourCornerAndBorder(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat, borderWidth: CGFloat, borderColor: UIColor){
+        let cornerRadii = UIView.CornerRadii.init(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
+        let path = createPathWithRoundedRect(bounds: self.bounds, cornerRadii:cornerRadii)
+        let shapLayer = CAShapeLayer()
+        shapLayer.frame = self.bounds
+        shapLayer.path = path
+        self.layer.mask = shapLayer
+        
+        // Add border
+        let borderLayer = CAShapeLayer()
+        borderLayer.path = shapLayer.path
+        borderLayer.fillColor = UIColor.clear.cgColor
+        borderLayer.strokeColor = borderColor.cgColor
+        borderLayer.lineWidth = borderWidth
+        borderLayer.frame =  self.bounds
+        self.layer.insertSublayer(borderLayer, at: 0)
+    }
+    
+    //添加4个不同大小的圆角
+    func addFourCorner(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat){
+        let cornerRadii = UIView.CornerRadii.init(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
+        let path = createPathWithRoundedRect(bounds: self.bounds, cornerRadii:cornerRadii)
+        let shapLayer = CAShapeLayer()
+        shapLayer.frame = self.bounds
+        shapLayer.path = path
+        self.layer.mask = shapLayer
+    }
+    
+    //各圆角大小
+    struct CornerRadii {
+        var topLeft :CGFloat = 0
+        var topRight :CGFloat = 0
+        var bottomLeft :CGFloat = 0
+        var bottomRight :CGFloat = 0
+    }
+    
+    //切圆角函数绘制线条
+    func createPathWithRoundedRect (bounds:CGRect,cornerRadii:CornerRadii) -> CGPath {
+        let minX = bounds.minX
+        let minY = bounds.minY
+        let maxX = bounds.maxX
+        let maxY = bounds.maxY
+        
+        //获取四个圆心
+        let topLeftCenterX = minX +  cornerRadii.topLeft
+        let topLeftCenterY = minY + cornerRadii.topLeft
+         
+        let topRightCenterX = maxX - cornerRadii.topRight
+        let topRightCenterY = minY + cornerRadii.topRight
+        
+        let bottomLeftCenterX = minX +  cornerRadii.bottomLeft
+        let bottomLeftCenterY = maxY - cornerRadii.bottomLeft
+         
+        let bottomRightCenterX = maxX -  cornerRadii.bottomRight
+        let bottomRightCenterY = maxY - cornerRadii.bottomRight
+        
+        //虽然顺时针参数是YES,在iOS中的UIView中,这里实际是逆时针
+        let path :CGMutablePath = CGMutablePath();
+         //顶 左
+        path.addArc(center: CGPoint(x: topLeftCenterX, y: topLeftCenterY), radius: cornerRadii.topLeft, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3 / 2, clockwise: false)
+        //顶右
+        path.addArc(center: CGPoint(x: topRightCenterX, y: topRightCenterY), radius: cornerRadii.topRight, startAngle: CGFloat.pi * 3 / 2, endAngle: 0, clockwise: false)
+        //底右
+        path.addArc(center: CGPoint(x: bottomRightCenterX, y: bottomRightCenterY), radius: cornerRadii.bottomRight, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false)
+        //底左
+        path.addArc(center: CGPoint(x: bottomLeftCenterX, y: bottomLeftCenterY), radius: cornerRadii.bottomLeft, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false)
+        path.closeSubpath();
+         return path;
+    }
+    
+}
+
+// MARK: - UIView的一些其他方法
+public extension UIView {
+    
+    // MARK: - 键盘收起来
+    /// 键盘收起来
+    func keyboardEndEditing() {
+        self.endEditing(true)
+    }
+    
+    // MARK: 将 View 转换成图片
+    /// 将 View 转换成图片
+    /// - Returns: 图片
+    func toImage() -> UIImage {
+        return UIGraphicsImageRenderer(size: self.frame.size).image { context in
+            self.layer.render(in: context.cgContext)
+        }
+    }
+
+}
+
+// MARK: - Toast吐司🍞
+extension UIView {
+    
+    // MARK: 普通消息
+    func toast(text: String) {
+        
+        var style = ToastStyle()
+        style.messageFont = .textF(14)
+        style.backgroundColor = UIColor.hexStringColor(hexString: "#000000", alpha: 0.85)
+        style.cornerRadius = 8
+        style.verticalPadding = 14
+        style.horizontalPadding = 44
+        self.makeToast(text, duration: 1.5, position: .center, style: style)
+    }
+}

+ 139 - 0
QuickSearchLocation/Classes/Category/UIViewController+Extension.swift

@@ -0,0 +1,139 @@
+//
+//  UIViewController+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+// MARK: - 一、基本的扩展
+public extension UIViewController {
+    
+    // MARK: 1.1、pop回上一个界面
+    /// pop回上一个界面
+    func popToPreviousVC() {
+        guard let nav = self.navigationController else { return }
+        if let index = nav.viewControllers.firstIndex(of: self), index > 0 {
+            let vc = nav.viewControllers[index - 1]
+            nav.popToViewController(vc, animated: true)
+        }
+    }
+    
+    // MARK: 1.2、获取push进来的 VC
+    /// 获取push进来的 VC
+    /// - Returns: push进来的 VC
+    func getPreviousNavVC() -> UIViewController? {
+        guard let nav = self.navigationController else { return nil }
+        if nav.viewControllers.count <= 1 {
+            return nil
+        }
+        if let index = nav.viewControllers.firstIndex(of: self), index > 0 {
+            let vc = nav.viewControllers[index - 1]
+            return vc
+        }
+        return nil
+    }
+    
+    // MARK: 1.3、获取顶部控制器(类方法)
+    /// 获取顶部控制器
+    /// - Returns: VC
+    static func topViewController() -> UIViewController? {
+        guard let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first, let rootVC = window.rootViewController  else {
+            return nil
+        }
+        return top(rootVC: rootVC)
+    }
+    
+    // MARK: 1.4、获取顶部控制器(实例方法)
+    /// 获取顶部控制器
+    /// - Returns: VC
+    func topViewController() -> UIViewController? {
+        guard let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first, let rootVC = window.rootViewController  else {
+            return nil
+        }
+        return Self.top(rootVC: rootVC)
+    }
+    
+    private static func top(rootVC: UIViewController?) -> UIViewController? {
+        if let presentedVC = rootVC?.presentedViewController {
+            return top(rootVC: presentedVC)
+        }
+        if let nav = rootVC as? UINavigationController,
+            let lastVC = nav.viewControllers.last {
+            return top(rootVC: lastVC)
+        }
+        if let tab = rootVC as? UITabBarController,
+            let selectedVC = tab.selectedViewController {
+            return top(rootVC: selectedVC)
+        }
+        return rootVC
+    }
+    
+    // MARK: 1.5、是否正在展示
+    /// 是否正在展示
+    var isCurrentVC: Bool {
+        return isViewLoaded == true && (view!.window != nil)
+    }
+    
+    // MARK: 1.6、关闭当前的控制器
+    /// 关闭当前的控制器
+    func closeCurrentVC() {
+        guard let nav = self.navigationController else {
+            dismiss(animated: true, completion: nil)
+            return
+        }
+        if nav.viewControllers.count > 1 {
+            nav.popViewController(animated: true)
+        } else if let _ = nav.presentingViewController {
+            nav.dismiss(animated: true, completion: nil)
+        }
+    }
+    
+    //MARK: 1.7、dismiss到某个vc
+    /// dismiss到某个vc
+    /// - Parameters:
+    ///   - vc: vc
+    ///   - animated: 是否需要动画
+    @discardableResult
+    func dismiss(vc: AnyClass, animated: Bool) -> Bool {
+        guard let targetPresentingVC = findPresentingViewController(fromVc: self, toVc: vc) else { return false }
+        targetPresentingVC.dismiss(animated: animated)
+        return true
+    }
+    
+    //MARK: 寻找到对应的presentingViewController
+    /// 寻找到对应的presentingViewController
+    /// - Parameters:
+    ///   - fromVc: 当前vc
+    ///   - toVc: 目标vc
+    /// - Returns: 目标presentingViewController
+    private func findPresentingViewController(fromVc: UIViewController, toVc: AnyClass) -> UIViewController? {
+        // 判断是否存在对应的presentingViewController
+        guard let vc = fromVc.presentingViewController else { return nil }
+        // 判断是否是目标vc
+        if (vc.isMember(of: toVc)) {
+            // 寻找到对应的presentingViewController
+            return vc
+        } else {
+            return findPresentingViewController(fromVc: vc, toVc: toVc)
+        }
+    }
+    
+    func pushVC(vc: UIViewController ) {
+        
+        if vc.isKind(of: UIViewController.self) == false {
+            return
+        }
+        
+        if self.navigationController == nil {
+            return
+        }
+        
+        if vc.hidesBottomBarWhenPushed == false {
+            vc.hidesBottomBarWhenPushed = true
+        }
+        
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+}

+ 42 - 0
QuickSearchLocation/Classes/Category/UIVisualEffectView+Extension.swift

@@ -0,0 +1,42 @@
+//
+//  UIVisualEffectView+Extension.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import Foundation
+import UIKit
+
+// MARK: - 一、基本的扩展
+public extension UIVisualEffectView {
+    
+    // MARK: 1.1、创建一个UIVisualEffectView对象
+    /// 创建一个UIVisualEffectView对象
+    /// - Parameters:
+    ///   - size: UIVisualEffectView的size
+    ///   - alpha: 模糊透明度
+    ///   - style: 模糊样式
+    ///   - isAddVibrancy: 是否添加UIVibrancyEffect
+    /// - Returns: 返回UIVisualEffectView
+    static func visualEffectView(size: CGSize, alpha: CGFloat = 1.0, style: UIBlurEffect.Style = .light, isAddVibrancy: Bool = true) -> UIVisualEffectView {
+        // 首先创建一个模糊效果
+        let blurEffect = UIBlurEffect(style: style)
+        // 接着创建一个承载模糊效果的视图
+        let blurView = UIVisualEffectView(effect: blurEffect)
+        // 毛玻璃的透明度
+        blurView.alpha = alpha
+        // 设置模糊视图的大小(全屏)
+        blurView.frame.size = size
+        // 创建并添加vibrancy视图
+        if isAddVibrancy {
+            /*
+             UIVibrancyEffect 主要用于放大和调整UIVisualEffectView 视图下面的内容的颜色,同时让UIVisualEffectView的  contentView中的内容看起来更加生动。通常UIVibrancyEffect 对象是与UIBlurEffect一起使用
+             */
+            let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
+            vibrancyView.frame.size = size
+            blurView.contentView.addSubview(vibrancyView)
+        }
+        return blurView
+    }
+}

+ 53 - 0
QuickSearchLocation/Classes/Common/Controller/QSLBaseController.swift

@@ -0,0 +1,53 @@
+//
+//  QSLBaseController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+import SnapKit
+
+class QSLBaseController: UIViewController {
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        // 默认隐藏 navbar
+        self.navigationController?.setNavigationBarHidden(true, animated: true)
+    }
+    
+    override func viewDidLoad() {
+        
+        super.viewDidLoad()
+        // 默认设置背景色为F6F6F6
+        self.view.backgroundColor = QSLColor.backGroundColor
+        
+        setBackButton()
+    }
+}
+
+extension QSLBaseController {
+    
+    func setBackButton() {
+        
+        let backButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100.rpx, height: 25.rpx))
+        backButton.title(self.title ?? "")
+        backButton.textColor(QSLColor.Color_202020)
+        backButton.mediumFont(17)
+        backButton.backgroundColor = .clear
+        backButton.image(UIImage(named: "public_back_btn"))
+        backButton.setImageTitleLayout(.imgLeft, spacing: 4.rpx)
+        backButton.addTarget(self, action: #selector(backBtnAction), for: .touchUpInside)
+        let item = UIBarButtonItem(customView: backButton)
+        self.navigationItem.leftBarButtonItem = item
+        self.navigationItem.hidesBackButton = true
+    }
+}
+
+extension QSLBaseController {
+    
+    @objc func backBtnAction() {
+        self.dismiss(animated: true)
+        self.navigationController?.popViewController(animated: true)
+    }
+}

+ 65 - 0
QuickSearchLocation/Classes/Common/Controller/QSLBaseNavController.swift

@@ -0,0 +1,65 @@
+//
+//  QSLBaseNavController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+class QSLBaseNavController: UINavigationController {
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        self.setNavBarAppearence()
+        self.interactivePopGestureRecognizer?.delegate = self;
+    }
+    
+    func setNavBarAppearence() {
+        
+        // 设置 navbar 背景颜色
+        self.navigationBar.setBackgroundImage(UIImage.image(color: .white, size: CGSize(width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kNavFrameH)), for: .any, barMetrics: .default)
+        // 取消 navbar 阴影
+        self.navigationBar.shadowImage = UIImage()
+        
+        // 设置文字颜色、大小
+        var textAttr:[NSAttributedString.Key : Any] = [NSAttributedString.Key : Any]()
+        textAttr[NSAttributedString.Key.foregroundColor] = UIColor.clear
+        textAttr[NSAttributedString.Key.font] = UIFont.textM(17)
+        self.navigationBar.titleTextAttributes = textAttr
+        
+        if #available(iOS 15.0, *) {
+            
+            let appearance = UINavigationBarAppearance()
+//            appearance.backgroundEffect = UIBlurEffect(style: .regular)
+            appearance.configureWithOpaqueBackground()
+            appearance.titleTextAttributes = textAttr
+            appearance.backgroundColor = .white
+            
+            appearance.shadowColor = .clear
+            self.navigationBar.standardAppearance = appearance
+            self.navigationBar.scrollEdgeAppearance = appearance
+        }
+    }
+}
+
+extension QSLBaseNavController {
+    
+    /// 设置当跳转到其他页面时隐藏 tabbar
+    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
+        
+        if children.count > 0 {
+            viewController.hidesBottomBarWhenPushed = true
+        }
+        super.pushViewController(viewController, animated: animated)
+    }
+}
+
+extension QSLBaseNavController: UIGestureRecognizerDelegate {
+    
+    /// 跳转到其他页面时开启侧滑
+    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+        return children.count > 1
+    }
+}

+ 179 - 0
QuickSearchLocation/Classes/Common/Controller/QSLWebController.swift

@@ -0,0 +1,179 @@
+//
+//  QSLWebController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/24.
+//
+
+import UIKit
+import WebKit
+
+class QSLWebViewController: QSLBaseController, WKNavigationDelegate {
+    
+    var isShowTop: Bool = false {
+        didSet {
+            updateTop()
+        }
+    }
+    
+    lazy var topView: QSLWebViewTopView = {
+        
+        let view = QSLWebViewTopView()
+        return view
+    }()
+    
+    var webUrl: String?
+
+    var webView: WKWebView = WKWebView()
+
+    var progressView:UIProgressView = UIProgressView()
+
+    var closeBtn: UIButton?
+
+    deinit {
+        webView.removeObserver(self, forKeyPath:"estimatedProgress")
+        webView.navigationDelegate = nil
+    }
+
+    override func viewDidLoad() {
+
+        super.viewDidLoad()
+
+        webView.addObserver(self, forKeyPath:"estimatedProgress", options: NSKeyValueObservingOptions.new, context:nil)
+        webView.navigationDelegate = self
+        // webview
+        view.addSubview(webView)
+        webView.snp.makeConstraints { (make)in
+            make.edges.equalTo(0)
+        }
+
+        // progressview
+        view.addSubview(progressView)
+        progressView.snp.makeConstraints { (make)in
+            make.width.equalToSuperview()
+            make.height.equalTo(3)
+            make.top.equalToSuperview()
+        }
+
+        progressView.tintColor = QSLColor.Color_202020
+        progressView.isHidden = true
+        
+        // load url
+        if webUrl != nil {
+            webView.load(URLRequest(url:URL(string: webUrl!)!))
+        }
+    }
+    
+    func updateTop() {
+        
+        topView.titleLabel.text = self.title
+        
+        topView.backBtnBlock = {
+            self.dismiss(animated: true)
+        }
+        
+        view.addSubview(topView)
+        topView.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+            make.height.equalTo(44)
+        }
+        
+        webView.snp.remakeConstraints { make in
+            make.top.equalTo(topView.snp.bottom)
+            make.left.right.bottom.equalTo(0)
+        }
+        
+        progressView.snp.remakeConstraints { (make)in
+            make.width.equalToSuperview()
+            make.height.equalTo(3)
+            make.top.equalTo(topView.snp.bottom)
+        }
+    }
+
+    override func viewWillAppear(_ animated:Bool) {
+//        super.viewWillAppear(animated)
+        self.navigationController?.setNavigationBarHidden(false, animated: animated)
+    }
+    
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        self.navigationController?.setNavigationBarHidden(true, animated: animated)
+    }
+
+    override func observeValue(forKeyPath keyPath:String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+
+        // 加载进度
+        if keyPath == #keyPath(WKWebView.estimatedProgress) {
+
+            let progress = Float(webView.estimatedProgress)
+            // TODO: 更新进度条等UI,例如:
+            progressView.progress = progress
+        }
+    }
+
+    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
+        progressView.isHidden = false
+        progressView.progress = 0
+    }
+        
+    func webView(_ webView:WKWebView, didFinish navigation: WKNavigation!) {
+
+        progressView.isHidden = true
+        progressView.setProgress(0, animated:false)
+    }
+
+    func webView(_ webView:WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
+        progressView.isHidden = true
+        progressView.setProgress(0, animated:false)
+    }
+
+}
+
+class QSLWebViewTopView: UIView {
+    
+    lazy var closeBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.image(UIImage(named: "common_close_btn"))
+        btn.addTarget(self, action: #selector(closeBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var titleLabel: UILabel = {
+       
+        let label = UILabel()
+        label.boldFont(18)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    var backBtnBlock: (() -> ())?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        addSubview(closeBtn)
+        addSubview(titleLabel)
+        
+        closeBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24, height: 24))
+            make.right.equalTo(-12)
+            make.centerY.equalToSuperview()
+        }
+        
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func closeBtnAction() {
+        if let backBtnBlock = self.backBtnBlock {
+            backBtnBlock()
+        }
+    }
+}
+

+ 318 - 0
QuickSearchLocation/Classes/Common/LoadingViewController.swift

@@ -0,0 +1,318 @@
+//
+//  LoadingViewController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/31.
+//
+
+import UIKit
+import Photos
+
+class LoadingViewController: UIViewController {
+    private let photoClassifier = PhotoClassifier()
+    private let progressView = UIProgressView(progressViewStyle: .default)
+    private let statusLabel = UILabel()
+    private let resultLabel = UILabel()
+    private let collectionView: UICollectionView
+    private var similarGroups: [[PHAsset]] = []
+    private var timer: Timer?
+    
+    // 添加详细进度标签
+    private let detailProgressLabel = UILabel()
+    private let percentageLabel = UILabel()
+    
+    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
+        // 初始化 CollectionView
+        let layout = UICollectionViewFlowLayout()
+        layout.scrollDirection = .vertical
+        layout.minimumLineSpacing = 10
+        layout.minimumInteritemSpacing = 5
+        
+        let screenWidth = UIScreen.main.bounds.width
+        let width = (screenWidth - 20) / 3
+        layout.itemSize = CGSize(width: width, height: width)
+        layout.sectionInset = UIEdgeInsets(top: 10, left: 5, bottom: 10, right: 5)
+        layout.headerReferenceSize = CGSize(width: screenWidth, height: 40)
+        
+        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
+        
+        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        setupUI()
+        startClassifying()
+    }
+    
+    private func setupUI() {
+           view.backgroundColor = .white
+           
+           // 设置进度容器视图
+           let progressContainer = UIView()
+           progressContainer.translatesAutoresizingMaskIntoConstraints = false
+           view.addSubview(progressContainer)
+           
+           // 配置所有标签
+           statusLabel.textAlignment = .center
+           statusLabel.font = .systemFont(ofSize: 16, weight: .medium)
+           
+           detailProgressLabel.textAlignment = .center
+           detailProgressLabel.font = .systemFont(ofSize: 14)
+           detailProgressLabel.textColor = .darkGray
+           
+           percentageLabel.textAlignment = .center
+           percentageLabel.font = .systemFont(ofSize: 14, weight: .medium)
+           percentageLabel.textColor = .systemBlue
+           
+           resultLabel.textAlignment = .center
+           resultLabel.numberOfLines = 0
+           resultLabel.font = .systemFont(ofSize: 14)
+           
+           // 配置进度条
+           progressView.progressTintColor = .systemBlue
+           progressView.trackTintColor = .systemGray5
+           
+           // 添加所有视图
+           [statusLabel, progressView, detailProgressLabel, percentageLabel, resultLabel].forEach {
+               $0.translatesAutoresizingMaskIntoConstraints = false
+               progressContainer.addSubview($0)
+           }
+           
+           // 布局约束
+           NSLayoutConstraint.activate([
+               progressContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+               progressContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+               progressContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+               progressContainer.heightAnchor.constraint(equalToConstant: 180),
+               
+               statusLabel.topAnchor.constraint(equalTo: progressContainer.topAnchor, constant: 20),
+               statusLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
+               
+               progressView.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 15),
+               progressView.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
+               progressView.widthAnchor.constraint(equalTo: progressContainer.widthAnchor, multiplier: 0.8),
+               
+               detailProgressLabel.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 10),
+               detailProgressLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
+               
+               percentageLabel.topAnchor.constraint(equalTo: detailProgressLabel.bottomAnchor, constant: 5),
+               percentageLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
+               
+               resultLabel.topAnchor.constraint(equalTo: percentageLabel.bottomAnchor, constant: 15),
+               resultLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
+               resultLabel.widthAnchor.constraint(equalTo: progressContainer.widthAnchor, multiplier: 0.8)
+           ])
+           
+           // 设置 CollectionView
+           collectionView.backgroundColor = .white
+           collectionView.delegate = self
+           collectionView.dataSource = self
+           collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: "PhotoCell")
+           collectionView.register(
+               HeaderView.self,
+               forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
+               withReuseIdentifier: "Header"
+           )
+           
+           collectionView.translatesAutoresizingMaskIntoConstraints = false
+           view.addSubview(collectionView)
+           
+           NSLayoutConstraint.activate([
+               collectionView.topAnchor.constraint(equalTo: progressContainer.bottomAnchor),
+               collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+               collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+               collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+           ])
+       }
+    
+    private func startClassifying() {
+        // 重置UI
+        progressView.progress = 0
+        statusLabel.text = "正在准备..."
+        detailProgressLabel.text = "初始化中"
+        percentageLabel.text = "0%"
+        resultLabel.text = ""
+        
+        // 请求相册权限
+        PHPhotoLibrary.requestAuthorization { [weak self] status in
+            guard let self = self else { return }
+            
+            DispatchQueue.main.async {
+                if status == .authorized {
+                    self.beginPhotoClassification()
+                } else {
+                    self.statusLabel.text = "需要相册访问权限"
+                    self.detailProgressLabel.text = "请在设置中允许访问相册"
+                }
+            }
+        }
+    }
+    
+    private func beginPhotoClassification() {
+            statusLabel.text = "正在加载照片..."
+            
+            let fetchOptions = PHFetchOptions()
+            let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
+            
+            // 更新总数
+            detailProgressLabel.text = "共发现 \(allPhotos.count) 张照片"
+            
+            photoClassifier.classifyPhotos(
+                assets: allPhotos,
+                progressHandler: { [weak self] (stage, progress) in
+                    DispatchQueue.main.async {
+                        self?.updateProgress(stage: stage, progress: progress)
+                    }
+                },
+                completion: { [weak self] result in
+                    guard let self = self else { return }
+                    
+                    DispatchQueue.main.async {
+                        self.updateProgress(stage: "分类完成", progress: 1.0)
+                        
+                        // 更新结果显示
+                        var resultText = "分类结果:\n"
+                        resultText += "截图:\(result.screenshots.count) 张\n"
+                        resultText += "相似照片组:\(result.similarPhotos.count) 组"
+                        self.resultLabel.text = resultText
+                        
+                        // 更新相似照片展示
+                        self.similarGroups = result.similarPhotos
+                        self.collectionView.reloadData()
+                    }
+                }
+            )
+        }
+    
+    private func updateProgress(stage: String, progress: Float) {
+            statusLabel.text = stage
+            progressView.progress = progress
+            percentageLabel.text = "\(Int(progress * 100))%"
+            
+            switch stage {
+            case "正在加载照片...":
+                detailProgressLabel.text = "正在读取照片数据"
+            case "正在提取特征...":
+                detailProgressLabel.text = "正在分析照片内容"
+            case "正在比较相似度...":
+                detailProgressLabel.text = "正在查找相似照片"
+            case "分类完成":
+                detailProgressLabel.text = "处理完成"
+            default:
+                break
+            }
+        }
+}
+
+// MARK: - UICollectionView DataSource & Delegate
+extension LoadingViewController: UICollectionViewDataSource, UICollectionViewDelegate {
+    func numberOfSections(in collectionView: UICollectionView) -> Int {
+        return similarGroups.count
+    }
+    
+    func collectionView(_ collectionView: UICollectionView,
+                       numberOfItemsInSection section: Int) -> Int {
+        return similarGroups[section].count
+    }
+    
+    func collectionView(_ collectionView: UICollectionView,
+                       cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = collectionView.dequeueReusableCell(
+            withReuseIdentifier: "PhotoCell",
+            for: indexPath
+        ) as! PhotoCell
+        
+        let asset = similarGroups[indexPath.section][indexPath.item]
+        cell.configure(with: asset)
+        return cell
+    }
+    
+    func collectionView(_ collectionView: UICollectionView,
+                       viewForSupplementaryElementOfKind kind: String,
+                       at indexPath: IndexPath) -> UICollectionReusableView {
+        if kind == UICollectionView.elementKindSectionHeader {
+            let header = collectionView.dequeueReusableSupplementaryView(
+                ofKind: kind,
+                withReuseIdentifier: "Header",
+                for: indexPath
+            ) as! HeaderView
+            
+            let groupCount = similarGroups[indexPath.section].count
+            header.titleLabel.text = "相似组 \(indexPath.section + 1) (\(groupCount) 张)"
+            return header
+        }
+        return UICollectionReusableView()
+    }
+}
+
+// MARK: - PhotoCell
+class PhotoCell: UICollectionViewCell {
+    private let imageView = UIImageView()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    private func setupUI() {
+        imageView.contentMode = .scaleAspectFill
+        imageView.clipsToBounds = true
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(imageView)
+        
+        NSLayoutConstraint.activate([
+            imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
+            imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+            imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+            imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
+        ])
+    }
+    
+    func configure(with asset: PHAsset) {
+        let options = PHImageRequestOptions()
+        options.deliveryMode = .fastFormat
+        
+        PHImageManager.default().requestImage(
+            for: asset,
+            targetSize: CGSize(width: 200, height: 200),
+            contentMode: .aspectFill,
+            options: options
+        ) { [weak self] image, _ in
+            self?.imageView.image = image
+        }
+    }
+}
+
+// MARK: - HeaderView
+class HeaderView: UICollectionReusableView {
+    let titleLabel = UILabel()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    private func setupUI() {
+        titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
+        addSubview(titleLabel)
+        
+        NSLayoutConstraint.activate([
+            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
+            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
+        ])
+    }
+}

+ 26 - 0
QuickSearchLocation/Classes/Common/Model/QSLContactModel.swift

@@ -0,0 +1,26 @@
+//
+//  QSLContactModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/12.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLContactModel: Modelable {
+    
+    var contactId: Int = 0
+    
+    var phone: String = ""
+    
+    var remark: String = ""
+    
+    var favor: Bool = false
+    
+    var createTime: Int = 0
+    
+    mutating func mapping(_ json: JSON) {
+        self.contactId = json["id"].intValue
+    }
+}

+ 42 - 0
QuickSearchLocation/Classes/Common/Model/QSLGoodModel.swift

@@ -0,0 +1,42 @@
+//
+//  QSLGoodModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/11.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLGoodModel: Modelable {
+    
+    var goodId: Int = 0
+    
+    var appleGoodsId: String = ""
+    
+    var sort: Int = 0
+    
+    var name: String = ""
+    
+    var level: Int = 0
+    
+    var content: String = ""
+    
+    var originalAmount: CGFloat = 0.0
+    
+    var amount: CGFloat = 0.0
+    
+    var subscriptionMillis: Int = 0
+    
+    var tag: String = ""
+    
+    var popular: Bool = false
+    
+    var newcomer: Bool = false
+    
+    var isSelect: Bool = false
+    
+    mutating func mapping(_ json: JSON) {
+        self.goodId = json["id"].intValue
+    }
+}

+ 29 - 0
QuickSearchLocation/Classes/Common/Model/QSLMapMessageModel.swift

@@ -0,0 +1,29 @@
+//
+//  QSLMapMessageModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/6.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLMapMessageModel: Modelable {
+    
+    var cmd: String = ""
+    
+    var data: String?
+    
+    var body: String = ""
+    
+    var lng: CGFloat?
+    
+    var lat: CGFloat?
+    
+    var addr: String?
+    
+    mutating func mapping(_ json: JSON) {
+              
+    }
+}
+

+ 21 - 0
QuickSearchLocation/Classes/Common/Model/QSLMapPointModel.swift

@@ -0,0 +1,21 @@
+//
+//  QSLMapPointModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/6.
+//
+
+import MoyaMapper
+import SwiftyJSON
+import MAMapKit
+
+class QSLMapPointModel: NSObject {
+    
+    var userId: String = ""
+    
+    var zIndex: Int = 0
+    
+    var pointAnnotation: MAPointAnnotation?
+
+}
+

+ 36 - 0
QuickSearchLocation/Classes/Common/Model/QSLMapTrackModel.swift

@@ -0,0 +1,36 @@
+//
+//  QSLMapTrackModel.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLMapTrackModel: Modelable {
+    
+    var pointId: String = ""
+    
+    var userId: String = ""
+    
+    var lng: CGFloat = 0.0
+    
+    var lat: CGFloat = 0.0
+    
+    var addr: String = ""
+    
+    var speed: CGFloat = 0.0
+    
+    var bearing: CGFloat = 0.0
+    
+    var timestamp: Int = 0
+    
+    var ts: CGFloat = 0.0
+    
+    var photo: String = ""
+    
+    mutating func mapping(_ json: JSON) {
+              
+    }
+}

+ 74 - 0
QuickSearchLocation/Classes/Common/Model/QSLMemberModel.swift

@@ -0,0 +1,74 @@
+//
+//  QSLMemberModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/9.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLMemberModel: Modelable {
+    
+    var userId: String = ""
+    
+    var level: Int = 0
+    
+    var startTimestamp: Int = 0
+    
+    var endTimestamp: Int = 0
+    
+    var expired: Bool = true
+    
+    var permanent: Bool = false
+    
+    mutating func mapping(_ json: JSON) {
+              
+    }
+    
+    func memberLevelString() -> String {
+        
+        var level = ""
+        
+        if self.expired {
+            level = "未开通"
+        }
+        
+        if self.permanent {
+            level = "终身会员"
+        }
+        
+        switch self.level {
+        case 0:
+            level = "未开通"
+            break;
+        case 100:
+            level = "日卡VIP"
+            break;
+        case 700:
+            level = "周卡VIP"
+            break;
+        case 3100:
+            level = "月度VIP"
+            break;
+        case 9200:
+            level = "季度VIP"
+            break;
+        case 36600:
+            level = "年度VIP"
+            break;
+        case 3660000:
+            level = "终身VIP"
+            break;
+        default:
+            level = "未开通"
+            break;
+        }
+        
+        return level
+    }
+    
+    func endTimestampString() -> String {
+        return Date.timestampToFormatterTimeString(timestamp: "\(endTimestamp)", format: "yyyy-MM-dd")
+    }
+}

+ 35 - 0
QuickSearchLocation/Classes/Common/Model/QSLMessageModel.swift

@@ -0,0 +1,35 @@
+//
+//  QSLMessageModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/5.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLMessageModel: Modelable {
+    
+    var messageId: Int = 0
+    
+    /*
+    1:你的好友请求已经发送
+    2:你的好友请求已经被接受
+    3:你的好友请求已经被拒绝
+    4:好友发来的求救
+    5:你的好友删除了你
+    */
+    var type: Int = 0
+    
+    var senderId: String = ""
+    
+    var senderPhone: String = ""
+    
+    var content: String = ""
+    
+    var createTime: String = ""
+    
+    mutating func mapping(_ json: JSON) {
+        self.messageId = json["id"].intValue
+    }
+}

+ 30 - 0
QuickSearchLocation/Classes/Common/Model/QSLOrderModel.swift

@@ -0,0 +1,30 @@
+//
+//  QSLOrderModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/11.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLOrderModel: Modelable {
+
+    // 订单号
+    var outTradeNo: String = ""
+    // 苹果支付
+    var appAccountToken: String = ""
+    
+    var receiptData: String = ""
+    
+    // 是否完成订单
+    var isFinish: Bool = false
+    
+    // 订单的商品
+    var selectGoods: QSLGoodModel = QSLGoodModel()
+    
+    mutating func mapping(_ json: JSON) {
+            
+    }
+}
+

+ 41 - 0
QuickSearchLocation/Classes/Common/Model/QSLRequestModel.swift

@@ -0,0 +1,41 @@
+//
+//  QSLRequestModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/5.
+//
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLRequestModel: Modelable {
+    
+    // 请求id
+    var requestId: Int = 0
+    
+    // 请求发送者id
+    var userId: String = ""
+    
+    // 请求发送者手机
+    var userPhone: String = ""
+    
+    // 请求接收者id(你)
+    var friendId: String = ""
+    
+    // 请求接受者手机(你)
+    var friendPhone: String = ""
+    
+    // 请求时间戳
+    var createTime: String = ""
+    
+    /*
+     1:待处理
+     2:接受
+     3:拒绝
+     */
+    var status: Int = 0
+    
+    mutating func mapping(_ json: JSON) {
+        self.requestId = json["id"].intValue
+    }
+}
+

+ 42 - 0
QuickSearchLocation/Classes/Common/Model/QSLUserModel.swift

@@ -0,0 +1,42 @@
+//
+//  QSLUserModel.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import MoyaMapper
+import SwiftyJSON
+
+struct QSLUserModel: Modelable {
+    
+    var userId: String = ""
+    
+    var authToken: String = ""
+    
+    var friendId: String = ""
+    
+    var phone: String = ""
+    
+    var remark: String = ""
+    
+    var timestamp: String = ""
+    
+    var blockedHim: Bool = false
+    
+    var blockedMe: Bool = false
+    
+    var location: QSLMapTrackModel = QSLMapTrackModel()
+    
+    // 是否为自己
+    var isMine: Bool = false
+    
+    // 会员截止时间
+    var vipEndTimestamp: Date?
+    
+    var memberModel: QSLMemberModel = QSLMemberModel()
+    
+    mutating func mapping(_ json: JSON) {
+        
+    }
+}

+ 316 - 0
QuickSearchLocation/Classes/Common/PhotoClassifier.swift

@@ -0,0 +1,316 @@
+import Photos
+import Vision
+
+class PhotoClassifier {
+    struct ClassifiedPhotos {
+        var screenshots: [PHAsset] = []
+        var locations: [String: [PHAsset]] = [:] // 按地点分组
+        var people: [String: [PHAsset]] = [:]     // 按人物分组
+        var similarPhotos: [[PHAsset]] = [] // 存储相似照片组
+    }
+    
+    func classifyPhotos(
+            assets: PHFetchResult<PHAsset>,
+            progressHandler: @escaping (String, Float) -> Void,
+            completion: @escaping (ClassifiedPhotos) -> Void
+        ) {
+            // 在后台队列处理
+            DispatchQueue.global(qos: .userInitiated).async {
+                var result = ClassifiedPhotos()
+                let group = DispatchGroup()
+                
+                // 开始处理
+                DispatchQueue.main.async {
+                    progressHandler("正在加载照片...", 0.0)
+                }
+                
+                // 1. 检测截图 (占总进度的 20%)
+                group.enter()
+                self.fetchScreenshots(from: assets) { screenshots in
+                    result.screenshots = screenshots
+                    DispatchQueue.main.async {
+                        progressHandler("正在检测截图...", 0.2)
+                    }
+                    group.leave()
+                }
+                
+                // 2. 检测相似照片 (占总进度的 80%)
+                group.enter()
+                self.detectSimilarPhotos(
+                    assets: assets,
+                    progressHandler: { stage, progress in
+                        // 将相似照片检测的进度映射到 20%-100% 的范围
+                        let mappedProgress = 0.2 + (progress * 0.8)
+                        DispatchQueue.main.async {
+                            progressHandler(stage, mappedProgress)
+                        }
+                    }
+                ) { similarPhotos in
+                    result.similarPhotos = similarPhotos
+                    group.leave()
+                }
+                
+                // 等待所有处理完成
+                group.notify(queue: .main) {
+                    progressHandler("分类完成", 1.0)
+                    completion(result)
+                }
+            }
+        }
+    
+    private func detectSimilarPhotos(
+            assets: PHFetchResult<PHAsset>,
+            progressHandler: @escaping (String, Float) -> Void,
+            completion: @escaping ([[PHAsset]]) -> Void
+        ) {
+            var similarGroups: [[PHAsset]] = []
+            let group = DispatchGroup()
+            var imageFeatures: [(asset: PHAsset, feature: VNFeaturePrintObservation)] = []
+            
+            // 创建处理队列
+            let processingQueue = DispatchQueue(label: "com.app.similarPhotos", qos: .userInitiated)
+            let semaphore = DispatchSemaphore(value: 5)
+            
+            // 1. 提取所有图片的特征
+            let totalAssets = assets.count
+            var processedAssets = 0
+            
+            progressHandler("正在加载照片...", 0.0)
+            
+            for i in 0..<assets.count {
+                let asset = assets[i]
+                group.enter()
+                semaphore.wait()
+                
+                let options = PHImageRequestOptions()
+                options.deliveryMode = .highQualityFormat
+                options.isSynchronous = false
+                options.resizeMode = .exact
+                
+                PHImageManager.default().requestImage(
+                    for: asset,
+                    targetSize: CGSize(width: 448, height: 448),
+                    contentMode: .aspectFit,
+                    options: options
+                ) { image, _ in
+                    defer {
+                        semaphore.signal()
+                    }
+                    
+                    guard let image = image,
+                          let cgImage = image.cgImage else {
+                        group.leave()
+                        return
+                    }
+                    
+                    processingQueue.async {
+                        do {
+                            let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
+                            let request = VNGenerateImageFeaturePrintRequest()
+                            try requestHandler.perform([request])
+                            
+                            if let result = request.results?.first as? VNFeaturePrintObservation {
+                                imageFeatures.append((asset, result))
+                                
+                                // 更新特征提取进度
+                                processedAssets += 1
+                                let progress = Float(processedAssets) / Float(totalAssets)
+                                progressHandler("正在提取特征...", progress * 0.6)
+                            }
+                        } catch {
+                            print("特征提取失败: \(error)")
+                        }
+                        group.leave()
+                    }
+                }
+            }
+            
+            // 2. 比较特征相似度并分组
+            group.notify(queue: processingQueue) {
+                progressHandler("正在比较相似度...", 0.6)
+                
+                // 近似度
+                let similarityThreshold: Float = 0.7
+                var processedComparisons = 0
+                let totalComparisons = (imageFeatures.count * (imageFeatures.count - 1)) / 2
+                var processedIndices = Set<Int>()
+                
+                for i in 0..<imageFeatures.count {
+                    if processedIndices.contains(i) { continue }
+                    
+                    var similarGroup: [PHAsset] = [imageFeatures[i].asset]
+                    processedIndices.insert(i)
+                    
+                    for j in (i + 1)..<imageFeatures.count {
+                        if processedIndices.contains(j) { continue }
+                        
+                        do {
+                            var distance: Float = 0
+                            try imageFeatures[i].feature.computeDistance(&distance, to: imageFeatures[j].feature)
+                            
+                            let similarity = 1 - distance
+                            if similarity >= similarityThreshold {
+                                similarGroup.append(imageFeatures[j].asset)
+                                processedIndices.insert(j)
+                            }
+                            
+                            // 更新比较进度
+                            processedComparisons += 1
+                            let compareProgress = Float(processedComparisons) / Float(totalComparisons)
+                            progressHandler("正在比较相似度...", 0.6 + compareProgress * 0.4)
+                        } catch {
+                            print("相似度计算失败: \(error)")
+                        }
+                    }
+                    
+                    if similarGroup.count > 1 {
+                        similarGroups.append(similarGroup)
+                    }
+                }
+                
+                // 按照照片数量降序排序
+                similarGroups.sort { $0.count > $1.count }
+                
+                DispatchQueue.main.async {
+                    completion(similarGroups)
+                }
+            }
+        }
+    
+    // 按地点分类
+    private func classifyByLocation(assets: PHFetchResult<PHAsset>,
+                                  completion: @escaping ([String: [PHAsset]]) -> Void) {
+        var locationGroups: [String: [PHAsset]] = [:]
+        let group = DispatchGroup()
+        let geocodeQueue = DispatchQueue(label: "com.app.geocoding")
+        let semaphore = DispatchSemaphore(value: 10) // 限制并发请求数
+        
+        assets.enumerateObjects { asset, _, _ in
+            if let location = asset.location {
+                group.enter()
+                semaphore.wait()
+                
+                geocodeQueue.async {
+                    let geocoder = CLGeocoder()
+                    geocoder.reverseGeocodeLocation(location) { placemarks, error in
+                        defer {
+                            semaphore.signal()
+                            group.leave()
+                        }
+                        
+                        if let placemark = placemarks?.first {
+                            let locationName = self.formatLocationName(placemark)
+                            DispatchQueue.main.async {
+                                if locationGroups[locationName] == nil {
+                                    locationGroups[locationName] = []
+                                }
+                                locationGroups[locationName]?.append(asset)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        
+        // 等待所有地理编码完成后回调
+        group.notify(queue: .main) {
+            completion(locationGroups)
+        }
+    }
+
+    // 格式化地点名称(只返回城市名)
+    private func formatLocationName(_ placemark: CLPlacemark) -> String {
+        if let city = placemark.locality {
+            return city
+        }
+        return "其他"
+    }
+    
+    // 按人物分类
+    private func classifyByPeople(assets: PHFetchResult<PHAsset>,
+                                completion: @escaping ([String: [PHAsset]]) -> Void) {
+        var peopleGroups: [String: [PHAsset]] = [:]
+        let group = DispatchGroup()
+        
+        // 创建一个数组来存储检测到人脸的照片
+        var facesArray: [PHAsset] = []
+        
+        // 遍历所有照片
+        assets.enumerateObjects { asset, _, _ in
+            group.enter()
+            
+            // 获取照片的缩略图进行人脸检测
+            let options = PHImageRequestOptions()
+            options.isSynchronous = false
+            options.deliveryMode = .fastFormat
+            
+            PHImageManager.default().requestImage(
+                for: asset,
+                targetSize: CGSize(width: 500, height: 500), // 使用较小的尺寸提高性能
+                contentMode: .aspectFit,
+                options: options
+            ) { image, _ in
+                guard let image = image else {
+                    group.leave()
+                    return
+                }
+                
+                // 使用 Vision 框架检测人脸
+                guard let ciImage = CIImage(image: image) else {
+                    group.leave()
+                    return
+                }
+                
+                let request = VNDetectFaceRectanglesRequest()
+                let handler = VNImageRequestHandler(ciImage: ciImage)
+                
+                do {
+                    try handler.perform([request])
+                    if let results = request.results, !results.isEmpty {
+                        // 检测到人脸,添加到数组
+                        DispatchQueue.main.async {
+                            facesArray.append(asset)
+                        }
+                    }
+                } catch {
+                    print("人脸检测失败: \(error)")
+                }
+                
+                group.leave()
+            }
+        }
+        
+        // 等待所有检测完成后更新结果
+        group.notify(queue: .main) {
+            if !facesArray.isEmpty {
+                peopleGroups["包含人脸的照片"] = facesArray
+            }
+            completion(peopleGroups)
+        }
+    }
+    
+    // 识别截图
+    private func fetchScreenshots(from assets: PHFetchResult<PHAsset>,
+                                completion: @escaping ([PHAsset]) -> Void) {
+        var screenshots: [PHAsset] = []
+        
+        // 获取系统的截图智能相册
+        let screenshotAlbums = PHAssetCollection.fetchAssetCollections(
+            with: .smartAlbum,
+            subtype: .smartAlbumScreenshots,
+            options: nil
+        )
+        
+        // 从截图相册中获取所有截图
+        screenshotAlbums.enumerateObjects { collection, _, _ in
+            let fetchOptions = PHFetchOptions()
+            let screenshotAssets = PHAsset.fetchAssets(in: collection, options: fetchOptions)
+            
+            screenshotAssets.enumerateObjects { asset, _, _ in
+                screenshots.append(asset)
+            }
+        }
+        
+        completion(screenshots)
+    }
+} 

+ 86 - 0
QuickSearchLocation/Classes/Common/Tool/QSLCacheManager.swift

@@ -0,0 +1,86 @@
+//
+//  QSLCacheManager.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/4.
+//
+
+import Foundation
+
+class QSLCacheManager {
+    
+    // 缓存用户信息模型
+    static func cacheModel(_ model: QSLUserModel) {
+//        QSLApi.updateToken(token: model.authToken)
+        let encoder = JSONEncoder()
+        do {
+            let data = try encoder.encode(model)
+            UserDefaults.standard.set(data, forKey: "QSLUserModel")
+        } catch {
+            debugPrint("Encode wrong: \(error)")
+        }
+    }
+
+    // 获取缓存的用户信息模型
+    static func getModel() -> QSLUserModel? {
+        if let data = UserDefaults.standard.data(forKey: "QSLUserModel") {
+            let decoder = JSONDecoder()
+            do {
+                let model = try decoder.decode(QSLUserModel.self, from: data)
+//                QSLApi.updateToken(token: model.authToken)
+                return model
+            } catch {
+                debugPrint("Decode wrong: \(error)")
+            }
+        }
+        return nil
+    }
+    
+    // 清空缓存,模型清空
+    static func clearCache() {
+        
+        if let _ = UserDefaults.standard.data(forKey: "QSLUserModel") {
+//            QSLB.shared.userModel = HolaUserModel()
+            UserDefaults.standard.removeObject(forKey: "QSLUserModel")
+        }
+    }
+}
+
+// 订单模型
+extension QSLCacheManager {
+    
+    // 缓存订单模型
+    static func cacheOrderModel(_ model: QSLOrderModel) {
+        let encoder = JSONEncoder()
+        do {
+            let data = try encoder.encode(model)
+            UserDefaults.standard.set(data, forKey: "QSLOrderModel")
+            UserDefaults.standard.synchronize()
+        } catch {
+            debugPrint("Encode wrong: \(error)")
+        }
+    }
+
+    // 获取缓存的模型
+    static func getOrderModel() -> QSLOrderModel? {
+        if let data = UserDefaults.standard.data(forKey: "QSLOrderModel") {
+            let decoder = JSONDecoder()
+            do {
+                let model = try decoder.decode(QSLOrderModel.self, from: data)
+                return model
+            } catch {
+                debugPrint("Decode wrong: \(error)")
+            }
+        }
+        return nil
+    }
+    
+    // 清空缓存,模型清空
+    static func clearOrderCache() {
+        
+        if let _ = UserDefaults.standard.data(forKey: "QSLOrderModel") {
+            UserDefaults.standard.removeObject(forKey: "QSLOrderModel")
+        }
+    }
+    
+}

+ 55 - 0
QuickSearchLocation/Classes/Common/Tool/QSLGravityManager.swift

@@ -0,0 +1,55 @@
+//
+//  QSLGravityManager.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/23.
+//
+
+import Foundation
+import GravityEngineSDK
+
+var gravityInstance: GravityEngineSDK?
+
+class QSLGravityManager {
+
+    func initGE() {
+        
+        // 引力引擎
+        let config = GEConfig()
+        
+        config.appid = QSLGravityConst.appid
+        config.accessToken = QSLGravityConst.accessToken
+
+        GravityEngineSDK.start(with: config)
+        let instance = GravityEngineSDK.sharedInstance(withAppid: config.appid)
+        
+        gravityInstance = instance
+
+        // 开启自动采集
+        instance?.enableAutoTrack(GravityEngineAutoTrackEventType.eventTypeAll)
+        
+        print("启动引力引擎")
+        
+        let idfa = QSLApi.params["idfa"] as? String ?? ""
+        
+        let idfv = QSLApi.params["idfv"] as? String ?? ""
+        
+        let version = QSLApi.params["appVersionCode"] as? Int ?? 100
+        
+        instance?.initializeGravityEngine(withAsaEnable: true, withCaid1: "", withCaid2: "", withSyncAttribution: true, withChannel: "AppStore", withSuccessCallback: { response in
+            print("gravity engine initialize success, response is", response)
+            
+            // 检查订单
+            QSLBaseManager.shared.initPayCheck()
+            
+            gravityInstance?.track(QSLGravityConst.launch_show)
+        }, withErrorCallback: { error in
+            print("gravity engine initialize failed, and error is", error)
+            
+            // 检查订单
+            QSLBaseManager.shared.initPayCheck()
+        })
+        
+    }
+    
+}

+ 68 - 0
QuickSearchLocation/Classes/Common/Tool/QSLJumpManager.swift

@@ -0,0 +1,68 @@
+//
+//  QSLJumpManager.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/24.
+//
+
+import Foundation
+
+enum JumpType: Int {
+    case createInspration = 0   // 灵感创作
+    case createDiy              // 自定义创作
+    case mine                   // 我的
+    case vip                    // 会员
+    case login                  // 登录
+    case accompaniment          // 伴奏
+}
+
+class QSLJumpManager: NSObject {
+    
+    static let shared = QSLJumpManager()
+    
+    private override init() {}
+}
+
+extension QSLJumpManager {
+    
+    // 跳转到Vip页面
+    func pushToVip(type: QSLVipJumpType) {
+        
+        let vc = QSLVipController()
+        vc.type = type
+        self.rootViewController()?.pushVC(vc: vc)
+    }
+    
+    // 跳转到登录页面
+    func pushToLogin(type: QSLLoginJumpType) {
+        
+        let vc = QSLLoginViewController()
+        vc.type = type
+        self.rootViewController()?.pushVC(vc: vc)
+    }
+    
+    // 跳转到紧急联系人页面
+    func pushToContact(type: QSLContactJumpPage) {
+        
+        let vc = QSLContactController()
+        vc.type = type
+        self.rootViewController()?.pushVC(vc: vc)
+    }
+    
+    // 跳转到轨迹页面
+    func pushToRoad(type: QSLRoadJumpType, model: QSLUserModel) {
+        
+        let vc = QSLRoadController(userModel: model)
+        vc.type = type
+        self.rootViewController()?.pushVC(vc: vc)
+    }
+    
+    // 跳转到添加好友页面
+    func pushToAdd(type: QSLAddJumpType) {
+        
+        let vc = QSLAddController()
+        vc.type = type
+        self.rootViewController()?.pushVC(vc: vc)
+    }
+}
+

+ 27 - 0
QuickSearchLocation/Classes/Common/Tool/QSLLoading.swift

@@ -0,0 +1,27 @@
+//
+//  QSLLoading.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/10.
+//
+
+import ProgressHUD
+
+class QSLLoading {
+    
+    static func show() {
+        ProgressHUD.animate(nil, .circleStrokeSpin, interaction: false)
+    }
+    
+    static func hide() {
+        ProgressHUD.dismiss()
+    }
+    
+    static func success(text: String) {
+        ProgressHUD.succeed(text, interaction: false, delay: 3)
+    }
+    
+    static func error(text: String) {
+        ProgressHUD.error(text, interaction: false, delay: 3)
+    }
+}

+ 176 - 0
QuickSearchLocation/Classes/Common/Tool/QSLSocketManager.swift

@@ -0,0 +1,176 @@
+//
+//  QSLSocketManager.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/6.
+//
+
+import SocketRocket
+import Foundation
+
+enum QSLSocketStatus: UInt {
+    case connecting      // 正在连接
+    case connected       // 已连接
+    case failed          // 失败
+    case closedByServer  // 系统关闭
+    case closedByUser    // 用户关闭
+    case received        // 接收消息
+}
+
+protocol QSLSocketManagerDelegate: AnyObject {
+    func socketDidReceiveMessage(with string: String)
+}
+
+class QSLSocketManager: NSObject, SRWebSocketDelegate {
+    
+    static let shared = QSLSocketManager()
+    
+    private var webSocket: SRWebSocket?
+    private var timer: Timer?
+    private var pingTimer: Timer?  // 每10秒钟发送一次ping消息
+    private var currentCount: UInt = 0  // 当前重连次数
+    
+    var delegate: QSLSocketManagerDelegate?
+    var urlString: String = ""
+    var overtime: TimeInterval = 3.0  // 重连时间间隔,默认3秒钟
+    var reconnectCount: UInt = UInt.max  // 重连次数,默认无限次
+    var status: QSLSocketStatus = .connecting
+    
+    private override init() {
+        super.init()
+        let url = "\(QSLApi.prodWSUrl)/websocket/\(QSLBaseManager.shared.userModel.authToken)"
+        self.urlString = url
+    }
+    
+    // MARK: - Public Methods
+    
+    @objc func connect() {
+        // 先关闭连接
+        self.webSocket?.close()
+        self.webSocket?.delegate = nil
+        
+        // 后开启连接
+        if let url = URL(string: self.urlString) {
+            self.webSocket = SRWebSocket(urlRequest: URLRequest(url: url))
+            self.webSocket?.delegate = self
+        }
+        
+        self.status = .connecting
+        self.webSocket?.open()
+    }
+    
+    func close() {
+        self.webSocket?.close()
+        self.webSocket = nil
+        self.timer?.invalidate()
+        self.timer = nil
+        self.pingTimer?.invalidate()
+        self.pingTimer = nil
+    }
+    
+    func reconnect() {
+        guard currentCount < reconnectCount else {
+            print("重连次数已用完……")
+            self.timer?.invalidate()
+            self.timer = nil
+            return
+        }
+        
+        // 计数器 +1
+        currentCount += 1
+        print("\(overtime)秒后进行第\(currentCount)次重试连接……")
+        
+        // 开启定时器进行重连
+        self.timer = Timer.scheduledTimer(timeInterval: overtime, target: self, selector: #selector(connect), userInfo: nil, repeats: false)
+        RunLoop.current.add(self.timer!, forMode: .common)
+    }
+    
+    func sendMessage(_ message: String) {
+        
+        do {
+            try self.webSocket?.send(string: message)
+            print("消息已发送")
+        } catch {
+            print("发送消息失败!")
+        }
+    }
+    
+    @objc func sendPingMessage() {
+        guard status == .connected else { return }
+        
+        do {
+            try self.webSocket?.sendPing(nil)
+            print("发送心跳包")
+        } catch {
+            print("发送心跳包失败!")
+        }
+    }
+    
+    // MARK: - SRWebSocketDelegate Methods
+    
+    func webSocket(_ webSocket: SRWebSocket, didReceiveMessageWith string: String) {
+        delegate?.socketDidReceiveMessage(with: string)
+        
+        let locationMessage = QSLMapMessageModel.mapModel(from: string)
+        switch locationMessage.cmd {
+        case "d.refresh.friend.list":
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshFriend, object: nil)
+            break
+        case "d.refresh.friend.message":
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshMessage, object: nil)
+            break
+        case "d.refresh.friend.request":
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshRequest, object: nil)
+            break
+        case "d.refresh.contact":
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshContact, object: nil)
+            break
+        case "d.refresh.member":
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshMember, object: nil)
+            break
+        default:
+            break
+        }
+        print("收到信息:\(string)")
+    }
+    
+    func webSocketDidOpen(_ webSocket: SRWebSocket) {
+        print("已链接服务器:\(webSocket.url ?? URL(fileURLWithPath: ""))")
+        
+        // 重置计数器
+        self.currentCount = 0
+        self.status = .connected
+        
+        // 开启ping定时器
+        self.pingTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(sendPingMessage), userInfo: nil, repeats: true)
+        RunLoop.current.add(self.pingTimer!, forMode: .common)
+    }
+    
+    func webSocket(_ webSocket: SRWebSocket, didFailWithError error: Error) {
+        print("链接失败:\(error.localizedDescription)")
+        
+        self.status = .failed
+        
+        // 尝试重新连接
+        reconnect()
+    }
+    
+    func webSocket(_ webSocket: SRWebSocket, didCloseWithCode code: Int, reason: String?, wasClean: Bool) {
+        print("链接已关闭:code:\(code)   reason:\(String(describing: reason))")
+        
+        if code == 1000 {
+            self.status = .closedByUser
+        } else {
+            self.status = .closedByServer
+            reconnect()
+        }
+    }
+    
+    func webSocket(_ webSocket: SRWebSocket, didReceivePingWith data: Data?) {
+        print("收到 Ping")
+    }
+    
+    func webSocket(_ webSocket: SRWebSocket, didReceivePong pongData: Data?) {
+        print("收到 Pong")
+    }
+}

+ 242 - 0
QuickSearchLocation/Classes/Common/View/QSLAlertView.swift

@@ -0,0 +1,242 @@
+//
+//  QSLAlertView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/4.
+//
+
+import UIKit
+
+class QSLAlertView: UIView {
+    
+    lazy var contentView: UIView = {
+        
+        let contentView = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW - 60.rpx, height: 152.0.rpx))
+        contentView.backgroundColor = .white
+        contentView.addRadius(radius: 8.rpx)
+        return contentView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+      
+        let label = UILabel()
+        label.text("温馨提示")
+        label.mediumFont(17)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var contentLabel: UILabel = {
+        
+        let label = UILabel()
+        label.numberOfLines = 0
+        label.text("登录之后才可以发送好友申请")
+        label.font(14)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        label.changeLineSpace(space: 4)
+        return label
+    }()
+    
+    lazy var oneButton: UIButton = {
+      
+        let btn = UIButton()
+        btn.addRadius(radius: 20.rpx)
+        btn.title("去登录")
+        btn.textColor(.white)
+        btn.mediumFont(16)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 150.rpx, height: 40.rpx, direction: .horizontal)
+        btn.addTarget(self, action: #selector(oneBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var firstButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.isHidden = true
+        btn.backgroundColor = .hexStringColor(hexString: "#F8F8F8")
+        btn.addRadius(radius: 20.rpx)
+        btn.title("取消")
+        btn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+        btn.mediumFont(16)
+        btn.addTarget(self, action: #selector(firstBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var secondButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.isHidden = true
+        btn.addRadius(radius: 20.rpx)
+        btn.title("确认")
+        btn.textColor(.white)
+        btn.mediumFont(16)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 118.rpx, height: 40.rpx, direction: .horizontal)
+        btn.addTarget(self, action: #selector(secondBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var closeButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "public_btn_close_AAA"), for: .normal)
+        btn.addTarget(self, action: #selector(closeBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    var oneBtnClosure: (() -> ())?
+    
+    var firstBtnClosure: (() -> ())?
+    
+    var secondBtnClosure: (() -> ())?
+    
+    var closeBtnClosure: (() -> ())?
+    
+    class func alert(view: UIView,
+                     title: String,
+                     content: String,
+                     isOneBtn: Bool = false,
+                     contentTextAlignment: NSTextAlignment = .center,
+                     oneBtnText: String = "去登录",
+                     oneBtnClosure: @escaping () -> () = {},
+                     firstBtnClosure: @escaping () -> () = {},
+                     secondBtnClosure: @escaping () -> () = {},
+                     closeBtnClosure: @escaping () -> () = {}) {
+        
+        let window = QSLAlertView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH))
+        window.titleLabel.text = title
+        window.contentLabel.text = content
+        window.contentLabel.changeLineSpace(space: 4)
+        window.contentLabel.textAlignment = contentTextAlignment
+        window.oneButton.title(oneBtnText)
+        window.oneButton.isHidden = !isOneBtn
+        window.firstButton.isHidden = isOneBtn
+        window.secondButton.isHidden = isOneBtn
+        window.oneBtnClosure = oneBtnClosure
+        window.firstBtnClosure = firstBtnClosure
+        window.secondBtnClosure = secondBtnClosure
+        window.closeBtnClosure = closeBtnClosure
+        
+        let contentHeight = content.heightAccording(width: QSLConst.qsl_kScreenW - 108.rpx, font: UIFont.textF(15), lineSpacing: 4)
+        window.contentView.snp.remakeConstraints { make in
+            make.size.equalTo(CGSize(width: QSLConst.qsl_kScreenW - 60.rpx, height: 65.rpx + contentHeight + 92.0.rpx))
+            make.center.equalToSuperview()
+        }
+        view.addSubview(window)
+        
+        gravityInstance?.track(QSLGravityConst.friend_delete_show)
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) {
+            window.backgroundColor = .hexStringColor(hexString: "#000000", alpha: 0.7)
+            window.contentView.isHidden = false
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    
+    // 单按钮点击事件
+    @objc func oneBtnAction() {
+        if let oneBtnClosure = self.oneBtnClosure {
+            oneBtnClosure()
+        }
+        removeView()
+    }
+    
+    // 取消按钮点击事件
+    @objc func firstBtnAction() {
+        if let firstBtnClosure = self.firstBtnClosure {
+            firstBtnClosure()
+        }
+        removeView()
+    }
+    
+    // 确认按钮点击事件
+    @objc func secondBtnAction() {
+        if let secondBtnClosure = self.secondBtnClosure {
+            secondBtnClosure()
+        }
+        removeView()
+    }
+    
+    // 关闭按钮点击事件
+    @objc func closeBtnAction() {
+        if let closeBtnClosure = self.closeBtnClosure {
+            closeBtnClosure()
+        }
+        removeView()
+    }
+    
+    // 移除
+    @objc func removeView() {
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) { [weak self] in
+            self?.backgroundColor = UIColor.init(white: 0, alpha: 0)
+            self?.contentView.isHidden = true
+        } completion: { [weak self] finished in
+            self?.removeFromSuperview()
+        }
+    }
+}
+
+extension QSLAlertView {
+    
+    func initView() {
+        
+        addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: QSLConst.qsl_kScreenW - 60.rpx, height: 152.0.rpx))
+            make.center.equalToSuperview()
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(24.rpx)
+        }
+        
+        contentView.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.left.equalTo(24.rpx)
+            make.right.equalTo(-24.rpx)
+            make.top.equalTo(65.rpx)
+        }
+        
+        contentView.addSubview(oneButton)
+        oneButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 150.rpx, height: 40.rpx))
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(firstButton)
+        firstButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.left.equalTo(24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(secondButton)
+        secondButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.right.equalTo(-24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(closeButton)
+        closeButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24.rpx, height: 24.rpx))
+            make.top.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+        }
+        
+        
+    }
+}

+ 263 - 0
QuickSearchLocation/Classes/Common/View/QSLPopView.swift

@@ -0,0 +1,263 @@
+//
+//  QSLPopView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/4.
+//
+
+import UIKit
+
+class QSLPopView: UIView {
+
+    private var items: [(image: UIImage?, title: String)] = []
+    private let arrowView = UIView()
+    private let tableView = UITableView()
+    private var didSelectItem: ((Int) -> Void)?
+    
+    var offsetX: CGFloat = 0.0
+    private let arrowWidth = 20.0
+    private let radius = 8.0
+
+    var arrowHeight: CGFloat = 10.0
+    var rowHeight: CGFloat = 50.0
+    var contentWidth: CGFloat = 200.0
+    var alertBackgroundColor: UIColor? {
+        didSet {
+            arrowView.backgroundColor = alertBackgroundColor
+        }
+    }
+    var textColor: UIColor?
+    var popMaskViewBackgroundColor = UIColor.black.withAlphaComponent(0.0)
+    
+    // 新增属性,控制是否显示遮罩层
+    var isShowMaskView: Bool = false
+    
+    // 遮罩层视图
+    private var popMaskView: UIView?
+    
+    init(
+        items: [(image: UIImage?, title: String)],
+        didSelectItem: ((Int) -> Void)?
+    ) {
+        super.init(frame: .zero)
+        alertBackgroundColor = .hexStringColor(hexString: "#4B4B4B", alpha: 0.95)
+        textColor = .white
+        self.items = items
+        self.didSelectItem = didSelectItem
+        setupView()
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    private func setupView() {
+        self.backgroundColor = .clear
+//        self.layer.shadowColor = UIColor.black.cgColor
+//        self.layer.shadowOpacity = 0.2
+//        self.layer.shadowOffset = CGSize(width: 0, height: 2)
+//        self.layer.shadowRadius = 4
+        
+        arrowView.backgroundColor = alertBackgroundColor
+        addSubview(arrowView)
+        
+        tableView.backgroundColor = .clear
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.register(QSLPopViewCell.self, forCellReuseIdentifier: "QSLPopViewCell")
+        tableView.separatorStyle = .none
+        tableView.layer.cornerRadius = radius
+        tableView.clipsToBounds = true
+        tableView.isScrollEnabled = false
+        addSubview(tableView)
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        
+        let tableHeight = CGFloat(items.count) * rowHeight
+        tableView.frame = CGRect(x: 0, y: arrowHeight, width: contentWidth, height: tableHeight)
+        updateArrowPosition()
+    }
+
+    private func updateArrowPosition() {
+        let arrowCenterX = contentWidth - 20.rpx - offsetX
+        arrowView.frame = CGRect(x: arrowCenterX - arrowWidth/2, y: 0, width: arrowWidth, height: arrowHeight)
+        arrowView.layer.mask = arrowMaskLayer()
+    }
+
+    private func arrowMaskLayer() -> CAShapeLayer {
+        let path = UIBezierPath()
+        path.move(to: CGPoint(x: 0, y: arrowHeight))
+        path.addLine(to: CGPoint(x: arrowWidth, y: arrowHeight))
+        path.addLine(to: CGPoint(x: arrowWidth/2, y: 0))
+        path.close()
+
+        let shapeLayer = CAShapeLayer()
+        shapeLayer.path = path.cgPath
+        return shapeLayer
+    }
+
+    // 新增方法,添加遮罩层
+    private func addMaskView(to targetSuperview: UIView) {
+        popMaskView = UIView(frame: targetSuperview.bounds)
+        popMaskView?.backgroundColor = popMaskViewBackgroundColor
+        popMaskView?.isUserInteractionEnabled = true
+        targetSuperview.addSubview(popMaskView!)
+
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(maskViewTapped))
+        popMaskView?.addGestureRecognizer(tapGesture)
+    }
+
+    // 遮罩层点击事件
+    @objc private func maskViewTapped() {
+//        hide()
+        hide(animate: true)
+
+    }
+
+    // 显示弹窗
+    func show(from button: UIButton, isWindow: Bool = false) {
+        guard let superview = button.superview else { return }
+
+        let maxOffsetX = (contentWidth / 2) - (arrowWidth / 2) - radius
+        offsetX = min(max(-maxOffsetX, offsetX), maxOffsetX)
+
+        let targetSuperview: UIView
+        if isWindow {
+            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+               let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) {
+                targetSuperview = keyWindow
+            } else {
+                targetSuperview = superview
+            }
+        } else {
+            targetSuperview = superview
+        }
+
+        if isShowMaskView {
+            addMaskView(to: targetSuperview) // 添加遮罩层
+        }
+
+        targetSuperview.addSubview(self)
+
+        let buttonFrame = button.convert(button.bounds, to: targetSuperview)
+        let originY = buttonFrame.maxY + 5
+        var originX = buttonFrame.midX - contentWidth / 2 + offsetX
+
+        let screenWidth = UIScreen.main.bounds.width
+        if originX < 0 { originX = 0 }
+        if originX + contentWidth > screenWidth {
+            originX = screenWidth - contentWidth
+        }
+
+        self.frame = CGRect(x: originX, y: originY, width: contentWidth, height: CGFloat(items.count) * rowHeight + arrowHeight)
+        updateArrowPosition()
+    }
+    
+    func show(from button: UIButton, selfVC:UIViewController) {
+
+        let maxOffsetX = (contentWidth / 2) - (arrowWidth / 2) - radius
+        offsetX = min(max(-maxOffsetX, offsetX), maxOffsetX)
+        let targetSuperview: UIView
+        targetSuperview = selfVC.view
+
+        if isShowMaskView {
+            addMaskView(to: targetSuperview) // 添加遮罩层
+        }
+        targetSuperview.addSubview(self)
+
+        let buttonFrame = button.convert(button.bounds, to: targetSuperview)
+        let originY = buttonFrame.maxY + 5
+        var originX = buttonFrame.midX - contentWidth / 2 + offsetX
+
+        let screenWidth = UIScreen.main.bounds.width
+        if originX < 0 { originX = 0 }
+        if originX + contentWidth > screenWidth {
+            originX = screenWidth - contentWidth
+        }
+
+        self.frame = CGRect(x: originX - 25.rpx, y: originY - 1.rpx, width: contentWidth, height: CGFloat(items.count) * rowHeight + arrowHeight)
+        updateArrowPosition()
+    }
+
+    
+    func hide(animate: Bool = false) {
+        if animate {
+            // 记录原始的frame
+            let tempFrame = self.frame
+            
+            // 计算动态锚点位置
+            let anchorX = calculateAnchorPointX(from: offsetX)
+            
+            // 设置锚点为动态计算值
+            self.layer.anchorPoint = CGPoint(x: anchorX, y: 0)
+            
+            // 恢复原始的frame
+            self.frame = tempFrame
+            
+            // 使用动画将视图缩小到右上角并隐藏
+            UIView.animate(withDuration: 0.2, animations: {
+                self.alpha = 0.0
+                self.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
+            }) { _ in
+                // 动画结束后移除视图和遮罩层
+                self.removeFromSuperview()
+                self.popMaskView?.removeFromSuperview()
+
+                // 恢复视图的初始状态
+                self.alpha = 1.0
+                self.transform = .identity
+                self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) // 恢复锚点为默认中心
+            }
+        } else {
+            // 无动画,直接移除视图和遮罩层
+            self.removeFromSuperview()
+            self.popMaskView?.removeFromSuperview()
+            self.alpha = 1.0
+        }
+    }
+
+    func calculateAnchorPointX(from offsetX: CGFloat) -> CGFloat {
+        // 计算合适的锚点X值
+        let maxOffsetX = (contentWidth / 2) - (arrowWidth / 2) - radius
+        let normalizedOffsetX = min(max(-maxOffsetX, offsetX), maxOffsetX) // 限制 offsetX 范围
+
+        // 当offsetX为负时,锚点应该往右
+        // 当offsetX为正时,锚点应该往左
+        let anchorX = 0.5 - (normalizedOffsetX / contentWidth)
+        
+        return anchorX
+    }
+
+
+
+}
+
+extension QSLPopView: UITableViewDelegate, UITableViewDataSource {
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return items.count
+    }
+
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        guard let cell = tableView.dequeueReusableCell(withIdentifier: "QSLPopViewCell", for: indexPath) as? QSLPopViewCell else {
+            return UITableViewCell()
+        }
+        let item = items[indexPath.row]
+        cell.selectionStyle = .none
+        cell.backgroundColor = alertBackgroundColor
+        cell.iconImageView.image = item.image
+        cell.titleLabel.textColor = textColor
+        cell.titleLabel.text = item.title
+        return cell
+    }
+
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return rowHeight
+    }
+
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        didSelectItem?(indexPath.row)
+        hide(animate: true)
+    }
+}

+ 44 - 0
QuickSearchLocation/Classes/Common/View/QSLPopViewCell.swift

@@ -0,0 +1,44 @@
+//
+//  QSLPopViewCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/4.
+//
+
+import UIKit
+
+class QSLPopViewCell: UITableViewCell {
+
+    let iconImageView: UIImageView = {
+        let imageView = UIImageView()
+        imageView.contentMode = .scaleAspectFit
+        return imageView
+    }()
+
+    let titleLabel: UILabel = {
+        let label = UILabel()
+        label.font = UIFont.systemFont(ofSize: 14)
+        label.textColor = .black
+        return label
+    }()
+
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+//        contentView.addSubview(iconImageView)
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+//        let imageSize: CGFloat = 24
+//        iconImageView.frame = CGRect(x: 15, y: (contentView.frame.height - imageSize) / 2, width: imageSize, height: imageSize)
+//        titleLabel.frame = CGRect(x: iconImageView.frame.maxX + 10, y: 0, width: contentView.frame.width - iconImageView.frame.maxX - 25, height: contentView.frame.height)
+    }
+}

+ 230 - 0
QuickSearchLocation/Classes/Common/View/QSLPrivacyAlertView.swift

@@ -0,0 +1,230 @@
+//
+//  QSLPrivacyAlertView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/9.
+//
+
+import UIKit
+import YYText
+
+class QSLPrivacyAlertView: UIView {
+    
+    lazy var contentView: UIView = {
+        
+        let contentView = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW - 60.rpx, height: 152.0.rpx))
+        contentView.backgroundColor = .white
+        contentView.addRadius(radius: 8.rpx)
+        return contentView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+      
+        let label = UILabel()
+        label.text("温馨提示")
+        label.mediumFont(17)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var contentLabel: YYLabel = {
+       
+        let label = YYLabel()
+        label.textAlignment = .center
+        label.numberOfLines = 0
+        
+        let attr = NSMutableAttributedString()
+        
+        let firstAttr = NSMutableAttributedString(string: "亲爱的用户,感谢您使用我们产品,为了更好的为您服务,我们可能向系统申请以下必要权限:定位信息权限、存储空间、电话权限等,用于应用的基本服务和功能),你有权拒绝或者撤回权限。 本软件非常重视您的隐私和个人信息,在您使用之前请仔细阅读")
+        firstAttr.yy_lineSpacing = 5
+        firstAttr.font(14)
+        firstAttr.color(.hexStringColor(hexString: "#404040"))
+        attr.append(firstAttr)
+        
+        let privacyHL = YYTextHighlight()
+        let privacyText = NSMutableAttributedString(string: "《隐私权政策》")
+        privacyText.yy_lineSpacing = 5
+        privacyText.font(14)
+        privacyText.color(.hexStringColor(hexString: "#2F79FF"))
+        privacyText.yy_setTextHighlight(privacyHL, range: NSRange(location: 0, length: "《隐私权政策》".count))
+        privacyHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.privacyAction()
+        }
+        attr.append(privacyText)
+
+        let andAttr = NSMutableAttributedString(string: "和")
+        andAttr.yy_lineSpacing = 5
+        andAttr.font(14)
+        andAttr.color(.hexStringColor(hexString: "#404040"))
+        attr.append(andAttr)
+    
+        let serviceHL = YYTextHighlight()
+        let serviceText = NSMutableAttributedString(string: "《服务条款》")
+        serviceText.yy_lineSpacing = 5
+        serviceText.font(14)
+        serviceText.color(.hexStringColor(hexString: "#2F79FF"))
+        serviceText.yy_setTextHighlight(serviceHL, range: NSRange(location: 0, length: "《服务条款》".count))
+        serviceHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.serviceAction()
+        }
+        attr.append(serviceText)
+        
+        let remainAttr = NSMutableAttributedString(string: "全文,如您同意,请点击点击下方的“同意”按钮。")
+        remainAttr.yy_lineSpacing = 5
+        remainAttr.font(14)
+        remainAttr.color(.hexStringColor(hexString: "#404040"))
+        attr.append(remainAttr)
+        
+        label.attributedText = attr
+        
+        return label
+    }()
+    
+    lazy var firstButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.backgroundColor = .hexStringColor(hexString: "#F8F8F8")
+        btn.addRadius(radius: 20.rpx)
+        btn.title("不同意")
+        btn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+        btn.mediumFont(16)
+        btn.addTarget(self, action: #selector(firstBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var secondButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.addRadius(radius: 20.rpx)
+        btn.title("同意")
+        btn.textColor(.white)
+        btn.mediumFont(16)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 118.rpx, height: 40.rpx, direction: .horizontal)
+        btn.addTarget(self, action: #selector(secondBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    var secondBtnClosure: (() -> ())?
+    
+    var serviceBlock: (() -> Void)?
+    
+    var privacyBlock: (() -> Void)?
+    
+    var isFirstClick: Bool = true
+    
+    class func alert(view: UIView,
+    secondBtnClosure: @escaping () -> () = {}, serviceCloure: @escaping () -> Void, privacyClosure: @escaping () -> Void) {
+        
+        let window = QSLPrivacyAlertView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH))
+        window.secondBtnClosure = secondBtnClosure
+        window.serviceBlock = serviceCloure
+        window.privacyBlock = privacyClosure
+        view.addSubview(window)
+        
+        gravityInstance?.track(QSLGravityConst.privacy_show)
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) {
+            window.backgroundColor = .hexStringColor(hexString: "#000000", alpha: 0.7)
+            window.contentView.isHidden = false
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // 取消按钮点击事件
+    @objc func firstBtnAction() {
+        if isFirstClick {
+            self.firstButton.title("不同意并退出")
+            isFirstClick = false
+            gravityInstance?.track(QSLGravityConst.privacy_disagree)
+        } else {
+            gravityInstance?.track(QSLGravityConst.privacy_second_disagree)
+            exit(0)
+        }
+    }
+    
+    // 确认按钮点击事件
+    @objc func secondBtnAction() {
+        if isFirstClick {
+            gravityInstance?.track(QSLGravityConst.privacy_agree)
+        } else {
+            gravityInstance?.track(QSLGravityConst.privacy_second_agree)
+        }
+        if let secondBtnClosure = self.secondBtnClosure {
+            secondBtnClosure()
+        }
+        removeView()
+    }
+    
+    // 移除
+    @objc func removeView() {
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) { [weak self] in
+            self?.backgroundColor = UIColor.init(white: 0, alpha: 0)
+            self?.contentView.isHidden = true
+        } completion: { [weak self] finished in
+            self?.removeFromSuperview()
+        }
+    }
+    
+    @objc func privacyAction() {
+        
+        if let privacyBlock = self.privacyBlock {
+            privacyBlock()
+        }
+    }
+    
+    @objc func serviceAction() {
+        
+        if let serviceBlock = self.serviceBlock {
+            serviceBlock()
+        }
+    }
+}
+
+extension QSLPrivacyAlertView {
+    
+    func initView() {
+        
+        addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: QSLConst.qsl_kScreenW - 60.rpx, height: 330.0.rpx))
+            make.center.equalToSuperview()
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(24.rpx)
+        }
+        
+        contentView.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.left.equalTo(24.rpx)
+            make.right.equalTo(-24.rpx)
+            make.top.equalTo(55.rpx)
+            make.height.equalTo(200.rpx)
+        }
+        
+        contentView.addSubview(firstButton)
+        firstButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.left.equalTo(24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(secondButton)
+        secondButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.right.equalTo(-24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+    }
+}

+ 36 - 0
QuickSearchLocation/Classes/Main/AppDelegate.swift

@@ -0,0 +1,36 @@
+//
+//  AppDelegate.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+import AMapFoundationKit
+import IQKeyboardManagerSwift
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+        // Override point for customization after application launch.
+        
+        Thread.sleep(forTimeInterval: 1)
+        IQKeyboardManager.shared.enable = true
+        AMapServices.shared().apiKey = QSLConfig.MapKey
+        
+        return true
+    }
+
+    
+    // MARK: UISceneSession Lifecycle
+    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
+        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+    }
+
+    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
+    }
+
+
+}
+

+ 24 - 0
QuickSearchLocation/Classes/Main/Base.lproj/Main.storyboard

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
+                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+</document>

+ 112 - 0
QuickSearchLocation/Classes/Main/CustomTabBarController.swift

@@ -0,0 +1,112 @@
+//
+//  CustomTabBarController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+class CustomTabBarController: UITabBarController {
+    
+    lazy var blurTabBar: UIView = {
+       
+        let _blurTabBar = UIView(frame: CGRect(x: 0, y: -1, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH))
+        _blurTabBar.isUserInteractionEnabled = true
+        self.tabBar.addSubview(_blurTabBar)
+        return _blurTabBar
+    }()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        // 设置 tabbar 样式
+        self.setTabBarAppearence()
+        // 添加 tab 页
+        self.addFoundationTab()
+        // 默认进入页面到主页
+        self.selectedIndex = 1
+    }
+    
+    func addFoundationTab() {
+        
+        self.addFriendTab()
+        self.addHomeTab()
+        self.addCheckTab()
+        self.addMineTab()
+    }
+}
+
+extension CustomTabBarController {
+    
+    func setTabBarAppearence() {
+        
+        self.tabBar.backgroundColor = .white
+        self.tabBar.shadowImage = UIImage.image(color: .hexStringColor(hexString: "#EEEEEE"), size: CGSize(width: QSLConst.qsl_kScreenW, height: 1))
+    }
+}
+
+extension CustomTabBarController: UITabBarControllerDelegate {
+    
+    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
+        switch item.tag {
+        case 0:
+            gravityInstance?.track(QSLGravityConst.tab_friend)
+            break
+        case 1:
+            gravityInstance?.track(QSLGravityConst.tab_location)
+            break
+        case 2:
+            gravityInstance?.track(QSLGravityConst.tab_message)
+            break
+        case 3:
+            gravityInstance?.track(QSLGravityConst.tab_mine)
+            break
+        default:
+            break
+        }
+    }
+}
+
+extension CustomTabBarController {
+    
+    /// 添加 好友 Tab
+    func addFriendTab() {
+        
+        let imageInsets = UIEdgeInsets.zero
+        let titlePostion = UIOffset(horizontal: 0, vertical: 0)
+        
+        let vc = QSLFriendController()
+        self.addChildViewController(childVc: vc, title: "好友", image: "tab_friends_normal", selectedImage: "tab_friends_selected", imageInsets: imageInsets, titlePosition: titlePostion, index: 0)
+    }
+    
+    /// 添加 主页定位 Tab
+    func addHomeTab() {
+        
+        let imageInsets = UIEdgeInsets.zero
+        let titlePostion = UIOffset(horizontal: 0, vertical: 0)
+        
+        let vc = QSLHomeController()
+        self.addChildViewController(childVc: vc, title: "定位", image: "tab_location_normal", selectedImage: "tab_location_selected", imageInsets: imageInsets, titlePosition: titlePostion, index: 1)
+    }
+    
+    /// 添加 消息 Tab
+    func addCheckTab() {
+        
+        let imageInsets = UIEdgeInsets.zero
+        let titlePostion = UIOffset(horizontal: 0, vertical: 0)
+        
+        let vc = QSLMessageController()
+        self.addChildViewController(childVc: vc, title: "消息", image: "tab_message_normal", selectedImage: "tab_message_selected", imageInsets: imageInsets, titlePosition: titlePostion, index: 2)
+    }
+    
+    /// 添加我的 Tab
+    func addMineTab() {
+        
+        let imageInsets = UIEdgeInsets.zero
+        let titlePostion = UIOffset(horizontal: 0, vertical: 0)
+        
+        let vc = QSLMineController()
+        self.addChildViewController(childVc: vc, title: "我的", image: "tab_mine_normal", selectedImage: "tab_mine_selected", imageInsets: imageInsets, titlePosition: titlePostion, index: 3)
+    }
+}

+ 233 - 0
QuickSearchLocation/Classes/Main/QSLBaseManager.swift

@@ -0,0 +1,233 @@
+//
+//  QSLBaseManager.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/4.
+//
+
+import Foundation
+import UIKit
+import AppTrackingTransparency
+import AdSupport
+import StoreKit
+import AdServices
+
+class QSLBaseManager {
+    
+    static let shared = QSLBaseManager()
+    
+    var userModel = QSLUserModel()
+    
+    private init() {}
+    
+    public func initConfig() {
+        
+        if let model = QSLCacheManager.getModel() {
+            self.userModel = model
+            QSLApi.updateToken(token: self.userModel.authToken)
+        }
+        
+        initializeSystem()
+        initIDFA()
+        initPayCheck()
+    }
+}
+
+extension QSLBaseManager {
+    
+    // 更新用户模型
+    func updateUser(model: QSLUserModel) {
+        
+        self.userModel = model
+        self.userModel.isMine = true
+        QSLCacheManager.cacheModel(userModel)
+    }
+    
+    // 登录更新模型
+    func loginUpdateUser(authToken: String, phone: String) {
+        QSLApi.updateToken(token: authToken)
+        userModel.authToken = authToken
+        userModel.phone = phone
+        userModel.remark = "自己"
+        userModel.isMine = true
+        QSLCacheManager.cacheModel(userModel)
+    }
+    
+    // 保存用户id
+    func saveUserId(id: String) {
+        
+        userModel.userId = id
+        QSLCacheManager.cacheModel(userModel)
+    }
+    
+    // 保存vip时间
+    func saveVipExpiredTime(time: Int) {
+        
+        if time != 0 {
+            self.userModel.vipEndTimestamp = Date(timeIntervalSince1970: TimeInterval(time / 1000))
+        } else {
+            self.userModel.vipEndTimestamp = nil
+        }
+    }
+    
+    // 是否登录状态
+    func isLogin() -> Bool {
+        
+        if userModel.authToken != "" {
+            return true
+        }
+        return false
+    }
+    
+    // 是否为vip
+    func isVip() -> Bool {
+        
+        if userModel.vipEndTimestamp?.compare(NSDate.now) == .orderedDescending {
+            return true
+        }
+        return false
+    }
+    
+    // 退出登录
+    func logOut() {
+        
+        print("用户退出登录")
+        
+        QSLCacheManager.clearCache()
+        QSLApi.updateToken(token: "")
+        self.saveVipExpiredTime(time: 0)
+        self.userModel.phone = ""
+        self.userModel.authToken = ""
+        self.userModel.userId = ""
+        self.userModel.remark = "自己"
+        self.userModel.memberModel = QSLMemberModel()
+        
+        // 发送通知
+        NotificationCenter.default.post(name: QSLNotification.QSLLogout, object: nil)
+    }
+}
+
+extension QSLBaseManager {
+    
+    // 检查漏单
+    func initPayCheck() {
+        
+        if let order = QSLCacheManager.getOrderModel() {
+            if order.receiptData.count > 0 {
+                if !order.isFinish {
+                    QSLVipManager.shared.orderModel = order
+                    QSLVipManager.shared.openAppRePay(receiptData: order.receiptData) { status, number in
+                        
+                    }
+                }
+            }
+        }
+    }
+}
+
+extension QSLBaseManager {
+    
+    func initializeSystem() {
+        
+        // 获取 APP 名称
+        if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String {
+            QSLApi.getAppName(appName: appName)
+        } else {
+            print("App名称未找到")
+        }
+        
+        // 获取包名
+        if let bundleIdentifier = Bundle.main.bundleIdentifier {
+            QSLApi.getPackageName(packageName: bundleIdentifier)
+            debugPrint("Bundle ID: \(bundleIdentifier)")
+        } else {
+            debugPrint("Unable to retrieve bundle identifier")
+        }
+        
+        // 获取版本号
+        if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
+            debugPrint("App Version: \(appVersion)")
+            QSLApi.getAppVersionName(appVersionName: appVersion)
+            
+            let array = appVersion.components(separatedBy: ".")
+            var appVersionCode = ""
+            for i in array {
+                appVersionCode.append(i)
+            }
+            if appVersionCode.count < 3 {
+                appVersionCode.append("0")
+            }
+            QSLApi.getAppVersionCode(appVersionCode: Int(appVersionCode) ?? 100)
+        } else {
+            debugPrint("Unable to retrieve app version")
+        }
+        
+        if let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String {
+            debugPrint("当前的Build号: \(buildNumber)")
+        }
+        
+        // 获取系统版本
+        let osVersion = String(format: "%@%@", UIDevice.current.systemName, UIDevice.current.systemVersion)
+        QSLApi.getOSVersion(osVersion: osVersion)
+            
+        // 获取 设置uuid 并更新
+        if let uuid = UIDevice.current.identifierForVendor?.uuidString {
+            QSLApi.getIDFV(newIDFV: uuid)
+            debugPrint("IDFV ID: \(uuid)")
+        } else {
+            debugPrint("获取 IDFV ID 参数失败")
+        }
+    }
+    
+    func initIDFA() {
+        
+        DispatchQueue.global().asyncAfter(deadline: .now() + 0.1){
+            if #available(iOS 14.0, *) {
+                // 用户请求授权获得IDFA权限
+                ATTrackingManager.requestTrackingAuthorization { status in
+                    
+                    if status == .authorized {
+                        
+                        debugPrint("授权成功")
+                        // 用户已授权
+                        let idfa = ASIdentifierManager.shared().advertisingIdentifier
+                        debugPrint("IDFA: \(idfa.uuidString)")
+                        QSLApi.getIDFA(newIDFA: idfa.uuidString)
+                        
+                        self.initGE()
+                    } else {
+                        
+                        debugPrint("无法获取idfa")
+                        self.initGE()
+                    }
+                }
+            } else {
+                
+                // 检查用户是否授权应用使用广告标识符
+                if ASIdentifierManager.shared().isAdvertisingTrackingEnabled {
+                    // 获取 IDFA
+                    let idfa = ASIdentifierManager.shared().advertisingIdentifier
+                    QSLApi.getIDFA(newIDFA: idfa.uuidString)
+                    debugPrint("IDFA: \(idfa)")
+                    
+                    self.initGE()
+                } else {
+                    debugPrint("用户拒绝广告追踪")
+                    
+                    self.initGE()
+                }
+            }
+        }
+    }
+    
+    // 引力引擎初始化
+    func initGE() {
+        
+        DispatchQueue.main.async {
+            if gravityInstance == nil {
+                let gravity = QSLGravityManager()
+                gravity.initGE()
+            }
+        }
+    }
+}

+ 49 - 0
QuickSearchLocation/Classes/Main/SceneDelegate.swift

@@ -0,0 +1,49 @@
+//
+//  SceneDelegate.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+
+    var window: UIWindow?
+
+    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+        
+        guard let _ = (scene as? UIWindowScene) else { return }
+    }
+
+    func sceneDidDisconnect(_ scene: UIScene) {
+        // Called as the scene is being released by the system.
+        // This occurs shortly after the scene enters the background, or when its session is discarded.
+        // Release any resources associated with this scene that can be re-created the next time the scene connects.
+        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
+    }
+
+    func sceneDidBecomeActive(_ scene: UIScene) {
+        // Called when the scene has moved from an inactive state to an active state.
+        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
+    }
+
+    func sceneWillResignActive(_ scene: UIScene) {
+        // Called when the scene will move from an active state to an inactive state.
+        // This may occur due to temporary interruptions (ex. an incoming phone call).
+    }
+
+    func sceneWillEnterForeground(_ scene: UIScene) {
+        // Called as the scene transitions from the background to the foreground.
+        // Use this method to undo the changes made on entering the background.
+    }
+
+    func sceneDidEnterBackground(_ scene: UIScene) {
+        // Called as the scene transitions from the foreground to the background.
+        // Use this method to save data, release shared resources, and store enough scene-specific state information
+        // to restore the scene back to its current state.
+    }
+
+
+}
+

+ 76 - 0
QuickSearchLocation/Classes/Main/ViewController.swift

@@ -0,0 +1,76 @@
+//
+//  ViewController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+class ViewController: UIViewController {
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        let isNotAgreePrivacyKey = UserDefaults.standard.bool(forKey: "isNotAgreePrivacyKey")
+        // 是否同意隐私政策
+        if !isNotAgreePrivacyKey {
+            QSLPrivacyAlertView.alert(view: view) {
+                
+                self.intoMain()
+                
+                UserDefaults.standard.set(true, forKey: "isNotAgreePrivacyKey")
+            } serviceCloure: {
+                self.agreementAction()
+            } privacyClosure: {
+                self.privacyAction()
+            }
+
+        } else {
+            intoMain()
+        }
+    }
+    
+    func intoMain() {
+        
+        QSLBaseManager.shared.initConfig()
+        
+        // 是否通过引导页
+        let isShowGuideKey = UserDefaults.standard.bool(forKey: "isShowGuideKey")
+        if !isShowGuideKey {
+            // 引导页
+            UserDefaults.standard.set(true, forKey: "isShowGuideKey")
+            if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate {
+                sceneDelegate.window?.rootViewController = QSLGuideController()
+                sceneDelegate.window?.makeKeyAndVisible()
+            }
+        } else {
+            // 主页
+            if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate {
+                sceneDelegate.window?.rootViewController = CustomTabBarController()
+                sceneDelegate.window?.makeKeyAndVisible()
+            }
+        }
+    }
+    
+    // 用户协议跳转
+    func agreementAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppServiceAgreementLink
+        vc.title = "用户协议"
+        vc.isShowTop = true
+        self.present(vc, animated: true)
+    }
+    
+    // 隐私协议跳转
+    func privacyAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppPrivacyAgreementLink
+        vc.title = "隐私政策"
+        vc.isShowTop = true
+        self.present(vc, animated: true)
+    }
+}
+

+ 1 - 0
QuickSearchLocation/Classes/Main/zh-Hans.lproj/Main.strings

@@ -0,0 +1 @@
+

+ 279 - 0
QuickSearchLocation/Classes/Network/QSLNetwork.swift

@@ -0,0 +1,279 @@
+//
+//  QSLNetwork.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/4.
+//
+
+import Foundation
+import Moya
+import MoyaMapper
+
+struct NetParameter: ModelableParameterType {
+    var successValue = "0"
+    var statusCodeKey = "code"
+    var tipStrKey = "msg"
+    var modelKey = "data"
+}
+
+let QSLNetworkProvider = MoyaProvider<QSLNetworkAPI>(plugins: [networkLoggerPlugin, MoyaMapperPlugin(NetParameter())])
+let networkLoggerPlugin = NetworkLoggerPlugin()
+
+
+enum QSLNetworkAPI {
+    
+    case userCode(dict: [String: Any])
+    case userMember(dict: [String: Any])
+    case userLogin(dict: [String: Any])
+    case userClear(dict: [String: Any])
+    case friendList(dict: [String: Any])
+    case requestSend(dict: [String: Any])
+    case requestList(dict: [String: Any])
+    case messageList(dict: [String: Any])
+    case requestAccept(dict: [String: Any])
+    case requestRefuse(dict: [String: Any])
+    case friendDelete(dict: [String: Any])
+    case friendRemark(dict: [String: Any])
+    case friendBlocked(dict: [String: Any])
+    case friendGet(dict: [String: Any])
+    case locationTrackQuery(dict: [String: Any])
+    case vipItemList(dict: [String: Any])
+    case vipOrderSubmitAndPay(dict: [String: Any])
+    case vipOrderPayStatus(dict: [String: Any])
+    case contactList(dict: [String: Any])
+    case contactCreate(dict: [String: Any])
+    case contactFavor(dict: [String: Any])
+    case contactDelete(dict: [String: Any])
+    case contactMayday(dict: [String: Any])
+    case contactMaydayFavor(dict: [String: Any])
+    case contactMaydayAll(dict: [String: Any])
+}
+
+extension QSLNetworkAPI: TargetType {
+    
+    // 地址
+    public var baseURL: URL {
+        switch QSLApi.environment {
+        case .local:
+            return URL(string: QSLApi.LocalUrl)!
+        case .dev:
+            return URL(string: QSLApi.devUrl)!
+        case .prod:
+            return URL(string: QSLApi.prodUrl)!
+        }
+    }
+    
+    var path: String {
+        switch self {
+        case .userCode: return QSLApi.user_code
+        case .userMember: return QSLApi.user_member
+        case .userLogin: return QSLApi.user_login
+        case .userClear: return QSLApi.user_clear
+        case .friendList: return QSLApi.friend_list
+        case .requestSend: return QSLApi.request_send
+        case .requestList: return QSLApi.request_list
+        case .messageList: return QSLApi.message_list
+        case .requestAccept: return QSLApi.request_accept
+        case .requestRefuse: return QSLApi.request_refuse
+        case .friendDelete: return QSLApi.friend_delete
+        case .friendRemark: return QSLApi.friend_remark
+        case .friendBlocked: return QSLApi.friend_blocked
+        case .friendGet: return QSLApi.friend_get
+        case .locationTrackQuery: return QSLApi.location_track_query
+        case .vipItemList: return QSLApi.vip_item_list
+        case .vipOrderSubmitAndPay: return QSLApi.vip_order_submitAndPay
+        case .vipOrderPayStatus: return QSLApi.vip_order_payStatus
+        case .contactList: return QSLApi.contact_list
+        case .contactCreate: return QSLApi.contact_create
+        case .contactFavor: return QSLApi.contact_favor
+        case .contactDelete: return QSLApi.contact_delete
+        case .contactMayday: return QSLApi.contact_mayday
+        case .contactMaydayFavor: return QSLApi.contact_mayday_favor
+        case .contactMaydayAll: return QSLApi.contact_mayday_all
+        }
+    }
+    
+    var method: Moya.Method { return .post }
+    
+    var task: Task {
+        var parameters : [String:Any] = QSLApi.params
+        switch self {
+        case let .userCode(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .userMember(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .userLogin(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .userClear(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .friendList(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .requestSend(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .requestList(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .messageList(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .requestAccept(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .requestRefuse(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .friendDelete(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .friendRemark(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .friendBlocked(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .friendGet(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .locationTrackQuery(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .vipItemList(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .vipOrderSubmitAndPay(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .vipOrderPayStatus(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .contactList(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .contactCreate(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .contactFavor(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .contactDelete(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .contactMayday(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .contactMaydayFavor(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        case let .contactMaydayAll(Dict):
+            for (key, value) in Dict {
+                parameters[key] = value
+            }
+            break
+        }
+        debugPrint(parameters)
+        return .requestParameters(parameters: parameters, encoding: JSONEncoding.default)
+    }
+    
+    var sampleData: Data { return "".data(using: String.Encoding.utf8)! }
+    
+    var headers: [String : String]? {
+        return nil
+    }
+}
+
+struct QSLNetwork {
+    
+    ///先添加一个闭包用于成功时后台返回数据的回调
+    typealias completeCallback = ((Response) -> (Void))
+    typealias failCallback = ((Int, String) -> (Void))
+    
+    func request(_ target: QSLNetworkAPI, success: @escaping completeCallback, fail: @escaping failCallback){
+
+        QSLNetworkProvider.request(target) { (result) in
+            
+            switch result {
+            case let .success(response):
+                
+                if response.statusCode == 200 {
+                    if response.toJSON(modelKey: "code") == 0 {
+                        success(response)
+                    } else {
+                        fail(response.toJSON(modelKey: "code").intValue, response.toJSON(modelKey: "msg").stringValue)
+                    }
+                } else if response.statusCode == 1006 {
+                    if QSLBaseManager.shared.isLogin() {
+                        QSLBaseManager.shared.logOut()
+                    }
+                    UIApplication.keyWindow?.toast(text: "登录状态失效")
+                } else {
+                    fail(response.statusCode, response.toJSON(modelKey: "msg").stringValue)
+                }
+                 
+                #if DEBUG
+                print("data:\(response.fetchJSONString())")
+                #endif
+                
+                break
+            case let .failure(error):
+                fail(error.errorCode, "网络错误,请检查后重试")
+                //网络连接失败,提示用户
+                UIApplication.keyWindow?.toast(text: "网络错误,请检查后重试")
+                break
+            }
+        }
+    }
+}

File diff suppressed because it is too large
+ 350 - 0
QuickSearchLocation/Classes/Pages/QSLAdd/Controller/QSLAddController.swift


+ 284 - 0
QuickSearchLocation/Classes/Pages/QSLAlert/View/QSLFriendAddAlertView.swift

@@ -0,0 +1,284 @@
+//
+//  QSLFriendAddAlertView.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/12.
+//
+
+import UIKit
+
+class QSLFriendAddAlertView: UIView {
+    
+    var viewController: UIViewController?
+    
+    var clickClosure: (() -> Void)?
+    
+    lazy var addBgView: UIView = {
+        
+        let view = UIView()
+        view.backgroundColor = UIColor.hexStringColor(hexString: "#000000", alpha: 0.5)
+        view.alpha = 0
+        
+        let tap = UITapGestureRecognizer(target: self, action: #selector(removeView))
+        view.addGestureRecognizer(tap)
+        return view
+    }()
+    
+    lazy var addContentView: UIView = {
+        
+        let view = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH - 100))
+        view.backgroundColor = .white
+        view.addCorner(conrners: [.topLeft, .topRight], radius: 20)
+        return view
+    }()
+    
+    lazy var addCloseButton: UIButton = {
+    
+        let button = UIButton(type: .custom)
+        button.image(UIImage(named: "public_btn_close_AAA"))
+        button.addTarget(self, action: #selector(removeView), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var addBgImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_add_bg")
+        return imageView
+    }()
+    
+    lazy var addMainView: UIView = {
+       
+        let scale = 1080.0 / 860.0
+        let topBgHeight = qsl_kScreenW / scale
+        let mainViewHeight = qsl_kScreenH - topBgHeight - 50.0
+        
+        let view = UIView(frame: CGRect(x: 0, y: topBgHeight - 50, width: qsl_kScreenW, height: mainViewHeight))
+        view.backgroundColor = .white
+        
+        let aPath = UIBezierPath()
+        aPath.lineWidth = 1.0
+        aPath.lineCapStyle = .round
+        aPath.lineJoinStyle = .round
+        
+        aPath.move(to: CGPoint(x: 0, y: 20))
+        aPath.addQuadCurve(to: CGPoint(x: qsl_kScreenW, y: 20), controlPoint: CGPoint(x: qsl_kScreenW / 2, y: -20))
+        aPath.addLine(to: CGPoint(x: qsl_kScreenW, y: mainViewHeight))
+        aPath.addLine(to: CGPoint(x: 0, y: mainViewHeight))
+        aPath.addLine(to: CGPoint(x: 0, y: 20))
+        
+        let maskLayer = CAShapeLayer()
+        maskLayer.frame = view.bounds
+        maskLayer.path = aPath.cgPath
+        
+        view.layer.mask = maskLayer
+        
+        return view
+    }()
+    
+    lazy var addMainTitleLabel: UILabel = {
+       
+        let label = UILabel()
+        label.mediumFont(20)
+        label.textColor = QSLColor.textColor_333
+        label.text = "添加好友"
+        return label
+    }()
+    
+    lazy var addMainSubTitleLabel: UILabel = {
+       
+        let label = UILabel()
+        label.font(13)
+        label.textColor = QSLColor.Color_888
+        label.text = "查看实时定位,开启轨迹守护"
+        return label
+    }()
+    
+    lazy var addMainPhoneView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addRadius(radius: 6)
+        return view
+    }()
+    
+    lazy var addMainPhoneTextField: UITextField = {
+        
+        let textF = UITextField()
+        textF.font = UIFont.textF(16)
+        textF.textColor = QSLColor.textColor_333
+        textF.keyboardType = .phonePad
+        textF.textAlignment = .left
+        textF.maxTextNumber = 11
+        
+        textF.setPlaceholderAttribute(font: UIFont.textF(16), color: QSLColor.textColor_AAA)
+        textF.placeholder = "输入手机号码 查看定位"
+        
+        return textF
+    }()
+    
+    lazy var addMainAddrBookBtn: UIButton = {
+       
+        let button = UIButton(type: .custom)
+        button.font(16)
+        button.textColor(QSLColor.textColor_333)
+        button.title("通讯录")
+        return button
+    }()
+    
+    lazy var lineIcon: UIImageView = {
+       
+        let _lineIcon = UIImageView()
+        _lineIcon.image = UIImage.image(color: UIColor.hexStringColor(hexString: "#AAAAAA", alpha: 0.4), size: CGSize(width: 1, height: 24))
+        return _lineIcon
+    }()
+    
+    lazy var addMainSearchBtn: UIButton = {
+       
+        let button = UIButton.normal()
+        button.title("立即添加")
+        button.addRadius(radius: 22)
+        button.isEnabled = false
+        return button
+    }()
+    
+    lazy var addMainTipsLabel: UILabel = {
+       
+        let label = UILabel()
+        
+        label.numberOfLines = 0
+        label.textAlignment = .center
+        
+        label.font(12)
+        label.textColor = QSLColor.textColor_D1
+        label.text = "温馨提示:文本在线配置内容,直接使用添加好友文本配置接口。"
+        return label
+    }()
+    
+    class func show(vc: UIViewController, clickClosure: @escaping () -> Void) {
+        
+        let window = QSLFriendAddAlertView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH))
+        window.viewController = vc
+        window.clickClosure = clickClosure
+        vc.view.addSubview(window)
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) {
+            window.addBgView.alpha = 1
+            window.addContentView.qsl_y = 100
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        self.setUpUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // 关闭本view
+    @objc func removeView() {
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) { [weak self] in
+            self?.addBgView.alpha = 0
+            self?.addContentView.qsl_y = QSLConst.qsl_kScreenH
+        } completion: { [weak self] finished in
+            self?.removeFromSuperview()
+        }
+    }
+    
+}
+
+extension QSLFriendAddAlertView {
+    
+    func setUpUI() {
+        
+        self.backgroundColor = .clear
+        
+        addSubview(addBgView)
+        addSubview(addContentView)
+        
+        addContentView.addSubview(addBgImageView)
+        addContentView.addSubview(addCloseButton)
+        addContentView.addSubview(addMainView)
+        
+        addMainView.addSubview(addMainTitleLabel)
+        addMainView.addSubview(addMainSubTitleLabel)
+        addMainView.addSubview(addMainPhoneView)
+        
+        addMainPhoneView.addSubview(addMainAddrBookBtn)
+        addMainPhoneView.addSubview(lineIcon)
+        addMainPhoneView.addSubview(addMainPhoneTextField)
+        
+        addMainView.addSubview(addMainSearchBtn)
+        addMainView.addSubview(addMainTipsLabel)
+        
+        addBgView.snp.makeConstraints { make in
+            make.edges.equalTo(0)
+        }
+        
+        addContentView.snp.makeConstraints { make in
+            make.top.equalTo(100)
+            make.left.right.bottom.equalTo(0)
+        }
+        addContentView.qsl_y = qsl_kScreenH
+        
+        let scale = 1080.0 / 860.0
+        let topBgHeight = qsl_kScreenW / scale
+        addBgImageView.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+            make.height.equalTo(topBgHeight)
+        }
+        
+        addCloseButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24, height: 24))
+            make.top.equalTo(16)
+            make.right.equalTo(-20)
+        }
+        
+        addMainTitleLabel.snp.makeConstraints { make in
+            make.top.equalTo(56)
+            make.centerX.equalTo(addMainView.snp.centerX)
+        }
+        
+        addMainSubTitleLabel.snp.makeConstraints { make in
+            make.top.equalTo(addMainTitleLabel.snp.bottom).offset(2)
+            make.centerX.equalTo(addMainView.snp.centerX)
+        }
+        
+        addMainPhoneView.snp.makeConstraints { make in
+            make.left.equalTo(24)
+            make.right.equalTo(-24)
+            make.height.equalTo(56)
+            make.top.equalTo(addMainSubTitleLabel.snp.bottom).offset(20)
+        }
+        
+        addMainAddrBookBtn.snp.makeConstraints { make in
+            make.top.bottom.right.equalTo(0)
+            make.width.equalTo(72)
+        }
+        
+        lineIcon.snp.makeConstraints { make in
+            make.right.equalTo(addMainAddrBookBtn.snp.left)
+            make.centerY.equalTo(addMainPhoneView.snp.centerY)
+        }
+        
+        addMainPhoneTextField.snp.makeConstraints { make in
+            make.left.equalTo(16)
+            make.right.equalTo(lineIcon.qsl_left).offset(-16)
+            make.top.bottom.equalTo(0)
+        }
+        
+        addMainSearchBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 200, height: 44))
+            make.top.equalTo(addMainPhoneView.snp.bottom).offset(36)
+            make.centerX.equalTo(snp.centerX)
+        }
+        
+        addMainTipsLabel.snp.makeConstraints { make in
+            make.left.equalTo(60)
+            make.right.equalTo(-60)
+            make.bottom.equalTo(-QSLConst.qsl_kTabbarBottom - 20)
+        }
+    }
+}

+ 185 - 0
QuickSearchLocation/Classes/Pages/QSLContact/Cell/QSLContactCell.swift

@@ -0,0 +1,185 @@
+//
+//  QSLContactCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/11.
+//
+
+import UIKit
+
+protocol QSLContactCellDelegate: NSObjectProtocol {
+    
+    func resortClickAction(model: QSLContactModel)
+    
+    func favorClickAction(model: QSLContactModel)
+    
+    func deleteClickAction(model: QSLContactModel)
+}
+
+class QSLContactCell: UITableViewCell {
+    
+    weak var delegate: QSLContactCellDelegate?
+    
+    var model: QSLContactModel?
+    
+    lazy var bgView: UIView = {
+        
+        let view = UIView()
+        view.addRadius(radius: 8.rpx)
+        view.backgroundColor = .white
+        return view
+    }()
+    
+    lazy var avatarImageView: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_other_avatar")
+        return imageView
+    }()
+    
+    lazy var nameLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("儿子")
+        label.mediumFont(16)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var resortBtn: UIButton = {
+        
+        let btn = UIButton()
+        btn.image(UIImage(named: "contact_resort_btn"))
+        btn.addTarget(self, action: #selector(resortBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var lineView: UIView = {
+        
+        let view = UIView()
+        view.backgroundColor = .hexStringColor(hexString: "#F0F0F0", alpha: 0.2)
+        return view
+    }()
+    
+    lazy var selectBtn: UIButton = {
+        
+        let btn = UIButton()
+        btn.image(UIImage(named: "contact_btn_unselected"), .normal)
+        btn.image(UIImage(named: "contact_btn_selected"), .selected)
+        btn.title("默认该联系人")
+        btn.font(13)
+        btn.textColor(QSLColor.Color_202020)
+        btn.setImageTitleLayout(.imgLeft, spacing: 6.rpx)
+        btn.addTarget(self, action: #selector(favorBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var deleteBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.image(UIImage(named: "contact_delete_btn"))
+        btn.title("删除")
+        btn.font(13)
+        btn.textColor(QSLColor.Color_202020)
+        btn.setImageTitleLayout(.imgLeft, spacing: 4.rpx)
+        btn.addTarget(self, action: #selector(deleteBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func config(model: QSLContactModel) {
+        
+        self.model = model
+        
+        self.nameLabel.text = model.remark.count > 0 ? model.remark : model.phone
+        
+        self.selectBtn.isSelected = model.favor
+    }
+}
+
+extension QSLContactCell {
+    
+    @objc func resortBtnAction() {
+        
+        if let model = self.model {
+            delegate?.resortClickAction(model: model)
+        }
+    }
+    
+    @objc func favorBtnAction() {
+        
+        if let model = self.model {
+            delegate?.favorClickAction(model: model)
+        }
+    }
+    
+    @objc func deleteBtnAction() {
+        
+        if let model = self.model {
+            delegate?.deleteClickAction(model: model)
+        }
+    }
+}
+
+extension QSLContactCell {
+    
+    func initView() {
+        
+        self.backgroundColor = .clear
+        self.contentView.backgroundColor = .clear
+        
+        self.contentView.addSubview(bgView)
+        bgView.snp.makeConstraints { make in
+            make.edges.equalTo(0)
+        }
+        
+        bgView.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 48.rpx, height: 48.rpx))
+            make.left.top.equalTo(16.rpx)
+        }
+        
+        bgView.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.centerY.equalTo(avatarImageView.snp.centerY)
+            make.left.equalTo(avatarImageView.snp.right).offset(12.rpx)
+        }
+        
+        bgView.addSubview(resortBtn)
+        resortBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 92.rpx, height: 32.rpx))
+            make.right.equalTo(-16.rpx)
+            make.centerY.equalTo(avatarImageView.snp.centerY)
+        }
+        
+        bgView.addSubview(lineView)
+        lineView.snp.makeConstraints { make in
+            make.left.equalTo(16.rpx)
+            make.right.equalTo(-16.rpx)
+            make.top.equalTo(76.rpx)
+            make.height.equalTo(1.rpx)
+        }
+        
+        bgView.addSubview(selectBtn)
+        selectBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 100.rpx, height: 20.rpx))
+            make.left.equalTo(lineView.snp.left)
+            make.top.equalTo(lineView.snp.bottom).offset(8.rpx)
+        }
+        
+        bgView.addSubview(deleteBtn)
+        deleteBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 50.rpx, height: 20.rpx))
+            make.right.equalTo(lineView.snp.right)
+            make.centerY.equalTo(selectBtn.snp.centerY)
+        }
+    }
+}

+ 55 - 0
QuickSearchLocation/Classes/Pages/QSLContact/Cell/QSLContactFailCell.swift

@@ -0,0 +1,55 @@
+//
+//  QSLContactFailCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/12.
+//
+
+import UIKit
+
+class QSLContactFailCell: UITableViewCell {
+    
+    lazy var failIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "contact_fail_icon")
+        return imageView
+    }()
+    
+    lazy var contentLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("【儿子】138 8888 8888")
+        label.font(14)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        return label
+    }()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        self.backgroundColor = QSLColor.backGroundColor
+        
+        self.addSubview(failIcon)
+        failIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 16.rpx, height: 16.rpx))
+            make.centerY.equalToSuperview()
+            make.left.equalTo(60.rpx)
+        }
+        
+        self.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.left.equalTo(failIcon.snp.right).offset(5.rpx)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func config(phone: String) {
+        
+        self.contentLabel.text("【用户】\(phone)")
+    }
+}

+ 397 - 0
QuickSearchLocation/Classes/Pages/QSLContact/Controller/QSLContactController.swift

@@ -0,0 +1,397 @@
+//
+//  QSLContactController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/11.
+//
+
+import UIKit
+import YYText
+
+enum QSLContactJumpPage: Int {
+    case shortcut = 1  // 快捷键
+    case mine     = 2  // 我的页面
+}
+
+class QSLContactController: QSLBaseController {
+    
+    var type: QSLContactJumpPage?
+    
+    var contactList: [QSLContactModel]?
+    
+    lazy var bgImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_bg")
+        return imageView
+    }()
+    
+    lazy var backButton: UIButton = {
+       
+        let button = UIButton()
+        button.image(UIImage(named: "public_back_btn"))
+        button.title("添加紧急联系人")
+        button.mediumFont(17)
+        button.textColor(QSLColor.Color_202020)
+        button.setImageTitleLayout(.imgLeft, spacing: 4.rpx)
+        button.addTarget(self, action: #selector(backBtnAction), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var contactIcon: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "contact_icon")
+        return imageView
+    }()
+    
+    lazy var contactLabel: UILabel = {
+       
+        let label = UILabel()
+        label.textAlignment = .center
+        label.numberOfLines = 0
+        label.text("使用一键求助,需要添加正确的紧急联系人手机号码,您的联系人将会收到短信以及APP消息通知。")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        label.changeLineSpace(space: 5)
+        return label
+    }()
+    
+    lazy var titleIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "contact_title_icon")
+        return imageView
+    }()
+    
+    lazy var addBtn: UIButton = {
+    
+        let btn = UIButton()
+        btn.title("添加联系人")
+        btn.textColor(.hexStringColor(hexString: "#15CBA1"))
+        btn.mediumFont(14)
+        btn.image(UIImage(named: "contact_add_icon"))
+        btn.addTarget(self, action: #selector(addBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var emptyAddLabel: YYLabel = {
+       
+        let label = YYLabel()
+        
+        let attr = NSMutableAttributedString()
+        
+        let firstAttr = NSMutableAttributedString(string: "未添加联系人,")
+        firstAttr.font(14)
+        firstAttr.color(.hexStringColor(hexString: "#A7A7A7"))
+        attr.append(firstAttr)
+        
+        let addHL = YYTextHighlight()
+        var addStr = "点我添加"
+        
+        let addText = NSMutableAttributedString(string: addStr)
+        
+        addText.font(14)
+        addText.color(.hexStringColor(hexString: "#15CBA1"))
+        addText.yy_setTextHighlight(addHL, range: NSRange(location: 0, length: addStr.count))
+        addHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.addBtnAction()
+        }
+        attr.append(addText)
+
+        label.attributedText = attr
+        
+        return label
+    }()
+    
+    lazy var contactTableView: UITableView = {
+        
+        let tableView = UITableView(frame: .zero, style: .grouped)
+        tableView.backgroundColor = QSLColor.backGroundColor
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.tableViewNeverAdjustContentInset()
+        tableView.bounces = true
+        tableView.isUserInteractionEnabled = true
+        tableView.isScrollEnabled = true
+        tableView.contentInsetAdjustmentBehavior = .never
+        tableView.register(cellClass: QSLContactCell.self)
+        
+        return tableView
+    }()
+    
+    lazy var bottomView: UIView = {
+        
+        let view = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: 76.rpx))
+        view.backgroundColor = .white
+        view.addFourCorner(topLeft: 12.rpx, topRight: 12.rpx, bottomLeft: 0, bottomRight: 0)
+        view.layer.shadowOffset = CGSize(width: 0, height: -1)
+        view.layer.shadowColor = UIColor.hexStringColor(hexString: "#A7A7A7", alpha: 0.1).cgColor
+        view.layer.shadowOpacity = 5
+        view.layer.shadowRadius = 0
+        return view
+    }()
+    
+    lazy var sendBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.addRadius(radius: 22.rpx)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#FF5146"), color2: .hexStringColor(hexString: "#FF766D"), width: QSLConst.qsl_kScreenW - 32.rpx, height: 44.rpx, direction: .horizontal)
+        btn.title("一键发送求助")
+        btn.mediumFont(16)
+        btn.textColor(.white)
+        btn.addTarget(self, action: #selector(sendAllBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.initView()
+        
+        self.requestContactList()
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(requestContactList), name: QSLNotification.QSLRefreshContact, object: nil)
+        
+        if let type = self.type {
+            if type == .mine {
+                gravityInstance?.track(QSLGravityConst.contact_show, properties: ["id": 1002])
+            } else {
+                gravityInstance?.track(QSLGravityConst.contact_show, properties: ["id": 1001])
+            }
+        }
+    }
+    
+}
+
+extension QSLContactController {
+    
+    @objc func addBtnAction() {
+        
+        if !QSLBaseManager.shared.isLogin() {
+            
+            if let view = self.tabBarController?.view {
+                
+                QSLAlertView.alert(view: view, title: "温馨提示", content: "登录即可体验查看轨迹记录", secondBtnClosure:  {
+                    
+                    QSLJumpManager.shared.pushToLogin(type: .contact)
+                })
+            }
+            return
+        }
+        
+        if !QSLBaseManager.shared.isVip() {
+            
+            QSLJumpManager.shared.pushToVip(type: .contact)
+            return
+        }
+        
+        QSLContactAddAlertView.alert(vc: self) { phone in
+            self.requestAddContact(phone: phone)
+        }
+    }
+    
+    @objc func sendAllBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.contact_resort_all)
+//        QSLContactSendFailAlertView.alert(view: self.view, contactList: self.contactList!)
+        QSLNetwork().request(.contactMaydayAll(dict: [:])) { response in
+            
+            if let data = response.fetchJSONString(path: "data>fail").data(using: .utf8), let failList = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] {
+                print(failList)
+                if failList.count > 0 {
+                    QSLContactSendFailAlertView.alert(view: self.view, contactList: failList)
+                    gravityInstance?.track(QSLGravityConst.contact_resort_all_fail_alert)
+                } else {
+                    self.view.toast(text: "求助消息发送成功")
+                    gravityInstance?.track(QSLGravityConst.contact_resort_all_success)
+                }
+            }
+        } fail: { code, error in
+            gravityInstance?.track(QSLGravityConst.contact_resort_all_fail)
+            self.view.toast(text: "求助消息发送失败,请稍后重试")
+        }
+    }
+}
+
+extension QSLContactController: QSLContactCellDelegate {
+    
+    func resortClickAction(model: QSLContactModel) {
+        
+        gravityInstance?.track(QSLGravityConst.contact_resort_click)
+        QSLAlertView.alert(view: self.view, title: "紧急求助", content: "确认向\(model.phone)发送短信求助?", isOneBtn: true, oneBtnText: "确认", oneBtnClosure:  {
+            gravityInstance?.track(QSLGravityConst.contact_resort_confirm)
+            QSLNetwork().request(.contactMayday(dict: ["phone": model.phone])) { reponse in
+                self.view.toast(text: "求助消息发送成功")
+                gravityInstance?.track(QSLGravityConst.contact_resort_success)
+            } fail: { code, error in
+                self.view.toast(text: "求助消息发送失败,请核实手机号码")
+                gravityInstance?.track(QSLGravityConst.contact_resort_fail)
+            }
+        }, closeBtnClosure:  {
+            gravityInstance?.track(QSLGravityConst.contact_resort_cancel)
+        })
+    }
+    
+    func favorClickAction(model: QSLContactModel) {
+        
+        QSLNetwork().request(.contactFavor(dict: ["phone": model.phone, "favor": true])) { reponse in
+            self.view.toast(text: "修改默认联系人成功")
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshContact, object: nil)
+        } fail: { code, error in
+            self.view.toast(text: "修改默认联系人失败,请稍后重试")
+        }
+    }
+    
+    func deleteClickAction(model: QSLContactModel) {
+        
+        gravityInstance?.track(QSLGravityConst.contact_delete)
+        QSLAlertView.alert(view: self.view, title: "移除紧急联系人", content: "您确定要将\n\(model.phone)移除紧急联系人吗?", isOneBtn: false, firstBtnClosure: {
+            gravityInstance?.track(QSLGravityConst.contact_delete_cancel)
+        }, secondBtnClosure:  {
+            gravityInstance?.track(QSLGravityConst.contact_delete_confirm)
+            QSLNetwork().request(.contactDelete(dict: ["phone": model.phone])) { reponse in
+                self.view.toast(text: "删除联系人成功")
+                NotificationCenter.default.post(name: QSLNotification.QSLRefreshContact, object: nil)
+            } fail: { code, error in
+                self.view.toast(text: "删除联系人失败,请稍后重试")
+            }
+        })
+    }
+    
+}
+
+extension QSLContactController {
+    
+    @objc func requestContactList() {
+        
+        QSLNetwork().request(.contactList(dict: [:])) { response in
+            
+            let list = response.mapArray(QSLContactModel.self, modelKey: "data>list")
+            self.contactList = list
+            self.contactTableView.reloadData()
+        } fail: { code, error in
+            
+            self.view.toast(text: error)
+        }
+    }
+    
+    func requestAddContact(phone: String) {
+        
+        QSLNetwork().request(.contactCreate(dict: ["phone": phone])) { response in
+            
+            self.view.toast(text: "添加成功")
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshContact, object: nil)
+        } fail: { code, error in
+            
+            self.view.toast(text: error)
+        }
+    }
+}
+
+extension QSLContactController: UITableViewDelegate, UITableViewDataSource {
+    
+    func numberOfSections(in tableView: UITableView) -> Int {
+        return self.contactList?.count ?? 0
+    }
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return 1
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        
+        let cell = tableView.dequeueReusableCell(cellType: QSLContactCell.self, cellForRowAt: indexPath)
+        cell.delegate = self
+        cell.selectionStyle = .none
+        if let model = self.contactList?[indexPath.section] {
+            cell.config(model: model)
+        }
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return 112.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 0.0001
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 8.rpx
+    }
+}
+
+extension QSLContactController {
+    
+    func initView() {
+        
+        self.view.addSubview(bgImageView)
+        bgImageView.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+        }
+        
+        self.view.addSubview(backButton)
+        backButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 150.rpx, height: 25.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH)
+        }
+        
+        self.view.addSubview(contactIcon)
+        contactIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 155.rpx, height: 126.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(backButton.snp.bottom).offset(24.rpx)
+        }
+        
+        self.view.addSubview(contactLabel)
+        contactLabel.snp.makeConstraints { make in
+            make.left.equalTo(20.rpx)
+            make.right.equalTo(-20.rpx)
+            make.top.equalTo(backButton.snp.bottom).offset(128.rpx)
+        }
+        
+        self.view.addSubview(titleIcon)
+        titleIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 89.rpx, height: 26.rpx))
+            make.left.equalTo(20.rpx)
+            make.top.equalTo(contactLabel.snp.bottom).offset(32.rpx)
+        }
+        
+        self.view.addSubview(addBtn)
+        addBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 90.rpx, height: 20.rpx))
+            make.right.equalTo(-12.rpx)
+            make.bottom.equalTo(titleIcon.snp.bottom)
+        }
+        
+        self.view.addSubview(emptyAddLabel)
+        emptyAddLabel.snp.makeConstraints { make in
+            make.top.equalTo(addBtn.snp.bottom).offset(110.rpx)
+            make.centerX.equalToSuperview()
+        }
+        
+        self.view.addSubview(bottomView)
+        bottomView.snp.makeConstraints { make in
+            make.left.right.bottom.equalTo(0)
+            make.height.equalTo(76.rpx)
+        }
+        
+        bottomView.addSubview(sendBtn)
+        sendBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 328.rpx, height: 44.rpx))
+            make.center.equalToSuperview()
+        }
+        
+        self.view.addSubview(contactTableView)
+        contactTableView.snp.makeConstraints { make in
+            make.top.equalTo(addBtn.snp.bottom).offset(13.rpx)
+            make.left.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+            make.bottom.equalTo(bottomView.snp.top)
+        }
+    }
+}

+ 285 - 0
QuickSearchLocation/Classes/Pages/QSLContact/View/QSLContactAddAlertView.swift

@@ -0,0 +1,285 @@
+//
+//  QSLContactAddAlertView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/12.
+//
+
+import UIKit
+import Contacts
+import ContactsUI
+
+class QSLContactAddAlertView: UIView {
+    
+    var vc: UIViewController?
+    
+    lazy var contentView: UIView = {
+        
+        let contentView = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW - 60.rpx, height: 152.0.rpx))
+        contentView.backgroundColor = .white
+        contentView.addRadius(radius: 8.rpx)
+        return contentView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+      
+        let label = UILabel()
+        label.text("添加紧急联系人")
+        label.mediumFont(17)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var addInputView: UIView = {
+      
+        let view = UIView()
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addRadius(radius: 4.rpx)
+        view.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#F2F2F2"))
+        return view
+    }()
+    
+    lazy var phoneTextField: UITextField = {
+       
+        let textField = UITextField()
+        textField.maxTextNumber = 11
+        textField.keyboardType = .numberPad
+        textField.textColor = QSLColor.Color_202020
+        textField.font = UIFont.textF(16)
+        textField.placeholder = "请输入手机号"
+        textField.setPlaceholderAttribute(font: UIFont.textF(16), color: UIColor.hexStringColor(hexString: "#A7A7A7"))
+        return textField
+    }()
+    
+    lazy var lineView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .hexStringColor(hexString: "#F2F2F2")
+        return view
+    }()
+    
+    lazy var addressBookBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.image(UIImage(named: "add_address_btn"))
+        btn.title("通讯录导入")
+        btn.textColor(.hexStringColor(hexString: "#11B58F"))
+        btn.font(13)
+        btn.addTarget(self, action: #selector(addressBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var firstButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.backgroundColor = .hexStringColor(hexString: "#F8F8F8")
+        btn.addRadius(radius: 20.rpx)
+        btn.title("取消")
+        btn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+        btn.mediumFont(16)
+        btn.addTarget(self, action: #selector(removeView), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var secondButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.addRadius(radius: 20.rpx)
+        btn.title("添加")
+        btn.textColor(.white)
+        btn.mediumFont(16)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 118.rpx, height: 40.rpx, direction: .horizontal)
+        btn.addTarget(self, action: #selector(addBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var closeButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "public_btn_close_AAA"), for: .normal)
+        btn.addTarget(self, action: #selector(removeView), for: .touchUpInside)
+        return btn
+    }()
+    
+    var secondBtnClosure: ((String) -> ())?
+    
+    var addressBtnClosure: (() -> ())?
+    
+    class func alert(vc: UIViewController,
+                     secondBtnClosure: @escaping (String) -> ()) {
+        
+        let window = QSLContactAddAlertView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH))
+        window.vc = vc
+        window.secondBtnClosure = secondBtnClosure
+        vc.view.addSubview(window)
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) {
+            window.backgroundColor = .hexStringColor(hexString: "#000000", alpha: 0.7)
+            window.contentView.isHidden = false
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func addBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.contact_add)
+        
+        guard let text = self.phoneTextField.text, text.count == 11 else {
+            self.vc?.view.toast(text: "请输入正确的电话号码")
+            return
+        }
+        
+        if let secondBtnClosure = self.secondBtnClosure {
+            secondBtnClosure(text)
+        }
+        
+        self.keyboardEndEditing()
+        self.removeView()
+    }
+    
+    @objc func addressBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.contact_addressbook)
+        
+        self.removeView()
+            
+        let status = CNContactStore.authorizationStatus(for: .contacts)
+        if status != .authorized {
+            
+            let store = CNContactStore()
+            store.requestAccess(for: .contacts) { granted, error in
+                if granted {
+                    DispatchQueue.main.async {
+                        
+                        let picker = CNContactPickerViewController()
+                        picker.delegate = self
+                        self.vc?.present(picker, animated: true, completion: nil)
+                    }
+                } else {
+                    
+                    DispatchQueue.main.async {
+                        self.vc?.view.toast(text: "请先开启通讯录权限")
+                    }
+                }
+            }
+        } else {
+            
+            let picker = CNContactPickerViewController()
+            picker.delegate = self
+            self.vc?.present(picker, animated: true, completion: nil)
+        }
+    }
+    
+    // 移除
+    @objc func removeView() {
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) { [weak self] in
+            self?.backgroundColor = UIColor.init(white: 0, alpha: 0)
+            self?.contentView.isHidden = true
+        } completion: { [weak self] finished in
+            self?.removeFromSuperview()
+        }
+    }
+}
+
+extension QSLContactAddAlertView: CNContactPickerDelegate {
+    
+    // 通讯录选择电话
+    func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
+        
+        let phoneArray = contact.phoneNumbers
+
+        if let firstPhone = phoneArray.first {
+            
+            let phoneNum = firstPhone.value
+            
+            let code = phoneNum.stringValue
+            
+            let phone = code.replacingOccurrences(of: " ", with: "")
+            
+            self.phoneTextField.text = phone
+            
+            print("Phone number is --- \(code)")
+        }
+
+        let name = CNContactFormatter.string(from: contact, style: .fullName) ?? "Unknown"
+        print("Full name is --- \(name)")
+    }
+}
+
+extension QSLContactAddAlertView {
+    
+    func initView() {
+        
+        addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: QSLConst.qsl_kScreenW - 60.rpx, height: 203.0.rpx))
+            make.center.equalToSuperview()
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(24.rpx)
+        }
+
+        contentView.addSubview(addInputView)
+        addInputView.snp.makeConstraints { make in
+            make.left.equalTo(29.rpx)
+            make.right.equalTo(-19.rpx)
+            make.top.equalTo(titleLabel.snp.bottom).offset(16.rpx)
+            make.height.equalTo(44.rpx)
+        }
+        
+        addInputView.addSubview(addressBookBtn)
+        addressBookBtn.snp.makeConstraints { make in
+            make.width.equalTo(104.rpx)
+            make.top.bottom.right.equalTo(0)
+        }
+        
+        addInputView.addSubview(lineView)
+        lineView.snp.makeConstraints { make in
+            make.top.equalTo(16.rpx)
+            make.bottom.equalTo(-16.rpx)
+            make.right.equalTo(addressBookBtn.snp.left)
+            make.width.equalTo(1.rpx)
+        }
+        
+        addInputView.addSubview(phoneTextField)
+        phoneTextField.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.top.bottom.equalTo(0)
+            make.right.equalTo(lineView.snp.left).offset(-12.rpx)
+        }
+        
+        contentView.addSubview(firstButton)
+        firstButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.left.equalTo(24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(secondButton)
+        secondButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.right.equalTo(-24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(closeButton)
+        closeButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24.rpx, height: 24.rpx))
+            make.top.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+        }
+    }
+}

+ 220 - 0
QuickSearchLocation/Classes/Pages/QSLContact/View/QSLContactSendFailAlertView.swift

@@ -0,0 +1,220 @@
+//
+//  QSLContactSendFailAlertView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/12.
+//
+
+import UIKit
+
+class QSLContactSendFailAlertView: UIView {
+    
+    var contactList: [String]?
+    
+    lazy var contentView: UIView = {
+        
+        let contentView = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW - 60.rpx, height: 152.0.rpx))
+        contentView.backgroundColor = .white
+        contentView.addRadius(radius: 8.rpx)
+        return contentView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+      
+        let label = UILabel()
+        label.text("添加紧急联系人")
+        label.mediumFont(17)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var contentLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("部分号码发送失败,请检查号码重试!")
+        label.font(15)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        return label
+    }()
+    
+    lazy var infoView: UIView = {
+       
+        let view = UIView()
+        view.addRadius(radius: 4.rpx)
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#F2F2F2"))
+        return view
+    }()
+    
+    lazy var infoTableView: UITableView = {
+        
+        let tableView = UITableView(frame: .zero, style: .grouped)
+        tableView.backgroundColor = QSLColor.backGroundColor
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.tableViewNeverAdjustContentInset()
+        tableView.bounces = true
+        tableView.isUserInteractionEnabled = true
+        tableView.isScrollEnabled = true
+        tableView.contentInsetAdjustmentBehavior = .never
+        tableView.register(cellClass: QSLContactFailCell.self)
+        
+        return tableView
+    }()
+    
+    lazy var oneButton: UIButton = {
+      
+        let btn = UIButton()
+        btn.addRadius(radius: 20.rpx)
+        btn.title("我知道了")
+        btn.textColor(.white)
+        btn.mediumFont(16)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 150.rpx, height: 40.rpx, direction: .horizontal)
+        btn.addTarget(self, action: #selector(oneBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var closeButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "public_btn_close_AAA"), for: .normal)
+        btn.addTarget(self, action: #selector(removeView), for: .touchUpInside)
+        return btn
+    }()
+    
+    class func alert(view: UIView, contactList: [String]) {
+        
+        let window = QSLContactSendFailAlertView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH))
+        window.contactList = contactList
+        window.infoTableView.reloadData()
+        let height = 20.rpx * contactList.count + 32.rpx + 10.rpx * (contactList.count - 1)
+        window.infoView.snp.updateConstraints { make in
+            make.height.equalTo(height)
+        }
+        window.contentView.snp.updateConstraints { make in
+            make.height.equalTo(187.rpx + height)
+        }
+        view.addSubview(window)
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) {
+            window.backgroundColor = .hexStringColor(hexString: "#000000", alpha: 0.7)
+            window.contentView.isHidden = false
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        initUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // 单按钮点击事件
+    @objc func oneBtnAction() {
+        removeView()
+    }
+    
+    // 移除
+    @objc func removeView() {
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) { [weak self] in
+            self?.backgroundColor = UIColor.init(white: 0, alpha: 0)
+            self?.contentView.isHidden = true
+        } completion: { [weak self] finished in
+            self?.removeFromSuperview()
+        }
+    }
+}
+
+extension QSLContactSendFailAlertView: UITableViewDelegate, UITableViewDataSource {
+    
+    func numberOfSections(in tableView: UITableView) -> Int {
+        return self.contactList?.count ?? 0
+    }
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return 1
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        
+        let cell = tableView.dequeueReusableCell(cellType: QSLContactFailCell.self, cellForRowAt: indexPath)
+        cell.selectionStyle = .none
+        if let phone = self.contactList?[indexPath.section] {
+            cell.config(phone: phone)
+        }
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return 20.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 0.0001
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 10.rpx
+    }
+}
+
+extension QSLContactSendFailAlertView {
+    
+    func initUI() {
+        
+        addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.width.equalTo(QSLConst.qsl_kScreenW - 60.rpx)
+            make.height.equalTo(203.0.rpx)
+            make.center.equalToSuperview()
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(24.rpx)
+        }
+        
+        contentView.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(16.rpx)
+        }
+        
+        contentView.addSubview(infoView)
+        infoView.snp.makeConstraints { make in
+            make.left.equalTo(24.rpx)
+            make.right.equalTo(-24.rpx)
+            make.top.equalTo(contentLabel.snp.bottom).offset(8.rpx)
+            make.height.equalTo(82.rpx)
+        }
+        
+        infoView.addSubview(infoTableView)
+        infoTableView.snp.makeConstraints { make in
+            make.left.equalTo(0)
+            make.right.equalTo(0)
+            make.top.equalTo(16.rpx)
+            make.bottom.equalTo(-16.rpx)
+        }
+        
+        contentView.addSubview(oneButton)
+        oneButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 150.rpx, height: 40.rpx))
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(closeButton)
+        closeButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24.rpx, height: 24.rpx))
+            make.top.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+        }
+    }
+}

+ 247 - 0
QuickSearchLocation/Classes/Pages/QSLFriend/Cell/QSLFriendTableViewCell.swift

@@ -0,0 +1,247 @@
+//
+//  QSLFriendTableViewCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/26.
+//
+
+import UIKit
+
+protocol QSLFriendTableViewCellDelegate: NSObjectProtocol {
+    
+    func moreBtnAction(model: QSLUserModel, btn: UIButton)
+    
+    func roadBtnClickAction(model: QSLUserModel)
+}
+
+class QSLFriendTableViewCell: UITableViewCell {
+    
+    weak var delegate: QSLFriendTableViewCellDelegate?
+    
+    var userModel: QSLUserModel?
+    
+    lazy var bgView: UIView = {
+      
+        let view = UIView()
+        view.addRadius(radius: 8.rpx)
+        view.backgroundColor = .white
+        return view
+    }()
+    
+    lazy var avatarImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_avatar")
+        return imageView
+    }()
+    
+    lazy var nameLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("用户")
+        label.mediumFont(16)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var moreBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.image(UIImage(named: "friends_cell_more_btn"))
+        btn.addTarget(self, action: #selector(moreBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var locateIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_location")
+        return imageView
+    }()
+    
+    lazy var addrLabel: UILabel = {
+       
+        let label = UILabel()
+        label.numberOfLines = 0
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        label.text = "未知"
+        label.changeLineSpace(space: 4)
+        return label
+    }()
+    
+    lazy var lineView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .hexStringColor(hexString: "#F0F0F0", alpha: 0.2)
+        return view
+    }()
+    
+    lazy var timeLabel: UILabel = {
+      
+        let label = UILabel()
+        label.text("未知")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var checkBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.addRadius(radius: 16.rpx)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 92.rpx, height: 32.rpx, direction: .horizontal)
+        btn.title("查看轨迹")
+        btn.textColor(.white)
+        btn.mediumFont(15)
+        btn.addTarget(self, action: #selector(checkBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var blurView: UIView = {
+       
+        let effect = UIBlurEffect(style: .light)
+        let blur = UIVisualEffectView(effect: effect)
+        blur.isHidden = true
+        return blur
+    }()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        initUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func config(model: QSLUserModel) {
+        self.userModel = model
+        
+        if model.isMine {
+            self.timeLabel.text = Date().formateCurrentDate
+            self.moreBtn.isHidden = true
+        } else {
+            self.moreBtn.isHidden = false
+            
+            if model.location.timestamp > 0 {
+                self.timeLabel.text = Date.timestampToFormatterTimeString(timestamp: "\(model.location.timestamp)")
+            } else {
+                self.timeLabel.text = "未知"
+            }
+        }
+        
+        if model.remark.count > 0 {
+            self.nameLabel.text = model.remark
+        } else {
+            self.nameLabel.text = model.phone
+        }
+        
+        if model.location.addr.count > 0 {
+            self.addrLabel.text = model.location.addr
+        } else {
+            self.addrLabel.text = "未知"
+        }
+        
+        if !QSLBaseManager.shared.isVip() && !model.isMine {
+            self.blurView.isHidden = false
+        } else {
+            if model.blockedMe {
+                self.blurView.isHidden = false
+            } else {
+                self.blurView.isHidden = true
+            }
+        }
+    }
+}
+
+extension QSLFriendTableViewCell {
+    
+    @objc func moreBtnAction() {
+        
+        if let model = self.userModel {
+            delegate?.moreBtnAction(model: model, btn: self.moreBtn)
+        }
+    }
+    
+    @objc func checkBtnAction() {
+        
+        if let model = self.userModel {
+            delegate?.roadBtnClickAction(model: model)
+        }
+    }
+}
+
+extension QSLFriendTableViewCell {
+    
+    func initUI() {
+        
+        self.backgroundColor = .clear
+        
+        self.contentView.addSubview(bgView)
+        bgView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        bgView.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 48.rpx, height: 48.rpx))
+            make.left.equalTo(16.rpx)
+            make.top.equalTo(22.rpx)
+        }
+        
+        bgView.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.top.equalTo(avatarImageView.snp.top).offset(5.rpx)
+        }
+        
+        bgView.addSubview(moreBtn)
+        moreBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24.rpx, height: 24.rpx))
+            make.right.equalTo(-16.rpx)
+            make.top.equalTo(20.rpx)
+        }
+        
+        bgView.addSubview(locateIcon)
+        locateIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 16.rpx, height: 16.rpx))
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.top.equalTo(nameLabel.snp.bottom).offset(8.rpx)
+        }
+        
+        bgView.addSubview(addrLabel)
+        addrLabel.snp.makeConstraints { make in
+            make.left.equalTo(locateIcon.snp.right)
+            make.right.equalTo(-16.rpx)
+            make.top.equalTo(locateIcon.snp.top)
+        }
+        
+        bgView.addSubview(blurView)
+        blurView.snp.makeConstraints { make in
+            make.edges.equalTo(addrLabel.snp.edges)
+        }
+        
+        bgView.addSubview(lineView)
+        lineView.snp.makeConstraints { make in
+            make.left.equalTo(16.rpx)
+            make.right.equalTo(-16.rpx)
+            make.height.equalTo(1.rpx)
+            make.bottom.equalTo(-47.rpx)
+        }
+        
+        bgView.addSubview(timeLabel)
+        timeLabel.snp.makeConstraints { make in
+            make.left.equalTo(16.rpx)
+            make.bottom.equalTo(-14.rpx)
+        }
+        
+        bgView.addSubview(checkBtn)
+        checkBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 92.rpx, height: 32.rpx))
+            make.right.equalTo(-16.rpx)
+            make.centerY.equalTo(timeLabel.snp.centerY)
+        }
+    }
+}

+ 355 - 0
QuickSearchLocation/Classes/Pages/QSLFriend/Controller/QSLFriendController.swift

@@ -0,0 +1,355 @@
+//
+//  QSLFriendController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+import CRRefresh
+import WMZDropDownMenu
+
+class QSLFriendController: QSLBaseController {
+    
+    var friendList: [QSLUserModel] = [QSLUserModel]()
+    
+    lazy var friendBg: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_bg")
+        return imageView
+    }()
+    
+    lazy var friendTitle: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_title")
+        return imageView
+    }()
+    
+    lazy var friTableView: UITableView = {
+        
+        let tableView = UITableView(frame: .zero, style: .grouped)
+        tableView.backgroundColor = .clear
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.tableViewNeverAdjustContentInset()
+        tableView.bounces = true
+        tableView.isUserInteractionEnabled = true
+        tableView.isScrollEnabled = true
+        tableView.contentInsetAdjustmentBehavior = .never
+        tableView.register(cellClass: QSLFriendTableViewCell.self)
+        
+        tableView.cr.addHeadRefresh(animator: NormalHeaderAnimator()) { [weak self] in
+            self?.requestFriendList()
+        }
+        
+        return tableView
+    }()
+    
+    lazy var bottomView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .white
+        view.layer.cornerRadius = 12.rpx
+        view.layer.shadowOffset = CGSize(width: 0, height: -1)
+        view.layer.shadowColor = UIColor.hexStringColor(hexString: "#A7A7A7", alpha: 0.1).cgColor
+        view.layer.shadowOpacity = 5
+        view.layer.shadowRadius = 0
+        return view
+    }()
+    
+    lazy var friAddBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.addRadius(radius: 22.rpx)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: QSLConst.qsl_kScreenW - 32.rpx, height: 44.rpx, direction: .horizontal)
+        btn.image(UIImage(named: "home_friends_header_add_icon"))
+        btn.title("添加好友")
+        btn.mediumFont(16)
+        btn.textColor(.white)
+        btn.setImageTitleLayout(.imgLeft, spacing: 0)
+        btn.addTarget(self, action: #selector(addButtonAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var resortBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "friends_resort_btn"), for: .normal)
+        btn.addTarget(self, action: #selector(resortBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    private var popView: QSLPopView?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        initializeView()
+        
+        friendList.append(QSLBaseManager.shared.userModel)
+        self.friTableView.reloadData()
+        
+        self.friTableView.cr.beginHeaderRefresh()
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(requestFriendList), name: QSLNotification.QSLLogin, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(requestFriendList), name: QSLNotification.QSLLogout, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(requestFriendList), name: QSLNotification.QSLRefreshRequest, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(requestFriendList), name: QSLNotification.QSLRefreshFriend, object: nil)
+    }
+}
+
+extension QSLFriendController {
+    
+    @objc func addButtonAction() {
+        
+        QSLJumpManager.shared.pushToAdd(type: .friend)
+    }
+    
+    @objc func resortBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.contact_shortcut_send)
+        QSLNetwork().request(.contactMaydayFavor(dict: [:])) { response in
+            self.view.toast(text: "求助信息发送成功")
+        } fail: { code, error in
+            self.view.toast(text: error)
+        }
+    }
+}
+
+extension QSLFriendController {
+    
+    // 请求好友列表
+    @objc func requestFriendList() {
+        
+        self.friendList.removeAll()
+        self.friendList.append(QSLBaseManager.shared.userModel)
+        
+        QSLNetwork().request(.friendList(dict: [:])) { response in
+            
+            self.friTableView.cr.endHeaderRefresh()
+            let list = response.mapArray(QSLUserModel.self, modelKey: "data>list")
+            self.friendList.append(contentsOf: list)
+            self.friTableView.reloadData()
+        } fail: { code, error in
+            
+            self.friTableView.cr.endHeaderRefresh()
+            self.friTableView.reloadData()
+        }
+    }
+}
+
+extension QSLFriendController: QSLFriendTableViewCellDelegate {
+    
+    func roadBtnClickAction(model: QSLUserModel) {
+        
+        if !QSLBaseManager.shared.isLogin() {
+            
+            if let view = self.tabBarController?.view {
+                
+                QSLAlertView.alert(view: view, title: "温馨提示", content: "登录即可体验查看轨迹记录", secondBtnClosure:  {
+                    
+                    QSLJumpManager.shared.pushToLogin(type: .road)
+                })
+            }
+            return
+        }
+        
+        if !QSLBaseManager.shared.isVip() {
+            
+            QSLJumpManager.shared.pushToVip(type: .friendRoad)
+            return
+        }
+        
+        QSLJumpManager.shared.pushToRoad(type: .friend, model: model)
+    }
+    
+    func moreBtnAction(model: QSLUserModel, btn: UIButton) {
+        
+        var item3Text = ""
+        if !model.blockedHim {
+            item3Text = "不给TA看"
+        } else {
+            item3Text = "给TA看"
+        }
+        
+        let items = [(UIImage(), "互删好友"), (UIImage(), "修改备注"), (UIImage(), item3Text), (UIImage(), "查看TA的手机号")]
+        popView = QSLPopView(items: items) { index in
+            print("选择了第 \(index) 项")
+            
+            switch index {
+            case 0:
+                self.deleteFri(model: model)
+                break
+            case 1:
+                self.editRemark(model: model)
+                break
+            case 2:
+                self.blockHime(model: model)
+                break
+            case 3:
+                self.checkPhoneNum(model: model)
+                break
+            default:
+                break
+            }
+        }
+        
+        popView?.contentWidth = 130.rpx
+        popView?.isShowMaskView = true
+        
+        popView?.show(from: btn, selfVC: self)
+    }
+    
+    // 删除好友
+    func deleteFri(model: QSLUserModel) {
+        
+        gravityInstance?.track(QSLGravityConst.friend_delete)
+        if let view = self.tabBarController?.view {
+            QSLAlertView.alert(view: view, title: "温馨提示", content: "互删好友后,双方将停止位置的分享,以及清除相关定位记录,是否确认互删?", isOneBtn: false, secondBtnClosure:  {
+                QSLNetwork().request(.friendDelete(dict: ["friendId": model.friendId])) { response in
+                    
+                    gravityInstance?.track(QSLGravityConst.friend_delete_success)
+                    self.view.toast(text: "删除成功")
+                    NotificationCenter.default.post(name: QSLNotification.QSLRefreshFriend, object: nil)
+                } fail: { code, error in
+                    
+                    gravityInstance?.track(QSLGravityConst.friend_delete_fail)
+                }
+            })
+        }
+    }
+    
+    // 修改备注
+    func editRemark(model: QSLUserModel) {
+        
+        gravityInstance?.track(QSLGravityConst.friend_remark)
+        if let view = self.tabBarController?.view {
+            QSLFriendRemarkAlertView.alert(view: view) { remark in
+                
+                QSLNetwork().request(.friendRemark(dict: ["friendId": model.friendId, "remark": remark])) { response in
+                    
+                    UIApplication.keyWindow?.toast(text: "修改成功")
+                    NotificationCenter.default.post(name: QSLNotification.QSLRefreshFriend, object: nil)
+                } fail: { code, error in
+                    
+                }
+            }
+        }
+    }
+    
+    // 给他看
+    func blockHime(model: QSLUserModel) {
+        
+        if model.blockedHim {
+            gravityInstance?.track(QSLGravityConst.friend_not_block_him)
+        } else {
+            gravityInstance?.track(QSLGravityConst.friend_block_him)
+        }
+        
+        QSLNetwork().request(.friendBlocked(dict: ["friendId": model.friendId, "blocked": !model.blockedHim])) { response in
+            
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshFriend, object: nil)
+        } fail: { code, error in
+            
+        }
+    }
+    
+    // 查看手机号码
+    func checkPhoneNum(model: QSLUserModel) {
+        
+        gravityInstance?.track(QSLGravityConst.friend_phoneNum)
+        if let view = self.tabBarController?.view {
+            QSLAlertView.alert(view: view, title: "Ta的手机号", content: model.phone, isOneBtn: true, oneBtnText: "复制号码", oneBtnClosure:  {
+                gravityInstance?.track(QSLGravityConst.friend_phoneNum_click)
+                UIPasteboard.general.string = model.phone
+                view.toast(text: "复制成功")
+            })
+        }
+    }
+}
+
+
+// MARK: - 设置Tableview
+extension QSLFriendController: UITableViewDelegate, UITableViewDataSource {
+    
+    func numberOfSections(in tableView: UITableView) -> Int {
+        return friendList.count
+    }
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return 1
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    
+        let cell = tableView.dequeueReusableCell(cellType: QSLFriendTableViewCell.self, cellForRowAt: indexPath)
+        cell.delegate = self
+        cell.selectionStyle = .none
+        let model = self.friendList[indexPath.section]
+        cell.config(model: model)
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return 154.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 0.0001
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 8.rpx
+    }
+    
+}
+
+extension QSLFriendController {
+    
+    func initializeView() {
+        
+        self.view.addSubview(friendBg)
+        friendBg.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+        }
+        
+        self.view.addSubview(friendTitle)
+        friendTitle.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 82.rpx, height: 26.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH)
+        }
+        
+        self.view.addSubview(bottomView)
+        bottomView.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.bottom.equalTo(-QSLConst.qsl_kTabbarFrameH)
+            make.height.equalTo(76.rpx)
+        }
+        
+        bottomView.addSubview(friAddBtn)
+        friAddBtn.snp.makeConstraints { make in
+            make.left.top.equalTo(16.rpx)
+            make.right.bottom.equalTo(-16.rpx)
+        }
+        
+        self.view.addSubview(friTableView)
+        friTableView.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+            make.top.equalTo(friendTitle.snp.bottom).offset(13.rpx)
+            make.bottom.equalTo(bottomView.snp.top)
+        }
+        
+        self.view.addSubview(resortBtn)
+        resortBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 60.rpx, height: 60.rpx))
+            make.right.equalTo(-12.rpx)
+            make.bottom.equalTo(bottomView.snp.top).offset(-32.rpx)
+        }
+    }
+}

+ 221 - 0
QuickSearchLocation/Classes/Pages/QSLFriend/View/QSLFriendRemarkAlertView.swift

@@ -0,0 +1,221 @@
+//
+//  QSLFriendRemarkAlertView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/5.
+//
+
+import UIKit
+
+class QSLFriendRemarkAlertView: UIView {
+    
+    lazy var contentView: UIView = {
+        
+        let contentView = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW - 60.rpx, height: 203.0.rpx))
+        contentView.backgroundColor = .white
+        contentView.addRadius(radius: 8.rpx)
+        return contentView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+      
+        let label = UILabel()
+        label.text("修改备注")
+        label.mediumFont(17)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var remarkView: UIView = {
+      
+        let view = UIView()
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addRadius(radius: 4.rpx)
+        view.addBorder(borderWidth: 1, borderColor: .hexStringColor(hexString: "#F2F2F2"))
+        return view
+    }()
+    
+    lazy var numText: UILabel = {
+        
+        let label = UILabel()
+        label.text("0/11")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var remarkTextField: UITextField = {
+      
+        let textField = UITextField()
+        textField.maxTextNumber = 11
+        textField.delegate = self
+        textField.textColor = QSLColor.Color_202020
+        textField.font = UIFont.textF(15)
+        textField.placeholder = "请输入备注"
+        textField.setPlaceholderAttribute(font: UIFont.textF(16), color: UIColor.hexStringColor(hexString: "#A7A7A7"))
+        return textField
+    }()
+    
+    lazy var firstButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.backgroundColor = .hexStringColor(hexString: "#F8F8F8")
+        btn.addRadius(radius: 20.rpx)
+        btn.title("取消")
+        btn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+        btn.mediumFont(16)
+        btn.addTarget(self, action: #selector(firstBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var secondButton: UIButton = {
+        
+        let btn = UIButton()
+        btn.addRadius(radius: 20.rpx)
+        btn.title("确认")
+        btn.textColor(.white)
+        btn.mediumFont(16)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 118.rpx, height: 40.rpx, direction: .horizontal)
+        btn.addTarget(self, action: #selector(secondBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var closeButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "public_btn_close_AAA"), for: .normal)
+        btn.addTarget(self, action: #selector(removeView), for: .touchUpInside)
+        return btn
+    }()
+    
+    var secondBtnClosure: ((String) -> ())?
+    
+    class func alert(view: UIView,
+                     secondBtnClosure: @escaping (String) -> ()) {
+        
+        let window = QSLFriendRemarkAlertView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH))
+        window.secondBtnClosure = secondBtnClosure
+        view.addSubview(window)
+        
+        gravityInstance?.track(QSLGravityConst.friend_remark_show)
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) {
+            window.backgroundColor = .hexStringColor(hexString: "#000000", alpha: 0.7)
+            window.contentView.isHidden = false
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        initUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // 取消按钮点击事件
+    @objc func firstBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.friend_remark_cancel)
+        removeView()
+    }
+    
+    // 确认按钮点击事件
+    @objc func secondBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.friend_remark_click)
+        
+        guard let text = self.remarkTextField.text, text.count > 0 else {
+            UIApplication.keyWindow?.toast(text: "请输入名称")
+            return
+        }
+        
+        if let secondBtnClosure = self.secondBtnClosure {
+            secondBtnClosure(text)
+        }
+        removeView()
+    }
+    
+    // 移除
+    @objc func removeView() {
+        
+        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: 0.05) { [weak self] in
+            self?.backgroundColor = UIColor.init(white: 0, alpha: 0)
+            self?.contentView.isHidden = true
+        } completion: { [weak self] finished in
+            self?.removeFromSuperview()
+        }
+    }
+}
+
+extension QSLFriendRemarkAlertView: UITextFieldDelegate {
+    
+    func textFieldDidChangeSelection(_ textField: UITextField) {
+        
+        let numCount = textField.text?.count
+        self.numText.text = "\(numCount ?? 0)/11"
+    }
+}
+
+extension QSLFriendRemarkAlertView {
+    
+    func initUI() {
+        
+        addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: QSLConst.qsl_kScreenW - 60.rpx, height: 203.0.rpx))
+            make.center.equalToSuperview()
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(24.rpx)
+        }
+        
+        contentView.addSubview(remarkView)
+        remarkView.snp.makeConstraints { make in
+            make.left.equalTo(29.rpx)
+            make.right.equalTo(-19.rpx)
+            make.top.equalTo(65.rpx)
+            make.height.equalTo(46.rpx)
+        }
+        
+        remarkView.addSubview(numText)
+        numText.snp.makeConstraints { make in
+            make.right.equalTo(-12.rpx)
+            make.width.equalTo(32.rpx)
+            make.centerY.equalToSuperview()
+        }
+        
+        remarkView.addSubview(remarkTextField)
+        remarkTextField.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.top.bottom.equalTo(0)
+            make.right.equalTo(numText.snp.left).offset(-12.rpx)
+        }
+        
+        contentView.addSubview(firstButton)
+        firstButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.left.equalTo(24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(secondButton)
+        secondButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 118.rpx, height: 40.rpx))
+            make.right.equalTo(-24.rpx)
+            make.bottom.equalTo(-24.rpx)
+        }
+        
+        contentView.addSubview(closeButton)
+        closeButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24.rpx, height: 24.rpx))
+            make.top.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+        }
+    }
+}

+ 139 - 0
QuickSearchLocation/Classes/Pages/QSLGuide/QSLGuideController.swift

@@ -0,0 +1,139 @@
+//
+//  QSLGuideController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/9.
+//
+
+import UIKit
+import GKCycleScrollView
+
+class QSLGuideController: QSLBaseController {
+    
+//    var currentPage
+    
+    lazy var cycleScrollView: GKCycleScrollView = {
+      
+        let cycleScrollView = GKCycleScrollView()
+        cycleScrollView.dataSource = self
+        cycleScrollView.delegate = self
+        cycleScrollView.isAutoScroll = false
+        cycleScrollView.isInfiniteLoop = false
+        cycleScrollView.isChangeAlpha = false
+        cycleScrollView.pageControl = pageControl
+        return cycleScrollView
+    }()
+    
+    lazy var pageControl: GKPageControl = {
+      
+        let pageControl = GKPageControl()
+        pageControl.style = .rectangle
+        pageControl.dotHeight = 4.rpx
+        pageControl.dotWidth = 20.rpx
+        pageControl.dotMargin = 4.rpx
+        pageControl.pageIndicatorTintColor = .hexStringColor(hexString: "#EEEEEE")
+        pageControl.currentPageIndicatorTintColor = QSLColor.themeMainColor
+        return pageControl
+    }()
+    
+    lazy var nextButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 280.rpx, height: 44.rpx, direction: .horizontal)
+        btn.title("下一步")
+        btn.mediumFont(16)
+        btn.textColor(.white)
+        btn.addRadius(radius: 22.rpx)
+        btn.addTarget(self, action: #selector(nextBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        self.view.addSubview(cycleScrollView)
+        cycleScrollView.snp.makeConstraints { make in
+            make.left.equalTo(40.rpx)
+            make.right.equalTo(-40.rpx)
+            make.height.equalTo(368.rpx)
+            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH + 72.rpx)
+        }
+        
+        self.view.addSubview(pageControl)
+        pageControl.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 68.rpx, height: 4.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(cycleScrollView.snp.bottom).offset(17.rpx)
+        }
+        
+        self.view.addSubview(nextButton)
+        nextButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 280.rpx, height: 44.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(pageControl.snp.bottom).offset(50.rpx)
+        }
+        
+        cycleScrollView.reloadData()
+    }
+    
+    // 点击下一步按钮
+    @objc func nextBtnAction() {
+        
+        switch cycleScrollView.currentSelectIndex {
+        case 0:
+            gravityInstance?.track(QSLGravityConst.guide_first_click)
+            break
+        case 1:
+            gravityInstance?.track(QSLGravityConst.guide_second_click)
+            break
+        case 2:
+            gravityInstance?.track(QSLGravityConst.guide_third_click)
+            break
+        default:
+            break
+        }
+        
+        if cycleScrollView.currentSelectIndex == 2 {
+            if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate {
+                sceneDelegate.window?.rootViewController = CustomTabBarController()
+                sceneDelegate.window?.makeKeyAndVisible()
+            }
+        } else {
+            cycleScrollView.scrollToCell(at: cycleScrollView.currentSelectIndex + 1, animated: true)
+        }
+    }
+}
+
+extension QSLGuideController: GKCycleScrollViewDataSource, GKCycleScrollViewDelegate {
+    
+    func numberOfCells(in cycleScrollView: GKCycleScrollView!) -> Int {
+        return 3
+    }
+    
+    func cycleScrollView(_ cycleScrollView: GKCycleScrollView!, cellForViewAt index: Int) -> GKCycleScrollViewCell! {
+        if let cell = cycleScrollView.dequeueReusableCell() {
+            cell.imageView.image = UIImage(named: "guide_pic_\(index + 1)")
+            return cell
+        }
+        
+        let cell = GKCycleScrollViewCell()
+        cell.imageView.image = UIImage(named: "guide_pic_\(index + 1)")
+        return cell
+    }
+    
+    func cycleScrollView(_ cycleScrollView: GKCycleScrollView!, didScrollCellTo index: Int) {
+        switch index {
+        case 0:
+            gravityInstance?.track(QSLGravityConst.guide_first_show)
+            break
+        case 1:
+            gravityInstance?.track(QSLGravityConst.guide_second_show)
+            break
+        case 2:
+            gravityInstance?.track(QSLGravityConst.guide_third_show)
+            break
+        default:
+            break
+        }
+    }
+}

+ 248 - 0
QuickSearchLocation/Classes/Pages/QSLHome/Cell/QSLHomeFriendTableViewCell.swift

@@ -0,0 +1,248 @@
+//
+//  QSLHomeFriendTableViewCell.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/12.
+//
+
+import UIKit
+import MarqueeLabel
+
+protocol QSLHomeFriendTableViewCellDelegate: NSObjectProtocol {
+    
+    func routeBtnAction(model: QSLUserModel)
+    
+    func locateBtnAction(model: QSLUserModel)
+}
+
+class QSLHomeFriendTableViewCell: UITableViewCell {
+    
+    var model: QSLUserModel?
+    
+    weak var delegate: QSLHomeFriendTableViewCellDelegate?
+    
+    lazy var bgView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .white
+        view.addRadius(radius: 8.rpx)
+        return view
+    }()
+    
+    lazy var avatarImageView: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_avatar")
+        return imageView
+    }()
+    
+    lazy var tagImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.isHidden = true
+        imageView.image = UIImage(named: "friends_cell_tag")
+        return imageView
+    }()
+    
+    lazy var nameLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text = "用户123"
+        label.font = UIFont.textM(16)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var timeLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text = "1分钟前"
+        label.font(12)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var locateButton: UIButton = {
+       
+        let button = UIButton()
+        button.setBackgroundImage(UIImage(named: "home_friends_locate_btn"), for: .normal)
+        button.addTarget(self, action: #selector(locateBtnAction), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var locateIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_location")
+        return imageView
+    }()
+    
+    lazy var addrLabel: MarqueeLabel = {
+       
+        let label = MarqueeLabel(frame: .zero, duration: 8.0, fadeLength: 10)
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        label.text = "广东奥林匹克广东奥林匹克广东奥林匹克  "
+        return label
+    }()
+    
+    lazy var checkButton: UIButton = {
+       
+        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 72.rpx, height: 28.rpx))
+        button.mediumFont(15)
+        button.textColor(QSLColor.themeMainColor)
+        button.title("轨迹")
+        
+        button.addCorner(conrners: .allCorners, radius: 22.rpx, borderWidth: 1.rpx, borderColor: QSLColor.themeMainColor)
+        
+        button.addTarget(self, action: #selector(checkButtonAction), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var blurView: UIView = {
+       
+        let effect = UIBlurEffect(style: .light)
+        let blur = UIVisualEffectView(effect: effect)
+        blur.isHidden = true
+        return blur
+    }()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        self.setCellUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func locateBtnAction() {
+        
+        if let model = self.model {
+            delegate?.locateBtnAction(model: model)
+        }
+    }
+    
+    @objc func checkButtonAction() {
+        
+        if let model = self.model {
+            delegate?.routeBtnAction(model: model)
+        }
+    }
+    
+    func config(model: QSLUserModel) {
+        
+        self.model = model
+        
+        if model.isMine {
+            
+            self.timeLabel.text = "10秒前"
+        } else {
+            
+            if model.location.timestamp > 0 {
+                let date = Date.timestampToFormatterDate(timestamp: "\(model.location.timestamp)")
+                self.timeLabel.text = date.callTimeAfterNow()
+            } else {
+                self.timeLabel.text = "未知"
+            }
+        }
+        
+        if model.remark.count > 0 {
+            self.nameLabel.text = model.remark
+        } else {
+            self.nameLabel.text = model.phone
+        }
+        
+        if model.location.addr.count > 0 {
+            self.addrLabel.text = model.location.addr + "  "
+        } else {
+            self.addrLabel.text = "未知"
+        }
+        
+        if !QSLBaseManager.shared.isVip() && !model.isMine {
+            self.blurView.isHidden = false
+        } else {
+            if model.blockedMe {
+                self.blurView.isHidden = false
+            } else {
+                self.blurView.isHidden = true
+            }
+        }
+    }
+}
+
+extension QSLHomeFriendTableViewCell {
+    
+    func setCellUI() {
+        
+        self.backgroundColor = .clear
+        self.contentView.backgroundColor = .clear
+        contentView.addSubview(bgView)
+        bgView.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+            make.top.equalTo(0)
+            make.bottom.equalTo(-8.rpx)
+        }
+        
+        bgView.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 48.rpx, height: 48.rpx))
+            make.left.equalTo(16.rpx)
+            make.top.equalTo(16.rpx)
+        }
+        
+//        bgView.addSubview(tagImageView)
+//        tagImageView.snp.makeConstraints { make in
+//            make.centerX.equalTo(avatarImageView.snp.centerX)
+//            make.centerY.equalTo(avatarImageView.snp.bottom)
+//        }
+        
+        bgView.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.top.equalTo(avatarImageView.snp.top).offset(4.rpx)
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+        }
+        
+        bgView.addSubview(timeLabel)
+        timeLabel.snp.makeConstraints { make in
+            make.centerY.equalTo(nameLabel.snp.centerY)
+            make.left.equalTo(nameLabel.snp.right).offset(8.rpx)
+        }
+        
+        bgView.addSubview(locateButton)
+        locateButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24.rpx, height: 24.rpx))
+            make.left.equalTo(timeLabel.snp.right).offset(6.rpx)
+            make.centerY.equalTo(timeLabel.snp.centerY)
+        }
+        
+        bgView.addSubview(checkButton)
+        checkButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 72.rpx, height: 28.rpx))
+            make.right.equalTo(-12.rpx)
+            make.centerY.equalTo(nameLabel.snp.centerY)
+        }
+        
+        bgView.addSubview(locateIcon)
+        locateIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 16.rpx, height: 16.rpx))
+            make.bottom.equalTo(avatarImageView.snp.bottom).offset(5.rpx)
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+        }
+        
+        bgView.addSubview(addrLabel)
+        addrLabel.snp.makeConstraints { make in
+            make.centerY.equalTo(locateIcon.snp.centerY)
+            make.left.equalTo(locateIcon.snp.right)
+            make.height.equalTo(20.rpx)
+            make.right.equalTo(-12.rpx)
+        }
+        
+        bgView.addSubview(blurView)
+        blurView.snp.makeConstraints { make in
+            make.edges.equalTo(addrLabel.snp.edges)
+        }
+    }
+}

+ 692 - 0
QuickSearchLocation/Classes/Pages/QSLHome/Controller/QSLHomeController.swift

@@ -0,0 +1,692 @@
+//
+//  QSLHomeController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+import SwiftyJSON
+import MAMapKit
+import AMapFoundationKit
+import AMapLocationKit
+
+class QSLHomeController: QSLBaseController {
+    
+    lazy var viewModel: QSLHomeViewModel = {
+       return QSLHomeViewModel()
+    }()
+    
+    var personalModel: QSLUserModel = QSLUserModel()
+    
+//    var friendList: [QSLUserModel] = [QSLUserModel]()
+    
+    override func viewDidLoad() {
+        
+        super.viewDidLoad()
+        
+        viewModel.initUIData()
+        setUpMap()
+        setUpUI()
+        
+        loadData()
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(loadData), name: QSLNotification.QSLLogin, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(loadData), name: QSLNotification.QSLLogout, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(loadData), name: QSLNotification.QSLRefreshFriend, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(loadData), name: QSLNotification.QSLRefreshRequest, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(requestVip), name: QSLNotification.QSLRefreshMember, object: nil)
+    }
+    
+    /// 高德地图
+    lazy var homeMapView = {
+        
+        let _mapView = MAMapView()
+        _mapView.delegate = self
+        
+        _mapView.minZoomLevel = 7;
+        _mapView.maxZoomLevel = 19;
+        _mapView.zoomLevel = 17;
+        
+        _mapView.showsCompass = false
+        _mapView.showsScale = false
+        _mapView.isRotateCameraEnabled = false
+        
+        _mapView.showsUserLocation = true
+        _mapView.userTrackingMode = .follow
+        
+        // 关闭显示精度圈
+        let represent = MAUserLocationRepresentation()
+        represent.showsAccuracyRing = false
+        _mapView.update(represent)
+        
+        let tap = UITapGestureRecognizer(target: self, action: #selector(mapTapAction))
+        _mapView.addGestureRecognizer(tap)
+        
+        return _mapView
+    }()
+    
+    /// 高德地图定位
+    lazy var homeMapLocationM = {
+        
+        let _homeMapLocationM = AMapLocationManager()
+        _homeMapLocationM.delegate = self
+        
+        _homeMapLocationM.distanceFilter = 100
+        _homeMapLocationM.locatingWithReGeocode = true
+        
+        _homeMapLocationM.desiredAccuracy = kCLLocationAccuracyHundredMeters
+        _homeMapLocationM.locationTimeout = 2
+        _homeMapLocationM.reGeocodeTimeout = 2
+        
+        _homeMapLocationM.pausesLocationUpdatesAutomatically = false
+        _homeMapLocationM.allowsBackgroundLocationUpdates = true
+        
+        return _homeMapLocationM
+    }()
+
+    /// 头部定位权限弹窗
+    lazy var homeAuthHeaderView = {
+       
+        let _homeAuthHeaderView = QSLHomeAuthHeaderView()
+        _homeAuthHeaderView.addRadius(radius: 6)
+        return _homeAuthHeaderView
+    }()
+    
+    lazy var homeButtonsView = {
+        
+        let _homeButtonsView = QSLHomeButtonView()
+        return _homeButtonsView
+    }()
+    
+    /// 定位按钮
+    lazy var homeLocateBtnView = {
+        
+        let _homeLocateBtnView = UIView()
+        _homeLocateBtnView.addRadius(radius: 6)
+        _homeLocateBtnView.effectViewWithAlpha(alpha: 1, size: CGSize(width: 40, height: 40), style: .light)
+        return _homeLocateBtnView
+    }()
+    
+    lazy var homeLocateBtn = {
+        
+        let _homeLocateBtn = UIButton(type: .custom)
+        _homeLocateBtn.image(UIImage(named: "home_btn_locate"))
+        return _homeLocateBtn
+    }()
+    
+    /// 无好友和未登录时的状态
+    lazy var homeEmptyView: QSLHomeEmptyView = {
+        
+        let _homeEmptyView = QSLHomeEmptyView()
+        _homeEmptyView.delegate = self
+        _homeEmptyView.isHidden = true
+        return _homeEmptyView
+    }()
+    
+    lazy var homeFriendView: QSLHomeFriendView = {
+       
+        let topHeight = QSLConst.qsl_kStatusBarFrameH
+        let view = QSLHomeFriendView(frame: CGRect(x: 0, y: QSLConst.qsl_kScreenH - viewModel.friendViewHeight - QSLConst.qsl_kTabbarFrameH, width: QSLConst.qsl_kScreenW, height: QSLConst.qsl_kScreenH - topHeight))
+        view.scrollTopHeight = topHeight - 40
+        view.delegate = self
+        return view
+    }()
+}
+
+// MARK: - 网络请求等
+extension QSLHomeController {
+    
+    @objc func loadData() {
+
+//        viewModel.refreshDataSoure(complete: { [unowned self] in
+//            self.homeFriendView.viewModel = self.viewModel
+//            let friViewTopY = QSLConst.qsl_kScreenH - viewModel.friendViewHeight - QSLConst.qsl_kTabbarFrameH
+//            homeFriendView.scrollCenterHeight = friViewTopY
+//            homeFriendView.snp.updateConstraints { make in
+//                make.top.equalTo(friViewTopY)
+//            }
+//        })
+        self.onceLocation()
+        
+        if QSLBaseManager.shared.isLogin() {
+            self.initSocket()
+        } else {
+            self.deposeSocket()
+        }
+        
+        requestFriendList()
+        requestVip()
+    }
+    
+    // 请求好友列表
+    @objc func requestFriendList() {
+        
+        self.homeMapView.removeAnnotations(self.homeMapView.annotations)
+        
+        viewModel.friendList.removeAll()
+        viewModel.friendLocationList.removeAll()
+        viewModel.friendList.append(QSLBaseManager.shared.userModel)
+        
+        QSLNetwork().request(.friendList(dict: [:])) { response in
+            
+            let list = response.mapArray(QSLUserModel.self, modelKey: "data>list")
+            self.viewModel.friendList.append(contentsOf: list)
+            self.homeFriendView.viewModel = self.viewModel
+            
+            for model in list {
+                let pointAnnotation = MAPointAnnotation()
+                let loc = CLLocationCoordinate2D(latitude: model.location.lat, longitude: model.location.lng)
+                pointAnnotation.coordinate = loc
+                
+                if model.remark.count > 0 {
+                    pointAnnotation.title = model.remark
+                } else {
+                    pointAnnotation.title = model.phone
+                }
+                pointAnnotation.subtitle = "300"
+                
+                let pointModel = QSLMapPointModel()
+                pointModel.userId = model.userId
+                pointModel.pointAnnotation = pointAnnotation
+                self.viewModel.friendLocationList.append(pointModel)
+                
+                if QSLBaseManager.shared.isVip() && !model.blockedMe {
+                    self.homeMapView.addAnnotation(pointAnnotation)
+                }
+            }
+        } fail: { code, error in
+            
+            self.homeFriendView.viewModel = self.viewModel
+        }
+    }
+    
+    // 请求vip信息
+    @objc func requestVip() {
+        
+        QSLNetwork().request(.userMember(dict: [:])) { response in
+            
+            let memberModel = response.mapObject(QSLMemberModel.self, modelKey: "data")
+            QSLBaseManager.shared.userModel.memberModel = memberModel
+            
+            if memberModel.expired {
+                QSLBaseManager.shared.saveVipExpiredTime(time: 0)
+            } else {
+                QSLBaseManager.shared.saveVipExpiredTime(time: memberModel.endTimestamp)
+            }
+            
+            QSLBaseManager.shared.saveUserId(id: memberModel.userId)
+            
+//            NotificationCenter.default.post(name: QSLNotification.QSLRefreshMember, object: nil)
+            
+        } fail: { code, error in
+            
+        }
+    }
+}
+
+// MARK: - 设置地图相关方法
+extension QSLHomeController: MAMapViewDelegate, AMapLocationManagerDelegate {
+    
+    // 初始化高德地图设置
+    func setUpMap() {
+        
+        AMapServices.shared().enableHTTPS = true
+        AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
+        AMapLocationManager.updatePrivacyAgree(.didAgree)
+        
+        homeMapLocationM.locatingWithReGeocode = true
+        homeMapLocationM.startUpdatingLocation()
+    }
+    
+    // 申请地图定位权限
+    func mapViewRequireLocationAuth(_ locationManager: CLLocationManager!) {
+        
+        locationManager.requestWhenInUseAuthorization()
+    }
+    
+    // 地图触摸事件
+    @objc func mapTapAction() {
+        
+        if self.homeFriendView.scrollLocation != .ScrollBottom {
+            self.homeFriendView.scrollToLocation(type: .ScrollBottom)
+        }
+    }
+    
+    // 获取一次性定位
+    func onceLocation() {
+        
+        // 先停止持续定位
+        self.homeMapLocationM.stopUpdatingLocation()
+        
+        self.homeMapLocationM.requestLocation(withReGeocode: true) { location, regeocode, error in
+            
+            if error != nil {
+               
+                self.personalModel = QSLBaseManager.shared.userModel
+                // 获取电话和名字
+//                self.personalModel.phone = QSLBaseManager.shared.userModel.phone
+                var name = "用户\(QSLBaseManager.shared.userModel.phone.suffix(4))"
+                if !QSLBaseManager.shared.isLogin() {
+                   name = "自己"
+                }
+                name = "自己"
+                self.personalModel.remark = name
+                // 地址
+                self.personalModel.location.addr = "请先开启位置权限"
+                QSLBaseManager.shared.updateUser(model: self.personalModel)
+
+                if self.viewModel.friendList.isEmpty {
+                    self.viewModel.friendList.append(self.personalModel)
+                } else {
+                    self.viewModel.friendList[0] = self.personalModel
+                }
+
+                self.homeFriendView.viewModel = self.viewModel
+                self.homeMapLocationM.startUpdatingLocation()
+                
+                return
+            }
+            
+                        
+            self.personalModel = QSLBaseManager.shared.userModel
+            var name = "用户\(QSLBaseManager.shared.userModel.phone.suffix(4))"
+            if !QSLBaseManager.shared.isLogin() {
+                name = "自己"
+            }
+            name = "自己"
+            self.personalModel.remark = name
+            
+            self.personalModel.location.addr = regeocode?.formattedAddress ?? "未知"
+            
+            self.personalModel.location.timestamp = Int(Date.secondStamp) ?? 0
+            
+            if let location = location {
+                // 地图居中
+                self.homeMapView.setCenter(location.coordinate, animated: true)
+                self.homeFriendView.scrollLocation = .ScrollCenter
+                
+                self.personalModel.location.lat = location.coordinate.latitude
+                self.personalModel.location.lng = location.coordinate.longitude
+                
+                // 速度
+                self.personalModel.location.speed = location.speed >= 0 ? location.speed : 0
+                
+                // 朝向
+                self.personalModel.location.bearing = location.course >= 0 ? location.course : 0
+            }
+            
+            QSLBaseManager.shared.updateUser(model: self.personalModel)
+            
+            
+            if QSLBaseManager.shared.isLogin() {
+                let locationData = self.personalModel.location
+                let addr = locationData.addr
+                if addr != "请先开启位置权限" {
+                    self.sendSocket()
+                }
+            }
+            
+            if self.viewModel.friendList.isEmpty {
+                self.viewModel.friendList.append(self.personalModel)
+            } else {
+                self.viewModel.friendList[0] = self.personalModel
+            }
+            
+            print("location: \(String(describing: location))")
+            if let regeocode = regeocode {
+                print("reGeocode: \(regeocode)")
+            }
+            
+            self.homeFriendView.viewModel = self.viewModel
+            self.homeMapLocationM.startUpdatingLocation()
+        }
+    }
+    
+    // 持续定位
+    func amapLocationManager(_ manager: AMapLocationManager!, didUpdate location: CLLocation!, reGeocode: AMapLocationReGeocode!) {
+        
+        // 获取电话和名字
+        self.personalModel = QSLBaseManager.shared.userModel
+        
+        var name = "用户\(QSLBaseManager.shared.userModel.phone.suffix(4))"
+        if !QSLBaseManager.shared.isLogin() {
+            name = "自己"
+        }
+        name = "自己"
+        self.personalModel.remark = name
+
+        // 时间戳
+        self.personalModel.location.timestamp = Int(Date.secondStamp) ?? 0
+
+        // 经纬度
+        self.personalModel.location.lat = location.coordinate.latitude
+        self.personalModel.location.lng = location.coordinate.longitude
+
+        // 速度
+        if location.speed >= 0 {
+            self.personalModel.location.speed = location.speed
+        } else {
+            self.personalModel.location.speed = 0
+        }
+
+        // 朝向
+        if location.course >= 0 {
+            self.personalModel.location.bearing = location.course
+        } else {
+            self.personalModel.location.bearing = 0
+        }
+
+        // 打印位置信息
+        print("location: {lat: \(location.coordinate.latitude); lon: \(location.coordinate.longitude); accuracy: \(location.horizontalAccuracy)}")
+
+        // 逆地理编码处理
+        if let reGeocode = reGeocode {
+            print("reGeocode: \(reGeocode)")
+            self.personalModel.location.addr = reGeocode.formattedAddress
+        } else {
+            self.personalModel.location.addr = "请先开启位置权限"
+        }
+
+        // 保存到模块
+        QSLBaseManager.shared.updateUser(model: self.personalModel)
+
+        // 更新好友列表
+        if self.viewModel.friendList.isEmpty {
+            self.viewModel.friendList.append(self.personalModel)
+        } else {
+            self.viewModel.friendList[0] = self.personalModel
+        }
+
+        self.homeFriendView.viewModel = self.viewModel
+        // 更新UI
+//        self.updateHomeUI()
+
+        // 如果已登录,发送定位信息
+        if QSLBaseManager.shared.isLogin() {
+            let locationData = self.personalModel.location
+            let addr = locationData.addr
+            if addr != "请先开启位置权限" {
+                self.sendSocket()
+            }
+        }
+    }
+    
+    func mapView(_ mapView: MAMapView!, viewFor annotation: (any MAAnnotation)!) -> MAAnnotationView! {
+        
+        // 处理用户位置的标注
+        if annotation is MAUserLocation {
+            let reuseIdentifier = "UserAnotation"
+            
+            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) as? QSLHomeAnnotatinView
+            if annotationView == nil {
+                annotationView = QSLHomeAnnotatinView(annotation: annotation, reuseIdentifier: reuseIdentifier)
+            }
+            
+            let name = "我"
+            annotationView?.title = name
+            
+            annotationView?.isEnabled = false
+            annotationView?.image = UIImage(named: "home_mine_anno")
+            annotationView?.canShowCallout = false
+            annotationView?.centerOffset = CGPoint(x: 0, y: -18)
+            annotationView?.calloutOffset = CGPoint(x: 0, y: 0)
+            annotationView?.zIndex = 360
+            annotationView?.isSelected = true
+            
+            return annotationView
+        }
+        
+        // 处理普通点标注
+        if annotation is MAPointAnnotation {
+            let reuseIdentifier = "OtherAnnotation"
+            
+            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) as? QSLHomeAnnotatinView
+            if annotationView == nil {
+                annotationView = QSLHomeAnnotatinView(annotation: annotation, reuseIdentifier: reuseIdentifier)
+                annotationView?.title = annotation.title
+            }
+            
+            if let subtitle = annotation.subtitle as? Int {
+                annotationView?.zIndex = subtitle
+            }
+            
+            annotationView?.isEnabled = false
+            annotationView?.image = UIImage(named: "home_mine_anno")
+            annotationView?.canShowCallout = false
+            annotationView?.centerOffset = CGPoint(x: 0, y: -18)
+            annotationView?.calloutOffset = CGPoint(x: 0, y: 0)
+            annotationView?.isSelected = true
+            
+            return annotationView
+        }
+        
+        return nil
+    }
+}
+
+// MARK: - 设置WebSocket
+extension QSLHomeController: QSLSocketManagerDelegate {
+    
+    // 初始化
+    func initSocket() {
+        
+        QSLSocketManager.shared.urlString = "\(QSLApi.prodWSUrl)/websocket/\(QSLBaseManager.shared.userModel.authToken)"
+        QSLSocketManager.shared.connect()
+        QSLSocketManager.shared.delegate = self
+    }
+    
+    // 关闭socket
+    func deposeSocket() {
+        
+        QSLSocketManager.shared.urlString = ""
+        QSLSocketManager.shared.close()
+        QSLSocketManager.shared.delegate = nil
+    }
+    
+    // 发送socket信息
+    func sendSocket() {
+        
+        var message = QSLMapMessageModel()
+        message.cmd = "u.location"
+        
+        var location = QSLMapTrackModel()
+        location.lat = self.personalModel.location.lat
+        location.lng = self.personalModel.location.lng
+        location.addr = self.personalModel.location.addr
+        location.speed = self.personalModel.location.speed
+        location.bearing = self.personalModel.location.bearing
+        location.timestamp = Int(Date.milliStamp) ?? 0
+        
+        message.body = location.toJSONString()
+        QSLSocketManager.shared.sendMessage(message.toJSONString())
+    }
+    
+    // 接收到消息
+    func socketDidReceiveMessage(with string: String) {
+ 
+        let locationModel = QSLMapMessageModel.mapModel(from: string)
+        if let data = locationModel.body.data(using: .utf8), let list = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [QSLMapTrackModel] {
+            
+            
+            if locationModel.cmd == "d.location.batch" {
+                
+                for model in list {
+                    
+                    var isBlockedMe = false
+                    for var user in viewModel.friendList {
+                        if model.userId == user.friendId {
+                            user.location = model
+                            isBlockedMe = user.blockedMe
+                        }
+                    }
+                    
+                    if !isBlockedMe && QSLBaseManager.shared.isVip() {
+                        
+                        for location in viewModel.friendLocationList {
+                            
+                            if model.userId == location.userId {
+                                
+                                let annotation = MAPointAnnotation()
+                                let cll2d = CLLocationCoordinate2D(latitude: model.lat, longitude: model.lng)
+                                annotation.coordinate = cll2d
+                                location.pointAnnotation = annotation
+                                self.homeMapView.addAnnotation(location.pointAnnotation)
+                            }
+                        }
+                    }
+                }
+                
+                self.homeFriendView.viewModel = self.viewModel
+            }
+        }
+    }
+}
+
+// MARK: - 设置界面代理方法
+extension QSLHomeController: QSLHomeEmptyViewDelegate, QSLHomeFriendViewDelegate {
+    
+    func viewDidScroll() {
+        switch self.homeFriendView.scrollLocation {
+        case .ScrollTop:
+            self.homeFriendView.snp.updateConstraints { make in
+                make.top.equalTo(QSLConst.qsl_kStatusBarFrameH - 40.rpx)
+            }
+            break
+        case .ScrollCenter:
+            self.homeFriendView.snp.updateConstraints { make in
+                make.top.equalTo(QSLConst.qsl_kScreenH - viewModel.friendViewHeight - QSLConst.qsl_kStatusBarFrameH)
+            }
+            break
+        case .ScrollBottom:
+            self.homeFriendView.snp.updateConstraints { make in
+                make.top.equalTo(QSLConst.qsl_kScreenH - QSLConst.qsl_kTabbarFrameH - 56.rpx - 40.rpx)
+            }
+            break
+        default:
+            break
+        }
+    }
+    
+    func addFriendAction(isSmall: Bool) {
+        
+        if isSmall {
+            QSLJumpManager.shared.pushToAdd(type: .homesmall)
+        } else {
+            QSLJumpManager.shared.pushToAdd(type: .homebig)
+        }
+    }
+    
+    func refreshBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.home_locate)
+        self.loadData()
+    }
+    
+    // 轨迹
+    func routeBtnAction(model: QSLUserModel) {
+        
+        if !QSLBaseManager.shared.isLogin() {
+            
+            if let view = self.tabBarController?.view {
+                QSLAlertView.alert(view: view, title: "温馨提示", content: "登录即可体验查看轨迹记录", secondBtnClosure:  {
+                    
+                    QSLJumpManager.shared.pushToLogin(type: .road)
+                })
+            }
+            
+            return
+        }
+        
+        if !QSLBaseManager.shared.isVip() {
+            
+            QSLJumpManager.shared.pushToVip(type: .homeRoad)
+            return
+        }
+        
+        QSLJumpManager.shared.pushToRoad(type: .home, model: model)
+    }
+    
+    // 点击列表的定位按钮
+    func locateBtnAction(model: QSLUserModel) {
+        
+        if !QSLBaseManager.shared.isVip() {
+            self.view.toast(text: "请先开通会员")
+            return
+        }
+        
+        if model.blockedMe {
+            return
+        }
+        
+        let location = CLLocationCoordinate2D(latitude: model.location.lat, longitude: model.location.lng)
+        self.homeMapView.setCenter(location, animated: true)
+        
+        for point in self.viewModel.friendLocationList {
+            
+            if point.userId == model.friendId {
+                self.homeMapView.removeAnnotation(point.pointAnnotation)
+                point.pointAnnotation?.subtitle = "350"
+                self.homeMapView.addAnnotation(point.pointAnnotation)
+            }
+        }
+    }
+    
+    func emptyFriPhoneViewClick() {
+        
+        if let vc = self.tabBarController {
+            QSLFriendAddAlertView.show(vc: vc) {
+                
+            }
+        }
+    }
+    
+    func homeFriPhoneViewClick() {
+        
+        if let vc = self.tabBarController {
+            QSLFriendAddAlertView.show(vc: vc) {
+                
+            }
+        }
+    }
+}
+
+// MARK: - 设置UI相关方法
+extension QSLHomeController {
+    
+    // 设置基础UI
+    func setUpUI() {
+        
+        view.addSubview(homeMapView)
+        homeMapView.snp.makeConstraints { make in
+            make.edges.equalTo(0)
+        }
+        
+//        view.addSubview(homeButtonsView)
+//        homeButtonsView.snp.makeConstraints { make in
+//            make.size.equalTo(CGSize(width: 40, height: 104))
+//            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH + 164)
+//            make.right.equalTo(QSLHomeViewModel.UX.viewRight)
+//        }
+//        
+//        view.addSubview(homeLocateBtnView)
+//        homeLocateBtnView.snp.makeConstraints { make in
+//            make.size.equalTo(CGSize(width: 40, height: 40))
+//            make.top.equalTo(self.homeButtonsView.snp.bottom).offset(16)
+//            make.right.equalTo(QSLHomeViewModel.UX.viewRight)
+//        }
+        
+        view.addSubview(homeFriendView)
+        homeFriendView.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.top.equalTo(QSLConst.qsl_kScreenH - viewModel.friendViewHeight - QSLConst.qsl_kTabbarFrameH)
+            make.height.equalTo(QSLConst.qsl_kScreenH - QSLConst.qsl_kTabbarFrameH)
+        }
+        
+        let friViewTopY = QSLConst.qsl_kScreenH - viewModel.friendViewHeight - QSLConst.qsl_kTabbarFrameH
+        homeFriendView.scrollCenterHeight = friViewTopY
+        homeFriendView.snp.updateConstraints { make in
+            make.top.equalTo(friViewTopY)
+        }
+    }
+}

+ 51 - 0
QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeAnnotatinView.swift

@@ -0,0 +1,51 @@
+//
+//  QSLHomeAnnotatinView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/6.
+//
+
+import UIKit
+import MAMapKit
+
+class QSLHomeAnnotatinView: MAAnnotationView {
+
+    var title: String?
+    var calloutView: MapLocationCalloutView?
+
+    override func setSelected(_ selected: Bool, animated: Bool) {
+        if self.isSelected == selected {
+            return
+        }
+        
+        if selected {
+            if calloutView == nil {
+                let width = (title?.widthAccording(height: 20.rpx, font: UIFont.textM(14), lineSpacing: 0) ?? 0)
+                calloutView = MapLocationCalloutView(frame: CGRect(x: 0, y: 0, width: width + 32.rpx, height: 20.rpx))
+                if let calloutView = calloutView {
+                    calloutView.center = CGPoint(x: bounds.size.width / 2 + calloutOffset.x,
+                                                 y: -calloutView.bounds.size.height / 2 + calloutOffset.y)
+                }
+            }
+            calloutView?.name = title
+            if let calloutView = calloutView {
+                addSubview(calloutView)
+            }
+        } else {
+            calloutView?.removeFromSuperview()
+        }
+
+        super.setSelected(selected, animated: animated)
+    }
+
+    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+        var view = super.hitTest(point, with: event)
+        if view == nil {
+            let convertedPoint = convert(point, from: self)
+            if bounds.contains(convertedPoint) {
+                view = self
+            }
+        }
+        return view
+    }
+}

+ 68 - 0
QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeAuthHeaderView.swift

@@ -0,0 +1,68 @@
+//
+//  QSLHomeAuthHeaderView.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import UIKit
+
+class QSLHomeAuthHeaderView: UIView {
+    
+    lazy var headerIcon: UIImageView = {
+        
+        let _headerIcon = UIImageView()
+        _headerIcon.image = UIImage(named: "home_icon_header")
+        return _headerIcon
+    }()
+    
+    lazy var headerLabel: UILabel = {
+        
+        let _headerLabel = UILabel()
+        _headerLabel.font(12)
+        _headerLabel.text = "开启定位权限,记录轨迹"
+        _headerLabel.textColor = QSLColor.textColor_333
+        return _headerLabel
+    }()
+    
+    lazy var headerArrowIcon: UIImageView = {
+        
+        let _headerArrowIcon = UIImageView()
+        _headerArrowIcon.image = UIImage(named: "public_arrow_right3")
+        return _headerArrowIcon
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+
+        self.effectViewWithAlpha(alpha: 1, size: CGSize(width: QSLConst.qsl_kScreenW - 80, height: 32), style: .light)
+        
+        addSubview(headerIcon)
+        addSubview(headerLabel)
+        addSubview(headerArrowIcon)
+    }
+    
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        
+        self.headerIcon.snp.makeConstraints { make in
+            make.left.equalTo(8)
+            make.centerY.equalTo(snp.centerY)
+        }
+        
+        self.headerLabel.snp.makeConstraints { make in
+            make.left.equalTo(self.headerIcon.snp.right).offset(6)
+            make.centerY.equalTo(snp.centerY)
+        }
+        
+        self.headerArrowIcon.snp.makeConstraints { make in
+            make.right.equalTo(-8)
+            make.centerY.equalTo(snp.centerY)
+        }
+    }
+
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 65 - 0
QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeButtonView.swift

@@ -0,0 +1,65 @@
+//
+//  QSLHomeButtonView.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import UIKit
+
+class QSLHomeButtonView: UIView {
+    
+    lazy var homeResortBtn: UIButton = {
+        
+        let _homeResortBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 52))
+        _homeResortBtn.textColor(QSLColor.textColor_333)
+        _homeResortBtn.font(10)
+        _homeResortBtn.title("求助")
+        
+        _homeResortBtn.image(UIImage(named: "home_btn_resort"))
+        _homeResortBtn.setImageTitleLayout(.imgTop, spacing: 2)
+        
+        return _homeResortBtn
+    }()
+    
+    lazy var homeRefreshBtn: UIButton = {
+        
+        let _homeRefreshBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 52))
+        _homeRefreshBtn.textColor(QSLColor.textColor_333)
+        _homeRefreshBtn.font(10)
+        _homeRefreshBtn.title("刷新")
+        
+        _homeRefreshBtn.image(UIImage(named: "home_btn_refresh"))
+        _homeRefreshBtn.setImageTitleLayout(.imgTop, spacing: 2)
+        
+        return _homeRefreshBtn
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        addRadius(radius: 6)
+        effectViewWithAlpha(alpha: 1, size: CGSize(width: 40, height: 104), style: .light)
+        
+        addSubview(homeResortBtn)
+        addSubview(homeRefreshBtn)
+    }
+    
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        
+        homeResortBtn.snp.makeConstraints { make in
+            make.top.left.right.equalTo(0)
+            make.height.equalTo(52)
+        }
+        
+        homeRefreshBtn.snp.makeConstraints { make in
+            make.bottom.left.right.equalTo(0)
+            make.height.equalTo(52)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 49 - 0
QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeCallOutView.swift

@@ -0,0 +1,49 @@
+//
+//  QSLHomeCallOutView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/6.
+//
+
+import UIKit
+
+class MapLocationCalloutView: UIView {
+    
+    var name: String? {
+        didSet {
+            nameLabel.text = name
+            
+            let width = (name?.widthAccording(height: 20.rpx, font: UIFont.textM(14), lineSpacing: 0) ?? 0) + 16.rpx
+            self.nameLabel.snp.updateConstraints { make in
+                make.width.equalTo(width)
+            }
+        }
+    }
+    
+    lazy var nameLabel: UILabel = {
+       
+        let label = UILabel()
+        label.textAlignment = .center
+        label.backgroundColor = .hexStringColor(hexString: "#000000", alpha: 0.7)
+        label.addRadius(radius: 4.rpx)
+        label.mediumFont(14)
+        label.textColor = .white
+        label.text = "11111"
+        return label
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        self.addSubview(self.nameLabel)
+        self.nameLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.height.equalTo(20.rpx)
+            make.width.equalTo(30.rpx)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 258 - 0
QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeEmptyView.swift

@@ -0,0 +1,258 @@
+//
+//  QSLHomeEmptyView.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import UIKit
+import MarqueeLabel
+
+protocol QSLHomeEmptyViewDelegate: NSObjectProtocol {
+    
+    func emptyFriPhoneViewClick()
+}
+
+class QSLHomeEmptyView: UIView {
+    
+    weak var delegate: QSLHomeEmptyViewDelegate?
+    
+    lazy var emptyBgView: UIView = {
+       
+        let _emptyBgView = UIView()
+        
+        if let image = UIImage.gradient([UIColor.hexStringColor(hexString: "#F6F6F6", alpha: 0), QSLColor.backGroundColor], size: CGSize(width: qsl_kScreenW, height: 175), locations: [0,22.0 / 175.0], direction: .vertical) {
+            _emptyBgView.backgroundColor = UIColor(patternImage: image)
+        }
+
+        return _emptyBgView
+    }()
+    
+    lazy var emptyFriView: UIView = {
+        
+        let _emptyFriView = UIView()
+        _emptyFriView.backgroundColor = .white
+        _emptyFriView.addRadius(radius: 6)
+        return _emptyFriView
+    }()
+    
+    lazy var emptyFriTitleIcon: UIImageView = {
+        
+        let _emptyFriTitleIcon = UIImageView()
+        _emptyFriTitleIcon.image = UIImage.image(color: QSLColor.themeMainColor, size: CGSize(width: 4, height: 16))
+        return _emptyFriTitleIcon
+    }()
+    
+    lazy var emptyFriTitleLabel: UILabel = {
+      
+        let _emptyFriTitleLabel = UILabel()
+        _emptyFriTitleLabel.boldFont(15)
+        _emptyFriTitleLabel.textColor = QSLColor.textColor_333
+        _emptyFriTitleLabel.text = "添加好友"
+        return _emptyFriTitleLabel
+    }()
+    
+    lazy var emptyFriPhoneView: UIView = {
+       
+        let _emptyFriPhoneView = UIView()
+        _emptyFriPhoneView.backgroundColor = QSLColor.backGroundColor
+        _emptyFriPhoneView.addRadius(radius: 4)
+        
+        _emptyFriPhoneView.isUserInteractionEnabled = true
+        let tap = UITapGestureRecognizer(target: self, action: #selector(emptyFriPhoneViewAction))
+        _emptyFriPhoneView.addGestureRecognizer(tap)
+        
+        return _emptyFriPhoneView
+    }()
+    
+    lazy var emptyFriPhoneLabel: UILabel = {
+        
+        let _emptyFriPhoneLabel = UILabel()
+        _emptyFriPhoneLabel.font(14)
+        _emptyFriPhoneLabel.textColor = QSLColor.textColor_AAA
+        _emptyFriPhoneLabel.text = "输入手机号码 查看定位"
+        return _emptyFriPhoneLabel
+    }()
+    
+    lazy var lineIcon: UIImageView = {
+       
+        let _lineIcon = UIImageView()
+        _lineIcon.image = UIImage.image(color: QSLColor.textColor_AAA, size: CGSize(width: 1, height: 16))
+        return _lineIcon
+    }()
+    
+    lazy var emptyFriAddLabel: UILabel = {
+        
+        let _emptyFriAddLabel = UILabel()
+        _emptyFriAddLabel.mediumFont(14)
+        _emptyFriAddLabel.textColor = QSLColor.textColor_333
+        _emptyFriAddLabel.text = "添加"
+        return _emptyFriAddLabel
+    }()
+    
+    lazy var emptyInfoView: UIView = {
+        
+        let _emptyInfoView = UIView()
+        _emptyInfoView.backgroundColor = UIColor.hexStringColor(hexString:"#E6CD9F")
+        _emptyInfoView.addRadius(radius: 14)
+        return _emptyInfoView
+    }()
+    
+    lazy var emptyInfoBgImageView: UIImageView = {
+        
+        let _emptyInfoBgImageView = UIImageView()
+        _emptyInfoBgImageView.image = UIImage(named: "home_empty_info_bg")
+        return _emptyInfoBgImageView
+    }()
+    
+    lazy var emptyInfoIcon: UIImageView = {
+        
+        let _emptyInfoIcon = UIImageView()
+        _emptyInfoIcon.image = UIImage(named: "home_empty_info_icon")
+        return _emptyInfoIcon
+    }()
+    
+    lazy var emptyInfoUserLabel: UILabel = {
+        
+        let _emptyInfoUserLabel = UILabel()
+        _emptyInfoUserLabel.font(12)
+        _emptyInfoUserLabel.textColor = .white
+        _emptyInfoUserLabel.text = "我:"
+        return _emptyInfoUserLabel
+    }()
+    
+    lazy var emptyInfoAddrLabel: MarqueeLabel = {
+       
+//        let _emptyInfoAddrLabel = MarqueeLabel(frame: CGRect(x: 0, y: 0, width: qsl_kScreenW - 224.0, height: 17.0), duration: 8.0, fadeLength: 10)
+        let _emptyInfoAddrLabel = MarqueeLabel(frame: .zero, duration: 8.0, fadeLength: 10)
+        _emptyInfoAddrLabel.font(12)
+        _emptyInfoAddrLabel.textColor = .white
+        _emptyInfoAddrLabel.text = "广东奥林匹克广东奥林匹克广东奥林匹克  "
+        return _emptyInfoAddrLabel
+    }()
+    
+    lazy var emptyInfoCheckLabel: UILabel = {
+        
+        let _emptyInfoCheckLabel = UILabel()
+        _emptyInfoCheckLabel.textAlignment = .right
+        _emptyInfoCheckLabel.font(12)
+        _emptyInfoCheckLabel.textColor = UIColor.hexStringColor(hexString: "#5A330F")
+        _emptyInfoCheckLabel.text = "查看轨迹"
+        return _emptyInfoCheckLabel
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        addSubview(emptyBgView)
+        emptyBgView.addSubview(emptyFriView)
+        emptyFriView.addSubview(emptyFriTitleIcon)
+        emptyFriView.addSubview(emptyFriTitleLabel)
+        emptyFriView.addSubview(emptyFriPhoneView)
+        emptyFriPhoneView.addSubview(emptyFriPhoneLabel)
+        emptyFriPhoneView.addSubview(lineIcon)
+        emptyFriPhoneView.addSubview(emptyFriAddLabel)
+        
+        addSubview(emptyInfoView)
+        emptyInfoView.addSubview(emptyInfoBgImageView)
+        emptyInfoView.addSubview(emptyInfoIcon)
+        emptyInfoView.addSubview(emptyInfoUserLabel)
+        emptyInfoView.addSubview(emptyInfoAddrLabel)
+        emptyInfoView.addSubview(emptyInfoCheckLabel)
+    }
+    
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        
+        emptyBgView.snp.makeConstraints { make in
+            make.edges.equalTo(0)
+        }
+        
+        emptyFriView.snp.makeConstraints { make in
+            make.left.equalTo(12)
+            make.right.bottom.equalTo(-12)
+            make.height.equalTo(127)
+        }
+        
+        emptyFriTitleIcon.snp.makeConstraints { make in
+            make.left.equalTo(12)
+            make.top.equalTo(20)
+        }
+        
+        emptyFriTitleLabel.snp.makeConstraints { make in
+            make.left.equalTo(emptyFriTitleIcon.snp.right).offset(6)
+            make.centerY.equalTo(emptyFriTitleIcon.snp.centerY)
+        }
+        
+        emptyFriPhoneView.snp.makeConstraints { make in
+            make.left.equalTo(12)
+            make.right.equalTo(-12)
+            make.bottom.equalTo(-25)
+            make.height.equalTo(44)
+        }
+        
+        emptyFriPhoneLabel.snp.makeConstraints { make in
+            make.left.equalTo(16)
+            make.centerY.equalTo(emptyFriPhoneView.snp.centerY)
+        }
+        
+        emptyFriAddLabel.snp.makeConstraints { make in
+            make.right.equalTo(-14)
+            make.centerY.equalTo(emptyFriPhoneView.snp.centerY)
+        }
+        
+        lineIcon.snp.makeConstraints { make in
+            make.right.equalTo(emptyFriAddLabel.snp.left).offset(-11)
+            make.centerY.equalTo(emptyFriPhoneView.snp.centerY)
+        }
+        
+        emptyInfoView.snp.makeConstraints { make in
+            make.left.equalTo(12);
+            make.bottom.equalTo(emptyFriView.snp.top).offset(-8);
+            make.width.equalTo(269);
+            make.height.equalTo(28)
+        }
+        
+        emptyInfoBgImageView.snp.makeConstraints { make in
+            make.left.top.bottom.equalTo(0)
+        }
+        
+        emptyInfoIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 24, height: 24))
+            make.left.top.equalTo(2)
+            make.bottom.equalTo(-2)
+        }
+        
+        emptyInfoUserLabel.snp.makeConstraints { make in
+            make.width.equalTo(22)
+            make.left.equalTo(emptyInfoIcon.snp.right).offset(6)
+            make.centerY.equalTo(emptyInfoIcon.snp.centerY)
+        }
+        
+        emptyInfoCheckLabel.snp.makeConstraints { make in
+            make.top.bottom.equalTo(0);
+            make.right.equalTo(-8);
+            make.width.equalTo(58)
+        }
+        
+        emptyInfoAddrLabel.snp.makeConstraints { make in
+            make.left.equalTo(emptyInfoUserLabel.snp.right)
+            make.centerY.equalTo(emptyInfoIcon.snp.centerY)
+            make.right.equalTo(emptyInfoCheckLabel.snp.left).offset(-8)
+            make.height.equalTo(17)
+        }
+        
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension QSLHomeEmptyView {
+    
+    @objc func emptyFriPhoneViewAction() {
+        
+        delegate?.emptyFriPhoneViewClick()
+    }
+}

+ 97 - 0
QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeFriendFooterView.swift

@@ -0,0 +1,97 @@
+//
+//  QSLHomeFriendFooterView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/25.
+//
+
+import UIKit
+
+protocol QSLHomeFriendFooterViewDelegate: NSObjectProtocol {
+    
+    func addButtonAction()
+}
+
+class QSLHomeFriendFooterView: UIView {
+    
+    weak var delegate: QSLHomeFriendFooterViewDelegate?
+    
+    lazy var bgView: UIView = {
+        
+        let view = UIView()
+        view.backgroundColor = .white
+        view.addRadius(radius: 8.rpx)
+        return view
+    }()
+    
+    lazy var mainImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "home_friends_footer_icon")
+        return imageView
+    }()
+    
+    lazy var addButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.addRadius(radius: 22.rpx)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 280.rpx, height: 44.rpx, direction: .horizontal)
+        btn.image(UIImage(named: "home_friends_header_add_icon"))
+        btn.title("添加好友")
+        btn.mediumFont(15)
+        btn.textColor(.white)
+        btn.setImageTitleLayout(.imgLeft, spacing: 0)
+        btn.addTarget(self, action: #selector(addButtonAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var noMoreLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("- 没有更多了 -")
+        label.font(12)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        self.addSubview(bgView)
+        bgView.snp.makeConstraints { make in
+            make.left.equalTo(8.rpx)
+            make.right.equalTo(-8.rpx)
+            make.height.equalTo(170.rpx)
+            make.top.equalTo(0)
+        }
+        
+        self.addSubview(noMoreLabel)
+        noMoreLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(bgView.snp.bottom).offset(8.rpx)
+        }
+        
+        bgView.addSubview(mainImageView)
+        mainImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 106.rpx, height: 83.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(11.rpx)
+        }
+        
+        bgView.addSubview(addButton)
+        addButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 280.rpx, height: 44.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(mainImageView.snp.bottom).offset(8.rpx)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func addButtonAction() {
+        
+        delegate?.addButtonAction()
+    }
+}

+ 458 - 0
QuickSearchLocation/Classes/Pages/QSLHome/View/QSLHomeFriendView.swift

@@ -0,0 +1,458 @@
+//
+//  QSLHomeFriendView.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/12.
+//
+
+import UIKit
+
+enum FriendViewLocation {
+    case ScrollTop
+    case ScrollCenter
+    case ScrollBottom
+}
+
+protocol QSLHomeFriendViewDelegate: NSObjectProtocol {
+    
+    func refreshBtnAction()
+    
+    func homeFriPhoneViewClick()
+    
+    func routeBtnAction(model: QSLUserModel)
+    
+    func locateBtnAction(model: QSLUserModel)
+    
+    func addFriendAction(isSmall: Bool)
+    
+    func viewDidScroll()
+}
+
+class QSLHomeFriendView: UIView {
+    
+    weak var delegate: QSLHomeFriendViewDelegate?
+    
+    var viewModel:QSLHomeViewModel? {
+        didSet {
+            friTableView.reloadData()
+        }
+    }
+    
+    /// 位置
+    var scrollLocation: FriendViewLocation?
+    /// 滑动到最后停下来的位置
+    var scrollStopY: CGFloat?
+    /// 滑动顶部距离顶部的距离
+    var scrollTopHeight: CGFloat?
+    /// 滑动中间距离顶部的距离
+    var scrollCenterHeight: CGFloat?
+    /// 滑动底部距离顶部的距离
+    var scrollBottomHeight: CGFloat?
+    
+    lazy var friMapLogoImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.isHidden = true
+        imageView.image = UIImage(named: "home_friends_map_logo")
+        return imageView
+    }()
+    
+    lazy var friExpandButton: UIButton = {
+       
+        let button = UIButton(type: .custom)
+        button.backgroundColor = UIColor.hexStringColor(hexString: "#333333", alpha: 0.6)
+        button.addRadius(radius: 2)
+        return button
+    }()
+    
+    lazy var refreshButton: UIButton = {
+        
+        let button = UIButton(type: .custom)
+        button.setBackgroundImage(UIImage(named: "home_refresh_btn"), for: .normal)
+        button.addTarget(self, action: #selector(refreshBtnAction), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var friBgView: UIView = {
+        
+        let view = UIView()
+        
+        view.clipsToBounds = true
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addRadius(radius: 12.rpx)
+        
+        return view
+    }()
+    
+    lazy var friHeaderView: UIView = {
+      
+        let view = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: 61.rpx))
+        if let image = UIImage.gradient([UIColor.hexStringColor(hexString: "#FFFFFF", alpha: 1), UIColor.hexStringColor(hexString: "#FFFFFF", alpha: 0)], size: CGSize(width: qsl_kScreenW, height: 61.rpx), locations: [0, 1], direction: .vertical) {
+            view.backgroundColor = UIColor(patternImage: image)
+        }
+        view.addFourCorner(topLeft: 12.rpx, topRight: 12.rpx, bottomLeft: 0, bottomRight: 0)
+        return view
+    }()
+    
+    lazy var friHeaderTitleIcon: UIImageView = {
+      
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "home_friends_header_title")
+        return imageView
+    }()
+    
+    lazy var friHeaderAddBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "home_friends_header_add_btn_bg"), for: .normal)
+        btn.image(UIImage(named: "home_friends_header_add_icon"))
+        btn.title("添加好友")
+        btn.mediumFont(15)
+        btn.textColor(.white)
+        btn.setImageTitleLayout(.imgLeft, spacing: 0)
+        btn.addTarget(self, action: #selector(topAddButtonAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var friTableView: UITableView = {
+        
+        let tableView = UITableView(frame: .zero, style: .grouped)
+        tableView.backgroundColor = .clear
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.tableViewNeverAdjustContentInset()
+        tableView.bounces = false
+        tableView.isUserInteractionEnabled = true
+        tableView.isScrollEnabled = false
+        tableView.contentInsetAdjustmentBehavior = .never
+        tableView.register(cellClass: QSLHomeFriendTableViewCell.self)
+        
+        return tableView
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        self.scrollCenterHeight = self.qsl_top
+        self.scrollBottomHeight = qsl_kScreenH - qsl_kTabbarFrameH - 56 - 40
+        self.setupUI()
+        self.addPanAction()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func refreshBtnAction() {
+        
+        delegate?.refreshBtnAction()
+    }
+
+}
+
+// MARK: - 设置UI
+extension QSLHomeFriendView {
+    
+    func setupUI() {
+        
+        addSubview(friBgView)
+        addSubview(friTableView)
+        
+        addSubview(friMapLogoImageView)
+        addSubview(friExpandButton)
+        addSubview(refreshButton)
+        
+        addSubview(friHeaderView)
+        friHeaderView.addSubview(friHeaderTitleIcon)
+        friHeaderView.addSubview(friHeaderAddBtn)
+        
+        friMapLogoImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 62, height: 20))
+            make.top.equalTo(0)
+            make.left.equalTo(12)
+        }
+        
+        friExpandButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 80, height: 4))
+            make.top.equalTo(friMapLogoImageView.snp.bottom).offset(8)
+            make.centerX.equalTo(snp.centerX)
+        }
+        
+        friBgView.snp.makeConstraints { make in
+            make.top.equalTo(friExpandButton.snp.bottom).offset(8.rpx)
+            make.left.bottom.right.equalTo(0)
+        }
+        
+        refreshButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 40.rpx, height: 40.rpx))
+            make.right.equalTo(-8.rpx)
+            make.bottom.equalTo(friBgView.snp.top).offset(-12.rpx)
+        }
+        
+        friHeaderView.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.height.equalTo(61.rpx)
+            make.top.equalTo(friExpandButton.snp.bottom).offset(8.rpx)
+        }
+        
+        friHeaderTitleIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 82.rpx, height: 26.5.rpx))
+            make.left.equalTo(16.rpx)
+            make.top.equalTo(16.rpx)
+        }
+        
+        friHeaderAddBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 106.rpx, height: 32.rpx))
+            make.right.equalTo(-8.rpx)
+            make.centerY.equalTo(friHeaderTitleIcon.snp.centerY)
+        }
+        
+        friTableView.snp.makeConstraints { make in
+            make.left.equalTo(0)
+            make.right.equalTo(0)
+            make.top.equalTo(friBgView.snp.top).offset(50.rpx)
+            make.bottom.equalTo(-12.rpx)
+        }
+    }
+}
+
+// MARK: - 点击事件
+extension QSLHomeFriendView {
+    
+    @objc func homeFriPhoneViewAction() {
+        delegate?.homeFriPhoneViewClick()
+    }
+    
+    @objc func topAddButtonAction() {
+        delegate?.addFriendAction(isSmall: true)
+    }
+}
+
+extension QSLHomeFriendView: QSLHomeFriendTableViewCellDelegate {
+    
+    func routeBtnAction(model: QSLUserModel) {
+        delegate?.routeBtnAction(model: model)
+    }
+    
+    func locateBtnAction(model: QSLUserModel) {
+        delegate?.locateBtnAction(model: model)
+    }
+}
+
+// MARK: - 添加滑动逻辑
+extension QSLHomeFriendView: UIGestureRecognizerDelegate {
+    
+    func addPanAction() {
+
+        let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction))
+        pan.delegate = self
+        self.addGestureRecognizer(pan)
+    }
+    
+    @objc func panAction(pan: UIPanGestureRecognizer) {
+        
+        let point = pan.translation(in: self)
+        if let scrollStopY = self.scrollStopY, scrollStopY > 0 {
+            pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+            return
+        }
+        
+        self.qsl_top += point.y
+        if let scrollTopHeight = self.scrollTopHeight, self.qsl_top < scrollTopHeight {
+            self.qsl_top = scrollTopHeight
+        }
+        
+        if let scrollBottomHeight = self.scrollBottomHeight, self.qsl_top > scrollBottomHeight {
+            self.qsl_top = scrollBottomHeight
+        }
+        
+        if pan.state == .ended || pan.state == .cancelled {
+            
+            let velocity = pan.velocity(in: self)
+            let speed = 350.0
+            
+            if let scrollLocation = self.scrollLocation {
+                switch scrollLocation {
+                case .ScrollTop:
+                    if velocity.y < -speed {
+                        self.scrollToLocation(type: .ScrollTop)
+                        pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+                        return
+                    } else if velocity.y > speed {
+                        self.scrollToLocation(type: .ScrollCenter)
+                        pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+                        return
+                    }
+                    break
+                case .ScrollCenter:
+                    if velocity.y < -speed {
+                        self.scrollToLocation(type: .ScrollTop)
+                        pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+                        return
+                    } else if velocity.y > speed {
+                        self.scrollToLocation(type: .ScrollBottom)
+                        pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+                        return
+                    }
+                    break
+                case .ScrollBottom:
+                    if velocity.y < -speed {
+                        self.scrollToLocation(type: .ScrollCenter)
+                        pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+                        return
+                    } else if velocity.y > speed {
+                        self.scrollToLocation(type: .ScrollBottom)
+                        pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+                        return
+                    }
+                    break
+                }
+            }
+            
+            if self.qsl_top < (qsl_kScreenH - qsl_kTabbarBottom) / 4 {
+                self.scrollToLocation(type: .ScrollTop)
+            } else if self.qsl_top < (qsl_kScreenH - qsl_kTabbarBottom) / 4 * 3 && self.qsl_top > (qsl_kScreenH - qsl_kTabbarBottom) / 4 {
+                self.scrollToLocation(type: .ScrollCenter)
+            } else {
+                self.scrollToLocation(type: .ScrollBottom)
+            }
+        }
+        
+        pan.setTranslation(CGPoint(x: 0, y: 0), in: self)
+    }
+    
+    func scrollToLocation(type: FriendViewLocation) {
+        
+        let animation = CASpringAnimation(keyPath: "position.y")
+        animation.damping = 10
+        animation.stiffness = 200
+        animation.mass = 1
+        animation.initialVelocity = 10
+        animation.duration = animation.settlingDuration
+//        animation.fromValue = self.friPhoneView.layer.position.y
+//        animation.toValue = self.friPhoneView.layer.position.y + 2
+        animation.isRemovedOnCompletion = false
+        animation.fillMode = .forwards
+        
+        let animation1 = CASpringAnimation(keyPath: "position.y")
+        animation1.damping = 10
+        animation1.stiffness = 200
+        animation1.mass = 1
+        animation1.initialVelocity = 10
+        animation1.duration = animation.settlingDuration
+        animation1.fromValue = self.friTableView.layer.position.y
+        animation1.toValue = self.friTableView.layer.position.y + 2
+        animation1.isRemovedOnCompletion = false
+        animation1.fillMode = .forwards
+        
+        self.scrollLocation = type
+        
+        if let topHeight = self.scrollTopHeight, let centerHeight = self.scrollCenterHeight, let bottomHeight = self.scrollBottomHeight {
+            switch type {
+            
+            case .ScrollTop:
+                
+                UIView.animate(withDuration: 0.2) {
+                    self.qsl_top = topHeight
+                } completion: { finished in
+                    self.friTableView.isScrollEnabled = true
+//                    self.friPhoneView.layer.add(animation, forKey: "animation")
+                    self.friTableView.layer.add(animation1, forKey: "animation")
+                }
+                break
+            case .ScrollCenter:
+                
+                UIView.animate(withDuration: 0.2) {
+                    self.qsl_top = centerHeight
+                } completion: { finished in
+                    self.friTableView.isScrollEnabled = false
+//                    self.friPhoneView.layer.add(animation, forKey: "animation")
+                    self.friTableView.layer.add(animation1, forKey: "animation")
+                }
+                break
+            case .ScrollBottom:
+                
+                UIView.animate(withDuration: 0.2) {
+                    self.qsl_top = bottomHeight
+                } completion: { finished in
+                    self.friTableView.isScrollEnabled = false
+//                    self.friPhoneView.layer.add(animation, forKey: "animation")
+                    self.friTableView.layer.add(animation1, forKey: "animation")
+                }
+                break
+            }
+        }
+    
+        delegate?.viewDidScroll()
+    }
+    
+    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
+        return true
+    }
+    
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        let currentPostion = scrollView.contentOffset.y
+        self.scrollStopY = currentPostion
+    }
+}
+
+extension QSLHomeFriendView: QSLHomeFriendFooterViewDelegate {
+    
+    func addButtonAction() {
+        delegate?.addFriendAction(isSmall: false)
+    }
+}
+
+// MARK: - 设置Tableview
+extension QSLHomeFriendView: UITableViewDelegate, UITableViewDataSource {
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        guard let friendList = viewModel?.friendList else { return 0 }
+        return friendList.count
+    }
+    
+    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
+        
+//        if indexPath.row == 0 {
+//            
+//            cell.addCorner(conrners: [.topRight, .topLeft], radius: 6)
+//        }
+//        
+//        if let friendList = viewModel?.friendList, indexPath.row == friendList.count - 1 {
+//            
+//            cell.addCorner(conrners: [.bottomLeft, .bottomRight], radius: 6)
+//        }
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    
+        let cell = tableView.dequeueReusableCell(cellType: QSLHomeFriendTableViewCell.self, cellForRowAt: indexPath)
+        cell.selectionStyle = .none
+        cell.delegate = self
+        if let model = viewModel?.friendList[indexPath.row] {
+            cell.config(model: model)
+        }
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return 94.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 0.0001
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 200.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
+        let view = QSLHomeFriendFooterView()
+        view.delegate = self
+        return view
+    }
+    
+}

+ 50 - 0
QuickSearchLocation/Classes/Pages/QSLHome/ViewModel/QSLHomeViewModel.swift

@@ -0,0 +1,50 @@
+//
+//  QSLHomeViewModel.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/11.
+//
+
+import UIKit
+
+class QSLHomeViewModel: NSObject {
+    
+    struct UX {
+        static let viewRight: CGFloat = -12
+        static let twofriendViewHeight: CGFloat = 281
+        static let moreThanTwoFriViewHeight: CGFloat = 320
+        static let friendViewHeight: CGFloat = 355.rpx
+    }
+    
+    var friendViewHeight: CGFloat = 0
+    
+    var friendList: [QSLUserModel] = [QSLUserModel]()
+    
+    var friendLocationList: [QSLMapPointModel] = [QSLMapPointModel]()
+    
+    typealias completionClosure = () -> Void
+    
+    func initUIData() {
+        friendViewHeight = UX.friendViewHeight
+    }
+}
+
+extension QSLHomeViewModel {
+    
+//    func refreshDataSoure(complete: completionClosure) {
+//        
+//        friendList = [QSLUserModel]()
+//        for _ in 0...4 {
+//            let model = QSLUserModel()
+//            friendList?.append(model)
+//        }
+//        if let friendList = self.friendList {
+//            if friendList.count < 2 {
+//                friendViewHeight = UX.twofriendViewHeight
+//            } else {
+//                friendViewHeight = UX.moreThanTwoFriViewHeight
+//            }
+//        }
+//        complete()
+//    }
+}

+ 423 - 0
QuickSearchLocation/Classes/Pages/QSLLogin/Controller/QSLLoginViewController.swift

@@ -0,0 +1,423 @@
+//
+//  QSLLoginViewController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/26.
+//
+
+import UIKit
+import YYText
+
+enum QSLLoginJumpType: Int {
+    
+    case mine    // 我的
+    case add     // 添加好友
+    case road    // 查看轨迹
+    case contact // 添加紧急联系人
+}
+
+class QSLLoginViewController: QSLBaseController {
+    
+    var type: QSLLoginJumpType?
+    
+    lazy var loginBg: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_bg")
+        return imageView
+    }()
+    
+    lazy var backButton: UIButton = {
+       
+        let button = UIButton()
+        button.image(UIImage(named: "public_back_btn"))
+        button.title("登录")
+        button.mediumFont(17)
+        button.textColor(QSLColor.Color_202020)
+        button.setImageTitleLayout(.imgLeft, spacing: 4.rpx)
+        button.addTarget(self, action: #selector(backBtnAction), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var logoImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_about_logo")
+        return imageView
+    }()
+    
+    lazy var phoneView: UIView = {
+      
+        let view = UIView()
+        view.addRadius(radius: 6.rpx)
+        view.backgroundColor = .hexStringColor(hexString: "#FAFAFA")
+        return view
+    }()
+    
+    lazy var phonePreLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("+86")
+        label.mediumFont(16)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var phoneLineView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .hexStringColor(hexString: "#E2E2E2")
+        return view
+    }()
+    
+    lazy var phoneTextField: UITextField = {
+       
+        let textField = UITextField()
+        textField.maxTextNumber = 11
+        textField.delegate = self
+        textField.keyboardType = .numberPad
+        textField.textColor = QSLColor.Color_202020
+        textField.font = UIFont.textF(16)
+        textField.placeholder = "请输入11位手机号码"
+        textField.setPlaceholderAttribute(font: UIFont.textF(16), color: UIColor.hexStringColor(hexString: "#A7A7A7"))
+        return textField
+    }()
+    
+    lazy var codeView: UIView = {
+      
+        let view = UIView()
+        view.addRadius(radius: 6.rpx)
+        view.backgroundColor = .hexStringColor(hexString: "#FAFAFA")
+        return view
+    }()
+    
+    lazy var codeTextField: UITextField = {
+      
+        let textField = UITextField()
+        textField.delegate = self
+        textField.keyboardType = .numberPad
+        textField.textColor = QSLColor.Color_202020
+        textField.font = UIFont.textF(16)
+        textField.placeholder = "请输入验证码"
+        textField.setPlaceholderAttribute(font: UIFont.textF(16), color: UIColor.hexStringColor(hexString: "#A7A7A7"))
+        return textField
+    }()
+    
+    lazy var codeButton: UIButton = {
+        
+        let button = UIButton()
+        button.addRadius(radius: 4.rpx)
+        button.setBackgroundColor(.hexStringColor(hexString: "#15CBA1", alpha: 0.2), forState: .disabled)
+        button.setBackgroundColor(.hexStringColor(hexString: "#15CBA1", alpha: 1), forState: .normal)
+        button.title("发送验证码")
+        button.textColor(.white)
+        button.font(14)
+        button.addTarget(self, action: #selector(codeButtonAction), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var selectBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.image(UIImage(named: "public_select_btn_false"), .normal)
+        btn.image(UIImage(named: "public_select_btn_true"), .selected)
+        btn.addTarget(self, action: #selector(selectButtonAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var serviceLabel: YYLabel = {
+       
+        let label = YYLabel()
+        
+        let attr = NSMutableAttributedString()
+        
+        let firstAttr = NSMutableAttributedString(string: "已阅读并同意")
+        firstAttr.font(12)
+        firstAttr.color(.hexStringColor(hexString: "#A7A7A7"))
+        attr.append(firstAttr)
+        
+        let blankAttr = NSMutableAttributedString(string: " ")
+        blankAttr.font(12)
+        attr.append(blankAttr)
+        
+        let privacyHL = YYTextHighlight()
+        var privacyStr = "《隐私权政策》"
+        
+        let privacyText = NSMutableAttributedString(string: privacyStr)
+        
+        privacyText.font(12)
+        privacyText.color(.hexStringColor(hexString: "#2F79FF"))
+        privacyText.yy_setTextHighlight(privacyHL, range: NSRange(location: 0, length: privacyStr.count))
+        privacyHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.privacyAction()
+        }
+        attr.append(privacyText)
+        
+        attr.append(blankAttr)
+        
+        let andAttr = NSMutableAttributedString(string: "和")
+        andAttr.font(12)
+        andAttr.color(.hexStringColor(hexString: "#A7A7A7"))
+        attr.append(andAttr)
+        
+        attr.append(blankAttr)
+        
+        let serviceHL = YYTextHighlight()
+        var serviceStr = "《用户协议》"
+
+        let serviceText = NSMutableAttributedString(string: serviceStr)
+        serviceText.font(12)
+        serviceText.color(.hexStringColor(hexString: "#2F79FF"))
+        serviceText.yy_setTextHighlight(serviceHL, range: NSRange(location: 0, length: serviceStr.count))
+        serviceHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.serviceAction()
+        }
+        
+        attr.append(serviceText)
+
+        label.attributedText = attr
+        
+        return label
+    }()
+    
+    lazy var loginBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.addRadius(radius: 22.rpx)
+   
+        if let image = UIImage.gradient([.hexStringColor(hexString: "#15CBA1", alpha: 0.2), .hexStringColor(hexString: "#1FE0BA", alpha: 0.2)], size: CGSize(width: 280.rpx, height: 44.rpx), locations: [0, 1], direction: .horizontal) {
+            btn.setBackgroundColor(UIColor(patternImage: image), forState: .disabled)
+        }
+        
+        if let image = UIImage.gradient([.hexStringColor(hexString: "#15CBA1"), .hexStringColor(hexString: "#1FE0BA")], size: CGSize(width: 280.rpx, height: 44.rpx), locations: [0, 1], direction: .horizontal) {
+            btn.setBackgroundColor(UIColor(patternImage: image), forState: .normal)
+        }
+        
+        btn.title("登录")
+        btn.textColor(.white)
+        btn.mediumFont(16)
+        
+        btn.addTarget(self, action: #selector(loginButtonAction), for: .touchUpInside)
+        
+        return btn
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        initializeView()
+        
+        if let type = self.type {
+            switch type {
+            case .mine:
+                gravityInstance?.track(QSLGravityConst.login_show, properties: ["id": 1001])
+            case .add:
+                gravityInstance?.track(QSLGravityConst.login_show, properties: ["id": 1002])
+            case .road:
+                gravityInstance?.track(QSLGravityConst.login_show, properties: ["id": 1003])
+            case .contact:
+                gravityInstance?.track(QSLGravityConst.login_show, properties: ["id": 1004])
+            }
+        }
+    }
+}
+
+extension QSLLoginViewController {
+    
+    @objc func privacyAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppPrivacyAgreementLink
+        vc.title = "隐私政策"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+    
+    @objc func serviceAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppServiceAgreementLink
+        vc.title = "服务协议"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+    
+    @objc func selectButtonAction() {
+        
+        gravityInstance?.track(QSLGravityConst.login_agree)
+        self.selectBtn.isSelected = !self.selectBtn.isSelected
+    }
+    
+    // 验证码
+    @objc func codeButtonAction() {
+                
+        gravityInstance?.track(QSLGravityConst.login_code)
+        if let phone = self.phoneTextField.text, phone.count == 11 {
+            
+            codeButton.countDown(60)
+            QSLNetwork().request(.userCode(dict: ["phone": phone]), success: { response in
+                
+            }, fail: { code, error in
+                
+                gravityInstance?.track(QSLGravityConst.login_code_fail, properties: ["id": 1001])
+                self.view.toast(text: "发送失败")
+            })
+        } else {
+            
+            gravityInstance?.track(QSLGravityConst.login_code_fail, properties: ["id": 1002])
+            self.view.toast(text: "请输入正确的手机号码")
+        }
+    }
+    
+    // 登录按钮
+    @objc func loginButtonAction() {
+        
+        gravityInstance?.track(QSLGravityConst.login_click)
+        
+        guard let phone = self.phoneTextField.text, phone.count == 11 else {
+            self.view.toast(text: "请输入正确的手机号码")
+            return
+        }
+        
+        guard let code = self.codeTextField.text else {
+            self.view.toast(text: "请输入验证码")
+            return
+        }
+        
+        if !self.selectBtn.isSelected {
+            gravityInstance?.track(QSLGravityConst.login_fail, properties: ["id": 1001])
+            self.view.toast(text: "请先阅读并同意《隐私权政策》和《用户协议》")
+            return
+        }
+        
+        QSLLoading.show()
+        QSLNetwork().request(.userLogin(dict: ["phone": phone, "code": code]), success: { response in
+            
+            QSLLoading.hide()
+            let authToken = response.toJSON(modelKey: "data>authToken").stringValue
+            QSLBaseManager.shared.loginUpdateUser(authToken: authToken, phone: phone)
+            UIApplication.keyWindow?.toast(text: "登录成功")
+            
+            gravityInstance?.track(QSLGravityConst.login_success)
+            // 发送通知
+            NotificationCenter.default.post(name: QSLNotification.QSLLogin, object: nil)
+            
+            // 返回
+            self.backBtnAction()
+            
+        }, fail: { code, error in
+            
+            if code == 1005 {
+                gravityInstance?.track(QSLGravityConst.login_fail, properties: ["id": 1002])
+            } else {
+                gravityInstance?.track(QSLGravityConst.login_fail, properties: ["id": 1003])
+            }
+            
+            QSLLoading.hide()
+            self.view.toast(text: error)
+        })
+    }
+}
+
+extension QSLLoginViewController: UITextFieldDelegate {
+    
+}
+
+extension QSLLoginViewController {
+    
+    func initializeView() {
+        
+        self.view.backgroundColor = .white
+        
+        self.view.addSubview(loginBg)
+        loginBg.snp.makeConstraints { make in
+            make.left.top.right.equalToSuperview()
+        }
+        
+        self.view.addSubview(backButton)
+        backButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 65.rpx, height: 25.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH)
+        }
+        
+        self.view.addSubview(logoImageView)
+        logoImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 70.rpx, height: 70.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(backButton.snp.bottom).offset(30.rpx)
+        }
+        
+        self.view.addSubview(phoneView)
+        phoneView.addSubview(phonePreLabel)
+        phoneView.addSubview(phoneLineView)
+        phoneView.addSubview(phoneTextField)
+        
+        phoneView.snp.makeConstraints { make in
+            make.left.equalTo(24.rpx)
+            make.right.equalTo(-24.rpx)
+            make.height.equalTo(50.rpx)
+            make.top.equalTo(logoImageView.snp.bottom).offset(40.rpx)
+        }
+        
+        phonePreLabel.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.centerY.equalToSuperview()
+        }
+        
+        phoneLineView.snp.makeConstraints { make in
+            make.top.equalTo(15.rpx)
+            make.bottom.equalTo(-15.rpx)
+            make.left.equalTo(49.rpx)
+            make.width.equalTo(1.rpx)
+        }
+        
+        phoneTextField.snp.makeConstraints { make in
+            make.left.equalTo(phoneLineView.snp.right).offset(25.rpx)
+            make.right.equalTo(-12.rpx)
+            make.top.bottom.equalTo(0)
+        }
+        
+        self.view.addSubview(codeView)
+        codeView.addSubview(codeTextField)
+        codeView.addSubview(codeButton)
+        
+        codeView.snp.makeConstraints { make in
+            make.left.equalTo(24.rpx)
+            make.right.equalTo(-24.rpx)
+            make.height.equalTo(50.rpx)
+            make.top.equalTo(phoneView.snp.bottom).offset(12.rpx)
+        }
+        
+        codeButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 90.rpx, height: 32.rpx))
+            make.right.equalTo(-12.rpx)
+            make.centerY.equalToSuperview()
+        }
+        
+        codeTextField.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.top.bottom.equalTo(0)
+            make.right.equalTo(codeButton.snp.left).offset(-12.rpx)
+        }
+        
+        self.view.addSubview(selectBtn)
+        self.view.addSubview(serviceLabel)
+        
+        selectBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 12.rpx, height: 12.rpx))
+            make.left.equalTo(25.rpx)
+            make.top.equalTo(codeView.snp.bottom).offset(14.rpx)
+        }
+        
+        serviceLabel.snp.makeConstraints { make in
+            make.centerY.equalTo(selectBtn.snp.centerY)
+            make.left.equalTo(selectBtn.snp.right).offset(4.rpx)
+        }
+        
+        self.view.addSubview(loginBtn)
+        loginBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 280.rpx, height: 44.rpx))
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(-QSLConst.qsl_kTabbarBottom - 95.rpx)
+        }
+    }
+}

+ 191 - 0
QuickSearchLocation/Classes/Pages/QSLMessage/Cell/QSLMessageTableViewCell.swift

@@ -0,0 +1,191 @@
+//
+//  QSLMessageTableViewCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/27.
+//
+
+import UIKit
+
+enum QSLMessageType: Int {
+    case sent      = 1  // 你的好友请求已经发送
+    case accepted  = 2  // 你的好友请求已经被接受
+    case refused   = 3  // 你的好友请求已经被拒绝
+    case resort    = 4  // 好友发来的求救
+    case delete    = 5  // 你的好友删除了你
+}
+
+protocol QSLMessageTableViewCellDelegate: NSObjectProtocol {
+    
+    func contactBtnAction(phone: String)
+}
+
+class QSLMessageTableViewCell: UITableViewCell {
+    
+    weak var delegate: QSLMessageTableViewCellDelegate?
+    
+    var model: QSLMessageModel?
+    
+    lazy var avatarImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_other_avatar")
+        return imageView
+    }()
+    
+    lazy var nameTitleLabel: UILabel = {
+      
+        let label = UILabel()
+        label.text("儿子")
+        label.mediumFont(15)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        return label
+    }()
+    
+    lazy var timeLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("2023-10-20  09:43")
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        label.font(13)
+        return label
+    }()
+    
+    lazy var contentLabel: UILabel = {
+       
+        let label = UILabel()
+        label.numberOfLines = 2
+        label.text("已拒绝 您的好友申请")
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        label.font(13)
+        label.setSpecificTextColor("已拒绝", color: .hexStringColor(hexString: "#FF5146"))
+        return label
+    }()
+    
+    lazy var lineView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .hexStringColor(hexString: "#F0F0F0", alpha: 0.2)
+        return view
+    }()
+    
+    lazy var resortView: UIView = {
+        
+        let view = UIView()
+        view.isHidden = true
+        view.gradientBackgroundColor(color1: .hexStringColor(hexString: "#FFDFDF", alpha: 0), color2:  .hexStringColor(hexString: "#FFCDCD"), width: QSLConst.qsl_kScreenW, height: 88.rpx, direction: .horizontal)
+        return view
+    }()
+    
+    lazy var contactBtn: UIButton = {
+        
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "message_contact_btn"), for: .normal)
+        btn.addTarget(self, action: #selector(contactBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        initializeView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func contactBtnAction() {
+        
+        if let model = self.model {
+            delegate?.contactBtnAction(phone: model.senderPhone)
+        }
+    }
+    
+    func config(model: QSLMessageModel) {
+        
+        self.model = model
+        
+        self.nameTitleLabel.text = model.senderPhone
+        
+        self.timeLabel.text = Date.timestampToFormatterTimeString(timestamp: model.createTime)
+        
+        switch QSLMessageType(rawValue: model.type) {
+        case .sent:
+            self.contentLabel.text = "已发出好友申请"
+            self.resortView.isHidden = true
+            break
+        case .accepted:
+            self.contentLabel.text = "已同意 您的好友申请"
+            self.contentLabel.setSpecificTextColor("已同意", color: .hexStringColor(hexString: "#15CBA1"))
+            self.resortView.isHidden = true
+            break
+        case .refused:
+            self.contentLabel.text = "已拒绝 您的好友申请"
+            self.contentLabel.setSpecificTextColor("已拒绝", color: .hexStringColor(hexString: "#FF5146"))
+            self.resortView.isHidden = true
+            break
+        case .resort:
+            self.contentLabel.text = "您的好友需要紧急求助,请您尽快联系他!"
+            self.resortView.isHidden = false
+            break
+        case .delete:
+            self.contentLabel.text = "已删除您的好友"
+            self.resortView.isHidden = true
+            break
+        default:
+            break
+        }
+    }
+}
+
+extension QSLMessageTableViewCell {
+    
+    func initializeView() {
+        
+        self.contentView.addSubview(resortView)
+        resortView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        self.contentView.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 48.rpx, height: 48.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(12.rpx)
+        }
+        
+        self.contentView.addSubview(nameTitleLabel)
+        nameTitleLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.bottom.equalTo(avatarImageView.snp.centerY)
+        }
+        
+        self.contentView.addSubview(timeLabel)
+        timeLabel.snp.makeConstraints { make in
+            make.right.equalTo(-12.rpx)
+            make.centerY.equalTo(nameTitleLabel.snp.centerY)
+        }
+        
+        self.contentView.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.right.equalTo(-129.rpx)
+            make.top.equalTo(nameTitleLabel.snp.bottom).offset(5.rpx)
+        }
+        
+        self.contentView.addSubview(lineView)
+        lineView.snp.makeConstraints { make in
+            make.left.equalTo(68.rpx)
+            make.right.equalTo(-20.rpx)
+            make.bottom.equalTo(0)
+            make.height.equalTo(1.rpx)
+        }
+        
+        self.resortView.addSubview(contactBtn)
+        contactBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 92.rpx, height: 32.rpx))
+            make.right.equalTo(-12.rpx)
+            make.bottom.equalTo(-12.rpx)
+        }
+    }
+}

+ 260 - 0
QuickSearchLocation/Classes/Pages/QSLMessage/Controller/QSLMessageController.swift

@@ -0,0 +1,260 @@
+//
+//  QSLMessageController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+import CRRefresh
+
+class QSLMessageController: QSLBaseController {
+    
+    lazy var topBgView: UIView = {
+       
+        let view = UIView()
+        view.gradientBackgroundColor(color1: .hexStringColor(hexString: "#EEFFF5"), color2: .hexStringColor(hexString: "#EEFFF5", alpha: 0), width: QSLConst.qsl_kScreenW, height: 52.rpx + QSLConst.qsl_kStatusBarFrameH, direction: .vertical)
+        return view
+    }()
+    
+    lazy var titleLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("消息中心")
+        label.boldFont(18)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var allMessBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.image(UIImage(named: "message_all_btn"))
+        btn.addTarget(self, action: #selector(allMessBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var messageTableView: UITableView = {
+        
+        let tableView = UITableView(frame: .zero, style: .grouped)
+        tableView.backgroundColor = .clear
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.tableViewNeverAdjustContentInset()
+        tableView.bounces = true
+        tableView.isUserInteractionEnabled = true
+        tableView.contentInsetAdjustmentBehavior = .never
+        tableView.register(cellClass: QSLMessageTableViewCell.self)
+        
+        tableView.cr.addHeadRefresh(animator: NormalHeaderAnimator()) { [weak self] in
+            self?.requestAllList()
+        }
+        
+        return tableView
+    }()
+    
+    // 信息列表
+    var messageList: [QSLMessageModel] = [QSLMessageModel]()
+    
+    var requestModel: QSLRequestModel?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        initializeView()
+        
+        messageTableView.cr.beginHeaderRefresh()
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(requestAllList), name: QSLNotification.QSLLogin, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(requestAllList), name: QSLNotification.QSLLogout, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(requestAllList), name: QSLNotification.QSLRefreshRequest, object: nil)
+    }
+    
+    // 点击新的好友按钮
+    @objc func allMessBtnAction() {
+        
+        let vc = QSLRequestController()
+        vc.title = "新的好友"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+extension QSLMessageController {
+    
+    // 请求全部
+    @objc func requestAllList() {
+        
+        requestRequestList {
+            self.requestMessage()
+        }
+    }
+    
+    // 请求好友请求列表
+    func requestRequestList(complete: @escaping () -> ()) {
+        
+        self.requestModel = nil
+        QSLNetwork().request(.requestList(dict: [:])) { response in
+            
+            let requestList = response.mapArray(QSLRequestModel.self, modelKey: "data>list")
+            if requestList.count > 0 {
+                self.requestModel = requestList.first
+            }
+            complete()
+        } fail: { code, error in
+            complete()
+        }
+    }
+    
+    // 请求消息列表
+    func requestMessage() {
+        
+        self.messageList.removeAll()
+        QSLNetwork().request(.messageList(dict: [:])) { response in
+            
+            self.messageTableView.cr.endHeaderRefresh()
+            let messageList = response.mapArray(QSLMessageModel.self, modelKey: "data>list")
+            self.messageList = messageList
+            
+            for message in self.messageList {
+                if message.type == 2 {
+                    gravityInstance?.track(QSLGravityConst.message_disagree_show)
+                } else if message.type == 3 {
+                    gravityInstance?.track(QSLGravityConst.message_agree_show)
+                } else if message.type == 4 {
+                    gravityInstance?.track(QSLGravityConst.message_resort_show)
+                }
+            }
+            self.messageTableView.reloadData()
+        } fail: { code, error in
+            self.messageTableView.cr.endHeaderRefresh()
+        }
+    }
+}
+
+extension QSLMessageController: QSLMessageHeaderViewDelegate, QSLMessageTableViewCellDelegate {
+    
+    // 电话联系
+    func contactBtnAction(phone: String) {
+        
+        gravityInstance?.track(QSLGravityConst.message_resort_click)
+        // 创建电话拨号的URL
+        if let url = URL(string: "tel://\(phone)"), UIApplication.shared.canOpenURL(url) {
+            // 打开URL
+            UIApplication.shared.open(url, options: [:], completionHandler: nil)
+        }
+    }
+    
+    // 拒绝点击
+    func refuseBtnAction(model: QSLRequestModel) {
+        
+        QSLNetwork().request(.requestRefuse(dict: ["id": model.requestId])) { response in
+            
+            // 发送通知
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshRequest, object: nil)
+        } fail: { code, error in
+            self.view.toast(text: error)
+        }
+    }
+    
+    // 同意点击
+    func accpetBtnAction(model: QSLRequestModel) {
+        
+        QSLAlertView.alert(view: self.view, title: "添加好友", content: "1.您同意添加该用户为好友,则视为您同意本应用合法收集储存和使用信息;\n2.并同意将您的位置、轨迹等信息分享给该好友。", isOneBtn: true, oneBtnText: "确认添加", oneBtnClosure:  {
+            QSLNetwork().request(.requestAccept(dict: ["id": model.requestId])) { response in
+                
+                // 发送通知
+                NotificationCenter.default.post(name: QSLNotification.QSLRefreshRequest, object: nil)
+            } fail: { code, error in
+                self.view.toast(text: error)
+            }
+        })
+    }
+    
+    func jumpToRequest() {
+        allMessBtnAction()
+    }
+}
+
+// MARK: - 设置Tableview
+extension QSLMessageController: UITableViewDelegate, UITableViewDataSource {
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return self.messageList.count
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    
+        let cell = tableView.dequeueReusableCell(cellType: QSLMessageTableViewCell.self, cellForRowAt: indexPath)
+        cell.selectionStyle = .none
+        let model = self.messageList[indexPath.row]
+        cell.config(model: model)
+        cell.delegate = self
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        let model = self.messageList[indexPath.row]
+        if QSLMessageType(rawValue: model.type) == .resort {
+            return 88.rpx
+        }
+        return 72.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        if let _ = self.requestModel {
+            return 164.rpx
+        }
+        return 0.001
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 0.0001
+    }
+    
+    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+        if var requestModel = self.requestModel {
+            let view = QSLMessageHeaderView()
+            view.delegate = self
+            view.config(model: requestModel)
+            return view
+        }
+        return nil
+    }
+    
+}
+
+extension QSLMessageController {
+    
+    func initializeView() {
+        
+        self.view.backgroundColor = .white
+        
+        self.view.addSubview(topBgView)
+        topBgView.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+            make.height.equalTo(52.rpx + QSLConst.qsl_kStatusBarFrameH)
+        }
+        
+        self.view.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.left.equalTo(20.rpx)
+            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH + 16.rpx)
+        }
+        
+        self.view.addSubview(allMessBtn)
+        allMessBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 26.rpx, height: 26.rpx))
+            make.right.equalTo(-20.rpx)
+            make.centerY.equalTo(titleLabel.snp.centerY)
+        }
+        
+        self.view.addSubview(messageTableView)
+        messageTableView.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.top.equalTo(topBgView.snp.bottom)
+            make.bottom.equalTo(-QSLConst.qsl_kTabbarFrameH)
+        }
+    }
+}

+ 179 - 0
QuickSearchLocation/Classes/Pages/QSLMessage/QSLRequest/QSLRequestCell.swift

@@ -0,0 +1,179 @@
+//
+//  QSLRequestCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/5.
+//
+
+import UIKit
+
+enum QSLRequestType: Int {
+    case wait     = 1
+    case agreed   = 2
+    case refused  = 3
+}
+
+protocol QSLRequestCellDelegate: NSObjectProtocol {
+    
+    func refuseBtnAction(model: QSLRequestModel)
+    
+    func accpetBtnAction(model: QSLRequestModel)
+}
+
+class QSLRequestCell: UITableViewCell {
+    
+    weak var delegate: QSLRequestCellDelegate?
+    
+    var model: QSLRequestModel?
+    
+    lazy var avatarImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_other_avatar")
+        return imageView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("用户1388888888向您发出了好友申请")
+        label.mediumFont(15)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        label.setSpecificTextColor("1388888888", color: .hexStringColor(hexString: "#15CBA1"))
+        return label
+    }()
+    
+    lazy var timeLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("2023-10-20  09:43")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var refuseBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.backgroundColor = QSLColor.backGroundColor
+        btn.title("拒绝")
+        btn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+        btn.addRadius(radius: 16.rpx)
+        btn.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#E2E2E2"))
+        btn.addTarget(self, action: #selector(refuseBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var agreeBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#15CBA1"), width: 120.rpx, height: 32.rpx, direction: .horizontal)
+        btn.title("同意")
+        btn.addRadius(radius: 16.rpx)
+        btn.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#E2E2E2"))
+        btn.addTarget(self, action: #selector(agreeBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func refuseBtnAction() {
+        
+        if let model = self.model {
+            delegate?.refuseBtnAction(model: model)
+        }
+    }
+    
+    @objc func agreeBtnAction() {
+        
+        if let model = self.model {
+            delegate?.accpetBtnAction(model: model)
+        }
+    }
+    
+    func config(model: QSLRequestModel) {
+        
+        self.model = model
+        self.titleLabel.text = "用户\(model.userPhone)向您发出了好友申请"
+        self.titleLabel.setSpecificTextColor(model.userPhone, color: .hexStringColor(hexString: "#15CBA1"))
+        self.timeLabel.text = Date.timestampToFormatterTimeString(timestamp: model.createTime)
+        
+        switch QSLRequestType(rawValue: model.status) {
+        case .wait:
+            self.agreeBtn.isHidden = false
+            self.refuseBtn.isEnabled = true
+            self.refuseBtn.title("拒绝")
+            self.refuseBtn.backgroundColor = QSLColor.backGroundColor
+            self.refuseBtn.layer.borderColor = UIColor.hexStringColor(hexString: "#E2E2E2").cgColor
+            self.refuseBtn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+            break
+        case .agreed:
+            self.agreeBtn.isHidden = true
+            self.refuseBtn.isEnabled = false
+            self.refuseBtn.backgroundColor = .white
+            self.refuseBtn.layer.borderColor = QSLColor.themeMainColor.cgColor
+            self.refuseBtn.title("已同意")
+            self.refuseBtn.textColor(QSLColor.themeMainColor)
+            break
+        case .refused:
+            self.agreeBtn.isHidden = true
+            self.refuseBtn.isEnabled = false
+            self.refuseBtn.title("已拒绝")
+            self.refuseBtn.backgroundColor = QSLColor.backGroundColor
+            self.refuseBtn.layer.borderColor = UIColor.hexStringColor(hexString: "#E2E2E2").cgColor
+            self.refuseBtn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+            break
+        default:
+            break
+        }
+    }
+}
+
+extension QSLRequestCell {
+    
+    func initView() {
+        
+        contentView.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 48.rpx, height: 48.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(20.rpx)
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.right.equalTo(-12.rpx)
+            make.bottom.equalTo(avatarImageView.snp.centerY)
+        }
+        
+        contentView.addSubview(timeLabel)
+        timeLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.right.equalTo(-12.rpx)
+            make.top.equalTo(titleLabel.snp.bottom).offset(3.rpx)
+        }
+        
+        contentView.addSubview(refuseBtn)
+        refuseBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 120.rpx, height: 32.rpx))
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.top.equalTo(timeLabel.snp.bottom).offset(16.rpx)
+        }
+        
+        contentView.addSubview(agreeBtn)
+        agreeBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 120.rpx, height: 32.rpx))
+            make.left.equalTo(refuseBtn.snp.right).offset(12.rpx)
+            make.top.equalTo(timeLabel.snp.bottom).offset(16.rpx)
+        }
+    }
+}

+ 158 - 0
QuickSearchLocation/Classes/Pages/QSLMessage/QSLRequest/QSLRequestController.swift

@@ -0,0 +1,158 @@
+//
+//  QSLRequestController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/5.
+//
+
+import UIKit
+import CRRefresh
+
+class QSLRequestController: QSLBaseController {
+    
+    lazy var requestTableView: UITableView = {
+        
+        let tableView = UITableView(frame: .zero, style: .grouped)
+        tableView.backgroundColor = .clear
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.tableViewNeverAdjustContentInset()
+        tableView.bounces = true
+        tableView.isUserInteractionEnabled = true
+        tableView.isScrollEnabled = false
+        tableView.contentInsetAdjustmentBehavior = .never
+        tableView.register(cellClass: QSLRequestCell.self)
+        
+        tableView.cr.addHeadRefresh(animator: NormalHeaderAnimator()) { [weak self] in
+            self?.requestRequestList()
+        }
+        
+        return tableView
+    }()
+    
+    var requestList: [QSLRequestModel] = [QSLRequestModel]()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        initializeView()
+        
+        requestTableView.cr.beginHeaderRefresh()
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(requestRequestList), name: QSLNotification.QSLRefreshRequest, object: nil)
+        
+        gravityInstance?.track(QSLGravityConst.message_request_show)
+    }
+    
+    override func viewWillAppear(_ animated:Bool) {
+        super.viewWillAppear(animated)
+        self.navigationController?.setNavigationBarHidden(false, animated: animated)
+    }
+    
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        self.navigationController?.setNavigationBarHidden(true, animated: animated)
+    }
+}
+
+extension QSLRequestController {
+    
+    // 请求好友请求列表
+    @objc func requestRequestList() {
+        
+        self.requestList.removeAll()
+        QSLNetwork().request(.requestList(dict: [:])) { response in
+            
+            self.requestTableView.cr.endHeaderRefresh()
+            let requestList = response.mapArray(QSLRequestModel.self, modelKey: "data>list")
+            self.requestList = requestList
+            self.requestTableView.reloadData()
+        } fail: { code, error in
+            
+            self.requestTableView.cr.endHeaderRefresh()
+            self.requestTableView.reloadData()
+        }
+    }
+}
+
+extension QSLRequestController: QSLRequestCellDelegate {
+    
+    // 拒绝点击
+    func refuseBtnAction(model: QSLRequestModel) {
+        
+        gravityInstance?.track(QSLGravityConst.message_request_disagree)
+        QSLNetwork().request(.requestRefuse(dict: ["id": model.requestId])) { response in
+            
+            // 发送通知
+            NotificationCenter.default.post(name: QSLNotification.QSLRefreshRequest, object: nil)
+        } fail: { code, error in
+            self.view.toast(text: error)
+        }
+    }
+    
+    // 同意点击
+    func accpetBtnAction(model: QSLRequestModel) {
+        
+        gravityInstance?.track(QSLGravityConst.message_request_agree)
+        gravityInstance?.track(QSLGravityConst.message_request_agree_show)
+        QSLAlertView.alert(view: self.view, title: "添加好友", content: "1.您同意添加该用户为好友,则视为您同意本应用合法收集储存和使用信息;\n2.并同意将您的位置、轨迹等信息分享给该好友。", isOneBtn: true, oneBtnText: "确认添加", oneBtnClosure:  {
+            gravityInstance?.track(QSLGravityConst.message_request_agree_show_confirm)
+            QSLNetwork().request(.requestAccept(dict: ["id": model.requestId])) { response in
+                
+                // 发送通知
+                NotificationCenter.default.post(name: QSLNotification.QSLRefreshRequest, object: nil)
+            } fail: { code, error in
+                self.view.toast(text: error)
+            }
+        }, closeBtnClosure: {
+            
+            gravityInstance?.track(QSLGravityConst.message_request_agree_show_cancel)
+        })
+    }
+}
+
+// MARK: - 设置Tableview
+extension QSLRequestController: UITableViewDelegate, UITableViewDataSource {
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return self.requestList.count
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    
+        let cell = tableView.dequeueReusableCell(cellType: QSLRequestCell.self, cellForRowAt: indexPath)
+        cell.selectionStyle = .none
+        let model = self.requestList[indexPath.row]
+        cell.config(model: model)
+        cell.delegate = self
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return 117.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 1.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 0.0001
+    }
+    
+}
+
+
+extension QSLRequestController {
+    
+    func initializeView() {
+        
+        self.view.addSubview(requestTableView)
+        requestTableView.snp.makeConstraints { make in
+            make.left.right.bottom.equalTo(0)
+            make.top.equalTo(0)
+        }
+    }
+}

+ 203 - 0
QuickSearchLocation/Classes/Pages/QSLMessage/View/QSLMessageHeaderView.swift

@@ -0,0 +1,203 @@
+//
+//  QSLMessageHeaderView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/5.
+//
+
+import UIKit
+
+protocol QSLMessageHeaderViewDelegate: NSObjectProtocol {
+    
+    func refuseBtnAction(model: QSLRequestModel)
+    
+    func accpetBtnAction(model: QSLRequestModel)
+    
+    func jumpToRequest()
+}
+
+class QSLMessageHeaderView: UIView {
+    
+    weak var delegate: QSLMessageHeaderViewDelegate?
+    
+    var model: QSLRequestModel?
+    
+    lazy var contentView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .white
+        return view
+    }()
+    
+    lazy var avatarImageView: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_other_avatar")
+        return imageView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("用户1388888888向您发出了好友申请")
+        label.mediumFont(15)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        label.setSpecificTextColor("1388888888", color: .hexStringColor(hexString: "#15CBA1"))
+        return label
+    }()
+    
+    lazy var timeLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("2023-10-20  09:43")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var refuseBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.backgroundColor = QSLColor.backGroundColor
+        btn.title("拒绝")
+        btn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+        btn.addRadius(radius: 16.rpx)
+        btn.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#E2E2E2"))
+        btn.addTarget(self, action: #selector(refuseBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var agreeBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#15CBA1"), width: 120.rpx, height: 32.rpx, direction: .horizontal)
+        btn.title("同意")
+        btn.addRadius(radius: 16.rpx)
+        btn.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#E2E2E2"))
+        btn.addTarget(self, action: #selector(agreeBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var checkBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.title("查看全部申请")
+        btn.image(UIImage(named: "public_arrow_right_A7"))
+        btn.textColor(.hexStringColor(hexString: "#A7A7A7"))
+        btn.font(13)
+        btn.setImageTitleLayout(.imgRight, spacing: 0)
+        btn.addTarget(self, action: #selector(checkBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    @objc func refuseBtnAction() {
+        
+        if let model = self.model {
+            delegate?.refuseBtnAction(model: model)
+        }
+    }
+    
+    @objc func agreeBtnAction() {
+        
+        if let model = self.model {
+            delegate?.accpetBtnAction(model: model)
+        }
+    }
+    
+    @objc func checkBtnAction() {
+        
+        delegate?.jumpToRequest()
+    }
+    
+    func config(model: QSLRequestModel) {
+        self.model = model
+        self.titleLabel.text = "用户\(model.userPhone)向您发出了好友申请"
+        self.titleLabel.setSpecificTextColor(model.userPhone, color: .hexStringColor(hexString: "#15CBA1"))
+        self.timeLabel.text = Date.timestampToFormatterTimeString(timestamp: model.createTime)
+        
+        switch QSLRequestType(rawValue: model.status) {
+        case .wait:
+            break
+        case .refused:
+            self.agreeBtn.isHidden = true
+            self.refuseBtn.title("已拒绝")
+            break
+        case .agreed:
+            self.agreeBtn.isHidden = true
+            self.refuseBtn.backgroundColor = .white
+            self.refuseBtn.layer.borderColor = QSLColor.themeMainColor.cgColor
+            self.refuseBtn.title("已同意")
+            self.refuseBtn.textColor(QSLColor.themeMainColor)
+            break
+        default:
+            break
+        }
+    }
+}
+
+extension QSLMessageHeaderView {
+    
+    func initView() {
+        
+        self.backgroundColor = QSLColor.backGroundColor
+        
+        self.addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.top.right.left.equalTo(0)
+            make.bottom.equalTo(-8.rpx)
+        }
+        
+        contentView.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 48.rpx, height: 48.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(20.rpx)
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.right.equalTo(-12.rpx)
+            make.bottom.equalTo(avatarImageView.snp.centerY)
+        }
+        
+        contentView.addSubview(timeLabel)
+        timeLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.right.equalTo(-12.rpx)
+            make.top.equalTo(titleLabel.snp.bottom).offset(3.rpx)
+        }
+        
+        contentView.addSubview(refuseBtn)
+        refuseBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 120.rpx, height: 32.rpx))
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.top.equalTo(timeLabel.snp.bottom).offset(16.rpx)
+        }
+        
+        contentView.addSubview(agreeBtn)
+        agreeBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 120.rpx, height: 32.rpx))
+            make.left.equalTo(refuseBtn.snp.right).offset(12.rpx)
+            make.top.equalTo(timeLabel.snp.bottom).offset(16.rpx)
+        }
+        
+        contentView.addSubview(checkBtn)
+        checkBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 100.rpx, height: 20.rpx))
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(-8.rpx)
+        }
+    }
+}
+
+

+ 85 - 0
QuickSearchLocation/Classes/Pages/QSLMine/Cell/QSLMineFuncCollectionViewCell.swift

@@ -0,0 +1,85 @@
+//
+//  QSLMineFuncTableViewCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/16.
+//
+
+import UIKit
+
+class QSLMineFuncTableViewCell: UITableViewCell {
+    
+    var cellDict:[String: String]? {
+        didSet {
+            updateUI()
+        }
+    }
+    
+    lazy var cellIcon: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "")
+        return imageView
+    }()
+    
+    lazy var cellLabel: UILabel = {
+       
+        let label = UILabel()
+        label.font(15)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var arrowIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_func_arrow")
+        return imageView
+    }()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        setUpUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension QSLMineFuncTableViewCell {
+    
+    func updateUI() {
+        
+        if let dict = self.cellDict {
+            
+            let image = UIImage(named: dict["image"] ?? "")
+            self.cellIcon.image = image
+            
+            self.cellLabel.text = dict["title"]
+        }
+    }
+    
+    func setUpUI() {
+        
+        addSubview(cellIcon)
+        addSubview(cellLabel)
+        addSubview(arrowIcon)
+        
+        cellIcon.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.centerY.equalToSuperview()
+            make.size.equalTo(CGSize(width: 24.rpx, height: 24.rpx))
+        }
+        
+        cellLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.left.equalTo(cellIcon.snp.right).offset(8.rpx)
+        }
+        
+        arrowIcon.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.right.equalTo(-12.rpx)
+        }
+    }
+}

+ 237 - 0
QuickSearchLocation/Classes/Pages/QSLMine/Controller/QSLMineController.swift

@@ -0,0 +1,237 @@
+//
+//  QSLMineController.swift
+//  QuickSearchLocation
+//
+//  Created by mac on 2024/4/10.
+//
+
+import UIKit
+
+class QSLMineController: QSLBaseController {
+    
+    var viewModel:QSLMineViewModel = QSLMineViewModel()
+    
+    lazy var mineBgImageView: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_bg")
+        return imageView
+    }()
+    
+    lazy var mineInfoView: QSLMineInfoView = {
+       
+        let view = QSLMineInfoView()
+        view.isUserInteractionEnabled = true
+        
+        let tap = UITapGestureRecognizer(target: self, action: #selector(jumpToLogin))
+        view.addGestureRecognizer(tap)
+        
+        return view
+    }()
+    
+    lazy var mineVipView: QSLMineVipView = {
+       
+        let view = QSLMineVipView()
+        view.delegate = self
+        return view
+    }()
+    
+    lazy var mineFuncView: QSLMineFuncView = {
+       
+        let view = QSLMineFuncView()
+        view.delegate = self
+        view.viewModel = viewModel
+        return view
+    }()
+    
+    override func viewDidLoad() {
+        
+        super.viewDidLoad()
+        setUpUI()
+        
+        updateUI()
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: QSLNotification.QSLLogin, object: nil)
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: QSLNotification.QSLLogout, object: nil)
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: QSLNotification.QSLRefreshMember, object: nil)
+    }
+}
+
+extension QSLMineController {
+    
+    // 清除用户数据
+    func requestClearUser() {
+        
+        QSLNetwork().request(.userClear(dict: [:])) { response in
+            
+            self.view.toast(text: "注销成功")
+            QSLBaseManager.shared.logOut()
+        } fail: { code, error in
+            
+        }
+    }
+}
+
+extension QSLMineController {
+    
+    // 跳转到登录页面
+    @objc func jumpToLogin() {
+        
+        if !QSLBaseManager.shared.isLogin() {
+            
+            gravityInstance?.track(QSLGravityConst.mine_login)
+            QSLJumpManager.shared.pushToLogin(type: .mine)
+        }
+    }
+}
+
+extension QSLMineController: QSLMineVipViewDelegate {
+    
+    // 跳转到vip页面
+    func unlockBtnAction() {
+        
+        gravityInstance?.track(QSLGravityConst.mine_vip)
+        QSLJumpManager.shared.pushToVip(type: .mine)
+    }
+}
+
+extension QSLMineController: QSLMineFuncViewDelegate {
+    
+    func didSelectRowAt(indexPath: IndexPath) {
+        switch indexPath.row {
+        case 0:
+            self.jumpToContact()
+            break
+        case 1:
+            // 权限跳转
+            self.authAction()
+            break
+        case 2:
+            // 关于我们
+            self.jumpToInfo()
+            break
+        case 3:
+            // 注销
+            self.logOffAction()
+            break
+        case 4:
+            // 退出登录
+            self.logoutAction()
+            break
+        case 5: break
+        case 6: break
+        case 7:
+            
+            break
+        default: break
+        }
+    }
+    
+    func jumpToContact() {
+        
+        gravityInstance?.track(QSLGravityConst.mine_contact)
+        QSLJumpManager.shared.pushToContact(type: .mine)
+//        let vc = QSLContactController()
+//        self.navigationController?.pushViewController(vc, animated: true)
+    }
+    
+    func jumpToInfo() {
+        
+        gravityInstance?.track(QSLGravityConst.mine_about)
+        let vc = QSLAppInfoController()
+        vc.title = "关于我们"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+    
+    // MARK: - 权限
+    func authAction() {
+        
+        gravityInstance?.track(QSLGravityConst.mine_auth)
+        if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
+            
+            UIApplication.shared.open(url)
+        }
+    }
+    
+    // MARK: - 注销账号
+    func logOffAction() {
+        
+        if let view = self.tabBarController?.view {
+            QSLAlertView.alert(view: view, title: "温馨提示", content: "确认注销账号吗?", secondBtnClosure:  {
+                self.view.toast(text: "注销成功")
+                self.requestClearUser()
+            })
+        }
+    }
+    
+    // MARK: - 退出登录
+    func logoutAction() {
+        
+        gravityInstance?.track(QSLGravityConst.mine_logout)
+        if let view = self.tabBarController?.view {
+            QSLAlertView.alert(view: view, title: "温馨提示", content: "确认退出登录吗?", firstBtnClosure: {
+                gravityInstance?.track(QSLGravityConst.mine_logout_cancel)
+            }, secondBtnClosure:  {
+                gravityInstance?.track(QSLGravityConst.mine_logout_confirm)
+                self.view.toast(text: "退出登录成功")
+                QSLBaseManager.shared.logOut()
+            })
+        }
+    }
+}
+
+extension QSLMineController {
+    
+    @objc func updateUI() {
+        
+        self.viewModel.initFuncData()
+        
+        mineFuncView.snp.updateConstraints { make in
+            make.height.equalTo(viewModel.funcViewHeight)
+        }
+        
+        self.mineFuncView.funcTableView.reloadData()
+        
+        if QSLBaseManager.shared.isLogin() {
+            
+            let name = "用户\(QSLBaseManager.shared.userModel.phone.suffix(4))"
+            self.mineInfoView.config(name: name, content: "您好,尊敬的用户")
+        } else {
+            self.mineInfoView.config(name: "立即登录", content: "解锁更多精彩内容")
+        }
+        
+        mineVipView.updateUI()
+    }
+    
+    func setUpUI() {
+        
+        view.addSubview(mineBgImageView)
+        mineBgImageView.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+        }
+        
+        view.addSubview(mineInfoView)
+        mineInfoView.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+            make.height.equalTo(122 + QSLConst.qsl_kStatusBarFrameH)
+        }
+        
+        view.addSubview(mineVipView)
+        mineVipView.snp.makeConstraints { make in
+            make.left.equalTo(12)
+            make.right.equalTo(-12)
+            make.top.equalTo(mineInfoView.snp.bottom).offset(10)
+            make.height.equalTo(90)
+        }
+        
+        view.addSubview(mineFuncView)
+        mineFuncView.snp.makeConstraints { make in
+            make.top.equalTo(mineVipView.snp.bottom).offset(12)
+            make.left.equalTo(12)
+            make.right.equalTo(-12)
+            make.height.equalTo(viewModel.funcViewHeight)
+        }
+    }
+}

+ 165 - 0
QuickSearchLocation/Classes/Pages/QSLMine/QSLAppInfo/QSLAppInfoController.swift

@@ -0,0 +1,165 @@
+//
+//  QSLAppInfoController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/9.
+//
+
+import UIKit
+import YYText
+
+class QSLAppInfoController: QSLBaseController {
+    
+    let photoClassifier = PhotoClassifier()
+    
+    lazy var appIcon: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_about_logo")
+        return imageView
+    }()
+    
+    lazy var appTitle: UILabel = {
+      
+        let label = UILabel()
+        label.text("手机关爱定位")
+        label.font(15)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var versionLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("当前版本:\(QSLApi.appVersionName)")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var serviceLabel: YYLabel = {
+       
+        let label = YYLabel()
+        
+        let attr = NSMutableAttributedString()
+        
+        let blankAttr = NSMutableAttributedString(string: " ")
+        blankAttr.font(12)
+        
+        let privacyHL = YYTextHighlight()
+        var privacyStr = "《隐私权政策》"
+        
+        let privacyText = NSMutableAttributedString(string: privacyStr)
+        
+        privacyText.font(12)
+        privacyText.color(.hexStringColor(hexString: "#2F79FF"))
+        privacyText.yy_setTextHighlight(privacyHL, range: NSRange(location: 0, length: privacyStr.count))
+        privacyHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.privacyAction()
+        }
+        attr.append(privacyText)
+        
+        attr.append(blankAttr)
+        
+        let andAttr = NSMutableAttributedString(string: "和")
+        andAttr.font(12)
+        andAttr.color(.hexStringColor(hexString: "#A7A7A7"))
+        attr.append(andAttr)
+        
+        attr.append(blankAttr)
+        
+        let serviceHL = YYTextHighlight()
+        var serviceStr = "《用户协议》"
+
+        let serviceText = NSMutableAttributedString(string: serviceStr)
+        serviceText.font(12)
+        serviceText.color(.hexStringColor(hexString: "#2F79FF"))
+        serviceText.yy_setTextHighlight(serviceHL, range: NSRange(location: 0, length: serviceStr.count))
+        serviceHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.serviceAction()
+        }
+        
+        attr.append(serviceText)
+
+        label.attributedText = attr
+        
+        return label
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        initView()
+
+        let loadingVC = LoadingViewController()
+        present(loadingVC, animated: true)
+//        photoClassifier.classifyPhotos { result in
+//            // 处理截图
+//            print("找到 \(result.screenshots.count) 张截图")
+//            
+//            // 处理地点分组
+//            for (location, assets) in result.locations {
+//                print("\(location): \(assets.count) 张照片")
+//            }
+//            
+//            // 处理人物分组
+//            for (person, assets) in result.people {
+//                print("\(person): \(assets.count) 张照片")
+//            }
+//        }
+    }
+    
+    override func viewWillAppear(_ animated:Bool) {
+//        super.viewWillAppear(animated)
+        self.navigationController?.setNavigationBarHidden(false, animated: animated)
+    }
+    
+    @objc func privacyAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppPrivacyAgreementLink
+        vc.title = "隐私政策"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+    
+    @objc func serviceAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppServiceAgreementLink
+        vc.title = "服务协议"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+extension QSLAppInfoController {
+    
+    func initView() {
+        
+        self.view.backgroundColor = .white
+        
+        self.view.addSubview(appIcon)
+        appIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 72.rpx, height: 72.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(73.rpx)
+        }
+        
+        self.view.addSubview(appTitle)
+        appTitle.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(appIcon.snp.bottom).offset(12.rpx)
+        }
+        
+        self.view.addSubview(versionLabel)
+        versionLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(appTitle.snp.bottom).offset(2.rpx)
+        }
+        
+        self.view.addSubview(serviceLabel)
+        serviceLabel.snp.makeConstraints { make in
+            make.bottom.equalTo(-30.rpx)
+            make.centerX.equalToSuperview()
+        }
+    }
+}

+ 105 - 0
QuickSearchLocation/Classes/Pages/QSLMine/View/QSLMineFuncView.swift

@@ -0,0 +1,105 @@
+//
+//  QSLMineFuncView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/16.
+//
+
+import UIKit
+
+protocol QSLMineFuncViewDelegate: NSObjectProtocol {
+    
+    func didSelectRowAt(indexPath: IndexPath)
+}
+
+class QSLMineFuncView: UIView {
+    
+    struct UX {
+        static let collectionWidth = (QSLConst.qsl_kScreenW - 24.0) / 4
+        static let collectionHeight = 76.0
+    }
+    
+    weak var delegate: QSLMineFuncViewDelegate?
+    
+    var viewModel: QSLMineViewModel?
+    
+    lazy var funcTableView: UITableView = {
+        
+        let tableView = UITableView(frame: .zero, style: .grouped)
+        tableView.backgroundColor = .clear
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.tableViewNeverAdjustContentInset()
+        tableView.bounces = false
+        tableView.isUserInteractionEnabled = true
+        tableView.isScrollEnabled = false
+        tableView.contentInsetAdjustmentBehavior = .never
+        tableView.register(cellClass: QSLMineFuncTableViewCell.self)
+        
+        return tableView
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setUpUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+// MARK: - 设置UI
+extension QSLMineFuncView {
+    
+    func setUpUI() {
+        
+        addSubview(funcTableView)
+        
+        self.addRadius(radius: 6)
+        self.backgroundColor = .white
+        
+        funcTableView.snp.makeConstraints { make in
+            make.left.right.bottom.equalTo(0)
+            make.top.equalTo(0)
+        }
+    }
+}
+
+// MARK: - 设置Tableview
+extension QSLMineFuncView: UITableViewDelegate, UITableViewDataSource {
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        guard let funcLists = viewModel?.funcLists else { return 0 }
+        return funcLists.count
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    
+        let cell = tableView.dequeueReusableCell(cellType: QSLMineFuncTableViewCell.self, cellForRowAt: indexPath)
+        cell.selectionStyle = .none
+        let funcModel = viewModel?.funcLists[indexPath.row]
+        cell.cellDict = funcModel
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        delegate?.didSelectRowAt(indexPath: indexPath)
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return QSLMineViewModel.UX.funcCellHeight
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 14.rpx
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 14.rpx
+    }
+    
+}

+ 79 - 0
QuickSearchLocation/Classes/Pages/QSLMine/View/QSLMineInfoView.swift

@@ -0,0 +1,79 @@
+//
+//  QSLMineInfoView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/15.
+//
+
+import UIKit
+
+class QSLMineInfoView: UIView {
+    
+    lazy var infoAvatarImageView: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_info_avatar")
+        return imageView
+    }()
+    
+    lazy var infoNameLabel: UILabel = {
+        
+        let label = UILabel()
+        label.boldFont(18)
+        label.textColor = QSLColor.Color_202020
+        label.text = "54K55Ye755m75b2V".decode
+        return label
+    }()
+    
+    lazy var infoContentLabel: UILabel = {
+        
+        let label = UILabel()
+        label.font(13)
+        label.textColor = QSLColor.textColor_A7
+        label.text = "解锁更多精彩内容"
+        return label
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func config(name: String, content: String) {
+        self.infoNameLabel.text = name
+        self.infoContentLabel.text = content
+    }
+}
+
+extension QSLMineInfoView {
+    
+    func setUI() {
+        
+        addSubview(infoAvatarImageView)
+        addSubview(infoNameLabel)
+        addSubview(infoContentLabel)
+        
+        infoAvatarImageView.snp.makeConstraints { make in
+            make.left.equalTo(16.rpx)
+            make.bottom.equalTo(-16.rpx)
+            make.size.equalTo(CGSize(width: 56.rpx, height: 56.rpx))
+        }
+        
+        infoNameLabel.snp.makeConstraints { make in
+            make.height.equalTo(26.rpx)
+            make.left.equalTo(infoAvatarImageView.snp.right).offset(12.rpx)
+            make.top.equalTo(infoAvatarImageView.snp.top).offset(7.rpx)
+        }
+        
+        infoContentLabel.snp.makeConstraints { make in
+            make.height.equalTo(17.rpx)
+            make.left.equalTo(infoAvatarImageView.snp.right).offset(12.rpx)
+            make.top.equalTo(infoNameLabel.snp.bottom).offset(2.rpx)
+        }
+    }
+}

+ 149 - 0
QuickSearchLocation/Classes/Pages/QSLMine/View/QSLMineVipView.swift

@@ -0,0 +1,149 @@
+//
+//  QSLMineVipView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/15.
+//
+
+import UIKit
+
+protocol QSLMineVipViewDelegate: NSObjectProtocol {
+    
+    func unlockBtnAction()
+}
+
+class QSLMineVipView: UIView {
+    
+    struct UX {
+        static let buttonWidth = (QSLConst.qsl_kScreenW - 24.0) / 3
+        static let buttonHeight = 32.0
+    }
+    
+    weak var delegate: QSLMineVipViewDelegate?
+    
+    lazy var vipBgImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_vip_bg")
+        return imageView
+    }()
+    
+    lazy var vipTitleIcon: UIImageView = {
+        
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "mine_vip_title")
+        return imageView
+    }()
+    
+    lazy var vipContentLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("升级VIP会员,解锁全部功能")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#FFFFFF", alpha: 0.8)
+        
+        let tap = UITapGestureRecognizer(target: self, action: #selector(contentLabelAction))
+        label.isUserInteractionEnabled = true
+        label.addGestureRecognizer(tap)
+        
+        return label
+    }()
+    
+    lazy var vipUnlockButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.addRadius(radius: 4.rpx)
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#FFE1CD"), color2: .hexStringColor(hexString: "#FEB484"), width: 80.rpx, height: 32.rpx, direction: .horizontal)
+        btn.title("立即开通")
+        btn.textColor(.hexStringColor(hexString: "#9B3800"))
+        btn.boldFont(14)
+        btn.addTarget(self, action: #selector(unlockBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setUpUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+}
+
+extension QSLMineVipView {
+    
+    @objc func unlockBtnAction() {
+        delegate?.unlockBtnAction()
+    }
+    
+    @objc func contentLabelAction() {
+        
+        let model = QSLBaseManager.shared.userModel.memberModel
+        if model.permanent {
+            delegate?.unlockBtnAction()
+        }
+    }
+}
+
+// MARK: - UI相关
+extension QSLMineVipView {
+    
+    func updateUI() {
+        
+        if QSLBaseManager.shared.isLogin() && QSLBaseManager.shared.isVip() {
+            let model = QSLBaseManager.shared.userModel.memberModel
+            if model.permanent {
+                
+                self.vipUnlockButton.isHidden = true
+                self.vipContentLabel.text("您已是尊贵的永久会员 >")
+            } else {
+                
+                self.vipUnlockButton.isHidden = false
+                let level = model.memberLevelString()
+                let endTime = model.endTimestampString()
+                self.vipUnlockButton.title("立即续费")
+                self.vipContentLabel.text("\(level):\(endTime)到期")
+            }
+            
+        } else {
+            
+            self.vipUnlockButton.isHidden = false
+            self.vipUnlockButton.title("立即开通")
+            self.vipContentLabel.text("升级VIP会员,解锁全部功能")
+        }
+    }
+    
+    func setUpUI() {
+        
+        addSubview(vipBgImageView)
+        addSubview(vipContentLabel)
+        addSubview(vipTitleIcon)
+        addSubview(vipUnlockButton)
+        
+        vipBgImageView.snp.makeConstraints { make in
+            make.edges.equalTo(0)
+        }
+        
+        vipTitleIcon.snp.makeConstraints { make in
+            make.left.equalTo(17.rpx)
+            make.bottom.equalTo(self.snp.centerY)
+            make.size.equalTo(CGSize(width: 65.5.rpx, height: 17.rpx))
+        }
+        
+        vipContentLabel.snp.makeConstraints { make in
+            make.left.equalTo(17.rpx)
+            make.top.equalTo(vipTitleIcon.snp.bottom).offset(5.rpx)
+        }
+        
+        vipUnlockButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 80.rpx, height: 32.rpx))
+            make.centerY.equalToSuperview()
+            make.right.equalTo(-20.rpx)
+        }
+        
+    }
+    
+}

+ 59 - 0
QuickSearchLocation/Classes/Pages/QSLMine/ViewModel/QSLMineViewModel.swift

@@ -0,0 +1,59 @@
+//
+//  QSLMineViewModel.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/4/16.
+//
+
+import Foundation
+
+class QSLMineViewModel: NSObject {
+    
+    struct UX {
+        static let funcCellHeight = 44.0.rpx
+    }
+    
+    var funcViewHeight = 72.0.rpx
+    
+    var funcLists = [["image":"mine_func_about","title":"关于我们"],
+                     ["image":"mine_func_logoff","title":"注销账号"],
+                     ["image":"mine_func_logout","title":"退出登录"]]
+    
+    override init() {
+        super.init()
+        
+        initFuncData()
+    }
+}
+
+extension QSLMineViewModel {
+    
+    func initFuncData() {
+   
+        if !QSLBaseManager.shared.isLogin() {
+            // 未登录
+            funcLists = [
+                ["image":"mine_func_emergency","title":"添加紧急联系人"],
+//                ["image":"mine_func_share","title":"分享好友"],
+//                ["image":"mine_func_advice","title":"用户反馈"],
+//                ["image":"mine_func_contact","title":"联系客服"],
+                ["image":"mine_func_auth","title":"权限设置"],
+                ["image":"mine_func_about","title":"关于我们"],
+            ]
+        } else {
+            // 登录
+            funcLists = [
+                ["image":"mine_func_emergency","title":"添加紧急联系人"],
+//                ["image":"mine_func_share","title":"分享好友"],
+//                ["image":"mine_func_advice","title":"用户反馈"],
+//                ["image":"mine_func_contact","title":"联系客服"],
+                ["image":"mine_func_auth","title":"权限设置"],
+                ["image":"mine_func_about","title":"关于我们"],
+                ["image":"mine_func_logoff","title":"注销账号"],
+                ["image":"mine_func_logout","title":"退出登录"]
+            ]
+        }
+        
+        funcViewHeight = 28.0.rpx + Double(funcLists.count) * UX.funcCellHeight
+    }
+}

+ 613 - 0
QuickSearchLocation/Classes/Pages/QSLRoad/Controller/QSLRoadController.swift

@@ -0,0 +1,613 @@
+//
+//  QSLRoadController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/29.
+//
+
+import UIKit
+import MAMapKit
+import AMapFoundationKit
+import AMapLocationKit
+import BRPickerView
+
+enum QSLRoadJumpType: Int {
+    
+    case home
+    case friend
+}
+
+class QSLRoadController: QSLBaseController {
+    
+    var type: QSLRoadJumpType?
+    
+    var userModel: QSLUserModel?
+    
+    // 高德地图
+    lazy var roadMapView = {
+        
+        let _mapView = MAMapView()
+        _mapView.delegate = self
+        
+        _mapView.minZoomLevel = 7;
+        _mapView.maxZoomLevel = 19;
+        _mapView.zoomLevel = 17;
+        
+        _mapView.showsCompass = false
+        _mapView.showsScale = false
+        _mapView.isRotateCameraEnabled = false
+        
+        return _mapView
+    }()
+    
+    /// 高德地图定位
+    lazy var roadMapLocationM = {
+        
+        let _homeMapLocationM = AMapLocationManager()
+        _homeMapLocationM.delegate = self
+        
+        _homeMapLocationM.distanceFilter = 100
+        _homeMapLocationM.locatingWithReGeocode = true
+        
+        _homeMapLocationM.desiredAccuracy = kCLLocationAccuracyHundredMeters
+        _homeMapLocationM.locationTimeout = 2
+        _homeMapLocationM.reGeocodeTimeout = 2
+        
+        _homeMapLocationM.pausesLocationUpdatesAutomatically = false
+        _homeMapLocationM.allowsBackgroundLocationUpdates = true
+        
+        return _homeMapLocationM
+    }()
+    
+    lazy var roadManager: MATraceManager = {
+       
+        let manager = MATraceManager()
+        return manager
+    }()
+    
+    lazy var roadLine: MAPolyline = {
+       
+        let line = MAPolyline()
+        return line
+    }()
+    
+    var beginAnnotation: MAPointAnnotation?
+    
+    var endAnnotation: MAPointAnnotation?
+    
+    var roadList: [QSLMapTrackModel]?
+    
+    lazy var backButton: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "route_back_btn"), for: .normal)
+        btn.addTarget(self, action: #selector(backBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var foldBtn: UIButton = {
+       
+        let btn = UIButton()
+//        btn.isSelected = false
+        btn.touchExtendInset = UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -20)
+        btn.addRadius(radius: 2.rpx)
+        btn.backgroundColor = .hexStringColor(hexString: "#000000", alpha: 0.7)
+        btn.addTarget(self, action: #selector(foldBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var mainView: QSLRoadMainView = {
+       
+        let view = QSLRoadMainView()
+        view.delegate = self
+        return view
+    }()
+    
+    lazy var startDatePicker: BRDatePickerView = {
+        
+        let datePicker = BRDatePickerView()
+        datePicker.pickerMode = .YMDHM
+        datePicker.maxDate = self.nowDate
+        
+        let style = BRPickerStyle()
+        style.topCornerRadius = 10.rpx
+        style.doneTextColor = QSLColor.themeMainColor
+        style.selectRowColor = .hexStringColor(hexString: "#EDFFF9")
+        style.selectRowTextColor = QSLColor.themeMainColor
+        style.clearPickerNewStyle = false
+        
+        datePicker.pickerStyle = style
+        
+        return datePicker
+    }()
+    
+    lazy var endDatePicker: BRDatePickerView = {
+        
+        let datePicker = BRDatePickerView()
+        datePicker.pickerMode = .YMDHM
+        datePicker.maxDate = self.nowDate
+        
+        let style = BRPickerStyle()
+        style.topCornerRadius = 10.rpx
+        style.doneTextColor = QSLColor.themeMainColor
+        style.selectRowColor = .hexStringColor(hexString: "#EDFFF9")
+        style.selectRowTextColor = QSLColor.themeMainColor
+        style.clearPickerNewStyle = false
+        
+        datePicker.pickerStyle = style
+        
+        return datePicker
+    }()
+    
+    // 开始日期
+    var startDate: Date = Date(timeIntervalSinceNow: -60 * 60 * 24)
+    
+    // 结束日期
+    var endDate: Date = Date()
+    
+    // 现在日期
+    var nowDate: Date = Date()
+    
+    init(userModel: QSLUserModel?) {
+        super.init(nibName: nil, bundle: nil)
+        self.userModel = userModel
+    }
+    
+    @MainActor required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        initView()
+        
+        if self.userModel?.remark.count != 0 {
+            self.mainView.titleLabel.text = "\(self.userModel?.remark ?? "")的轨迹"
+        } else {
+            self.mainView.titleLabel.text = "\(self.userModel?.phone ?? "")的轨迹"
+        }
+        
+        self.mainView.startTimeLabel.text = startDate.toformatterTimeString(formatter: "开始时间:yyyy-MM-dd HH:mm")
+        self.mainView.endTimeLabel.text = endDate.toformatterTimeString(formatter: "结束时间:yyyy-MM-dd HH:mm")
+        
+        requestRoad()
+        
+        if let type = self.type {
+            if type == .home {
+                gravityInstance?.track(QSLGravityConst.road_show, properties: ["id": 1001])
+            } else {
+                gravityInstance?.track(QSLGravityConst.road_show, properties: ["id": 1002])
+            }
+        }
+    }
+}
+
+extension QSLRoadController {
+    
+    @objc func foldBtnAction() {
+        
+        if !foldBtn.isSelected {
+            
+            UIView.animate(withDuration: 0.2) {
+                self.mainView.qsl_y = QSLConst.qsl_kScreenH - 61.rpx
+                self.foldBtn.qsl_y = QSLConst.qsl_kScreenH - 61.rpx - 6.rpx - 4.rpx
+            }
+        } else {
+            
+            UIView.animate(withDuration: 0.2) {
+                self.mainView.qsl_y = QSLConst.qsl_kScreenH - 369.rpx
+                self.foldBtn.qsl_y = QSLConst.qsl_kScreenH - 369.rpx - 6.rpx - 4.rpx
+            }
+        }
+        
+        foldBtn.isSelected = !foldBtn.isSelected
+    }
+}
+
+extension QSLRoadController {
+    
+    // 查询轨迹
+    func requestRoad() {
+        
+        self.roadMapView.remove(self.roadLine)
+        self.roadMapView.removeAnnotations(self.roadMapView.annotations)
+        
+        let start = self.startDate.timeIntervalSince1970 * 1000
+        let end = self.endDate.timeIntervalSince1970 * 1000
+        
+        var dict: [String: Any] = [String: Any]()
+        dict["userId"] = self.userModel?.friendId
+        dict["start"] = start
+        dict["end"] = end
+        
+        QSLLoading.show()
+        QSLNetwork().request(.locationTrackQuery(dict:dict)) { resposne in
+            
+            QSLLoading.hide()
+            let list = resposne.mapArray(QSLMapTrackModel.self, modelKey: "data>list")
+            self.roadList = list
+            
+            if list.count == 0 {
+                
+                QSLAlertView.alert(view: self.view, title: "温馨提示", content: "该时段内未查询到历史轨迹记录:\n\n1.可能是该用户未登录本软件;\n2.可能是该用户未运行我们的软件;\n 3.可能是对方未开启定位和网络连接等原因。", isOneBtn: true, oneBtnText: "我知道了")
+            } else {
+                
+                self.drawLine()
+            }
+        } fail: { code, error in
+            
+            QSLLoading.hide()
+            self.view.toast(text: error)
+        }
+    }
+}
+
+// MARK: - 设置地图相关方法
+extension QSLRoadController:MAMapViewDelegate, AMapLocationManagerDelegate {
+    
+    // 初始化高德地图设置
+    func setUpMap() {
+        
+        AMapServices.shared().enableHTTPS = true
+        AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
+        AMapLocationManager.updatePrivacyAgree(.didAgree)
+        
+        roadMapLocationM.startUpdatingLocation()
+    }
+    
+    // 申请地图定位权限
+    func mapViewRequireLocationAuth(_ locationManager: CLLocationManager!) {
+        
+        locationManager.requestWhenInUseAuthorization()
+    }
+    
+    func mapView(_ mapView: MAMapView!, viewFor annotation: (any MAAnnotation)!) -> MAAnnotationView! {
+        
+        if annotation is MAPointAnnotation {
+               
+           let reuseIdentifier = "pointReuseIdentifier"
+           
+           // 尝试从缓存池中重用AnnotationView
+            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) as? QSLHomeAnnotatinView
+           if annotationView == nil {
+               annotationView = QSLHomeAnnotatinView(annotation: annotation, reuseIdentifier: reuseIdentifier)
+               annotationView?.title = annotation.title
+           }
+           
+           // 设置AnnotationView属性
+           annotationView?.isEnabled = false
+           annotationView?.image = UIImage(named: annotation.subtitle ?? "")
+           
+           annotationView?.canShowCallout = false
+           
+           // 设置偏移量
+           annotationView?.centerOffset = CGPoint(x: 0, y: -18)
+           annotationView?.calloutOffset = CGPoint(x: 0, y: -5)
+           
+           // 判断标题是否是“终点”,决定是否选中
+           annotationView?.isSelected = true
+           
+           return annotationView
+       }
+       return nil
+    }
+    
+//    // 查询个人位置
+//    func searchMineLocation() {
+//        
+//        self.roadMapView.remove(self.roadLine)
+//        self.roadMapView.removeAnnotations(self.roadMapView.annotations)
+//        
+//        self.roadMapLocationM.requestLocation(withReGeocode: true) { location, regeocode, error in
+//            
+//            if let error = error as? NSError {
+//               
+//                if error.code == AMapLocationErrorCode.locateFailed.rawValue  {
+//                    return
+//                }
+//            }
+//            
+//            let beginPoint = MAPointAnnotation()
+//            if let location = location {
+//                let beginLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.latitude)
+//                beginPoint.coordinate = beginLocation
+//                beginPoint.title = self.userModel?.remark
+//                beginPoint.subtitle = ""
+//                self.beginAnnotation = beginPoint
+//                
+//                self.roadMapView.addAnnotation(self.beginAnnotation)
+//                self.roadMapView.setCenter(location.coordinate, animated: true)
+//                
+//                print("当前位置:\(location)")
+//            }
+//            
+//            if let regeocode = regeocode {
+//                print("当前地址:\(regeocode)")
+//                self.mainView.startAddressLabel.text = "起点:\(regeocode)"
+//            }
+//        }
+//    }
+//    
+//    // 查询其他用户位置
+//    func searchOthersLocation() {
+//        
+//        self.roadMapView.remove(self.roadLine)
+//        self.roadMapView.removeAnnotations(self.roadMapView.annotations)
+//        
+//
+//        if let model = self.userModel {
+//            
+//            QSLLoading.show()
+//            QSLNetwork().request(.friendGet(dict: ["friendId": model.userId])) { response in
+//                
+//                QSLLoading.hide()
+//                let user = response.mapObject(QSLUserModel.self, modelKey: "data")
+//                var beginPoint = MAPointAnnotation()
+//                var beginLocation = CLLocationCoordinate2D(latitude: model.location.lat, longitude: model.location.lng)
+//                beginPoint.coordinate = beginLocation
+//                beginPoint.title = self.userModel?.remark
+//                beginPoint.subtitle = ""
+//                self.beginAnnotation = beginPoint
+//                
+//                self.roadMapView.addAnnotation(self.beginAnnotation)
+//                
+//                self.roadMapView.setCenter(beginLocation, animated: true)
+//                self.mainView.startAddressLabel.text = "起点:\(model.location.addr)"
+//            } fail: { code, error in
+//                
+//                QSLLoading.hide()
+//                self.view.toast(text: error)
+//            }
+//        }
+//    }
+    
+    //绘制路线
+    func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! {
+        if overlay.isKind(of: MAPolyline.self) {
+            let polylineRender = MAPolylineRenderer.init(overlay: overlay)
+            polylineRender?.lineWidth = 4
+            polylineRender?.strokeColor = QSLColor.themeMainColor
+            polylineRender?.fillColor = QSLColor.themeMainColor
+            polylineRender?.lineJoinType = kMALineJoinRound
+            polylineRender?.lineCapType = kMALineCapRound
+            return polylineRender
+        }
+        return nil
+    }
+    
+    // 绘制轨迹
+    func drawLine() {
+        
+        var MATraceList: [MATraceLocation] = []
+        guard let roadList = self.roadList else { return }
+        for model in roadList {
+            let location = MATraceLocation()
+            location.speed = model.speed
+            location.time = model.ts
+            location.angle = model.bearing
+            
+            var loc = CLLocationCoordinate2D()
+            loc.latitude = model.lat
+            loc.longitude = model.lng
+            location.loc = loc
+            
+            MATraceList.append(location)
+        }
+        
+        self.roadManager.queryProcessedTrace(with: MATraceList, type: .aMap) { index, points in
+            
+        } finishCallback: { points, distance in
+            
+            print("轨迹纠偏success")
+            
+            // 纠偏成功添加纠偏后的折线
+            if let points = points {
+                var commonPolylineCoords = [CLLocationCoordinate2D](repeating: CLLocationCoordinate2D(), count: points.count)
+                for i in 0..<points.count {
+                    commonPolylineCoords[i].latitude = points[i].latitude
+                    commonPolylineCoords[i].longitude = points[i].longitude
+                }
+                
+                // 构造折线对象
+                if let commonPolyline = MAPolyline(coordinates: &commonPolylineCoords, count: UInt(points.count)) {
+                    self.roadLine = commonPolyline
+                    
+                    // 在地图上添加折线对象
+                    self.roadMapView.add(self.roadLine)
+                    
+                    //调用划线后,调用下面方法,设置地图可视矩形的范围
+                    self.roadMapView.setVisibleMapRect(self.roadLine.boundingMapRect, edgePadding: UIEdgeInsets(top: 40, left: 40, bottom: 40, right: 40), animated: true)
+                }
+                
+                // 添加起点图标
+                if let startTrack = points.first {
+                    let startPoint = MAPointAnnotation()
+                    startPoint.coordinate = CLLocationCoordinate2D(latitude: startTrack.latitude, longitude: startTrack.longitude)
+                    startPoint.title = "起点"
+                    startPoint.subtitle = "road_begin_icon"
+                    self.beginAnnotation = startPoint
+                }
+                
+                // 添加终点图标
+                if let endTrack = points.last {
+                    let endPoint = MAPointAnnotation()
+                    endPoint.coordinate = CLLocationCoordinate2D(latitude: endTrack.latitude, longitude: endTrack.longitude)
+                    endPoint.title = "终点"
+                    endPoint.subtitle = "road_end_icon"
+                    self.endAnnotation = endPoint
+                }
+                
+                // 设置地址
+                if let startModel = self.roadList?.first,
+                   let endModel = self.roadList?.last {
+                    
+                    self.mainView.startAddressLabel.text = "起点:\(startModel.addr)"
+                    self.mainView.endAddressLabel.text = "终点:\(endModel.addr)"
+                }
+                
+                // 添加起点和终点图标
+                if let startPoint = self.beginAnnotation,
+                   let endPoint = self.endAnnotation {
+                    self.roadMapView.addAnnotation(startPoint)
+                    self.roadMapView.addAnnotation(endPoint)
+                }
+            }
+        } failedCallback: { code, error in
+            
+            print("轨迹纠偏FailError: \(error ?? "")")
+            
+            // 纠偏失败添加原来的折线
+            if let roadList = self.roadList {
+                
+                var commonPolylineCoords = [CLLocationCoordinate2D](repeating: CLLocationCoordinate2D(), count: roadList.count)
+                
+                for i in 0..<roadList.count {
+                    commonPolylineCoords[i].latitude = roadList[i].lat
+                    commonPolylineCoords[i].longitude = roadList[i].lng
+                }
+                
+                // 构造折线对象
+                if let commonPolyline = MAPolyline(coordinates: &commonPolylineCoords, count: UInt(roadList.count)) {
+                    
+                    self.roadLine = commonPolyline
+                    
+                    // 在地图上添加折线对象
+                    self.roadMapView.add(self.roadLine)
+                    
+                    //调用划线后,调用下面方法,设置地图可视矩形的范围
+                    self.roadMapView.setVisibleMapRect(self.roadLine.boundingMapRect, edgePadding: UIEdgeInsets(top: 40, left: 40, bottom: 40, right: 40), animated: true)
+                }
+                
+                // 添加起点图标
+                if let startTrack = roadList.first {
+                    let startPoint = MAPointAnnotation()
+                    startPoint.coordinate = CLLocationCoordinate2D(latitude: startTrack.lat, longitude: startTrack.lng)
+                    startPoint.title = "起点"
+                    startPoint.subtitle = "road_begin_icon"
+                    self.beginAnnotation = startPoint
+                }
+                
+                // 添加终点图标
+                if let endTrack = roadList.last {
+                    let endPoint = MAPointAnnotation()
+                    endPoint.coordinate = CLLocationCoordinate2D(latitude: endTrack.lat, longitude: endTrack.lng)
+                    endPoint.title = "终点"
+                    endPoint.subtitle = "road_end_icon"
+                    self.endAnnotation = endPoint
+                }
+                
+                // 设置地址
+                if let startTrack = roadList.first, let endTrack = roadList.last {
+                    self.mainView.startAddressLabel.text = "起点:\(startTrack.addr)"
+                    self.mainView.endAddressLabel.text = "终点:\(endTrack.addr)"
+                }
+                
+                // 添加起点和终点注释
+                if let startPoint = self.beginAnnotation,
+                   let endPoint = self.endAnnotation {
+                    self.roadMapView.addAnnotation(startPoint)
+                    self.roadMapView.addAnnotation(endPoint)
+                }
+            }
+        }
+
+    }
+}
+
+extension QSLRoadController: QSLRoadMainViewDelegate {
+    
+    func searchClickAction() {
+        
+        gravityInstance?.track(QSLGravityConst.road_search)
+        self.requestRoad()
+    }
+    
+    // 开始时间点击
+    func startTimeClickAction() {
+        
+        self.startDatePicker.selectDate = self.startDate
+        self.startDatePicker.show()
+        self.startDatePicker.resultBlock = { [weak self] date, value in
+            gravityInstance?.track(QSLGravityConst.road_time_confirm)
+            self?.mainView.startTimeLabel.text = "开始时间:\(value ?? "")"
+            
+            if let date = date {
+                self?.startDate = date
+                // 获取目标时间(加24小时)
+                var nextDay = date.addingTimeInterval(24 * 60 * 60)
+
+                // 获取时间戳
+                let startTime = date.timeIntervalSince1970
+                let nowTime = self?.nowDate.timeIntervalSince1970
+
+                // 判断时间差是否小于24小时
+                if (nowTime ?? 0) - startTime < 24 * 60 * 60 {
+                    nextDay = self?.nowDate ?? Date() // 当前时间
+                }
+                
+                self?.mainView.endTimeLabel.text = nextDay.toformatterTimeString(formatter: "结束时间:yyyy-MM-dd HH:mm")
+                self?.endDate = nextDay
+                self?.endDatePicker.selectDate = self?.endDate
+            }
+        }
+        
+        self.startDatePicker.cancelBlock = {
+            
+            gravityInstance?.track(QSLGravityConst.road_time_cancel)
+        }
+    }
+    
+    // 结束时间点击
+    func endTimeClickAction() {
+        
+        self.endDatePicker.selectDate = self.endDate
+        self.endDatePicker.show()
+        self.endDatePicker.resultBlock = { [weak self] date, value in
+            gravityInstance?.track(QSLGravityConst.road_time_confirm)
+            self?.mainView.endTimeLabel.text = "结束时间:\(value ?? "")"
+            
+            if let date = date {
+                
+                self?.endDate = date
+            }
+        }
+        
+        self.endDatePicker.cancelBlock = {
+            
+            gravityInstance?.track(QSLGravityConst.road_time_cancel)
+        }
+    }
+}
+
+extension QSLRoadController {
+    
+    func initView() {
+        
+        self.view.addSubview(roadMapView)
+        roadMapView.snp.makeConstraints { make in
+            make.top.right.left.equalTo(0)
+            make.edges.equalToSuperview()
+        }
+        
+        self.view.addSubview(mainView)
+        mainView.snp.makeConstraints { make in
+            make.left.right.bottom.equalTo(0)
+            make.height.equalTo(369.rpx)
+        }
+        
+        self.view.addSubview(foldBtn)
+        foldBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 20.rpx, height: 4.rpx))
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(mainView.snp.top).offset(-6.rpx)
+        }
+        
+        self.view.addSubview(backButton)
+        backButton.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 50.rpx, height: 50.rpx))
+            make.left.equalTo(0)
+            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH)
+        }
+    }
+}

+ 311 - 0
QuickSearchLocation/Classes/Pages/QSLRoad/View/QSLRoadMainView.swift

@@ -0,0 +1,311 @@
+//
+//  QSLRoadMainView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/3.
+//
+
+import UIKit
+
+protocol QSLRoadMainViewDelegate: NSObjectProtocol {
+    
+    func startTimeClickAction()
+    
+    func endTimeClickAction()
+    
+    func searchClickAction()
+}
+
+class QSLRoadMainView: UIView {
+    
+    weak var delegate: QSLRoadMainViewDelegate?
+    
+    lazy var contentView: UIView = {
+        
+        let view = UIView(frame: CGRect(x: 0, y: 0, width: QSLConst.qsl_kScreenW, height: 369.rpx))
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addFourCorner(topLeft: 12.rpx, topRight: 12.rpx, bottomLeft: 0, bottomRight: 0)
+        return view
+    }()
+    
+    lazy var avatarImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_other_avatar")
+        return imageView
+    }()
+    
+    lazy var titleLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("儿子的轨迹")
+        label.mediumFont(16)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var timeView: UIView = {
+      
+        let view = UIView()
+        view.backgroundColor = .white
+        view.addRadius(radius: 8.rpx)
+        return view
+    }()
+    
+    lazy var startTimeView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addRadius(radius: 4.rpx)
+        view.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#F2F2F2"))
+        
+        view.isUserInteractionEnabled = true
+        let tap = UITapGestureRecognizer(target: self, action: #selector(startTimeAction))
+        view.addGestureRecognizer(tap)
+        return view
+    }()
+    
+    lazy var startTimeLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("开始时间:2023-07-07 14:33")
+        label.font(14)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var startTimeArrow: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "route_time_arrow")
+        return imageView
+    }()
+    
+    lazy var endTimeView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addRadius(radius: 4.rpx)
+        view.addBorder(borderWidth: 1.rpx, borderColor: .hexStringColor(hexString: "#F2F2F2"))
+        
+        view.isUserInteractionEnabled = true
+        let tap = UITapGestureRecognizer(target: self, action: #selector(endTimeAction))
+        view.addGestureRecognizer(tap)
+        return view
+    }()
+    
+    lazy var endTimeLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("结束时间:2023-07-08 14:33")
+        label.font(14)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var endTimeArrow: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "route_time_arrow")
+        return imageView
+    }()
+    
+    lazy var startAddressPoint: UIView = {
+        
+        let view = UIView()
+        view.addRadius(radius: 3.5.rpx)
+        view.backgroundColor = .hexStringColor(hexString: "#12C172")
+        return view
+    }()
+    
+    lazy var startAddressLabel: UILabel = {
+        
+        let label = UILabel()
+        label.numberOfLines = 0
+        label.text("起点:")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var lineView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .hexStringColor(hexString: "#F5F5F5")
+        return view
+    }()
+    
+    lazy var endAddressPoint: UIView = {
+        
+        let view = UIView()
+        view.addRadius(radius: 3.5.rpx)
+        view.backgroundColor = .hexStringColor(hexString: "#F3353A")
+        return view
+    }()
+    
+    lazy var endAddressLabel: UILabel = {
+        
+        let label = UILabel()
+        label.numberOfLines = 0
+        label.text("终点:")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        return label
+    }()
+    
+    lazy var searchBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.gradientBackgroundColor(color1: .hexStringColor(hexString: "#15CBA1"), color2: .hexStringColor(hexString: "#1FE0BA"), width: 280.rpx, height: 44.rpx, direction: .horizontal)
+        btn.addRadius(radius: 22.rpx)
+        btn.title("查询轨迹")
+        btn.textColor(.white)
+        btn.mediumFont(16.rpx)
+        btn.addTarget(self, action: #selector(searchAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension QSLRoadMainView {
+    
+    @objc func searchAction() {
+        delegate?.searchClickAction()
+    }
+    
+    @objc func startTimeAction() {
+        delegate?.startTimeClickAction()
+    }
+    
+    @objc func endTimeAction() {
+        delegate?.endTimeClickAction()
+    }
+}
+
+extension QSLRoadMainView {
+    
+    func initView() {
+        
+        self.backgroundColor = .clear
+        addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.left.right.bottom.equalTo(0)
+            make.height.equalTo(369.rpx)
+        }
+        
+        contentView.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 32.rpx, height: 32.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(16.rpx)
+        }
+        
+        contentView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.centerY.equalTo(avatarImageView.snp.centerY)
+        }
+        
+        contentView.addSubview(timeView)
+        timeView.snp.makeConstraints { make in
+            make.left.equalTo(8.rpx)
+            make.right.equalTo(-8.rpx)
+            make.top.equalTo(56.rpx)
+            make.bottom.equalTo(-QSLConst.qsl_kTabbarBottom)
+        }
+        
+        timeView.addSubview(startTimeView)
+        startTimeView.snp.makeConstraints { make in
+            make.left.equalTo(20.rpx)
+            make.right.equalTo(-17.rpx)
+            make.top.equalTo(20.rpx)
+            make.height.equalTo(36.rpx)
+        }
+        
+        startTimeView.addSubview(startTimeLabel)
+        startTimeLabel.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.centerY.equalToSuperview()
+        }
+        
+        startTimeView.addSubview(startTimeArrow)
+        startTimeArrow.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 20.rpx, height: 20.rpx))
+            make.right.equalTo(-12.rpx)
+            make.centerY.equalToSuperview()
+        }
+        
+        timeView.addSubview(endTimeView)
+        endTimeView.snp.makeConstraints { make in
+            make.left.equalTo(20.rpx)
+            make.right.equalTo(-17.rpx)
+            make.top.equalTo(startTimeView.snp.bottom).offset(12.rpx)
+            make.height.equalTo(36.rpx)
+        }
+        
+        endTimeView.addSubview(endTimeLabel)
+        endTimeLabel.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.centerY.equalToSuperview()
+        }
+        
+        endTimeView.addSubview(endTimeArrow)
+        endTimeArrow.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 20.rpx, height: 20.rpx))
+            make.right.equalTo(-12.rpx)
+            make.centerY.equalToSuperview()
+        }
+        
+        timeView.addSubview(startAddressPoint)
+        startAddressPoint.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 7.rpx, height: 7.rpx))
+            make.left.equalTo(20.rpx)
+            make.top.equalTo(endTimeView.snp.bottom).offset(32.rpx)
+        }
+        
+        timeView.addSubview(startAddressLabel)
+        startAddressLabel.snp.makeConstraints { make in
+            make.left.equalTo(startAddressPoint.snp.right).offset(10.rpx)
+            make.right.equalTo(-17.rpx)
+            make.centerY.equalTo(startAddressPoint.snp.centerY)
+        }
+        
+        timeView.addSubview(lineView)
+        lineView.snp.makeConstraints { make in
+            make.width.equalTo(1.rpx)
+            make.height.equalTo(27.rpx)
+            make.top.equalTo(startAddressPoint.snp.bottom).offset(8.rpx)
+            make.centerX.equalTo(startAddressPoint.snp.centerX)
+        }
+        
+        timeView.addSubview(endAddressPoint)
+        endAddressPoint.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 7.rpx, height: 7.rpx))
+            make.left.equalTo(20.rpx)
+            make.top.equalTo(lineView.snp.bottom).offset(8.rpx)
+        }
+        
+        timeView.addSubview(endAddressLabel)
+        endAddressLabel.snp.makeConstraints { make in
+            make.left.equalTo(endAddressPoint.snp.right).offset(10.rpx)
+            make.right.equalTo(-17.rpx)
+            make.centerY.equalTo(endAddressPoint.snp.centerY)
+        }
+        
+        timeView.addSubview(searchBtn)
+        searchBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 280.rpx, height: 44.rpx))
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(-24.rpx)
+        }
+    }
+}

+ 71 - 0
QuickSearchLocation/Classes/Pages/QSLVip/Cell/QSLVipCommentCellView.swift

@@ -0,0 +1,71 @@
+//
+//  QSLVipCommentCellView.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/28.
+//
+
+import UIKit
+
+class QSLVipCommentCellView: UIView {
+    
+    lazy var avatarImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "friends_cell_other_avatar")
+        return imageView
+    }()
+    
+    lazy var nameLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("用户198****1259")
+        label.mediumFont(15)
+        label.textColor = QSLColor.Color_202020
+        return label
+    }()
+    
+    lazy var commentLabel: UILabel = {
+        
+        let label = UILabel()
+        label.numberOfLines = 0
+        label.text("上班没时间,远程遛娃,非常方便很好用。")
+        label.font(15)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        return label
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        self.addSubview(avatarImageView)
+        avatarImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 32.rpx, height: 32.rpx))
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(0.rpx)
+        }
+        
+        self.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.centerY.equalTo(avatarImageView.snp.centerY)
+        }
+        
+        self.addSubview(commentLabel)
+        commentLabel.snp.makeConstraints { make in
+            make.left.equalTo(avatarImageView.snp.right).offset(8.rpx)
+            make.right.equalTo(-12.rpx)
+            make.top.equalTo(nameLabel.snp.bottom).offset(8.rpx)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func config(name: String, comment: String) {
+        
+        self.nameLabel.text = name
+        self.commentLabel.text = comment
+    }
+}

+ 173 - 0
QuickSearchLocation/Classes/Pages/QSLVip/Cell/QSLVipGoodCollectionViewCell.swift

@@ -0,0 +1,173 @@
+//
+//  QSLVipGoodCollectionViewCell.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/28.
+//
+
+import UIKit
+
+class QSLVipGoodCollectionViewCell: UICollectionViewCell {
+    
+    var goodModel: QSLGoodModel?
+    
+    lazy var bgView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .white
+        view.addRadius(radius: 8.rpx)
+        view.addBorder(borderWidth: 2.rpx, borderColor: .hexStringColor(hexString: "#EEEEEE"))
+        return view
+    }()
+    
+    lazy var tagIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "vip_goods_tag_icon")
+        return imageView
+    }()
+    
+    lazy var goodNameLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("年度会员")
+        label.boldFont(13)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        return label
+    }()
+    
+    lazy var goodDailyPriceLabel: UILabel = {
+        
+        let label = UILabel()
+        let text = "0.35元/天"
+        label.text(text)
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#F12A1D")
+        if let range = text.range(of: "/") {
+            let nsRange = NSRange(text.startIndex..<range.lowerBound, in: text)
+            label.setRangeFontText(font: .textM(20), range: nsRange)
+        }
+        return label
+    }()
+    
+    lazy var priceLabel: UILabel = {
+       
+        let label = UILabel()
+        label.textAlignment = .center
+        label.addRadius(radius: 12.rpx)
+        label.backgroundColor = .hexStringColor(hexString: "#F6F6F6")
+        label.text("¥128")
+        label.font(12)
+        label.textColor = .hexStringColor(hexString: "#404040")
+        return label
+    }()
+    
+    lazy var originPriceLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("¥299")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#A7A7A7")
+        label.centerLineText(lineValue: 1, underlineColor: .hexStringColor(hexString: "#A7A7A7"))
+        return label
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        initView()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func config(model: QSLGoodModel) {
+        self.goodModel = model
+        
+        if model.isSelect {
+            self.bgView.layer.borderColor = UIColor.hexStringColor(hexString: "#E7B983").cgColor
+            self.bgView.backgroundColor = .hexStringColor(hexString: "#FFF8EF")
+        } else {
+            self.bgView.layer.borderColor = UIColor.hexStringColor(hexString: "#EEEEEE").cgColor
+            self.bgView.backgroundColor = .white
+        }
+        
+        self.tagIcon.isHidden = !model.popular
+        
+        self.goodNameLabel.text = model.name
+        self.goodDailyPriceLabel.text = model.content
+        
+        if let range = model.content.range(of: "/") {
+            let nsRange = NSRange(model.content.startIndex..<range.lowerBound, in: model.content)
+            self.goodDailyPriceLabel.setRangeFontText(font: .textM(20), range: nsRange)
+        }
+
+        var priceText = ""
+        if model.amount.truncatingRemainder(dividingBy: 100) == 0 {
+            priceText = "¥\(Int(model.amount / 100))"
+        } else {
+            priceText = String(format: "¥%.2lf", model.amount / 100 )
+        }
+        self.priceLabel.text = priceText
+        let width = priceText.singleLineWidth(font: .textF(12)) + 28.rpx
+        self.priceLabel.snp.updateConstraints { make in
+            make.width.equalTo(width)
+        }
+        
+        var orinalPriceText = ""
+        if model.originalAmount.truncatingRemainder(dividingBy: 100) == 0 {
+            orinalPriceText = "¥\(Int(model.originalAmount / 100))"
+        } else {
+            orinalPriceText = String(format: "¥%.2lf", model.originalAmount / 100 )
+        }
+        self.originPriceLabel.text = orinalPriceText
+    }
+}
+
+extension QSLVipGoodCollectionViewCell {
+    
+    func initView() {
+        
+        self.contentView.addSubview(bgView)
+        bgView.snp.makeConstraints { make in
+            make.right.bottom.equalTo(0)
+            make.left.equalTo(6.rpx)
+            make.top.equalTo(11.rpx)
+        }
+        
+        self.contentView.addSubview(tagIcon)
+        tagIcon.snp.makeConstraints { make in
+            make.left.equalTo(0)
+            make.top.equalTo(0)
+            make.size.equalTo(CGSize(width: 79.rpx, height: 29.rpx))
+        }
+        
+        self.bgView.addSubview(goodNameLabel)
+        goodNameLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(16.rpx)
+        }
+        
+        self.bgView.addSubview(goodDailyPriceLabel)
+        goodDailyPriceLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(43.rpx)
+        }
+        
+        self.bgView.addSubview(priceLabel)
+        let width = "¥128".singleLineWidth(font: .textF(12)) + 28.rpx
+        priceLabel.snp.makeConstraints { make in
+            make.width.equalTo(width)
+            make.height.equalTo(21.rpx)
+            make.bottom.equalTo(-26.rpx)
+            make.centerX.equalToSuperview()
+        }
+        
+        self.bgView.addSubview(originPriceLabel)
+        originPriceLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(-6.rpx)
+        }
+    }
+}

+ 709 - 0
QuickSearchLocation/Classes/Pages/QSLVip/Controller/QSLVipController.swift

@@ -0,0 +1,709 @@
+//
+//  QSLVipController.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/11/27.
+//
+
+import UIKit
+import YYText
+
+enum QSLVipJumpType: Int {
+    
+    case homeRoad    // 定位查看轨迹
+    case add         // 添加好友
+    case friendRoad  // 好友列表查看轨迹
+    case contact     // 添加紧急联系人
+    case mine        // 
+}
+
+class QSLVipController: QSLBaseController {
+    
+    var type: QSLVipJumpType?
+    
+    var goodList: [QSLGoodModel] = [QSLGoodModel]()
+    
+    var selectGood: QSLGoodModel? {
+        didSet {
+            updateSelectGoodUI()
+        }
+    }
+    
+    lazy var vipBg: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "vip_bg")
+        return imageView
+    }()
+    
+    lazy var backButton: UIButton = {
+       
+        let button = UIButton()
+        button.image(UIImage(named: "public_back_btn_white"))
+        button.title("会员中心")
+        button.mediumFont(17)
+        button.textColor(.white)
+        button.setImageTitleLayout(.imgLeft, spacing: 4.rpx)
+        button.addTarget(self, action: #selector(backBtnAction), for: .touchUpInside)
+        return button
+    }()
+    
+    lazy var scrollView: UIScrollView = {
+       
+        let scrollView = UIScrollView()
+//        scrollView.delegate = self
+        scrollView.bounces = false
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.contentInsetAdjustmentBehavior = .never
+        return scrollView
+    }()
+    
+    lazy var cardBannerImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "vip_banner_card")
+        return imageView
+    }()
+    
+    lazy var vipTimeLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("升级VIP会员,解锁全部功能")
+        label.font(13)
+        label.textColor = .hexStringColor(hexString: "#AF7655")
+        return label
+    }()
+    
+    lazy var funcBannerImageView: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "vip_banner_func")
+        return imageView
+    }()
+    
+    lazy var mainView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = QSLColor.backGroundColor
+        view.addRadius(radius: 12.rpx)
+        return view
+    }()
+    
+    lazy var goodsBgView: UIView = {
+       
+        let view = UIView()
+        view.gradientBackgroundColor(color1: .hexStringColor(hexString: "#FFF8F2"), color2: .hexStringColor(hexString: "#FFFFFF"), width: QSLConst.qsl_kScreenW, height: 255.rpx, direction: .vertical)
+        return view
+    }()
+    
+    lazy var vipGoodsTitleIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "vip_title_icon")
+        return imageView
+    }()
+    
+    lazy var goodsCollectionView: UICollectionView = {
+            
+        let layout = UICollectionViewFlowLayout()
+        layout.minimumLineSpacing = 0
+        layout.scrollDirection = .horizontal
+        
+        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
+        collectionView.backgroundColor = .clear
+        
+        collectionView.dataSource = self
+        collectionView.delegate = self
+        
+        collectionView.showsHorizontalScrollIndicator = false
+        collectionView.bounces = false
+        
+        collectionView.register(cellClass: QSLVipGoodCollectionViewCell.self)
+        
+        return collectionView
+    }()
+    
+    lazy var selectBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.image(UIImage(named: "public_select_btn_false"), .normal)
+        btn.image(UIImage(named: "public_select_btn_true"), .selected)
+        btn.addTarget(self, action: #selector(selectBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var serviceLabel: YYLabel = {
+       
+        let label = YYLabel()
+        
+        let attr = NSMutableAttributedString()
+        
+        let firstAttr = NSMutableAttributedString(string: "购买即同意")
+        firstAttr.font(12)
+        firstAttr.color(.hexStringColor(hexString: "#A7A7A7"))
+        attr.append(firstAttr)
+        
+        let blankAttr = NSMutableAttributedString(string: " ")
+        blankAttr.font(12)
+        attr.append(blankAttr)
+        
+        let privacyHL = YYTextHighlight()
+        var privacyStr = "《隐私权政策》"
+        
+        let privacyText = NSMutableAttributedString(string: privacyStr)
+        
+        privacyText.font(12)
+        privacyText.color(.hexStringColor(hexString: "#E7B983"))
+        privacyText.yy_setTextHighlight(privacyHL, range: NSRange(location: 0, length: privacyStr.count))
+        privacyHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.privacyAction()
+        }
+        attr.append(privacyText)
+        
+        attr.append(blankAttr)
+        
+        let andAttr = NSMutableAttributedString(string: "和")
+        andAttr.font(12)
+        andAttr.color(.hexStringColor(hexString: "#A7A7A7"))
+        attr.append(andAttr)
+        
+        attr.append(blankAttr)
+        
+        let serviceHL = YYTextHighlight()
+        var serviceStr = "《用户协议》"
+
+        let serviceText = NSMutableAttributedString(string: serviceStr)
+        serviceText.font(12)
+        serviceText.color(.hexStringColor(hexString: "#E7B983"))
+        serviceText.yy_setTextHighlight(serviceHL, range: NSRange(location: 0, length: serviceStr.count))
+        serviceHL.tapAction = { [weak self] containerView, text, range, rect in
+            self?.serviceAction()
+        }
+        
+        attr.append(serviceText)
+
+        label.attributedText = attr
+        
+        return label
+    }()
+    
+    lazy var commentView: UIView = {
+       
+        let view = UIView()
+        view.backgroundColor = .white
+        return view
+    }()
+    
+    lazy var commentTitleIcon: UIImageView = {
+       
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "vip_comment_title_icon")
+        return imageView
+    }()
+    
+    lazy var comment1: QSLVipCommentCellView = {
+       
+        let view = QSLVipCommentCellView()
+        view.config(name: "用户189****7913", comment: "上班没时间,远程遛娃,非常方便很好用。")
+        return view
+    }()
+    
+    lazy var comment2: QSLVipCommentCellView = {
+       
+        let view = QSLVipCommentCellView()
+        view.config(name: "用户189****7913", comment: "用了之后,才发现真的可以找到他。")
+        return view
+    }()
+
+    lazy var comment3: QSLVipCommentCellView = {
+       
+        let view = QSLVipCommentCellView()
+        view.config(name: "用户189****7913", comment: "轨迹很准,一目了然,奶奶出门遇到危险直接一键报警,我就收到信息了")
+        return view
+    }()
+    
+    lazy var bottomView: UIView = {
+       
+        let view = UIView()
+        view.gradientBackgroundColor(color1: .hexStringColor(hexString: "#00434E"), color2: .hexStringColor(hexString: "#0E5E61"), width: QSLConst.qsl_kScreenW - 24.rpx, height: 50.rpx, direction: .horizontal)
+        view.addRadius(radius: 25.rpx)
+        return view
+    }()
+    
+    lazy var unlockBtn: UIButton = {
+       
+        let btn = UIButton()
+        btn.setBackgroundImage(UIImage(named: "vip_unlock_btn_bg"), for: .normal)
+        btn.title("立即解锁")
+        btn.textColor(.hexStringColor(hexString: "#9B3800"))
+        btn.mediumFont(18)
+        btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: 30.rpx, bottom: 0, right: 0)
+        btn.addTarget(self, action: #selector(unlockBtnAction), for: .touchUpInside)
+        return btn
+    }()
+    
+    lazy var priceIconLabel: UILabel = {
+       
+        let label = UILabel()
+        label.text("¥")
+        label.textColor = .hexStringColor(hexString: "#FFF8EF")
+        label.font(14)
+        return label
+    }()
+    
+    lazy var priceLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("168")
+        label.textColor = .hexStringColor(hexString: "#FFF8EF")
+        label.font(24)
+        return label
+    }()
+    
+    lazy var goodTypeLabel: UILabel = {
+        
+        let label = UILabel()
+        label.text("/ 永久会员")
+        label.textColor = .hexStringColor(hexString: "#FFF8EF")
+        label.font(12)
+        return label
+    }()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        initializeView()
+        
+        updateUI()
+        
+        requestItemList()
+        
+        if let type = self.type {
+            switch type {
+            case .homeRoad:
+                gravityInstance?.track(QSLGravityConst.vip_show, properties: ["id": 1001])
+            case .add:
+                gravityInstance?.track(QSLGravityConst.vip_show, properties: ["id": 1002])
+            case .friendRoad:
+                gravityInstance?.track(QSLGravityConst.vip_show, properties: ["id": 1003])
+            case .contact:
+                gravityInstance?.track(QSLGravityConst.vip_show, properties: ["id": 1004])
+            case .mine:
+                gravityInstance?.track(QSLGravityConst.vip_show, properties: ["id": 1006])
+            }
+        }
+    }
+}
+
+extension QSLVipController {
+    
+    @objc func privacyAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppPrivacyAgreementLink
+        vc.title = "隐私政策"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+    
+    @objc func serviceAction() {
+        
+        let vc = QSLWebViewController()
+        vc.webUrl = QSLConfig.AppServiceAgreementLink
+        vc.title = "服务协议"
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
+    
+    @objc func selectBtnAction() {
+        
+        selectBtn.isSelected = !selectBtn.isSelected
+    }
+    
+    @objc func unlockBtnAction() {
+        
+        switch self.selectGood?.level {
+        case 100 :
+            gravityInstance?.track(QSLGravityConst.vip_buy_click, properties: ["id": 1006])
+            break
+        case 700:
+            gravityInstance?.track(QSLGravityConst.vip_buy_click, properties: ["id": 1005])
+            break;
+        case 3100:
+            gravityInstance?.track(QSLGravityConst.vip_buy_click, properties: ["id": 1004])
+            break;
+        case 9200:
+            gravityInstance?.track(QSLGravityConst.vip_buy_click, properties: ["id": 1003])
+            break;
+        case 36600:
+            gravityInstance?.track(QSLGravityConst.vip_buy_click, properties: ["id": 1002])
+            break;
+        case 3660000:
+            gravityInstance?.track(QSLGravityConst.vip_buy_click, properties: ["id": 1001])
+            break;
+        default:
+            break;
+        }
+        
+        if !selectBtn.isSelected {
+            self.view.toast(text: "请先同意《隐私权政策》和《用户协议》")
+            return
+        }
+        
+        if goodList.count > 0, let selectGood = self.selectGood {
+            QSLLoading.show()
+            QSLVipManager.shared.startPay(goods: selectGood) { status, outTradeNo in
+                QSLVipManager.shared.isPaying = false
+                if status == .success {
+                    QSLLoading.success(text: "支付成功")
+//                    NSLocalizedString("Payment successful", comment: "支付成功")
+                    
+//                    if let payType = self.payType {
+//                        geInstance?.track(HolaGravityConst.vip_open_success, properties: ["id": payType])
+//                        
+//                        if payType == 1004 {
+//                            geInstance?.track(HolaGravityConst.vip_alert_pay, properties: ["id": 1002])
+//                        }
+//                    }
+                    
+//                    let val: Float = 0.1
+//                    HolaSaManager.shared.addEventAttribution(eventDict: ["event_name": "pay", "event_val": val])
+                    
+                    // 引力传递支付事件
+                    if let selectGood = self.selectGood {
+                        gravityInstance?.trackPayEvent(withAmount: Int32(selectGood.amount), withPayType: "CNY", withOrderId: outTradeNo, withPayReason: selectGood.name, withPayMethod: "apple")
+                    }
+                    
+                    if let type = self.type {
+                        switch type {
+                        case .homeRoad:
+                            gravityInstance?.track(QSLGravityConst.vip_success_page, properties: ["id": 1001])
+                        case .add:
+                            gravityInstance?.track(QSLGravityConst.vip_success_page, properties: ["id": 1002])
+                        case .friendRoad:
+                            gravityInstance?.track(QSLGravityConst.vip_success_page, properties: ["id": 1003])
+                        case .contact:
+                            gravityInstance?.track(QSLGravityConst.vip_success_page, properties: ["id": 1004])
+                        case .mine:
+                            gravityInstance?.track(QSLGravityConst.vip_success_page, properties: ["id": 1006])
+                        }
+                    }
+
+                    switch self.selectGood?.level {
+                    case 100 :
+                        gravityInstance?.track(QSLGravityConst.vip_success_good, properties: ["id": 1])
+                        break
+                    case 700:
+                        gravityInstance?.track(QSLGravityConst.vip_success_good, properties: ["id": 5])
+                        break;
+                    case 3100:
+                        gravityInstance?.track(QSLGravityConst.vip_success_good, properties: ["id": 9])
+                        break;
+                    case 9200:
+                        gravityInstance?.track(QSLGravityConst.vip_success_good, properties: ["id": 13])
+                        break;
+                    case 36600:
+                        gravityInstance?.track(QSLGravityConst.vip_success_good, properties: ["id": 17])
+                        break;
+                    case 3660000:
+                        gravityInstance?.track(QSLGravityConst.vip_success_good, properties: ["id": 21])
+                        break;
+                    default:
+                        break;
+                    }
+
+                    
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                        self.navigationController?.popViewController(animated: true)
+                        NotificationCenter.default.post(name: QSLNotification.QSLRefreshMember, object: nil)
+                    }
+                } else if status == .cancel {
+                    QSLLoading.error(text: "支付取消")
+                } else if status == .fail {
+                    gravityInstance?.track(QSLGravityConst.vip_fail)
+                    QSLLoading.error(text: "支付失败")
+                } else if status == .searchFail {
+                    
+                    QSLLoading.error(text: "查询订单失败,请稍后重试")
+                }
+            }
+        }
+    }
+}
+
+extension QSLVipController {
+    
+    func requestItemList() {
+        
+        QSLNetwork().request(.vipItemList(dict: [:])) { response in
+            let list = response.mapArray(QSLGoodModel.self, modelKey: "data>list")
+            self.goodList = list
+            if self.goodList.count > 0 {
+                self.goodList[0].isSelect = true
+                self.selectGood = self.goodList[0]
+            }
+            self.goodsCollectionView.reloadData()
+        } fail: { code, error in
+            self.view.toast(text: "加载商品列表失败")
+        }
+    }
+}
+
+// MARK: - 设置 CollectionView
+extension QSLVipController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
+    
+    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        return goodList.count
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        
+        let cell = collectionView.dequeueReusableCell(cellType: QSLVipGoodCollectionViewCell.self, cellForRowAt: indexPath)
+        let model = self.goodList[indexPath.row]
+        cell.config(model: model)
+        return cell
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        
+        for i in 0..<self.goodList.count {
+            self.goodList[i].isSelect = false
+        }
+        
+        self.goodList[indexPath.row].isSelect = true
+        self.selectGood = self.goodList[indexPath.row]
+        
+        switch self.selectGood?.level {
+        case 100 :
+            gravityInstance?.track(QSLGravityConst.vip_good_select, properties: ["id": 1006])
+            break
+        case 700:
+            gravityInstance?.track(QSLGravityConst.vip_good_select, properties: ["id": 1005])
+            break;
+        case 3100:
+            gravityInstance?.track(QSLGravityConst.vip_good_select, properties: ["id": 1004])
+            break;
+        case 9200:
+            gravityInstance?.track(QSLGravityConst.vip_good_select, properties: ["id": 1003])
+            break;
+        case 36600:
+            gravityInstance?.track(QSLGravityConst.vip_good_select, properties: ["id": 1002])
+            break;
+        case 3660000:
+            gravityInstance?.track(QSLGravityConst.vip_good_select, properties: ["id": 1001])
+            break;
+        default:
+            break;
+        }
+        
+        self.goodsCollectionView.reloadData()
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+        
+        return CGSize(width: 104.rpx, height: 143.rpx)
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
+        return 4.rpx
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
+        return 0
+    }
+}
+
+extension QSLVipController {
+    
+    func updateUI() {
+        
+        if QSLBaseManager.shared.isLogin() && QSLBaseManager.shared.isVip() {
+            let model = QSLBaseManager.shared.userModel.memberModel
+            if model.permanent {
+                
+                self.vipTimeLabel.text("您已是尊贵的永久会员")
+                self.bottomView.isHidden = true
+            } else {
+                
+                let level = model.memberLevelString()
+                let endTime = model.endTimestampString()
+                self.vipTimeLabel.text("\(level):\(endTime)到期")
+            }
+        } else {
+            
+            self.vipTimeLabel.text("升级VIP会员,解锁全部功能")
+        }
+    }
+    
+    func updateSelectGoodUI() {
+        var priceText = ""
+        if let selectGood = self.selectGood {
+            if selectGood.amount.truncatingRemainder(dividingBy: 100) == 0 {
+                priceText = "\(Int(selectGood.amount / 100))"
+            } else {
+                priceText = String(format: "%.2lf", selectGood.amount / 100 )
+            }
+        }
+        self.priceLabel.text(priceText)
+        self.goodTypeLabel.text("/ \(self.selectGood?.name ?? "")")
+    }
+    
+    func initializeView() {
+        
+        self.view.addSubview(vipBg)
+        vipBg.snp.makeConstraints { make in
+            make.left.top.right.equalTo(0)
+        }
+        
+        self.view.addSubview(backButton)
+        backButton.snp.makeConstraints { make in
+            make.size.equalTo(100.rpx)
+            make.height.equalTo(25.rpx)
+            make.left.equalTo(12.rpx)
+            make.top.equalTo(QSLConst.qsl_kStatusBarFrameH)
+        }
+        
+        self.view.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.top.equalTo(QSLConst.qsl_kNavFrameH)
+            make.bottom.equalTo(0)
+        }
+        
+        scrollView.addSubview(cardBannerImageView)
+        cardBannerImageView.snp.makeConstraints { make in
+            make.top.equalTo(0)
+            make.centerX.equalToSuperview()
+            make.width.equalTo(336.rpx)
+            make.height.equalTo(108.rpx)
+        }
+        
+        cardBannerImageView.addSubview(vipTimeLabel)
+        vipTimeLabel.snp.makeConstraints { make in
+            make.left.equalTo(23.rpx)
+            make.bottom.equalTo(-19.rpx)
+        }
+        
+        scrollView.addSubview(funcBannerImageView)
+        funcBannerImageView.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 336.rpx, height: 172.rpx))
+            make.centerX.equalToSuperview()
+            make.top.equalTo(cardBannerImageView.snp.bottom).offset(14.rpx)
+        }
+        
+        scrollView.addSubview(mainView)
+        mainView.snp.makeConstraints { make in
+            make.left.right.bottom.equalTo(0)
+            make.top.equalTo(funcBannerImageView.snp.bottom).offset(-24.rpx)
+            make.width.equalTo(QSLConst.qsl_kScreenW)
+            make.height.equalTo(700.rpx)
+        }
+        
+        mainView.addSubview(goodsBgView)
+        goodsBgView.snp.makeConstraints { make in
+            make.top.left.right.equalTo(0)
+            make.height.equalTo(255.rpx)
+        }
+        
+        goodsBgView.addSubview(vipGoodsTitleIcon)
+        vipGoodsTitleIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 117.rpx, height: 26.rpx))
+            make.left.equalTo(20.rpx)
+            make.top.equalTo(16.rpx)
+        }
+        
+        goodsBgView.addSubview(goodsCollectionView)
+        goodsCollectionView.snp.makeConstraints { make in
+            make.left.equalTo(18.rpx)
+            make.right.equalTo(-22.rpx)
+            make.height.equalTo(143.rpx)
+            make.top.equalTo(vipGoodsTitleIcon.snp.bottom).offset(10.rpx)
+        }
+        
+        goodsBgView.addSubview(selectBtn)
+        selectBtn.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 12.rpx, height: 12.rpx))
+            make.left.equalTo(23.rpx)
+            make.top.equalTo(goodsCollectionView.snp.bottom).offset(16.rpx)
+        }
+        
+        goodsBgView.addSubview(serviceLabel)
+        serviceLabel.snp.makeConstraints { make in
+            make.left.equalTo(selectBtn.snp.right).offset(2.rpx)
+            make.centerY.equalTo(selectBtn.snp.centerY)
+        }
+        
+        mainView.addSubview(commentView)
+        commentView.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.top.equalTo(goodsBgView.snp.bottom).offset(8.rpx)
+            make.bottom.equalTo(0)
+        }
+        
+        commentView.addSubview(commentTitleIcon)
+        commentTitleIcon.snp.makeConstraints { make in
+            make.size.equalTo(CGSize(width: 83.rpx, height: 26.rpx))
+            make.top.equalTo(16.rpx)
+            make.left.equalTo(12.rpx)
+        }
+        
+        let commentHeight1 = "上班没时间,远程遛娃,非常方便很好用。".heightAccording(width: QSLConst.qsl_kScreenW - 52.rpx - 12.rpx, font: .textF(15), lineSpacing: 2) + 35.rpx
+        commentView.addSubview(comment1)
+        comment1.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.top.equalTo(commentTitleIcon.snp.bottom).offset(16.rpx)
+            make.height.equalTo(commentHeight1)
+        }
+        
+        let commentHeight2 = "用了之后,才发现真的可以找到他。".heightAccording(width: QSLConst.qsl_kScreenW - 52.rpx - 12.rpx, font: .textF(15), lineSpacing: 2) + 35.rpx
+        commentView.addSubview(comment2)
+        comment2.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.top.equalTo(comment1.snp.bottom).offset(30.rpx)
+            make.height.equalTo(commentHeight2)
+        }
+        
+        let commentHeight3 = "轨迹很准,一目了然,奶奶出门遇到危险直接一键报警,我就收到信息了".heightAccording(width: QSLConst.qsl_kScreenW - 52.rpx - 12.rpx, font: .textF(15), lineSpacing: 2) + 35.rpx
+        commentView.addSubview(comment3)
+        comment3.snp.makeConstraints { make in
+            make.left.right.equalTo(0)
+            make.top.equalTo(comment2.snp.bottom).offset(30.rpx)
+            make.height.equalTo(commentHeight3)
+        }
+        
+        scrollView.snp.makeConstraints { make in
+            make.bottom.equalTo(mainView)
+        }
+        
+        self.view.addSubview(bottomView)
+        bottomView.snp.makeConstraints { make in
+            make.left.equalTo(12.rpx)
+            make.right.equalTo(-12.rpx)
+            make.bottom.equalTo(-QSLConst.qsl_kTabbarBottom)
+            make.height.equalTo(50.rpx)
+        }
+        
+        bottomView.addSubview(unlockBtn)
+        unlockBtn.snp.makeConstraints { make in
+            make.right.top.bottom.equalTo(0)
+            make.width.equalTo(165.rpx)
+        }
+        
+        bottomView.addSubview(priceIconLabel)
+        priceIconLabel.snp.makeConstraints { make in
+            make.left.equalTo(20.rpx)
+            make.bottom.equalTo(-12.rpx)
+        }
+        
+        bottomView.addSubview(priceLabel)
+        priceLabel.snp.makeConstraints { make in
+            make.left.equalTo(priceIconLabel.snp.right).offset(2.rpx)
+            make.bottom.equalTo(-10.rpx)
+        }
+        
+        bottomView.addSubview(goodTypeLabel)
+        goodTypeLabel.snp.makeConstraints { make in
+            make.left.equalTo(priceLabel.snp.right).offset(2.rpx)
+            make.bottom.equalTo(-14.rpx)
+        }
+    
+    }
+}

+ 352 - 0
QuickSearchLocation/Classes/Pages/QSLVip/QSLVipManager.swift

@@ -0,0 +1,352 @@
+//
+//  QSLVipManager.swift
+//  QuickSearchLocation
+//
+//  Created by Destiny on 2024/12/11.
+//
+
+import StoreKit
+
+typealias payComplete = ((QSLPayStatus, String) -> ())
+typealias restoreComplete = ((Bool) -> ())
+
+enum QSLPayStatus: Int {
+    case fail = 0
+    case success
+    case cancel
+    case ignore
+    case searchFail
+}
+
+class QSLVipManager: NSObject {
+    
+    static let shared = QSLVipManager()
+    
+    // 选择支付的商品
+    var selectGoods: QSLGoodModel?
+    // 订单模型
+    var orderModel: QSLOrderModel?
+    
+    var payCompleteCloure: payComplete?
+    
+    var restoreCompleteClosure: restoreComplete?
+    
+    // 支付查询的次数
+    var statusSearchCount = 0
+    
+    var isCheck: Bool = false
+    // 是否正在恢复
+    var isPaying: Bool = false
+    // 是否正在恢复
+    var isRestoring: Bool = false
+    
+    override init() {
+        super.init()
+        SKPaymentQueue.default().add(self)
+    }
+    
+    deinit {
+        SKPaymentQueue.default().remove(self)
+    }
+}
+
+extension QSLVipManager {
+    
+//    func check() {
+//        print("检查自动续费订单")
+//        self.isCheck = true
+//    }
+    
+    // 开始支付
+    func startPay(goods: QSLGoodModel, complete: payComplete?) {
+        
+        self.isPaying = true
+        self.isCheck = false
+        
+        // 支付前查询自动续费的订单,将订单置为支付状态
+        let transactions = SKPaymentQueue.default().transactions
+        if transactions.count > 0 {
+            for tran in transactions {
+                if tran.transactionState == .purchased {
+                    SKPaymentQueue.default().finishTransaction(tran)
+                }
+            }
+        }
+        
+        selectGoods = goods
+        payCompleteCloure = complete
+        
+        self.requestOrderPay()
+    }
+    
+    // 应用启动后检查未支付完成的订单
+    func openAppRePay(receiptData: String, complete: payComplete?) {
+        
+        payCompleteCloure = complete
+        self.requestOrderResult(receiptData: receiptData)
+    }
+    
+//    func restoreAction(complete: restoreComplete?) {
+//        
+//        if complete != nil {
+//            self.restoreCompleteClosure = complete
+//        }
+//        
+//        // 支付前查询自动续费的订单,将订单置为支付状态
+//        let transactions = SKPaymentQueue.default().transactions
+//        if transactions.count > 0 {
+//            for tran in transactions {
+//                if tran.transactionState == .purchased {
+//                    SKPaymentQueue.default().finishTransaction(tran)
+//                }
+//            }
+//        }
+//        
+//        self.isPaying = true
+//        self.isCheck = false
+//        
+//        print("开始恢复订阅")
+//        SKPaymentQueue.default().restoreCompletedTransactions()
+//        
+//        Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { timer in
+//            if let restoreCompleteClosure = self.restoreCompleteClosure {
+//                print("恢复订阅 15秒超时")
+//                restoreCompleteClosure(false)
+//            }
+//            self.restoreCompleteClosure = nil
+//            // 停止定时器并从运行循环中移除
+//            timer.invalidate()
+//        }
+//    }
+}
+
+// 网络请求
+extension QSLVipManager {
+    
+    // 发起支付请求
+    func requestOrderPay() {
+        
+        var dict = [String: Any]()
+        dict["itemId"] = self.selectGoods?.goodId
+        dict["payPlatform"] = 2
+        dict["payMethod"] = 3
+        
+        QSLNetwork().request(.vipOrderSubmitAndPay(dict: dict)) { response in
+            self.orderModel = response.mapObject(QSLOrderModel.self, modelKey: "data")
+            if let selectGoods = self.selectGoods {
+                self.orderModel?.selectGoods = selectGoods
+            }
+            self.submitIAP()
+        } fail: { code, msg in
+            if let payCompleteCloure = self.payCompleteCloure {
+                payCompleteCloure(.fail, msg)
+                self.payCompleteCloure = nil
+            }
+        }
+    }
+    
+    // 网络请求订单结果
+    func requestOrderResult(receiptData: String) {
+        
+        var dict = [String: String]()
+        
+        if let tradeNum = self.orderModel?.outTradeNo {
+            dict["outTradeNo"] = tradeNum
+            dict["receiptData"] = receiptData
+        }
+        
+        QSLNetwork().request(.vipOrderPayStatus(dict: dict)) { [weak self] response in
+            let payStatus = response.toJSON(modelKey: "data>payStatus").intValue
+            // 继续轮询查询支付状态
+            if payStatus == 0 || payStatus == 1 {
+                QSLVipManager.shared.statusSearchCount = QSLVipManager.shared.statusSearchCount + 1
+                if QSLVipManager.shared.statusSearchCount < 10 {
+                    self?.cacheOrder(isFinish: false)
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
+                        // 在主线程上执行查询
+                        DispatchQueue.main.async {
+                            self?.requestOrderResult(receiptData: receiptData)
+                        }
+                    }
+                } else {
+                    self?.cacheOrder(isFinish: false)
+                    // 查询失败
+                    if let payCompleteCloure = self?.payCompleteCloure {
+                        payCompleteCloure(.searchFail, "")
+                    }
+                    self?.payCompleteCloure = nil
+                    QSLVipManager.shared.statusSearchCount = 0
+                }
+            } else if payStatus == 2 {
+                self?.cacheOrder(isFinish: true)
+                // 支付成功
+                if let payCompleteCloure = self?.payCompleteCloure {
+                    payCompleteCloure(.success, self?.orderModel?.outTradeNo ?? "")
+                }
+                self?.payCompleteCloure = nil
+                QSLVipManager.shared.statusSearchCount = 0
+            } else if payStatus == 3 {
+                self?.cacheOrder(isFinish: false)
+                // 支付失败
+                if let payCompleteCloure = self?.payCompleteCloure {
+                    payCompleteCloure(.fail, "")
+                }
+                self?.payCompleteCloure = nil
+                QSLVipManager.shared.statusSearchCount = 0
+            } else if payStatus == 4 {
+                QSLVipManager.shared.statusSearchCount = 0
+            }
+        } fail: { code, msg in
+        
+            self.cacheOrder(isFinish: false)
+            if let payCompleteCloure = self.payCompleteCloure {
+                payCompleteCloure(.fail, "")
+            }
+            self.payCompleteCloure = nil
+            QSLVipManager.shared.statusSearchCount = 0
+        }
+    }
+    
+//    func requestRestoreOrder(receiptData: String) {
+//        
+//        self.isRestoring = true
+//        
+//        var params: [String: Any] = [String: Any]()
+//        params["receiptData"] = receiptData
+//        params["payPlatform"] = 2
+//        params["payMethod"] = 3
+//        
+//        QSLNetwork().request(.memberResume(dict: params)) { response in
+//            
+//            self.isRestoring = false
+//            if let restoreCompleteClosure = self.restoreCompleteClosure {
+//                restoreCompleteClosure(true)
+//            }
+//            self.restoreCompleteClosure = nil
+//        } fail: { code, msg in
+//            
+//            if let restoreCompleteClosure = self.restoreCompleteClosure {
+//                restoreCompleteClosure(false)
+//            }
+//            self.restoreCompleteClosure = nil
+//        }
+//    }
+}
+
+extension QSLVipManager: SKPaymentTransactionObserver {
+    
+    // 提交内购
+    func submitIAP() {
+        
+        if SKPaymentQueue.canMakePayments() {
+            
+            let payment = SKMutablePayment()
+            if let appleGoodId = self.selectGoods?.appleGoodsId {
+                payment.productIdentifier = appleGoodId
+            }
+            payment.applicationUsername = self.orderModel?.appAccountToken
+            payment.quantity = 1
+            SKPaymentQueue.default().add(payment)
+        }
+    }
+    
+    // 查询订单
+    func localSearchOrder(transaction: SKPaymentTransaction) {
+        
+        if let receiptUrl = Bundle.main.appStoreReceiptURL {
+            let receiptData = try? Data(contentsOf: receiptUrl)
+            if let receiptString = receiptData?.base64EncodedString(options: .endLineWithLineFeed) {
+                debugPrint("receiptString = \(String(describing: receiptString))")
+                
+//                if let orderModel = HolaOrderCacheManager.getModel() {
+//                    if orderModel.receiptData.count > 0 {
+//                        if orderModel.receiptData == receiptString {
+//                            // 支付失败
+//                            if let payCompleteCloure = self.payCompleteCloure {
+//                                payCompleteCloure(.cancel, "")
+//                            }
+//                            self.payCompleteCloure = nil
+//                            return
+//                        }
+//                    }
+//                }
+                self.orderModel?.receiptData = receiptString
+                self.cacheOrder(isFinish: false)
+                self.requestOrderResult(receiptData: receiptString)
+            }
+        }
+    }
+    
+//    // 恢复订阅
+//    func restoreOrder(transaction: SKPaymentTransaction) {
+//        
+//        if isRestoring { return }
+//        
+//        if let receiptUrl = Bundle.main.appStoreReceiptURL {
+//            let receiptData = try? Data(contentsOf: receiptUrl)
+//            if let receiptString = receiptData?.base64EncodedString(options: .endLineWithLineFeed) {
+//                debugPrint("receiptString = \(String(describing: receiptString))")
+//                self.requestRestoreOrder(receiptData: receiptString)
+//            }
+//        }
+//    }
+    
+    // 缓存支付的结果
+    func cacheOrder(isFinish: Bool) {
+        
+        if var order = self.orderModel {
+            order.isFinish = isFinish
+            QSLCacheManager.cacheOrderModel(order)
+        }
+    }
+    
+    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
+        
+        print("监听AppStore支付状态 updatedTransactions")
+        
+        DispatchQueue.main.async {
+            for tran in transactions {
+                switch tran.transactionState {
+                    
+                case .purchased:
+                    print("商品已苹果支付成功")
+                    
+                    if self.isCheck {
+                        SKPaymentQueue.default().finishTransaction(tran)
+                    } else {
+                        self.localSearchOrder(transaction: tran)
+                        SKPaymentQueue.default().finishTransaction(tran)
+                    }
+                case .purchasing:
+                    print("正在购买中,商品已添加进列表")
+                case .restored:
+                    print("恢复订阅")
+//                    self.restoreOrder(transaction: tran)
+                    SKPaymentQueue.default().finishTransaction(tran)
+                case .failed:
+                    print("商品支付失败")
+                    // 需要前台响应
+                    if let payCompleteCloure = self.payCompleteCloure {
+                        payCompleteCloure(.fail, "")
+                    }
+                    self.payCompleteCloure = nil
+                    // 需要前台响应
+                    if let restoreCompleteClosure = self.restoreCompleteClosure {
+                        restoreCompleteClosure(false)
+                    }
+                    self.restoreCompleteClosure = nil
+                    SKPaymentQueue.default().finishTransaction(tran)
+                    
+                case .deferred:
+                    print("未确定")
+                default:
+                    break
+                }
+            }
+        }
+    }
+    
+    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
+        print("恢复购买失败\(error.localizedDescription)")
+    }
+}

+ 0 - 0
QuickSearchLocation/Frameworks/GravityEngineSDK.framework/.DS_Store


Some files were not shown because too many files changed in this diff