- 行程详情

- 历史行程
- 谁看过我
- 气泡
This commit is contained in:
linshujie 2026-06-26 18:34:05 +08:00
parent e438736fb4
commit 5a334ae182
45 changed files with 2371 additions and 52 deletions

View File

@ -176,6 +176,20 @@
307073E62FD18A20004C37CC /* GroupChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E22FD18A20004C37CC /* GroupChatVC.swift */; }; 307073E62FD18A20004C37CC /* GroupChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E22FD18A20004C37CC /* GroupChatVC.swift */; };
307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */; }; 307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */; };
30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A7A9102FCAEE3D00105780 /* GroupListPopView.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 */; }; 30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */; };
30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; }; 30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; };
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8502FCD331C00C33B5C /* GroupAPI.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 = "<group>"; }; 307073E32FD18A20004C37CC /* GroupChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatView.swift; sourceTree = "<group>"; };
307073E92FD2715A004C37CC /* GroupChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatViewModel.swift; sourceTree = "<group>"; }; 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatViewModel.swift; sourceTree = "<group>"; };
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListPopView.swift; sourceTree = "<group>"; }; 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListPopView.swift; sourceTree = "<group>"; };
30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailVC.swift; sourceTree = "<group>"; };
30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailView.swift; sourceTree = "<group>"; };
30A87A4F2FEE4E4B0095E7C6 /* ScheduleHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleHistoryView.swift; sourceTree = "<group>"; };
30A87A512FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleHistoryVC.swift; sourceTree = "<group>"; };
30A87A532FEE50B10095E7C6 /* ScheduleHistoryVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleHistoryVM.swift; sourceTree = "<group>"; };
30A87A562FEE5A350095E7C6 /* ScheduleViewedVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewedVC.swift; sourceTree = "<group>"; };
30A87A582FEE5A4C0095E7C6 /* ScheduleViewedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewedView.swift; sourceTree = "<group>"; };
30A87A5A2FEE5AC20095E7C6 /* ScheduleViewedVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewedVM.swift; sourceTree = "<group>"; };
30A87A5D2FEE71A50095E7C6 /* CreateBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleView.swift; sourceTree = "<group>"; };
30A87A5F2FEE71B80095E7C6 /* CreateBubbleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleVC.swift; sourceTree = "<group>"; };
30A87A612FEE724D0095E7C6 /* CreateBubbleTiemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTiemView.swift; sourceTree = "<group>"; };
30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = "<group>"; };
30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = "<group>"; };
30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = "<group>"; };
30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = "<group>"; }; 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = "<group>"; };
30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = "<group>"; }; 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = "<group>"; };
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = "<group>"; }; 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = "<group>"; };
@ -930,6 +958,7 @@
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */, 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */,
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */, 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */,
30D87CDC2FDFF07500E958FD /* InteractionView.swift */, 30D87CDC2FDFF07500E958FD /* InteractionView.swift */,
30A87A5C2FEE711C0095E7C6 /* Bubble */,
30CCDE4F2FE2782700F5214A /* SignIn */, 30CCDE4F2FE2782700F5214A /* SignIn */,
30CCDE562FE39F6B00F5214A /* SOS */, 30CCDE562FE39F6B00F5214A /* SOS */,
); );
@ -1216,6 +1245,48 @@
path = GroupChat; path = GroupChat;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
30A87A492FEE1CB20095E7C6 /* ItineraryDetail */ = {
isa = PBXGroup;
children = (
30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */,
30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */,
);
path = ItineraryDetail;
sourceTree = "<group>";
};
30A87A4E2FEE4E390095E7C6 /* ScheduleHistory */ = {
isa = PBXGroup;
children = (
30A87A512FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift */,
30A87A532FEE50B10095E7C6 /* ScheduleHistoryVM.swift */,
30A87A4F2FEE4E4B0095E7C6 /* ScheduleHistoryView.swift */,
);
path = ScheduleHistory;
sourceTree = "<group>";
};
30A87A552FEE5A130095E7C6 /* ScheduleViewed */ = {
isa = PBXGroup;
children = (
30A87A562FEE5A350095E7C6 /* ScheduleViewedVC.swift */,
30A87A5A2FEE5AC20095E7C6 /* ScheduleViewedVM.swift */,
30A87A582FEE5A4C0095E7C6 /* ScheduleViewedView.swift */,
);
path = ScheduleViewed;
sourceTree = "<group>";
};
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 = "<group>";
};
30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = { 30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1302,6 +1373,9 @@
30D74ABB2FEA67CE0050EB2C /* CreateSchedule */, 30D74ABB2FEA67CE0050EB2C /* CreateSchedule */,
30D74BF22FEB6F5B0050EB2C /* LocationPicker */, 30D74BF22FEB6F5B0050EB2C /* LocationPicker */,
30BF300A2FED09A300D9CB52 /* ScheduleDetail */, 30BF300A2FED09A300D9CB52 /* ScheduleDetail */,
30A87A492FEE1CB20095E7C6 /* ItineraryDetail */,
30A87A4E2FEE4E390095E7C6 /* ScheduleHistory */,
30A87A552FEE5A130095E7C6 /* ScheduleViewed */,
); );
path = Schedule; path = Schedule;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1644,6 +1718,8 @@
305A76922FCA8C7000227D26 /* LogUtils.swift in Sources */, 305A76922FCA8C7000227D26 /* LogUtils.swift in Sources */,
305A76932FCA8C7000227D26 /* AddImageCell.swift in Sources */, 305A76932FCA8C7000227D26 /* AddImageCell.swift in Sources */,
30EFF3D82FDA8F1000EB35D4 /* EmergencyContactVC.swift in Sources */, 30EFF3D82FDA8F1000EB35D4 /* EmergencyContactVC.swift in Sources */,
30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */,
30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */,
30DC185E2FD1211D0041DCD1 /* VipRightsVC.swift in Sources */, 30DC185E2FD1211D0041DCD1 /* VipRightsVC.swift in Sources */,
305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */, 305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */,
305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */, 305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */,
@ -1660,7 +1736,9 @@
3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */, 3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */,
30EFF3DA2FDA935D00EB35D4 /* EmergencyContactFooterView.swift in Sources */, 30EFF3DA2FDA935D00EB35D4 /* EmergencyContactFooterView.swift in Sources */,
3062E8C22FCFB86800CEF511 /* CreateGroupViewModel.swift in Sources */, 3062E8C22FCFB86800CEF511 /* CreateGroupViewModel.swift in Sources */,
30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */,
305A769B2FCA8C7000227D26 /* PopupAnimators.swift in Sources */, 305A769B2FCA8C7000227D26 /* PopupAnimators.swift in Sources */,
30A87A4D2FEE1DB40095E7C6 /* ItineraryDetailView.swift in Sources */,
305A769C2FCA8C7000227D26 /* PopupViewController.swift in Sources */, 305A769C2FCA8C7000227D26 /* PopupViewController.swift in Sources */,
305A769D2FCA8C7000227D26 /* PopupViewController+Extension.swift in Sources */, 305A769D2FCA8C7000227D26 /* PopupViewController+Extension.swift in Sources */,
30EFF3C42FDA431D00EB35D4 /* ChangePhoneVC.swift in Sources */, 30EFF3C42FDA431D00EB35D4 /* ChangePhoneVC.swift in Sources */,
@ -1677,6 +1755,7 @@
305A76A42FCA8C7000227D26 /* Date+Extension.swift in Sources */, 305A76A42FCA8C7000227D26 /* Date+Extension.swift in Sources */,
30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */, 30D74AAE2FEA13E00050EB2C /* ScheduleView.swift in Sources */,
305A76A52FCA8C7000227D26 /* Dictionay+Extension.swift in Sources */, 305A76A52FCA8C7000227D26 /* Dictionay+Extension.swift in Sources */,
30A87A522FEE4E5D0095E7C6 /* ScheduleHistoryVC.swift in Sources */,
305A76A62FCA8C7000227D26 /* Int+Extension.swift in Sources */, 305A76A62FCA8C7000227D26 /* Int+Extension.swift in Sources */,
30EFF3D62FDA8F0100EB35D4 /* EmergencyContactView.swift in Sources */, 30EFF3D62FDA8F0100EB35D4 /* EmergencyContactView.swift in Sources */,
30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */, 30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */,
@ -1686,6 +1765,7 @@
305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */, 305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */,
305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */, 305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */,
305A76AB2FCA8C7000227D26 /* ScaleType.swift in Sources */, 305A76AB2FCA8C7000227D26 /* ScaleType.swift in Sources */,
30A87A572FEE5A350095E7C6 /* ScheduleViewedVC.swift in Sources */,
30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */, 30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */,
305A76AC2FCA8C7000227D26 /* String+Extension.swift in Sources */, 305A76AC2FCA8C7000227D26 /* String+Extension.swift in Sources */,
30D74AB82FEA36A50050EB2C /* ItineraryService.swift in Sources */, 30D74AB82FEA36A50050EB2C /* ItineraryService.swift in Sources */,
@ -1697,6 +1777,7 @@
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */, 305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */,
305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */, 305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */,
305A76B32FCA8C7000227D26 /* UILabel+Extension.swift in Sources */, 305A76B32FCA8C7000227D26 /* UILabel+Extension.swift in Sources */,
30A87A5B2FEE5AC20095E7C6 /* ScheduleViewedVM.swift in Sources */,
305A76B42FCA8C7000227D26 /* UINavigationController+FDFullscreenPopGesture.m in Sources */, 305A76B42FCA8C7000227D26 /* UINavigationController+FDFullscreenPopGesture.m in Sources */,
305A76B52FCA8C7000227D26 /* UITableView+Extension.swift in Sources */, 305A76B52FCA8C7000227D26 /* UITableView+Extension.swift in Sources */,
30CCDE582FE39F8C00F5214A /* SOSView.swift in Sources */, 30CCDE582FE39F8C00F5214A /* SOSView.swift in Sources */,
@ -1720,7 +1801,9 @@
3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */, 3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */,
30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */, 30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */,
305A76C22FCA8C7000227D26 /* BaseViewModel.swift in Sources */, 305A76C22FCA8C7000227D26 /* BaseViewModel.swift in Sources */,
30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */,
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */, 30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */,
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */,
30DC185B2FD11E7A0041DCD1 /* NavigationTitleView.swift in Sources */, 30DC185B2FD11E7A0041DCD1 /* NavigationTitleView.swift in Sources */,
30DC185C2FD11E7A0041DCD1 /* WebViewController.swift in Sources */, 30DC185C2FD11E7A0041DCD1 /* WebViewController.swift in Sources */,
305A76C32FCA8C7000227D26 /* MainTabBarController.swift in Sources */, 305A76C32FCA8C7000227D26 /* MainTabBarController.swift in Sources */,
@ -1742,6 +1825,7 @@
305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */, 305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */,
3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */, 3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */,
305A76CF2FCA8C7000227D26 /* CountDownService.swift in Sources */, 305A76CF2FCA8C7000227D26 /* CountDownService.swift in Sources */,
30A87A502FEE4E4B0095E7C6 /* ScheduleHistoryView.swift in Sources */,
305A76D02FCA8C7000227D26 /* MoneyFormatter.swift in Sources */, 305A76D02FCA8C7000227D26 /* MoneyFormatter.swift in Sources */,
305A76D12FCA8C7000227D26 /* TimeSpecificNotificationManager.swift in Sources */, 305A76D12FCA8C7000227D26 /* TimeSpecificNotificationManager.swift in Sources */,
305A76D22FCA8C7000227D26 /* ThemeManager.swift in Sources */, 305A76D22FCA8C7000227D26 /* ThemeManager.swift in Sources */,
@ -1760,6 +1844,7 @@
305A76DC2FCA8C7000227D26 /* InputSubject.swift in Sources */, 305A76DC2FCA8C7000227D26 /* InputSubject.swift in Sources */,
305A76DD2FCA8C7000227D26 /* NSObject+Rx.swift in Sources */, 305A76DD2FCA8C7000227D26 /* NSObject+Rx.swift in Sources */,
305A79902FCAC61A00227D26 /* InviteMemberVC.swift in Sources */, 305A79902FCAC61A00227D26 /* InviteMemberVC.swift in Sources */,
30A87A542FEE50B10095E7C6 /* ScheduleHistoryVM.swift in Sources */,
305A76DE2FCA8C7000227D26 /* ObservableType+ObjectMapper.swift in Sources */, 305A76DE2FCA8C7000227D26 /* ObservableType+ObjectMapper.swift in Sources */,
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */, 305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */,
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */, 30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */,
@ -1771,6 +1856,7 @@
30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */, 30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */,
30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */, 30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */,
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */, 30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */,
30A87A592FEE5A4C0095E7C6 /* ScheduleViewedView.swift in Sources */,
30EFF3CF2FDA669800EB35D4 /* MyProfileVC.swift in Sources */, 30EFF3CF2FDA669800EB35D4 /* MyProfileVC.swift in Sources */,
30CCDE5C2FE3A1A800F5214A /* SOSPracticeView.swift in Sources */, 30CCDE5C2FE3A1A800F5214A /* SOSPracticeView.swift in Sources */,
30EFF3E52FDAA93400EB35D4 /* PrivacyPolicyVC.swift in Sources */, 30EFF3E52FDAA93400EB35D4 /* PrivacyPolicyVC.swift in Sources */,
@ -1800,8 +1886,10 @@
30BAB8532FCD337C00C33B5C /* GroupService.swift in Sources */, 30BAB8532FCD337C00C33B5C /* GroupService.swift in Sources */,
305A76EE2FCA8C7000227D26 /* MineView.swift in Sources */, 305A76EE2FCA8C7000227D26 /* MineView.swift in Sources */,
305A76EF2FCA8C7000227D26 /* MineViewController.swift in Sources */, 305A76EF2FCA8C7000227D26 /* MineViewController.swift in Sources */,
30A87A5E2FEE71A50095E7C6 /* CreateBubbleView.swift in Sources */,
30D74ABF2FEA67F30050EB2C /* CreateScheduleView.swift in Sources */, 30D74ABF2FEA67F30050EB2C /* CreateScheduleView.swift in Sources */,
305A76F02FCA8C7000227D26 /* MineViewModel.swift in Sources */, 305A76F02FCA8C7000227D26 /* MineViewModel.swift in Sources */,
30A87A602FEE71B80095E7C6 /* CreateBubbleVC.swift in Sources */,
305A76F12FCA8C7000227D26 /* SystemService.swift in Sources */, 305A76F12FCA8C7000227D26 /* SystemService.swift in Sources */,
30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */, 30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */,
30C4C01B2FDBF09D009215C1 /* RemoveMemberView.swift in Sources */, 30C4C01B2FDBF09D009215C1 /* RemoveMemberView.swift in Sources */,

