- 二级页面

This commit is contained in:
linshujie 2026-08-11 10:59:45 +08:00
parent 738d0b710f
commit 5098bcda0e
54 changed files with 3154 additions and 997 deletions

View File

@ -47,6 +47,7 @@
305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */; }; 305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */; };
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */; }; 305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */; };
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */; }; 305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */; };
55B217A33022F0B100784706 /* UIDevice+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A33022F0B100784707 /* UIDevice+Extension.swift */; };
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */; }; 305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */; };
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */; }; 305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */; };
305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */; }; 305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */; };
@ -186,6 +187,11 @@
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */; }; 30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */; };
30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */; }; 30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */; };
30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */; }; 30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */; };
55B219C13024C00100784722 /* BubbleHeroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784721 /* BubbleHeroView.swift */; };
55B219C13024C00100784724 /* CreateBubbleSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784723 /* CreateBubbleSetupView.swift */; };
55B219C13024C00100784726 /* BubbleKnowledgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784725 /* BubbleKnowledgeView.swift */; };
55B219C13024C00100784728 /* BubbleKnowledgeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */; };
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; };
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */; }; 30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */; };
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; }; 30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; };
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; }; 30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
@ -233,6 +239,8 @@
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */; }; 30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */; };
30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */; }; 30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */; };
30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */; }; 30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */; };
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */; }; 30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */; };
30D74AB42FEA25B90050EB2C /* ViewedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */; }; 30D74AB42FEA25B90050EB2C /* ViewedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */; };
30D74AB62FEA34FF0050EB2C /* ItineraryAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */; }; 30D74AB62FEA34FF0050EB2C /* ItineraryAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */; };
@ -308,6 +316,12 @@
55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D73023010000784736 /* PhoneReportDetailVC.swift */; }; 55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D73023010000784736 /* PhoneReportDetailVC.swift */; };
55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D93023010000784738 /* PhoneReportDetailView.swift */; }; 55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D93023010000784738 /* PhoneReportDetailView.swift */; };
55B217DC302301000078473B /* PhoneReportDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */; }; 55B217DC302301000078473B /* PhoneReportDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */; };
55B218A13024A00100784702 /* AboutVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784701 /* AboutVC.swift */; };
55B218A13024A00100784704 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784703 /* AboutView.swift */; };
55B218A13024A00100784706 /* CancelAccountVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784705 /* CancelAccountVC.swift */; };
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; };
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; };
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; };
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; }; 55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; };
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; }; 55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; }; 55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
@ -358,6 +372,7 @@
305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = "<group>"; }; 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = "<group>"; };
305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; }; 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; };
305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; }; 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; };
55B217A33022F0B100784707 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = "<group>"; };
305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; }; 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; };
305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; }; 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; };
305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resource.swift"; sourceTree = "<group>"; }; 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resource.swift"; sourceTree = "<group>"; };
@ -503,6 +518,11 @@
30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = "<group>"; }; 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = "<group>"; };
30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = "<group>"; }; 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = "<group>"; };
30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = "<group>"; }; 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = "<group>"; };
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
55B219C13024C00100784723 /* CreateBubbleSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleSetupView.swift; sourceTree = "<group>"; };
55B219C13024C00100784725 /* BubbleKnowledgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeView.swift; sourceTree = "<group>"; };
55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleKnowledgeVC.swift; sourceTree = "<group>"; };
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationView.swift; sourceTree = "<group>"; }; 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationView.swift; sourceTree = "<group>"; };
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; }; 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; };
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; }; 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
@ -551,6 +571,8 @@
30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSSignalHelper.swift; sourceTree = "<group>"; }; 30D74AAA2FE8C7700050EB2C /* GPSSignalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSSignalHelper.swift; sourceTree = "<group>"; };
30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = "<group>"; }; 30D74AAD2FEA13E00050EB2C /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = "<group>"; };
30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleVC.swift; sourceTree = "<group>"; }; 30D74AAF2FEA13ED0050EB2C /* ScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleVC.swift; sourceTree = "<group>"; };
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewModel.swift; sourceTree = "<group>"; }; 30D74AB12FEA1D5D0050EB2C /* ScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewModel.swift; sourceTree = "<group>"; };
30D74AB32FEA25B90050EB2C /* ViewedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedModel.swift; sourceTree = "<group>"; }; 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedModel.swift; sourceTree = "<group>"; };
30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryAPI.swift; sourceTree = "<group>"; }; 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryAPI.swift; sourceTree = "<group>"; };
@ -630,6 +652,12 @@
55B217D73023010000784736 /* PhoneReportDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailVC.swift; sourceTree = "<group>"; }; 55B217D73023010000784736 /* PhoneReportDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailVC.swift; sourceTree = "<group>"; };
55B217D93023010000784738 /* PhoneReportDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailView.swift; sourceTree = "<group>"; }; 55B217D93023010000784738 /* PhoneReportDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailView.swift; sourceTree = "<group>"; };
55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailViewModel.swift; sourceTree = "<group>"; }; 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailViewModel.swift; sourceTree = "<group>"; };
55B218A13024A00100784701 /* AboutVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutVC.swift; sourceTree = "<group>"; };
55B218A13024A00100784703 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
55B218A13024A00100784705 /* CancelAccountVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountVC.swift; sourceTree = "<group>"; };
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; }; 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; }; 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
@ -792,6 +820,7 @@
305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */, 305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */,
305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */, 305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */,
305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */, 305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */,
55B217A33022F0B100784707 /* UIDevice+Extension.swift */,
305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */, 305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */,
305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */, 305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */,
305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */, 305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */,
@ -1023,6 +1052,7 @@
30C4C01E2FDC0EA6009215C1 /* GroupInfo */, 30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
307073E42FD18A20004C37CC /* GroupChat */, 307073E42FD18A20004C37CC /* GroupChat */,
30B74B3F2FF2435200F6744D /* GroupMemberList */, 30B74B3F2FF2435200F6744D /* GroupMemberList */,
55B218A13024A00100784712 /* MemberInfo */,
30EFF3A22FD7C58400EB35D4 /* GroupSetting */, 30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */, 30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */,
30C4C0172FDBF066009215C1 /* RemoveMember */, 30C4C0172FDBF066009215C1 /* RemoveMember */,
@ -1099,6 +1129,8 @@
55B217C03022F0A000784720 /* MinePhotoWallView.swift */, 55B217C03022F0A000784720 /* MinePhotoWallView.swift */,
30EFF3CB2FDA667A00EB35D4 /* MyProfile */, 30EFF3CB2FDA667A00EB35D4 /* MyProfile */,
30EFF3BC2FD9585200EB35D4 /* Account */, 30EFF3BC2FD9585200EB35D4 /* Account */,
55B218A13024A00100784710 /* About */,
55B218A13024A00100784711 /* CancelAccount */,
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */, 30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */, 30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
30C4C0122FDB9178009215C1 /* CheckPermission */, 30C4C0122FDB9178009215C1 /* CheckPermission */,
@ -1113,6 +1145,7 @@
305A76352FCA8C7000227D26 /* Map */, 305A76352FCA8C7000227D26 /* Map */,
305A76262FCA8C7000227D26 /* Group */, 305A76262FCA8C7000227D26 /* Group */,
30D74AAC2FEA13BD0050EB2C /* Schedule */, 30D74AAC2FEA13BD0050EB2C /* Schedule */,
55B218B13024B00100784720 /* Explore */,
305A76392FCA8C7000227D26 /* Mine */, 305A76392FCA8C7000227D26 /* Mine */,
305A798E2FCAC5F600227D26 /* InviteMember */, 305A798E2FCAC5F600227D26 /* InviteMember */,
3062E8C52FCFD01000CEF511 /* VipRecharge */, 3062E8C52FCFD01000CEF511 /* VipRecharge */,
@ -1380,6 +1413,10 @@
children = ( children = (
30A87A5F2FEE71B80095E7C6 /* CreateBubbleVC.swift */, 30A87A5F2FEE71B80095E7C6 /* CreateBubbleVC.swift */,
30A87A5D2FEE71A50095E7C6 /* CreateBubbleView.swift */, 30A87A5D2FEE71A50095E7C6 /* CreateBubbleView.swift */,
55B219C13024C00100784721 /* BubbleHeroView.swift */,
55B219C13024C00100784723 /* CreateBubbleSetupView.swift */,
55B219C13024C00100784725 /* BubbleKnowledgeView.swift */,
55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */,
30A87A612FEE724D0095E7C6 /* CreateBubbleTiemView.swift */, 30A87A612FEE724D0095E7C6 /* CreateBubbleTiemView.swift */,
30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */, 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */,
30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */, 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */,
@ -1393,6 +1430,7 @@
children = ( children = (
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */, 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */,
30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */, 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */,
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */,
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */, 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */,
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */, 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */,
30EBF71F2FEFD6F2009A8A87 /* GroupChooseView.swift */, 30EBF71F2FEFD6F2009A8A87 /* GroupChooseView.swift */,
@ -1526,6 +1564,15 @@
path = SOS; path = SOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
55B218B13024B00100784720 /* Explore */ = {
isa = PBXGroup;
children = (
55B218B13024B00100784721 /* FeatureIntroVC.swift */,
55B218B13024B00100784723 /* FeatureIntroView.swift */,
);
path = Explore;
sourceTree = "<group>";
};
30D74AAC2FEA13BD0050EB2C /* Schedule */ = { 30D74AAC2FEA13BD0050EB2C /* Schedule */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1727,6 +1774,33 @@
path = TodayTrackDetail; path = TodayTrackDetail;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
55B218A13024A00100784710 /* About */ = {
isa = PBXGroup;
children = (
55B218A13024A00100784701 /* AboutVC.swift */,
55B218A13024A00100784703 /* AboutView.swift */,
);
path = About;
sourceTree = "<group>";
};
55B218A13024A00100784711 /* CancelAccount */ = {
isa = PBXGroup;
children = (
55B218A13024A00100784705 /* CancelAccountVC.swift */,
55B218A13024A00100784707 /* CancelAccountView.swift */,
);
path = CancelAccount;
sourceTree = "<group>";
};
55B218A13024A00100784712 /* MemberInfo */ = {
isa = PBXGroup;
children = (
55B218A13024A00100784709 /* MemberInfoVC.swift */,
55B218A13024A0010078470B /* MemberInfoView.swift */,
);
path = MemberInfo;
sourceTree = "<group>";
};
55B217DE302301000078473D /* PhoneReportDetail */ = { 55B217DE302301000078473D /* PhoneReportDetail */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1922,6 +1996,10 @@
30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */, 30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */,
30EFF3D82FDA8F1000EB35D4 /* EmergencyContactVC.swift in Sources */, 30EFF3D82FDA8F1000EB35D4 /* EmergencyContactVC.swift in Sources */,
30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */, 30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */,
55B219C13024C00100784722 /* BubbleHeroView.swift in Sources */,
55B219C13024C00100784724 /* CreateBubbleSetupView.swift in Sources */,
55B219C13024C00100784726 /* BubbleKnowledgeView.swift in Sources */,
55B219C13024C00100784728 /* BubbleKnowledgeVC.swift in Sources */,
30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */, 30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */,
30DC185E2FD1211D0041DCD1 /* VipRightsVC.swift in Sources */, 30DC185E2FD1211D0041DCD1 /* VipRightsVC.swift in Sources */,
305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */, 305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */,
@ -1982,6 +2060,7 @@
305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */, 305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */,
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */, 305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */,
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */, 305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */,
55B217A33022F0B100784706 /* UIDevice+Extension.swift in Sources */,
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */, 305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */,
55B217A13022F0A000784702 /* FontManager.swift in Sources */, 55B217A13022F0A000784702 /* FontManager.swift in Sources */,
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */, 305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */,
@ -2023,6 +2102,12 @@
55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */, 55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */,
55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */, 55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */,
55B217DC302301000078473B /* PhoneReportDetailViewModel.swift in Sources */, 55B217DC302301000078473B /* PhoneReportDetailViewModel.swift in Sources */,
55B218A13024A00100784702 /* AboutVC.swift in Sources */,
55B218A13024A00100784704 /* AboutView.swift in Sources */,
55B218A13024A00100784706 /* CancelAccountVC.swift in Sources */,
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */,
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */,
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */,
30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */, 30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */,
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */, 30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */,
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */, 30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */,
@ -2047,6 +2132,8 @@
305A76CC2FCA8C7000227D26 /* FileTools.swift in Sources */, 305A76CC2FCA8C7000227D26 /* FileTools.swift in Sources */,
305A76CD2FCA8C7000227D26 /* Permission.swift in Sources */, 305A76CD2FCA8C7000227D26 /* Permission.swift in Sources */,
30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */, 30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */,
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */,
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */,
305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */, 305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */,
3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */, 3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */,
305A76CF2FCA8C7000227D26 /* CountDownService.swift in Sources */, 305A76CF2FCA8C7000227D26 /* CountDownService.swift in Sources */,
@ -2079,6 +2166,7 @@
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */, 305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */,
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */, 30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */,
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */, 30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */,
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */,
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */, 30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */,
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */, 305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */,
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */, 55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */,

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "clock@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "clock@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "pin@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "pin@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "logo@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "logo@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "icon@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "icon@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,8 @@
{
"images" : [
{ "idiom" : "universal", "scale" : "1x" },
{ "filename" : "icon@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "icon@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "warning@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "warning@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

View File

@ -29,6 +29,8 @@ extension DefaultsKeys {
var unlockCountTotal: DefaultsKey<Int> { .init("unlockCountTotal", defaultValue: 0) } var unlockCountTotal: DefaultsKey<Int> { .init("unlockCountTotal", defaultValue: 0) }
/// yyyy-MM-dd /// yyyy-MM-dd
var unlockCountDate: DefaultsKey<String> { .init("unlockCountDate", defaultValue: "") } var unlockCountDate: DefaultsKey<String> { .init("unlockCountDate", defaultValue: "") }
/// key = yyyy-MM-dd
var unlockCountByDay: DefaultsKey<[String: Int]> { .init("unlockCountByDay", defaultValue: [:]) }
/// ///
var unlockWasLocked: DefaultsKey<Bool> { .init("unlockWasLocked", defaultValue: false) } var unlockWasLocked: DefaultsKey<Bool> { .init("unlockWasLocked", defaultValue: false) }
var unlockLastCountedAt: DefaultsKey<Date?> { .init("unlockLastCountedAt") } var unlockLastCountedAt: DefaultsKey<Date?> { .init("unlockLastCountedAt") }

View File

@ -0,0 +1,101 @@
//
// UIDevice+Extension.swift
// QuickLocation
//
import UIKit
extension UIDevice {
/// `iPhone17,1`
static var machineIdentifier: String {
var systemInfo = utsname()
uname(&systemInfo)
return withUnsafePointer(to: &systemInfo.machine.0) { ptr in
String(cString: ptr)
}
}
/// `iPhone 16 Pro`退 `iPhone`
static var modelName: String {
deviceModelMap[machineIdentifier] ?? "iPhone"
}
/// `nil`
static var batteryPercent: Int? {
let device = UIDevice.current
if !device.isBatteryMonitoringEnabled {
device.isBatteryMonitoringEnabled = true
}
let level = device.batteryLevel
guard level >= 0 else { return nil }
return Int((level * 100).rounded())
}
/// /
static var isBatteryCharging: Bool {
let device = UIDevice.current
if !device.isBatteryMonitoringEnabled {
device.isBatteryMonitoringEnabled = true
}
switch device.batteryState {
case .charging, .full:
return true
default:
return false
}
}
private static let deviceModelMap: [String: String] = [
"iPhone10,1": "iPhone 8",
"iPhone10,4": "iPhone 8",
"iPhone10,2": "iPhone 8 Plus",
"iPhone10,5": "iPhone 8 Plus",
// iPhone X
"iPhone10,3": "iPhone X",
"iPhone10,6": "iPhone X",
"iPhone11,2": "iPhone XS",
"iPhone11,4": "iPhone XS Max",
"iPhone11,6": "iPhone XS Max",
"iPhone11,8": "iPhone XR",
// iPhone11
"iPhone12,1": "iPhone 11",
"iPhone12,3": "iPhone 11 Pro",
"iPhone12,5": "iPhone 11 Pro Max",
// iPhone12
"iPhone13,1": "iPhone 12 mini",
"iPhone13,2": "iPhone 12",
"iPhone13,3": "iPhone 12 Pro",
"iPhone13,4": "iPhone 12 Pro Max",
// iPhone13
"iPhone14,4": "iPhone 13 mini",
"iPhone14,5": "iPhone 13",
"iPhone14,2": "iPhone 13 Pro",
"iPhone14,3": "iPhone 13 Pro Max",
// iPhone14
"iPhone14,7": "iPhone 14",
"iPhone14,8": "iPhone 14 Plus",
"iPhone15,2": "iPhone 14 Pro",
"iPhone15,3": "iPhone 14 Pro Max",
// iPhone15
"iPhone15,4": "iPhone 15",
"iPhone15,5": "iPhone 15 Plus",
"iPhone16,1": "iPhone 15 Pro",
"iPhone16,2": "iPhone 15 Pro Max",
// iPhone16
"iPhone17,1": "iPhone 16 Pro",
"iPhone17,2": "iPhone 16 Pro Max",
"iPhone17,3": "iPhone 16",
"iPhone17,4": "iPhone 16 Plus",
"iPhone17,5": "iPhone 16e",
// iPhone17 2025
"iPhone18,1": "iPhone 17 Pro",
"iPhone18,2": "iPhone 17 Pro Max",
"iPhone18,3": "iPhone 17",
"iPhone18,4": "iPhone Air",
// Simulator
"i386": "Simulator",
"x86_64": "Simulator",
"arm64": "Simulator"
]
}

View File

@ -9,7 +9,7 @@ final class MainTabBarController: UITabBarController {
// MARK: - View Controllers // MARK: - View Controllers
private lazy var locationNav = BaseNavigationController(rootViewController: HomeViewController()) private lazy var locationNav = BaseNavigationController(rootViewController: HomeViewController())
private lazy var exploreNav = BaseNavigationController(rootViewController: ScheduleVC()) private lazy var exploreNav = BaseNavigationController(rootViewController: FeatureIntroVC())
private lazy var circlesNav = BaseNavigationController(rootViewController: GroupViewController()) private lazy var circlesNav = BaseNavigationController(rootViewController: GroupViewController())
private lazy var mineNav = BaseNavigationController(rootViewController: MineViewController()) private lazy var mineNav = BaseNavigationController(rootViewController: MineViewController())
@ -93,6 +93,14 @@ final class MainTabBarController: UITabBarController {
func setTabBarHidden(_ hidden: Bool) { func setTabBarHidden(_ hidden: Bool) {
customTabBar.isHidden = hidden customTabBar.isHidden = hidden
} }
/// Tab customTabBar UITabBarController.selectedIndex
func selectTab(at index: Int) {
guard index >= 0, index < navControllers.count else { return }
customTabBar.setSelectedIndex(index)
selectedIndex = index
updateTabBarVisibility()
}
} }
// MARK: - QuickLocationTabBarDelegate // MARK: - QuickLocationTabBarDelegate

View File

@ -10,6 +10,7 @@ import Moya
import SwiftyJSON import SwiftyJSON
import RxSwift import RxSwift
import CommonCrypto import CommonCrypto
import UIKit
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
import GeYanSdk import GeYanSdk
#endif #endif
@ -56,12 +57,7 @@ extension ApiManager {
headers["x-version"] = kAppShortVersion headers["x-version"] = kAppShortVersion
headers["x-device-id"] = ApiManager.getMD5ID() headers["x-device-id"] = ApiManager.getMD5ID()
// //
var systemInfo = utsname() headers["x-mobile-model"] = UIDevice.machineIdentifier
uname(&systemInfo)
let platform = withUnsafePointer(to: &systemInfo.machine.0) { ptr in
return String(cString: ptr)
}
headers["x-mobile-model"] = platform
if let loginToken = Defaults[\.loginToken] { if let loginToken = Defaults[\.loginToken] {
headers["x-token"] = loginToken headers["x-token"] = loginToken

View File

@ -79,6 +79,16 @@ enum Route: String {
case todayTrackDetail = "todayTrackDetail" case todayTrackDetail = "todayTrackDetail"
/// ///
case phoneReportDetail = "phoneReportDetail" case phoneReportDetail = "phoneReportDetail"
///
case about = "about"
///
case memberInfo = "memberInfo"
///
case cancelAccount = "cancelAccount"
/// /
case signIn = "signIn"
/// SOS
case sos = "sos"
/// ///
case promotionalActivities = "promotionalActivities" case promotionalActivities = "promotionalActivities"
} }
@ -362,6 +372,31 @@ extension AppRouter: AppRouterProtocol {
AppRouter.register(Route.phoneReportDetail) { url, parameters in AppRouter.register(Route.phoneReportDetail) { url, parameters in
PhoneReportDetailVC(userInfo: parameters) PhoneReportDetailVC(userInfo: parameters)
} }
// MARK: -
AppRouter.register(Route.about) { _, _ in
AboutVC()
}
// MARK: -
AppRouter.register(Route.memberInfo) { _, parameters in
MemberInfoVC(userInfo: parameters)
}
// MARK: -
AppRouter.register(Route.cancelAccount) { _, _ in
CancelAccountVC()
}
// MARK: -
AppRouter.register(Route.signIn) { _, _ in
SignInVC(lastLocation: nil)
}
// MARK: - SOS
AppRouter.register(Route.sos) { _, _ in
SOSViewController()
}
// MARK: - // MARK: -
AppRouter.register(Route.promotionalActivities) { url, parameters in AppRouter.register(Route.promotionalActivities) { url, parameters in

View File

@ -187,10 +187,18 @@ final class UnlockCountManager: NSObject {
private func rollDayIfNeeded() -> Bool { private func rollDayIfNeeded() -> Bool {
migrateScreenTimeStorageIfNeeded() migrateScreenTimeStorageIfNeeded()
let today = todayString() let today = todayString()
guard Defaults[\.unlockCountDate] != today else { return false } let previousDate = Defaults[\.unlockCountDate]
guard previousDate != today else { return false }
flushScreenSession() flushScreenSession()
//
if !previousDate.isEmpty {
var byDay = Defaults[\.unlockCountByDay]
byDay[previousDate] = Defaults[\.unlockCountToday]
Defaults[\.unlockCountByDay] = byDay
}
Defaults[\.unlockCountDate] = today Defaults[\.unlockCountDate] = today
Defaults[\.unlockCountToday] = 0 Defaults[\.unlockCountToday] = 0
Defaults[\.appUsageToday] = 0 Defaults[\.appUsageToday] = 0
@ -308,6 +316,9 @@ final class UnlockCountManager: NSObject {
rollDayIfNeeded() rollDayIfNeeded()
Defaults[\.unlockCountToday] = Defaults[\.unlockCountToday] + 1 Defaults[\.unlockCountToday] = Defaults[\.unlockCountToday] + 1
Defaults[\.unlockCountTotal] = Defaults[\.unlockCountTotal] + 1 Defaults[\.unlockCountTotal] = Defaults[\.unlockCountTotal] + 1
var byDay = Defaults[\.unlockCountByDay]
byDay[todayString()] = Defaults[\.unlockCountToday]
Defaults[\.unlockCountByDay] = byDay
Defaults[\.unlockLastCountedAt] = now Defaults[\.unlockLastCountedAt] = now
Defaults[\.unlockWasLocked] = false Defaults[\.unlockWasLocked] = false
isSessionLocked = false isSessionLocked = false

View File

@ -0,0 +1,52 @@
//
// FeatureIntroVC.swift
// QuickLocation
//
import UIKit
import CoreLocation
import SwiftyUserDefaults
final class FeatureIntroVC: BaseViewController {
override var isNavigationBarHidden: Bool { true }
private var rootView: FeatureIntroView!
override func loadView() {
rootView = FeatureIntroView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.onTapItem = { [weak self] action in
self?.handle(action)
}
}
private func handle(_ action: FeatureIntroAction) {
switch action {
case .signIn:
let vc = SignInVC(lastLocation: savedLastLocation)
vc.isNeedLogin = true
AppRouter.push(vc)
case .createBubble:
AppRouter.push(Route.createBubble)
case .searchLocation:
AppRouter.push(Route.searchLocation)
case .sos:
let vc = SOSViewController()
vc.isNeedLogin = true
AppRouter.push(vc)
case .placeholder:
DLToast.show(text: "敬请期待")
}
}
private var savedLastLocation: CLLocation? {
guard let lat = Defaults[\.currentLatitude],
let lon = Defaults[\.currentLongitude] else { return nil }
return CLLocation(latitude: lat, longitude: lon)
}
}

View File

@ -0,0 +1,193 @@
//
// FeatureIntroView.swift
// QuickLocation
//
import UIKit
struct FeatureIntroItem {
let title: String
let subtitle: String
let background: UIColor
let titleColor: UIColor
let subtitleColor: UIColor
let arrowTint: UIColor
let action: FeatureIntroAction
}
enum FeatureIntroAction {
case signIn
case createBubble
case searchLocation
case sos
case placeholder
}
final class FeatureIntroView: UIView {
var onTapItem: ((FeatureIntroAction) -> Void)?
private let headerTitleLab = UILabel()
private let headerSubLab = UILabel()
private let sheet = UIView()
private let grid = UIStackView()
private let items: [FeatureIntroItem] = [
FeatureIntroItem(
title: "还在吗?",
subtitle: "每日打卡签到 记录美好生活",
background: UIColor(hexStr: "#EDE7FF"),
titleColor: UIColor(hexStr: "#5B4B8A"),
subtitleColor: UIColor(hexStr: "#8B7BB8"),
arrowTint: UIColor(hexStr: "#7B6AAE"),
action: .signIn
),
FeatureIntroItem(
title: "隐身气泡",
subtitle: "只想做个淡人 默默隐身",
background: UIColor(hexStr: "#D9ECFF"),
titleColor: UIColor(hexStr: "#2F6FAE"),
subtitleColor: UIColor(hexStr: "#6A9BC4"),
arrowTint: UIColor(hexStr: "#3E8FD0"),
action: .createBubble
),
FeatureIntroItem(
title: "一键锁机",
subtitle: "专注当下 拒绝手机干扰",
background: UIColor(hexStr: "#DDF5E8"),
titleColor: UIColor(hexStr: "#2F8A5B"),
subtitleColor: UIColor(hexStr: "#6AAD88"),
arrowTint: UIColor(hexStr: "#3EAE72"),
action: .placeholder
),
FeatureIntroItem(
title: "飞鸽传书",
subtitle: "实时查看位置 守护安全出行",
background: UIColor(hexStr: "#FFF3D6"),
titleColor: UIColor(hexStr: "#A67C2D"),
subtitleColor: UIColor(hexStr: "#C4A46A"),
arrowTint: UIColor(hexStr: "#C9953A"),
action: .placeholder
),
FeatureIntroItem(
title: "查位置",
subtitle: "实时查看位置 守护安全出行",
background: UIColor(hexStr: "#D6F4F2"),
titleColor: UIColor(hexStr: "#2A8A84"),
subtitleColor: UIColor(hexStr: "#66B0AB"),
arrowTint: UIColor(hexStr: "#35A8A0"),
action: .searchLocation
),
FeatureIntroItem(
title: "SOS",
subtitle: "紧急求助 一键呼叫",
background: UIColor(hexStr: "#FFE4E8"),
titleColor: UIColor(hexStr: "#D6455D"),
subtitleColor: UIColor(hexStr: "#E28A98"),
arrowTint: UIColor(hexStr: "#E2556C"),
action: .sos
)
]
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#F0F1F3")
setupUI()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupUI() {
headerTitleLab.text = "功能介绍"
headerTitleLab.font = .systemFont(ofSize: 28, weight: .bold)
headerTitleLab.textColor = UIColor(hexStr: "#1F2A44")
addSubview(headerTitleLab)
headerTitleLab.layoutChain.top(kStatusBarHeight + 20).left(20)
headerSubLab.text = "视频+图片"
headerSubLab.font = .systemFont(ofSize: 28, weight: .bold)
headerSubLab.textColor = UIColor(hexStr: "#1F2A44")
addSubview(headerSubLab)
headerSubLab.layoutChain.topToBottomOfView(headerTitleLab, offset: 2).left(20)
sheet.backgroundColor = .white
sheet.layer.cornerRadius = 28
sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
sheet.clipsToBounds = true
addSubview(sheet)
sheet.layoutChain
.topToBottomOfView(headerSubLab, offset: 24)
.edgesHorzontal()
.bottom()
grid.axis = .vertical
grid.spacing = 12
grid.distribution = .fillEqually
sheet.addSubview(grid)
grid.layoutChain
.top(24)
.edgesHorzontal(16)
.bottom(24 + 70) // leave room for custom tab bar
for row in 0..<3 {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.spacing = 12
rowStack.distribution = .fillEqually
for col in 0..<2 {
let item = items[row * 2 + col]
let card = FeatureIntroCardView(item: item)
card.addTarget(self, action: #selector(cardTapped(_:)), for: .touchUpInside)
card.tag = row * 2 + col
rowStack.addArrangedSubview(card)
}
grid.addArrangedSubview(rowStack)
}
}
@objc private func cardTapped(_ sender: FeatureIntroCardView) {
let idx = sender.tag
guard items.indices.contains(idx) else { return }
onTapItem?(items[idx].action)
}
}
final class FeatureIntroCardView: UIControl {
private let titleLab = UILabel()
private let subLab = UILabel()
private let arrowBtn = UIView()
init(item: FeatureIntroItem) {
super.init(frame: .zero)
backgroundColor = item.background
layer.cornerRadius = 20
clipsToBounds = true
titleLab.text = item.title
titleLab.font = .systemFont(ofSize: 18, weight: .bold)
titleLab.textColor = item.titleColor
addSubview(titleLab)
titleLab.layoutChain.top(16).left(14).right(14)
subLab.text = item.subtitle
subLab.font = .systemFont(ofSize: 11, weight: .medium)
subLab.textColor = item.subtitleColor
subLab.numberOfLines = 2
addSubview(subLab)
subLab.layoutChain.topToBottomOfView(titleLab, offset: 6).left(14).right(14)
arrowBtn.backgroundColor = .white
arrowBtn.isUserInteractionEnabled = false
arrowBtn.layer.cornerRadius = 14
addSubview(arrowBtn)
arrowBtn.layoutChain.left(14).bottom(14).width(28).height(28)
let arrow = UIImageView(image: UIImage(systemName: "chevron.right"))
arrow.tintColor = item.arrowTint
arrow.contentMode = .scaleAspectFit
arrowBtn.addSubview(arrow)
arrow.layoutChain.center().width(10).height(12)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

View File

@ -27,8 +27,9 @@ class InviteJoinVC: BaseViewController {
super.viewDidLoad() super.viewDidLoad()
// //
rootView.shareBtn.rx.tap.subscribe(onNext: { [weak self] in rootView.shareBtn.rx.tap.subscribe(onNext: { _ in
self?.shareInviteCard() SharePopView.show()
// self?.shareInviteCard()
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
// //

View File

@ -0,0 +1,159 @@
//
// MemberInfoVC.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import ObjectMapper
import Kingfisher
final class MemberInfoVC: BaseViewController {
private var rootView: MemberInfoView!
private var members: [GroupMemberModel]
private var selectedUserId: String
private var groupModel: GroupModel?
init(members: [GroupMemberModel], selectedUserId: String, groupModel: GroupModel?) {
self.members = members
self.selectedUserId = selectedUserId
self.groupModel = groupModel
super.init(nibName: nil, bundle: nil)
}
convenience init(userInfo: [String: Any]) {
let rawMembers = userInfo["members"]
let membersJson: [[String: Any]]
if let arr = rawMembers as? [[String: Any]] {
membersJson = arr
} else if let arr = rawMembers as? [Any] {
membersJson = arr.compactMap { $0 as? [String: Any] }
} else {
membersJson = []
}
let members = membersJson.compactMap { GroupMemberModel(JSON: $0) }
let selected = (userInfo["userId"] as? String) ?? members.first?.user_id ?? ""
let group = (userInfo["groupJson"] as? [String: Any]).flatMap { GroupModel(JSON: $0) }
self.init(members: members, selectedUserId: selected, groupModel: group)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func loadView() {
rootView = MemberInfoView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.memberCV.dataSource = self
rootView.memberCV.delegate = self
bind()
refreshDetail()
rootView.memberCV.reloadData()
}
private func bind() {
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
rootView.switchGroupBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.switchGroup()
})
.disposed(by: disposeBag)
}
private func switchGroup() {
guard let groupModel else { return }
GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in
guard let self, let key = groupKey else { return }
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
.subscribe(onNext: { [weak self] _ in
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
self?.navigationController?.popViewController(animated: true)
})
.disposed(by: self.disposeBag)
}
}
private func refreshDetail() {
let member = members.first { $0.user_id == selectedUserId }
let isSelf = selectedUserId == AppContextManager.shared.userId
let joinText: String = {
guard let ts = member?.join_time, ts > 0 else { return "**" }
let date = Date(timeIntervalSince1970: TimeInterval(ts) / 1000)
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd"
return f.string(from: date)
}()
let circleName: String = {
guard let gm = groupModel else { return "共同圈子" }
let name = gm.groups.first(where: { $0.group_key == gm.default_group_key })?.name
?? gm.groups.first?.name
return (name?.isEmpty == false) ? name! : "共同圈子"
}()
if isSelf {
let sexText: String = {
switch AppContextManager.shared.sex {
case 1: return ""
case 2: return ""
default: return "未设置"
}
}()
rootView.renderRows([
MemberInfoRow(title: "头像", value: "", kind: .avatar, avatarURL: member?.head_pic) { [weak self] in
AppRouter.push(Route.myProfile)
_ = self
},
MemberInfoRow(title: "昵称", value: member?.nick_name ?? "", kind: .disclosure) {
AppRouter.push(Route.myProfile)
},
MemberInfoRow(title: "性别", value: sexText, kind: .disclosure) {
AppRouter.push(Route.myProfile)
},
MemberInfoRow(title: "添加时间", value: joinText, kind: .text),
MemberInfoRow(title: "共同圈子", value: circleName, kind: .tag)
])
} else {
rootView.renderRows([
MemberInfoRow(title: "昵称", value: member?.nick_name ?? "", kind: .text),
MemberInfoRow(title: "备注", value: member?.nick_name ?? "", kind: .disclosure) {
DLToast.show(text: "敬请期待")
},
MemberInfoRow(title: "关系", value: "未绑定", kind: .disclosure) {
DLToast.show(text: "敬请期待")
},
MemberInfoRow(title: "性别", value: "保密", kind: .text),
MemberInfoRow(title: "添加时间", value: joinText, kind: .text),
MemberInfoRow(title: "共同圈子", value: circleName, kind: .tag)
])
}
}
}
extension MemberInfoVC: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
members.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
let model = members[indexPath.item]
cell.configure(
model: model,
isCurrentUser: model.user_id == AppContextManager.shared.userId,
isSelected: model.user_id == selectedUserId
)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedUserId = members[indexPath.item].user_id
collectionView.reloadData()
refreshDetail()
}
}

View File

@ -0,0 +1,196 @@
//
// MemberInfoView.swift
// QuickLocation
//
import UIKit
import Kingfisher
final class MemberInfoView: UIView {
lazy var navView = BaseNavigationView(title: "成员信息")
let switchGroupBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = .white.withAlphaComponent(0.8)
btn.setTitle(" 切换圈子 ", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
btn.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
btn.cornerRadius = 8
return btn
}()
lazy var memberCV: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 61, height: 90)
layout.minimumLineSpacing = 12
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.showsHorizontalScrollIndicator = false
cv.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
cv.register(GroupMemberListCell.self)
cv.layer.cornerRadius = 20
return cv
}()
let scrollView = UIScrollView()
let contentView = UIView()
let detailCard = UIView()
private let rowsStack = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#F7F7F7")
setupUI()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupUI() {
addSubview(navView)
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
navView.addRightButton(switchGroupBtn)
addSubview(scrollView)
scrollView.layoutChain.topToBottomOfView(navView).edgesHorzontal().bottom()
scrollView.addSubview(contentView)
contentView.layoutChain.edges().widthToView(scrollView)
contentView.addSubview(memberCV)
memberCV.layoutChain.top(8).edgesHorzontal(15).height(100)
detailCard.backgroundColor = .white
detailCard.layer.cornerRadius = 16
contentView.addSubview(detailCard)
detailCard.layoutChain
.topToBottomOfView(memberCV, offset: 12)
.edgesHorzontal(15)
.bottom(24)
rowsStack.axis = .vertical
rowsStack.spacing = 0
detailCard.addSubview(rowsStack)
rowsStack.layoutChain.edges(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
}
func renderRows(_ rows: [MemberInfoRow]) {
rowsStack.arrangedSubviews.forEach {
rowsStack.removeArrangedSubview($0)
$0.removeFromSuperview()
}
for (i, row) in rows.enumerated() {
let view = MemberInfoRowView(row: row)
rowsStack.addArrangedSubview(view)
view.layoutChain.height(row.kind == .tag ? 56 : 52)
if i < rows.count - 1 {
let line = UIView()
line.backgroundColor = UIColor(hexStr: "#F2F2F2")
view.addSubview(line)
line.layoutChain.left(15).right(15).bottom(0).height(0.5)
}
}
}
}
enum MemberInfoRowKind {
case text
case avatar
case tag
case disclosure
}
struct MemberInfoRow {
let title: String
let value: String
let kind: MemberInfoRowKind
let action: (() -> Void)?
let avatarURL: String?
init(title: String, value: String, kind: MemberInfoRowKind = .text, avatarURL: String? = nil, action: (() -> Void)? = nil) {
self.title = title
self.value = value
self.kind = kind
self.avatarURL = avatarURL
self.action = action
}
}
final class MemberInfoRowView: UIControl {
private let titleLab = UILabel()
private let valueLab = UILabel()
private let arrow = UIImageView(image: UIImage(named: "Mine/arrow_right"))
private let avatar = UIImageView()
private let tagLab = UILabel()
private let actionHandler: (() -> Void)?
init(row: MemberInfoRow) {
self.actionHandler = row.action
super.init(frame: .zero)
titleLab.text = row.title
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#293445")
addSubview(titleLab)
titleLab.layoutChain.left(15).centerY()
valueLab.text = row.value
valueLab.font = .systemFont(ofSize: 14, weight: .medium)
valueLab.textColor = row.value == "未设置" || row.value == "未绑定" || row.value == "保密"
? UIColor(hexStr: "#AAAAAA")
: UIColor(hexStr: "#293445")
valueLab.textAlignment = .right
arrow.contentMode = .scaleAspectFit
avatar.contentMode = .scaleAspectFill
avatar.clipsToBounds = true
avatar.layer.cornerRadius = 14
tagLab.text = row.value
tagLab.font = .systemFont(ofSize: 12, weight: .medium)
tagLab.textColor = UIColor(hexStr: "#E8A045")
tagLab.backgroundColor = UIColor(hexStr: "#FFF3E0")
tagLab.textAlignment = .center
tagLab.layer.cornerRadius = 10
tagLab.clipsToBounds = true
switch row.kind {
case .text:
addSubview(valueLab)
valueLab.layoutChain.right(15).centerY().leftToRightOfView(titleLab, offset: 12)
case .disclosure:
addSubview(arrow)
arrow.layoutChain.right(15).centerY().width(12).height(12)
addSubview(valueLab)
valueLab.layoutChain.rightToLeftOfView(arrow, offset: 6).centerY().leftToRightOfView(titleLab, offset: 12)
case .avatar:
addSubview(arrow)
arrow.layoutChain.right(15).centerY().width(12).height(12)
addSubview(avatar)
avatar.layoutChain.rightToLeftOfView(arrow, offset: 8).centerY().width(28).height(28)
if let pic = row.avatarURL, !pic.isEmpty, let img = UIImage(named: "UserIcon/\(pic)") {
avatar.image = img
} else if let urlStr = row.avatarURL, let u = URL(string: urlStr), urlStr.hasPrefix("http") {
avatar.kf.setImage(with: u, placeholder: UIImage(named: "Common/default_avatar"))
} else {
avatar.image = UIImage(named: "Common/default_avatar")
}
case .tag:
addSubview(tagLab)
tagLab.layoutChain.right(15).centerY().height(20)
tagLab.setContentHuggingPriority(.required, for: .horizontal)
let pad = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
tagLab.layoutMargins = pad
// approximate width from text
let w = (row.value as NSString).size(withAttributes: [.font: tagLab.font!]).width + 20
tagLab.layoutChain.width(max(60, w))
}
if row.action != nil {
addTarget(self, action: #selector(tap), for: .touchUpInside)
}
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@objc private func tap() { actionHandler?() }
}

View File

@ -0,0 +1,230 @@
//
// BubbleHeroView.swift
// QuickLocation
//
import UIKit
/// +
final class BubbleHeroView: UIView {
private let bubbleContainer = UIView()
private let gradientLayer = CAGradientLayer()
private let bubbleMaskLayer = CAShapeLayer()
private let borderLayer = CAShapeLayer()
private let avatarView = UIImageView()
private let decoBubble1 = UIView()
private let decoBubble2 = UIView()
private var displayLink: CADisplayLink?
private var avatarCenter = CGPoint.zero
private var velocity = CGPoint(x: 42, y: 36)
private var avatarAngle: CGFloat = 0
private var lastTimestamp: CFTimeInterval = 0
private var sameDirectionBounceCount = 0
private var lastBounceNormal = CGPoint.zero
private var hasRandomizedMotion = false
private let avatarSize: CGFloat = 64
private let moveSpeed: CGFloat = 85
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = false
setupUI()
reloadAvatar()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
deinit { stopAnimation() }
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
if UIAccessibility.isReduceMotionEnabled {
stopAnimation()
resetAvatarCenter(randomize: false)
} else {
startAnimation()
}
} else {
stopAnimation()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let diameter = bounds.width
guard diameter > 0 else { return }
let bubbleFrame = CGRect(x: 0, y: 0, width: diameter, height: diameter)
bubbleContainer.frame = bubbleFrame
gradientLayer.frame = bubbleContainer.bounds
bubbleMaskLayer.path = UIBezierPath(ovalIn: bubbleContainer.bounds).cgPath
bubbleMaskLayer.frame = bubbleContainer.bounds
borderLayer.path = UIBezierPath(ovalIn: bubbleContainer.bounds).cgPath
borderLayer.frame = bubbleContainer.bounds
decoBubble1.frame = CGRect(x: bubbleFrame.minX + diameter * 0.12, y: bubbleFrame.maxY + 8, width: 32, height: 32)
decoBubble2.frame = CGRect(x: bubbleFrame.maxX - diameter * 0.22, y: bubbleFrame.maxY + 18, width: 20, height: 20)
decoBubble1.layer.cornerRadius = 16
decoBubble2.layer.cornerRadius = 10
if avatarCenter == .zero || !bubbleContainer.bounds.contains(avatarCenter) {
resetAvatarCenter(randomize: true)
}
layoutAvatar()
}
func reloadAvatar() {
let icon = AppContextManager.shared.avaterIcon
avatarView.image = icon.size.width > 0 ? icon : UIImage(named: "Common/default_avatar")
}
private func setupUI() {
bubbleContainer.clipsToBounds = true
addSubview(bubbleContainer)
gradientLayer.type = .radial
gradientLayer.colors = [
UIColor.white.cgColor,
UIColor.white.cgColor,
UIColor(hexStr: "#FFE8F2").cgColor,
UIColor(hexStr: "#FFC9EF").cgColor
]
gradientLayer.locations = [0, 0.58, 0.82, 1]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.46)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
bubbleContainer.layer.addSublayer(gradientLayer)
bubbleMaskLayer.fillColor = UIColor.white.cgColor
bubbleContainer.layer.mask = bubbleMaskLayer
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.white.withAlphaComponent(0.3).cgColor
borderLayer.lineWidth = 1
bubbleContainer.layer.addSublayer(borderLayer)
avatarView.contentMode = .scaleAspectFill
avatarView.clipsToBounds = true
avatarView.layer.cornerRadius = 16
avatarView.layer.borderWidth = 3
avatarView.layer.borderColor = UIColor.white.cgColor
bubbleContainer.addSubview(avatarView)
decoBubble1.backgroundColor = UIColor(hexStr: "#FFD6E8").withAlphaComponent(0.85)
decoBubble2.backgroundColor = UIColor(hexStr: "#FFD6E8").withAlphaComponent(0.65)
addSubview(decoBubble1)
addSubview(decoBubble2)
}
private func randomizeVelocity() {
let angle = CGFloat.random(in: 0...(2 * .pi))
velocity = CGPoint(x: cos(angle) * moveSpeed, y: sin(angle) * moveSpeed)
hasRandomizedMotion = true
}
private func normalizeVelocity() {
let speed = hypot(velocity.x, velocity.y)
guard speed > 0.001 else {
randomizeVelocity()
return
}
let scale = moveSpeed / speed
velocity.x *= scale
velocity.y *= scale
}
private func resetAvatarCenter(randomize: Bool) {
let bounds = bubbleContainer.bounds
guard bounds.width > 0 else { return }
let offsetRange = bounds.width * 0.08
avatarCenter = CGPoint(
x: bounds.midX + CGFloat.random(in: -offsetRange...offsetRange),
y: bounds.midY + CGFloat.random(in: -offsetRange...offsetRange)
)
if randomize {
avatarAngle = CGFloat.random(in: -0.21...0.21)
randomizeVelocity()
sameDirectionBounceCount = 0
lastBounceNormal = .zero
}
layoutAvatar()
}
private func layoutAvatar() {
avatarView.bounds = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize)
avatarView.center = avatarCenter
avatarView.transform = CGAffineTransform(rotationAngle: avatarAngle)
}
private func startAnimation() {
guard displayLink == nil else { return }
if !hasRandomizedMotion {
randomizeVelocity()
}
lastTimestamp = 0
let link = CADisplayLink(target: self, selector: #selector(tick(_:)))
link.add(to: .main, forMode: .common)
displayLink = link
}
private func stopAnimation() {
displayLink?.invalidate()
displayLink = nil
lastTimestamp = 0
}
@objc private func tick(_ link: CADisplayLink) {
guard bubbleContainer.bounds.width > 0 else { return }
let dt = lastTimestamp > 0 ? CGFloat(link.timestamp - lastTimestamp) : CGFloat(1.0 / 60.0)
lastTimestamp = link.timestamp
guard dt > 0, dt < 0.05 else { return }
let bubbleRadius = min(bubbleContainer.bounds.width, bubbleContainer.bounds.height) / 2
let maxDist = max(8, bubbleRadius - avatarSize / 2 - 4)
let center = CGPoint(x: bubbleContainer.bounds.midX, y: bubbleContainer.bounds.midY)
avatarCenter.x += velocity.x * dt
avatarCenter.y += velocity.y * dt
let offset = CGPoint(x: avatarCenter.x - center.x, y: avatarCenter.y - center.y)
let dist = hypot(offset.x, offset.y)
if dist > maxDist {
let nx = offset.x / max(dist, 0.001)
let ny = offset.y / max(dist, 0.001)
avatarCenter.x = center.x + nx * maxDist
avatarCenter.y = center.y + ny * maxDist
if abs(nx - lastBounceNormal.x) < 0.15 && abs(ny - lastBounceNormal.y) < 0.15 {
sameDirectionBounceCount += 1
} else {
sameDirectionBounceCount = 0
}
lastBounceNormal = CGPoint(x: nx, y: ny)
let dot = velocity.x * nx + velocity.y * ny
velocity.x = velocity.x - 2 * dot * nx
velocity.y = velocity.y - 2 * dot * ny
let tx = -ny
let ty = nx
let jitterSign: CGFloat = Bool.random() ? 1 : -1
let jitter = CGFloat.random(in: 16...26) * jitterSign
velocity.x += tx * jitter
velocity.y += ty * jitter
normalizeVelocity()
avatarAngle += CGFloat.random(in: -0.05...0.05)
if sameDirectionBounceCount >= 3 {
randomizeVelocity()
sameDirectionBounceCount = 0
lastBounceNormal = .zero
}
}
layoutAvatar()
}
}

View File

@ -0,0 +1,25 @@
//
// BubbleKnowledgeVC.swift
// QuickLocation
//
import UIKit
final class BubbleKnowledgeVC: BaseViewController {
override var isNavigationBarHidden: Bool { true }
private var rootView: BubbleKnowledgeView!
override func loadView() {
rootView = BubbleKnowledgeView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
}
}

View File

@ -0,0 +1,147 @@
//
// BubbleKnowledgeView.swift
// QuickLocation
//
import UIKit
final class BubbleKnowledgeView: UIView {
lazy var navView = BaseNavigationView(title: "气泡知识")
private let scrollView = UIScrollView()
private let contentView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#FAFAFA")
setupUI()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupUI() {
addSubview(navView)
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
addSubview(scrollView)
scrollView.layoutChain.topToBottomOfView(navView).edgesHorzontal().bottom()
scrollView.addSubview(contentView)
contentView.layoutChain.edges().widthToView(scrollView)
let sections: [(String, String, Bool)] = [
("1", "气泡基础知识", true),
("2", "气泡持续时间", false),
("3", "签到", false),
("4", "弹出您的气泡", false)
]
var previous: UIView?
for item in sections {
let block = makeSection(index: item.0, title: item.1, withFeatureList: item.2)
contentView.addSubview(block)
if let previous {
block.layoutChain.topToBottomOfView(previous, offset: 20).edgesHorzontal(16)
} else {
block.layoutChain.top(16).edgesHorzontal(16)
}
previous = block
}
previous?.layoutChain.bottom(24)
}
private func makeSection(index: String, title: String, withFeatureList: Bool) -> UIView {
let card = UIView()
card.backgroundColor = .white
card.layer.cornerRadius = 16
let badge = UILabel()
badge.text = index
badge.font = .systemFont(ofSize: 14, weight: .bold)
badge.textColor = .white
badge.textAlignment = .center
badge.backgroundColor = UIColor(hexStr: "#16B3FF")
badge.layer.cornerRadius = 8
badge.clipsToBounds = true
card.addSubview(badge)
badge.layoutChain.top(16).left(16).width(28).height(28)
let titleLab = UILabel()
titleLab.text = title
titleLab.font = .systemFont(ofSize: 16, weight: .bold)
titleLab.textColor = UIColor(hexStr: "#293445")
card.addSubview(titleLab)
titleLab.layoutChain.leftToRightOfView(badge, offset: 10).centerY(badge)
let body = UILabel()
body.numberOfLines = 0
body.font = .systemFont(ofSize: 14, weight: .medium)
body.textColor = UIColor(hexStr: "#767676")
card.addSubview(body)
body.layoutChain.topToBottomOfView(badge, offset: 12).edgesHorzontal(16)
switch index {
case "1":
body.text = "当您在气泡中时,圈子成员只能看到您创建气泡时的位置,而不是您的实时位置。安全功能仍然有效。"
case "2":
body.text = "气泡会在设定的时间到期,但您或圈子成员可以提前「弹出」气泡。"
case "3":
body.text = "虽然气泡有设定到期时间,但完成「签到」也会弹出气泡。"
case "4":
body.text = "点击「弹出气泡」可手动结束气泡,之后圈子将能看到您的精确位置。"
default:
break
}
var bottomAnchor: UIView = body
if withFeatureList {
let list = makeFeatureList()
card.addSubview(list)
list.layoutChain.topToBottomOfView(body, offset: 12).edgesHorzontal(16).bottom(16)
bottomAnchor = list
} else {
body.layoutChain.bottom(16)
}
_ = bottomAnchor
return card
}
private func makeFeatureList() -> UIView {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 0
let rows: [(String, String)] = [
("碰撞提醒", "开启"),
("SOS警报", "开启"),
("位置共享", "开启"),
("低电量通知", "关闭"),
("安全驾驶通知", "关闭"),
("位置通知", "关闭")
]
for (i, row) in rows.enumerated() {
let line = UIView()
let title = UILabel()
title.text = row.0
title.font = .systemFont(ofSize: 14, weight: .medium)
title.textColor = UIColor(hexStr: "#293445")
let status = UILabel()
status.text = row.1
status.font = .systemFont(ofSize: 14, weight: .medium)
status.textColor = UIColor(hexStr: "#9CA3AF")
line.addSubview(title)
line.addSubview(status)
title.layoutChain.left().centerY().top(12).bottom(12)
status.layoutChain.right().centerY()
stack.addArrangedSubview(line)
if i < rows.count - 1 {
let sep = UIView()
sep.backgroundColor = UIColor(hexStr: "#F2F2F2")
sep.layoutChain.height(0.5)
stack.addArrangedSubview(sep)
}
}
return stack
}
}

View File

@ -2,8 +2,6 @@
// CreateBubbleDoneView.swift // CreateBubbleDoneView.swift
// QuickLocation // QuickLocation
// //
// Created by on 2026/6/26.
//
import UIKit import UIKit
import RxSwift import RxSwift
@ -13,9 +11,15 @@ class CreateBubbleDoneView: UIView {
var disposeBag = DisposeBag() var disposeBag = DisposeBag()
let heroView = BubbleHeroView()
let popupBtn = UIButton(type: .custom)
private var countdownTimer: Timer? private var countdownTimer: Timer?
private var endDate: Date = Date() private var endDate: Date = Date()
private let clockImage = UIImageView()
private let timeLab = UILabel()
func startCountdown(endDate: Date) { func startCountdown(endDate: Date) {
self.endDate = endDate self.endDate = endDate
countdownTimer?.invalidate() countdownTimer?.invalidate()
@ -38,150 +42,51 @@ class CreateBubbleDoneView: UIView {
let seconds = Int(remaining) % 60 let seconds = Int(remaining) % 60
timeLab.text = String(format: "%02d:%02d:%02d", hours, minutes, seconds) timeLab.text = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
} }
private func setupRx() {
}
private func setupUI() { private func setupUI() {
addSubview(titleLab) addSubview(heroView)
addSubview(timeIcon) addSubview(clockImage)
addSubview(timeLab) clockImage.addSubview(timeLab)
addSubview(messageView)
addSubview(iconView)
addSubview(scrollView)
addSubview(popupBtn) addSubview(popupBtn)
titleLab.layoutChain heroView.layoutChain
.top(15) .top(33)
.leading(30)
.trailing(30)
.heightToWidth(1)
clockImage.image = UIImage(named: "Bubble/clock")
clockImage.contentMode = .scaleAspectFit
clockImage.layoutChain
.topToBottomOfView(heroView, offset: 8)
.centerX() .centerX()
.width(200)
timeLab.layoutChain .height(120)
.topToBottomOfView(titleLab, offset: 12)
.centerX() timeLab.font = .systemFont(ofSize: 28, weight: .heavy)
timeLab.textColor = .white
timeIcon.layoutChain timeLab.textAlignment = .center
.rightToLeftOfView(timeLab, offset: -8) timeLab.layoutChain.centerX(clockImage).centerY(clockImage, offset: 4)
.centerY(timeLab)
popupBtn.setTitle("弹出气泡", for: .normal)
messageView.layoutChain popupBtn.setTitleColor(.white, for: .normal)
.topToBottomOfView(timeLab, offset: 25) popupBtn.titleLabel?.font = FontManager.boboBold(18)
.left(79) popupBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
.right(30) popupBtn.layer.cornerRadius = 16
popupBtn.layoutChain.left(20).right(20).bottom(34).height(56)
iconView.layoutChain
.rightToLeftOfView(messageView, offset: -9)
.bottomToView(messageView)
.height(40)
.width(40)
popupBtn.layoutChain
.width(240)
.height(50)
.centerX()
.bottom(kSafeBottomMargin + 20)
scrollView.layoutChain
.topToBottomOfView(messageView, offset: 20)
.edgesHorzontal()
.bottomToTopOfView(popupBtn, offset: -20)
} }
lazy var titleLab: UILabel = {
let label = UILabel()
label.text = "您的泡将在以下时间弹出:"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.numberOfLines = 0
return label
}()
lazy var popupBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("弹出气泡", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25
return btn
}()
lazy var timeIcon: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Bubble/time")
return view
}()
lazy var timeLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 20, weight: .bold)
label.textColor = ThemeManager.shared.color.titleAuxColor
return label
}()
lazy var iconView: UIImageView = {
let view = UIImageView(image: UIImage(named: "UserIcon/7"))
view.cornerRadius = 20
return view
}()
lazy var messageView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EBF6F9")
view.cornerRadius = 10
view.addSubview(messageLab)
messageLab.layoutChain
.edgesHorzontal(18)
.edgesVertical(15)
return view
}()
lazy var messageLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.numberOfLines = 0
return label
}()
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.backgroundColor = .clear
view.showsVerticalScrollIndicator = false
view.bounces = false
let contentView = UIView()
contentView.backgroundColor = .clear
view.addSubview(contentView)
contentView.layoutChain.edges().widthToView(view)
let textImgView = UIImageView()
textImgView.image = UIImage(named: "Bubble/text")
contentView.addSubview(textImgView)
textImgView.layoutChain
.edgesVertical()
.edgesHorzontal(30)
.heightToWidth(1450/630)
return view
}()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: .zero) super.init(frame: .zero)
self.isHidden = true
backgroundColor = .white backgroundColor = .white
setupUI() setupUI()
setupRx()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit { deinit {
countdownTimer?.invalidate() countdownTimer?.invalidate()
} }
} }

View File

@ -0,0 +1,289 @@
//
// CreateBubbleSetupView.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import SwiftDate
/// picker + sheet
final class CreateBubbleSetupView: UIView {
var disposeBag = DisposeBag()
let selectedHour = BehaviorRelay<Int>(value: 1)
var messageText: String = ""
var onConfirmCreate: (() -> Void)?
var onSheetVisibilityChanged: ((Bool) -> Void)?
private let hours = Array(1...6)
let heroView = BubbleHeroView()
let confirmBtn = UIButton(type: .custom)
let sheetConfirmBtn = UIButton(type: .custom)
private let subtitleLab = UILabel()
private let titleLab = UILabel()
private let pickerView = UIPickerView()
private let hourLab = UILabel()
private let sheetOverlay = UIView()
private let sheetPanel = UIView()
private let pinIcon = UIImageView()
private let sheetTitleLab = UILabel()
private let messageView = UIView()
private let messageLab = UILabel()
private let previewAvatar = UIImageView()
private let tipStack = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setupUI()
setupPicker()
setupSheet()
pickerView.selectRow(0, inComponent: 0, animated: false)
selectedHour.accept(hours[0])
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func showNotifySheet(message: String) {
messageText = message
messageLab.text = message
sheetOverlay.isHidden = false
sheetOverlay.alpha = 0
sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
onSheetVisibilityChanged?(true)
UIView.animate(withDuration: 0.28, delay: 0, options: .curveEaseOut) {
self.sheetOverlay.alpha = 1
self.sheetPanel.transform = .identity
}
}
func dismissNotifySheet() {
onSheetVisibilityChanged?(false)
UIView.animate(withDuration: 0.22, delay: 0, options: .curveEaseIn) {
self.sheetOverlay.alpha = 0
self.sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
} completion: { _ in
self.sheetOverlay.isHidden = true
}
}
var isSheetVisible: Bool { !sheetOverlay.isHidden && sheetOverlay.alpha > 0.01 }
private func setupUI() {
addSubview(heroView)
addSubview(titleLab)
addSubview(subtitleLab)
addSubview(pickerView)
addSubview(hourLab)
addSubview(confirmBtn)
heroView.layoutChain
.top(33)
.leading(30)
.trailing(30)
.heightToWidth(1)
titleLab.text = "您会在气泡待多久?"
titleLab.font = FontManager.boboBold(30)
titleLab.textColor = UIColor(hexStr: "#293445")
titleLab.textAlignment = .center
titleLab.layoutChain.topToBottomOfView(heroView, offset: 25).centerX()
subtitleLab.text = "设置气泡时间"
subtitleLab.font = .systemFont(ofSize: 18, weight: .bold)
subtitleLab.textColor = UIColor(hexStr: "#9CA3AF")
subtitleLab.layoutChain.topToBottomOfView(titleLab, offset: 4).centerX()
pickerView.layoutChain
.topToBottomOfView(subtitleLab, offset: 12)
.edgesHorzontal(15)
.height(180)
hourLab.text = "小时"
hourLab.font = .systemFont(ofSize: 16, weight: .medium)
hourLab.textColor = UIColor(hexStr: "#16B3FF")
hourLab.layoutChain.centerX(self, offset: 100).centerY(pickerView)
confirmBtn.setTitle("确定", for: .normal)
confirmBtn.setTitleColor(.white, for: .normal)
confirmBtn.titleLabel?.font = FontManager.boboBold(18)
confirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
confirmBtn.layer.cornerRadius = 16
confirmBtn.layoutChain.left(20).right(20).bottom(34).height(56)
}
private func setupPicker() {
pickerView.delegate = self
pickerView.dataSource = self
pickerView.backgroundColor = .clear
}
override func layoutSubviews() {
super.layoutSubviews()
clearPickerSelectionBackground()
}
private func clearPickerSelectionBackground() {
pickerView.subviews.forEach { sub in
sub.backgroundColor = .clear
if sub.frame.height <= 1 {
sub.isHidden = true
}
}
}
private func setupSheet() {
sheetOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.45)
sheetOverlay.isHidden = true
addSubview(sheetOverlay)
sheetOverlay.layoutChain.edges()
sheetPanel.backgroundColor = .white
sheetPanel.layer.cornerRadius = 24
sheetPanel.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
sheetOverlay.addSubview(sheetPanel)
sheetPanel.layoutChain.edgesHorzontal().bottom()
pinIcon.image = UIImage(named: "Bubble/pin")
pinIcon.contentMode = .scaleAspectFit
sheetPanel.addSubview(pinIcon)
pinIcon.layoutChain.top(-22).centerX().width(44).height(44)
sheetTitleLab.text = "通知您的圈子"
sheetTitleLab.font = .systemFont(ofSize: 18, weight: .bold)
sheetTitleLab.textColor = UIColor(hexStr: "#293445")
sheetTitleLab.textAlignment = .center
sheetPanel.addSubview(sheetTitleLab)
sheetTitleLab.layoutChain.top(28).centerX()
messageView.backgroundColor = UIColor(hexStr: "#EBF6F9")
messageView.layer.cornerRadius = 12
sheetPanel.addSubview(messageView)
messageView.addSubview(messageLab)
messageLab.font = .systemFont(ofSize: 15, weight: .medium)
messageLab.textColor = UIColor(hexStr: "#293445")
messageLab.numberOfLines = 0
messageLab.layoutChain.edgesHorzontal(16).edgesVertical(14)
previewAvatar.contentMode = .scaleAspectFill
previewAvatar.clipsToBounds = true
previewAvatar.layer.cornerRadius = 18
previewAvatar.layer.borderWidth = 2
previewAvatar.layer.borderColor = UIColor.white.cgColor
let avatar = AppContextManager.shared.avaterIcon
previewAvatar.image = avatar.size.width > 0 ? avatar : UIImage(named: "Common/default_avatar")
sheetPanel.addSubview(previewAvatar)
messageView.layoutChain
.topToBottomOfView(sheetTitleLab, offset: 20)
.left(24)
.right(72)
previewAvatar.layoutChain
.right(24)
.bottomToView(messageView)
.width(40)
.height(40)
tipStack.axis = .vertical
tipStack.spacing = 12
sheetPanel.addSubview(tipStack)
tipStack.layoutChain
.topToBottomOfView(messageView, offset: 16)
.edgesHorzontal(24)
[
"您的圈子成员只会看到您在气泡中。",
"当您在气泡中,将不会共享您的确切位置。"
].forEach { tipStack.addArrangedSubview(makeTipRow($0)) }
sheetConfirmBtn.setTitle("确定", for: .normal)
sheetConfirmBtn.setTitleColor(.white, for: .normal)
sheetConfirmBtn.titleLabel?.font = FontManager.boboBold(18)
sheetConfirmBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
sheetConfirmBtn.layer.cornerRadius = 16
sheetPanel.addSubview(sheetConfirmBtn)
sheetConfirmBtn.layoutChain
.topToBottomOfView(tipStack, offset: 20)
.edgesHorzontal(20)
.height(56)
.bottom(24 + kSafeBottomMargin)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask))
sheetOverlay.addGestureRecognizer(tap)
}
private func makeTipRow(_ text: String) -> UIView {
let row = UIView()
let star = UIImageView(image: UIImage(systemName: "sparkle"))
star.tintColor = UIColor(hexStr: "#16B3FF")
star.contentMode = .scaleAspectFit
let lab = UILabel()
lab.text = text
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textColor = UIColor(hexStr: "#767676")
lab.numberOfLines = 0
row.addSubview(star)
row.addSubview(lab)
star.layoutChain.left().top(2).width(14).height(14)
lab.layoutChain.leftToRightOfView(star, offset: 8).right().top().bottom()
return row
}
@objc private func tapMask(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: sheetPanel)
if sheetPanel.bounds.contains(point) { return }
dismissNotifySheet()
}
func buildMessage(for hours: Int) -> String {
let endDate = Calendar.current.date(byAdding: .hour, value: hours, to: Date()) ?? Date()
let timeStr = endDate.toFormat("HH:mm")
return "\(timeStr)之前,我会一直在这个区域。如果需要我帮忙,请给我发消息。"
}
}
extension CreateBubbleSetupView: UIPickerViewDataSource, UIPickerViewDelegate {
func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
hours.count
}
func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { 44 }
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let container = view as? UIView ?? UIView()
container.subviews.forEach { $0.removeFromSuperview() }
let label = UILabel()
label.textAlignment = .center
label.font = .systemFont(ofSize: 20, weight: .medium)
let selected = row == pickerView.selectedRow(inComponent: component)
label.textColor = selected ? UIColor(hexStr: "#16B3FF") : UIColor(hexStr: "#999999")
label.text = "\(hours[row])"
container.addSubview(label)
label.layoutChain.center().width(60).height(36)
if selected {
let capsule = UIView()
capsule.backgroundColor = UIColor(hexStr: "#E3F6FF")
capsule.layer.cornerRadius = 14
container.insertSubview(capsule, at: 0)
capsule.layoutChain.center().width(130).height(36)
}
return container
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedHour.accept(hours[row])
pickerView.reloadAllComponents()
clearPickerSelectionBackground()
}
}

View File

@ -2,8 +2,6 @@
// CreateBubbleVC.swift // CreateBubbleVC.swift
// QuickLocation // QuickLocation
// //
// Created by on 2026/6/26.
//
import UIKit import UIKit
import RxSwift import RxSwift
@ -18,58 +16,85 @@ class CreateBubbleVC: BaseViewController {
rootView = CreateBubbleView(frame: UIScreen.main.bounds) rootView = CreateBubbleView(frame: UIScreen.main.bounds)
view = rootView view = rootView
} }
private var doneBtn: UIButton {
rootView.createBubbleTipsView.doneBtn
}
private var popupBtn: UIButton {
rootView.createBubbleDoneView.popupBtn
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
bind()
doneBtn.rx.tap.subscribe(onNext: { [weak self] in restoreActiveBubbleIfNeeded()
guard let self = self else { return }
guard let systemConfig = AppContextManager.shared.systemConfig, systemConfig.isIntercept == false else {
let hours = self.rootView.createBubbleTiemView.selectedHour.value
requestSetBubble(enable: true, keep_time: hours)
return
}
if AppContextManager.shared.vip > 1 {
let hours = self.rootView.createBubbleTiemView.selectedHour.value
requestSetBubble(enable: true, keep_time: hours)
}
else {
CreateBubblePopView.show()
}
}).disposed(by: disposeBag)
popupBtn.rx.tap.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.showConfirmPop(title: "温馨提醒",
message: "您确定要弹出这个气泡吗?",
confirmText: "爆裂",
confirmBlock: {
self.requestSetBubble(enable: false, keep_time: -1)
}, cancelText: "取消")
}).disposed(by: disposeBag)
guard AppContextManager.shared.status == 2, let endDate = Defaults[\.bubbleKeepTimes] else { return }
self.rootView.navView.titleLabel.text = "活动气泡"
self.rootView.createBubbleDoneView.messageLab.text = self.rootView.createBubbleTipsView.messageText
self.rootView.createBubbleTipsView.isHidden = true
self.rootView.createBubbleDoneView.isHidden = false
self.rootView.createBubbleDoneView.startCountdown(endDate: endDate)
} }
private func bind() {
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
rootView.moreBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.navigationController?.pushViewController(BubbleKnowledgeVC(), animated: true)
})
.disposed(by: disposeBag)
rootView.setupView.confirmBtn.rx.tap
.subscribe(onNext: { [weak self] in
guard let self else { return }
let hours = self.rootView.setupView.selectedHour.value
let message = self.rootView.setupView.buildMessage(for: hours)
self.rootView.setupView.showNotifySheet(message: message)
})
.disposed(by: disposeBag)
rootView.setupView.onSheetVisibilityChanged = { [weak self] visible in
self?.rootView.updateNavTitleForSheet(visible)
}
rootView.setupView.sheetConfirmBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.submitCreate()
})
.disposed(by: disposeBag)
rootView.doneView.popupBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.showConfirmPop(
title: "温馨提醒",
message: "您确定要弹出这个气泡吗?",
confirmText: "爆裂",
confirmBlock: { [weak self] in
self?.requestSetBubble(enable: false, keep_time: -1)
},
cancelText: "取消"
)
})
.disposed(by: disposeBag)
}
private func submitCreate() {
guard let systemConfig = AppContextManager.shared.systemConfig, systemConfig.isIntercept == false else {
let hours = rootView.setupView.selectedHour.value
requestSetBubble(enable: true, keep_time: hours)
return
}
if AppContextManager.shared.vip > 1 {
let hours = rootView.setupView.selectedHour.value
requestSetBubble(enable: true, keep_time: hours)
} else {
CreateBubblePopView.show()
}
}
private func restoreActiveBubbleIfNeeded() {
guard AppContextManager.shared.status == 2,
let endDate = Defaults[\.bubbleKeepTimes] else { return }
rootView.showDone(animated: false)
rootView.doneView.startCountdown(endDate: endDate)
}
private func requestSetBubble(enable: Bool, keep_time: Int) { private func requestSetBubble(enable: Bool, keep_time: Int) {
DLToast.showLoading() DLToast.showLoading()
UserService.setBubble(enable: enable, keep_time: keep_time).subscribe(onNext: { response in UserService.setBubble(enable: enable, keep_time: keep_time).subscribe(onNext: { [weak self] _ in
DLToast.dismiss() DLToast.dismiss()
guard let self else { return }
guard enable else { guard enable else {
DLToast.show(text: "气泡已弹出") { DLToast.show(text: "气泡已弹出") {
Defaults[\.bubbleKeepTimes] = nil Defaults[\.bubbleKeepTimes] = nil
@ -79,13 +104,11 @@ class CreateBubbleVC: BaseViewController {
return return
} }
AppContextManager.shared.updateAccountStatus(2) AppContextManager.shared.updateAccountStatus(2)
self.rootView.navView.titleLabel.text = "活动气泡" self.rootView.setupView.dismissNotifySheet()
self.rootView.createBubbleDoneView.messageLab.text = self.rootView.createBubbleTipsView.messageText
let endDate = Calendar.current.date(byAdding: .hour, value: keep_time, to: Date()) ?? Date() let endDate = Calendar.current.date(byAdding: .hour, value: keep_time, to: Date()) ?? Date()
Defaults[\.bubbleKeepTimes] = endDate Defaults[\.bubbleKeepTimes] = endDate
self.rootView.createBubbleDoneView.startCountdown(endDate: endDate) self.rootView.doneView.startCountdown(endDate: endDate)
self.rootView.createBubbleTipsView.isHidden = true self.rootView.showDone(animated: true)
self.rootView.createBubbleDoneView.isHidden = false }, onError: { _ in }).disposed(by: disposeBag)
}).disposed(by: disposeBag)
} }
} }

View File

@ -2,8 +2,6 @@
// CreateBubbleView.swift // CreateBubbleView.swift
// QuickLocation // QuickLocation
// //
// Created by on 2026/6/26.
//
import UIKit import UIKit
import RxSwift import RxSwift
@ -13,80 +11,83 @@ import SwiftDate
class CreateBubbleView: UIView { class CreateBubbleView: UIView {
var disposeBag = DisposeBag() var disposeBag = DisposeBag()
var createBubbleTiemView: CreateBubbleTiemView = CreateBubbleTiemView()
var createBubbleTipsView: CreateBubbleTipsView = CreateBubbleTipsView()
var createBubbleDoneView: CreateBubbleDoneView = CreateBubbleDoneView()
private func setupRx() {
createBubbleTiemView.onNextTap = { [weak self] in
guard let self = self else { return }
self.navView.titleLabel.text = "通知您的圈子"
let hours = self.createBubbleTiemView.selectedHour.value
let endDate = Calendar.current.date(byAdding: .hour, value: hours, to: Date()) ?? Date()
let timeStr = endDate.toFormat("HH:mm")
let message = "\(timeStr)之前,我会一直在这个区域。如果需要我帮忙,请给我发消息。"
self.createBubbleTipsView.messageLab.text = message
self.createBubbleTipsView.messageText = message
self.createBubbleTiemView.isHidden = true
self.createBubbleTipsView.isHidden = false
}
}
private func setupUI() {
addSubview(createBubbleTiemView)
addSubview(createBubbleTipsView)
addSubview(createBubbleDoneView)
addSubview(navBgView)
addSubview(navView)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
createBubbleTiemView.layoutChain let setupView = CreateBubbleSetupView()
.topToBottomOfView(navView) let doneView = CreateBubbleDoneView()
.edges(excludingEdge: .top)
createBubbleTipsView.layoutChain
.topToBottomOfView(navView)
.edges(excludingEdge: .top)
createBubbleDoneView.layoutChain let moreBtn: UIButton = {
.topToBottomOfView(navView) let btn = UIButton(type: .custom)
.edges(excludingEdge: .top) btn.setImage(UIImage(systemName: "ellipsis"), for: .normal)
} btn.tintColor = UIColor(hexStr: "#293445")
btn.backgroundColor = UIColor.white.withAlphaComponent(0.9)
btn.layer.cornerRadius = 8
btn.layer.borderWidth = 1
btn.layer.borderColor = UIColor(hexStr: "#E8E8E8").cgColor
return btn
}()
lazy var navBgView: UIImageView = { lazy var navBgView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2") iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill iv.contentMode = .scaleAspectFill
iv.isHidden = true
return iv return iv
}() }()
lazy var navView: BaseNavigationView = { lazy var navView: BaseNavigationView = {
let nav = BaseNavigationView(title: "设置气泡时间") BaseNavigationView(title: "设置气泡时间")
return nav
}() }()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: .zero) super.init(frame: frame)
backgroundColor = .white backgroundColor = .white
setupUI() setupUI()
setupRx() showSetup(animated: false)
createBubbleTiemView.isHidden = false
createBubbleTipsView.isHidden = true
createBubbleDoneView.isHidden = true
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
private func setupUI() {
addSubview(navBgView)
addSubview(navView)
addSubview(setupView)
addSubview(doneView)
navBgView.layoutChain.edges(excludingEdge: .bottom).heightToWidth(160 / 375)
navView.layoutChain.edges(excludingEdge: .bottom).height(kNaviHeight)
navView.addRightButton(moreBtn)
moreBtn.layoutChain.width(36).height(36)
setupView.layoutChain.topToBottomOfView(navView).edges(excludingEdge: .top)
doneView.layoutChain.topToBottomOfView(navView).edges(excludingEdge: .top)
}
func showSetup(animated: Bool) {
doneView.isHidden = true
setupView.isHidden = false
moreBtn.isHidden = false
navView.titleLabel.text = setupView.isSheetVisible ? "通知您的圈子" : "设置气泡时间"
if animated {
setupView.alpha = 0
UIView.animate(withDuration: 0.25) { self.setupView.alpha = 1 }
}
}
func showDone(animated: Bool) {
setupView.isHidden = true
doneView.isHidden = false
moreBtn.isHidden = false
navView.titleLabel.text = "活动气泡"
doneView.heroView.reloadAvatar()
if animated {
doneView.alpha = 0
UIView.animate(withDuration: 0.25) { self.doneView.alpha = 1 }
}
}
func updateNavTitleForSheet(_ visible: Bool) {
navView.titleLabel.text = visible ? "通知您的圈子" : "设置气泡时间"
}
} }

