diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index b8d1d35..0aca42c 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -176,6 +176,20 @@ 307073E62FD18A20004C37CC /* GroupChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E22FD18A20004C37CC /* GroupChatVC.swift */; }; 307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */; }; 30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */; }; + 30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */; }; + 30A87A4D2FEE1DB40095E7C6 /* ItineraryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */; }; + 30A87A502FEE4E4B0095E7C6 /* ScheduleHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4F2FEE4E4B0095E7C6 /* ScheduleHistoryView.swift */; }; + 30A87A522FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A512FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift */; }; + 30A87A542FEE50B10095E7C6 /* ScheduleHistoryVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A532FEE50B10095E7C6 /* ScheduleHistoryVM.swift */; }; + 30A87A572FEE5A350095E7C6 /* ScheduleViewedVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A562FEE5A350095E7C6 /* ScheduleViewedVC.swift */; }; + 30A87A592FEE5A4C0095E7C6 /* ScheduleViewedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A582FEE5A4C0095E7C6 /* ScheduleViewedView.swift */; }; + 30A87A5B2FEE5AC20095E7C6 /* ScheduleViewedVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A5A2FEE5AC20095E7C6 /* ScheduleViewedVM.swift */; }; + 30A87A5E2FEE71A50095E7C6 /* CreateBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A5D2FEE71A50095E7C6 /* CreateBubbleView.swift */; }; + 30A87A602FEE71B80095E7C6 /* CreateBubbleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A5F2FEE71B80095E7C6 /* CreateBubbleVC.swift */; }; + 30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A612FEE724D0095E7C6 /* CreateBubbleTiemView.swift */; }; + 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 */; }; 30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */; }; 30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; }; 30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */; }; @@ -439,6 +453,20 @@ 307073E32FD18A20004C37CC /* GroupChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatView.swift; sourceTree = ""; }; 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatViewModel.swift; sourceTree = ""; }; 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListPopView.swift; sourceTree = ""; }; + 30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailVC.swift; sourceTree = ""; }; + 30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailView.swift; sourceTree = ""; }; + 30A87A4F2FEE4E4B0095E7C6 /* ScheduleHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleHistoryView.swift; sourceTree = ""; }; + 30A87A512FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleHistoryVC.swift; sourceTree = ""; }; + 30A87A532FEE50B10095E7C6 /* ScheduleHistoryVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleHistoryVM.swift; sourceTree = ""; }; + 30A87A562FEE5A350095E7C6 /* ScheduleViewedVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewedVC.swift; sourceTree = ""; }; + 30A87A582FEE5A4C0095E7C6 /* ScheduleViewedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewedView.swift; sourceTree = ""; }; + 30A87A5A2FEE5AC20095E7C6 /* ScheduleViewedVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewedVM.swift; sourceTree = ""; }; + 30A87A5D2FEE71A50095E7C6 /* CreateBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleView.swift; sourceTree = ""; }; + 30A87A5F2FEE71B80095E7C6 /* CreateBubbleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleVC.swift; sourceTree = ""; }; + 30A87A612FEE724D0095E7C6 /* CreateBubbleTiemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTiemView.swift; sourceTree = ""; }; + 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = ""; }; + 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = ""; }; + 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = ""; }; 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = ""; }; 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = ""; }; 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = ""; }; @@ -930,6 +958,7 @@ 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */, 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */, 30D87CDC2FDFF07500E958FD /* InteractionView.swift */, + 30A87A5C2FEE711C0095E7C6 /* Bubble */, 30CCDE4F2FE2782700F5214A /* SignIn */, 30CCDE562FE39F6B00F5214A /* SOS */, ); @@ -1216,6 +1245,48 @@ path = GroupChat; sourceTree = ""; }; + 30A87A492FEE1CB20095E7C6 /* ItineraryDetail */ = { + isa = PBXGroup; + children = ( + 30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */, + 30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */, + ); + path = ItineraryDetail; + sourceTree = ""; + }; + 30A87A4E2FEE4E390095E7C6 /* ScheduleHistory */ = { + isa = PBXGroup; + children = ( + 30A87A512FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift */, + 30A87A532FEE50B10095E7C6 /* ScheduleHistoryVM.swift */, + 30A87A4F2FEE4E4B0095E7C6 /* ScheduleHistoryView.swift */, + ); + path = ScheduleHistory; + sourceTree = ""; + }; + 30A87A552FEE5A130095E7C6 /* ScheduleViewed */ = { + isa = PBXGroup; + children = ( + 30A87A562FEE5A350095E7C6 /* ScheduleViewedVC.swift */, + 30A87A5A2FEE5AC20095E7C6 /* ScheduleViewedVM.swift */, + 30A87A582FEE5A4C0095E7C6 /* ScheduleViewedView.swift */, + ); + path = ScheduleViewed; + sourceTree = ""; + }; + 30A87A5C2FEE711C0095E7C6 /* Bubble */ = { + isa = PBXGroup; + children = ( + 30A87A5F2FEE71B80095E7C6 /* CreateBubbleVC.swift */, + 30A87A5D2FEE71A50095E7C6 /* CreateBubbleView.swift */, + 30A87A612FEE724D0095E7C6 /* CreateBubbleTiemView.swift */, + 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */, + 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */, + 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */, + ); + path = Bubble; + sourceTree = ""; + }; 30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = { isa = PBXGroup; children = ( @@ -1302,6 +1373,9 @@ 30D74ABB2FEA67CE0050EB2C /* CreateSchedule */, 30D74BF22FEB6F5B0050EB2C /* LocationPicker */, 30BF300A2FED09A300D9CB52 /* ScheduleDetail */, + 30A87A492FEE1CB20095E7C6 /* ItineraryDetail */, + 30A87A4E2FEE4E390095E7C6 /* ScheduleHistory */, + 30A87A552FEE5A130095E7C6 /* ScheduleViewed */, ); path = Schedule; sourceTree = ""; @@ -1644,6 +1718,8 @@ 305A76922FCA8C7000227D26 /* LogUtils.swift in Sources */, 305A76932FCA8C7000227D26 /* AddImageCell.swift in Sources */, 30EFF3D82FDA8F1000EB35D4 /* EmergencyContactVC.swift in Sources */, + 30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */, + 30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */, 30DC185E2FD1211D0041DCD1 /* VipRightsVC.swift in Sources */, 305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */, 305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */, @@ -1660,7 +1736,9 @@ 3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */, 30EFF3DA2FDA935D00EB35D4 /* EmergencyContactFooterView.swift in Sources */, 3062E8C22FCFB86800CEF511 /* CreateGroupViewModel.swift in Sources */, + 30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */, 305A769B2FCA8C7000227D26 /* PopupAnimators.swift in Sources */, + 30A87A4D2FEE1DB40095E7C6 /* ItineraryDetailView.swift in Sources */, 305A769C2FCA8C7000227D26 /* PopupViewController.swift in Sources */, 305A769D2FCA8C7000227D26 /* PopupViewController+Extension.swift in Sources */, 30EFF3C42FDA431D00EB35D4 /* ChangePhoneVC.swift in Sources */, @@ -1677,6 +1755,7 @@ 305A76A42FCA8C7000227D26 /* Date+Extension.swift in Sources */, 30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */, 305A76A52FCA8C7000227D26 /* Dictionay+Extension.swift in Sources */, + 30A87A522FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift in Sources */, 305A76A62FCA8C7000227D26 /* Int+Extension.swift in Sources */, 30EFF3D62FDA8F0100EB35D4 /* EmergencyContactView.swift in Sources */, 30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */, @@ -1686,6 +1765,7 @@ 305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */, 305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */, 305A76AB2FCA8C7000227D26 /* ScaleType.swift in Sources */, + 30A87A572FEE5A350095E7C6 /* ScheduleViewedVC.swift in Sources */, 30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */, 305A76AC2FCA8C7000227D26 /* String+Extension.swift in Sources */, 30D74AB82FEA36A50050EB2C /* ItineraryService.swift in Sources */, @@ -1697,6 +1777,7 @@ 305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */, 305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */, 305A76B32FCA8C7000227D26 /* UILabel+Extension.swift in Sources */, + 30A87A5B2FEE5AC20095E7C6 /* ScheduleViewedVM.swift in Sources */, 305A76B42FCA8C7000227D26 /* UINavigationController+FDFullscreenPopGesture.m in Sources */, 305A76B52FCA8C7000227D26 /* UITableView+Extension.swift in Sources */, 30CCDE582FE39F8C00F5214A /* SOSView.swift in Sources */, @@ -1720,7 +1801,9 @@ 3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */, 30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */, 305A76C22FCA8C7000227D26 /* BaseViewModel.swift in Sources */, + 30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */, 30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */, + 30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */, 30DC185B2FD11E7A0041DCD1 /* NavigationTitleView.swift in Sources */, 30DC185C2FD11E7A0041DCD1 /* WebViewController.swift in Sources */, 305A76C32FCA8C7000227D26 /* MainTabBarController.swift in Sources */, @@ -1742,6 +1825,7 @@ 305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */, 3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */, 305A76CF2FCA8C7000227D26 /* CountDownService.swift in Sources */, + 30A87A502FEE4E4B0095E7C6 /* ScheduleHistoryView.swift in Sources */, 305A76D02FCA8C7000227D26 /* MoneyFormatter.swift in Sources */, 305A76D12FCA8C7000227D26 /* TimeSpecificNotificationManager.swift in Sources */, 305A76D22FCA8C7000227D26 /* ThemeManager.swift in Sources */, @@ -1760,6 +1844,7 @@ 305A76DC2FCA8C7000227D26 /* InputSubject.swift in Sources */, 305A76DD2FCA8C7000227D26 /* NSObject+Rx.swift in Sources */, 305A79902FCAC61A00227D26 /* InviteMemberVC.swift in Sources */, + 30A87A542FEE50B10095E7C6 /* ScheduleHistoryVM.swift in Sources */, 305A76DE2FCA8C7000227D26 /* ObservableType+ObjectMapper.swift in Sources */, 305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */, 30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */, @@ -1771,6 +1856,7 @@ 30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */, 30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */, 30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */, + 30A87A592FEE5A4C0095E7C6 /* ScheduleViewedView.swift in Sources */, 30EFF3CF2FDA669800EB35D4 /* MyProfileVC.swift in Sources */, 30CCDE5C2FE3A1A800F5214A /* SOSPracticeView.swift in Sources */, 30EFF3E52FDAA93400EB35D4 /* PrivacyPolicyVC.swift in Sources */, @@ -1800,8 +1886,10 @@ 30BAB8532FCD337C00C33B5C /* GroupService.swift in Sources */, 305A76EE2FCA8C7000227D26 /* MineView.swift in Sources */, 305A76EF2FCA8C7000227D26 /* MineViewController.swift in Sources */, + 30A87A5E2FEE71A50095E7C6 /* CreateBubbleView.swift in Sources */, 30D74ABF2FEA67F30050EB2C /* CreateScheduleView.swift in Sources */, 305A76F02FCA8C7000227D26 /* MineViewModel.swift in Sources */, + 30A87A602FEE71B80095E7C6 /* CreateBubbleVC.swift in Sources */, 305A76F12FCA8C7000227D26 /* SystemService.swift in Sources */, 30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */, 30C4C01B2FDBF09D009215C1 /* RemoveMemberView.swift in Sources */, diff --git a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate index 17d18b1..19a2575 100644 Binary files a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate and b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/QuickLocation/API/ItineraryAPI.swift b/QuickLocation/API/ItineraryAPI.swift index e192c30..06259b4 100644 --- a/QuickLocation/API/ItineraryAPI.swift +++ b/QuickLocation/API/ItineraryAPI.swift @@ -36,6 +36,12 @@ enum ItineraryAPI { /// - Parameters: /// - id: 行程ID case delete(id: String) + + /// 关注行程 + /// - Parameters: + /// - id: 行程ID + /// - op: 1关注 2取消关注 + case follow(id: String, op: Int) } extension ItineraryAPI: MultiTargetProtocol { @@ -50,6 +56,8 @@ extension ItineraryAPI: MultiTargetProtocol { return "mapi/itinerary/route/set" case .delete: return "mapi/itinerary/route/delete" + case .follow: + return "mapi/itinerary/route/follow" } } @@ -71,7 +79,9 @@ extension ItineraryAPI: MultiTargetProtocol { params["follow"] = follow params["own"] = own params["history"] = history - params["group_key"] = group_key + if !group_key.isEmpty { + params["group_key"] = group_key + } params["page"] = page params["limit"] = 20 return .requestParameters(parameters: params, encoding: URLEncoding()) @@ -98,6 +108,13 @@ extension ItineraryAPI: MultiTargetProtocol { var params = Parameters() params["id"] = id return .requestParameters(parameters: params, encoding: URLEncoding()) + + case let .follow(id, op): + var params = Parameters() + params["id"] = id + params["op"] = op + return .requestParameters(parameters: params, encoding: JSONEncoding()) + } } } diff --git a/QuickLocation/API/UserAPI.swift b/QuickLocation/API/UserAPI.swift index 85f76ac..67cbd23 100644 --- a/QuickLocation/API/UserAPI.swift +++ b/QuickLocation/API/UserAPI.swift @@ -66,6 +66,12 @@ enum UserAPI { /// 查看关注用户列表 case followList + + /// 设置气泡 + /// - Parameters: + /// - enable: + /// - keep_time: + case bubble(enable: Bool, keep_time: Int) } extension UserAPI: MultiTargetProtocol { @@ -100,6 +106,8 @@ extension UserAPI: MultiTargetProtocol { return "api/user/notice" case .followList: return "mapi/user/followed" + case .bubble: + return "mapi/bubble/operate" } } @@ -183,6 +191,12 @@ extension UserAPI: MultiTargetProtocol { case .followList: return .requestParameters(parameters: Parameters(), encoding: URLEncoding()) + + case let .bubble(enable, keep_time): + var params = Parameters() + params["enable"] = enable + params["keep_time"] = keep_time + return .requestParameters(parameters: params, encoding: JSONEncoding()) } } } diff --git a/QuickLocation/Assets.xcassets/Bubble/Contents.json b/QuickLocation/Assets.xcassets/Bubble/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Bubble/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/QuickLocation/Assets.xcassets/Bubble/time.imageset/Contents.json b/QuickLocation/Assets.xcassets/Bubble/time.imageset/Contents.json new file mode 100644 index 0000000..6a4d508 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Bubble/time.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Bubble/time.imageset/Vector@2x.png b/QuickLocation/Assets.xcassets/Bubble/time.imageset/Vector@2x.png new file mode 100644 index 0000000..6a63773 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Bubble/time.imageset/Vector@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Bubble/time.imageset/Vector@3x.png b/QuickLocation/Assets.xcassets/Bubble/time.imageset/Vector@3x.png new file mode 100644 index 0000000..97697e0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Bubble/time.imageset/Vector@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/Contents.json new file mode 100644 index 0000000..72ff416 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "upgrade_bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "upgrade_bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/upgrade_bg@2x.png b/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/upgrade_bg@2x.png new file mode 100644 index 0000000..61b952d Binary files /dev/null and b/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/upgrade_bg@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/upgrade_bg@3x.png b/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/upgrade_bg@3x.png new file mode 100644 index 0000000..0ca460a Binary files /dev/null and b/QuickLocation/Assets.xcassets/Bubble/upgrade_bg.imageset/upgrade_bg@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Contents.json b/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Contents.json new file mode 100644 index 0000000..eeff2c1 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle_42232@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle_42232@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Rectangle_42232@2x.png b/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Rectangle_42232@2x.png new file mode 100644 index 0000000..024a79b Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Rectangle_42232@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Rectangle_42232@3x.png b/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Rectangle_42232@3x.png new file mode 100644 index 0000000..927704d Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/checkbox.imageset/Rectangle_42232@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Contents.json b/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Contents.json new file mode 100644 index 0000000..ad1527e --- /dev/null +++ b/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_2303@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_2303@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Group_2303@2x.png b/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Group_2303@2x.png new file mode 100644 index 0000000..df448af Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Group_2303@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Group_2303@3x.png b/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Group_2303@3x.png new file mode 100644 index 0000000..1582a23 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/checkbox_selected.imageset/Group_2303@3x.png differ diff --git a/QuickLocation/Main/Tabbar/MainTabBarController.swift b/QuickLocation/Main/Tabbar/MainTabBarController.swift index 72cec02..e145772 100644 --- a/QuickLocation/Main/Tabbar/MainTabBarController.swift +++ b/QuickLocation/Main/Tabbar/MainTabBarController.swift @@ -59,7 +59,7 @@ final class MainTabBarController: UITabBarController { customTabBar.delegate = self // 点击"探索"(index=1)时游客跳登录、不选中 customTabBar.shouldSelectTab = { index in - if index == 1, AppContextManager.shared.isGuest { + if index == 1 || index == 2, AppContextManager.shared.isGuest { AppRouter.push(Route.login) return false } @@ -98,7 +98,7 @@ final class MainTabBarController: UITabBarController { extension MainTabBarController: QuickLocationTabBarDelegate { func tabBar(_ tabBar: QuickLocationTabBar, didSelectTabAt index: Int) { // 点击"探索"(index=1)时判断游客,是则跳登录、不选中 - if index == 1, AppContextManager.shared.isGuest { + if index == 1 || index == 2, AppContextManager.shared.isGuest { AppRouter.push(Route.login) return } diff --git a/QuickLocation/Manager/App/RouterManager.swift b/QuickLocation/Manager/App/RouterManager.swift index 2b31ced..2998a67 100644 --- a/QuickLocation/Manager/App/RouterManager.swift +++ b/QuickLocation/Manager/App/RouterManager.swift @@ -57,6 +57,14 @@ enum Route: String { case createSchedule = "createSchedule" /// 行程详情 case scheduleDetail = "scheduleDetail" + /// 行程路线 + case itineraryDetail = "itineraryDetail" + /// 历史行程 + case scheduleHistory = "scheduleHistory" + /// 看过我 + case scheduleViewed = "scheduleViewed" + /// 创建气泡 + case createBubble = "createBubble" } extension Route: RouterTarget { @@ -275,6 +283,28 @@ extension AppRouter: AppRouterProtocol { return ScheduleDetailVC(routeId: routeId, scheduleJson: parameters["scheduleJson"].safeDictionary as! [String : Any]) } + + // MARK: - 行程路线 + AppRouter.register(Route.itineraryDetail) { url, parameters in + return ItineraryDetailVC(scheduleJson: parameters["scheduleJson"].safeDictionary as! [String : Any]) + } + + // MARK: - 历史行程 + AppRouter.register(Route.scheduleHistory) { url, parameters in + ScheduleHistoryVC() + } + + // MARK: - 谁看过我 + AppRouter.register(Route.scheduleViewed) { url, parameters in + ScheduleViewedVC() + } + + // MARK: - 创建气泡 + AppRouter.register(Route.createBubble) { url, parameters in + let vc = CreateBubbleVC() + vc.isNeedLogin = true + return vc + } } } diff --git a/QuickLocation/Section/Home/Bubble/CreateBubbleDoneView.swift b/QuickLocation/Section/Home/Bubble/CreateBubbleDoneView.swift new file mode 100644 index 0000000..ec682c5 --- /dev/null +++ b/QuickLocation/Section/Home/Bubble/CreateBubbleDoneView.swift @@ -0,0 +1,159 @@ +// +// CreateBubbleDoneView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa + +class CreateBubbleDoneView: UIView { + + var disposeBag = DisposeBag() + + private func setupRx() { + + } + + private func setupUI() { + addSubview(titleLab) + addSubview(timeIcon) + addSubview(timeLab) + addSubview(messageView) + addSubview(iconView) + addSubview(popupBtn) + + titleLab.layoutChain + .top(15) + .centerX() + + timeLab.layoutChain + .topToBottomOfView(titleLab, offset: 12) + .centerX() + + timeIcon.layoutChain + .rightToLeftOfView(timeLab, offset: -8) + .centerY(timeLab) + + messageView.layoutChain + .topToBottomOfView(timeLab, offset: 41) + .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) + } + + 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 + }() + + private var countdownTimer: Timer? + private var endDate: Date = Date() + + func startCountdown(endDate: Date) { + self.endDate = endDate + countdownTimer?.invalidate() + countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + self?.updateTime() + } + updateTime() + } + + private func updateTime() { + let remaining = endDate.timeIntervalSince(Date()) + if remaining <= 0 { + timeLab.text = "00:00:00" + countdownTimer?.invalidate() + countdownTimer = nil + return + } + let hours = Int(remaining) / 3600 + let minutes = (Int(remaining) % 3600) / 60 + let seconds = Int(remaining) % 60 + timeLab.text = String(format: "%02d:%02d:%02d", hours, minutes, seconds) + } + + deinit { + countdownTimer?.invalidate() + } + + override init(frame: CGRect) { + super.init(frame: .zero) + self.isHidden = true + backgroundColor = .white + setupUI() + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/QuickLocation/Section/Home/Bubble/CreateBubblePopView.swift b/QuickLocation/Section/Home/Bubble/CreateBubblePopView.swift new file mode 100644 index 0000000..68c222f --- /dev/null +++ b/QuickLocation/Section/Home/Bubble/CreateBubblePopView.swift @@ -0,0 +1,120 @@ +// +// CreateBubblePopView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa + +class CreateBubblePopView: UIView { + + private static let shared = CreateBubblePopView(frame: CGRect(origin: .zero, size: kScreenSize)) + + var disposeBag = DisposeBag() + + static func show() { + guard let superView = kKeyWindow else { + return + } + + if CreateBubblePopView.shared.superview != nil { + CreateBubblePopView.shared.removeFromSuperview() + CreateBubblePopView.shared.bgView.frame = .zero + } + CreateBubblePopView.shared.bgView.alpha = 1 + CreateBubblePopView.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight) + superView.addSubview(CreateBubblePopView.shared) + superView.bringSubviewToFront(CreateBubblePopView.shared) + + UIView.animate(withDuration: 0.25) { + CreateBubblePopView.shared.bgView.alpha = 1 + } + } + + /// 关闭 + static func dismiss() { + guard CreateBubblePopView.shared.superview != nil else { return } + + UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) { + CreateBubblePopView.shared.bgView.alpha = 0 + } completion: { _ in + CreateBubblePopView.shared.removeFromSuperview() + } + } + + private func setupRx() { + upgradedBtn.rx.tap.subscribe(onNext: { _ in + CreateBubblePopView.dismiss() + AppRouter.push(Route.vipRecharge) + }).disposed(by: disposeBag) + + closeBtn.rx.tap.subscribe(onNext: { _ in + CreateBubblePopView.dismiss() + }).disposed(by: disposeBag) + } + + private lazy var bgView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.5) + return view + }() + + lazy var vipImgView: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "Bubble/vip_pop") + view.contentMode = .scaleAspectFill + return view + }() + + lazy var upgradedBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.backgroundColor = .clear + btn.setBackgroundImage(UIImage(named: "Bubble/upgrade_bg"), for: .normal) + return btn + }() + + lazy var closeBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.backgroundColor = .clear + btn.setBackgroundImage(UIImage(named: "Group/close"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 100, bottom: 100, right: 100) + return btn + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + addSubview(bgView) + bgView.addSubview(vipImgView) + bgView.addSubview(upgradedBtn) + bgView.addSubview(closeBtn) + + vipImgView.layoutChain + .centerY() + .edgesHorzontal(25) + .heightToWidth(814/648) + + upgradedBtn.layoutChain + .topToBottomOfView(vipImgView, offset: -20) + .centerX() + .width(240) + .height(60) + + closeBtn.layoutChain + .topToBottomOfView(upgradedBtn, offset: 15) + .centerX() + .width(22) + .height(22) + + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/QuickLocation/Section/Home/Bubble/CreateBubbleTiemView.swift b/QuickLocation/Section/Home/Bubble/CreateBubbleTiemView.swift new file mode 100644 index 0000000..3e779d3 --- /dev/null +++ b/QuickLocation/Section/Home/Bubble/CreateBubbleTiemView.swift @@ -0,0 +1,123 @@ +// +// CreateBubbleTiemView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa +import RxGesture + +class CreateBubbleTiemView: UIView { + + var disposeBag = DisposeBag() + let selectedHour = BehaviorRelay(value: 1) + var onNextTap: (() -> Void)? + private let hours = Array(1...6) + + private func setupRx() { + nextBtn.rx.tap.subscribe(onNext: { [weak self] _ in + self?.onNextTap?() + }).disposed(by: disposeBag) + } + + private func setupUI() { + addSubview(titleLab) + addSubview(pickerView) + addSubview(hourLab) + addSubview(nextBtn) + + titleLab.layoutChain + .top(15) + .centerX() + + pickerView.layoutChain + .centerX().centerY() + .edgesHorzontal(15) + .height(180) + + hourLab.layoutChain + .centerX(self, offset: 50) + .centerY(pickerView) + + nextBtn.layoutChain + .centerX() + .width(240) + .height(50) + .bottom(kSafeBottomMargin + 10) + } + + lazy var titleLab: UILabel = { + let label = UILabel() + label.text = "您会在这个气泡里待多久?" + label.font = .systemFont(ofSize: 20, weight: .bold) + label.textColor = ThemeManager.shared.color.titleAuxColor + return label + }() + + lazy var pickerView: UIPickerView = { + let pv = UIPickerView() + pv.delegate = self + pv.dataSource = self + return pv + }() + + lazy var hourLab: UILabel = { + let label = UILabel() + label.text = "小时" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = UIColor(hexStr: "#16B3FF") + return label + }() + + lazy var nextBtn: 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 + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + self.isHidden = true + backgroundColor = .white + setupUI() + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension CreateBubbleTiemView: UIPickerViewDataSource, UIPickerViewDelegate { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + hours.count + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + let label = view as? UILabel ?? UILabel() + label.textAlignment = .center + label.font = .systemFont(ofSize: 20, weight: .medium) + let text = "\(hours[row])" + let selected = row == pickerView.selectedRow(inComponent: component) + label.textColor = selected ? UIColor(hexStr: "#16B3FF") : UIColor(hexStr: "#999999") + label.text = text + return label + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + selectedHour.accept(hours[row]) + pickerView.reloadAllComponents() + } +} diff --git a/QuickLocation/Section/Home/Bubble/CreateBubbleTipsView.swift b/QuickLocation/Section/Home/Bubble/CreateBubbleTipsView.swift new file mode 100644 index 0000000..09b77e4 --- /dev/null +++ b/QuickLocation/Section/Home/Bubble/CreateBubbleTipsView.swift @@ -0,0 +1,120 @@ +// +// CreateBubbleTipsView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa + +class CreateBubbleTipsView: UIView { + + var disposeBag = DisposeBag() + var messageText: String = "" + + private func setupRx() { + } + + private func setupUI() { + addSubview(titleLab) + addSubview(tipsLab) + addSubview(messageView) + addSubview(iconView) + addSubview(doneBtn) + + titleLab.layoutChain + .top(15) + .edgesHorzontal(58) + + messageView.layoutChain + .topToBottomOfView(titleLab, offset: 47) + .left(79) + .right(30) + + iconView.layoutChain + .rightToLeftOfView(messageView, offset: -9) + .bottomToView(messageView) + .height(40) + .width(40) + + doneBtn.layoutChain + .centerX() + .width(240) + .height(50) + .bottom(kSafeBottomMargin + 10) + + tipsLab.layoutChain + .edgesHorzontal(38) + .bottomToTopOfView(doneBtn, offset: -20) + } + + lazy var titleLab: UILabel = { + let label = UILabel() + label.text = "我们让您的圈子知道您创建了一个带有以下消息的气泡" + label.font = .systemFont(ofSize: 20, weight: .bold) + label.textColor = ThemeManager.shared.color.titleAuxColor + label.numberOfLines = 0 + 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 tipsLab: UILabel = { + let label = UILabel() + label.text = "您的圈子成员只会看到您在气泡中。当您在气泡中,将不会共享您的确切位置。" + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = ThemeManager.shared.color.titleAuxColor + label.numberOfLines = 0 + return label + }() + + lazy var doneBtn: 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 + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + self.isHidden = true + backgroundColor = .white + setupUI() + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/QuickLocation/Section/Home/Bubble/CreateBubbleVC.swift b/QuickLocation/Section/Home/Bubble/CreateBubbleVC.swift new file mode 100644 index 0000000..6d8f7b4 --- /dev/null +++ b/QuickLocation/Section/Home/Bubble/CreateBubbleVC.swift @@ -0,0 +1,48 @@ +// +// CreateBubbleVC.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa + +class CreateBubbleVC: BaseViewController { + + fileprivate var rootView: CreateBubbleView! + + override func loadView() { + rootView = CreateBubbleView(frame: UIScreen.main.bounds) + view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + + rootView.createBubbleTipsView.doneBtn.rx.tap.subscribe(onNext: { [weak self] in + guard let self = self else { return } + if AppContextManager.shared.vip > 1 { + let hours = self.rootView.createBubbleTiemView.selectedHour.value + requestSetBubble(enable: true, keep_time: hours) + } + else { + CreateBubblePopView.show() + } + }).disposed(by: disposeBag) + } + + private func requestSetBubble(enable: Bool, keep_time: Int) { + DLToast.showLoading() + UserService.setBubble(enable: enable, keep_time: keep_time).subscribe(onNext: { response in + DLToast.dismiss() + self.rootView.navTitleLabel.text = "活动气泡" + self.rootView.createBubbleDoneView.messageLab.text = self.rootView.createBubbleTipsView.messageText + let endDate = Calendar.current.date(byAdding: .hour, value: keep_time, to: Date()) ?? Date() + self.rootView.createBubbleDoneView.startCountdown(endDate: endDate) + self.rootView.createBubbleTipsView.isHidden = true + self.rootView.createBubbleDoneView.isHidden = false + }).disposed(by: disposeBag) + } +} diff --git a/QuickLocation/Section/Home/Bubble/CreateBubbleView.swift b/QuickLocation/Section/Home/Bubble/CreateBubbleView.swift new file mode 100644 index 0000000..f5c9e6c --- /dev/null +++ b/QuickLocation/Section/Home/Bubble/CreateBubbleView.swift @@ -0,0 +1,127 @@ +// +// CreateBubbleView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa + +class CreateBubbleView: UIView { + + var disposeBag = DisposeBag() + + var createBubbleTiemView: CreateBubbleTiemView = CreateBubbleTiemView() + var createBubbleTipsView: CreateBubbleTipsView = CreateBubbleTipsView() + var createBubbleDoneView: CreateBubbleDoneView = CreateBubbleDoneView() + + private func setupRx() { + backBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.shared.popOrDismiss() + }).disposed(by: disposeBag) + + createBubbleTiemView.onNextTap = { [weak self] in + guard let self = self else { return } + self.navTitleLabel.text = "通知您的圈子" + let hours = self.createBubbleTiemView.selectedHour.value + let endDate = Calendar.current.date(byAdding: .hour, value: hours, to: Date()) ?? Date() + let fmt = DateFormatter() + fmt.dateFormat = "HH:mm" + let timeStr = fmt.string(from: endDate) + 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(navBarView) + navBarView.addSubview(navTitleLabel) + navBarView.addSubview(backBtn) + + navBgView.layoutChain + .edges(excludingEdge: .bottom) + .heightToWidth(160/375) + + navBarView.layoutChain + .edges(excludingEdge: .bottom) + .height(kNaviHeight) + + navTitleLabel.layoutChain + .top(kStatusBarHeight + 12) + .centerY(backBtn) + .centerX() + + backBtn.layoutChain + .centerY(navTitleLabel) + .left(15) + .width(24) + .height(24) + + createBubbleTiemView.layoutChain + .topToBottomOfView(navBarView) + .edges(excludingEdge: .top) + + createBubbleTipsView.layoutChain + .topToBottomOfView(navBarView) + .edges(excludingEdge: .top) + + createBubbleDoneView.layoutChain + .topToBottomOfView(navBarView) + .edges(excludingEdge: .top) + } + + lazy var navBgView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage(named: "Common/navBar_bg_2") + iv.contentMode = .scaleAspectFill + return iv + }() + + lazy var navBarView: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + lazy var navTitleLabel: UILabel = { + let label = UILabel() + label.text = "设置气泡时间" + label.font = .systemFont(ofSize: 18, weight: .medium) + label.textColor = ThemeManager.shared.color.titleAuxColor + label.textAlignment = .center + return label + }() + + 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 + }() + + + override init(frame: CGRect) { + super.init(frame: .zero) + backgroundColor = .white + setupUI() + setupRx() + createBubbleTiemView.isHidden = false + createBubbleTipsView.isHidden = true + createBubbleDoneView.isHidden = true + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index ce62059..001e231 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -127,6 +127,11 @@ class HomeViewController: BaseViewController { // MARK: - Actions private func reactiveAction() { + // 气泡 + rootView.bubbleView.rx.tapGesture.subscribe { _ in + AppRouter.push(Route.createBubble) + }.disposed(by: disposeBag) + // 签到 rootView.signInView.rx.tapGesture.subscribe { _ in let vc = SignInVC(lastLocation: self.lastLocation) diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift index 2a67e88..a68ec01 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift @@ -26,7 +26,9 @@ class CreateSchedulePopView: UIView { tagListView.tagViews.forEach { $0.layer.cornerRadius = 4 } - tagListView.invalidateIntrinsicContentSize() // 通知系统重新算高 + // 强制布局使 TagListView 宽度正确 → 重新排列标签 → intrinsicContentSize 正确 + tagListView.setNeedsLayout() + tagListView.layoutIfNeeded() } // MARK: - UI diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift index 0545f5c..d790f02 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift @@ -359,13 +359,8 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { } // 缩放至包含所有点 - let lats = validPoints.map { $0.latitude } - let lons = validPoints.map { $0.longitude } - if let minLat = lats.min(), let maxLat = lats.max(), - let minLon = lons.min(), let maxLon = lons.max() { - let center = CLLocationCoordinate2D(latitude: (minLat + maxLat) / 2, longitude: (minLon + maxLon) / 2) - let span = MACoordinateSpan(latitudeDelta: (maxLat - minLat) * 2.5 + 0.01, longitudeDelta: (maxLon - minLon) * 2.5 + 0.01) - rootView.mapView.setRegion(MACoordinateRegion(center: center, span: span), animated: true) + if !pointAnnotations.isEmpty { + rootView.mapView.showAnnotations(pointAnnotations, animated: true) } #endif } diff --git a/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailVC.swift b/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailVC.swift new file mode 100644 index 0000000..ddbaf6e --- /dev/null +++ b/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailVC.swift @@ -0,0 +1,294 @@ +// +// ItineraryDetailVC.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa +import ObjectMapper +import SwiftyUserDefaults +#if !targetEnvironment(simulator) +import AMapNaviKit +import AMapSearchKit +#endif + +class ItineraryDetailVC: BaseViewController { + + fileprivate var rootView: ItineraryDetailView! + private var points: [SchedulePointModel] = [] + private let routeSearch = AMapSearchAPI() + private var routeOverlays: [MAPolyline] = [] + private var pointAnnotations: [MAPointAnnotation] = [] + + init(scheduleJson: [String: Any]) { + let model = ScheduleModel(JSON: scheduleJson) + self.points = model?.points ?? [] + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + rootView = ItineraryDetailView(frame: UIScreen.main.bounds) + view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + setupMap() + addPointAnnotations() + requestRoute() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + if isMovingFromParent || isBeingDismissed { + rootView.cleanupMap() + } + } + + // MARK: - Map + + private func setupMap() { + #if !targetEnvironment(simulator) + rootView.mapView.delegate = self + rootView.mapView.showsUserLocation = false + routeSearch?.delegate = self + if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] { + let coord = CLLocationCoordinate2D(latitude: lat, longitude: lon) + if CLLocationCoordinate2DIsValid(coord) { + rootView.mapView.setCenter(coord, animated: false) + rootView.mapView.setZoomLevel(14, animated: false) + } + } + #endif + } + + private func addPointAnnotations() { + #if !targetEnvironment(simulator) + for ann in pointAnnotations { rootView.mapView.removeAnnotation(ann) } + pointAnnotations.removeAll() + + // lat/lon 都为 0 的视为无效坐标,跳过 + let validPoints = points.filter { + guard let lat = $0.latitude, let lon = $0.longitude else { return false } + return abs(lat) > 0.0001 && abs(lon) > 0.0001 + } + for (i, p) in validPoints.enumerated() { + let ann = MAPointAnnotation() + ann.coordinate = CLLocationCoordinate2D(latitude: p.latitude!, longitude: p.longitude!) + ann.title = "\(i + 1)" + let timeStr = view.getDateInterval2String(date: "\(p.expected_timestamp / 1000)", dateFormat: "yyyy-MM-dd HH:mm") + ann.subtitle = "\(p.street)|\(timeStr)" + rootView.mapView.addAnnotation(ann) + pointAnnotations.append(ann) + } + + // 缩放到包含所有标注点 + if !pointAnnotations.isEmpty { + rootView.mapView.showAnnotations(pointAnnotations, animated: true) + } + #endif + } + + private func requestRoute() { + #if !targetEnvironment(simulator) + let validPoints = points.filter { ($0.latitude ?? 0) != 0 || ($0.longitude ?? 0) != 0 } + guard validPoints.count >= 2 else { return } + + let request = AMapDrivingRouteSearchRequest() + request.origin = AMapGeoPoint.location(withLatitude: CGFloat(validPoints[0].latitude ?? 0), + longitude: CGFloat(validPoints[0].longitude ?? 0)) + request.destination = AMapGeoPoint.location(withLatitude: CGFloat(validPoints.last?.latitude ?? 0), + longitude: CGFloat(validPoints.last?.longitude ?? 0)) + if validPoints.count > 2 { + var waypoints: [AMapGeoPoint] = [] + for i in 1.. UIImage? { + let size = CGSize(width: 28, height: 28) + let rect = CGRect(origin: .zero, size: size) + UIGraphicsBeginImageContextWithOptions(size, false, 0) + guard let ctx = UIGraphicsGetCurrentContext() else { return nil } + ctx.setLineWidth(1) + ctx.setStrokeColor(UIColor.white.cgColor) + ctx.setFillColor(UIColor(hexStr: "#16B3FF").cgColor) + let path = UIBezierPath(ovalIn: rect) + path.fill() + path.stroke() + let text = "\(num)" as NSString + let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 13), .foregroundColor: UIColor.white] + let strSize = text.size(withAttributes: attrs) + text.draw(at: CGPoint(x: (size.width - strSize.width) / 2, y: (size.height - strSize.height) / 2)) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return img + } + + /// 生成 callout 背景图 + private static func calloutImage() -> UIImage? { + let size = CGSize(width: 200, height: 50) + UIGraphicsBeginImageContextWithOptions(size, false, 0) + guard let ctx = UIGraphicsGetCurrentContext() else { return nil } + ctx.setFillColor(UIColor.white.cgColor) + let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8) + path.fill() + ctx.setStrokeColor(UIColor(hexStr: "#16B3FF").cgColor) + ctx.setLineWidth(1) + path.stroke() + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return img + } +} + +#if !targetEnvironment(simulator) +// MARK: - MAMapViewDelegate +extension ItineraryDetailVC: MAMapViewDelegate { + func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { + guard !(annotation is MAUserLocation) else { return nil } + guard let pointAnn = annotation as? MAPointAnnotation else { return nil } + + // 行程点标注 + if let num = Int(pointAnn.title ?? "") { + let id = "ItineraryPin" + var view = mapView.dequeueReusableAnnotationView(withIdentifier: id) + if view == nil { + view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) + } else { + view?.annotation = annotation + } + view?.image = Self.numberImage(num) + view?.centerOffset = CGPoint(x: 0, y: -14) + + // callout + let callout = CalloutView(frame: CGRect(x: 0, y: 0, width: 200, height: 50)) + if let subtitle = pointAnn.subtitle { + let parts = subtitle.components(separatedBy: "|") + if parts.count == 2 { + callout.nameLab.text = parts[0] + callout.timeLab.text = parts[1] + } + } + view?.customCalloutView = callout + return view + } + return nil + } + + func mapView(_ mapView: MAMapView!, didSelect view: MAAnnotationView!) { + // 显示自定义 callout + if let customCallout = view.customCalloutView { + view.addSubview(customCallout) + customCallout.frame = CGRect(x: (view.bounds.width - 200) / 2, y: -55, width: 200, height: 50) + } + } + + func mapView(_ mapView: MAMapView!, didDeselect view: MAAnnotationView!) { + view.customCalloutView?.removeFromSuperview() + } +} + +// MARK: - AMapSearchDelegate +extension ItineraryDetailVC: AMapSearchDelegate { + func onRouteSearchDone(_ request: AMapRouteSearchBaseRequest!, response: AMapRouteSearchResponse!) { + guard let path = response.route?.paths?.first as? AMapPath else { return } + var coords: [CLLocationCoordinate2D] = [] + for step in path.steps { + guard let polylineStr = step.polyline else { continue } + for point in polylineStr.components(separatedBy: ";") { + let latLon = point.components(separatedBy: ",") + if latLon.count == 2, let lon = Double(latLon[0]), let lat = Double(latLon[1]) { + coords.append(CLLocationCoordinate2D(latitude: lat, longitude: lon)) + } + } + } + guard coords.count > 1 else { return } + var mutableCoords = coords + if let polyline = MAPolyline(coordinates: &mutableCoords, count: UInt(coords.count)) { + rootView.mapView.add(polyline) + routeOverlays.append(polyline) + } + } + + func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) { + print("Route search error: \(error.localizedDescription)") + } + + func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! { + if let polyline = overlay as? MAPolyline { + let r = MAPolylineRenderer(polyline: polyline) + r?.strokeColor = UIColor(hexStr: "#16B3FF") + r?.lineWidth = 4 + r?.lineDashType = kMALineDashTypeSquare + return r + } + return nil + } +} + +// MARK: - MAAnnotationView + customCalloutView +private var calloutViewKey: UInt8 = 0 +extension MAAnnotationView { + var customCalloutView: UIView? { + get { objc_getAssociatedObject(self, &calloutViewKey) as? UIView } + set { objc_setAssociatedObject(self, &calloutViewKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } +} + +// MARK: - CalloutView +class CalloutView: UIView { + let nameLab: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 13, weight: .medium) + l.textColor = UIColor(hexStr: "#333333") + l.textAlignment = .center + return l + }() + + let timeLab: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 11, weight: .regular) + l.textColor = UIColor(hexStr: "#999999") + l.textAlignment = .center + return l + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + layer.cornerRadius = 8 + layer.shadowColor = UIColor.black.withAlphaComponent(0.15).cgColor + layer.shadowOffset = .zero + layer.shadowRadius = 4 + layer.shadowOpacity = 1 + + addSubview(nameLab) + addSubview(timeLab) + nameLab.layoutChain.top(8).centerX().edgesHorzontal(10) + timeLab.layoutChain.topToBottomOfView(nameLab, offset: 4).centerX().edgesHorzontal(10).bottom(8) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +#endif diff --git a/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailView.swift b/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailView.swift new file mode 100644 index 0000000..2fc750b --- /dev/null +++ b/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailView.swift @@ -0,0 +1,107 @@ +// +// ItineraryDetailView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa +import AMapNaviKit + +class ItineraryDetailView: UIView { + + var disposeBag = DisposeBag() + + private func setupRx() { + backBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.shared.popOrDismiss() + }).disposed(by: disposeBag) + } + + private func setupUI() { + addSubview(mapView) + addSubview(navBgView) + addSubview(navBarView) + navBarView.addSubview(navTitleLabel) + navBarView.addSubview(backBtn) + + navBgView.layoutChain + .edges(excludingEdge: .bottom) + .heightToWidth(160/375) + + navBarView.layoutChain + .edges(excludingEdge: .bottom) + .height(kNaviHeight) + + navTitleLabel.layoutChain + .top(kStatusBarHeight + 12) + .centerY(backBtn) + .centerX() + + backBtn.layoutChain + .centerY(navTitleLabel) + .left(15) + .width(24) + .height(24) + + mapView.layoutChain + .edges() + } + + lazy var navBgView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage(named: "Common/navBar_bg_2") + iv.contentMode = .scaleAspectFill + return iv + }() + + lazy var navBarView: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + lazy var navTitleLabel: UILabel = { + let label = UILabel() + label.text = "行程路线" + label.font = .systemFont(ofSize: 18, weight: .medium) + label.textColor = ThemeManager.shared.color.titleAuxColor + label.textAlignment = .center + return label + }() + + 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 mapView: MAMapView! = { + let mv = MAMapView() + mv.zoomLevel = 14 + mv.showsUserLocation = false + mv.showsCompass = false + mv.userTrackingMode = .none + return mv + }() + + func cleanupMap() { + mapView?.delegate = nil + mapView?.removeFromSuperview() + mapView = nil + } + + override init(frame: CGRect) { + super.init(frame: .zero) + backgroundColor = .white + setupUI() + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift index c644123..d05826b 100644 --- a/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift +++ b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift @@ -28,11 +28,38 @@ class ScheduleDetailVC: BaseViewController { // Do any additional setup after loading the view. setupData() bindViewModel() + reactiveAction() requestFollowList() viewModel.loadPointData() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + rootView.vipTipsLab.text = AppContextManager.shared.vip > 1 ? "" : "升级 VIP,可查看具体人员与节点" + } + + private func reactiveAction() { + rootView.operateBtn.rx.tap.subscribe(onNext: { _ in + guard let model = self.viewModel.scheduModel else { return } + if model.is_own { + AppRouter.push(Route.createSchedule, userInfo: ["scheduleJson": model.toJSON()]) + } + else { + self.requestSetFollow(id: model.id, op: self.rootView.operateBtn.isSelected) + } + }).disposed(by: disposeBag) + + rootView.routeBtn.rx.tap.subscribe(onNext: { _ in + guard let model = self.viewModel.scheduModel else { return } + AppRouter.push(Route.itineraryDetail, userInfo: ["scheduleJson": model.toJSON()]) + }).disposed(by: disposeBag) + + rootView.vipTipsLab.rx.tapGesture.subscribe(onNext: { _ in + AppRouter.push(Route.vipRecharge) + }).disposed(by: disposeBag) + } + private func bindViewModel() { viewModel.output.sectionedItems .bind(to: rootView.collectionView.rx.items(dataSource: dataSource)) @@ -47,18 +74,37 @@ class ScheduleDetailVC: BaseViewController { guard let model = viewModel.scheduModel else { return } rootView.dateLab.text = rootView.dateLab.getDateInterval2String(date: "\(model.timestamp / 1000)", dateFormat: "yyyy年MM月dd日") rootView.creatorIcon.image = model.userIcon + + var operateName = "" + if model.is_own { + operateName = "编辑行程" + } + else { + operateName = "关注行程" + rootView.operateBtn.isSelected = model.is_follow + } + rootView.operateBtn.setTitle(operateName, for: .normal) } // MARK: - API private func requestFollowList() { - dl.showLoading() - UserService.followList().subscribe(onNext: { response in - self.dl.dismiss() + DLToast.showLoading() + ItineraryService.queryFollowList(id: viewModel.routeId).subscribe(onNext: { response in + DLToast.dismiss() self.viewModel.loadViewedData(response.list) self.rootView.noDataLab.isHidden = response.list.count > 0 }, onError: { _ in }).disposed(by: disposeBag) } + private func requestSetFollow(id: String, op: Bool) { + DLToast.showLoading() + ItineraryService.follow(id: id, op: op ? 2 : 1).subscribe(onNext: { response in + DLToast.dismiss() + DLToast.show(text: op ? "已取消" : "已关注此行程, \n行程路线将自动添加到您的行程列表。") + self.rootView.operateBtn.isSelected = !op + }, onError: { _ in }).disposed(by: disposeBag) + } + // MARK: - dataSource private lazy var dataSource: RxCollectionViewSectionedReloadDataSource = { RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, model in diff --git a/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailView.swift b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailView.swift index b0555e7..e621f4d 100644 --- a/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailView.swift +++ b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailView.swift @@ -254,11 +254,54 @@ class ScheduleDetailView: UIView { v.layer.shadowOffset = CGSize(width: 0, height: -2) v.layer.shadowRadius = 10 v.layer.shadowOpacity = 1 + + forwardBtn.isHidden = true + v.addSubview(forwardBtn) + v.addSubview(operateBtn) + v.addSubview(routeBtn) + + forwardBtn.layoutChain + .centerY() + .left(15) + .width(80) + + routeBtn.layoutChain + .centerY() + .right(15) + .width(80) + + operateBtn.layoutChain + .centerY() + .leftToRightOfView(forwardBtn, offset: 20) + .rightToLeftOfView(routeBtn, offset: -20) + .height(50) + return v }() + + lazy var forwardBtn: UIButton = makeVerticalButton(image: UIImage(named: "Schedule/forward"), title: "发送到圈子") + lazy var routeBtn: UIButton = makeVerticalButton(image: UIImage(named: "Schedule/route"), title: "查看路线") + + /// 创建图片在上、文字在下的按钮 + private func makeVerticalButton(image: UIImage?, title: String) -> UIButton { + let btn = UIButton(type: .custom) + btn.setImage(image, for: .normal) + btn.setTitle(title, for: .normal) + btn.setTitleColor(UIColor(hexStr: "#333333"), for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) + btn.titleLabel?.textAlignment = .center + btn.imageView?.contentMode = .scaleAspectFit + // 图片在上、文字在下 + let width = btn.titleLabel?.intrinsicContentSize.width ?? 0 + btn.imageEdgeInsets = UIEdgeInsets(top: -20, left: 0, bottom: 0, right: -width) + btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: -24, bottom: -24, right: 0) + btn.contentEdgeInsets = UIEdgeInsets(top: 24, left: 0, bottom: 24, right: 0) + return btn + } lazy var operateBtn: UIButton = { let btn = UIButton() + btn.setTitle("取消关注", for: .selected) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) @@ -271,8 +314,6 @@ class ScheduleDetailView: UIView { backgroundColor = .white setupUI() setupRx() - - vipTipsLab.text = AppContextManager.shared.vip > 1 ? "" : "升级 VIP,查看具体事件" } required init?(coder aDecoder: NSCoder) { diff --git a/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryVC.swift b/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryVC.swift new file mode 100644 index 0000000..6dc92d6 --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryVC.swift @@ -0,0 +1,69 @@ +// +// ScheduleHistoryVC.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa +import RxDataSources +import MJRefresh + +class ScheduleHistoryVC: BaseViewController { + + fileprivate var rootView: ScheduleHistoryView! + + override func loadView() { + rootView = ScheduleHistoryView(frame: UIScreen.main.bounds) + view = rootView + } + + private var viewModel = ScheduleHistoryViewModel() + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + bindViewModel() + requestData() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + private func requestData() { + dl.showLoading() + viewModel.refresh() + } + + private func bindViewModel() { + viewModel.output.sectionedItems + .bind(to: rootView.tableView.rx.items(dataSource: tableViewDataSource)) + .disposed(by: disposeBag) + + viewModel.output.refreshResult.subscribe(onNext: { [weak self] (status, isEmpty) in + guard let self = self else { return } + self.dl.dismiss() + self.rootView.tableView.refresh(status: status, isEmpty: isEmpty) + }).disposed(by: disposeBag) + + rootView.tableView.rx.modelSelected(ScheduleModel.self) + .subscribe(viewModel.cellAction.inputs) + .disposed(by: disposeBag) + } + + // MARK: - dataSource + lazy private var tableViewDataSource: RxTableViewSectionedReloadDataSource = { + return RxTableViewSectionedReloadDataSource( + configureCell: { (_, tableView, indexPath, model) in + let cell: ScheduleHistoryListCell = tableView.dequeueReusableCell(for: indexPath) + cell.configure(model) + return cell + } + ) + }() + +} diff --git a/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryVM.swift b/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryVM.swift new file mode 100644 index 0000000..4845e05 --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryVM.swift @@ -0,0 +1,61 @@ +// +// ScheduleHistoryVM.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import RxSwift +import RxCocoa +import RxDataSources +import MJRefresh +import ObjectMapper + +class ScheduleHistoryViewModel: ViewModelType { + + struct Input {} + + struct Output { + var sectionedItems: Observable<[ScheduleListSectionModel]> + var refreshResult: Observable + var pagination: Observable + var error: Observable + } + + let input: Input + let output: Output + + private var listService: ListService + private let sectionedItems = PublishSubject<[ScheduleListSectionModel]>() + + var refresh: MJRefreshComponentAction { + return { + self.listService.request.onNext(.refresh) + } + } + var loadMore: MJRefreshComponentAction { + return { + self.listService.request.onNext(.more) + } + } + + lazy var cellAction: Action = { this in + return Action { model in + AppRouter.push(Route.itineraryDetail, userInfo: ["scheduleJson": model.toJSON()]) + return .empty() + } + }(self) + + // MARK: - init + init() { + listService = ItineraryService.query(history: true) + + input = Input() + output = Output( + sectionedItems: listService.animatableSectionedItems, + refreshResult: listService.refreshResult, + pagination: listService.pagination, + error: listService.error + ) + } +} diff --git a/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryView.swift b/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryView.swift new file mode 100644 index 0000000..46b90fb --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleHistory/ScheduleHistoryView.swift @@ -0,0 +1,241 @@ +// +// ScheduleHistoryView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa +import TagListView + +class ScheduleHistoryView: UIView { + + var disposeBag = DisposeBag() + + private func setupRx() { + backBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.shared.popOrDismiss() + }).disposed(by: disposeBag) + } + + private func setupUI() { + addSubview(navBgView) + addSubview(navBarView) + navBarView.addSubview(navTitleLabel) + navBarView.addSubview(backBtn) + addSubview(tableView) + + navBgView.layoutChain + .edges(excludingEdge: .bottom) + .heightToWidth(160/375) + + navBarView.layoutChain + .edges(excludingEdge: .bottom) + .height(kNaviHeight) + + navTitleLabel.layoutChain + .top(kStatusBarHeight + 12) + .centerY(backBtn) + .centerX() + + backBtn.layoutChain + .centerY(navTitleLabel) + .left(15) + .width(24) + .height(24) + + tableView.layoutChain + .topToBottomOfView(navBarView, offset: 15) + .edges(excludingEdge: .top) + + } + + lazy var navBgView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage(named: "Common/navBar_bg_2") + iv.contentMode = .scaleAspectFill + return iv + }() + + lazy var navBarView: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + lazy var navTitleLabel: UILabel = { + let label = UILabel() + label.text = "历史行程" + label.font = .systemFont(ofSize: 18, weight: .medium) + label.textColor = ThemeManager.shared.color.titleAuxColor + label.textAlignment = .center + return label + }() + + 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 + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + backgroundColor = .white + setupUI() + setupRx() + } + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.estimatedRowHeight = 80 + tableView.rowHeight = UITableView.automaticDimension + tableView.showsVerticalScrollIndicator = false + tableView.register(ScheduleHistoryListCell.self) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin + 20, right: 0) + return tableView + }() + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - ScheduleHistoryListCell +class ScheduleHistoryListCell: UITableViewCell { + + var disposeBag = DisposeBag() + + func configure(_ model: ScheduleModel) { + nameLab.text = model.nick_name + " 的行程路线" + monthLab.text = getDateInterval2String(date: "\(model.timestamp/1000)", dateFormat: "MM月") + + let groupNames = model.groups.map { $0.group_name } + tagListView.removeAllTags() + tagListView.addTags(groupNames) + tagListView.tagViews.forEach { + $0.layer.cornerRadius = 2 + } + tagListView.invalidateIntrinsicContentSize() + + guard let pointModel = model.points.last else { return } + dateLab.text = getDateInterval2String(date: "\(pointModel.expected_timestamp/1000)", dateFormat: "MM.dd") + } + + override init(style: CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + setupSubviews() + } + + private func setupSubviews() { + contentView.addSubview(bgView) + bgView.addSubview(cornerView) + cornerView.addSubview(nameLab) + cornerView.addSubview(monthLab) + cornerView.addSubview(dateLab) + cornerView.addSubview(tagListView) + + bgView.layoutChain + .top(15).bottom(15) + .edgesHorzontal(15) + + cornerView.layoutChain.edges() + + monthLab.layoutChain + .top(15) + .left(11) + .width(35) + + dateLab.layoutChain + .topToBottomOfView(monthLab, offset: 4) + .leftToView(monthLab) + .width(35) + + nameLab.layoutChain + .top(15) + .leftToRightOfView(monthLab, offset: 15) + + tagListView.layoutChain + .leftToRightOfView(dateLab, offset: 15) + .bottom(15) + .right(15) + + nameLab.layoutChain.bottomToTopOfView(tagListView, offset:-5) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } + + lazy var bgView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.05).cgColor + view.layer.shadowOffset = CGSize(width: 0, height: 0) + view.layer.shadowOpacity = 1 + view.layer.shadowRadius = 8 + return view + }() + + lazy var cornerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.cornerRadius = 10 + return view + }() + + lazy var monthLab: UILabel = { + let label = UILabel() + label.textColor = ThemeManager.shared.color.titleAuxColor + label.font = .systemFont(ofSize: 14, weight: .medium) + return label + }() + + lazy var dateLab: UILabel = { + let label = UILabel() + label.textColor = ThemeManager.shared.color.subTitleColor + label.font = .systemFont(ofSize: 12, weight: .regular) + return label + }() + + lazy var nameLab: UILabel = { + let label = UILabel() + label.textColor = ThemeManager.shared.color.titleAuxColor + label.font = .systemFont(ofSize: 14, weight: .medium) + return label + }() + + lazy var tagListView: TagListView = { + let view = TagListView() + view.textFont = UIFont.systemFont(ofSize: 8, weight: .regular) + view.textColor = UIColor(hexStr: "#B78C56") + view.tagBackgroundColor = UIColor(hexStr: "#FFF3E4") + view.paddingX = 6 // 水平内边距 + view.paddingY = 6 // 垂直内边距 + view.alignment = .left // 对齐 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() +} diff --git a/QuickLocation/Section/Schedule/ScheduleModel.swift b/QuickLocation/Section/Schedule/ScheduleModel.swift index b2a1c3b..e6edfad 100644 --- a/QuickLocation/Section/Schedule/ScheduleModel.swift +++ b/QuickLocation/Section/Schedule/ScheduleModel.swift @@ -65,7 +65,7 @@ struct ScheduleModel: Mappable, Equatable { /// 行程点 var points: [SchedulePointModel] = [] /// 圈子 - var groups: [ScheduleGroupModel] = [] + var groups: [GroupCommonModel] = [] init?(map: Map) { @@ -205,7 +205,7 @@ extension SchedulePointEventModel: IdentifiableType { } /// 行程分享的圈子 -struct ScheduleGroupModel: Mappable, Equatable { +struct GroupCommonModel: Mappable, Equatable { var uuid: String = UUID().uuidString /// id var group_key: String = "" @@ -222,7 +222,7 @@ struct ScheduleGroupModel: Mappable, Equatable { } } -extension ScheduleGroupModel: IdentifiableType { +extension GroupCommonModel: IdentifiableType { public typealias Identity = String public var identity: String { diff --git a/QuickLocation/Section/Schedule/ScheduleVC.swift b/QuickLocation/Section/Schedule/ScheduleVC.swift index 623813e..867f6fe 100644 --- a/QuickLocation/Section/Schedule/ScheduleVC.swift +++ b/QuickLocation/Section/Schedule/ScheduleVC.swift @@ -27,15 +27,16 @@ class ScheduleVC: BaseViewController { super.viewDidLoad() // Do any additional setup after loading the view. + rootView.allCheckBtn.isSelected = true setupTableViewHeaderFooter() bindViewModel() reactiveAction() - - requestFollowList() + } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + requestFollowList() requestData() } @@ -54,13 +55,42 @@ class ScheduleVC: BaseViewController { self.rootView.tableView.refresh(status: status, isEmpty: isEmpty) }).disposed(by: disposeBag) + rootView.collectionView.rx.modelSelected(ViewedModel.self) + .subscribe(viewModel.viewedCellAction.inputs) + .disposed(by: disposeBag) + rootView.tableView.rx.modelSelected(ScheduleModel.self) .subscribe(viewModel.cellAction.inputs) .disposed(by: disposeBag) } private func reactiveAction() { - + // 全部 + rootView.allCheckBtn.rx.tap.subscribe(onNext: { [weak self] _ in + guard let self = self, !self.rootView.allCheckBtn.isSelected else { return } + self.rootView.allCheckBtn.isSelected = true + self.rootView.followedCheckBtn.isSelected = false + self.rootView.mineCheckBtn.isSelected = false + self.changeCondition(follow: false, own: false) + }).disposed(by: disposeBag) + + // 我关注的 + rootView.followedCheckBtn.rx.tap.subscribe(onNext: { [weak self] _ in + guard let self = self, !self.rootView.followedCheckBtn.isSelected else { return } + self.rootView.allCheckBtn.isSelected = false + self.rootView.followedCheckBtn.isSelected = true + self.rootView.mineCheckBtn.isSelected = false + self.changeCondition(follow: true, own: false) + }).disposed(by: disposeBag) + + // 我的 + rootView.mineCheckBtn.rx.tap.subscribe(onNext: { [weak self] _ in + guard let self = self, !self.rootView.mineCheckBtn.isSelected else { return } + self.rootView.allCheckBtn.isSelected = false + self.rootView.followedCheckBtn.isSelected = false + self.rootView.mineCheckBtn.isSelected = true + self.changeCondition(follow: false, own: true) + }).disposed(by: disposeBag) } private func setupTableViewHeaderFooter() { @@ -79,19 +109,33 @@ class ScheduleVC: BaseViewController { // MARK: - API private func requestFollowList() { - dl.showLoading() + DLToast.showLoading() UserService.followList().subscribe(onNext: { response in - self.dl.dismiss() + DLToast.dismiss() self.viewModel.loadViewedData(response.list) - }, onError: { _ in }).disposed(by: disposeBag) + }).disposed(by: disposeBag) + } + + private func requestSetFollow(id: String, op: Bool) { + DLToast.showLoading() + ItineraryService.follow(id: id, op: op ? 2 : 1).subscribe(onNext: { response in + DLToast.dismiss() + DLToast.show(text: op ? "已取消" : "已关注此行程, \n行程路线将自动添加到您的行程列表。") + self.requestData() + }).disposed(by: disposeBag) } // MARK: - API 行程列表 private func requestData() { - dl.showLoading() viewModel.refresh() } + // 更换查询条件 + private func changeCondition(follow: Bool, own: Bool) { + viewModel.changeCondition(follow: follow, own: own) + requestData() + } + // MARK: - dataSource private lazy var dataSource: RxCollectionViewSectionedReloadDataSource = { RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, model in @@ -115,7 +159,7 @@ class ScheduleVC: BaseViewController { // 关注 cell.followBtn.rx.tap.subscribe(onNext: { _ in - + self.requestSetFollow(id: model.id, op: cell.followBtn.isSelected) }).disposed(by: cell.disposeBag) return cell diff --git a/QuickLocation/Section/Schedule/ScheduleView.swift b/QuickLocation/Section/Schedule/ScheduleView.swift index f616127..27ad4c7 100644 --- a/QuickLocation/Section/Schedule/ScheduleView.swift +++ b/QuickLocation/Section/Schedule/ScheduleView.swift @@ -8,6 +8,7 @@ import UIKit import RxSwift import RxCocoa +import TagListView class ScheduleView: UIView { @@ -112,6 +113,9 @@ class ScheduleView: UIView { historyBtn.setTitle("历史行程", for: .normal) historyBtn.setTitleColor(ThemeManager.shared.color.titleAuxColor, for: .normal) historyBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) + historyBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.push(Route.scheduleHistory) + }).disposed(by: disposeBag) view.addSubview(historyBtn) historyBtn.layoutChain .right(15) @@ -145,7 +149,7 @@ class ScheduleView: UIView { lazy var travelRouteView: UIView = { let view = UIView() view.backgroundColor = .clear - + let titleLab = UILabel() titleLab.text = "行程路线" titleLab.font = .systemFont(ofSize: 16, weight: .medium) @@ -154,7 +158,7 @@ class ScheduleView: UIView { titleLab.layoutChain .top(15) .edgesVertical(5) - + let dotView = UIView() dotView.backgroundColor = UIColor(hexStr: "#16B3FF") dotView.cornerRadius = 2 @@ -164,17 +168,48 @@ class ScheduleView: UIView { .centerY(titleLab) .width(4) .height(11) - + titleLab.layoutChain.leftToRightOfView(dotView, offset: 5) - + + // 筛选 checkbox(从右到左) + view.addSubview(allCheckBtn) + view.addSubview(followedCheckBtn) + view.addSubview(mineCheckBtn) + + allCheckBtn.layoutChain + .right(15) + .centerY(titleLab) + followedCheckBtn.layoutChain + .rightToLeftOfView(allCheckBtn, offset: -20) + .centerY(titleLab) + mineCheckBtn.layoutChain + .rightToLeftOfView(followedCheckBtn, offset: -20) + .centerY(titleLab) + return view }() + + lazy var allCheckBtn: UIButton = makeCheckButton(title: "全部") + lazy var followedCheckBtn: UIButton = makeCheckButton(title: "我关注的") + lazy var mineCheckBtn: UIButton = makeCheckButton(title: "我的") + + private func makeCheckButton(title: String) -> UIButton { + let btn = UIButton(type: .custom) + btn.setImage(UIImage(named: "Schedule/checkbox"), for: .normal) + btn.setImage(UIImage(named: "Schedule/checkbox_selected"), for: .selected) + btn.setTitle(" \(title)", for: .normal) + btn.setTitleColor(UIColor(hexStr: "#333333"), for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) + btn.sizeToFit() + return btn + } lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.backgroundColor = .clear tableView.separatorStyle = .none tableView.estimatedRowHeight = 80 + tableView.rowHeight = UITableView.automaticDimension tableView.showsVerticalScrollIndicator = false tableView.register(ScheduleListPopCell.self) tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) @@ -283,18 +318,25 @@ class ScheduleListPopCell: UITableViewCell { func configure(_ model: ScheduleModel) { nameLab.text = model.nick_name + " 的行程路线" monthLab.text = getDateInterval2String(date: "\(model.timestamp/1000)", dateFormat: "MM月") + editBtn.isHidden = !model.is_own + followBtn.isHidden = model.is_own + followBtn.isSelected = model.is_follow ? true : false - if model.is_own { - editBtn.isHidden = false - followBtn.isHidden = true + let groupNames = model.groups.map { $0.group_name } + // addTags 内部立即用 frame.width 排标签,先给个预估宽度避免算错 + if !groupNames.isEmpty { + let w = contentView.frame.width > 0 ? contentView.frame.width : UIScreen.main.bounds.width + tagListView.frame.size.width = w - 170 } - else { - editBtn.isHidden = true - followBtn.isHidden = model.is_follow ? true : false + tagListView.removeAllTags() + tagListView.addTags(groupNames) + tagListView.tagViews.forEach { + $0.layer.cornerRadius = 2 } - + tagListView.invalidateIntrinsicContentSize() + guard let pointModel = model.points.last else { return } - dateLab.text = getDateInterval2String(date: "\(pointModel.expected_timestamp/1000)", dateFormat: "MM.dd") + dateLab.text = getDateInterval2String(date: "\(pointModel.expected_timestamp/1000)", dateFormat: "MM.dd") } override init(style: CellStyle, reuseIdentifier: String?) { @@ -310,38 +352,47 @@ class ScheduleListPopCell: UITableViewCell { cornerView.addSubview(nameLab) cornerView.addSubview(monthLab) cornerView.addSubview(dateLab) + cornerView.addSubview(tagListView) cornerView.addSubview(editBtn) cornerView.addSubview(followBtn) bgView.layoutChain - .edgesVertical(15) + .top(15).bottom(15) .edgesHorzontal(15) - .height(80) cornerView.layoutChain.edges() monthLab.layoutChain - .bottomToCenterYOfView(cornerView) + .top(15) .left(11) + .width(35) dateLab.layoutChain - .topToBottomOfView(monthLab) + .topToBottomOfView(monthLab, offset: 4) .leftToView(monthLab) + .width(35) + + editBtn.layoutChain + .top(15) + .right(10) + .width(70) + .height(28) nameLab.layoutChain - .centerY() + .top(15) .leftToRightOfView(monthLab, offset: 15) - editBtn.layoutChain - .right(10) - .centerY() - .width(56) - .height(28) + tagListView.layoutChain + .leftToRightOfView(dateLab, offset: 15) + .bottom(15) + .rightToLeftOfView(editBtn, offset: -10) + + nameLab.layoutChain.bottomToTopOfView(tagListView, offset:-5) followBtn.layoutChain .topToView(editBtn) .rightToView(editBtn) - .width(56) + .width(70) .height(28) } @@ -420,6 +471,7 @@ class ScheduleListPopCell: UITableViewCell { let btn = UIButton() btn.isHidden = true btn.setTitle("关注", for: .normal) + btn.setTitle("取消关注", for: .selected) btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) btn.backgroundColor = .white @@ -429,4 +481,15 @@ class ScheduleListPopCell: UITableViewCell { return btn }() + lazy var tagListView: TagListView = { + let view = TagListView() + view.textFont = UIFont.systemFont(ofSize: 8, weight: .regular) + view.textColor = UIColor(hexStr: "#B78C56") + view.tagBackgroundColor = UIColor(hexStr: "#FFF3E4") + view.paddingX = 6 // 水平内边距 + view.paddingY = 6 // 垂直内边距 + view.alignment = .left // 对齐 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() } diff --git a/QuickLocation/Section/Schedule/ScheduleViewModel.swift b/QuickLocation/Section/Schedule/ScheduleViewModel.swift index bd66e43..38f305e 100644 --- a/QuickLocation/Section/Schedule/ScheduleViewModel.swift +++ b/QuickLocation/Section/Schedule/ScheduleViewModel.swift @@ -48,6 +48,13 @@ class ScheduleViewModel: ViewModelType { sectionedItems.onNext(list.mapSection()) } + lazy var viewedCellAction: Action = { this in + return Action { model in + AppRouter.push(Route.scheduleViewed) + return .empty() + } + }(self) + lazy var cellAction: Action = { this in return Action { model in AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()]) @@ -55,6 +62,18 @@ class ScheduleViewModel: ViewModelType { } }(self) + /// 切换条件 + /// - Parameters: + /// - follow: 只查看关注的行程 + /// - own: 只查看自己创建的行程 + /// - history: true查看历史行程,默认查看今天之后的行程 + /// - group_key: 过滤圈子查询 + func changeCondition(follow: Bool, own: Bool) { + listService.targetTransform = { page in + ItineraryAPI.query(follow: follow, own: own, history: false, group_key: "", page: page).multiTarget + } + } + // MARK: - init init() { listService = ItineraryService.query() diff --git a/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedVC.swift b/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedVC.swift new file mode 100644 index 0000000..f04955d --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedVC.swift @@ -0,0 +1,65 @@ +// +// ScheduleViewedVC.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa +import RxDataSources +import MJRefresh + +class ScheduleViewedVC: BaseViewController { + + fileprivate var rootView: ScheduleViewedView! + + override func loadView() { + rootView = ScheduleViewedView(frame: UIScreen.main.bounds) + view = rootView + } + + private var viewModel = ScheduleViewedViewModel() + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + bindViewModel() + requestFollowList() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + private func requestFollowList() { + DLToast.showLoading() + UserService.followList().subscribe(onNext: { response in + DLToast.dismiss() + self.viewModel.loadViewedData(response.list) + }).disposed(by: disposeBag) + } + + private func bindViewModel() { + viewModel.output.sectionedItems + .bind(to: rootView.tableView.rx.items(dataSource: tableViewDataSource)) + .disposed(by: disposeBag) + + rootView.tableView.rx.modelSelected(ViewedModel.self) + .subscribe(viewModel.cellAction.inputs) + .disposed(by: disposeBag) + } + + // MARK: - dataSource + lazy private var tableViewDataSource: RxTableViewSectionedReloadDataSource = { + return RxTableViewSectionedReloadDataSource( + configureCell: { (_, tableView, indexPath, model) in + let cell: ScheduleViewedListCell = tableView.dequeueReusableCell(for: indexPath) + cell.configure(model) + return cell + } + ) + }() +} diff --git a/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedVM.swift b/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedVM.swift new file mode 100644 index 0000000..0c873c1 --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedVM.swift @@ -0,0 +1,40 @@ +// +// ScheduleViewedVM.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import RxSwift +import RxCocoa +import RxDataSources +import ObjectMapper + +struct ScheduleViewedViewModel { + + struct Output { + var sectionedItems: Observable<[ViewedListSectionModel]> + } + + let output: Output + + private let sectionedItems = PublishSubject<[ViewedListSectionModel]>() + + lazy var cellAction: Action = { this in + return Action { model in + + return .empty() + } + }(self) + + func loadViewedData(_ list: [ViewedModel]) { + sectionedItems.onNext(list.mapSection()) + } + + // MARK: - init + init() { + output = Output( + sectionedItems: sectionedItems + ) + } +} diff --git a/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedView.swift b/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedView.swift new file mode 100644 index 0000000..69900c6 --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleViewed/ScheduleViewedView.swift @@ -0,0 +1,258 @@ +// +// ScheduleViewedView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/26. +// + +import UIKit +import RxSwift +import RxCocoa +import TagListView + +class ScheduleViewedView: UIView { + + var disposeBag = DisposeBag() + + private func setupRx() { + backBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.shared.popOrDismiss() + }).disposed(by: disposeBag) + } + + private func setupUI() { + addSubview(navBgView) + addSubview(navBarView) + navBarView.addSubview(navTitleLabel) + navBarView.addSubview(backBtn) + addSubview(tableView) + + navBgView.layoutChain + .edges(excludingEdge: .bottom) + .heightToWidth(160/375) + + navBarView.layoutChain + .edges(excludingEdge: .bottom) + .height(kNaviHeight) + + navTitleLabel.layoutChain + .top(kStatusBarHeight + 12) + .centerY(backBtn) + .centerX() + + backBtn.layoutChain + .centerY(navTitleLabel) + .left(15) + .width(24) + .height(24) + + tableView.layoutChain + .topToBottomOfView(navBarView, offset: 15) + .edges(excludingEdge: .top) + + } + + lazy var navBgView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage(named: "Common/navBar_bg_2") + iv.contentMode = .scaleAspectFill + return iv + }() + + lazy var navBarView: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + lazy var navTitleLabel: UILabel = { + let label = UILabel() + label.text = "看过我的" + label.font = .systemFont(ofSize: 18, weight: .medium) + label.textColor = ThemeManager.shared.color.titleAuxColor + label.textAlignment = .center + return label + }() + + 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 + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + backgroundColor = .white + setupUI() + setupRx() + } + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.estimatedRowHeight = 86 + tableView.rowHeight = UITableView.automaticDimension + tableView.showsVerticalScrollIndicator = false + tableView.register(ScheduleViewedListCell.self) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin + 20, right: 0) + return tableView + }() + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +// MARK: - ScheduleViewedListCell +class ScheduleViewedListCell: UITableViewCell { + + var disposeBag = DisposeBag() + + func configure(_ model: ViewedModel) { + iconView.image = model.userIcon + nameLab.text = model.nick_name + viewedCountLab.text = "看过我\(model.count)次" + let groupNames = model.groups.map { $0.group_name } + // addTags 内部用 frame.width 排标签,先给预估宽度 + if !groupNames.isEmpty { + let w = contentView.frame.width > 0 ? contentView.frame.width : UIScreen.main.bounds.width + tagListView.frame.size.width = w - 185 + } + tagListView.removeAllTags() + tagListView.addTags(groupNames) + tagListView.tagViews.forEach { + $0.layer.cornerRadius = 2 + } + tagListView.invalidateIntrinsicContentSize() + } + + override init(style: CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + setupSubviews() + } + + private func setupSubviews() { + contentView.addSubview(bgView) + bgView.addSubview(cornerView) + cornerView.addSubview(iconView) + cornerView.addSubview(nameLab) + cornerView.addSubview(viewedCountLab) + cornerView.addSubview(tagTitleLab) + cornerView.addSubview(tagListView) + + bgView.layoutChain + .top(10).bottom() + .edgesHorzontal(15) + + cornerView.layoutChain.edges() + + iconView.layoutChain + .left(15) + .top(18) + .height(50) + .widthToHeight(1) + + nameLab.layoutChain + .topToView(iconView) + .leftToRightOfView(iconView, offset: 10) + + viewedCountLab.layoutChain + .right(15) + .centerY(nameLab) + + tagTitleLab.layoutChain + .topToBottomOfView(nameLab, offset: 9) + .leftToView(nameLab) + .width(60) + + tagListView.layoutChain + .topToBottomOfView(nameLab, offset: 5) + .leftToRightOfView(tagTitleLab, offset: 5) + .bottom(20) + .right(15) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } + + lazy var bgView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.05).cgColor + view.layer.shadowOffset = CGSize(width: 0, height: 0) + view.layer.shadowOpacity = 1 + view.layer.shadowRadius = 8 + return view + }() + + lazy var cornerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.cornerRadius = 10 + return view + }() + + lazy var iconView: UIImageView = { + let view = UIImageView() + view.backgroundColor = .clear + view.contentMode = .scaleAspectFill + view.cornerRadius = 25 + return view + }() + + lazy var nameLab: UILabel = { + let label = UILabel() + label.textColor = ThemeManager.shared.color.titleAuxColor + label.font = .systemFont(ofSize: 14, weight: .medium) + return label + }() + + lazy var viewedCountLab: UILabel = { + let label = UILabel() + label.textColor = UIColor(hexStr: "#999999") + label.font = .systemFont(ofSize: 12, weight: .medium) + return label + }() + + lazy var tagTitleLab: UILabel = { + let label = UILabel() + label.text = "共同的圈子:" + label.textColor = UIColor(hexStr: "#999999") + label.font = .systemFont(ofSize: 10, weight: .medium) + return label + }() + + lazy var tagListView: TagListView = { + let view = TagListView() + view.textFont = UIFont.systemFont(ofSize: 8, weight: .regular) + view.textColor = UIColor(hexStr: "#B78C56") + view.tagBackgroundColor = UIColor(hexStr: "#FFF3E4") + view.paddingX = 6 // 水平内边距 + view.paddingY = 6 // 垂直内边距 + view.alignment = .left // 对齐 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() +} diff --git a/QuickLocation/Section/Schedule/ViewedModel.swift b/QuickLocation/Section/Schedule/ViewedModel.swift index bdfdff3..7bce48d 100644 --- a/QuickLocation/Section/Schedule/ViewedModel.swift +++ b/QuickLocation/Section/Schedule/ViewedModel.swift @@ -39,6 +39,9 @@ struct ViewedModel: Mappable, Equatable { /// 查看次数 var count: Int = 0 + /// 共同圈子 + var groups: [GroupCommonModel] = [] + init?(map: Map) { } @@ -48,6 +51,7 @@ struct ViewedModel: Mappable, Equatable { nick_name <- map["nick_name"] head_pic <- map["head_pic"] count <- map["count"] + groups <- map["groups"] } } diff --git a/QuickLocation/Service/ItineraryService.swift b/QuickLocation/Service/ItineraryService.swift index 4595eef..7fb96ab 100644 --- a/QuickLocation/Service/ItineraryService.swift +++ b/QuickLocation/Service/ItineraryService.swift @@ -33,10 +33,10 @@ struct ItineraryService { /// 查询行程关注人 /// - Parameters: /// - id: 行程ID - static func queryFollowList(id: String) -> Observable { + static func queryFollowList(id: String) -> Observable { let api = ItineraryAPI.queryFollowList(id: id).multiTarget return APIProvider.request(token: api) - .map(ResponseModel.self) + .map(ViewedListResponse.self) .asObservable() } @@ -62,4 +62,15 @@ struct ItineraryService { .map(ResponseModel.self) .asObservable() } + + /// 关注行程 + /// - Parameters: + /// - id: 行程ID + /// - op: 1关注 2取消关注 + static func follow(id: String, op: Int) -> Observable { + let api = ItineraryAPI.follow(id: id, op: op).multiTarget + return APIProvider.request(token: api) + .map(ResponseModel.self) + .asObservable() + } } diff --git a/QuickLocation/Service/UserService.swift b/QuickLocation/Service/UserService.swift index 48c85ef..8c8e2ca 100644 --- a/QuickLocation/Service/UserService.swift +++ b/QuickLocation/Service/UserService.swift @@ -140,4 +140,15 @@ struct UserService { .map(ViewedListResponse.self) .asObservable() } + + /// 设置气泡 + /// - Parameters: + /// - enable: + /// - keep_time: + static func setBubble(enable: Bool, keep_time: Int) -> Observable { + let api = UserAPI.bubble(enable: enable, keep_time: keep_time).multiTarget + return APIProvider.request(token: api) + .map(ResponseModel.self) + .asObservable() + } }