View File

@ -36,6 +36,12 @@ enum ItineraryAPI {
/// - Parameters: /// - Parameters:
/// - id: ID /// - id: ID
case delete(id: String) case delete(id: String)
///
/// - Parameters:
/// - id: ID
/// - op: 1 2
case follow(id: String, op: Int)
} }
extension ItineraryAPI: MultiTargetProtocol { extension ItineraryAPI: MultiTargetProtocol {
@ -50,6 +56,8 @@ extension ItineraryAPI: MultiTargetProtocol {
return "mapi/itinerary/route/set" return "mapi/itinerary/route/set"
case .delete: case .delete:
return "mapi/itinerary/route/delete" return "mapi/itinerary/route/delete"
case .follow:
return "mapi/itinerary/route/follow"
} }
} }
@ -71,7 +79,9 @@ extension ItineraryAPI: MultiTargetProtocol {
params["follow"] = follow params["follow"] = follow
params["own"] = own params["own"] = own
params["history"] = history params["history"] = history
if !group_key.isEmpty {
params["group_key"] = group_key params["group_key"] = group_key
}
params["page"] = page params["page"] = page
params["limit"] = 20 params["limit"] = 20
return .requestParameters(parameters: params, encoding: URLEncoding()) return .requestParameters(parameters: params, encoding: URLEncoding())
@ -98,6 +108,13 @@ extension ItineraryAPI: MultiTargetProtocol {
var params = Parameters() var params = Parameters()
params["id"] = id params["id"] = id
return .requestParameters(parameters: params, encoding: URLEncoding()) 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())
} }
} }
} }