View File

@ -77,7 +77,7 @@ class GroupMemberView2: UIView {
private func setupCurrentUserDeviceInfo() { private func setupCurrentUserDeviceInfo() {
phoneModelIcon.image = UIImage(named: "Home/member_phone") phoneModelIcon.image = UIImage(named: "Home/member_phone")
phoneModelLab.text = Self.localPhoneModelName() phoneModelLab.text = UIDevice.modelName
phoneModelLab.textColor = Self.batteryDefaultTextColor phoneModelLab.textColor = Self.batteryDefaultTextColor
networkIcon.image = UIImage(named: "Home/member_wifi") networkIcon.image = UIImage(named: "Home/member_wifi")
@ -131,20 +131,8 @@ class GroupMemberView2: UIView {
// MARK: - Local device helpers // MARK: - Local device helpers
private static func localPhoneModelName() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let identifier = withUnsafePointer(to: &systemInfo.machine.0) { ptr in
String(cString: ptr)
}
return deviceModelMap[identifier] ?? "iPhone"
}
private static func localBatteryPercent() -> Int? { private static func localBatteryPercent() -> Int? {
UIDevice.current.isBatteryMonitoringEnabled = true UIDevice.batteryPercent
let level = UIDevice.current.batteryLevel
guard level >= 0 else { return nil }
return Int((level * 100).rounded())
} }
private static func localNetworkStatusText() -> String { private static func localNetworkStatusText() -> String {
@ -161,59 +149,6 @@ class GroupMemberView2: UIView {
return "无网络" return "无网络"
} }
private static let deviceModelMap: [String: String] = [
"iPhone10,1": "iPhone 8",
"iPhone10,4": "iPhone 8",
"iPhone10,2": "iPhone 8 Plus",
"iPhone10,5": "iPhone 8 Plus",
// iPhone X
"iPhone10,3": "iPhone X",
"iPhone10,6": "iPhone X",
"iPhone11,2": "iPhone XS",
"iPhone11,4": "iPhone XS Max",
"iPhone11,6": "iPhone XS Max",
"iPhone11,8": "iPhone XR",
// iPhone11
"iPhone12,1": "iPhone 11",
"iPhone12,3": "iPhone 11 Pro",
"iPhone12,5": "iPhone 11 Pro Max",
// iPhone12
"iPhone13,1": "iPhone 12 mini",
"iPhone13,2": "iPhone 12",
"iPhone13,3": "iPhone 12 Pro",
"iPhone13,4": "iPhone 12 Pro Max",
// iPhone13
"iPhone14,4": "iPhone 13 mini",
"iPhone14,5": "iPhone 13",
"iPhone14,2": "iPhone 13 Pro",
"iPhone14,3": "iPhone 13 Pro Max",
// iPhone14
"iPhone14,7": "iPhone 14",
"iPhone14,8": "iPhone 14 Plus",
"iPhone15,2": "iPhone 14 Pro",
"iPhone15,3": "iPhone 14 Pro Max",
// iPhone15
"iPhone15,4": "iPhone 15",
"iPhone15,5": "iPhone 15 Plus",
"iPhone16,1": "iPhone 15 Pro",
"iPhone16,2": "iPhone 15 Pro Max",
// iPhone16
"iPhone17,1": "iPhone 16 Pro",
"iPhone17,2": "iPhone 16 Pro Max",
"iPhone17,3": "iPhone 16",
"iPhone17,4": "iPhone 16 Plus",
"iPhone17,5": "iPhone 16e",
// iPhone17 2025
"iPhone18,1": "iPhone 17 Pro",
"iPhone18,2": "iPhone 17 Pro Max",
"iPhone18,3": "iPhone 17",
"iPhone18,4": "iPhone Air",
// Simulator
"i386": "Simulator",
"x86_64": "Simulator",
"arm64": "Simulator"
]
private func setupRx() { private func setupRx() {
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,

View File

@ -58,6 +58,7 @@ final class PhoneReportDetailView: UIView {
let deviceCard = UIView() let deviceCard = UIView()
let deviceNameLab = UILabel() let deviceNameLab = UILabel()
let storageLab = UILabel() let storageLab = UILabel()
let storageBarTrack = UIView()
let storageBar = UIProgressView(progressViewStyle: .default) let storageBar = UIProgressView(progressViewStyle: .default)
let deviceImage = UIImageView(image: UIImage(named: "Home/phone_device")) let deviceImage = UIImageView(image: UIImage(named: "Home/phone_device"))
@ -65,14 +66,18 @@ final class PhoneReportDetailView: UIView {
let screenSection = PhoneReportChartSectionView( let screenSection = PhoneReportChartSectionView(
title: "屏幕使用时间", title: "屏幕使用时间",
summaryPrefix: "今日使用时长", summaryPrefix: "今日使用时长",
barColor: UIColor(hexStr: "#5EC8FF"), summaryColor: UIColor(hexStr: "#00ADFE"),
highlightColor: UIColor(hexStr: "#2F9BFF") barGradient: (UIColor(hexStr: "#C0EAFF"), UIColor(hexStr: "#ECF9FF")),
highlightGradient: (UIColor(hexStr: "#16B3FF"), UIColor(hexStr: "#9EDFFF")),
tipColor: UIColor(hexStr: "#20B6FF")
) )
let unlockSection = PhoneReportChartSectionView( let unlockSection = PhoneReportChartSectionView(
title: "手机使用次数", title: "手机使用次数",
summaryPrefix: "今日使用次数", summaryPrefix: "今日使用次数",
barColor: UIColor(hexStr: "#B7E86A"), summaryColor: UIColor(hexStr: "#68C025"),
highlightColor: UIColor(hexStr: "#83DE00"), barGradient: (UIColor(hexStr: "#DDFFA1"), UIColor(hexStr: "#F2FFDC")),
highlightGradient: (UIColor(hexStr: "#AAF52A"), UIColor(hexStr: "#E8FFC1")),
tipColor: UIColor(hexStr: "#86E73D"),
valueSuffix: "" valueSuffix: ""
) )
@ -95,26 +100,29 @@ final class PhoneReportDetailView: UIView {
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight) navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
navView.addRightButton(switchGroupBtn) navView.addRightButton(switchGroupBtn)
addSubview(memberCV)
memberCV.layoutChain
.topToBottomOfView(navView, offset: 8)
.edgesHorzontal(15)
.height(100)
addSubview(scrollView) addSubview(scrollView)
scrollView.showsVerticalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false
scrollView.clipsToBounds = true
scrollView.delaysContentTouches = false
scrollView.canCancelContentTouches = true
scrollView.layoutChain scrollView.layoutChain
.topToBottomOfView(memberCV, offset: 12) .topToBottomOfView(navView)
.edgesHorzontal() .edgesHorzontal()
.bottom() .bottom()
scrollView.addSubview(contentView) scrollView.addSubview(contentView)
contentView.layoutChain.edges().widthToView(scrollView) contentView.layoutChain.edges().widthToView(scrollView)
contentView.addSubview(memberCV)
memberCV.layoutChain
.top(8)
.edgesHorzontal(15)
.height(100)
statusTitleLab.text = "手机状态" statusTitleLab.text = "手机状态"
statusTitleLab.font = .systemFont(ofSize: 16, weight: .bold) statusTitleLab.font = .systemFont(ofSize: 16, weight: .bold)
statusTitleLab.textColor = UIColor(hexStr: "#293445") statusTitleLab.textColor = UIColor(hexStr: "#293445")
contentView.addSubview(statusTitleLab) contentView.addSubview(statusTitleLab)
statusTitleLab.layoutChain.top().left(15) statusTitleLab.layoutChain.topToBottomOfView(memberCV, offset: 12).left(15)
statusCard.backgroundColor = .white statusCard.backgroundColor = .white
statusCard.applyPhoneReportCardStyle(cornerRadius: 20) statusCard.applyPhoneReportCardStyle(cornerRadius: 20)
@ -136,10 +144,10 @@ final class PhoneReportDetailView: UIView {
batteryRing.layoutChain.top(23).centerX().width(100).height(100) batteryRing.layoutChain.top(23).centerX().width(100).height(100)
estimateLab.font = .systemFont(ofSize: 12, weight: .medium) estimateLab.font = .systemFont(ofSize: 12, weight: .medium)
estimateLab.textColor = UIColor(hexStr: "#6B7280") estimateLab.textColor = UIColor(hexStr: "#293445")
estimateLab.textAlignment = .center estimateLab.textAlignment = .center
estimateLab.backgroundColor = UIColor(hexStr: "#E8EEF5") estimateLab.backgroundColor = UIColor(hexStr: "#FAFAFA")
estimateLab.cornerRadius = 10 estimateLab.cornerRadius = 5
leftPanel.addSubview(estimateLab) leftPanel.addSubview(estimateLab)
estimateLab.layoutChain estimateLab.layoutChain
.topToBottomOfView(batteryRing, offset: 12) .topToBottomOfView(batteryRing, offset: 12)
@ -177,16 +185,24 @@ final class PhoneReportDetailView: UIView {
deviceCard.addSubview(storageLab) deviceCard.addSubview(storageLab)
storageLab.layoutChain.topToBottomOfView(deviceNameLab, offset: 4).left(10) storageLab.layoutChain.topToBottomOfView(deviceNameLab, offset: 4).left(10)
storageBar.progressTintColor = UIColor(hexStr: "#5EC8FF") storageBarTrack.backgroundColor = UIColor(hexStr: "#E0E0E0")
storageBar.trackTintColor = UIColor(hexStr: "#E5E7EB") storageBarTrack.layer.cornerRadius = 5
deviceCard.addSubview(storageBar) storageBarTrack.clipsToBounds = true
storageBar.layoutChain deviceCard.addSubview(storageBarTrack)
storageBarTrack.layoutChain
.topToBottomOfView(storageLab, offset: 6) .topToBottomOfView(storageLab, offset: 6)
.left(10) .left(10)
.right(60) .right(60)
.height(4) .height(10)
.bottom(10) .bottom(10)
storageBar.progressTintColor = UIColor(hexStr: "#16B3FF")
storageBar.trackTintColor = UIColor(hexStr: "#16B3FF", alpha: 0.3)
storageBarTrack.addSubview(storageBar)
storageBar.layoutChain
.edgesHorzontal(3)
.centerY()
deviceImage.contentMode = .scaleAspectFit deviceImage.contentMode = .scaleAspectFit
deviceCard.addSubview(deviceImage) deviceCard.addSubview(deviceImage)
deviceImage.layoutChain.right(8).centerY().width(36).height(54) deviceImage.layoutChain.right(8).centerY().width(36).height(54)
@ -204,7 +220,7 @@ final class PhoneReportDetailView: UIView {
} }
func apply(_ snap: PhoneReportDetailViewModel.StatusSnapshot) { func apply(_ snap: PhoneReportDetailViewModel.StatusSnapshot) {
batteryRing.configure(percent: snap.batteryPercent, text: snap.batteryText) batteryRing.configure(percent: snap.batteryPercent, text: snap.batteryText, isCharging: snap.isCharging)
estimateLab.text = "预计使用\(snap.estimateText)" estimateLab.text = "预计使用\(snap.estimateText)"
brightnessCard.setValue(snap.brightnessText) brightnessCard.setValue(snap.brightnessText)
volumeCard.setValue(snap.volumeText) volumeCard.setValue(snap.volumeText)
@ -212,8 +228,8 @@ final class PhoneReportDetailView: UIView {
deviceNameLab.text = snap.deviceText deviceNameLab.text = snap.deviceText
storageLab.text = snap.storageText storageLab.text = snap.storageText
storageBar.progress = Float(snap.storageRatio) storageBar.progress = Float(snap.storageRatio)
screenSection.setSummary("\(screenSection.summaryPrefix) \(snap.screenTodayText)") screenSection.setSummary(snap.screenTodayText)
unlockSection.setSummary("\(unlockSection.summaryPrefix) \(snap.unlockTodayText)") unlockSection.setSummary("\(snap.unlockTodayText)")
screenSection.setBars(snap.screenBars) screenSection.setBars(snap.screenBars)
unlockSection.setBars(snap.unlockBars) unlockSection.setBars(snap.unlockBars)
} }
@ -274,8 +290,9 @@ final class PhoneBatteryRingView: UIView {
progress.path = path.cgPath progress.path = path.cgPath
} }
func configure(percent: Int?, text: String) { func configure(percent: Int?, text: String, isCharging: Bool = false) {
valueLab.text = text valueLab.text = text
bolt.isHidden = !isCharging
progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100 progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100
} }
} }
@ -298,12 +315,12 @@ final class PhoneStatusMiniCard: UIView {
titleLab.textColor = UIColor(hexStr: "#AAAAAA") titleLab.textColor = UIColor(hexStr: "#AAAAAA")
titleLab.textAlignment = .center titleLab.textAlignment = .center
addSubview(titleLab) addSubview(titleLab)
titleLab.layoutChain.leftToRightOfView(icon, offset: 4).right(5) titleLab.layoutChain.leftToRightOfView(icon, offset: 4).right(5, relation: .greaterThanOrEqual).centerY(icon)
valueLab.font = .systemFont(ofSize: 16, weight: .medium) valueLab.font = .systemFont(ofSize: 16, weight: .medium)
valueLab.textColor = UIColor(hexStr: "#293445") valueLab.textColor = UIColor(hexStr: "#293445")
valueLab.textAlignment = .center valueLab.textAlignment = .center
addSubview(valueLab) addSubview(valueLab)
valueLab.layoutChain.topToBottomOfView(icon, offset: 19).edgesHorzontal(2).bottom(16) valueLab.layoutChain.topToBottomOfView(icon, offset: 10).edgesHorzontal(2)
} }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@ -316,20 +333,33 @@ final class PhoneStatusMiniCard: UIView {
final class PhoneReportChartSectionView: UIView { final class PhoneReportChartSectionView: UIView {
let summaryPrefix: String let summaryPrefix: String
private let titleLab = UILabel() private let titleLab = UILabel()
private let summaryPrefixLab = UILabel()
private let summaryLab = UILabel() private let summaryLab = UILabel()
private let dayBtn = UIButton(type: .system) private let dayBtn = UIButton(type: .system)
private let weekBtn = UIButton(type: .system) private let weekBtn = UIButton(type: .system)
private let chart = PhoneReportBarChartView() private let chart = PhoneReportBarChartView()
private let barColor: UIColor private let summaryColor: UIColor
private let highlightColor: UIColor private let barGradient: (UIColor, UIColor)
private let highlightGradient: (UIColor, UIColor)
private let tipColor: UIColor
private let valueSuffix: String private let valueSuffix: String
var onModeChanged: ((PhoneReportDetailViewModel.ChartMode) -> Void)? var onModeChanged: ((PhoneReportDetailViewModel.ChartMode) -> Void)?
init(title: String, summaryPrefix: String, barColor: UIColor, highlightColor: UIColor, valueSuffix: String = "") { init(
title: String,
summaryPrefix: String,
summaryColor: UIColor,
barGradient: (UIColor, UIColor),
highlightGradient: (UIColor, UIColor),
tipColor: UIColor,
valueSuffix: String = ""
) {
self.summaryPrefix = summaryPrefix self.summaryPrefix = summaryPrefix
self.barColor = barColor self.summaryColor = summaryColor
self.highlightColor = highlightColor self.barGradient = barGradient
self.highlightGradient = highlightGradient
self.tipColor = tipColor
self.valueSuffix = valueSuffix self.valueSuffix = valueSuffix
super.init(frame: .zero) super.init(frame: .zero)
titleLab.text = title titleLab.text = title
@ -344,26 +374,43 @@ final class PhoneReportChartSectionView: UIView {
addSubview(card) addSubview(card)
card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180) card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180)
summaryLab.font = .systemFont(ofSize: 13, weight: .medium) summaryPrefixLab.text = summaryPrefix
summaryLab.textColor = UIColor(hexStr: "#293445") summaryPrefixLab.font = .systemFont(ofSize: 12, weight: .medium)
card.addSubview(summaryLab) summaryPrefixLab.textColor = UIColor(hexStr: "#293445")
summaryLab.layoutChain.top(14).left(14) summaryPrefixLab.setContentCompressionResistancePriority(.required, for: .horizontal)
card.addSubview(summaryPrefixLab)
summaryPrefixLab.layoutChain.top(14).left(14)
configureToggle(dayBtn, title: "", selected: true) configureToggle(dayBtn, title: "", selected: true)
configureToggle(weekBtn, title: "", selected: false) configureToggle(weekBtn, title: "", selected: false)
let toggle = UIStackView(arrangedSubviews: [weekBtn, dayBtn]) let toggle = UIStackView(arrangedSubviews: [weekBtn, dayBtn])
toggle.axis = .horizontal toggle.axis = .horizontal
toggle.spacing = 0 toggle.spacing = 0
toggle.backgroundColor = UIColor(hexStr: "#F3F4F6")
toggle.cornerRadius = 12 let toggleBg = UIView()
card.addSubview(toggle) toggleBg.backgroundColor = UIColor(hexStr: "#EAEAEA")
toggle.layoutChain.centerY(summaryLab).right(14).height(28) toggleBg.layer.cornerRadius = 16
toggleBg.clipsToBounds = true
toggleBg.addSubview(toggle)
toggle.layoutChain.edges(UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))
card.addSubview(toggleBg)
toggleBg.layoutChain.centerY(summaryPrefixLab).right(14).height(32)
summaryLab.font = .systemFont(ofSize: 12, weight: .bold)
summaryLab.textColor = summaryColor
summaryLab.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
card.addSubview(summaryLab)
summaryLab.layoutChain
.centerY(summaryPrefixLab)
.leftToRightOfView(summaryPrefixLab, offset: 4)
.rightToLeftOfView(toggleBg, offset: 8, relation: .lessThanOrEqual)
dayBtn.addTarget(self, action: #selector(tapDay), for: .touchUpInside) dayBtn.addTarget(self, action: #selector(tapDay), for: .touchUpInside)
weekBtn.addTarget(self, action: #selector(tapWeek), for: .touchUpInside) weekBtn.addTarget(self, action: #selector(tapWeek), for: .touchUpInside)
card.addSubview(chart) card.addSubview(chart)
chart.layoutChain.topToBottomOfView(summaryLab, offset: 16).edgesHorzontal(14).bottom(14) chart.layoutChain.topToBottomOfView(summaryPrefixLab, offset: 16).edgesHorzontal(25).bottom(14)
} }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@ -398,100 +445,174 @@ final class PhoneReportChartSectionView: UIView {
} }
func setBars(_ bars: [PhoneReportDetailViewModel.ChartBar]) { func setBars(_ bars: [PhoneReportDetailViewModel.ChartBar]) {
chart.configure(bars: bars, color: barColor, highlight: highlightColor, suffix: valueSuffix) chart.configure(
bars: bars,
barGradient: barGradient,
highlightGradient: highlightGradient,
tipColor: tipColor,
suffix: valueSuffix
)
} }
} }
final class PhoneReportBarChartView: UIView { final class PhoneReportBarChartView: UIView {
private var bars: [PhoneReportDetailViewModel.ChartBar] = [] private var bars: [PhoneReportDetailViewModel.ChartBar] = []
private var color: UIColor = .systemBlue private var barGradient: (UIColor, UIColor) = (.systemBlue, .systemBlue)
private var highlight: UIColor = .systemBlue private var highlightGradient: (UIColor, UIColor) = (.systemBlue, .systemBlue)
private var tipColor: UIColor = .systemBlue
private var suffix = "" private var suffix = ""
private var barLayers: [CALayer] = [] private var barLayers: [CALayer] = []
private var labelViews: [UILabel] = [] private var labelViews: [UILabel] = []
private var tipLab = UILabel() private var barFrames: [CGRect] = []
private var selectedIndex: Int?
private var barSpacing: CGFloat = 0
private var baselineY: CGFloat = 0
private let tipLab = UILabel()
private let baseline = UIView()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
clipsToBounds = false
isUserInteractionEnabled = true
baseline.backgroundColor = UIColor(hexStr: "#F2F2F2")
addSubview(baseline)
tipLab.font = .systemFont(ofSize: 11, weight: .bold) tipLab.font = .systemFont(ofSize: 11, weight: .bold)
tipLab.textColor = .white tipLab.textColor = .white
tipLab.backgroundColor = UIColor(hexStr: "#2F9BFF")
tipLab.textAlignment = .center tipLab.textAlignment = .center
tipLab.cornerRadius = 8 tipLab.cornerRadius = 6
tipLab.clipsToBounds = true tipLab.clipsToBounds = true
tipLab.isHidden = true tipLab.isHidden = true
tipLab.isUserInteractionEnabled = false
addSubview(tipLab) addSubview(tipLab)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
addGestureRecognizer(tap)
} }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(bars: [PhoneReportDetailViewModel.ChartBar], color: UIColor, highlight: UIColor, suffix: String) { func configure(
bars: [PhoneReportDetailViewModel.ChartBar],
barGradient: (UIColor, UIColor),
highlightGradient: (UIColor, UIColor),
tipColor: UIColor,
suffix: String
) {
let previous = self.bars
self.bars = bars self.bars = bars
self.color = color self.barGradient = barGradient
self.highlight = highlight self.highlightGradient = highlightGradient
self.tipColor = tipColor
self.suffix = suffix self.suffix = suffix
let dataChanged = previous.count != bars.count
|| zip(previous, bars).contains { $0.label != $1.label || $0.value != $1.value }
if dataChanged || selectedIndex == nil {
selectedIndex = bars.firstIndex(where: \.isToday) ?? (bars.isEmpty ? nil : bars.count - 1)
}
setNeedsLayout() setNeedsLayout()
} }
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: self)
guard !barFrames.isEmpty else { return }
for (i, frame) in barFrames.enumerated() {
let hitX = frame.minX - barSpacing / 2
let hitW = frame.width + barSpacing
let hit = CGRect(x: hitX, y: 0, width: hitW, height: max(baselineY, bounds.height))
if hit.contains(point) {
guard selectedIndex != i else { return }
selectedIndex = i
setNeedsLayout()
return
}
}
}
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
barLayers.forEach { $0.removeFromSuperlayer() } barLayers.forEach { $0.removeFromSuperlayer() }
barLayers.removeAll() barLayers.removeAll()
labelViews.forEach { $0.removeFromSuperview() } labelViews.forEach { $0.removeFromSuperview() }
labelViews.removeAll() labelViews.removeAll()
barFrames.removeAll()
tipLab.isHidden = true tipLab.isHidden = true
guard !bars.isEmpty else { return } guard !bars.isEmpty else {
baseline.isHidden = true
return
}
baseline.isHidden = false
let maxVal = max(bars.map(\.value).max() ?? 1, 1) let maxVal = max(bars.map(\.value).max() ?? 1, 1)
let count = bars.count let count = bars.count
let labelH: CGFloat = 16 let labelH: CGFloat = 16
let chartH = bounds.height - labelH - 24 let chartH = bounds.height - labelH - 24
let spacing: CGFloat = 8 let barW: CGFloat = 16
let barW = max(8, (bounds.width - spacing * CGFloat(count - 1)) / CGFloat(count)) barSpacing = count > 1
? max(0, (bounds.width - barW * CGFloat(count)) / CGFloat(count - 1))
: 0
baselineY = bounds.height - labelH
baseline.frame = CGRect(x: 0, y: baselineY - 0.5, width: bounds.width, height: 1)
if selectedIndex == nil || selectedIndex! >= count {
selectedIndex = bars.firstIndex(where: \.isToday) ?? count - 1
}
for (i, bar) in bars.enumerated() { for (i, bar) in bars.enumerated() {
let ratio = CGFloat(bar.value) / CGFloat(maxVal) let ratio = CGFloat(bar.value) / CGFloat(maxVal)
let h = max(4, chartH * max(0.05, ratio)) let h = max(4, chartH * max(0.05, ratio))
let x = CGFloat(i) * (barW + spacing) let x = CGFloat(i) * (barW + barSpacing)
let y = bounds.height - labelH - h let y = baselineY - h
let layer = CALayer() let barFrame = CGRect(x: x, y: y, width: barW, height: h)
layer.frame = CGRect(x: x, y: y, width: barW, height: h) barFrames.append(barFrame)
layer.backgroundColor = (bar.isToday ? highlight : color.withAlphaComponent(0.35)).cgColor
layer.cornerRadius = 4 let isSelected = i == selectedIndex
self.layer.addSublayer(layer) let gradient = CAGradientLayer()
barLayers.append(layer) gradient.frame = barFrame
let colors = isSelected ? highlightGradient : barGradient
gradient.colors = [colors.0.cgColor, colors.1.cgColor]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1)
gradient.cornerRadius = 4
layer.insertSublayer(gradient, below: tipLab.layer)
barLayers.append(gradient)
let lab = UILabel() let lab = UILabel()
lab.text = bar.label lab.text = bar.label
lab.font = .systemFont(ofSize: 10, weight: .medium) lab.font = .systemFont(ofSize: 10, weight: .medium)
lab.textColor = UIColor(hexStr: "#9CA3AF") lab.textColor = UIColor(hexStr: "#9CA3AF")
lab.textAlignment = .center lab.textAlignment = .center
lab.frame = CGRect(x: x - 6, y: bounds.height - labelH, width: barW + 12, height: labelH) lab.frame = CGRect(x: x - 10, y: bounds.height - labelH, width: barW + 20, height: labelH)
addSubview(lab) insertSubview(lab, belowSubview: tipLab)
labelViews.append(lab) labelViews.append(lab)
if bar.isToday, bar.value > 0 { if isSelected {
let hours = bar.value / 3600 tipLab.text = tipText(for: bar)
let mins = (bar.value % 3600) / 60 tipLab.backgroundColor = tipColor
let text: String
if suffix == "" {
text = "\(bar.value)"
} else if hours > 0 {
text = mins > 0 ? "\(hours)h \(mins)m" : "\(hours)h"
} else {
text = "\(mins)m"
}
tipLab.text = " \(text) "
tipLab.sizeToFit() tipLab.sizeToFit()
let tipW = tipLab.bounds.width + 5 + 5
let tipH = tipLab.bounds.height + 4 + 4
// chart
tipLab.frame = CGRect( tipLab.frame = CGRect(
x: max(0, x + barW / 2 - tipLab.bounds.width / 2 - 6), x: x + barW / 2 - tipW / 2,
y: max(0, y - 22), y: max(0, y - tipH - 4),
width: tipLab.bounds.width + 12, width: tipW,
height: 18 height: tipH
) )
tipLab.backgroundColor = highlight
tipLab.isHidden = false tipLab.isHidden = false
} }
} }
bringSubviewToFront(tipLab)
}
private func tipText(for bar: PhoneReportDetailViewModel.ChartBar) -> String {
if suffix == "" {
return "\(bar.value)"
}
let hours = bar.value / 3600
let mins = (bar.value % 3600) / 60
if hours > 0 {
return mins > 0 ? "\(hours)h \(mins)m" : "\(hours)h"
}
return "\(mins)m"
} }
} }

