- 二级页面
|
|
@ -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 */,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 367 B |
|
After Width: | Height: | Size: 780 B |
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/SearchLocation/warning.imageset/warning@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 293 B |
BIN
QuickLocation/Assets.xcassets/SearchLocation/warning.imageset/warning@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 546 B |
|
|
@ -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") }
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -363,6 +373,31 @@ extension AppRouter: AppRouterProtocol {
|
||||||
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
|
||||||
PromotionalActivitiesVC()
|
PromotionalActivitiesVC()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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") }
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
// 下载图片到相册
|
// 下载图片到相册
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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?() }
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -39,141 +43,43 @@ class CreateBubbleDoneView: UIView {
|
||||||
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)
|
||||||
|
.height(120)
|
||||||
|
|
||||||
timeLab.layoutChain
|
timeLab.font = .systemFont(ofSize: 28, weight: .heavy)
|
||||||
.topToBottomOfView(titleLab, offset: 12)
|
timeLab.textColor = .white
|
||||||
.centerX()
|
timeLab.textAlignment = .center
|
||||||
|
timeLab.layoutChain.centerX(clockImage).centerY(clockImage, offset: 4)
|
||||||
|
|
||||||
timeIcon.layoutChain
|
popupBtn.setTitle("弹出气泡", for: .normal)
|
||||||
.rightToLeftOfView(timeLab, offset: -8)
|
popupBtn.setTitleColor(.white, for: .normal)
|
||||||
.centerY(timeLab)
|
popupBtn.titleLabel?.font = FontManager.boboBold(18)
|
||||||
|
popupBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||||
messageView.layoutChain
|
popupBtn.layer.cornerRadius = 16
|
||||||
.topToBottomOfView(timeLab, offset: 25)
|
popupBtn.layoutChain.left(20).right(20).bottom(34).height(56)
|
||||||
.left(79)
|
|
||||||
.right(30)
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -183,5 +89,4 @@ class CreateBubbleDoneView: UIView {
|
||||||
deinit {
|
deinit {
|
||||||
countdownTimer?.invalidate()
|
countdownTimer?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
// CreateBubbleVC.swift
|
// CreateBubbleVC.swift
|
||||||
// QuickLocation
|
// QuickLocation
|
||||||
//
|
//
|
||||||
// Created by 八条 on 2026/6/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
|
@ -19,57 +17,84 @@ class CreateBubbleVC: BaseViewController {
|
||||||
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()
|
||||||
|
restoreActiveBubbleIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
doneBtn.rx.tap.subscribe(onNext: { [weak self] in
|
private func bind() {
|
||||||
guard let self = self else { return }
|
rootView.navView.onBackTapped = { [weak self] in
|
||||||
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
guard let systemConfig = AppContextManager.shared.systemConfig, systemConfig.isIntercept == false else {
|
rootView.moreBtn.rx.tap
|
||||||
let hours = self.rootView.createBubbleTiemView.selectedHour.value
|
.subscribe(onNext: { [weak self] in
|
||||||
requestSetBubble(enable: true, keep_time: hours)
|
self?.navigationController?.pushViewController(BubbleKnowledgeVC(), animated: true)
|
||||||
return
|
})
|
||||||
}
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
if AppContextManager.shared.vip > 1 {
|
rootView.setupView.confirmBtn.rx.tap
|
||||||
let hours = self.rootView.createBubbleTiemView.selectedHour.value
|
.subscribe(onNext: { [weak self] in
|
||||||
requestSetBubble(enable: true, keep_time: hours)
|
guard let self else { return }
|
||||||
}
|
let hours = self.rootView.setupView.selectedHour.value
|
||||||
else {
|
let message = self.rootView.setupView.buildMessage(for: hours)
|
||||||
CreateBubblePopView.show()
|
self.rootView.setupView.showNotifySheet(message: message)
|
||||||
}
|
})
|
||||||
}).disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
popupBtn.rx.tap.subscribe(onNext: { [weak self] in
|
rootView.setupView.onSheetVisibilityChanged = { [weak self] visible in
|
||||||
guard let self = self else { return }
|
self?.rootView.updateNavTitleForSheet(visible)
|
||||||
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 }
|
rootView.setupView.sheetConfirmBtn.rx.tap
|
||||||
self.rootView.navView.titleLabel.text = "活动气泡"
|
.subscribe(onNext: { [weak self] in
|
||||||
self.rootView.createBubbleDoneView.messageLab.text = self.rootView.createBubbleTipsView.messageText
|
self?.submitCreate()
|
||||||
self.rootView.createBubbleTipsView.isHidden = true
|
})
|
||||||
self.rootView.createBubbleDoneView.isHidden = false
|
.disposed(by: disposeBag)
|
||||||
self.rootView.createBubbleDoneView.startCountdown(endDate: endDate)
|
|
||||||
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
// CreateBubbleView.swift
|
// CreateBubbleView.swift
|
||||||
// QuickLocation
|
// QuickLocation
|
||||||
//
|
//
|
||||||
// Created by 八条 on 2026/6/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
|
@ -14,79 +12,82 @@ class CreateBubbleView: UIView {
|
||||||
|
|
||||||
var disposeBag = DisposeBag()
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
var createBubbleTiemView: CreateBubbleTiemView = CreateBubbleTiemView()
|
let setupView = CreateBubbleSetupView()
|
||||||
var createBubbleTipsView: CreateBubbleTipsView = CreateBubbleTipsView()
|
let doneView = CreateBubbleDoneView()
|
||||||
var createBubbleDoneView: CreateBubbleDoneView = CreateBubbleDoneView()
|
|
||||||
|
|
||||||
private func setupRx() {
|
let moreBtn: UIButton = {
|
||||||
createBubbleTiemView.onNextTap = { [weak self] in
|
let btn = UIButton(type: .custom)
|
||||||
guard let self = self else { return }
|
btn.setImage(UIImage(systemName: "ellipsis"), for: .normal)
|
||||||
self.navView.titleLabel.text = "通知您的圈子"
|
btn.tintColor = UIColor(hexStr: "#293445")
|
||||||
let hours = self.createBubbleTiemView.selectedHour.value
|
btn.backgroundColor = UIColor.white.withAlphaComponent(0.9)
|
||||||
let endDate = Calendar.current.date(byAdding: .hour, value: hours, to: Date()) ?? Date()
|
btn.layer.cornerRadius = 8
|
||||||
let timeStr = endDate.toFormat("HH:mm")
|
btn.layer.borderWidth = 1
|
||||||
let message = "\(timeStr)之前,我会一直在这个区域。如果需要我帮忙,请给我发消息。"
|
btn.layer.borderColor = UIColor(hexStr: "#E8E8E8").cgColor
|
||||||
self.createBubbleTipsView.messageLab.text = message
|
return btn
|
||||||
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
|
|
||||||
.topToBottomOfView(navView)
|
|
||||||
.edges(excludingEdge: .top)
|
|
||||||
|
|
||||||
createBubbleTipsView.layoutChain
|
|
||||||
.topToBottomOfView(navView)
|
|
||||||
.edges(excludingEdge: .top)
|
|
||||||
|
|
||||||
createBubbleDoneView.layoutChain
|
|
||||||
.topToBottomOfView(navView)
|
|
||||||
.edges(excludingEdge: .top)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ? "通知您的圈子" : "设置气泡时间"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = 1e9),128G 机型会显示约 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
// 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 {
|
||||||
|
|
||||||
|
|
@ -21,113 +20,129 @@ class SearchLocationResultVC: BaseViewController {
|
||||||
|
|
||||||
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
|
private func showGroupPicker() {
|
||||||
guard let model = self.groupModel else {
|
guard let model = groupModel else {
|
||||||
self.requestGroupInfo(showPicker: true)
|
requestGroupInfo(showPicker: true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
GroupChooseView.show(groupModel: model) { _ in }
|
presentGroupPicker(with: model)
|
||||||
}).disposed(by: disposeBag)
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
// SearchLocationResultView.swift
|
// SearchLocationResultView.swift
|
||||||
// QuickLocation
|
// QuickLocation
|
||||||
//
|
//
|
||||||
// Created by 八条 on 2026/6/27.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
|
@ -14,20 +12,17 @@ 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)
|
||||||
|
|
@ -35,14 +30,26 @@ class SearchLocationResultView: UIView {
|
||||||
|
|
||||||
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()
|
||||||
|
|
@ -52,122 +59,147 @@ class SearchLocationResultView: UIView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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.bounces = false
|
||||||
|
|
||||||
view.addSubview(normalLottieView)
|
let contentView = UIView()
|
||||||
normalLottieView.layoutChain
|
contentView.backgroundColor = .clear
|
||||||
.top(20)
|
view.addSubview(contentView)
|
||||||
.width(270)
|
contentView.layoutChain.edges().widthToView(view)
|
||||||
.heightToWidth(1)
|
|
||||||
.centerX()
|
|
||||||
|
|
||||||
view.addSubview(phoneAreaLab)
|
contentView.addSubview(headerView)
|
||||||
phoneAreaLab.layoutChain
|
headerView.layoutChain.top(12).edgesHorzontal()
|
||||||
.topToBottomOfView(normalLottieView)
|
|
||||||
.centerX()
|
|
||||||
|
|
||||||
view.addSubview(messageLab)
|
contentView.addSubview(resultCard)
|
||||||
messageLab.layoutChain
|
resultCard.layoutChain
|
||||||
.topToBottomOfView(phoneAreaLab, offset: 10)
|
.topToBottomOfView(headerView, offset: 20)
|
||||||
.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
|
||||||
}()
|
}()
|
||||||
|
|
@ -181,7 +213,6 @@ class SearchLocationResultView: UIView {
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -189,166 +220,68 @@ class SearchLocationResultView: UIView {
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -356,13 +289,12 @@ class SearchLocationResultView: UIView {
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -372,7 +304,6 @@ class SearchLocationResultView: UIView {
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -29,20 +29,13 @@ class SearchLocationView: UIView {
|
||||||
}.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)
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +48,6 @@ class SearchLocationView: UIView {
|
||||||
addSubview(searchProgressView)
|
addSubview(searchProgressView)
|
||||||
|
|
||||||
videoView.layoutChain.edges()
|
videoView.layoutChain.edges()
|
||||||
|
|
||||||
searchProgressView.layoutChain
|
searchProgressView.layoutChain
|
||||||
.edgesHorzontal(15)
|
.edgesHorzontal(15)
|
||||||
.bottom(kSafeBottomMargin + 20)
|
.bottom(kSafeBottomMargin + 20)
|
||||||
|
|
@ -63,7 +55,7 @@ class SearchLocationView: UIView {
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -74,106 +66,8 @@ class SearchLocationView: UIView {
|
||||||
.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,18 +77,16 @@ 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 = {
|
||||||
|
|
@ -208,28 +100,15 @@ class SearchLocationView: UIView {
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -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,9 +252,9 @@ 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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -419,7 +275,7 @@ class SearchLocationView: UIView {
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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") }
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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") }
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
// 圈主 id:group_key 含 userId
|
// 圈主 id:group_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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||