View File

@ -66,6 +66,12 @@ enum UserAPI {
/// ///
case followList case followList
///
/// - Parameters:
/// - enable:
/// - keep_time
case bubble(enable: Bool, keep_time: Int)
} }
extension UserAPI: MultiTargetProtocol { extension UserAPI: MultiTargetProtocol {
@ -100,6 +106,8 @@ extension UserAPI: MultiTargetProtocol {
return "api/user/notice" return "api/user/notice"
case .followList: case .followList:
return "mapi/user/followed" return "mapi/user/followed"
case .bubble:
return "mapi/bubble/operate"
} }
} }
@ -183,6 +191,12 @@ extension UserAPI: MultiTargetProtocol {
case .followList: case .followList:
return .requestParameters(parameters: Parameters(), encoding: URLEncoding()) 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())
} }
} }
} }

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

View File

@ -59,7 +59,7 @@ final class MainTabBarController: UITabBarController {
customTabBar.delegate = self customTabBar.delegate = self
// ""index=1 // ""index=1
customTabBar.shouldSelectTab = { index in customTabBar.shouldSelectTab = { index in
if index == 1, AppContextManager.shared.isGuest { if index == 1 || index == 2, AppContextManager.shared.isGuest {
AppRouter.push(Route.login) AppRouter.push(Route.login)
return false return false
} }
@ -98,7 +98,7 @@ final class MainTabBarController: UITabBarController {
extension MainTabBarController: QuickLocationTabBarDelegate { extension MainTabBarController: QuickLocationTabBarDelegate {
func tabBar(_ tabBar: QuickLocationTabBar, didSelectTabAt index: Int) { func tabBar(_ tabBar: QuickLocationTabBar, didSelectTabAt index: Int) {
// ""index=1 // ""index=1
if index == 1, AppContextManager.shared.isGuest { if index == 1 || index == 2, AppContextManager.shared.isGuest {
AppRouter.push(Route.login) AppRouter.push(Route.login)
return return
} }

View File

@ -57,6 +57,14 @@ enum Route: String {
case createSchedule = "createSchedule" case createSchedule = "createSchedule"
/// ///
case scheduleDetail = "scheduleDetail" case scheduleDetail = "scheduleDetail"
/// 线
case itineraryDetail = "itineraryDetail"
///
case scheduleHistory = "scheduleHistory"
///
case scheduleViewed = "scheduleViewed"
///
case createBubble = "createBubble"
} }
extension Route: RouterTarget { extension Route: RouterTarget {
@ -275,6 +283,28 @@ extension AppRouter: AppRouterProtocol {
return ScheduleDetailVC(routeId: routeId, return ScheduleDetailVC(routeId: routeId,
scheduleJson: parameters["scheduleJson"].safeDictionary as! [String : Any]) 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
}
} }
} }

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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<Int>(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()
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -127,6 +127,11 @@ class HomeViewController: BaseViewController {
// MARK: - Actions // MARK: - Actions
private func reactiveAction() { private func reactiveAction() {
//
rootView.bubbleView.rx.tapGesture.subscribe { _ in
AppRouter.push(Route.createBubble)
}.disposed(by: disposeBag)
// //
rootView.signInView.rx.tapGesture.subscribe { _ in rootView.signInView.rx.tapGesture.subscribe { _ in
let vc = SignInVC(lastLocation: self.lastLocation) let vc = SignInVC(lastLocation: self.lastLocation)

View File

@ -26,7 +26,9 @@ class CreateSchedulePopView: UIView {
tagListView.tagViews.forEach { tagListView.tagViews.forEach {
$0.layer.cornerRadius = 4 $0.layer.cornerRadius = 4
} }
tagListView.invalidateIntrinsicContentSize() // // 使 TagListView intrinsicContentSize
tagListView.setNeedsLayout()
tagListView.layoutIfNeeded()
} }
// MARK: - UI // MARK: - UI

View File

@ -359,13 +359,8 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
} }
// //
let lats = validPoints.map { $0.latitude } if !pointAnnotations.isEmpty {
let lons = validPoints.map { $0.longitude } rootView.mapView.showAnnotations(pointAnnotations, animated: true)
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)
} }
#endif #endif
} }

View File

@ -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..<validPoints.count - 1 {
let p = validPoints[i]
if let wp = AMapGeoPoint.location(withLatitude: CGFloat(p.latitude ?? 0),
longitude: CGFloat(p.longitude ?? 0)) {
waypoints.append(wp)
}
}
request.waypoints = waypoints
}
request.strategy = 0
routeSearch?.aMapDrivingRouteSearch(request)
#endif
}
///
private static func numberImage(_ num: Int) -> 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

View File

@ -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")
}
}

View File

@ -28,11 +28,38 @@ class ScheduleDetailVC: BaseViewController {
// Do any additional setup after loading the view. // Do any additional setup after loading the view.
setupData() setupData()
bindViewModel() bindViewModel()
reactiveAction()
requestFollowList() requestFollowList()
viewModel.loadPointData() 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() { private func bindViewModel() {
viewModel.output.sectionedItems viewModel.output.sectionedItems
.bind(to: rootView.collectionView.rx.items(dataSource: dataSource)) .bind(to: rootView.collectionView.rx.items(dataSource: dataSource))
@ -47,18 +74,37 @@ class ScheduleDetailVC: BaseViewController {
guard let model = viewModel.scheduModel else { return } guard let model = viewModel.scheduModel else { return }
rootView.dateLab.text = rootView.dateLab.getDateInterval2String(date: "\(model.timestamp / 1000)", dateFormat: "yyyy年MM月dd日") rootView.dateLab.text = rootView.dateLab.getDateInterval2String(date: "\(model.timestamp / 1000)", dateFormat: "yyyy年MM月dd日")
rootView.creatorIcon.image = model.userIcon 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 // MARK: - API
private func requestFollowList() { private func requestFollowList() {
dl.showLoading() DLToast.showLoading()
UserService.followList().subscribe(onNext: { response in ItineraryService.queryFollowList(id: viewModel.routeId).subscribe(onNext: { response in
self.dl.dismiss() DLToast.dismiss()
self.viewModel.loadViewedData(response.list) self.viewModel.loadViewedData(response.list)
self.rootView.noDataLab.isHidden = response.list.count > 0 self.rootView.noDataLab.isHidden = response.list.count > 0
}, onError: { _ in }).disposed(by: disposeBag) }, 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 // MARK: - dataSource
private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> = { private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> { datasource, collectionView, indexPath, model in RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> { datasource, collectionView, indexPath, model in

View File

@ -254,11 +254,54 @@ class ScheduleDetailView: UIView {
v.layer.shadowOffset = CGSize(width: 0, height: -2) v.layer.shadowOffset = CGSize(width: 0, height: -2)
v.layer.shadowRadius = 10 v.layer.shadowRadius = 10
v.layer.shadowOpacity = 1 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 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 = { lazy var operateBtn: UIButton = {
let btn = UIButton() let btn = UIButton()
btn.setTitle("取消关注", for: .selected)
btn.setTitleColor(.white, for: .normal) btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
@ -271,8 +314,6 @@ class ScheduleDetailView: UIView {
backgroundColor = .white backgroundColor = .white
setupUI() setupUI()
setupRx() setupRx()
vipTipsLab.text = AppContextManager.shared.vip > 1 ? "" : "升级 VIP查看具体事件"
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {

View File

@ -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<ScheduleListSectionModel> = {
return RxTableViewSectionedReloadDataSource<ScheduleListSectionModel>(
configureCell: { (_, tableView, indexPath, model) in
let cell: ScheduleHistoryListCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model)
return cell
}
)
}()
}

View File

@ -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<RefreshResult>
var pagination: Observable<PaginationModel?>
var error: Observable<Error>
}
let input: Input
let output: Output
private var listService: ListService<ScheduleListResponse>
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<ScheduleModel, Void> = { 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
)
}
}

View File

@ -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
}()
}

View File

@ -65,7 +65,7 @@ struct ScheduleModel: Mappable, Equatable {
/// ///
var points: [SchedulePointModel] = [] var points: [SchedulePointModel] = []
/// ///
var groups: [ScheduleGroupModel] = [] var groups: [GroupCommonModel] = []
init?(map: Map) { init?(map: Map) {
@ -205,7 +205,7 @@ extension SchedulePointEventModel: IdentifiableType {
} }
/// ///
struct ScheduleGroupModel: Mappable, Equatable { struct GroupCommonModel: Mappable, Equatable {
var uuid: String = UUID().uuidString var uuid: String = UUID().uuidString
/// id /// id
var group_key: String = "" var group_key: String = ""
@ -222,7 +222,7 @@ struct ScheduleGroupModel: Mappable, Equatable {
} }
} }
extension ScheduleGroupModel: IdentifiableType { extension GroupCommonModel: IdentifiableType {
public typealias Identity = String public typealias Identity = String
public var identity: String { public var identity: String {

View File

@ -27,15 +27,16 @@ class ScheduleVC: BaseViewController {
super.viewDidLoad() super.viewDidLoad()
// Do any additional setup after loading the view. // Do any additional setup after loading the view.
rootView.allCheckBtn.isSelected = true
setupTableViewHeaderFooter() setupTableViewHeaderFooter()
bindViewModel() bindViewModel()
reactiveAction() reactiveAction()
requestFollowList()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
requestFollowList()
requestData() requestData()
} }
@ -54,13 +55,42 @@ class ScheduleVC: BaseViewController {
self.rootView.tableView.refresh(status: status, isEmpty: isEmpty) self.rootView.tableView.refresh(status: status, isEmpty: isEmpty)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
rootView.collectionView.rx.modelSelected(ViewedModel.self)
.subscribe(viewModel.viewedCellAction.inputs)
.disposed(by: disposeBag)
rootView.tableView.rx.modelSelected(ScheduleModel.self) rootView.tableView.rx.modelSelected(ScheduleModel.self)
.subscribe(viewModel.cellAction.inputs) .subscribe(viewModel.cellAction.inputs)
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
private func reactiveAction() { 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() { private func setupTableViewHeaderFooter() {
@ -79,19 +109,33 @@ class ScheduleVC: BaseViewController {
// MARK: - API // MARK: - API
private func requestFollowList() { private func requestFollowList() {
dl.showLoading() DLToast.showLoading()
UserService.followList().subscribe(onNext: { response in UserService.followList().subscribe(onNext: { response in
self.dl.dismiss() DLToast.dismiss()
self.viewModel.loadViewedData(response.list) 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 // MARK: - API
private func requestData() { private func requestData() {
dl.showLoading()
viewModel.refresh() viewModel.refresh()
} }
//
private func changeCondition(follow: Bool, own: Bool) {
viewModel.changeCondition(follow: follow, own: own)
requestData()
}
// MARK: - dataSource // MARK: - dataSource
private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> = { private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> { datasource, collectionView, indexPath, model in RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> { datasource, collectionView, indexPath, model in
@ -115,7 +159,7 @@ class ScheduleVC: BaseViewController {
// //
cell.followBtn.rx.tap.subscribe(onNext: { _ in cell.followBtn.rx.tap.subscribe(onNext: { _ in
self.requestSetFollow(id: model.id, op: cell.followBtn.isSelected)
}).disposed(by: cell.disposeBag) }).disposed(by: cell.disposeBag)
return cell return cell

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
import RxSwift import RxSwift
import RxCocoa import RxCocoa
import TagListView
class ScheduleView: UIView { class ScheduleView: UIView {
@ -112,6 +113,9 @@ class ScheduleView: UIView {
historyBtn.setTitle("历史行程", for: .normal) historyBtn.setTitle("历史行程", for: .normal)
historyBtn.setTitleColor(ThemeManager.shared.color.titleAuxColor, for: .normal) historyBtn.setTitleColor(ThemeManager.shared.color.titleAuxColor, for: .normal)
historyBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) historyBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
historyBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.scheduleHistory)
}).disposed(by: disposeBag)
view.addSubview(historyBtn) view.addSubview(historyBtn)
historyBtn.layoutChain historyBtn.layoutChain
.right(15) .right(15)
@ -167,14 +171,45 @@ class ScheduleView: UIView {
titleLab.layoutChain.leftToRightOfView(dotView, offset: 5) 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 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 = { lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain) let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear tableView.backgroundColor = .clear
tableView.separatorStyle = .none tableView.separatorStyle = .none
tableView.estimatedRowHeight = 80 tableView.estimatedRowHeight = 80
tableView.rowHeight = UITableView.automaticDimension
tableView.showsVerticalScrollIndicator = false tableView.showsVerticalScrollIndicator = false
tableView.register(ScheduleListPopCell.self) tableView.register(ScheduleListPopCell.self)
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
@ -283,15 +318,22 @@ class ScheduleListPopCell: UITableViewCell {
func configure(_ model: ScheduleModel) { func configure(_ model: ScheduleModel) {
nameLab.text = model.nick_name + " 的行程路线" nameLab.text = model.nick_name + " 的行程路线"
monthLab.text = getDateInterval2String(date: "\(model.timestamp/1000)", dateFormat: "MM月") 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 { let groupNames = model.groups.map { $0.group_name }
editBtn.isHidden = false // addTags frame.width
followBtn.isHidden = true if !groupNames.isEmpty {
let w = contentView.frame.width > 0 ? contentView.frame.width : UIScreen.main.bounds.width
tagListView.frame.size.width = w - 170
} }
else { tagListView.removeAllTags()
editBtn.isHidden = true tagListView.addTags(groupNames)
followBtn.isHidden = model.is_follow ? true : false tagListView.tagViews.forEach {
$0.layer.cornerRadius = 2
} }
tagListView.invalidateIntrinsicContentSize()
guard let pointModel = model.points.last else { return } 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")
@ -310,38 +352,47 @@ class ScheduleListPopCell: UITableViewCell {
cornerView.addSubview(nameLab) cornerView.addSubview(nameLab)
cornerView.addSubview(monthLab) cornerView.addSubview(monthLab)
cornerView.addSubview(dateLab) cornerView.addSubview(dateLab)
cornerView.addSubview(tagListView)
cornerView.addSubview(editBtn) cornerView.addSubview(editBtn)
cornerView.addSubview(followBtn) cornerView.addSubview(followBtn)
bgView.layoutChain bgView.layoutChain
.edgesVertical(15) .top(15).bottom(15)
.edgesHorzontal(15) .edgesHorzontal(15)
.height(80)
cornerView.layoutChain.edges() cornerView.layoutChain.edges()
monthLab.layoutChain monthLab.layoutChain
.bottomToCenterYOfView(cornerView) .top(15)
.left(11) .left(11)
.width(35)
dateLab.layoutChain dateLab.layoutChain
.topToBottomOfView(monthLab) .topToBottomOfView(monthLab, offset: 4)
.leftToView(monthLab) .leftToView(monthLab)
.width(35)
nameLab.layoutChain
.centerY()
.leftToRightOfView(monthLab, offset: 15)
editBtn.layoutChain editBtn.layoutChain
.top(15)
.right(10) .right(10)
.centerY() .width(70)
.width(56)
.height(28) .height(28)
nameLab.layoutChain
.top(15)
.leftToRightOfView(monthLab, offset: 15)
tagListView.layoutChain
.leftToRightOfView(dateLab, offset: 15)
.bottom(15)
.rightToLeftOfView(editBtn, offset: -10)
nameLab.layoutChain.bottomToTopOfView(tagListView, offset:-5)
followBtn.layoutChain followBtn.layoutChain
.topToView(editBtn) .topToView(editBtn)
.rightToView(editBtn) .rightToView(editBtn)
.width(56) .width(70)
.height(28) .height(28)
} }
@ -420,6 +471,7 @@ class ScheduleListPopCell: UITableViewCell {
let btn = UIButton() let btn = UIButton()
btn.isHidden = true btn.isHidden = true
btn.setTitle("关注", for: .normal) btn.setTitle("关注", for: .normal)
btn.setTitle("取消关注", for: .selected)
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.backgroundColor = .white btn.backgroundColor = .white
@ -429,4 +481,15 @@ class ScheduleListPopCell: UITableViewCell {
return btn 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
}()
} }

View File

@ -48,6 +48,13 @@ class ScheduleViewModel: ViewModelType {
sectionedItems.onNext(list.mapSection()) sectionedItems.onNext(list.mapSection())
} }
lazy var viewedCellAction: Action<ViewedModel, Void> = { this in
return Action { model in
AppRouter.push(Route.scheduleViewed)
return .empty()
}
}(self)
lazy var cellAction: Action<ScheduleModel, Void> = { this in lazy var cellAction: Action<ScheduleModel, Void> = { this in
return Action { model in return Action { model in
AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()]) AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()])
@ -55,6 +62,18 @@ class ScheduleViewModel: ViewModelType {
} }
}(self) }(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 // MARK: - init
init() { init() {
listService = ItineraryService.query() listService = ItineraryService.query()

View File

@ -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<ViewedListSectionModel> = {
return RxTableViewSectionedReloadDataSource<ViewedListSectionModel>(
configureCell: { (_, tableView, indexPath, model) in
let cell: ScheduleViewedListCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model)
return cell
}
)
}()
}

View File

@ -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<ViewedModel, Void> = { this in
return Action { model in
return .empty()
}
}(self)
func loadViewedData(_ list: [ViewedModel]) {
sectionedItems.onNext(list.mapSection())
}
// MARK: - init
init() {
output = Output(
sectionedItems: sectionedItems
)
}
}

View File

@ -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
}()
}

View File

@ -39,6 +39,9 @@ struct ViewedModel: Mappable, Equatable {
/// ///
var count: Int = 0 var count: Int = 0
///
var groups: [GroupCommonModel] = []
init?(map: Map) { init?(map: Map) {
} }
@ -48,6 +51,7 @@ struct ViewedModel: Mappable, Equatable {
nick_name <- map["nick_name"] nick_name <- map["nick_name"]
head_pic <- map["head_pic"] head_pic <- map["head_pic"]
count <- map["count"] count <- map["count"]
groups <- map["groups"]
} }
} }

View File

@ -33,10 +33,10 @@ struct ItineraryService {
/// ///
/// - Parameters: /// - Parameters:
/// - id: ID /// - id: ID
static func queryFollowList(id: String) -> Observable<ResponseModel> { static func queryFollowList(id: String) -> Observable<ViewedListResponse> {
let api = ItineraryAPI.queryFollowList(id: id).multiTarget let api = ItineraryAPI.queryFollowList(id: id).multiTarget
return APIProvider.request(token: api) return APIProvider.request(token: api)
.map(ResponseModel.self) .map(ViewedListResponse.self)
.asObservable() .asObservable()
} }
@ -62,4 +62,15 @@ struct ItineraryService {
.map(ResponseModel.self) .map(ResponseModel.self)
.asObservable() .asObservable()
} }
///
/// - Parameters:
/// - id: ID
/// - op: 1 2
static func follow(id: String, op: Int) -> Observable<ResponseModel> {
let api = ItineraryAPI.follow(id: id, op: op).multiTarget
return APIProvider.request(token: api)
.map(ResponseModel.self)
.asObservable()
}
} }

View File

@ -140,4 +140,15 @@ struct UserService {
.map(ViewedListResponse.self) .map(ViewedListResponse.self)
.asObservable() .asObservable()
} }
///
/// - Parameters:
/// - enable:
/// - keep_time
static func setBubble(enable: Bool, keep_time: Int) -> Observable<ResponseModel> {
let api = UserAPI.bubble(enable: enable, keep_time: keep_time).multiTarget
return APIProvider.request(token: api)
.map(ResponseModel.self)
.asObservable()
}
} }