View File

@ -26,9 +26,10 @@ final class PhoneReportDetailViewModel {
let isToday: Bool let isToday: Bool
} }
struct StatusSnapshot { struct StatusSnapshot {
let batteryText: String let batteryText: String
let batteryPercent: Int? let batteryPercent: Int?
let isCharging: Bool
let estimateText: String let estimateText: String
let brightnessText: String let brightnessText: String
let volumeText: String let volumeText: String
@ -46,6 +47,7 @@ final class PhoneReportDetailViewModel {
StatusSnapshot( StatusSnapshot(
batteryText: batteryText, batteryText: batteryText,
batteryPercent: batteryPercent, batteryPercent: batteryPercent,
isCharging: isCharging,
estimateText: estimateText, estimateText: estimateText,
brightnessText: brightnessText, brightnessText: brightnessText,
volumeText: volumeText ?? self.volumeText, volumeText: volumeText ?? self.volumeText,
@ -73,6 +75,7 @@ final class PhoneReportDetailViewModel {
private var volumeObservation: NSKeyValueObservation? private var volumeObservation: NSKeyValueObservation?
init(members: [GroupMemberModel], selectedUserId: String) { init(members: [GroupMemberModel], selectedUserId: String) {
UIDevice.current.isBatteryMonitoringEnabled = true
self.members = members self.members = members
self.selectedMemberId = BehaviorRelay(value: selectedUserId) self.selectedMemberId = BehaviorRelay(value: selectedUserId)
self.snapshot = BehaviorRelay(value: Self.emptySnapshot()) self.snapshot = BehaviorRelay(value: Self.emptySnapshot())
@ -114,6 +117,20 @@ final class PhoneReportDetailViewModel {
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
// /
let batteryCenter = NotificationCenter.default
Observable.merge(
batteryCenter.rx.notification(UIDevice.batteryLevelDidChangeNotification),
batteryCenter.rx.notification(UIDevice.batteryStateDidChangeNotification)
)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
guard let self else { return }
guard self.selectedMemberId.value == AppContextManager.shared.userId else { return }
self.refresh(userId: self.selectedMemberId.value)
})
.disposed(by: disposeBag)
observeSystemVolume() observeSystemVolume()
} }
@ -143,16 +160,13 @@ final class PhoneReportDetailViewModel {
private func makeSelfSnapshot() -> StatusSnapshot { private func makeSelfSnapshot() -> StatusSnapshot {
let mgr = UnlockCountManager.shared let mgr = UnlockCountManager.shared
UIDevice.current.isBatteryMonitoringEnabled = true let batteryPercent = UIDevice.batteryPercent
let batteryLevel = UIDevice.current.batteryLevel let isCharging = UIDevice.isBatteryCharging
let batteryValid = batteryLevel >= 0
let battery = Int(batteryLevel * 100)
let screenSec = mgr.todayScreenTimeSeconds let screenSec = mgr.todayScreenTimeSeconds
let unlock = mgr.todayCount let unlock = mgr.todayCount
let usage = mgr.todayAppUsageCount let usage = mgr.todayAppUsageCount
let batteryText = batteryValid ? "\(battery)%" : placeholder let batteryText = batteryPercent.map { "\($0)%" } ?? placeholder
let batteryPercent: Int? = batteryValid ? battery : nil
let brightnessText = Self.brightnessText() ?? placeholder let brightnessText = Self.brightnessText() ?? placeholder
let volumeText = Self.volumeText() ?? placeholder let volumeText = Self.volumeText() ?? placeholder
let wifiText = Self.wifiSSID() ?? placeholder let wifiText = Self.wifiSSID() ?? placeholder
@ -174,11 +188,12 @@ final class PhoneReportDetailViewModel {
return StatusSnapshot( return StatusSnapshot(
batteryText: batteryText, batteryText: batteryText,
batteryPercent: batteryPercent, batteryPercent: batteryPercent,
isCharging: isCharging,
estimateText: placeholder, estimateText: placeholder,
brightnessText: brightnessText, brightnessText: brightnessText,
volumeText: volumeText, volumeText: volumeText,
wifiText: wifiText, wifiText: wifiText,
deviceText: UIDevice.current.model, deviceText: UIDevice.modelName,
storageText: storageText, storageText: storageText,
storageRatio: storageRatio, storageRatio: storageRatio,
screenTodayText: screenTodayText, screenTodayText: screenTodayText,
@ -195,6 +210,7 @@ final class PhoneReportDetailViewModel {
return StatusSnapshot( return StatusSnapshot(
batteryText: hasBattery ? "\(battery)%" : placeholder, batteryText: hasBattery ? "\(battery)%" : placeholder,
batteryPercent: hasBattery ? battery : nil, batteryPercent: hasBattery ? battery : nil,
isCharging: false,
estimateText: placeholder, estimateText: placeholder,
brightnessText: placeholder, brightnessText: placeholder,
volumeText: placeholder, volumeText: placeholder,
@ -214,6 +230,7 @@ final class PhoneReportDetailViewModel {
StatusSnapshot( StatusSnapshot(
batteryText: "**", batteryText: "**",
batteryPercent: nil, batteryPercent: nil,
isCharging: false,
estimateText: "**", estimateText: "**",
brightnessText: "**", brightnessText: "**",
volumeText: "**", volumeText: "**",
@ -295,68 +312,145 @@ final class PhoneReportDetailViewModel {
} }
} }
private static func storageText() -> String? { /// iPhone GB + ImportantUsage
private static func storageBytes() -> (used: Int64, total: Int64)? {
let url = URL(fileURLWithPath: NSHomeDirectory())
let keys: Set<URLResourceKey> = [
.volumeTotalCapacityKey,
.volumeAvailableCapacityForImportantUsageKey
]
guard guard
let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()), let values = try? url.resourceValues(forKeys: keys),
let total = attrs[.systemSize] as? NSNumber, let total = values.volumeTotalCapacity,
let free = attrs[.systemFreeSize] as? NSNumber let available = values.volumeAvailableCapacityForImportantUsage,
total > 0
else { return nil } else { return nil }
let used = total.doubleValue - free.doubleValue let used = max(0, Int64(total) - available)
let usedGB = used / 1_073_741_824 return (used, Int64(total))
let totalGB = total.doubleValue / 1_073_741_824 }
return String(format: "%.1fGB/%.0fGB", usedGB, totalGB)
private static func formatStorageGB(_ bytes: Int64, fractionDigits: Int) -> String {
// Settings 使1GB = 1e9128G 128GB 119GB
let gb = Double(bytes) / 1_000_000_000
return String(format: "%.\(fractionDigits)fGB", gb)
}
private static func storageText() -> String? {
guard let info = storageBytes() else { return nil }
let usedText = formatStorageGB(info.used, fractionDigits: 1)
let totalText = formatStorageGB(info.total, fractionDigits: 0)
return "\(usedText)/\(totalText)"
} }
private static func storageRatio() -> CGFloat? { private static func storageRatio() -> CGFloat? {
guard guard let info = storageBytes(), info.total > 0 else { return nil }
let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()), return CGFloat(Double(info.used) / Double(info.total))
let total = attrs[.systemSize] as? NSNumber,
let free = attrs[.systemFreeSize] as? NSNumber,
total.doubleValue > 0
else { return nil }
return CGFloat((total.doubleValue - free.doubleValue) / total.doubleValue)
} }
private static func screenBars(from mgr: UnlockCountManager, mode: ChartMode) -> [ChartBar] { private static func screenBars(from mgr: UnlockCountManager, mode: ChartMode) -> [ChartBar] {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
let byDay = Defaults[\.screenTimeByDay] let byDay = Defaults[\.screenTimeByDay]
let todayValue = mgr.todayScreenTimeSeconds
return (0..<7).map { offset -> ChartBar in switch mode {
let dayOffset = offset - 6 case .day:
let date = calendar.date(byAdding: .day, value: dayOffset, to: today) ?? today return dayBars(byDay: byDay, todayValue: todayValue, labelStyle: .monthDaySlash)
let key = formatter.string(from: date) case .week:
let label = formatter.string(from: date).suffix(5).replacingOccurrences(of: "-", with: "/") return weekBars(byDay: byDay, todayValue: todayValue)
let value: Int
if dayOffset == 0 {
value = mgr.todayScreenTimeSeconds
} else {
value = byDay[key] ?? 0
}
// week mode: keep same day bars for now (UI toggle still works)
_ = mode
return ChartBar(label: String(label), value: value, isToday: dayOffset == 0)
} }
} }
private static func unlockBars(unlock: Int, mode: ChartMode) -> [ChartBar] { private static func unlockBars(unlock: Int, mode: ChartMode) -> [ChartBar] {
var byDay = Defaults[\.unlockCountByDay]
let todayKey = dayKey(for: Date())
byDay[todayKey] = unlock
switch mode {
case .day:
return dayBars(byDay: byDay, todayValue: unlock, labelStyle: .monthDay)
case .week:
return weekBars(byDay: byDay, todayValue: unlock)
}
}
private enum DayLabelStyle {
case monthDaySlash // 08/04
case monthDay // 8/4
}
private static func dayKey(for date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar.current
formatter.timeZone = TimeZone.current
formatter.dateFormat = "yyyy-MM-dd"
return formatter.string(from: date)
}
private static func dayBars(
byDay: [String: Int],
todayValue: Int,
labelStyle: DayLabelStyle
) -> [ChartBar] {
let calendar = Calendar.current let calendar = Calendar.current
let today = calendar.startOfDay(for: Date()) let today = calendar.startOfDay(for: Date())
let formatter = DateFormatter() let labelFormatter = DateFormatter()
formatter.dateFormat = "M/d" labelFormatter.locale = Locale(identifier: "en_US_POSIX")
switch labelStyle {
case .monthDaySlash:
labelFormatter.dateFormat = "MM/dd"
case .monthDay:
labelFormatter.dateFormat = "M/d"
}
return (0..<7).map { offset -> ChartBar in return (0..<7).map { offset -> ChartBar in
let dayOffset = offset - 6 let dayOffset = offset - 6
let date = calendar.date(byAdding: .day, value: dayOffset, to: today) ?? today let date = calendar.date(byAdding: .day, value: dayOffset, to: today) ?? today
let key = dayKey(for: date)
let isToday = dayOffset == 0 let isToday = dayOffset == 0
_ = mode let value = isToday ? todayValue : (byDay[key] ?? 0)
return ChartBar( return ChartBar(
label: formatter.string(from: date), label: labelFormatter.string(from: date),
value: isToday ? unlock : 0, value: value,
isToday: isToday isToday: isToday
) )
} }
} }
/// 7 label M/d
private static func weekBars(byDay: [String: Int], todayValue: Int) -> [ChartBar] {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
guard let currentWeek = calendar.dateInterval(of: .weekOfYear, for: today) else {
return dayBars(byDay: byDay, todayValue: todayValue, labelStyle: .monthDay)
}
let labelFormatter = DateFormatter()
labelFormatter.locale = Locale(identifier: "en_US_POSIX")
labelFormatter.dateFormat = "M/d"
let todayKey = dayKey(for: today)
return (0..<7).map { offset -> ChartBar in
let weekOffset = offset - 6
let weekStart = calendar.date(byAdding: .weekOfYear, value: weekOffset, to: currentWeek.start)
?? currentWeek.start
let weekInterval = calendar.dateInterval(of: .weekOfYear, for: weekStart)
?? DateInterval(start: weekStart, duration: 7 * 24 * 3600)
var sum = 0
var cursor = weekInterval.start
while cursor < weekInterval.end {
let key = dayKey(for: cursor)
if key == todayKey {
sum += todayValue
} else {
sum += byDay[key] ?? 0
}
guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
cursor = next
}
return ChartBar(
label: labelFormatter.string(from: weekInterval.start),
value: sum,
isToday: weekOffset == 0
)
}
}
} }

View File

@ -0,0 +1,164 @@
//
// SearchLocationHeaderView.swift
// QuickLocation
//
import UIKit
/// hero + 使 + +
final class SearchLocationHeaderView: UIView {
let usageLab = UILabel()
let headlineLab = UILabel()
let subheadLab = UILabel()
let marqueeScrollView = UIScrollView()
/// + image backgroundColor
let heroImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupMarquee()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func startMarqueeAnimation() {
marqueeScrollView.layer.removeAllAnimations()
marqueeScrollView.contentOffset.x = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
let contentW = self.marqueeScrollView.contentSize.width
guard contentW > self.marqueeScrollView.bounds.width else { return }
let duration = contentW / 100
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear, .repeat]) {
self.marqueeScrollView.contentOffset.x = contentW - self.marqueeScrollView.bounds.width
}
}
}
private func setupUI() {
addSubview(heroImageView)
addSubview(usageLab)
addSubview(headlineLab)
addSubview(subheadLab)
addSubview(marqueeScrollView)
heroImageView.backgroundColor = UIColor(hexStr: "#FFD54F")
heroImageView.contentMode = .scaleAspectFit
heroImageView.layoutChain
.top()
.edgesHorzontal(24)
.heightToWidth(220 / 327)
let count = Int.random(in: 8000...15000)
let usageText = "\(count) 人正在使用此功能"
let attr = NSMutableAttributedString(string: usageText)
if let range = usageText.range(of: "\(count)") {
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
range: NSRange(range, in: usageText))
}
usageLab.attributedText = attr
usageLab.font = .systemFont(ofSize: 14, weight: .medium)
usageLab.textColor = UIColor(hexStr: "#999999")
usageLab.textAlignment = .center
usageLab.layoutChain
.topToBottomOfView(heroImageView, offset: 8)
.centerX()
headlineLab.text = "实时查看TA的位置"
headlineLab.font = FontManager.boboBold(30)
headlineLab.textColor = UIColor(hexStr: "#293445")
headlineLab.textAlignment = .center
headlineLab.layoutChain
.topToBottomOfView(usageLab, offset: 16)
.centerX()
subheadLab.text = "为你安心守护出行安全"
subheadLab.font = .systemFont(ofSize: 18, weight: .bold)
subheadLab.textColor = UIColor(hexStr: "#293445")
subheadLab.textAlignment = .center
subheadLab.layoutChain
.topToBottomOfView(headlineLab, offset: 6)
.centerX()
marqueeScrollView.backgroundColor = .clear
marqueeScrollView.showsHorizontalScrollIndicator = false
marqueeScrollView.isScrollEnabled = false
marqueeScrollView.layoutChain
.topToBottomOfView(subheadLab, offset: 12)
.edgesHorzontal()
.height(36)
.bottom()
}
private func randomPhoneNumber() -> String {
let prefixes = [
"134","135","136","137","138","139","147","150","151","152","157","158","159","178","182","183","184","187","188","198",
"130","131","132","145","155","156","166","175","176","185","186","196",
"133","149","153","173","177","180","181","189","191","199"
]
let prefix = prefixes.randomElement()!
var suffix = ""
for _ in 0..<8 { suffix.append("\(Int.random(in: 0...9))") }
return prefix + suffix
}
private func setupMarquee() {
let surnames = ["", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", ""]
let container = UIView()
container.backgroundColor = .clear
marqueeScrollView.addSubview(container)
var prevView: UIView?
for _ in 0..<10 {
let iconIndex = Int.random(in: 1...15)
let surname = surnames.randomElement() ?? ""
let maskedName = "\(surname)**"
let phone = randomPhoneNumber()
let maskedPhone = phone.prefix(3) + "******" + phone.suffix(2)
let itemView = UIView()
itemView.backgroundColor = UIColor(hexStr: "#EFF9FF")
itemView.cornerRadius = 8
container.addSubview(itemView)
let avatar = UIImageView(image: UIImage(named: "UserIcon/\(iconIndex)"))
avatar.contentMode = .scaleAspectFill
avatar.backgroundColor = UIColor(hexStr: "#E0E0E0")
avatar.cornerRadius = 12
avatar.clipsToBounds = true
itemView.addSubview(avatar)
let label = UILabel()
label.font = .systemFont(ofSize: 13, weight: .medium)
let fullText = "\(maskedName) 定位到了 \(maskedPhone)"
let attr = NSMutableAttributedString(string: fullText)
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
range: NSRange(fullText.range(of: maskedName)!, in: fullText))
if let phoneRange = fullText.range(of: maskedPhone) {
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
range: NSRange(phoneRange, in: fullText))
}
label.attributedText = attr
itemView.addSubview(label)
avatar.layoutChain.left(10).centerY().width(24).height(24)
label.layoutChain.leftToRightOfView(avatar, offset: 6).centerY().right(10)
itemView.layoutChain.centerY().height(30)
if let prev = prevView {
itemView.layoutChain.leftToRightOfView(prev, offset: 16)
} else {
itemView.layoutChain.left()
}
prevView = itemView
}
if let last = prevView {
container.layoutChain.edges().heightToView(marqueeScrollView).rightToView(last)
}
}
}

View File

@ -2,138 +2,153 @@
// SearchLocationResultVC.swift // SearchLocationResultVC.swift
// QuickLocation // QuickLocation
// //
// Created by on 2026/6/27.
//
import UIKit import UIKit
import RxSwift import RxSwift
import RxCocoa import RxCocoa
import Lottie import ObjectMapper
import URLNavigator
class SearchLocationResultVC: BaseViewController { class SearchLocationResultVC: BaseViewController {
fileprivate var rootView: SearchLocationResultView! fileprivate var rootView: SearchLocationResultView!
override func loadView() { override func loadView() {
rootView = SearchLocationResultView(frame: UIScreen.main.bounds) rootView = SearchLocationResultView(frame: UIScreen.main.bounds)
view = rootView view = rootView
} }
private let phone: String private let phone: String
private let code: Int private let code: Int
private let memberData: [String : Any] private let memberData: [String: Any]
private var groupModel: GroupModel? private var groupModel: GroupModel?
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
rootView.unlockVipView.isHidden = AppContextManager.shared.vip > 1 let isVip = AppContextManager.shared.vip > 1
AppContextManager.shared.vip > 1 ? nil : rootView.unlockVipLottieView.play() rootView.unlockVipView.isHidden = isVip
rootView.scrollView.isHidden = !isVip
reactiveAction() reactiveAction()
requestPhoneArea() requestPhoneArea()
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
rootView.startMarqueeAnimation()
}
private func reactiveAction() { private func reactiveAction() {
rootView.successBtn.rx.tap.subscribe(onNext: { _ in rootView.actionBtn.rx.tap.subscribe(onNext: { _ in
if self.code == 0 { // switch self.code {
case 0:
NotificationCenter.default.post(name: .ShowMemberLocationNotification, NotificationCenter.default.post(name: .ShowMemberLocationNotification,
object: nil, object: nil,
userInfo: self.memberData) userInfo: self.memberData)
AppRouter.shared.popToRoot() self.navigationController?.popToRootViewController(animated: true)
} if let tab = self.tabBarController as? MainTabBarController
else { // Ta ?? UIViewController.topMost?.tabBarController as? MainTabBarController {
guard let model = self.groupModel else { tab.selectTab(at: 0)
self.requestGroupInfo(showPicker: true)
return
} }
GroupChooseView.show(groupModel: model) { _ in } case 20004:
SharePopView.show()
case 20005:
self.showGroupPicker()
default:
break
} }
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
rootView.inviteBtn.rx.tap.subscribe(onNext: { _ in
guard let model = self.groupModel else {
self.requestGroupInfo(showPicker: true)
return
}
GroupChooseView.show(groupModel: model) { _ in }
}).disposed(by: disposeBag)
} }
private func showGroupPicker() {
guard let model = groupModel else {
requestGroupInfo(showPicker: true)
return
}
presentGroupPicker(with: model)
}
private func presentGroupPicker(with model: GroupModel) {
GroupListPopView.show(groupModel: model) { groupKey in
guard let key = groupKey,
let groupInfo = model.groups.first(where: { $0.group_key == key })?.toJSON() else { return }
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": groupInfo])
}
}
private func shareApp() {
let text = "快来极速定位,实时守护家人出行安全"
let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil)
present(activity, animated: true)
}
private func handleCode() { private func handleCode() {
guard AppContextManager.shared.vip > 1 else { return } guard AppContextManager.shared.vip > 1 else { return }
var fileName = "" rootView.actionBtn.isHidden = true
var message = "" rootView.inviteView.isHidden = true
var tips = ""
switch code { switch code {
case 0: // case 0:
fileName = "phone_search_success" rootView.configureResult(
message = "定位成功" status: "定位成功",
tips = "点击去查看可获得具体位置" disclaimer: "点击去查看可获得具体位置",
rootView.inviteView.isHidden = false showInviteRow: false
rootView.successTipsLab.text = "已获得具体位置" )
rootView.successBtn.setTitle("去查看", for: .normal) rootView.actionBtn.setTitle("去查看", for: .normal)
rootView.phoneLab.text = phone rootView.actionBtn.isHidden = false
rootView.successLottieView.play() case 20004:
case 20004: // rootView.configureResult(
fileName = "phone_search_fail" status: "用户未注册",
message = "此用户未注册" disclaimer: "根据最新的相关法规需用户双方均安装该APP才可实现定位未安装APP的用户将不再提供具体定位功能。",
tips = "根据最新的相关法规需用户双方均安装该APP才可实现定位未安装APP的用户将不再提供具体定位功能。" showInviteRow: false
rootView.inviteBtn.isHidden = false )
case 20005: // rootView.actionBtn.setTitle("分享", for: .normal)
fileName = "phone_search_success" rootView.actionBtn.isHidden = false
message = "定位成功" case 20005:
tips = "根据最新的相关法规,您查询的用户与您不在同一个圈子时, 不再提供具体的位置定位邀请ta加入你的圈子后可随时查看位置。" rootView.configureResult(
rootView.inviteView.isHidden = false status: "定位成功",
rootView.successTipsLab.text = "邀请加入圈子后分享位置" disclaimer: "根据最新的相关法规,您查询的用户与您不在同一个圈子时, 不再提供具体的位置定位邀请ta加入你的圈子后可随时查看位置。",
rootView.successBtn.setTitle("邀请他", for: .normal) showInviteRow: false
rootView.phoneLab.text = phone )
rootView.successLottieView.play() rootView.actionBtn.setTitle("邀请TA加入", for: .normal)
rootView.actionBtn.isHidden = false
requestGroupInfo() requestGroupInfo()
default: default:
break break
} }
if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
self.rootView.normalLottieView.animation = LottieAnimation.filepath(path)
self.rootView.normalLottieView.play()
}
self.rootView.messageLab.text = message
self.rootView.tipsLab.text = tips
} }
// MARK: -
private func requestPhoneArea() { private func requestPhoneArea() {
SystemService.phoneArea(phone: phone).subscribe(onNext: { response in SystemService.phoneArea(phone: phone).subscribe(onNext: { response in
guard let data = response.data else { return } guard let data = response.data else { return }
self.handleCode() self.handleCode()
let province = data["Province"].safeString let province = data["Province"].safeString
let city = data["City"].safeString let city = data["City"].safeString
self.rootView.phoneAreaLab.text = "\(province)·\(city)" let area = "\(province)·\(city)"
self.rootView.unlockVipPhoneAreaLab.text = "\(province)·\(city)" self.rootView.phoneAreaLab.text = area
self.rootView.unlockVipPhoneAreaLab.text = area
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
// MARK: -
private func requestGroupInfo(showPicker: Bool = false) { private func requestGroupInfo(showPicker: Bool = false) {
GroupService.groupInfo().subscribe { [weak self] response in GroupService.groupInfo().subscribe { [weak self] response in
guard let self = self, let model = response.model else { return } guard let self = self, let model = response.model else { return }
self.groupModel = model self.groupModel = model
if showPicker { if showPicker {
GroupChooseView.show(groupModel: model) { _ in } self.presentGroupPicker(with: model)
} }
}.disposed(by: disposeBag) }.disposed(by: disposeBag)
} }
// MARK: - Init init(phone: String, code: Int, memberData: [String: Any] = [:]) {
init(phone: String, code: Int, memberData: [String : Any] = [:]) {
self.phone = phone self.phone = phone
self.code = code self.code = code
self.memberData = memberData self.memberData = memberData
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }

View File

@ -2,8 +2,6 @@
// SearchLocationResultView.swift // SearchLocationResultView.swift
// QuickLocation // QuickLocation
// //
// Created by on 2026/6/27.
//
import UIKit import UIKit
import RxSwift import RxSwift
@ -11,368 +9,301 @@ import RxCocoa
import Lottie import Lottie
class SearchLocationResultView: UIView { class SearchLocationResultView: UIView {
var disposeBag = DisposeBag() var disposeBag = DisposeBag()
private func setupRx() { let headerView = SearchLocationHeaderView()
}
private func setupUI() { private func setupUI() {
addSubview(unlockVipView) addSubview(unlockVipView)
addSubview(navBgView) addSubview(navBgView)
addSubview(navView) addSubview(navView)
addSubview(scrollView)
addSubview(normalView)
addSubview(successLottieView)
navBgView.layoutChain navBgView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
.heightToWidth(160/375) .heightToWidth(160 / 375)
navView.layoutChain navView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
.height(kNaviHeight) .height(kNaviHeight)
unlockVipView.layoutChain.edges() unlockVipView.layoutChain.edges()
successLottieView.layoutChain.edges() scrollView.layoutChain
normalView.layoutChain
.topToBottomOfView(navView) .topToBottomOfView(navView)
.edges(excludingEdge: .top) .edges(excludingEdge: .top)
} }
// MARK: - Views func startMarqueeAnimation() {
headerView.startMarqueeAnimation()
}
func configureResult(status: String, disclaimer: String, showInviteRow: Bool) {
statusPillLab.text = status
disclaimerLab.text = disclaimer
disclaimerLab.textAlignment = disclaimer.contains("去查看") ? .center : .left
disclaimerIcon.isHidden = disclaimer.contains("去查看")
inviteView.isHidden = !showInviteRow
// 72pt
inviteView.layoutChain
.topToBottomOfView(disclaimerLab, offset: showInviteRow ? 16 : 0)
.height(showInviteRow ? 72 : 0)
}
lazy var navBgView: UIImageView = { lazy var navBgView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2") iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill iv.contentMode = .scaleAspectFill
return iv return iv
}() }()
lazy var navView: BaseNavigationView = { lazy var navView: BaseNavigationView = {
let nav = BaseNavigationView(title: " ") BaseNavigationView(title: " ")
return nav
}() }()
// MARK: - Lottie lazy var scrollView: UIScrollView = {
lazy var successLottieView: LottieAnimationView = { let view = UIScrollView()
let view = LottieAnimationView(name: "phone_search_fireworks")
view.loopMode = .loop
view.isUserInteractionEnabled = false
return view
}()
// MARK: - vip
lazy var normalView: UIView = {
let view = UIView()
view.backgroundColor = .clear view.backgroundColor = .clear
view.showsVerticalScrollIndicator = false
view.addSubview(normalLottieView) view.bounces = false
normalLottieView.layoutChain
.top(20) let contentView = UIView()
.width(270) contentView.backgroundColor = .clear
.heightToWidth(1) view.addSubview(contentView)
.centerX() contentView.layoutChain.edges().widthToView(view)
view.addSubview(phoneAreaLab) contentView.addSubview(headerView)
phoneAreaLab.layoutChain headerView.layoutChain.top(12).edgesHorzontal()
.topToBottomOfView(normalLottieView)
.centerX() contentView.addSubview(resultCard)
resultCard.layoutChain
view.addSubview(messageLab) .topToBottomOfView(headerView, offset: 20)
messageLab.layoutChain
.topToBottomOfView(phoneAreaLab, offset: 10)
.centerX()
view.addSubview(tipsLab)
tipsLab.layoutChain
.topToBottomOfView(messageLab, offset: 15)
.edgesHorzontal(31)
view.addSubview(inviteBtn)
inviteBtn.layoutChain
.topToBottomOfView(tipsLab, offset: 30)
.edgesHorzontal(68)
.height(50)
view.addSubview(inviteView)
inviteView.layoutChain
.topToBottomOfView(tipsLab, offset: 25)
.edgesHorzontal(15) .edgesHorzontal(15)
.height(80)
contentView.addSubview(actionBtn)
actionBtn.layoutChain
.topToBottomOfView(resultCard, offset: 24)
.edgesHorzontal(30)
.height(56)
.bottom(kSafeBottomMargin + 30)
return view return view
}() }()
lazy var resultCard: UIView = {
let card = UIView()
card.backgroundColor = UIColor(hexStr: "#EFF9FF")
card.cornerRadius = 16
let locationRow = UIView()
card.addSubview(locationRow)
let pinIcon = UIImageView(image: UIImage(named: "Home/member_location"))
pinIcon.contentMode = .scaleAspectFit
locationRow.addSubview(pinIcon)
locationRow.addSubview(phoneAreaLab)
pinIcon.layoutChain.left().top().bottom().width(18).height(18)
phoneAreaLab.layoutChain
.leftToRightOfView(pinIcon, offset: 6)
.right()
.centerY(pinIcon)
locationRow.layoutChain.top(20).centerX()
let pillContainer = UIView()
pillContainer.backgroundColor = .white
pillContainer.cornerRadius = 14
card.addSubview(pillContainer)
pillContainer.addSubview(statusPillLab)
statusPillLab.layoutChain.edgesHorzontal(58).edgesVertical(13)
pillContainer.layoutChain
.topToBottomOfView(locationRow, offset: 11)
.centerX()
.height(48)
card.addSubview(disclaimerIcon)
disclaimerIcon.layoutChain
.topToBottomOfView(pillContainer, offset: 14)
.left(24)
.width(12)
.height(12)
card.addSubview(disclaimerLab)
disclaimerLab.layoutChain
.topToBottomOfView(pillContainer, offset: 11)
.leftToRightOfView(disclaimerIcon, offset: 3)
.right(23)
card.addSubview(inviteView)
inviteView.layoutChain
.topToBottomOfView(disclaimerLab, offset: 0)
.edgesHorzontal(12)
.height(0)
.bottom(16)
return card
}()
lazy var phoneAreaLab: UILabel = { lazy var phoneAreaLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = " " label.font = .systemFont(ofSize: 18, weight: .bold)
label.font = .systemFont(ofSize: 20, weight: .semibold) label.textColor = UIColor(hexStr: "#293445")
return label
}()
lazy var statusPillLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#16B3FF") label.textColor = UIColor(hexStr: "#16B3FF")
label.textAlignment = .center
return label return label
}() }()
lazy var messageLab: UILabel = { private lazy var disclaimerIcon: UIImageView = {
let label = UILabel() let iv = UIImageView(image: UIImage(named: "SearchLocation/warning"))
label.font = .systemFont(ofSize: 16, weight: .bold) iv.contentMode = .scaleAspectFit
label.textColor = UIColor(hexStr: "#333333") return iv
return label
}() }()
lazy var tipsLab: UILabel = { lazy var disclaimerLab: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .medium) label.font = .systemFont(ofSize: 12, weight: .regular)
label.textColor = UIColor(hexStr: "#999999") label.textColor = UIColor(hexStr: "#999999")
label.numberOfLines = 0 label.numberOfLines = 0
return label return label
}() }()
lazy var normalLottieView: LottieAnimationView = { lazy var actionBtn: UIButton = {
let view = LottieAnimationView()
view.loopMode = .loop
return view
}()
lazy var inviteBtn: UIButton = {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("邀请Ta加入", for: .normal)
btn.setTitleColor(.white, for: .normal) btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.titleLabel?.font = FontManager.boboBold(18)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25 btn.cornerRadius = 20
btn.isHidden = true btn.isHidden = true
return btn return btn
}() }()
var inviteBtn: UIButton { actionBtn }
lazy var inviteView: UIView = { lazy var inviteView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EFF9FF") view.backgroundColor = .white
view.cornerRadius = 16 view.cornerRadius = 12
view.isHidden = true view.isHidden = true
view.addSubview(successBtn) view.addSubview(successBtn)
successBtn.layoutChain successBtn.layoutChain.right(12).centerY().width(90).height(36)
.right(15)
.centerY()
.width(90)
.height(36)
view.addSubview(successTipsLab) view.addSubview(successTipsLab)
successTipsLab.layoutChain successTipsLab.layoutChain.topToCenterYOfView(view).left(12)
.topToCenterYOfView(view)
.left(15)
view.addSubview(phoneLab) view.addSubview(phoneLab)
phoneLab.layoutChain phoneLab.layoutChain.bottomToCenterYOfView(view).left(12)
.bottomToCenterYOfView(view)
.left(15)
return view return view
}() }()
lazy var phoneLab: UILabel = { lazy var phoneLab: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .bold) label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor(hexStr: "#333333") label.textColor = UIColor(hexStr: "#333333")
return label return label
}() }()
lazy var successTipsLab: UILabel = { lazy var successTipsLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "邀请加入圈子后分享位置"
label.font = .systemFont(ofSize: 10, weight: .medium) label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = UIColor(hexStr: "#999999") label.textColor = UIColor(hexStr: "#999999")
return label return label
}() }()
lazy var successBtn: UIButton = { lazy var successBtn: UIButton = {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("邀请Ta", for: .normal)
btn.setTitleColor(.white, for: .normal) btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.backgroundColor = UIColor(hexStr: "#16B3FF")
btn.cornerRadius = 18 btn.cornerRadius = 18
return btn return btn
}() }()
// MARK: - vip // VC
lazy var messageLab: UILabel = statusPillLab
lazy var tipsLab: UILabel = disclaimerLab
lazy var normalLottieView: LottieAnimationView = {
let view = LottieAnimationView()
view.isHidden = true
return view
}()
lazy var successLottieView: LottieAnimationView = {
let view = LottieAnimationView(name: "phone_search_fireworks")
view.isHidden = true
return view
}()
lazy var unlockVipView: UIView = { lazy var unlockVipView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = .clear view.backgroundColor = .white
view.clipsToBounds = true
view.isHidden = true view.isHidden = true
let bgImgView = UIImageView(image: UIImage(named: "SearchLocation/result_bg")) let heroPlaceholder = UIImageView()
bgImgView.contentMode = .scaleAspectFill heroPlaceholder.backgroundColor = UIColor(hexStr: "#F5F5F5")
view.addSubview(bgImgView) heroPlaceholder.cornerRadius = 16
bgImgView.layoutChain view.addSubview(heroPlaceholder)
.edges(excludingEdge: .bottom) heroPlaceholder.layoutChain
.heightToWidth(457/357) .top(120)
.edgesHorzontal(40)
view.addSubview(unlockVipLottieView) .height(200)
unlockVipLottieView.layoutChain
.edgesHorzontal(60)
.heightToWidth(1)
.top(150)
// .bottomToView(bgImgView, offset: -15)
view.addSubview(unlockVipPhoneAreaLab) view.addSubview(unlockVipPhoneAreaLab)
unlockVipPhoneAreaLab.layoutChain unlockVipPhoneAreaLab.layoutChain
.topToBottomOfView(unlockVipLottieView) .topToBottomOfView(heroPlaceholder, offset: 16)
.centerX() .centerX()
let locationView = UIView()
locationView.backgroundColor = .clear
view.addSubview(locationView)
locationView.layoutChain
.topToBottomOfView(unlockVipPhoneAreaLab, offset: 10)
.centerX()
let locationTitleLab = UILabel()
locationTitleLab.text = "位置:"
locationTitleLab.font = .systemFont(ofSize: 16, weight: .bold)
locationTitleLab.textColor = UIColor(hexStr: "#16B3FF")
locationView.addSubview(locationTitleLab)
locationTitleLab.layoutChain
.left()
.centerY()
let maskView = UIImageView(image: UIImage(named: "SearchLocation/mask"))
locationView.addSubview(maskView)
maskView.layoutChain
.leftToRightOfView(locationTitleLab)
.right()
.edgesVertical()
.width(144)
.height(30)
let unlockVipBtn = UIButton() let unlockVipBtn = UIButton()
unlockVipBtn.setTitle("开通会员", for: .normal) unlockVipBtn.setTitle("开通会员", for: .normal)
unlockVipBtn.setTitleColor(.white, for: .normal) unlockVipBtn.setTitleColor(.white, for: .normal)
unlockVipBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) unlockVipBtn.titleLabel?.font = FontManager.boboBold(16)
unlockVipBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) unlockVipBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
unlockVipBtn.cornerRadius = 25 unlockVipBtn.cornerRadius = 25
unlockVipBtn.rx.tap.subscribe(onNext: { _ in unlockVipBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.vipRecharge) AppRouter.push(Route.vipRecharge)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
view.addSubview(unlockVipBtn) view.addSubview(unlockVipBtn)
unlockVipBtn.layoutChain unlockVipBtn.layoutChain
.edgesHorzontal(15) .edgesHorzontal(15)
.bottom(kSafeBottomMargin + 20) .bottom(kSafeBottomMargin + 20)
.height(50) .height(50)
//
let tipsDot3 = UIView()
tipsDot3.backgroundColor = UIColor(hexStr: "#16B3FF")
tipsDot3.cornerRadius = 2.5
let tipsLab3 = UILabel()
tipsLab3.text = "最后出现地点"
tipsLab3.font = .systemFont(ofSize: 12, weight: .medium)
tipsLab3.textColor = UIColor(hexStr: "#333333")
view.addSubview(tipsLab3)
view.addSubview(tipsDot3)
tipsLab3.layoutChain
.bottomToTopOfView(unlockVipBtn, offset: -20)
.rightToCenterXOfView(view, offset: 30)
tipsDot3.layoutChain
.rightToLeftOfView(tipsLab3, offset: -8)
.width(5)
.heightToWidth(1)
.centerY(tipsLab3)
// "24"
let tipsDot4 = UIView()
tipsDot4.backgroundColor = UIColor(hexStr: "#16B3FF")
tipsDot4.cornerRadius = 2.5
let tipsLab4 = UILabel()
tipsLab4.text = "近24小时轨迹"
tipsLab4.font = .systemFont(ofSize: 12, weight: .medium)
tipsLab4.textColor = UIColor(hexStr: "#333333")
view.addSubview(tipsLab4)
view.addSubview(tipsDot4)
tipsDot4.layoutChain
.leftToCenterXOfView(view, offset: 30)
.width(5)
.heightToWidth(1)
.centerY(tipsLab4)
tipsLab4.layoutChain
.bottomToTopOfView(unlockVipBtn, offset: -20)
.leftToRightOfView(tipsDot4, offset: 8)
// 线
let tipsDot1 = UIView()
tipsDot1.backgroundColor = UIColor(hexStr: "#16B3FF")
tipsDot1.cornerRadius = 2.5
let tipsLab1 = UILabel()
tipsLab1.text = "是否在线"
tipsLab1.font = .systemFont(ofSize: 12, weight: .medium)
tipsLab1.textColor = UIColor(hexStr: "#333333")
view.addSubview(tipsLab1)
view.addSubview(tipsDot1)
tipsDot1.layoutChain
.leftToView(tipsDot3)
.width(5)
.heightToWidth(1)
.centerY(tipsLab1)
tipsLab1.layoutChain
.bottomToTopOfView(tipsLab3, offset: -10)
.leftToRightOfView(tipsDot1, offset: 8)
//
let tipsDot2 = UIView()
tipsDot2.backgroundColor = UIColor(hexStr: "#16B3FF")
tipsDot2.cornerRadius = 2.5
let tipsLab2 = UILabel()
tipsLab2.text = "实时位置变化"
tipsLab2.font = .systemFont(ofSize: 12, weight: .medium)
tipsLab2.textColor = UIColor(hexStr: "#333333")
view.addSubview(tipsLab2)
view.addSubview(tipsDot2)
tipsDot2.layoutChain
.leftToView(tipsDot4)
.width(5)
.heightToWidth(1)
.centerY(tipsLab2)
tipsLab2.layoutChain
.bottomToTopOfView(tipsLab4, offset: -10)
.leftToRightOfView(tipsDot2, offset: 8)
//
let unlockVipTitleLab = UILabel() let unlockVipTitleLab = UILabel()
unlockVipTitleLab.text = "再迈一步,你就能看到:" unlockVipTitleLab.text = "再迈一步,你就能看到:"
unlockVipTitleLab.font = .systemFont(ofSize: 14, weight: .medium) unlockVipTitleLab.font = .systemFont(ofSize: 14, weight: .medium)
unlockVipTitleLab.textColor = UIColor(hexStr: "#16B3FF") unlockVipTitleLab.textColor = UIColor(hexStr: "#16B3FF")
view.addSubview(unlockVipTitleLab) view.addSubview(unlockVipTitleLab)
unlockVipTitleLab.layoutChain unlockVipTitleLab.layoutChain
.bottomToTopOfView(tipsLab1, offset: -8) .bottomToTopOfView(unlockVipBtn, offset: -60)
.centerX() .centerX()
return view return view
}() }()
lazy var unlockVipLottieView: LottieAnimationView = { lazy var unlockVipLottieView: LottieAnimationView = {
let view = LottieAnimationView(name: "phone_search_no_vip") let view = LottieAnimationView(name: "phone_search_no_vip")
view.loopMode = .loop view.isHidden = true
return view return view
}() }()
lazy var unlockVipPhoneAreaLab: UILabel = { lazy var unlockVipPhoneAreaLab: UILabel = {
let label = UILabel() let label = UILabel()
label.text = " "
label.font = .systemFont(ofSize: 20, weight: .semibold) label.font = .systemFont(ofSize: 20, weight: .semibold)
label.textColor = UIColor(hexStr: "#16B3FF") label.textColor = UIColor(hexStr: "#16B3FF")
return label return label
}() }()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: .zero) super.init(frame: .zero)
backgroundColor = .white backgroundColor = .white
setupUI() setupUI()
setupRx()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {

View File

@ -2,8 +2,6 @@
// SearchLocationView.swift // SearchLocationView.swift
// QuickLocation // QuickLocation
// //
// Created by on 2026/6/27.
//
import UIKit import UIKit
import RxSwift import RxSwift
@ -21,6 +19,8 @@ class SearchLocationView: UIView {
private var player: AVPlayer? private var player: AVPlayer?
private var timeObserver: Any? private var timeObserver: Any?
let headerView = SearchLocationHeaderView()
private func setupRx() { private func setupRx() {
phoneInputTF.rx.text phoneInputTF.rx.text
.map { text -> String? in .map { text -> String? in
@ -28,21 +28,14 @@ class SearchLocationView: UIView {
return String(text.prefix(11)) return String(text.prefix(11))
}.bind(to: phoneInputTF.rx.text) }.bind(to: phoneInputTF.rx.text)
.disposed(by: disposeBag) .disposed(by: disposeBag)
phoneInputTF.rx.text.orEmpty.map { phone -> Bool in phoneInputTF.rx.text.orEmpty.map { $0.count == 11 }
if phone.count == 11 { .bind(to: searchBtn.rx.isEnabled)
return true .disposed(by: disposeBag)
} else {
return false
}
}
.bind(to: searchBtn.rx.isEnabled)
.disposed(by: disposeBag)
phoneInputTF.rx.controlEvent(.editingDidEndOnExit) phoneInputTF.rx.controlEvent(.editingDidEndOnExit)
.subscribe(onNext: { [weak self] in .subscribe(onNext: { [weak self] in
guard let self = self else { return } self?.phoneInputTF.resignFirstResponder()
self.phoneInputTF.resignFirstResponder()
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -53,127 +46,28 @@ class SearchLocationView: UIView {
addSubview(scrollView) addSubview(scrollView)
addSubview(videoView) addSubview(videoView)
addSubview(searchProgressView) addSubview(searchProgressView)
videoView.layoutChain.edges()
videoView.layoutChain.edges()
searchProgressView.layoutChain searchProgressView.layoutChain
.edgesHorzontal(15) .edgesHorzontal(15)
.bottom(kSafeBottomMargin + 20) .bottom(kSafeBottomMargin + 20)
.height(221) .height(221)
navBgView.layoutChain navBgView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
.heightToWidth(160/375) .heightToWidth(160 / 375)
navView.layoutChain navView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
.height(kNaviHeight) .height(kNaviHeight)
scrollView.layoutChain scrollView.layoutChain
.topToBottomOfView(navView) .topToBottomOfView(navView)
.edges(excludingEdge: .top) .edges(excludingEdge: .top)
} }
// MARK: - Marquee
private func randomPhoneNumber() -> String {
// 3
let prefixes = [
"134","135","136","137","138","139","147","150","151","152","157","158","159","178","182","183","184","187","188","198",
"130","131","132","145","155","156","166","175","176","185","186","196",
"133","149","153","173","177","180","181","189","191","199"
]
//
let prefix = prefixes.randomElement()!
// 8
var suffix = ""
for _ in 0..<8 {
suffix.append("\(Int.random(in: 0...9))")
}
return prefix + suffix
}
private func setupMarquee() {
let surnames = ["", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", ""]
let container = UIView()
container.backgroundColor = .clear
marqueeScrollView.addSubview(container)
var prevView: UIView?
for _ in 0..<10 {
let iconIndex = Int.random(in: 1...15)
let surname = surnames.randomElement() ?? ""
let maskedName = "\(surname)**"
let phone = randomPhoneNumber()
let maskedPhone = phone.prefix(3) + "******" + phone.suffix(2)
let itemView = UIView()
itemView.backgroundColor = UIColor(hexStr: "#EFF9FF")
itemView.cornerRadius = 6
container.addSubview(itemView)
let avatar = UIImageView(image: UIImage(named: "UserIcon/\(iconIndex)"))
avatar.contentMode = .scaleAspectFill
avatar.cornerRadius = 12
avatar.clipsToBounds = true
itemView.addSubview(avatar)
let label = UILabel()
label.font = .systemFont(ofSize: 13, weight: .medium)
let fullText = "\(maskedName) 定位到了 \(maskedPhone)"
let attr = NSMutableAttributedString(string: fullText)
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
range: NSRange(fullText.range(of: maskedName)!, in: fullText))
if let phoneRange = fullText.range(of: maskedPhone) {
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
range: NSRange(phoneRange, in: fullText))
}
label.attributedText = attr
itemView.addSubview(label)
avatar.layoutChain
.left(10).centerY()
.width(24).height(24)
label.layoutChain
.leftToRightOfView(avatar, offset: 6)
.centerY().right(10)
itemView.layoutChain
.centerY()
.height(30)
if let prev = prevView {
itemView.layoutChain.leftToRightOfView(prev, offset: 20)
} else {
itemView.layoutChain.left()
}
prevView = itemView
}
if let last = prevView {
container.layoutChain
.edges().heightToView(marqueeScrollView)
.rightToView(last)
}
}
func startMarqueeAnimation() { func startMarqueeAnimation() {
marqueeScrollView.layer.removeAllAnimations() headerView.startMarqueeAnimation()
marqueeScrollView.contentOffset.x = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
let contentW = self.marqueeScrollView.contentSize.width
guard contentW > self.marqueeScrollView.bounds.width else { return }
let duration = contentW / 100
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear, .repeat]) {
self.marqueeScrollView.contentOffset.x = contentW - self.marqueeScrollView.bounds.width
}
}
} }
override func willMove(toWindow newWindow: UIWindow?) { override func willMove(toWindow newWindow: UIWindow?) {
@ -183,56 +77,41 @@ class SearchLocationView: UIView {
} }
} }
// MARK: - Views
lazy var navBgView: UIImageView = { lazy var navBgView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2") iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill iv.contentMode = .scaleAspectFill
iv.isHidden = true
return iv return iv
}() }()
lazy var navView: BaseNavigationView = { lazy var navView: BaseNavigationView = {
let nav = BaseNavigationView(title: "找人助手") BaseNavigationView(title: " ")
return nav
}() }()
lazy var scrollView: UIScrollView = { lazy var scrollView: UIScrollView = {
let view = UIScrollView() let view = UIScrollView()
view.backgroundColor = .clear view.backgroundColor = .clear
view.showsVerticalScrollIndicator = false view.showsVerticalScrollIndicator = false
view.bounces = false view.bounces = false
let contentView = UIView() let contentView = UIView()
contentView.backgroundColor = .clear contentView.backgroundColor = .clear
view.addSubview(contentView) view.addSubview(contentView)
contentView.layoutChain.edges().widthToView(view) contentView.layoutChain.edges().widthToView(view)
let bgImgView = UIImageView() contentView.addSubview(headerView)
bgImgView.image = UIImage(named: "SearchLocation/bg_1") headerView.layoutChain
contentView.addSubview(bgImgView) .top(12)
bgImgView.layoutChain
.top(15)
.edgesHorzontal(56.5)
.heightToWidth(626/524)
contentView.addSubview(titleLab)
titleLab.layoutChain
.topToBottomOfView(bgImgView, offset: 0)
.centerX()
contentView.addSubview(marqueeScrollView)
marqueeScrollView.layoutChain
.topToBottomOfView(titleLab, offset: 10)
.edgesHorzontal() .edgesHorzontal()
.height(30) .widthToView(contentView)
contentView.addSubview(searchInputView) contentView.addSubview(searchInputView)
searchInputView.layoutChain searchInputView.layoutChain
.topToBottomOfView(marqueeScrollView, offset: 20) .topToBottomOfView(headerView, offset: 20)
.edgesHorzontal(15) .edgesHorzontal(15)
.bottom(kSafeBottomMargin + 30) .bottom(kSafeBottomMargin + 30)
return view return view
}() }()
@ -240,12 +119,12 @@ class SearchLocationView: UIView {
let v = UIView() let v = UIView()
v.backgroundColor = .black v.backgroundColor = .black
v.isHidden = true v.isHidden = true
return v return v
}() }()
func playVideo(completion: (() -> Void)? = nil) { func playVideo(completion: (() -> Void)? = nil) {
guard let path = Bundle.main.path(forResource: "search_interlude", ofType: "mp4") ?? Bundle.main.path(forResource: "search_interlude", ofType: "mp4", inDirectory: "video") else { guard let path = Bundle.main.path(forResource: "search_interlude", ofType: "mp4")
?? Bundle.main.path(forResource: "search_interlude", ofType: "mp4", inDirectory: "video") else {
print("[video] file not found in bundle: \(Bundle.main.resourcePath ?? "")") print("[video] file not found in bundle: \(Bundle.main.resourcePath ?? "")")
return return
} }
@ -259,7 +138,6 @@ class SearchLocationView: UIView {
playerLayer.videoGravity = .resizeAspectFill playerLayer.videoGravity = .resizeAspectFill
videoView.layer.addSublayer(playerLayer) videoView.layer.addSublayer(playerLayer)
//
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: 600), queue: .main) { [weak self] time in timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: 600), queue: .main) { [weak self] time in
guard let self = self, let duration = self.player?.currentItem?.duration.seconds, duration > 0 else { return } guard let self = self, let duration = self.player?.currentItem?.duration.seconds, duration > 0 else { return }
self.updateVideoProgress(Float(time.seconds / duration)) self.updateVideoProgress(Float(time.seconds / duration))
@ -288,52 +166,24 @@ class SearchLocationView: UIView {
} }
} }
lazy var titleLab: UILabel = {
let label = UILabel()
label.text = "\(Int.random(in: 1000...10000)) 人正在使用此功能"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var marqueeScrollView: UIScrollView = {
let sv = UIScrollView()
sv.backgroundColor = .clear
sv.showsHorizontalScrollIndicator = false
sv.isScrollEnabled = false
return sv
}()
lazy var searchInputView: UIView = { lazy var searchInputView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EFF9FF") view.backgroundColor = UIColor(hexStr: "#EFFAFF")
view.cornerRadius = 10 view.cornerRadius = 40
let titleLab = UILabel()
titleLab.text = "TA在哪输入号码就知道"
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
view.addSubview(titleLab)
titleLab.layoutChain
.top(18)
.left(15)
let inputView = UIView() let inputView = UIView()
inputView.backgroundColor = .white inputView.backgroundColor = .white
inputView.cornerRadius = 4 inputView.cornerRadius = 14
view.addSubview(inputView) view.addSubview(inputView)
inputView.layoutChain inputView.layoutChain
.topToBottomOfView(titleLab, offset: 20) .top(20)
.edgesHorzontal(15) .edgesHorzontal(15)
.height(40) .height(44)
let contactsBtn = UIButton() let contactsBtn = UIButton()
contactsBtn.setTitle(" 通讯录导入", for: .normal) contactsBtn.setTitle("通讯录导入", for: .normal)
contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular) contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular)
contactsBtn.setImage(UIImage(named: "SearchLocation/contacts"), for: .normal)
contactsBtn.extendEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15)
contactsBtn.rx.tap.subscribe(onNext: { [weak self] in contactsBtn.rx.tap.subscribe(onNext: { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.phoneInputTF.resignFirstResponder() self.phoneInputTF.resignFirstResponder()
@ -352,11 +202,7 @@ class SearchLocationView: UIView {
} }
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
inputView.addSubview(contactsBtn) inputView.addSubview(contactsBtn)
contactsBtn.layoutChain contactsBtn.layoutChain.edgesVertical().width(60).right(12)
.right(15)
.width(90)
.edgesVertical()
contactsBtn.sizeToFit()
inputView.addSubview(phoneInputTF) inputView.addSubview(phoneInputTF)
phoneInputTF.layoutChain phoneInputTF.layoutChain
@ -364,21 +210,31 @@ class SearchLocationView: UIView {
.left(15) .left(15)
.rightToLeftOfView(contactsBtn, offset: -8) .rightToLeftOfView(contactsBtn, offset: -8)
let warningIcon = UIImageView(image: UIImage(named: "SearchLocation/warning"))
warningIcon.contentMode = .scaleAspectFit
view.addSubview(warningIcon)
warningIcon.layoutChain
.topToBottomOfView(inputView, offset: 10)
.left(15)
.width(14)
.height(14)
let tipsLab = UILabel() let tipsLab = UILabel()
tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私" tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私"
tipsLab.font = .systemFont(ofSize: 12, weight: .regular) tipsLab.font = .systemFont(ofSize: 12, weight: .regular)
tipsLab.textColor = ThemeManager.shared.color.contentColor tipsLab.textColor = UIColor(hexStr: "#999999")
view.addSubview(tipsLab) view.addSubview(tipsLab)
tipsLab.layoutChain tipsLab.layoutChain
.topToBottomOfView(inputView, offset: 6) .leftToRightOfView(warningIcon, offset: 4)
.centerY(warningIcon)
.right(15) .right(15)
view.addSubview(searchBtn) view.addSubview(searchBtn)
searchBtn.layoutChain searchBtn.layoutChain
.topToBottomOfView(tipsLab, offset: 35) .topToBottomOfView(warningIcon, offset: 24)
.edgesHorzontal(52) .edgesHorzontal(20)
.height(50) .height(50)
.bottom(30) .bottom(24)
return view return view
}() }()
@ -386,7 +242,7 @@ class SearchLocationView: UIView {
lazy var phoneInputTF: UITextField = { lazy var phoneInputTF: UITextField = {
let textField = UITextField() let textField = UITextField()
textField.font = .systemFont(ofSize: 14, weight: .medium) textField.font = .systemFont(ofSize: 14, weight: .medium)
textField.placeholder = "请输入要查找的号码" textField.placeholder = "输入要查看的号码"
textField.keyboardType = .numberPad textField.keyboardType = .numberPad
textField.returnKeyType = .done textField.returnKeyType = .done
return textField return textField
@ -396,12 +252,12 @@ class SearchLocationView: UIView {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("开始查找", for: .normal) btn.setTitle("开始查找", for: .normal)
btn.setTitleColor(.white, for: .normal) btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.titleLabel?.font = FontManager.boboBold(16)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25 btn.cornerRadius = 20
return btn return btn
}() }()
lazy var searchProgressView: UIView = { lazy var searchProgressView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.8) view.backgroundColor = .black.withAlphaComponent(0.8)
@ -409,17 +265,17 @@ class SearchLocationView: UIView {
view.borderWidth = 0.5 view.borderWidth = 0.5
view.borderColor = .white view.borderColor = .white
view.isHidden = true view.isHidden = true
view.addSubview(searchPhoneLottieView) view.addSubview(searchPhoneLottieView)
searchPhoneLottieView.layoutChain searchPhoneLottieView.layoutChain
.top(30) .top(30)
.left(15) .left(15)
.width(120) .width(120)
.heightToWidth(1) .heightToWidth(1)
let steps = ["分析用户号码", "正在找 CGI 蜂窝参数", "SS7 信息交流", "号码已获授权", "处理完成"] let steps = ["分析用户号码", "正在找 CGI 蜂窝参数", "SS7 信息交流", "号码已获授权", "处理完成"]
var prevStepView: UIView? var prevStepView: UIView?
for (i, text) in steps.enumerated() { for text in steps {
let stepView = UIView() let stepView = UIView()
view.addSubview(stepView) view.addSubview(stepView)
@ -434,14 +290,8 @@ class SearchLocationView: UIView {
label.textColor = .white label.textColor = .white
stepView.addSubview(label) stepView.addSubview(label)
icon.layoutChain icon.layoutChain.left().centerY().width(18).height(18)
.left().centerY() label.layoutChain.leftToRightOfView(icon, offset: 8).centerY().right()
.width(18).height(18)
label.layoutChain
.leftToRightOfView(icon, offset: 8)
.centerY().right()
stepView.layoutChain stepView.layoutChain
.leftToRightOfView(searchPhoneLottieView, offset: 10) .leftToRightOfView(searchPhoneLottieView, offset: 10)
.right(10) .right(10)
@ -455,7 +305,6 @@ class SearchLocationView: UIView {
prevStepView = stepView prevStepView = stepView
} }
// +
progressBar.layer.cornerRadius = 4 progressBar.layer.cornerRadius = 4
progressBar.clipsToBounds = true progressBar.clipsToBounds = true
progressBar.trackTintColor = .white.withAlphaComponent(0.2) progressBar.trackTintColor = .white.withAlphaComponent(0.2)
@ -483,16 +332,13 @@ class SearchLocationView: UIView {
private var progressIcons: [UIImageView] = [] private var progressIcons: [UIImageView] = []
private lazy var progressBar: UIProgressView = { private lazy var progressBar: UIProgressView = {
let p = UIProgressView() UIProgressView()
return p
}() }()
private lazy var progressLab: UILabel = { private lazy var progressLab: UILabel = {
let l = UILabel() UILabel()
return l
}() }()
/// (0~1)
func updateVideoProgress(_ progress: Float) { func updateVideoProgress(_ progress: Float) {
let clamped = max(0, min(1, progress)) let clamped = max(0, min(1, progress))
progressBar.progress = clamped progressBar.progress = clamped
@ -517,7 +363,6 @@ class SearchLocationView: UIView {
backgroundColor = .white backgroundColor = .white
setupUI() setupUI()
setupRx() setupRx()
setupMarquee()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {

View File

@ -0,0 +1,70 @@
//
// AboutVC.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
final class AboutVC: BaseViewController {
private var rootView: AboutView!
override func loadView() {
rootView = AboutView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
private func bind() {
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
rootView.feedbackBtn.rx.controlEvent(.touchUpInside)
.subscribe(onNext: { _ in
DLToast.show(text: "敬请期待")
})
.disposed(by: disposeBag)
rootView.rateBtn.rx.controlEvent(.touchUpInside)
.subscribe(onNext: { _ in
Self.openAppStoreReview()
})
.disposed(by: disposeBag)
rootView.agreementBtn.rx.controlEvent(.touchUpInside)
.subscribe(onNext: { _ in
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.userAgreementUrl])
})
.disposed(by: disposeBag)
rootView.privacyBtn.rx.controlEvent(.touchUpInside)
.subscribe(onNext: { _ in
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.privacyPolicyUrl])
})
.disposed(by: disposeBag)
rootView.kidsPrivacyBtn.rx.controlEvent(.touchUpInside)
.subscribe(onNext: { _ in
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.kidsPrivacyUrl])
})
.disposed(by: disposeBag)
}
private static func openAppStoreReview() {
// Bundle appId
let appId = AppSettings.shared.appId
guard !appId.isEmpty,
let url = URL(string: "itms-apps://itunes.apple.com/app/id\(appId)?action=write-review") else {
DLToast.show(text: "暂无法打开评价")
return
}
UIApplication.shared.open(url)
}
}

View File

@ -0,0 +1,124 @@
//
// AboutView.swift
// QuickLocation
//
import UIKit
final class AboutView: UIView {
lazy var navView: BaseNavigationView = {
BaseNavigationView(title: "关于我们")
}()
lazy var navBgView: UIImageView = {
let iv = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
iv.contentMode = .scaleAspectFill
return iv
}()
let logoImage = UIImageView(image: UIImage(named: "Mine/"))
let nameImage = UIImageView(image: UIImage(named: "Mine/about_logo"))
let versionLab = UILabel()
let feedbackBtn = AboutMenuButton(title: "意见反馈")
let rateBtn = AboutMenuButton(title: "给个好评")
let agreementBtn = AboutMenuButton(title: "用户协议")
let privacyBtn = AboutMenuButton(title: "隐私政策")
let kidsPrivacyBtn = AboutMenuButton(title: "儿童隐私政策")
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#FAFAFA")
setupUI()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupUI() {
addSubview(navBgView)
addSubview(navView)
navBgView.layoutChain.edges(excludingEdge: .bottom).heightToWidth(160 / 375)
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
logoImage.contentMode = .scaleAspectFill
logoImage.backgroundColor = .lightGray
logoImage.cornerRadius = 30
addSubview(logoImage)
logoImage.layoutChain
.topToBottomOfView(navView, offset: 67)
.centerX()
.width(100)
.heightToWidth(1)
addSubview(nameImage)
nameImage.layoutChain
.topToBottomOfView(logoImage, offset: 24)
.centerX()
.width(114)
.height(25)
let card1 = makeCard()
addSubview(card1)
card1.layoutChain.topToBottomOfView(nameImage, offset: 40).edgesHorzontal(15)
stackInCard(card1, buttons: [feedbackBtn, rateBtn])
let card2 = makeCard()
addSubview(card2)
card2.layoutChain.topToBottomOfView(card1, offset: 12).edgesHorzontal(15)
stackInCard(card2, buttons: [agreementBtn, privacyBtn, kidsPrivacyBtn])
versionLab.text = kAppShortVersion
versionLab.font = .systemFont(ofSize: 12, weight: .medium)
versionLab.textColor = UIColor(hexStr: "#AAAAAA")
versionLab.textAlignment = .center
addSubview(versionLab)
versionLab.layoutChain.bottom(34).centerX()
}
private func makeCard() -> UIView {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = 16
v.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.08).cgColor
v.layer.shadowOffset = CGSize(width: 0, height: 2)
v.layer.shadowOpacity = 1
v.layer.shadowRadius = 8
return v
}
private func stackInCard(_ card: UIView, buttons: [AboutMenuButton]) {
let stack = UIStackView(arrangedSubviews: buttons)
stack.axis = .vertical
stack.spacing = 0
card.addSubview(stack)
stack.layoutChain.edges()
buttons.forEach { $0.layoutChain.height(52) }
for (i, btn) in buttons.enumerated() where i < buttons.count - 1 {
let line = UIView()
line.backgroundColor = UIColor(hexStr: "#F2F2F2")
btn.addSubview(line)
line.layoutChain.left(15).right(15).bottom(0).height(0.5)
}
}
}
final class AboutMenuButton: UIControl {
private let titleLab = UILabel()
private let arrow = UIImageView(image: UIImage(named: "Mine/arrow_right"))
init(title: String) {
super.init(frame: .zero)
titleLab.text = title
titleLab.font = .systemFont(ofSize: 15, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#293445")
addSubview(titleLab)
titleLab.layoutChain.left(15).centerY()
arrow.contentMode = .scaleAspectFit
addSubview(arrow)
arrow.layoutChain.right(15).centerY().width(12).height(12)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

View File

@ -53,9 +53,7 @@ class AccountVC: BaseViewController {
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
rootView.cancelledBtn.rx.tap.subscribe(onNext: { _ in rootView.cancelledBtn.rx.tap.subscribe(onNext: { _ in
CancellationPopView.show { AppRouter.push(Route.cancelAccount)
self.deleteAccount()
}
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleClearCacheTap)) let tap = UITapGestureRecognizer(target: self, action: #selector(handleClearCacheTap))

View File

@ -290,10 +290,10 @@ class AccountView: UIView {
lazy var logoutBtn: UIButton = { lazy var logoutBtn: UIButton = {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("退出登录", for: .normal) btn.setTitle("退出登录", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.titleLabel?.font = FontManager.boboBold(18)
btn.setTitleColor(.white, for: .normal) btn.setTitleColor(.white, for: .normal)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25 btn.cornerRadius = 16
return btn return btn
}() }()

View File

@ -0,0 +1,105 @@
//
// CancelAccountVC.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
#if !targetEnvironment(simulator)
import GeYanSdk
#endif
final class CancelAccountVC: BaseViewController {
private var rootView: CancelAccountView!
private let requiredPhrase = "我自愿注销本账号"
override func loadView() {
rootView = CancelAccountView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
private func bind() {
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
rootView.checkItems.forEach { item in
item.checkBtn.rx.tap
.subscribe(onNext: { [weak self, weak item] in
item?.checkBtn.isSelected.toggle()
self?.refreshSaveEnabled()
})
.disposed(by: disposeBag)
}
rootView.confirmInput.rx.text.orEmpty
.subscribe(onNext: { [weak self] _ in
self?.refreshSaveEnabled()
})
.disposed(by: disposeBag)
rootView.saveBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.submit()
})
.disposed(by: disposeBag)
}
private func refreshSaveEnabled() {
let allChecked = rootView.checkItems.allSatisfy(\.isChecked)
let phraseOK = rootView.confirmInput.text == requiredPhrase
rootView.setSaveEnabled(allChecked && phraseOK)
}
private func submit() {
guard rootView.checkItems.allSatisfy(\.isChecked),
rootView.confirmInput.text == requiredPhrase else { return }
DLToast.showLoading()
UserService.deleteAccount().subscribe(onNext: { [weak self] _ in
DLToast.dismiss()
self?.showSuccessToast {
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
#if !targetEnvironment(simulator)
GeYanSdk.deletePreResultCache()
#endif
AppContextManager.shared.deleteAccount()
GroupIMService.shared.logout()
AppDelegate.shared.showMainViewController()
}
}, onError: { _ in }).disposed(by: disposeBag)
}
private func showSuccessToast(completion: @escaping () -> Void) {
let toast = UIView()
toast.backgroundColor = UIColor.black.withAlphaComponent(0.75)
toast.layer.cornerRadius = 12
view.addSubview(toast)
toast.layoutChain.center().width(120).height(120)
let icon = UIImageView(image: UIImage(systemName: "checkmark.circle"))
icon.tintColor = .white
icon.contentMode = .scaleAspectFit
toast.addSubview(icon)
icon.layoutChain.top(28).centerX().width(36).height(36)
let lab = UILabel()
lab.text = "注销成功"
lab.textColor = .white
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textAlignment = .center
toast.addSubview(lab)
lab.layoutChain.topToBottomOfView(icon, offset: 12).edgesHorzontal(8)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
toast.removeFromSuperview()
completion()
}
}
}

View File

@ -0,0 +1,165 @@
//
// CancelAccountView.swift
// QuickLocation
//
import UIKit
final class CancelAccountView: UIView {
lazy var navView = BaseNavigationView(title: "注销账号")
let scrollView = UIScrollView()
let contentView = UIView()
let warningIcon = UIImageView(image: UIImage(named: "Mine/cancel_warning"))
let introLab = UILabel()
let card = UIView()
let confirmInput = UITextField()
let saveBtn = UIButton(type: .custom)
private(set) var checkItems: [CancelCheckItemView] = []
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#F9F9F9")
setupUI()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupUI() {
addSubview(navView)
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
addSubview(saveBtn)
saveBtn.setTitle("保存", for: .normal)
saveBtn.titleLabel?.font = FontManager.boboBold(18)
saveBtn.setTitleColor(.white, for: .normal)
saveBtn.backgroundColor = UIColor(hexStr: "#FF9B9B")
saveBtn.layer.cornerRadius = 20
saveBtn.isEnabled = false
saveBtn.alpha = 0.5
saveBtn.layoutChain.left(20).right(20).bottom(34).height(56)
addSubview(scrollView)
scrollView.keyboardDismissMode = .onDrag
scrollView.backgroundColor = .clear
scrollView.layoutChain
.topToBottomOfView(navView)
.edgesHorzontal()
.bottomToTopOfView(saveBtn, offset: -12)
scrollView.addSubview(contentView)
contentView.backgroundColor = UIColor(hexStr: "#F9F9F9")
contentView.layoutChain.edges().widthToView(scrollView)
warningIcon.contentMode = .scaleAspectFit
contentView.addSubview(warningIcon)
warningIcon.layoutChain.top(24).centerX().width(68).height(68)
introLab.text = "为保证你的账号安全,在你提交的注销申请生效前,需要同时满足以下条件:"
introLab.font = .systemFont(ofSize: 16, weight: .medium)
introLab.textColor = UIColor(hexStr: "#3D3D3D")
introLab.numberOfLines = 0
contentView.addSubview(introLab)
introLab.layoutChain.topToBottomOfView(warningIcon, offset: 16).edgesHorzontal(20)
card.backgroundColor = .white
card.layer.cornerRadius = 24
contentView.addSubview(card)
card.layoutChain
.topToBottomOfView(introLab, offset: 16)
.edgesHorzontal(15)
.bottom(20)
let specs: [(String, String)] = [
("1. 放弃当前的VIP权益", "当前充值的VIP我们不会退款请解除你的自动续费如果有同意请打勾"),
("2. 当前账号没有违规及其他的法律纠纷", "同意请打勾"),
("3. 确认是账号本人进行注销操作,自愿承担后果", "同意请打勾"),
("4. 账号注销后账号不可登录,不可恢复", "同意请打勾")
]
var previous: UIView?
for spec in specs {
let item = CancelCheckItemView(title: spec.0, subtitle: spec.1)
checkItems.append(item)
card.addSubview(item)
if let previous {
item.layoutChain.topToBottomOfView(previous).edgesHorzontal(14)
} else {
item.layoutChain.top(14).edgesHorzontal(14)
}
previous = item
}
let tip5 = UILabel()
tip5.text = "5. 请在输入框中输入以下文字"
tip5.font = .systemFont(ofSize: 16, weight: .medium)
tip5.textColor = UIColor(hexStr: "#293445")
card.addSubview(tip5)
tip5.layoutChain.topToBottomOfView(previous!, offset: 12).left(14).right(14)
let phrase = UILabel()
phrase.text = "我自愿注销本账号"
phrase.font = .systemFont(ofSize: 16, weight: .medium)
phrase.textColor = UIColor(hexStr: "#F64545")
card.addSubview(phrase)
phrase.layoutChain.topToBottomOfView(tip5, offset: 8).leftToView(tip5, offset: 16)
confirmInput.backgroundColor = UIColor(hexStr: "#F5F5F5")
confirmInput.layer.cornerRadius = 8
confirmInput.borderWidth = 1
confirmInput.borderColor = UIColor(hexStr: "#DEDEDE")
confirmInput.font = .systemFont(ofSize: 14, weight: .medium)
confirmInput.textColor = UIColor(hexStr: "#293445")
confirmInput.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
confirmInput.leftViewMode = .always
confirmInput.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
confirmInput.rightViewMode = .always
card.addSubview(confirmInput)
confirmInput.layoutChain
.topToBottomOfView(phrase, offset: 10)
.left(14)
.right(14)
.height(40)
.bottom(16)
}
func setSaveEnabled(_ enabled: Bool) {
saveBtn.isEnabled = enabled
saveBtn.alpha = enabled ? 1 : 0.5
saveBtn.backgroundColor = UIColor(hexStr: enabled ? "#FF5252" : "#FF9B9B")
}
}
final class CancelCheckItemView: UIView {
let checkBtn = UIButton(type: .custom)
private let titleLab = UILabel()
private let subLab = UILabel()
var isChecked: Bool { checkBtn.isSelected }
init(title: String, subtitle: String) {
super.init(frame: .zero)
titleLab.text = title
titleLab.font = .systemFont(ofSize: 15, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#293445")
titleLab.numberOfLines = 0
addSubview(titleLab)
subLab.text = subtitle
subLab.font = .systemFont(ofSize: 13, weight: .medium)
subLab.textColor = UIColor(hexStr: "#767676")
subLab.numberOfLines = 0
addSubview(subLab)
checkBtn.setBackgroundImage(UIImage(named: "Common/checkbox"), for: .normal)
checkBtn.setBackgroundImage(UIImage(named: "Common/checkbox_on"), for: .selected)
checkBtn.extendEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 20, right: 20)
addSubview(checkBtn)
titleLab.layoutChain.top(10).left(0).rightToLeftOfView(checkBtn, offset: 8)
subLab.layoutChain.topToBottomOfView(titleLab, offset: 4).left(0).rightToLeftOfView(checkBtn, offset: 12).bottom(10)
checkBtn.layoutChain.right(0).centerY(titleLab).width(24).height(24)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

View File

@ -12,7 +12,8 @@ final class MinePhotoWallView: UIView {
var onCenteredMember: ((GroupMemberModel) -> Void)? var onCenteredMember: ((GroupMemberModel) -> Void)?
var onCopyID: ((String) -> Void)? var onCopyID: ((String) -> Void)?
var onTapSelfAvatar: (() -> Void)? ///
var onTapMember: ((GroupMemberModel) -> Void)?
private var members: [GroupMemberModel] = [] private var members: [GroupMemberModel] = []
/// members /// members
@ -463,15 +464,13 @@ extension MinePhotoWallView: UICollectionViewDataSource, UICollectionViewDelegat
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = members[indexPath.item] let model = members[indexPath.item]
if model.user_id == AppContextManager.shared.userId {
onTapSelfAvatar?()
}
centeredIndex = indexPath.item centeredIndex = indexPath.item
let before = collectionView.contentOffset let before = collectionView.contentOffset
scrollToIndex(indexPath.item, animated: true) scrollToIndex(indexPath.item, animated: true)
applyArcAndFocus() applyArcAndFocus()
updateInfo(model) updateInfo(model)
onCenteredMember?(model) onCenteredMember?(model)
onTapMember?(model)
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if abs(self.collectionView.contentOffset.x - before.x) < 0.5, !self.isDragging { if abs(self.collectionView.contentOffset.x - before.x) < 0.5, !self.isDragging {

View File

@ -9,6 +9,7 @@ import UIKit
import RxSwift import RxSwift
import RxCocoa import RxCocoa
import RxDataSources import RxDataSources
import ObjectMapper
final class MineViewController: BaseViewController { final class MineViewController: BaseViewController {
@ -23,6 +24,8 @@ final class MineViewController: BaseViewController {
private let viewModel = MineViewModel() private let viewModel = MineViewModel()
private var groupKey: String = "" private var groupKey: String = ""
private var groupModel: GroupModel?
private var photoWallMembers: [GroupMemberModel] = []
override func loadView() { override func loadView() {
rootView = MineView(frame: UIScreen.main.bounds) rootView = MineView(frame: UIScreen.main.bounds)
@ -64,8 +67,8 @@ final class MineViewController: BaseViewController {
DLToast.showSuccess(text: "已复制") DLToast.showSuccess(text: "已复制")
} }
rootView.photoWall.onTapSelfAvatar = { rootView.photoWall.onTapMember = { [weak self] member in
AppRouter.push(Route.myProfile) self?.openMemberInfo(selectedUserId: member.user_id)
} }
rootView.onQuickTap = { [weak self] title in rootView.onQuickTap = { [weak self] title in
@ -94,8 +97,10 @@ final class MineViewController: BaseViewController {
private func requestGroupMembers() { private func requestGroupMembers() {
GroupService.groupInfo().subscribe(onNext: { [weak self] response in GroupService.groupInfo().subscribe(onNext: { [weak self] response in
guard let self = self, let model = response.model else { return } guard let self = self, let model = response.model else { return }
self.groupModel = model
self.groupKey = model.default_group_key self.groupKey = model.default_group_key
let sorted = self.viewModel.sortedMembers(model.select_group_employee, groupKey: model.default_group_key) let sorted = self.viewModel.sortedMembers(model.select_group_employee, groupKey: model.default_group_key)
self.photoWallMembers = sorted
// idgroup_key userId // idgroup_key userId
let ownerId = sorted.first(where: { model.default_group_key.contains($0.user_id) })?.user_id ?? "" let ownerId = sorted.first(where: { model.default_group_key.contains($0.user_id) })?.user_id ?? ""
self.rootView.photoWall.reload( self.rootView.photoWall.reload(
@ -105,4 +110,17 @@ final class MineViewController: BaseViewController {
) )
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
private func openMemberInfo(selectedUserId: String) {
let members = photoWallMembers.map { $0.toJSON() }
guard !members.isEmpty else { return }
var userInfo: [String: Any] = [
"members": members,
"userId": selectedUserId
]
if let group = groupModel {
userInfo["groupJson"] = group.toJSON()
}
AppRouter.push(Route.memberInfo, userInfo: userInfo)
}
} }

View File

@ -59,8 +59,10 @@ class MineViewModel {
requestWechatService() requestWechatService()
case "权限检测": case "权限检测":
AppRouter.push(Route.checkPermission) AppRouter.push(Route.checkPermission)
case "数据安全", "状态设置", "关于我们": case "数据安全", "状态设置":
DLToast.show(text: "敬请期待") DLToast.show(text: "敬请期待")
case "关于我们":
AppRouter.push(Route.about)
default: default:
break break
} }

View File

@ -15,7 +15,11 @@ class VipRechargeView: UIView {
private func setupRx() { private func setupRx() {
navView.onBackTapped = { VipWaivePopView.show() } navView.onBackTapped = { VipWaivePopView.show() }
backBtn.rx.tap.subscribe { _ in
VipWaivePopView.show()
}.disposed(by: disposeBag)
agreementLab.rx.tapGesture.subscribe { _ in agreementLab.rx.tapGesture.subscribe { _ in
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.userAgreementUrl]) AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.userAgreementUrl])
}.disposed(by: disposeBag) }.disposed(by: disposeBag)
@ -45,11 +49,19 @@ class VipRechargeView: UIView {
bottomView.addSubview(payBtnView) bottomView.addSubview(payBtnView)
bottomView.addSubview(payPriceView) bottomView.addSubview(payPriceView)
addSubview(backBtn)
addSubview(navView) addSubview(navView)
backBtn.layoutChain
.top(kStatusBarHeight + 5)
.left(15)
.width(36)
.height(36)
navView.layoutChain navView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
.height(kNaviHeight) .height(kNaviHeight)
navView.backgroundColor = .white
navView.alpha = 0 navView.alpha = 0
bottomView.layoutChain bottomView.layoutChain
@ -131,6 +143,13 @@ class VipRechargeView: UIView {
lazy var navView = BaseNavigationView(title: "升级会员") lazy var navView = BaseNavigationView(title: "升级会员")
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var scrollView: UIScrollView = { lazy var scrollView: UIScrollView = {
let view = UIScrollView() let view = UIScrollView()
view.backgroundColor = UIColor(hexStr: "#F5FBFB") view.backgroundColor = UIColor(hexStr: "#F5FBFB")
@ -716,7 +735,7 @@ class VipRechargeView: UIView {
extension VipRechargeView: UIScrollViewDelegate { extension VipRechargeView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
let maxY = scrollView.contentOffset.y let maxY = scrollView.contentOffset.y
let alpha = maxY / kNaviHeight let alpha = maxY / (kNaviHeight / 2)
navView.alpha = alpha < 0.0 ? 0.0 : alpha navView.alpha = alpha < 0.0 ? 0.0 : alpha
} }
} }