- 二级页面
|
|
@ -47,6 +47,7 @@
|
|||
305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74F62FCA8C7000227D26 /* UIApplicationExtension.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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
|
|
@ -186,6 +187,11 @@
|
|||
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */; };
|
||||
30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.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 */; };
|
||||
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.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 */; };
|
||||
30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AAD2FEA13E00050EB2C /* ScheduleView.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 */; };
|
||||
30D74AB42FEA25B90050EB2C /* ViewedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AB32FEA25B90050EB2C /* ViewedModel.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 */; };
|
||||
55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D93023010000784738 /* PhoneReportDetailView.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 */; };
|
||||
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -503,6 +518,11 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -551,6 +571,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -630,6 +652,12 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -792,6 +820,7 @@
|
|||
305A74F62FCA8C7000227D26 /* UIApplicationExtension.swift */,
|
||||
305A74F72FCA8C7000227D26 /* UIButton+Extension.swift */,
|
||||
305A74F82FCA8C7000227D26 /* UIColor+Extension.swift */,
|
||||
55B217A33022F0B100784707 /* UIDevice+Extension.swift */,
|
||||
305A74F92FCA8C7000227D26 /* UIFont+Extension.swift */,
|
||||
305A74FA2FCA8C7000227D26 /* UIImage+Extension.swift */,
|
||||
305A74FB2FCA8C7000227D26 /* UIImage+Resource.swift */,
|
||||
|
|
@ -1023,6 +1052,7 @@
|
|||
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
|
||||
307073E42FD18A20004C37CC /* GroupChat */,
|
||||
30B74B3F2FF2435200F6744D /* GroupMemberList */,
|
||||
55B218A13024A00100784712 /* MemberInfo */,
|
||||
30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
|
||||
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */,
|
||||
30C4C0172FDBF066009215C1 /* RemoveMember */,
|
||||
|
|
@ -1099,6 +1129,8 @@
|
|||
55B217C03022F0A000784720 /* MinePhotoWallView.swift */,
|
||||
30EFF3CB2FDA667A00EB35D4 /* MyProfile */,
|
||||
30EFF3BC2FD9585200EB35D4 /* Account */,
|
||||
55B218A13024A00100784710 /* About */,
|
||||
55B218A13024A00100784711 /* CancelAccount */,
|
||||
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
|
||||
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
|
||||
30C4C0122FDB9178009215C1 /* CheckPermission */,
|
||||
|
|
@ -1113,6 +1145,7 @@
|
|||
305A76352FCA8C7000227D26 /* Map */,
|
||||
305A76262FCA8C7000227D26 /* Group */,
|
||||
30D74AAC2FEA13BD0050EB2C /* Schedule */,
|
||||
55B218B13024B00100784720 /* Explore */,
|
||||
305A76392FCA8C7000227D26 /* Mine */,
|
||||
305A798E2FCAC5F600227D26 /* InviteMember */,
|
||||
3062E8C52FCFD01000CEF511 /* VipRecharge */,
|
||||
|
|
@ -1380,6 +1413,10 @@
|
|||
children = (
|
||||
30A87A5F2FEE71B80095E7C6 /* CreateBubbleVC.swift */,
|
||||
30A87A5D2FEE71A50095E7C6 /* CreateBubbleView.swift */,
|
||||
55B219C13024C00100784721 /* BubbleHeroView.swift */,
|
||||
55B219C13024C00100784723 /* CreateBubbleSetupView.swift */,
|
||||
55B219C13024C00100784725 /* BubbleKnowledgeView.swift */,
|
||||
55B219C13024C00100784727 /* BubbleKnowledgeVC.swift */,
|
||||
30A87A612FEE724D0095E7C6 /* CreateBubbleTiemView.swift */,
|
||||
30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */,
|
||||
30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */,
|
||||
|
|
@ -1393,6 +1430,7 @@
|
|||
children = (
|
||||
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */,
|
||||
30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */,
|
||||
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */,
|
||||
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */,
|
||||
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */,
|
||||
30EBF71F2FEFD6F2009A8A87 /* GroupChooseView.swift */,
|
||||
|
|
@ -1526,6 +1564,15 @@
|
|||
path = SOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
55B218B13024B00100784720 /* Explore */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55B218B13024B00100784721 /* FeatureIntroVC.swift */,
|
||||
55B218B13024B00100784723 /* FeatureIntroView.swift */,
|
||||
);
|
||||
path = Explore;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30D74AAC2FEA13BD0050EB2C /* Schedule */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1727,6 +1774,33 @@
|
|||
path = TodayTrackDetail;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1922,6 +1996,10 @@
|
|||
30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */,
|
||||
30EFF3D82FDA8F1000EB35D4 /* EmergencyContactVC.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 */,
|
||||
30DC185E2FD1211D0041DCD1 /* VipRightsVC.swift in Sources */,
|
||||
305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */,
|
||||
|
|
@ -1982,6 +2060,7 @@
|
|||
305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */,
|
||||
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */,
|
||||
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */,
|
||||
55B217A33022F0B100784706 /* UIDevice+Extension.swift in Sources */,
|
||||
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */,
|
||||
55B217A13022F0A000784702 /* FontManager.swift in Sources */,
|
||||
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */,
|
||||
|
|
@ -2023,6 +2102,12 @@
|
|||
55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */,
|
||||
55B217DA3023010000784739 /* PhoneReportDetailView.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 */,
|
||||
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */,
|
||||
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */,
|
||||
|
|
@ -2047,6 +2132,8 @@
|
|||
305A76CC2FCA8C7000227D26 /* FileTools.swift in Sources */,
|
||||
305A76CD2FCA8C7000227D26 /* Permission.swift in Sources */,
|
||||
30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */,
|
||||
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */,
|
||||
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */,
|
||||
305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */,
|
||||
3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */,
|
||||
305A76CF2FCA8C7000227D26 /* CountDownService.swift in Sources */,
|
||||
|
|
@ -2079,6 +2166,7 @@
|
|||
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */,
|
||||
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */,
|
||||
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */,
|
||||
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */,
|
||||
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */,
|
||||
305A76E02FCA8C7000227D26 /* GroupView.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) }
|
||||
/// 解锁统计:日期 yyyy-MM-dd
|
||||
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 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
|
||||
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 mineNav = BaseNavigationController(rootViewController: MineViewController())
|
||||
|
||||
|
|
@ -93,6 +93,14 @@ final class MainTabBarController: UITabBarController {
|
|||
func setTabBarHidden(_ hidden: Bool) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Moya
|
|||
import SwiftyJSON
|
||||
import RxSwift
|
||||
import CommonCrypto
|
||||
import UIKit
|
||||
#if !targetEnvironment(simulator)
|
||||
import GeYanSdk
|
||||
#endif
|
||||
|
|
@ -56,12 +57,7 @@ extension ApiManager {
|
|||
headers["x-version"] = kAppShortVersion
|
||||
headers["x-device-id"] = ApiManager.getMD5ID()
|
||||
// 机型
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let platform = withUnsafePointer(to: &systemInfo.machine.0) { ptr in
|
||||
return String(cString: ptr)
|
||||
}
|
||||
headers["x-mobile-model"] = platform
|
||||
headers["x-mobile-model"] = UIDevice.machineIdentifier
|
||||
|
||||
if let loginToken = Defaults[\.loginToken] {
|
||||
headers["x-token"] = loginToken
|
||||
|
|
|
|||
|
|
@ -79,6 +79,16 @@ enum Route: String {
|
|||
case todayTrackDetail = "todayTrackDetail"
|
||||
/// 手机报告详情
|
||||
case phoneReportDetail = "phoneReportDetail"
|
||||
/// 关于我们
|
||||
case about = "about"
|
||||
/// 成员信息
|
||||
case memberInfo = "memberInfo"
|
||||
/// 注销账号
|
||||
case cancelAccount = "cancelAccount"
|
||||
/// 还在吗 / 打卡
|
||||
case signIn = "signIn"
|
||||
/// SOS
|
||||
case sos = "sos"
|
||||
/// 充值优惠弹窗
|
||||
case promotionalActivities = "promotionalActivities"
|
||||
}
|
||||
|
|
@ -363,6 +373,31 @@ extension AppRouter: AppRouterProtocol {
|
|||
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: - 充值优惠弹窗
|
||||
AppRouter.register(Route.promotionalActivities) { url, parameters in
|
||||
PromotionalActivitiesVC()
|
||||
|
|
|
|||
|
|
@ -187,10 +187,18 @@ final class UnlockCountManager: NSObject {
|
|||
private func rollDayIfNeeded() -> Bool {
|
||||
migrateScreenTimeStorageIfNeeded()
|
||||
let today = todayString()
|
||||
guard Defaults[\.unlockCountDate] != today else { return false }
|
||||
let previousDate = Defaults[\.unlockCountDate]
|
||||
guard previousDate != today else { return false }
|
||||
|
||||
flushScreenSession()
|
||||
|
||||
// 切日前落盘旧日解锁次数
|
||||
if !previousDate.isEmpty {
|
||||
var byDay = Defaults[\.unlockCountByDay]
|
||||
byDay[previousDate] = Defaults[\.unlockCountToday]
|
||||
Defaults[\.unlockCountByDay] = byDay
|
||||
}
|
||||
|
||||
Defaults[\.unlockCountDate] = today
|
||||
Defaults[\.unlockCountToday] = 0
|
||||
Defaults[\.appUsageToday] = 0
|
||||
|
|
@ -308,6 +316,9 @@ final class UnlockCountManager: NSObject {
|
|||
rollDayIfNeeded()
|
||||
Defaults[\.unlockCountToday] = Defaults[\.unlockCountToday] + 1
|
||||
Defaults[\.unlockCountTotal] = Defaults[\.unlockCountTotal] + 1
|
||||
var byDay = Defaults[\.unlockCountByDay]
|
||||
byDay[todayString()] = Defaults[\.unlockCountToday]
|
||||
Defaults[\.unlockCountByDay] = byDay
|
||||
Defaults[\.unlockLastCountedAt] = now
|
||||
Defaults[\.unlockWasLocked] = 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()
|
||||
|
||||
// 分享邀请卡片到微信
|
||||
rootView.shareBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
self?.shareInviteCard()
|
||||
rootView.shareBtn.rx.tap.subscribe(onNext: { _ in
|
||||
SharePopView.show()
|
||||
// self?.shareInviteCard()
|
||||
}).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
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
|
|
@ -13,9 +11,15 @@ class CreateBubbleDoneView: UIView {
|
|||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
let heroView = BubbleHeroView()
|
||||
let popupBtn = UIButton(type: .custom)
|
||||
|
||||
private var countdownTimer: Timer?
|
||||
private var endDate: Date = Date()
|
||||
|
||||
private let clockImage = UIImageView()
|
||||
private let timeLab = UILabel()
|
||||
|
||||
func startCountdown(endDate: Date) {
|
||||
self.endDate = endDate
|
||||
countdownTimer?.invalidate()
|
||||
|
|
@ -39,141 +43,43 @@ class CreateBubbleDoneView: UIView {
|
|||
timeLab.text = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
private func setupRx() {
|
||||
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(titleLab)
|
||||
addSubview(timeIcon)
|
||||
addSubview(timeLab)
|
||||
addSubview(messageView)
|
||||
addSubview(iconView)
|
||||
addSubview(scrollView)
|
||||
addSubview(heroView)
|
||||
addSubview(clockImage)
|
||||
clockImage.addSubview(timeLab)
|
||||
addSubview(popupBtn)
|
||||
|
||||
titleLab.layoutChain
|
||||
.top(15)
|
||||
heroView.layoutChain
|
||||
.top(33)
|
||||
.leading(30)
|
||||
.trailing(30)
|
||||
.heightToWidth(1)
|
||||
|
||||
clockImage.image = UIImage(named: "Bubble/clock")
|
||||
clockImage.contentMode = .scaleAspectFit
|
||||
clockImage.layoutChain
|
||||
.topToBottomOfView(heroView, offset: 8)
|
||||
.centerX()
|
||||
.width(200)
|
||||
.height(120)
|
||||
|
||||
timeLab.layoutChain
|
||||
.topToBottomOfView(titleLab, offset: 12)
|
||||
.centerX()
|
||||
timeLab.font = .systemFont(ofSize: 28, weight: .heavy)
|
||||
timeLab.textColor = .white
|
||||
timeLab.textAlignment = .center
|
||||
timeLab.layoutChain.centerX(clockImage).centerY(clockImage, offset: 4)
|
||||
|
||||
timeIcon.layoutChain
|
||||
.rightToLeftOfView(timeLab, offset: -8)
|
||||
.centerY(timeLab)
|
||||
|
||||
messageView.layoutChain
|
||||
.topToBottomOfView(timeLab, offset: 25)
|
||||
.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)
|
||||
popupBtn.setTitle("弹出气泡", for: .normal)
|
||||
popupBtn.setTitleColor(.white, for: .normal)
|
||||
popupBtn.titleLabel?.font = FontManager.boboBold(18)
|
||||
popupBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
popupBtn.layer.cornerRadius = 16
|
||||
popupBtn.layoutChain.left(20).right(20).bottom(34).height(56)
|
||||
}
|
||||
|
||||
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) {
|
||||
super.init(frame: .zero)
|
||||
self.isHidden = true
|
||||
backgroundColor = .white
|
||||
setupUI()
|
||||
setupRx()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
|
@ -183,5 +89,4 @@ class CreateBubbleDoneView: UIView {
|
|||
deinit {
|
||||
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
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
|
|
@ -19,57 +17,84 @@ class CreateBubbleVC: BaseViewController {
|
|||
view = rootView
|
||||
}
|
||||
|
||||
private var doneBtn: UIButton {
|
||||
rootView.createBubbleTipsView.doneBtn
|
||||
}
|
||||
|
||||
private var popupBtn: UIButton {
|
||||
rootView.createBubbleDoneView.popupBtn
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
bind()
|
||||
restoreActiveBubbleIfNeeded()
|
||||
}
|
||||
|
||||
doneBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
private func bind() {
|
||||
rootView.navView.onBackTapped = { [weak self] in
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
rootView.moreBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.navigationController?.pushViewController(BubbleKnowledgeVC(), animated: true)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.setupView.confirmBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let self else { return }
|
||||
let hours = self.rootView.setupView.selectedHour.value
|
||||
let message = self.rootView.setupView.buildMessage(for: hours)
|
||||
self.rootView.setupView.showNotifySheet(message: message)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.setupView.onSheetVisibilityChanged = { [weak self] visible in
|
||||
self?.rootView.updateNavTitleForSheet(visible)
|
||||
}
|
||||
|
||||
rootView.setupView.sheetConfirmBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.submitCreate()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.doneView.popupBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.showConfirmPop(
|
||||
title: "温馨提醒",
|
||||
message: "您确定要弹出这个气泡吗?",
|
||||
confirmText: "爆裂",
|
||||
confirmBlock: { [weak self] in
|
||||
self?.requestSetBubble(enable: false, keep_time: -1)
|
||||
},
|
||||
cancelText: "取消"
|
||||
)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func submitCreate() {
|
||||
guard let systemConfig = AppContextManager.shared.systemConfig, systemConfig.isIntercept == false else {
|
||||
let hours = self.rootView.createBubbleTiemView.selectedHour.value
|
||||
let hours = rootView.setupView.selectedHour.value
|
||||
requestSetBubble(enable: true, keep_time: hours)
|
||||
return
|
||||
}
|
||||
|
||||
if AppContextManager.shared.vip > 1 {
|
||||
let hours = self.rootView.createBubbleTiemView.selectedHour.value
|
||||
let hours = rootView.setupView.selectedHour.value
|
||||
requestSetBubble(enable: true, keep_time: hours)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
CreateBubblePopView.show()
|
||||
}
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
popupBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.showConfirmPop(title: "温馨提醒",
|
||||
message: "您确定要弹出这个气泡吗?",
|
||||
confirmText: "爆裂",
|
||||
confirmBlock: {
|
||||
self.requestSetBubble(enable: false, keep_time: -1)
|
||||
}, cancelText: "取消")
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
guard AppContextManager.shared.status == 2, let endDate = Defaults[\.bubbleKeepTimes] else { return }
|
||||
self.rootView.navView.titleLabel.text = "活动气泡"
|
||||
self.rootView.createBubbleDoneView.messageLab.text = self.rootView.createBubbleTipsView.messageText
|
||||
self.rootView.createBubbleTipsView.isHidden = true
|
||||
self.rootView.createBubbleDoneView.isHidden = false
|
||||
self.rootView.createBubbleDoneView.startCountdown(endDate: endDate)
|
||||
private func 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) {
|
||||
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()
|
||||
guard let self else { return }
|
||||
guard enable else {
|
||||
DLToast.show(text: "气泡已弹出") {
|
||||
Defaults[\.bubbleKeepTimes] = nil
|
||||
|
|
@ -79,13 +104,11 @@ class CreateBubbleVC: BaseViewController {
|
|||
return
|
||||
}
|
||||
AppContextManager.shared.updateAccountStatus(2)
|
||||
self.rootView.navView.titleLabel.text = "活动气泡"
|
||||
self.rootView.createBubbleDoneView.messageLab.text = self.rootView.createBubbleTipsView.messageText
|
||||
self.rootView.setupView.dismissNotifySheet()
|
||||
let endDate = Calendar.current.date(byAdding: .hour, value: keep_time, to: Date()) ?? Date()
|
||||
Defaults[\.bubbleKeepTimes] = endDate
|
||||
self.rootView.createBubbleDoneView.startCountdown(endDate: endDate)
|
||||
self.rootView.createBubbleTipsView.isHidden = true
|
||||
self.rootView.createBubbleDoneView.isHidden = false
|
||||
}).disposed(by: disposeBag)
|
||||
self.rootView.doneView.startCountdown(endDate: endDate)
|
||||
self.rootView.showDone(animated: true)
|
||||
}, onError: { _ in }).disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// CreateBubbleView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
|
|
@ -14,79 +12,82 @@ class CreateBubbleView: UIView {
|
|||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
var createBubbleTiemView: CreateBubbleTiemView = CreateBubbleTiemView()
|
||||
var createBubbleTipsView: CreateBubbleTipsView = CreateBubbleTipsView()
|
||||
var createBubbleDoneView: CreateBubbleDoneView = CreateBubbleDoneView()
|
||||
let setupView = CreateBubbleSetupView()
|
||||
let doneView = CreateBubbleDoneView()
|
||||
|
||||
private func setupRx() {
|
||||
createBubbleTiemView.onNextTap = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.navView.titleLabel.text = "通知您的圈子"
|
||||
let hours = self.createBubbleTiemView.selectedHour.value
|
||||
let endDate = Calendar.current.date(byAdding: .hour, value: hours, to: Date()) ?? Date()
|
||||
let timeStr = endDate.toFormat("HH:mm")
|
||||
let message = "\(timeStr)之前,我会一直在这个区域。如果需要我帮忙,请给我发消息。"
|
||||
self.createBubbleTipsView.messageLab.text = message
|
||||
self.createBubbleTipsView.messageText = message
|
||||
self.createBubbleTiemView.isHidden = true
|
||||
self.createBubbleTipsView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(createBubbleTiemView)
|
||||
addSubview(createBubbleTipsView)
|
||||
addSubview(createBubbleDoneView)
|
||||
|
||||
addSubview(navBgView)
|
||||
addSubview(navView)
|
||||
|
||||
navBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.heightToWidth(160/375)
|
||||
|
||||
navView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(kNaviHeight)
|
||||
|
||||
createBubbleTiemView.layoutChain
|
||||
.topToBottomOfView(navView)
|
||||
.edges(excludingEdge: .top)
|
||||
|
||||
createBubbleTipsView.layoutChain
|
||||
.topToBottomOfView(navView)
|
||||
.edges(excludingEdge: .top)
|
||||
|
||||
createBubbleDoneView.layoutChain
|
||||
.topToBottomOfView(navView)
|
||||
.edges(excludingEdge: .top)
|
||||
}
|
||||
let moreBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setImage(UIImage(systemName: "ellipsis"), for: .normal)
|
||||
btn.tintColor = UIColor(hexStr: "#293445")
|
||||
btn.backgroundColor = UIColor.white.withAlphaComponent(0.9)
|
||||
btn.layer.cornerRadius = 8
|
||||
btn.layer.borderWidth = 1
|
||||
btn.layer.borderColor = UIColor(hexStr: "#E8E8E8").cgColor
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var navBgView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.image = UIImage(named: "Common/navBar_bg_2")
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.isHidden = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
lazy var navView: BaseNavigationView = {
|
||||
let nav = BaseNavigationView(title: "设置气泡时间")
|
||||
return nav
|
||||
BaseNavigationView(title: "设置气泡时间")
|
||||
}()
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
setupUI()
|
||||
setupRx()
|
||||
createBubbleTiemView.isHidden = false
|
||||
createBubbleTipsView.isHidden = true
|
||||
createBubbleDoneView.isHidden = true
|
||||
showSetup(animated: false)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
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() {
|
||||
phoneModelIcon.image = UIImage(named: "Home/member_phone")
|
||||
phoneModelLab.text = Self.localPhoneModelName()
|
||||
phoneModelLab.text = UIDevice.modelName
|
||||
phoneModelLab.textColor = Self.batteryDefaultTextColor
|
||||
|
||||
networkIcon.image = UIImage(named: "Home/member_wifi")
|
||||
|
|
@ -131,20 +131,8 @@ class GroupMemberView2: UIView {
|
|||
|
||||
// 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? {
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
let level = UIDevice.current.batteryLevel
|
||||
guard level >= 0 else { return nil }
|
||||
return Int((level * 100).rounded())
|
||||
UIDevice.batteryPercent
|
||||
}
|
||||
|
||||
private static func localNetworkStatusText() -> String {
|
||||
|
|
@ -161,59 +149,6 @@ class GroupMemberView2: UIView {
|
|||
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() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ final class PhoneReportDetailView: UIView {
|
|||
let deviceCard = UIView()
|
||||
let deviceNameLab = UILabel()
|
||||
let storageLab = UILabel()
|
||||
let storageBarTrack = UIView()
|
||||
let storageBar = UIProgressView(progressViewStyle: .default)
|
||||
let deviceImage = UIImageView(image: UIImage(named: "Home/phone_device"))
|
||||
|
||||
|
|
@ -65,14 +66,18 @@ final class PhoneReportDetailView: UIView {
|
|||
let screenSection = PhoneReportChartSectionView(
|
||||
title: "屏幕使用时间",
|
||||
summaryPrefix: "今日使用时长",
|
||||
barColor: UIColor(hexStr: "#5EC8FF"),
|
||||
highlightColor: UIColor(hexStr: "#2F9BFF")
|
||||
summaryColor: UIColor(hexStr: "#00ADFE"),
|
||||
barGradient: (UIColor(hexStr: "#C0EAFF"), UIColor(hexStr: "#ECF9FF")),
|
||||
highlightGradient: (UIColor(hexStr: "#16B3FF"), UIColor(hexStr: "#9EDFFF")),
|
||||
tipColor: UIColor(hexStr: "#20B6FF")
|
||||
)
|
||||
let unlockSection = PhoneReportChartSectionView(
|
||||
title: "手机使用次数",
|
||||
summaryPrefix: "今日使用次数",
|
||||
barColor: UIColor(hexStr: "#B7E86A"),
|
||||
highlightColor: UIColor(hexStr: "#83DE00"),
|
||||
summaryColor: UIColor(hexStr: "#68C025"),
|
||||
barGradient: (UIColor(hexStr: "#DDFFA1"), UIColor(hexStr: "#F2FFDC")),
|
||||
highlightGradient: (UIColor(hexStr: "#AAF52A"), UIColor(hexStr: "#E8FFC1")),
|
||||
tipColor: UIColor(hexStr: "#86E73D"),
|
||||
valueSuffix: "次"
|
||||
)
|
||||
|
||||
|
|
@ -95,26 +100,29 @@ final class PhoneReportDetailView: UIView {
|
|||
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
|
||||
navView.addRightButton(switchGroupBtn)
|
||||
|
||||
addSubview(memberCV)
|
||||
memberCV.layoutChain
|
||||
.topToBottomOfView(navView, offset: 8)
|
||||
.edgesHorzontal(15)
|
||||
.height(100)
|
||||
|
||||
addSubview(scrollView)
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.clipsToBounds = true
|
||||
scrollView.delaysContentTouches = false
|
||||
scrollView.canCancelContentTouches = true
|
||||
scrollView.layoutChain
|
||||
.topToBottomOfView(memberCV, offset: 12)
|
||||
.topToBottomOfView(navView)
|
||||
.edgesHorzontal()
|
||||
.bottom()
|
||||
scrollView.addSubview(contentView)
|
||||
contentView.layoutChain.edges().widthToView(scrollView)
|
||||
|
||||
contentView.addSubview(memberCV)
|
||||
memberCV.layoutChain
|
||||
.top(8)
|
||||
.edgesHorzontal(15)
|
||||
.height(100)
|
||||
|
||||
statusTitleLab.text = "手机状态"
|
||||
statusTitleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
statusTitleLab.textColor = UIColor(hexStr: "#293445")
|
||||
contentView.addSubview(statusTitleLab)
|
||||
statusTitleLab.layoutChain.top().left(15)
|
||||
statusTitleLab.layoutChain.topToBottomOfView(memberCV, offset: 12).left(15)
|
||||
|
||||
statusCard.backgroundColor = .white
|
||||
statusCard.applyPhoneReportCardStyle(cornerRadius: 20)
|
||||
|
|
@ -136,10 +144,10 @@ final class PhoneReportDetailView: UIView {
|
|||
batteryRing.layoutChain.top(23).centerX().width(100).height(100)
|
||||
|
||||
estimateLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
estimateLab.textColor = UIColor(hexStr: "#6B7280")
|
||||
estimateLab.textColor = UIColor(hexStr: "#293445")
|
||||
estimateLab.textAlignment = .center
|
||||
estimateLab.backgroundColor = UIColor(hexStr: "#E8EEF5")
|
||||
estimateLab.cornerRadius = 10
|
||||
estimateLab.backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||
estimateLab.cornerRadius = 5
|
||||
leftPanel.addSubview(estimateLab)
|
||||
estimateLab.layoutChain
|
||||
.topToBottomOfView(batteryRing, offset: 12)
|
||||
|
|
@ -177,16 +185,24 @@ final class PhoneReportDetailView: UIView {
|
|||
deviceCard.addSubview(storageLab)
|
||||
storageLab.layoutChain.topToBottomOfView(deviceNameLab, offset: 4).left(10)
|
||||
|
||||
storageBar.progressTintColor = UIColor(hexStr: "#5EC8FF")
|
||||
storageBar.trackTintColor = UIColor(hexStr: "#E5E7EB")
|
||||
deviceCard.addSubview(storageBar)
|
||||
storageBar.layoutChain
|
||||
storageBarTrack.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||
storageBarTrack.layer.cornerRadius = 5
|
||||
storageBarTrack.clipsToBounds = true
|
||||
deviceCard.addSubview(storageBarTrack)
|
||||
storageBarTrack.layoutChain
|
||||
.topToBottomOfView(storageLab, offset: 6)
|
||||
.left(10)
|
||||
.right(60)
|
||||
.height(4)
|
||||
.height(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
|
||||
deviceCard.addSubview(deviceImage)
|
||||
deviceImage.layoutChain.right(8).centerY().width(36).height(54)
|
||||
|
|
@ -204,7 +220,7 @@ final class PhoneReportDetailView: UIView {
|
|||
}
|
||||
|
||||
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)"
|
||||
brightnessCard.setValue(snap.brightnessText)
|
||||
volumeCard.setValue(snap.volumeText)
|
||||
|
|
@ -212,8 +228,8 @@ final class PhoneReportDetailView: UIView {
|
|||
deviceNameLab.text = snap.deviceText
|
||||
storageLab.text = snap.storageText
|
||||
storageBar.progress = Float(snap.storageRatio)
|
||||
screenSection.setSummary("\(screenSection.summaryPrefix) \(snap.screenTodayText)")
|
||||
unlockSection.setSummary("\(unlockSection.summaryPrefix) \(snap.unlockTodayText)次")
|
||||
screenSection.setSummary(snap.screenTodayText)
|
||||
unlockSection.setSummary("\(snap.unlockTodayText)次")
|
||||
screenSection.setBars(snap.screenBars)
|
||||
unlockSection.setBars(snap.unlockBars)
|
||||
}
|
||||
|
|
@ -274,8 +290,9 @@ final class PhoneBatteryRingView: UIView {
|
|||
progress.path = path.cgPath
|
||||
}
|
||||
|
||||
func configure(percent: Int?, text: String) {
|
||||
func configure(percent: Int?, text: String, isCharging: Bool = false) {
|
||||
valueLab.text = text
|
||||
bolt.isHidden = !isCharging
|
||||
progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100
|
||||
}
|
||||
}
|
||||
|
|
@ -298,12 +315,12 @@ final class PhoneStatusMiniCard: UIView {
|
|||
titleLab.textColor = UIColor(hexStr: "#AAAAAA")
|
||||
titleLab.textAlignment = .center
|
||||
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.textColor = UIColor(hexStr: "#293445")
|
||||
valueLab.textAlignment = .center
|
||||
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") }
|
||||
|
|
@ -316,20 +333,33 @@ final class PhoneStatusMiniCard: UIView {
|
|||
final class PhoneReportChartSectionView: UIView {
|
||||
let summaryPrefix: String
|
||||
private let titleLab = UILabel()
|
||||
private let summaryPrefixLab = UILabel()
|
||||
private let summaryLab = UILabel()
|
||||
private let dayBtn = UIButton(type: .system)
|
||||
private let weekBtn = UIButton(type: .system)
|
||||
private let chart = PhoneReportBarChartView()
|
||||
private let barColor: UIColor
|
||||
private let highlightColor: UIColor
|
||||
private let summaryColor: UIColor
|
||||
private let barGradient: (UIColor, UIColor)
|
||||
private let highlightGradient: (UIColor, UIColor)
|
||||
private let tipColor: UIColor
|
||||
private let valueSuffix: String
|
||||
|
||||
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.barColor = barColor
|
||||
self.highlightColor = highlightColor
|
||||
self.summaryColor = summaryColor
|
||||
self.barGradient = barGradient
|
||||
self.highlightGradient = highlightGradient
|
||||
self.tipColor = tipColor
|
||||
self.valueSuffix = valueSuffix
|
||||
super.init(frame: .zero)
|
||||
titleLab.text = title
|
||||
|
|
@ -344,26 +374,43 @@ final class PhoneReportChartSectionView: UIView {
|
|||
addSubview(card)
|
||||
card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180)
|
||||
|
||||
summaryLab.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
summaryLab.textColor = UIColor(hexStr: "#293445")
|
||||
card.addSubview(summaryLab)
|
||||
summaryLab.layoutChain.top(14).left(14)
|
||||
summaryPrefixLab.text = summaryPrefix
|
||||
summaryPrefixLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
summaryPrefixLab.textColor = UIColor(hexStr: "#293445")
|
||||
summaryPrefixLab.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
card.addSubview(summaryPrefixLab)
|
||||
summaryPrefixLab.layoutChain.top(14).left(14)
|
||||
|
||||
configureToggle(dayBtn, title: "天", selected: true)
|
||||
configureToggle(weekBtn, title: "周", selected: false)
|
||||
let toggle = UIStackView(arrangedSubviews: [weekBtn, dayBtn])
|
||||
toggle.axis = .horizontal
|
||||
toggle.spacing = 0
|
||||
toggle.backgroundColor = UIColor(hexStr: "#F3F4F6")
|
||||
toggle.cornerRadius = 12
|
||||
card.addSubview(toggle)
|
||||
toggle.layoutChain.centerY(summaryLab).right(14).height(28)
|
||||
|
||||
let toggleBg = UIView()
|
||||
toggleBg.backgroundColor = UIColor(hexStr: "#EAEAEA")
|
||||
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)
|
||||
weekBtn.addTarget(self, action: #selector(tapWeek), for: .touchUpInside)
|
||||
|
||||
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") }
|
||||
|
|
@ -398,100 +445,174 @@ final class PhoneReportChartSectionView: UIView {
|
|||
}
|
||||
|
||||
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 {
|
||||
private var bars: [PhoneReportDetailViewModel.ChartBar] = []
|
||||
private var color: UIColor = .systemBlue
|
||||
private var highlight: UIColor = .systemBlue
|
||||
private var barGradient: (UIColor, UIColor) = (.systemBlue, .systemBlue)
|
||||
private var highlightGradient: (UIColor, UIColor) = (.systemBlue, .systemBlue)
|
||||
private var tipColor: UIColor = .systemBlue
|
||||
private var suffix = ""
|
||||
private var barLayers: [CALayer] = []
|
||||
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) {
|
||||
super.init(frame: frame)
|
||||
clipsToBounds = false
|
||||
isUserInteractionEnabled = true
|
||||
baseline.backgroundColor = UIColor(hexStr: "#F2F2F2")
|
||||
addSubview(baseline)
|
||||
tipLab.font = .systemFont(ofSize: 11, weight: .bold)
|
||||
tipLab.textColor = .white
|
||||
tipLab.backgroundColor = UIColor(hexStr: "#2F9BFF")
|
||||
tipLab.textAlignment = .center
|
||||
tipLab.cornerRadius = 8
|
||||
tipLab.cornerRadius = 6
|
||||
tipLab.clipsToBounds = true
|
||||
tipLab.isHidden = true
|
||||
tipLab.isUserInteractionEnabled = false
|
||||
addSubview(tipLab)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
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.color = color
|
||||
self.highlight = highlight
|
||||
self.barGradient = barGradient
|
||||
self.highlightGradient = highlightGradient
|
||||
self.tipColor = tipColor
|
||||
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()
|
||||
}
|
||||
|
||||
@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() {
|
||||
super.layoutSubviews()
|
||||
barLayers.forEach { $0.removeFromSuperlayer() }
|
||||
barLayers.removeAll()
|
||||
labelViews.forEach { $0.removeFromSuperview() }
|
||||
labelViews.removeAll()
|
||||
barFrames.removeAll()
|
||||
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 count = bars.count
|
||||
let labelH: CGFloat = 16
|
||||
let chartH = bounds.height - labelH - 24
|
||||
let spacing: CGFloat = 8
|
||||
let barW = max(8, (bounds.width - spacing * CGFloat(count - 1)) / CGFloat(count))
|
||||
let barW: CGFloat = 16
|
||||
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() {
|
||||
let ratio = CGFloat(bar.value) / CGFloat(maxVal)
|
||||
let h = max(4, chartH * max(0.05, ratio))
|
||||
let x = CGFloat(i) * (barW + spacing)
|
||||
let y = bounds.height - labelH - h
|
||||
let layer = CALayer()
|
||||
layer.frame = CGRect(x: x, y: y, width: barW, height: h)
|
||||
layer.backgroundColor = (bar.isToday ? highlight : color.withAlphaComponent(0.35)).cgColor
|
||||
layer.cornerRadius = 4
|
||||
self.layer.addSublayer(layer)
|
||||
barLayers.append(layer)
|
||||
let x = CGFloat(i) * (barW + barSpacing)
|
||||
let y = baselineY - h
|
||||
let barFrame = CGRect(x: x, y: y, width: barW, height: h)
|
||||
barFrames.append(barFrame)
|
||||
|
||||
let isSelected = i == selectedIndex
|
||||
let gradient = CAGradientLayer()
|
||||
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()
|
||||
lab.text = bar.label
|
||||
lab.font = .systemFont(ofSize: 10, weight: .medium)
|
||||
lab.textColor = UIColor(hexStr: "#9CA3AF")
|
||||
lab.textAlignment = .center
|
||||
lab.frame = CGRect(x: x - 6, y: bounds.height - labelH, width: barW + 12, height: labelH)
|
||||
addSubview(lab)
|
||||
lab.frame = CGRect(x: x - 10, y: bounds.height - labelH, width: barW + 20, height: labelH)
|
||||
insertSubview(lab, belowSubview: tipLab)
|
||||
labelViews.append(lab)
|
||||
|
||||
if bar.isToday, bar.value > 0 {
|
||||
let hours = bar.value / 3600
|
||||
let mins = (bar.value % 3600) / 60
|
||||
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) "
|
||||
if isSelected {
|
||||
tipLab.text = tipText(for: bar)
|
||||
tipLab.backgroundColor = tipColor
|
||||
tipLab.sizeToFit()
|
||||
let tipW = tipLab.bounds.width + 5 + 5
|
||||
let tipH = tipLab.bounds.height + 4 + 4
|
||||
// 与柱子水平居中,允许超出 chart 左右边界
|
||||
tipLab.frame = CGRect(
|
||||
x: max(0, x + barW / 2 - tipLab.bounds.width / 2 - 6),
|
||||
y: max(0, y - 22),
|
||||
width: tipLab.bounds.width + 12,
|
||||
height: 18
|
||||
x: x + barW / 2 - tipW / 2,
|
||||
y: max(0, y - tipH - 4),
|
||||
width: tipW,
|
||||
height: tipH
|
||||
)
|
||||
tipLab.backgroundColor = highlight
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ final class PhoneReportDetailViewModel {
|
|||
struct StatusSnapshot {
|
||||
let batteryText: String
|
||||
let batteryPercent: Int?
|
||||
let isCharging: Bool
|
||||
let estimateText: String
|
||||
let brightnessText: String
|
||||
let volumeText: String
|
||||
|
|
@ -46,6 +47,7 @@ final class PhoneReportDetailViewModel {
|
|||
StatusSnapshot(
|
||||
batteryText: batteryText,
|
||||
batteryPercent: batteryPercent,
|
||||
isCharging: isCharging,
|
||||
estimateText: estimateText,
|
||||
brightnessText: brightnessText,
|
||||
volumeText: volumeText ?? self.volumeText,
|
||||
|
|
@ -73,6 +75,7 @@ final class PhoneReportDetailViewModel {
|
|||
private var volumeObservation: NSKeyValueObservation?
|
||||
|
||||
init(members: [GroupMemberModel], selectedUserId: String) {
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
self.members = members
|
||||
self.selectedMemberId = BehaviorRelay(value: selectedUserId)
|
||||
self.snapshot = BehaviorRelay(value: Self.emptySnapshot())
|
||||
|
|
@ -114,6 +117,20 @@ final class PhoneReportDetailViewModel {
|
|||
})
|
||||
.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()
|
||||
}
|
||||
|
||||
|
|
@ -143,16 +160,13 @@ final class PhoneReportDetailViewModel {
|
|||
|
||||
private func makeSelfSnapshot() -> StatusSnapshot {
|
||||
let mgr = UnlockCountManager.shared
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
let batteryLevel = UIDevice.current.batteryLevel
|
||||
let batteryValid = batteryLevel >= 0
|
||||
let battery = Int(batteryLevel * 100)
|
||||
let batteryPercent = UIDevice.batteryPercent
|
||||
let isCharging = UIDevice.isBatteryCharging
|
||||
let screenSec = mgr.todayScreenTimeSeconds
|
||||
let unlock = mgr.todayCount
|
||||
let usage = mgr.todayAppUsageCount
|
||||
|
||||
let batteryText = batteryValid ? "\(battery)%" : placeholder
|
||||
let batteryPercent: Int? = batteryValid ? battery : nil
|
||||
let batteryText = batteryPercent.map { "\($0)%" } ?? placeholder
|
||||
let brightnessText = Self.brightnessText() ?? placeholder
|
||||
let volumeText = Self.volumeText() ?? placeholder
|
||||
let wifiText = Self.wifiSSID() ?? placeholder
|
||||
|
|
@ -174,11 +188,12 @@ final class PhoneReportDetailViewModel {
|
|||
return StatusSnapshot(
|
||||
batteryText: batteryText,
|
||||
batteryPercent: batteryPercent,
|
||||
isCharging: isCharging,
|
||||
estimateText: placeholder,
|
||||
brightnessText: brightnessText,
|
||||
volumeText: volumeText,
|
||||
wifiText: wifiText,
|
||||
deviceText: UIDevice.current.model,
|
||||
deviceText: UIDevice.modelName,
|
||||
storageText: storageText,
|
||||
storageRatio: storageRatio,
|
||||
screenTodayText: screenTodayText,
|
||||
|
|
@ -195,6 +210,7 @@ final class PhoneReportDetailViewModel {
|
|||
return StatusSnapshot(
|
||||
batteryText: hasBattery ? "\(battery)%" : placeholder,
|
||||
batteryPercent: hasBattery ? battery : nil,
|
||||
isCharging: false,
|
||||
estimateText: placeholder,
|
||||
brightnessText: placeholder,
|
||||
volumeText: placeholder,
|
||||
|
|
@ -214,6 +230,7 @@ final class PhoneReportDetailViewModel {
|
|||
StatusSnapshot(
|
||||
batteryText: "**",
|
||||
batteryPercent: nil,
|
||||
isCharging: false,
|
||||
estimateText: "**",
|
||||
brightnessText: "**",
|
||||
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
|
||||
let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()),
|
||||
let total = attrs[.systemSize] as? NSNumber,
|
||||
let free = attrs[.systemFreeSize] as? NSNumber
|
||||
let values = try? url.resourceValues(forKeys: keys),
|
||||
let total = values.volumeTotalCapacity,
|
||||
let available = values.volumeAvailableCapacityForImportantUsage,
|
||||
total > 0
|
||||
else { return nil }
|
||||
let used = total.doubleValue - free.doubleValue
|
||||
let usedGB = used / 1_073_741_824
|
||||
let totalGB = total.doubleValue / 1_073_741_824
|
||||
return String(format: "%.1fGB/%.0fGB", usedGB, totalGB)
|
||||
let used = max(0, Int64(total) - available)
|
||||
return (used, Int64(total))
|
||||
}
|
||||
|
||||
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? {
|
||||
guard
|
||||
let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()),
|
||||
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)
|
||||
guard let info = storageBytes(), info.total > 0 else { return nil }
|
||||
return CGFloat(Double(info.used) / Double(info.total))
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
return (0..<7).map { offset -> ChartBar in
|
||||
let dayOffset = offset - 6
|
||||
let date = calendar.date(byAdding: .day, value: dayOffset, to: today) ?? today
|
||||
let key = formatter.string(from: date)
|
||||
let label = formatter.string(from: date).suffix(5).replacingOccurrences(of: "-", with: "/")
|
||||
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)
|
||||
let todayValue = mgr.todayScreenTimeSeconds
|
||||
switch mode {
|
||||
case .day:
|
||||
return dayBars(byDay: byDay, todayValue: todayValue, labelStyle: .monthDaySlash)
|
||||
case .week:
|
||||
return weekBars(byDay: byDay, todayValue: todayValue)
|
||||
}
|
||||
}
|
||||
|
||||
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 today = calendar.startOfDay(for: Date())
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "M/d"
|
||||
let labelFormatter = DateFormatter()
|
||||
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
|
||||
let dayOffset = offset - 6
|
||||
let date = calendar.date(byAdding: .day, value: dayOffset, to: today) ?? today
|
||||
let key = dayKey(for: date)
|
||||
let isToday = dayOffset == 0
|
||||
_ = mode
|
||||
let value = isToday ? todayValue : (byDay[key] ?? 0)
|
||||
return ChartBar(
|
||||
label: formatter.string(from: date),
|
||||
value: isToday ? unlock : 0,
|
||||
label: labelFormatter.string(from: date),
|
||||
value: value,
|
||||
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
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import Lottie
|
||||
import ObjectMapper
|
||||
import URLNavigator
|
||||
|
||||
class SearchLocationResultVC: BaseViewController {
|
||||
|
||||
|
|
@ -27,106 +26,122 @@ class SearchLocationResultVC: BaseViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
rootView.unlockVipView.isHidden = AppContextManager.shared.vip > 1
|
||||
AppContextManager.shared.vip > 1 ? nil : rootView.unlockVipLottieView.play()
|
||||
let isVip = AppContextManager.shared.vip > 1
|
||||
rootView.unlockVipView.isHidden = isVip
|
||||
rootView.scrollView.isHidden = !isVip
|
||||
|
||||
reactiveAction()
|
||||
requestPhoneArea()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
rootView.startMarqueeAnimation()
|
||||
}
|
||||
|
||||
private func reactiveAction() {
|
||||
rootView.successBtn.rx.tap.subscribe(onNext: { _ in
|
||||
if self.code == 0 { // 去查看
|
||||
rootView.actionBtn.rx.tap.subscribe(onNext: { _ in
|
||||
switch self.code {
|
||||
case 0:
|
||||
NotificationCenter.default.post(name: .ShowMemberLocationNotification,
|
||||
object: nil,
|
||||
userInfo: self.memberData)
|
||||
AppRouter.shared.popToRoot()
|
||||
self.navigationController?.popToRootViewController(animated: true)
|
||||
if let tab = self.tabBarController as? MainTabBarController
|
||||
?? UIViewController.topMost?.tabBarController as? MainTabBarController {
|
||||
tab.selectTab(at: 0)
|
||||
}
|
||||
else { // 邀请Ta → 弹出圈子选择
|
||||
guard let model = self.groupModel else {
|
||||
self.requestGroupInfo(showPicker: true)
|
||||
return
|
||||
}
|
||||
GroupChooseView.show(groupModel: model) { _ in }
|
||||
case 20004:
|
||||
SharePopView.show()
|
||||
case 20005:
|
||||
self.showGroupPicker()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
rootView.inviteBtn.rx.tap.subscribe(onNext: { _ in
|
||||
guard let model = self.groupModel else {
|
||||
self.requestGroupInfo(showPicker: true)
|
||||
private func showGroupPicker() {
|
||||
guard let model = groupModel else {
|
||||
requestGroupInfo(showPicker: true)
|
||||
return
|
||||
}
|
||||
GroupChooseView.show(groupModel: model) { _ in }
|
||||
}).disposed(by: disposeBag)
|
||||
presentGroupPicker(with: model)
|
||||
}
|
||||
|
||||
private func presentGroupPicker(with model: GroupModel) {
|
||||
GroupListPopView.show(groupModel: model) { groupKey in
|
||||
guard let key = groupKey,
|
||||
let groupInfo = model.groups.first(where: { $0.group_key == key })?.toJSON() else { return }
|
||||
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": groupInfo])
|
||||
}
|
||||
}
|
||||
|
||||
private func shareApp() {
|
||||
let text = "快来极速定位,实时守护家人出行安全"
|
||||
let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil)
|
||||
present(activity, animated: true)
|
||||
}
|
||||
|
||||
private func handleCode() {
|
||||
guard AppContextManager.shared.vip > 1 else { return }
|
||||
|
||||
var fileName = ""
|
||||
var message = ""
|
||||
var tips = ""
|
||||
rootView.actionBtn.isHidden = true
|
||||
rootView.inviteView.isHidden = true
|
||||
|
||||
switch code {
|
||||
case 0: // 圈子成员
|
||||
fileName = "phone_search_success"
|
||||
message = "定位成功"
|
||||
tips = "点击去查看可获得具体位置"
|
||||
rootView.inviteView.isHidden = false
|
||||
rootView.successTipsLab.text = "已获得具体位置"
|
||||
rootView.successBtn.setTitle("去查看", for: .normal)
|
||||
rootView.phoneLab.text = phone
|
||||
rootView.successLottieView.play()
|
||||
case 20004: // 未注册
|
||||
fileName = "phone_search_fail"
|
||||
message = "此用户未注册"
|
||||
tips = "根据最新的相关法规,需用户双方均安装该APP才可实现定位,未安装APP的用户将不再提供具体定位功能。"
|
||||
rootView.inviteBtn.isHidden = false
|
||||
case 20005: // 不在一个圈子里
|
||||
fileName = "phone_search_success"
|
||||
message = "定位成功"
|
||||
tips = "根据最新的相关法规,您查询的用户与您不在同一个圈子时, 不再提供具体的位置定位,邀请ta加入你的圈子后可随时查看位置。"
|
||||
rootView.inviteView.isHidden = false
|
||||
rootView.successTipsLab.text = "邀请加入圈子后分享位置"
|
||||
rootView.successBtn.setTitle("邀请他", for: .normal)
|
||||
rootView.phoneLab.text = phone
|
||||
rootView.successLottieView.play()
|
||||
case 0:
|
||||
rootView.configureResult(
|
||||
status: "定位成功",
|
||||
disclaimer: "点击去查看可获得具体位置",
|
||||
showInviteRow: false
|
||||
)
|
||||
rootView.actionBtn.setTitle("去查看", for: .normal)
|
||||
rootView.actionBtn.isHidden = false
|
||||
case 20004:
|
||||
rootView.configureResult(
|
||||
status: "用户未注册",
|
||||
disclaimer: "根据最新的相关法规,需用户双方均安装该APP才可实现定位,未安装APP的用户将不再提供具体定位功能。",
|
||||
showInviteRow: false
|
||||
)
|
||||
rootView.actionBtn.setTitle("分享", for: .normal)
|
||||
rootView.actionBtn.isHidden = false
|
||||
case 20005:
|
||||
rootView.configureResult(
|
||||
status: "定位成功",
|
||||
disclaimer: "根据最新的相关法规,您查询的用户与您不在同一个圈子时, 不再提供具体的位置定位,邀请ta加入你的圈子后可随时查看位置。",
|
||||
showInviteRow: false
|
||||
)
|
||||
rootView.actionBtn.setTitle("邀请TA加入", for: .normal)
|
||||
rootView.actionBtn.isHidden = false
|
||||
requestGroupInfo()
|
||||
default:
|
||||
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() {
|
||||
SystemService.phoneArea(phone: phone).subscribe(onNext: { response in
|
||||
guard let data = response.data else { return }
|
||||
self.handleCode()
|
||||
let province = data["Province"].safeString
|
||||
let city = data["City"].safeString
|
||||
self.rootView.phoneAreaLab.text = "\(province)·\(city)"
|
||||
self.rootView.unlockVipPhoneAreaLab.text = "\(province)·\(city)"
|
||||
let area = "\(province)·\(city)"
|
||||
self.rootView.phoneAreaLab.text = area
|
||||
self.rootView.unlockVipPhoneAreaLab.text = area
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - 圈子列表
|
||||
private func requestGroupInfo(showPicker: Bool = false) {
|
||||
GroupService.groupInfo().subscribe { [weak self] response in
|
||||
guard let self = self, let model = response.model else { return }
|
||||
self.groupModel = model
|
||||
if showPicker {
|
||||
GroupChooseView.show(groupModel: model) { _ in }
|
||||
self.presentGroupPicker(with: model)
|
||||
}
|
||||
}.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
init(phone: String, code: Int, memberData: [String: Any] = [:]) {
|
||||
self.phone = phone
|
||||
self.code = code
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// SearchLocationResultView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
|
|
@ -14,16 +12,13 @@ class SearchLocationResultView: UIView {
|
|||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
private func setupRx() {
|
||||
}
|
||||
let headerView = SearchLocationHeaderView()
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(unlockVipView)
|
||||
addSubview(navBgView)
|
||||
addSubview(navView)
|
||||
|
||||
addSubview(normalView)
|
||||
addSubview(successLottieView)
|
||||
addSubview(scrollView)
|
||||
|
||||
navBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
|
|
@ -35,14 +30,26 @@ class SearchLocationResultView: UIView {
|
|||
|
||||
unlockVipView.layoutChain.edges()
|
||||
|
||||
successLottieView.layoutChain.edges()
|
||||
|
||||
normalView.layoutChain
|
||||
scrollView.layoutChain
|
||||
.topToBottomOfView(navView)
|
||||
.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 = {
|
||||
let iv = UIImageView()
|
||||
|
|
@ -52,122 +59,147 @@ class SearchLocationResultView: UIView {
|
|||
}()
|
||||
|
||||
lazy var navView: BaseNavigationView = {
|
||||
let nav = BaseNavigationView(title: " ")
|
||||
return nav
|
||||
BaseNavigationView(title: " ")
|
||||
}()
|
||||
|
||||
// MARK: - 成功烟花Lottie
|
||||
lazy var successLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView(name: "phone_search_fireworks")
|
||||
view.loopMode = .loop
|
||||
view.isUserInteractionEnabled = false
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - 已解锁vip
|
||||
lazy var normalView: UIView = {
|
||||
let view = UIView()
|
||||
lazy var scrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
view.backgroundColor = .clear
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.bounces = false
|
||||
|
||||
view.addSubview(normalLottieView)
|
||||
normalLottieView.layoutChain
|
||||
.top(20)
|
||||
.width(270)
|
||||
.heightToWidth(1)
|
||||
.centerX()
|
||||
let contentView = UIView()
|
||||
contentView.backgroundColor = .clear
|
||||
view.addSubview(contentView)
|
||||
contentView.layoutChain.edges().widthToView(view)
|
||||
|
||||
view.addSubview(phoneAreaLab)
|
||||
phoneAreaLab.layoutChain
|
||||
.topToBottomOfView(normalLottieView)
|
||||
.centerX()
|
||||
contentView.addSubview(headerView)
|
||||
headerView.layoutChain.top(12).edgesHorzontal()
|
||||
|
||||
view.addSubview(messageLab)
|
||||
messageLab.layoutChain
|
||||
.topToBottomOfView(phoneAreaLab, offset: 10)
|
||||
.centerX()
|
||||
|
||||
view.addSubview(tipsLab)
|
||||
tipsLab.layoutChain
|
||||
.topToBottomOfView(messageLab, offset: 15)
|
||||
.edgesHorzontal(31)
|
||||
|
||||
view.addSubview(inviteBtn)
|
||||
inviteBtn.layoutChain
|
||||
.topToBottomOfView(tipsLab, offset: 30)
|
||||
.edgesHorzontal(68)
|
||||
.height(50)
|
||||
|
||||
view.addSubview(inviteView)
|
||||
inviteView.layoutChain
|
||||
.topToBottomOfView(tipsLab, offset: 25)
|
||||
contentView.addSubview(resultCard)
|
||||
resultCard.layoutChain
|
||||
.topToBottomOfView(headerView, offset: 20)
|
||||
.edgesHorzontal(15)
|
||||
.height(80)
|
||||
|
||||
contentView.addSubview(actionBtn)
|
||||
actionBtn.layoutChain
|
||||
.topToBottomOfView(resultCard, offset: 24)
|
||||
.edgesHorzontal(30)
|
||||
.height(56)
|
||||
.bottom(kSafeBottomMargin + 30)
|
||||
|
||||
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 = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
label.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
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.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var messageLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
label.textColor = UIColor(hexStr: "#333333")
|
||||
return label
|
||||
private lazy var disclaimerIcon: UIImageView = {
|
||||
let iv = UIImageView(image: UIImage(named: "SearchLocation/warning"))
|
||||
iv.contentMode = .scaleAspectFit
|
||||
return iv
|
||||
}()
|
||||
|
||||
lazy var tipsLab: UILabel = {
|
||||
lazy var disclaimerLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var normalLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView()
|
||||
view.loopMode = .loop
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var inviteBtn: UIButton = {
|
||||
lazy var actionBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("邀请Ta加入", 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.cornerRadius = 25
|
||||
btn.cornerRadius = 20
|
||||
btn.isHidden = true
|
||||
return btn
|
||||
}()
|
||||
|
||||
var inviteBtn: UIButton { actionBtn }
|
||||
|
||||
lazy var inviteView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
|
||||
view.cornerRadius = 16
|
||||
view.backgroundColor = .white
|
||||
view.cornerRadius = 12
|
||||
view.isHidden = true
|
||||
|
||||
view.addSubview(successBtn)
|
||||
successBtn.layoutChain
|
||||
.right(15)
|
||||
.centerY()
|
||||
.width(90)
|
||||
.height(36)
|
||||
successBtn.layoutChain.right(12).centerY().width(90).height(36)
|
||||
|
||||
view.addSubview(successTipsLab)
|
||||
successTipsLab.layoutChain
|
||||
.topToCenterYOfView(view)
|
||||
.left(15)
|
||||
successTipsLab.layoutChain.topToCenterYOfView(view).left(12)
|
||||
|
||||
view.addSubview(phoneLab)
|
||||
phoneLab.layoutChain
|
||||
.bottomToCenterYOfView(view)
|
||||
.left(15)
|
||||
phoneLab.layoutChain.bottomToCenterYOfView(view).left(12)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
|
@ -181,7 +213,6 @@ class SearchLocationResultView: UIView {
|
|||
|
||||
lazy var successTipsLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "邀请加入圈子后分享位置"
|
||||
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
return label
|
||||
|
|
@ -189,166 +220,68 @@ class SearchLocationResultView: UIView {
|
|||
|
||||
lazy var successBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("邀请Ta", for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
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
|
||||
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 = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.clipsToBounds = true
|
||||
view.backgroundColor = .white
|
||||
view.isHidden = true
|
||||
|
||||
let bgImgView = UIImageView(image: UIImage(named: "SearchLocation/result_bg"))
|
||||
bgImgView.contentMode = .scaleAspectFill
|
||||
view.addSubview(bgImgView)
|
||||
bgImgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.heightToWidth(457/357)
|
||||
|
||||
view.addSubview(unlockVipLottieView)
|
||||
unlockVipLottieView.layoutChain
|
||||
.edgesHorzontal(60)
|
||||
.heightToWidth(1)
|
||||
.top(150)
|
||||
// .bottomToView(bgImgView, offset: -15)
|
||||
let heroPlaceholder = UIImageView()
|
||||
heroPlaceholder.backgroundColor = UIColor(hexStr: "#F5F5F5")
|
||||
heroPlaceholder.cornerRadius = 16
|
||||
view.addSubview(heroPlaceholder)
|
||||
heroPlaceholder.layoutChain
|
||||
.top(120)
|
||||
.edgesHorzontal(40)
|
||||
.height(200)
|
||||
|
||||
view.addSubview(unlockVipPhoneAreaLab)
|
||||
unlockVipPhoneAreaLab.layoutChain
|
||||
.topToBottomOfView(unlockVipLottieView)
|
||||
.topToBottomOfView(heroPlaceholder, offset: 16)
|
||||
.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()
|
||||
unlockVipBtn.setTitle("开通会员", for: .normal)
|
||||
unlockVipBtn.setTitleColor(.white, for: .normal)
|
||||
unlockVipBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
unlockVipBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
unlockVipBtn.titleLabel?.font = FontManager.boboBold(16)
|
||||
unlockVipBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
unlockVipBtn.cornerRadius = 25
|
||||
unlockVipBtn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.push(Route.vipRecharge)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
view.addSubview(unlockVipBtn)
|
||||
unlockVipBtn.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
.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()
|
||||
unlockVipTitleLab.text = "再迈一步,你就能看到:"
|
||||
unlockVipTitleLab.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
unlockVipTitleLab.textColor = UIColor(hexStr: "#16B3FF")
|
||||
view.addSubview(unlockVipTitleLab)
|
||||
unlockVipTitleLab.layoutChain
|
||||
.bottomToTopOfView(tipsLab1, offset: -8)
|
||||
.bottomToTopOfView(unlockVipBtn, offset: -60)
|
||||
.centerX()
|
||||
|
||||
return view
|
||||
|
|
@ -356,13 +289,12 @@ class SearchLocationResultView: UIView {
|
|||
|
||||
lazy var unlockVipLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView(name: "phone_search_no_vip")
|
||||
view.loopMode = .loop
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var unlockVipPhoneAreaLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
label.textColor = UIColor(hexStr: "#16B3FF")
|
||||
return label
|
||||
|
|
@ -372,7 +304,6 @@ class SearchLocationResultView: UIView {
|
|||
super.init(frame: .zero)
|
||||
backgroundColor = .white
|
||||
setupUI()
|
||||
setupRx()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// SearchLocationView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
|
|
@ -21,6 +19,8 @@ class SearchLocationView: UIView {
|
|||
private var player: AVPlayer?
|
||||
private var timeObserver: Any?
|
||||
|
||||
let headerView = SearchLocationHeaderView()
|
||||
|
||||
private func setupRx() {
|
||||
phoneInputTF.rx.text
|
||||
.map { text -> String? in
|
||||
|
|
@ -29,20 +29,13 @@ class SearchLocationView: UIView {
|
|||
}.bind(to: phoneInputTF.rx.text)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
phoneInputTF.rx.text.orEmpty.map { phone -> Bool in
|
||||
if phone.count == 11 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
phoneInputTF.rx.text.orEmpty.map { $0.count == 11 }
|
||||
.bind(to: searchBtn.rx.isEnabled)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
phoneInputTF.rx.controlEvent(.editingDidEndOnExit)
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.phoneInputTF.resignFirstResponder()
|
||||
self?.phoneInputTF.resignFirstResponder()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
|
@ -55,7 +48,6 @@ class SearchLocationView: UIView {
|
|||
addSubview(searchProgressView)
|
||||
|
||||
videoView.layoutChain.edges()
|
||||
|
||||
searchProgressView.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
|
|
@ -74,106 +66,8 @@ class SearchLocationView: UIView {
|
|||
.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() {
|
||||
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
|
||||
}
|
||||
}
|
||||
headerView.startMarqueeAnimation()
|
||||
}
|
||||
|
||||
override func willMove(toWindow newWindow: UIWindow?) {
|
||||
|
|
@ -183,18 +77,16 @@ class SearchLocationView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
lazy var navBgView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.image = UIImage(named: "Common/navBar_bg_2")
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.isHidden = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
lazy var navView: BaseNavigationView = {
|
||||
let nav = BaseNavigationView(title: "找人助手")
|
||||
return nav
|
||||
BaseNavigationView(title: " ")
|
||||
}()
|
||||
|
||||
lazy var scrollView: UIScrollView = {
|
||||
|
|
@ -208,28 +100,15 @@ class SearchLocationView: UIView {
|
|||
view.addSubview(contentView)
|
||||
contentView.layoutChain.edges().widthToView(view)
|
||||
|
||||
let bgImgView = UIImageView()
|
||||
bgImgView.image = UIImage(named: "SearchLocation/bg_1")
|
||||
contentView.addSubview(bgImgView)
|
||||
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)
|
||||
contentView.addSubview(headerView)
|
||||
headerView.layoutChain
|
||||
.top(12)
|
||||
.edgesHorzontal()
|
||||
.height(30)
|
||||
.widthToView(contentView)
|
||||
|
||||
contentView.addSubview(searchInputView)
|
||||
searchInputView.layoutChain
|
||||
.topToBottomOfView(marqueeScrollView, offset: 20)
|
||||
.topToBottomOfView(headerView, offset: 20)
|
||||
.edgesHorzontal(15)
|
||||
.bottom(kSafeBottomMargin + 30)
|
||||
|
||||
|
|
@ -240,12 +119,12 @@ class SearchLocationView: UIView {
|
|||
let v = UIView()
|
||||
v.backgroundColor = .black
|
||||
v.isHidden = true
|
||||
|
||||
return v
|
||||
}()
|
||||
|
||||
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 ?? "")")
|
||||
return
|
||||
}
|
||||
|
|
@ -259,7 +138,6 @@ class SearchLocationView: UIView {
|
|||
playerLayer.videoGravity = .resizeAspectFill
|
||||
videoView.layer.addSublayer(playerLayer)
|
||||
|
||||
// 进度跟踪
|
||||
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 }
|
||||
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 = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
|
||||
view.cornerRadius = 10
|
||||
|
||||
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)
|
||||
view.backgroundColor = UIColor(hexStr: "#EFFAFF")
|
||||
view.cornerRadius = 40
|
||||
|
||||
let inputView = UIView()
|
||||
inputView.backgroundColor = .white
|
||||
inputView.cornerRadius = 4
|
||||
inputView.cornerRadius = 14
|
||||
view.addSubview(inputView)
|
||||
inputView.layoutChain
|
||||
.topToBottomOfView(titleLab, offset: 20)
|
||||
.top(20)
|
||||
.edgesHorzontal(15)
|
||||
.height(40)
|
||||
.height(44)
|
||||
|
||||
let contactsBtn = UIButton()
|
||||
contactsBtn.setTitle("通讯录导入", for: .normal)
|
||||
contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
|
||||
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
|
||||
guard let self = self else { return }
|
||||
self.phoneInputTF.resignFirstResponder()
|
||||
|
|
@ -352,11 +202,7 @@ class SearchLocationView: UIView {
|
|||
}
|
||||
}).disposed(by: disposeBag)
|
||||
inputView.addSubview(contactsBtn)
|
||||
contactsBtn.layoutChain
|
||||
.right(15)
|
||||
.width(90)
|
||||
.edgesVertical()
|
||||
contactsBtn.sizeToFit()
|
||||
contactsBtn.layoutChain.edgesVertical().width(60).right(12)
|
||||
|
||||
inputView.addSubview(phoneInputTF)
|
||||
phoneInputTF.layoutChain
|
||||
|
|
@ -364,21 +210,31 @@ class SearchLocationView: UIView {
|
|||
.left(15)
|
||||
.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()
|
||||
tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私"
|
||||
tipsLab.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
tipsLab.textColor = ThemeManager.shared.color.contentColor
|
||||
tipsLab.textColor = UIColor(hexStr: "#999999")
|
||||
view.addSubview(tipsLab)
|
||||
tipsLab.layoutChain
|
||||
.topToBottomOfView(inputView, offset: 6)
|
||||
.leftToRightOfView(warningIcon, offset: 4)
|
||||
.centerY(warningIcon)
|
||||
.right(15)
|
||||
|
||||
view.addSubview(searchBtn)
|
||||
searchBtn.layoutChain
|
||||
.topToBottomOfView(tipsLab, offset: 35)
|
||||
.edgesHorzontal(52)
|
||||
.topToBottomOfView(warningIcon, offset: 24)
|
||||
.edgesHorzontal(20)
|
||||
.height(50)
|
||||
.bottom(30)
|
||||
.bottom(24)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
|
@ -386,7 +242,7 @@ class SearchLocationView: UIView {
|
|||
lazy var phoneInputTF: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
textField.placeholder = "请输入要查找的号码"
|
||||
textField.placeholder = "输入要查看的号码"
|
||||
textField.keyboardType = .numberPad
|
||||
textField.returnKeyType = .done
|
||||
return textField
|
||||
|
|
@ -396,9 +252,9 @@ class SearchLocationView: UIView {
|
|||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("开始查找", 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.cornerRadius = 25
|
||||
btn.cornerRadius = 20
|
||||
return btn
|
||||
}()
|
||||
|
||||
|
|
@ -419,7 +275,7 @@ class SearchLocationView: UIView {
|
|||
|
||||
let steps = ["分析用户号码", "正在找 CGI 蜂窝参数", "SS7 信息交流", "号码已获授权", "处理完成"]
|
||||
var prevStepView: UIView?
|
||||
for (i, text) in steps.enumerated() {
|
||||
for text in steps {
|
||||
let stepView = UIView()
|
||||
view.addSubview(stepView)
|
||||
|
||||
|
|
@ -434,14 +290,8 @@ class SearchLocationView: UIView {
|
|||
label.textColor = .white
|
||||
stepView.addSubview(label)
|
||||
|
||||
icon.layoutChain
|
||||
.left().centerY()
|
||||
.width(18).height(18)
|
||||
|
||||
label.layoutChain
|
||||
.leftToRightOfView(icon, offset: 8)
|
||||
.centerY().right()
|
||||
|
||||
icon.layoutChain.left().centerY().width(18).height(18)
|
||||
label.layoutChain.leftToRightOfView(icon, offset: 8).centerY().right()
|
||||
stepView.layoutChain
|
||||
.leftToRightOfView(searchPhoneLottieView, offset: 10)
|
||||
.right(10)
|
||||
|
|
@ -455,7 +305,6 @@ class SearchLocationView: UIView {
|
|||
prevStepView = stepView
|
||||
}
|
||||
|
||||
// 进度条 + 百分比
|
||||
progressBar.layer.cornerRadius = 4
|
||||
progressBar.clipsToBounds = true
|
||||
progressBar.trackTintColor = .white.withAlphaComponent(0.2)
|
||||
|
|
@ -483,16 +332,13 @@ class SearchLocationView: UIView {
|
|||
private var progressIcons: [UIImageView] = []
|
||||
|
||||
private lazy var progressBar: UIProgressView = {
|
||||
let p = UIProgressView()
|
||||
return p
|
||||
UIProgressView()
|
||||
}()
|
||||
|
||||
private lazy var progressLab: UILabel = {
|
||||
let l = UILabel()
|
||||
return l
|
||||
UILabel()
|
||||
}()
|
||||
|
||||
/// 根据视频进度更新步骤图标和进度条 (0~1)
|
||||
func updateVideoProgress(_ progress: Float) {
|
||||
let clamped = max(0, min(1, progress))
|
||||
progressBar.progress = clamped
|
||||
|
|
@ -517,7 +363,6 @@ class SearchLocationView: UIView {
|
|||
backgroundColor = .white
|
||||
setupUI()
|
||||
setupRx()
|
||||
setupMarquee()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
rootView.cancelledBtn.rx.tap.subscribe(onNext: { _ in
|
||||
CancellationPopView.show {
|
||||
self.deleteAccount()
|
||||
}
|
||||
AppRouter.push(Route.cancelAccount)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleClearCacheTap))
|
||||
|
|
|
|||
|
|
@ -290,10 +290,10 @@ class AccountView: UIView {
|
|||
lazy var logoutBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("退出登录", for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
btn.cornerRadius = 25
|
||||
btn.cornerRadius = 16
|
||||
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 onCopyID: ((String) -> Void)?
|
||||
var onTapSelfAvatar: (() -> Void)?
|
||||
/// 点击照片墙上某成员(含自己)→ 进入成员信息
|
||||
var onTapMember: ((GroupMemberModel) -> Void)?
|
||||
|
||||
private var members: [GroupMemberModel] = []
|
||||
/// 与 members 对齐的夹子颜色下标(相邻不同色)
|
||||
|
|
@ -463,15 +464,13 @@ extension MinePhotoWallView: UICollectionViewDataSource, UICollectionViewDelegat
|
|||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
let model = members[indexPath.item]
|
||||
if model.user_id == AppContextManager.shared.userId {
|
||||
onTapSelfAvatar?()
|
||||
}
|
||||
centeredIndex = indexPath.item
|
||||
let before = collectionView.contentOffset
|
||||
scrollToIndex(indexPath.item, animated: true)
|
||||
applyArcAndFocus()
|
||||
updateInfo(model)
|
||||
onCenteredMember?(model)
|
||||
onTapMember?(model)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if abs(self.collectionView.contentOffset.x - before.x) < 0.5, !self.isDragging {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import RxSwift
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import ObjectMapper
|
||||
|
||||
final class MineViewController: BaseViewController {
|
||||
|
||||
|
|
@ -23,6 +24,8 @@ final class MineViewController: BaseViewController {
|
|||
|
||||
private let viewModel = MineViewModel()
|
||||
private var groupKey: String = ""
|
||||
private var groupModel: GroupModel?
|
||||
private var photoWallMembers: [GroupMemberModel] = []
|
||||
|
||||
override func loadView() {
|
||||
rootView = MineView(frame: UIScreen.main.bounds)
|
||||
|
|
@ -64,8 +67,8 @@ final class MineViewController: BaseViewController {
|
|||
DLToast.showSuccess(text: "已复制")
|
||||
}
|
||||
|
||||
rootView.photoWall.onTapSelfAvatar = {
|
||||
AppRouter.push(Route.myProfile)
|
||||
rootView.photoWall.onTapMember = { [weak self] member in
|
||||
self?.openMemberInfo(selectedUserId: member.user_id)
|
||||
}
|
||||
|
||||
rootView.onQuickTap = { [weak self] title in
|
||||
|
|
@ -94,8 +97,10 @@ final class MineViewController: BaseViewController {
|
|||
private func requestGroupMembers() {
|
||||
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
||||
guard let self = self, let model = response.model else { return }
|
||||
self.groupModel = model
|
||||
self.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
|
||||
let ownerId = sorted.first(where: { model.default_group_key.contains($0.user_id) })?.user_id ?? ""
|
||||
self.rootView.photoWall.reload(
|
||||
|
|
@ -105,4 +110,17 @@ final class MineViewController: BaseViewController {
|
|||
)
|
||||
}).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()
|
||||
case "权限检测":
|
||||
AppRouter.push(Route.checkPermission)
|
||||
case "数据安全", "状态设置", "关于我们":
|
||||
case "数据安全", "状态设置":
|
||||
DLToast.show(text: "敬请期待")
|
||||
case "关于我们":
|
||||
AppRouter.push(Route.about)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ class VipRechargeView: UIView {
|
|||
private func setupRx() {
|
||||
navView.onBackTapped = { VipWaivePopView.show() }
|
||||
|
||||
backBtn.rx.tap.subscribe { _ in
|
||||
VipWaivePopView.show()
|
||||
}.disposed(by: disposeBag)
|
||||
|
||||
agreementLab.rx.tapGesture.subscribe { _ in
|
||||
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.userAgreementUrl])
|
||||
}.disposed(by: disposeBag)
|
||||
|
|
@ -45,11 +49,19 @@ class VipRechargeView: UIView {
|
|||
bottomView.addSubview(payBtnView)
|
||||
bottomView.addSubview(payPriceView)
|
||||
|
||||
addSubview(backBtn)
|
||||
addSubview(navView)
|
||||
|
||||
backBtn.layoutChain
|
||||
.top(kStatusBarHeight + 5)
|
||||
.left(15)
|
||||
.width(36)
|
||||
.height(36)
|
||||
|
||||
navView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(kNaviHeight)
|
||||
navView.backgroundColor = .white
|
||||
navView.alpha = 0
|
||||
|
||||
bottomView.layoutChain
|
||||
|
|
@ -131,6 +143,13 @@ class VipRechargeView: UIView {
|
|||
|
||||
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 = {
|
||||
let view = UIScrollView()
|
||||
view.backgroundColor = UIColor(hexStr: "#F5FBFB")
|
||||
|
|
@ -716,7 +735,7 @@ class VipRechargeView: UIView {
|
|||
extension VipRechargeView: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let maxY = scrollView.contentOffset.y
|
||||
let alpha = maxY / kNaviHeight
|
||||
let alpha = maxY / (kNaviHeight / 2)
|
||||
navView.alpha = alpha < 0.0 ? 0.0 : alpha
|
||||
}
|
||||
}
|
||||
|
|
|
|||