diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 0c2bc75..395d8c6 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -194,6 +194,11 @@ 30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; }; 30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; }; 30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */; }; + 30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */; }; + 30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */; }; + 30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; }; + 30B74B432FF2438800F6744D /* GroupMemberListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B422FF2438800F6744D /* GroupMemberListView.swift */; }; + 30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */; }; 30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */; }; 30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; }; 30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */; }; @@ -476,6 +481,11 @@ 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = ""; }; 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = ""; }; 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultView.swift; sourceTree = ""; }; + 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = ""; }; + 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = ""; }; + 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = ""; }; + 30B74B422FF2438800F6744D /* GroupMemberListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListView.swift; sourceTree = ""; }; + 30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVM.swift; sourceTree = ""; }; 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = ""; }; 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = ""; }; 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = ""; }; @@ -948,6 +958,7 @@ 30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */, 30C4C01E2FDC0EA6009215C1 /* GroupInfo */, 307073E42FD18A20004C37CC /* GroupChat */, + 30B74B3F2FF2435200F6744D /* GroupMemberList */, 30EFF3A22FD7C58400EB35D4 /* GroupSetting */, 30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */, 30C4C0172FDBF066009215C1 /* RemoveMember */, @@ -972,6 +983,7 @@ 30A87A5C2FEE711C0095E7C6 /* Bubble */, 30CCDE4F2FE2782700F5214A /* SignIn */, 30CCDE562FE39F6B00F5214A /* SOS */, + 30B74B382FF2105C00F6744D /* GroupSchedule */, ); path = Home; sourceTree = ""; @@ -1310,6 +1322,25 @@ path = SearchLocation; sourceTree = ""; }; + 30B74B382FF2105C00F6744D /* GroupSchedule */ = { + isa = PBXGroup; + children = ( + 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */, + 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */, + ); + path = GroupSchedule; + sourceTree = ""; + }; + 30B74B3F2FF2435200F6744D /* GroupMemberList */ = { + isa = PBXGroup; + children = ( + 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */, + 30B74B422FF2438800F6744D /* GroupMemberListView.swift */, + 30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */, + ); + path = GroupMemberList; + sourceTree = ""; + }; 30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = { isa = PBXGroup; children = ( @@ -1722,6 +1753,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */, 30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */, 305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */, 305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */, @@ -1786,6 +1818,7 @@ 305A76A72FCA8C7000227D26 /* NSAttributedString+Extension.swift in Sources */, 30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */, 305A76A82FCA8C7000227D26 /* ObjectMapper+Extension.swift in Sources */, + 30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */, 305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */, 305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */, 305A76AB2FCA8C7000227D26 /* ScaleType.swift in Sources */, @@ -1938,6 +1971,7 @@ 305A76FB2FCA8C7000227D26 /* EmptyDataSet.swift in Sources */, 30BF300E2FED09CC00D9CB52 /* ScheduleDetailVC.swift in Sources */, 305A76FC2FCA8C7000227D26 /* EmptyDataSetDelegate.swift in Sources */, + 30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */, 305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */, 30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */, 30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */, @@ -1953,6 +1987,7 @@ 30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */, 30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */, 305A77062FCA8C7000227D26 /* MXScrollViewController.m in Sources */, + 30B74B432FF2438800F6744D /* GroupMemberListView.swift in Sources */, 305A77072FCA8C7000227D26 /* Helper.swift in Sources */, 30BF30102FED0C8E00D9CB52 /* ScheduleDetailVM.swift in Sources */, 30DA36BD2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift in Sources */, @@ -1971,6 +2006,7 @@ 305A770F2FCA8C7000227D26 /* DLCustomPopVC.swift in Sources */, 30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */, 30CCDE532FE2786600F5214A /* SignInView.swift in Sources */, + 30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */, 305A77102FCA8C7000227D26 /* DLSheetPopVC.swift in Sources */, 30EFF3D32FDA69F400EB35D4 /* AvatarIconListView.swift in Sources */, 30EFF3B72FD8F86200EB35D4 /* ReviewMemberListVM.swift in Sources */, diff --git a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate index 1ff7333..db5533c 100644 Binary files a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate and b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/QuickLocation/API/ItineraryAPI.swift b/QuickLocation/API/ItineraryAPI.swift index 06259b4..a4dbf13 100644 --- a/QuickLocation/API/ItineraryAPI.swift +++ b/QuickLocation/API/ItineraryAPI.swift @@ -82,8 +82,10 @@ extension ItineraryAPI: MultiTargetProtocol { if !group_key.isEmpty { params["group_key"] = group_key } - params["page"] = page - params["limit"] = 20 + if page != -1 { + params["page"] = page + params["limit"] = 20 + } return .requestParameters(parameters: params, encoding: URLEncoding()) case let .queryFollowList(id): diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Contents.json new file mode 100644 index 0000000..e1fe595 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_408@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_408@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Group_408@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Group_408@2x.png new file mode 100644 index 0000000..09b6055 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Group_408@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Group_408@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Group_408@3x.png new file mode 100644 index 0000000..4af9b9c Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/1.imageset/Group_408@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Contents.json new file mode 100644 index 0000000..4a3a217 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1914@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1914@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Group_1914@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Group_1914@2x.png new file mode 100644 index 0000000..3f7040a Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Group_1914@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Group_1914@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Group_1914@3x.png new file mode 100644 index 0000000..22ca879 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/2.imageset/Group_1914@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Contents.json new file mode 100644 index 0000000..3cf8484 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1987@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1987@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Group_1987@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Group_1987@2x.png new file mode 100644 index 0000000..39f561a Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Group_1987@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Group_1987@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Group_1987@3x.png new file mode 100644 index 0000000..3a7df52 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/3.imageset/Group_1987@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Contents.json new file mode 100644 index 0000000..a70ccf0 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1914@2x(1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1914@3x(1).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@2x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@2x(1).png new file mode 100644 index 0000000..b6f03dd Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@2x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@3x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@3x(1).png new file mode 100644 index 0000000..536a0e7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@3x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Contents.json new file mode 100644 index 0000000..4c1ac75 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1986@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1986@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Group_1986@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Group_1986@2x.png new file mode 100644 index 0000000..f079adb Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Group_1986@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Group_1986@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Group_1986@3x.png new file mode 100644 index 0000000..061c2db Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/5.imageset/Group_1986@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Contents.json new file mode 100644 index 0000000..ba81939 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_408@2x(1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_408@3x(1).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@2x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@2x(1).png new file mode 100644 index 0000000..5542400 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@2x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@3x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@3x(1).png new file mode 100644 index 0000000..aefd900 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@3x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Contents.json new file mode 100644 index 0000000..9354123 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1985@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1985@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Group_1985@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Group_1985@2x.png new file mode 100644 index 0000000..9cd8a0f Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Group_1985@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Group_1985@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Group_1985@3x.png new file mode 100644 index 0000000..538005c Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/7.imageset/Group_1985@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Contents.json new file mode 100644 index 0000000..7c3ef2c --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1988@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1988@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Group_1988@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Group_1988@2x.png new file mode 100644 index 0000000..0561363 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Group_1988@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Group_1988@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Group_1988@3x.png new file mode 100644 index 0000000..cf2706a Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/8.imageset/Group_1988@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Contents.json new file mode 100644 index 0000000..6c33f34 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1900@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1900@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@2x.png new file mode 100644 index 0000000..c3d9549 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@3x.png new file mode 100644 index 0000000..1918c9f Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Contents.json new file mode 100644 index 0000000..a27253f --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1900@2x(1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1900@3x(1).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@2x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@2x(1).png new file mode 100644 index 0000000..816aef7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@2x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@3x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@3x(1).png new file mode 100644 index 0000000..4c4300e Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@3x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Contents.json new file mode 100644 index 0000000..6a4d508 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@2x.png new file mode 100644 index 0000000..82f21b1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@3x.png new file mode 100644 index 0000000..62b5944 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Contents.json new file mode 100644 index 0000000..6851f2b --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x(1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x(1).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@2x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@2x(1).png new file mode 100644 index 0000000..6a601e7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@2x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@3x(1).png b/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@3x(1).png new file mode 100644 index 0000000..602c901 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@3x(1).png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Contents.json new file mode 100644 index 0000000..491dc35 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Union@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Union@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png new file mode 100644 index 0000000..ac0d5b7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png new file mode 100644 index 0000000..e5ea904 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Contents.json new file mode 100644 index 0000000..84958d2 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1989@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1989@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@2x.png new file mode 100644 index 0000000..0cb1c49 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@3x.png new file mode 100644 index 0000000..7f6e50e Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@3x.png differ diff --git a/QuickLocation/Manager/Account/AppContextManager.swift b/QuickLocation/Manager/Account/AppContextManager.swift index 161622d..8f22e2d 100644 --- a/QuickLocation/Manager/Account/AppContextManager.swift +++ b/QuickLocation/Manager/Account/AppContextManager.swift @@ -58,7 +58,8 @@ class AppContextManager: NSObject { var sex: Int { account?.sex ?? -1 } - /// VIP + + /// 会员 1:非会员 2:普通会员 3:终身会员 var vip: Int { account?.vip ?? 1 } diff --git a/QuickLocation/Manager/App/RouterManager.swift b/QuickLocation/Manager/App/RouterManager.swift index 6a804f5..6fd8030 100644 --- a/QuickLocation/Manager/App/RouterManager.swift +++ b/QuickLocation/Manager/App/RouterManager.swift @@ -69,6 +69,10 @@ enum Route: String { case searchLocation = "searchLocation" /// 查找位置结果 case searchLocationResult = "searchLocationResult" + /// 圈子行程列表 + case groupSchedule = "groupSchedule" + /// 圈子成员列表 + case groupMemberList = "groupMemberList" } extension Route: RouterTarget { @@ -323,6 +327,17 @@ extension AppRouter: AppRouterProtocol { code: parameters["code"].safeInt, memberData: parameters["memberData"].safeDictionary as! [String : Any]) } + + // MARK: - 圈子行程列表 + AppRouter.register(Route.groupSchedule) { url, parameters in + let groupKey = parameters["groupKey"].safeString + return GroupScheduleVC(groupKey: groupKey) + } + + // MARK: - 圈子成员列表 + AppRouter.register(Route.groupMemberList) { url, parameters in + GroupMemberListVC(groupKey: parameters["groupKey"].safeString) + } } } diff --git a/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift b/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift index 79847da..eafe769 100644 --- a/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift +++ b/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift @@ -183,10 +183,16 @@ final class GroupChatVC: BaseViewController { }) .disposed(by: disposeBag) + // 审核 rootView.reviewBtn.rx.tap.subscribe(onNext: { _ in AppRouter.push(Route.reviewMemberList, userInfo: ["groupId": self.viewModel.groupId]) }).disposed(by: disposeBag) + // 成员列表 + rootView.memberBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.push(Route.groupMemberList, userInfo: ["groupKey": self.viewModel.groupId]) + }).disposed(by: disposeBag) + // 语音按钮 rootView.voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in guard let self = self else { return } diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift new file mode 100644 index 0000000..c021c17 --- /dev/null +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift @@ -0,0 +1,103 @@ +// +// GroupMemberListVC.swift +// QuickLocation +// +// Created by 八条 on 2026/6/29. +// + +import UIKit +import RxSwift +import RxCocoa +import RxDataSources +import ObjectMapper + +class GroupMemberListVC: BaseViewController { + + fileprivate var rootView: GroupMemberListView! + + override func loadView() { + rootView = GroupMemberListView(frame: UIScreen.main.bounds) + view = rootView + } + + private var viewModel: GroupMemberListVM + + override func viewDidLoad() { + super.viewDidLoad() + + bindViewModel() + reactiveAction() + + requestGroupInfo() + } + + private func reactiveAction() { + + } + + private var selectedRow = 0 + + private func bindViewModel() { + viewModel.output.sectionedItems + .bind(to: rootView.collectionView.rx.items(dataSource: memberDataSource)) + .disposed(by: disposeBag) + + viewModel.output.drivingSectionedItems + .bind(to: rootView.drivingEventCV.rx.items(dataSource: drivingDataSource)) + .disposed(by: disposeBag) + + // 成员点击 + rootView.collectionView.rx.modelSelected(GroupMemberModel.self) + .subscribe(onNext: { [weak self] model in + guard let self = self, let row = self.viewModel.rowOf(userId: model.user_id) else { return } + self.selectedRow = row + self.rootView.selectedMemberIsSelf = self.viewModel.isCurrentUser(id: model.user_id) + self.rootView.collectionView.reloadData() + }).disposed(by: disposeBag) + } + + // MARK: - dataSource + + private lazy var memberDataSource: RxCollectionViewSectionedReloadDataSource = { + RxCollectionViewSectionedReloadDataSource { [weak self] datasource, collectionView, indexPath, model in + let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath) + cell.configure(model: model, + isCurrentUser: self?.viewModel.isCurrentUser(id: model.user_id) ?? false, + isSelected: indexPath.row == (self?.selectedRow ?? 0)) + return cell + } + }() + + private lazy var drivingDataSource: RxCollectionViewSectionedReloadDataSource = { + RxCollectionViewSectionedReloadDataSource { _, collectionView, indexPath, item in + let cell: DrivingEventCell = collectionView.dequeueReusableCell(for: indexPath) + cell.configure(item) + return cell + } + }() + + // MARK: - API + private func requestGroupInfo() { + DLToast.showLoading() + GroupService.groupInfoByKey(viewModel.groupKey).subscribe { response in + DLToast.dismiss() + guard let model = response.model else { return } + self.viewModel.groupModel = model + self.viewModel.loadData(response.list) + // 默认选中第一个成员,更新日期可点范围 + if let first = self.viewModel.firstMemberId { + self.rootView.selectedMemberIsSelf = self.viewModel.isCurrentUser(id: first) + } + }.disposed(by: disposeBag) + } + + // MARK: - Init + init(groupKey: String) { + viewModel = GroupMemberListVM(groupKey: groupKey) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift new file mode 100644 index 0000000..861baa3 --- /dev/null +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift @@ -0,0 +1,130 @@ +// +// GroupMemberListVM.swift +// QuickLocation +// +// Created by 八条 on 2026/6/29. +// + +import RxSwift +import RxRelay +import RxDataSources +import ObjectMapper + +// MARK: - DrivingStats 模型 +struct DrivingStatsData { + let distance_km: Double? + let frequent_lane_change: Int? + let hard_acceleration: Int? + let hard_braking: Int? + let long_driving: Int? + let low_speeding: Int? + let max_speed: Double? + let period: String? + let sharp_turn: Int? + let signal_loss: Int? + let speeding: Int? + let total: Int? +} + +struct DrivingEventItem: IdentifiableType, Equatable { + typealias Identity = String + let identity: String + let title: String + let iconName: String + let count: Int + + init(title: String, iconName: String, count: Int = 0) { + self.identity = title + self.title = title + self.iconName = iconName + self.count = count + } +} + +typealias DrivingEventSection = SectionModel + +class GroupMemberListVM { + + let groupKey: String + var groupModel: GroupInfoModel? + + struct Output { + var sectionedItems: Observable<[GroupMemberListSectionModel]> + var drivingSectionedItems: Observable<[DrivingEventSection]> + } + + let output: Output + + private var disposeBag = DisposeBag() + private let sectionedItems = PublishSubject<[GroupMemberListSectionModel]>() + private let drivingItemsRelay = BehaviorRelay<[DrivingEventItem]>(value: GroupMemberListVM.defaultDrivingEvents) + + private var memberList: [GroupMemberModel] = [] + + private static let defaultDrivingEvents: [DrivingEventItem] = [ + DrivingEventItem(title: "急加速", iconName: "GroupMemberList/1"), + DrivingEventItem(title: "急转向", iconName: "GroupMemberList/3"), + DrivingEventItem(title: "急刹", iconName: "GroupMemberList/5"), + DrivingEventItem(title: "手机干扰", iconName: "GroupMemberList/7"), + DrivingEventItem(title: "超速", iconName: "GroupMemberList/2"), + DrivingEventItem(title: "低速", iconName: "GroupMemberList/4"), + DrivingEventItem(title: "频繁变道", iconName: "GroupMemberList/6"), + DrivingEventItem(title: "长时间驾驶", iconName: "GroupMemberList/8") + ] + + // 是否当前用户 + func isCurrentUser(id: String) -> Bool { + id == AppContextManager.shared.userId + } + + // 是否圈主 + func isGroupOwn(id: String) -> Bool { + guard let model = groupModel else { return false } + return model.group_key.contains(id) + } + + func loadData(_ list: [GroupMemberModel]) { + var tempmemberList = list + tempmemberList.moveToFirst { $0.is_online == true } + tempmemberList.moveToFirst { $0.user_id == AppContextManager.shared.userId } + tempmemberList.moveToFirst { isGroupOwn(id: $0.user_id) } + memberList = tempmemberList + sectionedItems.onNext(memberList.mapSection()) + } + + /// 第一个成员的 userId + var firstMemberId: String? { memberList.first?.user_id } + + /// 获取成员在列表中的行号 + func rowOf(userId: String) -> Int? { + memberList.firstIndex(where: { $0.user_id == userId }) + } + + /// 更新驾驶事件统计 + func updateDrivingStats(_ stats: DrivingStatsData) { + let items = Self.defaultDrivingEvents.enumerated().map { i, item in + let count: Int + switch i { + case 0: count = stats.hard_acceleration ?? 0 + case 1: count = stats.speeding ?? 0 + case 2: count = stats.sharp_turn ?? 0 + case 3: count = stats.low_speeding ?? 0 + case 4: count = stats.hard_braking ?? 0 + case 5: count = stats.frequent_lane_change ?? 0 + case 6: count = stats.signal_loss ?? 0 + case 7: count = stats.long_driving ?? 0 + default: count = 0 + } + return DrivingEventItem(title: item.title, iconName: item.iconName, count: count) + } + drivingItemsRelay.accept(items) + } + + init(groupKey: String) { + self.groupKey = groupKey + output = Output( + sectionedItems: sectionedItems.asObservable(), + drivingSectionedItems: drivingItemsRelay.map { $0.mapSection() }.asObservable() + ) + } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift new file mode 100644 index 0000000..6e5c72a --- /dev/null +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift @@ -0,0 +1,760 @@ +// +// GroupMemberListView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/29. +// + +import UIKit +import RxSwift +import RxCocoa + +class GroupMemberListView: UIView { + + var disposeBag = DisposeBag() + + func updateArrowVisibility() { + let offsetX = collectionView.contentOffset.x + let contentW = collectionView.contentSize.width + let viewW = collectionView.bounds.width + memberArrowLeft.isHidden = offsetX <= 0 + memberArrowRight.isHidden = offsetX >= contentW - viewW + } + + private func setupRx() { + backBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.shared.popOrDismiss() + }).disposed(by: disposeBag) + + collectionView.rx.contentOffset + .subscribe(onNext: { [weak self] _ in + self?.updateArrowVisibility() + }) + .disposed(by: disposeBag) + + // 选中日期变化时更新月份 + selectedDate.subscribe(onNext: { [weak self] date in + guard let self = self else { return } + let fmt = DateFormatter() + fmt.dateFormat = "MM月" + self.monthLab.text = fmt.string(from: date) + }).disposed(by: disposeBag) + + // 往前/往后翻 7 天 + datePreviousBtn.rx.tap.subscribe(onNext: { [weak self] _ in + guard let self = self else { return } + let target = self.dateCollectionView.contentOffset.x - self.dateItemWidth * CGFloat(self.daysPerPage) + self.dateCollectionView.setContentOffset(CGPoint(x: max(0, target), y: 0), animated: true) + }).disposed(by: disposeBag) + + dateNextBtn.rx.tap.subscribe(onNext: { [weak self] _ in + guard let self = self else { return } + var target = self.dateCollectionView.contentOffset.x + self.dateItemWidth * CGFloat(self.daysPerPage) + let maxOffset = CGFloat(self.dateItems.count) * self.dateItemWidth - self.dateCollectionView.bounds.width + target = min(max(0, maxOffset), target) + self.dateCollectionView.setContentOffset(CGPoint(x: target, y: 0), animated: true) + }).disposed(by: disposeBag) + } + + private func setupUI() { + addSubview(navBgView) + addSubview(navBarView) + navBarView.addSubview(navTitleLabel) + navBarView.addSubview(backBtn) + + addSubview(memberArrowLeft) + addSubview(collectionView) + addSubview(memberArrowRight) + addSubview(reportView) + + 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) + + collectionView.layoutChain + .topToBottomOfView(navBarView, offset: 0) + .height(90) + .edgesHorzontal(28) + + memberArrowLeft.layoutChain + .rightToLeftOfView(collectionView, offset: -8) + .height(14) + .width(5) + .centerY(collectionView) + + memberArrowRight.layoutChain + .leftToRightOfView(collectionView, offset: 8) + .height(14) + .width(5) + .centerY(collectionView) + + reportView.layoutChain + .topToBottomOfView(collectionView, offset: 10) + .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 + }() + + // MARK: - 成员列表 + lazy var memberArrowLeft: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "GroupMemberList/member_left") + view.isHidden = true + return view + }() + + lazy var memberArrowRight: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "GroupMemberList/member_right") + view.isHidden = true + return view + }() + + lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + let cvWidth = kScreenWidth - 56 + layout.itemSize = CGSize(width: 65, height: 90) + layout.minimumLineSpacing = 4 + layout.scrollDirection = .horizontal + + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.backgroundColor = .clear + cv.showsHorizontalScrollIndicator = false + cv.register(GroupMemberListCell.self) + return cv + }() + + // MARK: - 报告 + lazy var reportView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#F5FBFF") + view.layer.cornerRadius = 20 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + let monthView = UIView() + monthView.backgroundColor = UIColor(hexStr: "#57C7FF") + monthView.cornerRadius = 6 + monthView.addSubview(monthLab) + monthLab.layoutChain + .edgesVertical(2) + .edgesHorzontal(12) + view.addSubview(monthView) + monthView.layoutChain + .top(15) + .left(15) + + let totalLab = UILabel() + totalLab.text = "本周总计里程" + totalLab.font = .systemFont(ofSize: 10, weight: .medium) + totalLab.textColor = UIColor(hexStr: "#333333") + view.addSubview(totalLab) + totalLab.layoutChain + .leftToRightOfView(monthView, offset: 21) + .centerY(monthView) + view.addSubview(mileageLab) + mileageLab.layoutChain + .leftToRightOfView(totalLab, offset: 5) + .centerY(monthView) + + let highLab = UILabel() + highLab.text = "本周最高时速" + highLab.font = .systemFont(ofSize: 10, weight: .medium) + highLab.textColor = UIColor(hexStr: "#333333") + view.addSubview(highLab) + highLab.layoutChain + .leftToRightOfView(mileageLab, offset: 23) + .centerY(monthView) + view.addSubview(speedLab) + speedLab.layoutChain + .leftToRightOfView(highLab, offset: 5) + .centerY(monthView) + + view.addSubview(dateView) + dateView.layoutChain + .topToBottomOfView(monthView, offset: 10) + .edgesHorzontal() + + view.addSubview(scrollView) + scrollView.layoutChain + .topToBottomOfView(dateView) + .edges(excludingEdge: .top) + + return view + }() + + lazy var monthLab: UILabel = { + let label = UILabel() + label.text = " " + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = UIColor(hexStr: "#0F2846") + label.textAlignment = .center + return label + }() + + /// 里程 + lazy var mileageLab: UILabel = { + let label = UILabel() + label.text = "0km" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = UIColor(hexStr: "#16B3FF") + return label + }() + + /// 速度 + lazy var speedLab: UILabel = { + let label = UILabel() + label.text = "0km/h" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = UIColor(hexStr: "#16B3FF") + return label + }() + + // MARK: - 日期 + private var dateItems: [DateItem] = [] + let selectedDate = BehaviorRelay(value: Date()) + private let daysPerPage = 7 + + /// 当前选中的成员是否是自己 + var selectedMemberIsSelf = true { + didSet { updateDateSelectability() } + } + + struct DateItem { + let date: Date + let day: Int + let isToday: Bool + let isFuture: Bool + let isSelectable: Bool + } + + /// 根据 VIP 和成员关系计算最大可查询天数 + private var maxSelectableDays: Int { + switch AppContextManager.shared.vip { + case 1: return 0 // 非会员不可点 + case 2: return selectedMemberIsSelf ? 7 : 1 + case 3: return selectedMemberIsSelf ? 30 : 14 + default: return 0 + } + } + + /// 重新设置 dateItems 的 isSelectable + private func updateDateSelectability() { + let calendar = Calendar.current + let today = Date() + let maxDays = maxSelectableDays + dateItems = dateItems.map { item in + let daysAgo = calendar.dateComponents([.day], from: item.date, to: today).day ?? 0 + return DateItem(date: item.date, day: item.day, isToday: item.isToday, + isFuture: item.isFuture, + isSelectable: maxDays > 0 && !item.isFuture && daysAgo <= maxDays - 1) + } + dateCollectionView.reloadData() + } + + private func generateDateItems() { + let calendar = Calendar.current + let today = Date() + // 31 天前 → 今天 → 之后 3 天 = 35 天,正好 5 页 × 7 天 + let pastDays = 31 + let futureDays = 3 + let totalDays = pastDays + 1 + futureDays // 35 + + dateItems = (0.. today + let daysAgo = calendar.dateComponents([.day], from: date, to: today).day ?? 0 + return DateItem(date: date, day: day, + isToday: calendar.isDateInToday(date), + isFuture: isFuture, + isSelectable: maxSelectableDays > 0 && !isFuture && daysAgo <= maxSelectableDays - 1) + } + } + + /// 日期 + lazy var dateView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + generateDateItems() + + view.addSubview(datePreviousBtn) + view.addSubview(dateNextBtn) + view.addSubview(dateCollectionView) + + datePreviousBtn.layoutChain + .left(15) + .edgesVertical(10) + .width(20) + .height(20) + + dateNextBtn.layoutChain + .right(15) + .centerY() + .width(20) + .height(20) + + dateCollectionView.layoutChain + .leftToRightOfView(datePreviousBtn, offset: 5) + .rightToLeftOfView(dateNextBtn, offset: -5) + .edgesVertical() + + // 直接翻到最后一页(第 4 页,index 28 开始),今天在第 3 位 + DispatchQueue.main.async { + let page: CGFloat = 4 // 第 5 页,items 28-34 + let offset = page * CGFloat(self.daysPerPage) * self.dateItemWidth + self.dateCollectionView.contentOffset.x = offset + } + + return view + }() + + private var dateItemWidth: CGFloat { + let available = kScreenWidth - 15 - 20 - 5 - 5 - 20 - 15 + return floor(available / CGFloat(daysPerPage)) + } + + lazy var dateCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.itemSize = CGSize(width: dateItemWidth, height: 40) + layout.minimumLineSpacing = 0 + layout.scrollDirection = .horizontal + layout.sectionInset = .zero + + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.backgroundColor = .clear + cv.showsHorizontalScrollIndicator = false + cv.isPagingEnabled = true + cv.register(DateCell.self) + cv.dataSource = self + cv.delegate = self + return cv + }() + + lazy var datePreviousBtn: UIButton = { + let btn = UIButton() + btn.setImage(UIImage(named: "GroupMemberList/date_left"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 0) + return btn + }() + + lazy var dateNextBtn: UIButton = { + let btn = UIButton() + btn.setImage(UIImage(named: "GroupMemberList/date_right"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 15) + return btn + }() + + // MARK: - 驾驶分析 + lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.backgroundColor = .clear + view.showsVerticalScrollIndicator = false + view.bounces = false + view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin, right: 0) + + let contentView = UIView() + contentView.backgroundColor = .clear + view.addSubview(contentView) + contentView.layoutChain.edges().widthToView(view) + + contentView.addSubview(drivingAnalysisView) + drivingAnalysisView.layoutChain + .top(5) + .edgesHorzontal(15) + .height(249) + .bottom(20) + + return view + }() + + lazy var drivingAnalysisView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.cornerRadius = 10 + + let titleBg = UIImageView(image: UIImage(named: "GroupMemberList/title_bg")) + view.addSubview(titleBg) + + let titleLab = UILabel() + titleLab.text = "驾驶分析" + titleLab.font = .systemFont(ofSize: 16, weight: .medium) + view.addSubview(titleLab) + titleLab.layoutChain + .top(15) + .centerX() + + titleBg.layoutChain + .centerX() + .bottomToView(titleLab, offset: 5) + + view.addSubview(drivingEventCV) + drivingEventCV.layoutChain + .topToBottomOfView(titleBg, offset: 20) + .edgesHorzontal() + .bottom(20) + + return view + }() + + lazy var drivingEventCV: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.itemSize = CGSize(width: 66, height: 75) + layout.minimumLineSpacing = 15 + layout.scrollDirection = .vertical + layout.sectionInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.backgroundColor = .clear + cv.showsHorizontalScrollIndicator = false + cv.isScrollEnabled = false + cv.register(DrivingEventCell.self) + return cv + }() + + override func layoutSubviews() { + super.layoutSubviews() + updateArrowVisibility() + } + + override init(frame: CGRect) { + super.init(frame: .zero) + backgroundColor = .white + setupUI() + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UICollectionViewDataSource & Delegate (日期) +extension GroupMemberListView: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + dateItems.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: DateCell = collectionView.dequeueReusableCell(for: indexPath) + let item = dateItems[indexPath.row] + let isSel = Calendar.current.isDate(item.date, inSameDayAs: selectedDate.value) + cell.configure(item: item, isSelected: isSel) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard collectionView == dateCollectionView else { return } + let item = dateItems[indexPath.row] + guard item.isSelectable else { return } + let oldDate = selectedDate.value + selectedDate.accept(item.date) + if let oldRow = dateItems.firstIndex(where: { Calendar.current.isDate($0.date, inSameDayAs: oldDate) }), + let oldCell = dateCollectionView.cellForItem(at: IndexPath(row: oldRow, section: 0)) as? DateCell { + oldCell.configure(item: dateItems[oldRow], isSelected: false) + } + if let newCell = dateCollectionView.cellForItem(at: indexPath) as? DateCell { + newCell.configure(item: item, isSelected: true) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard scrollView == dateCollectionView else { return } + let targetX = targetContentOffset.pointee.x + let idx = round(targetX / dateItemWidth) + targetContentOffset.pointee.x = idx * dateItemWidth + } +} + +// MARK: - DateCell +class DateCell: UICollectionViewCell { + + private let bgView: UIView = { + let v = UIView() + v.backgroundColor = UIColor(hexStr: "#16B3FF") + v.cornerRadius = 11 + v.isHidden = true + return v + }() + + private let dayLab: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 14, weight: .medium) + l.textAlignment = .center + return l + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(bgView) + contentView.addSubview(dayLab) + bgView.layoutChain.centerX().centerY().width(36).height(22) + dayLab.layoutChain.centerX().centerY() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func configure(item: GroupMemberListView.DateItem, isSelected: Bool) { + dayLab.text = "\(item.day)" + let disabled = item.isFuture || !item.isSelectable + bgView.isHidden = !isSelected || disabled + dayLab.textColor = disabled ? UIColor(hexStr: "#D1D1D6") + : isSelected ? .white + : UIColor(hexStr: "#333333") + } +} + +// MARK: - GroupMemberListCell +class GroupMemberListCell: UICollectionViewCell { + + func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) { + avaterImgView.image = model.userIcon + vipIcon.image = model.vipIcon + nameLab.text = model.nick_name + nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846") + selectedBgView.isHidden = !isSelected + // 会员权益 + if AppContextManager.shared.vip > 1, model.is_online { + batteryInfoView.isHidden = model.battery.int == 0 + // 电量 16是电池图标宽度,右边有电池造型需要减去 + let batteryPercent = min(CGFloat(model.battery.int), 100) + batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0) + batteryLab.text = "\(model.battery)%" + } + } + + private func setupSubviews() { + contentView.addSubview(selectedBgView) + contentView.addSubview(avaterImgView) + contentView.addSubview(vipIcon) + contentView.addSubview(batteryInfoView) + batteryInfoView.addSubview(cornerView) + cornerView.addSubview(batteryView) + cornerView.addSubview(batteryIcon) + cornerView.addSubview(batteryLab) + contentView.addSubview(nameLab) + + setupLayout() + } + + private func setupLayout() { + selectedBgView.layoutChain.edges() + + avaterImgView.layoutChain + .top(11) + .edgesHorzontal(10) + .heightToWidth(1) + + batteryInfoView.layoutChain + .leftToView(avaterImgView) + .rightToView(avaterImgView) + .bottomToView(avaterImgView) + .height(12) + + cornerView.layoutChain.edges() + + batteryIcon.layoutChain + .left(7) + .centerY() + .width(16) + .height(8) + + batteryView.layoutChain + .topToView(batteryIcon) + .leftToView(batteryIcon, offset: -1) + .bottomToView(batteryIcon) + + batteryLab.layoutChain + .leftToRightOfView(batteryIcon, offset: 4) + .right(5) + .centerY() + + vipIcon.layoutChain + .topToView(avaterImgView, offset: -8) + .leftToView(avaterImgView, offset: -6) + .width(25) + .height(21) + + nameLab.layoutChain + .topToBottomOfView(batteryInfoView, offset: 4) + .edgesHorzontal() + + + } + + lazy var selectedBgView: UIImageView = { + let view = UIImageView(image: UIImage(named: "GroupMemberList/selected_bg")) + view.contentMode = .scaleAspectFill + view.isHidden = true + return view + }() + + lazy var avaterImgView: UIImageView = { + let view = UIImageView() + view.backgroundColor = .lightGray + view.contentMode = .scaleAspectFill + view.cornerRadius = 25 + return view + }() + + lazy var batteryInfoView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.1).cgColor + view.layer.shadowOffset = CGSize(width: 0, height: 2) + view.layer.shadowOpacity = 1 + view.layer.shadowRadius = 6 + view.isHidden = true + return view + }() + + lazy var cornerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.cornerRadius = 6 + return view + }() + + lazy var batteryView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#75E582") + return view + }() + + lazy var batteryIcon: UIImageView = { + let view = UIImageView() + view.backgroundColor = .clear + view.image = UIImage(named: "Home/battery") + return view + }() + + lazy var batteryLab: UILabel = { + let label = UILabel() + label.textColor = UIColor(hexStr: "#D4D4D4") + label.font = .systemFont(ofSize: 6, weight: .medium) + return label + }() + + lazy var vipIcon: UIImageView = { + let view = UIImageView() + return view + }() + + lazy var nameLab: UILabel = { + let label = UILabel() + label.textColor = UIColor(hexStr: "#0F2846") + label.font = .systemFont(ofSize: 12, weight: .medium) + label.textAlignment = .center + return label + }() + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - DrivingEventCell +class DrivingEventCell: UICollectionViewCell { + + func configure(_ item: DrivingEventItem) { + iconView.image = UIImage(named: item.iconName) + nameLab.text = "\(item.title)\n(\(item.count))" + } + + private let bgView: UIView = { + let v = UIView() + v.backgroundColor = UIColor(hexStr: "#F5FBFF") + v.cornerRadius = 8 + return v + }() + + lazy var iconView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFill + return view + }() + + lazy var nameLab: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 12, weight: .regular) + l.textAlignment = .center + l.numberOfLines = 0 + return l + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(bgView) + contentView.addSubview(iconView) + contentView.addSubview(nameLab) + + bgView.layoutChain + .edges(excludingEdge: .top) + .height(60) + + iconView.layoutChain + .top() + .centerX() + .width(32) + .heightToWidth(1) + + nameLab.layoutChain + .topToBottomOfView(iconView, offset: 8) + .edgesHorzontal() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } +} diff --git a/QuickLocation/Section/Home/GroupListPopView.swift b/QuickLocation/Section/Home/GroupListPopView.swift index d523674..a75f8a4 100644 --- a/QuickLocation/Section/Home/GroupListPopView.swift +++ b/QuickLocation/Section/Home/GroupListPopView.swift @@ -102,7 +102,7 @@ class GroupListPopView: UIView { tableView.showsVerticalScrollIndicator = false tableView.bounces = false tableView.isScrollEnabled = false - tableView.register(GroupListPopCell.self) + tableView.register(GroupListCell.self) tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10)) tableView.dataSource = self tableView.delegate = self @@ -273,7 +273,7 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: GroupListPopCell = tableView.dequeueReusableCell(for: indexPath) + let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(model: groupList[indexPath.row], isSelected: defaultGroupKey == groupList[indexPath.row].group_key) return cell @@ -286,7 +286,7 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate { // MARK: - GroupListPopCell -class GroupListPopCell: UITableViewCell { +class GroupListCell: UITableViewCell { func configure(model: GroupInfoModel, isSelected: Bool) { avaterImgView.image = model.groupIcon diff --git a/QuickLocation/Section/Home/GroupSchedule/GroupScheduleVC.swift b/QuickLocation/Section/Home/GroupSchedule/GroupScheduleVC.swift new file mode 100644 index 0000000..68aa91e --- /dev/null +++ b/QuickLocation/Section/Home/GroupSchedule/GroupScheduleVC.swift @@ -0,0 +1,226 @@ +// +// GroupScheduleVC.swift +// QuickLocation +// +// Created by 八条 on 2026/6/29. +// + +import UIKit +import RxSwift +import RxCocoa +import ObjectMapper +import SwiftyUserDefaults +import AMapNaviKit +import AMapSearchKit + +class GroupScheduleVC: BaseViewController { + + fileprivate var rootView: GroupScheduleView! + + override func loadView() { + rootView = GroupScheduleView(frame: UIScreen.main.bounds) + view = rootView + } + + private let groupKey: String + private var scheduleList: [ScheduleModel] = [] + private var selectedIndex: Int? + private let routeSearch = AMapSearchAPI() + private var routeOverlays: [MAPolyline] = [] + private var pointAnnotations: [MAPointAnnotation] = [] + + override func viewDidLoad() { + super.viewDidLoad() + rootView.tableView.dataSource = self + rootView.tableView.delegate = self + setupMap() + requestGroupScheduleList() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + if isMovingFromParent || isBeingDismissed { + rootView.cleanupMap() + } + } + + // MARK: - API + private func requestGroupScheduleList() { + DLToast.showLoading() + ItineraryService.groupScheduleList(groupKey: groupKey).subscribe(onNext: { response in + DLToast.dismiss() + guard response.list.count > 0 else { + DLToast.show(text: "暂无行程数据") { + AppRouter.shared.popOrDismiss() + } + return + } + self.scheduleList = response.list + self.rootView.tableView.reloadData() + }).disposed(by: disposeBag) + } + + // MARK: - Map + private func setupMap() { + rootView.mapView.delegate = self + 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) + } + } + } + + private func showScheduleOnMap(_ model: ScheduleModel) { + // 清除旧标注和路线 + for ann in pointAnnotations { rootView.mapView.removeAnnotation(ann) } + for ol in routeOverlays { rootView.mapView.remove(ol) } + pointAnnotations.removeAll() + routeOverlays.removeAll() + + // 添加带序号的标注 + let validPoints = model.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)" + rootView.mapView.addAnnotation(ann) + pointAnnotations.append(ann) + } + + // 缩放至所有点位 + if !pointAnnotations.isEmpty { + rootView.mapView.showAnnotations(pointAnnotations, animated: true) + } + + // 驾车路线 + guard validPoints.count >= 2 else { return } + let request = AMapDrivingRouteSearchRequest() + request.origin = AMapGeoPoint.location(withLatitude: CGFloat(validPoints[0].latitude!), + longitude: CGFloat(validPoints[0].longitude!)) + request.destination = AMapGeoPoint.location(withLatitude: CGFloat(validPoints.last!.latitude!), + longitude: CGFloat(validPoints.last!.longitude!)) + if validPoints.count > 2 { + var waypoints: [AMapGeoPoint] = [] + for i in 1.. UIImage? { + let size = CGSize(width: 20, height: 20) + 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: 11), .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 + } + + // MARK: - Init + init(groupKey: String) { + self.groupKey = groupKey + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UITableViewDataSource & Delegate +extension GroupScheduleVC: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + scheduleList.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: GroupScheduleCell = tableView.dequeueReusableCell(for: indexPath) + cell.configure(scheduleList[indexPath.row], isSelected: indexPath.row == selectedIndex) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + selectedIndex = indexPath.row + tableView.reloadData() + showScheduleOnMap(scheduleList[indexPath.row]) + rootView.dismissPanel() + } +} + +// MARK: - MAMapViewDelegate +extension GroupScheduleVC: MAMapViewDelegate { + func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { + guard !(annotation is MAUserLocation), let pointAnn = annotation as? MAPointAnnotation else { return nil } + if let num = Int(pointAnn.title ?? "") { + let id = "SchedulePin" + 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: -15) + return view + } + return nil + } + + 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 = 3 + r?.lineDashType = kMALineDashTypeSquare + return r + } + return nil + } +} + +// MARK: - AMapSearchDelegate +extension GroupScheduleVC: 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 error: \(error.localizedDescription)") + } +} diff --git a/QuickLocation/Section/Home/GroupSchedule/GroupScheduleView.swift b/QuickLocation/Section/Home/GroupSchedule/GroupScheduleView.swift new file mode 100644 index 0000000..5653315 --- /dev/null +++ b/QuickLocation/Section/Home/GroupSchedule/GroupScheduleView.swift @@ -0,0 +1,361 @@ +// +// GroupScheduleView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/29. +// + +import UIKit +import RxSwift +import RxCocoa +import AMapNaviKit + +class GroupScheduleView: UIView { + + var disposeBag = DisposeBag() + let selectedSchedule = PublishSubject() + + // MARK: - PopView 拖拽 + private var popTopConstraint: NSLayoutConstraint? + private var isLimitsSet = false + private let popDownHeight: CGFloat = 250 + private var popUpLimit: CGFloat = 0 + private var panStartTop: CGFloat = 0 + private var isSubCanScroll = false + + 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) + addSubview(bottomView) + bottomView.addSubview(lineView) + bottomView.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) + + mapView.layoutChain + .top() + .edgesHorzontal() + .bottom() + + // 底部面板 + bottomView.layoutChain + .edgesHorzontal() + .bottom() + .top(kScreenHeight - popDownHeight) + + lineView.layoutChain + .top(8) + .centerX() + .width(36) + .height(4) + + tableView.layoutChain + .topToBottomOfView(lineView, offset: 10) + .edgesHorzontal() + .bottom() + + popTopConstraint = bottomView.jh_constraint( + .top, toAttribute: .top, otherView: bottomView.superview, relation: .equal + ) + + let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) + pan.delegate = self + bottomView.addGestureRecognizer(pan) + } + + // MARK: - Pan Gesture + + @objc private func handlePan(_ pan: UIPanGestureRecognizer) { + guard isLimitsSet, let topConstraint = popTopConstraint else { return } + + switch pan.state { + case .began: + panStartTop = bottomView.frame.minY + + case .changed: + let newTop = panStartTop + pan.translation(in: self).y + let scrollOffset = tableView.contentOffset.y + + if isSubCanScroll { + if scrollOffset > 0 { return } + if pan.velocity(in: self).y > 0 || bottomView.frame.minY > popUpLimit + 1 { + isSubCanScroll = false + panStartTop = bottomView.frame.minY + } + } else { + if bottomView.frame.minY <= popUpLimit && newTop <= popUpLimit { + isSubCanScroll = true + panStartTop = bottomView.frame.minY + topConstraint.constant = popUpLimit + return + } + } + + let clamped = max(popUpLimit, min(kScreenHeight - popDownHeight, newTop)) + topConstraint.constant = clamped + + case .ended, .cancelled: + let velocity = pan.velocity(in: self) + let frameMinY = bottomView.frame.minY + let isNearUp = abs(frameMinY - popUpLimit) < abs(frameMinY - (kScreenHeight - popDownHeight)) + let target: CGFloat + if frameMinY <= popUpLimit + 5 { + target = isNearUp ? popUpLimit : (kScreenHeight - popDownHeight) + } else if abs(velocity.y) > 200 { + target = velocity.y < 0 ? popUpLimit : (kScreenHeight - popDownHeight) + } else { + target = isNearUp ? popUpLimit : (kScreenHeight - popDownHeight) + } + topConstraint.constant = target + isSubCanScroll = target == popUpLimit + + UIView.animate(withDuration: 0.3, delay: 0, + usingSpringWithDamping: 0.85, + initialSpringVelocity: abs(velocity.y) / 1000, + options: [.allowUserInteraction]) { + self.layoutIfNeeded() + } + + default: + break + } + } + + override func layoutSubviews() { + super.layoutSubviews() + if !isLimitsSet { + isLimitsSet = true + popUpLimit = navBarView.frame.maxY + } + } + + // MARK: - Views + + 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 + }() + + lazy var bottomView: UIView = { + let v = UIView() + v.backgroundColor = .white + v.layer.cornerRadius = 16 + v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + return v + }() + + lazy var lineView: UIView = { + let v = UIView() + v.backgroundColor = UIColor(hexStr: "#EBEBEB") + v.cornerRadius = 2 + return v + }() + + lazy var tableView: UITableView = { + let tv = UITableView(frame: .zero, style: .plain) + tv.backgroundColor = .clear + tv.separatorStyle = .none + tv.estimatedRowHeight = 137 + tv.rowHeight = UITableView.automaticDimension + tv.register(GroupScheduleCell.self) + return tv + }() + + /// 收起底部面板到初始位置 + func dismissPanel() { + guard let topConstraint = popTopConstraint else { return } + isSubCanScroll = false + topConstraint.constant = kScreenHeight - popDownHeight + UIView.animate(withDuration: 0.3) { self.layoutIfNeeded() } + } + + func cleanupMap() { + #if !targetEnvironment(simulator) + mapView?.delegate = nil + mapView?.removeFromSuperview() + mapView = nil + #endif + } + + override init(frame: CGRect) { + super.init(frame: .zero) + backgroundColor = .white + setupUI() + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UIGestureRecognizerDelegate +extension GroupScheduleView: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool { + return true + } +} + +// MARK: - GroupScheduleCell +class GroupScheduleCell: UITableViewCell { + + func configure(_ model: ScheduleModel, isSelected: Bool) { + dateLab.text = "行程时间:\(getDateInterval2String(date: "\(model.timestamp/1000)", dateFormat: "yyyy年MM月dd日"))" + iconView.image = model.userIcon + nameLab.text = "\(model.nick_name) 的行程路线" + selectedBgView.isHidden = !isSelected + } + + private func setupSubviews() { + contentView.addSubview(bgView) + bgView.addSubview(selectedBgView) + bgView.addSubview(dateLab) + bgView.addSubview(detailView) + + bgView.layoutChain + .top() + .edgesHorzontal(15) + .height(137) + .bottom(12) + + selectedBgView.layoutChain.edges() + + dateLab.layoutChain + .top(9) + .left(15) + + detailView.layoutChain + .topToBottomOfView(dateLab, offset: 8) + .edges(all: 15, excludingEdge: .top) + } + + lazy var bgView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#EEFAFF") + view.cornerRadius = 10 + return view + }() + + lazy var selectedBgView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#C0EAFF") + view.borderWidth = 1 + view.borderColor = UIColor(hexStr: "#16B3FF") + view.cornerRadius = 10 + view.isHidden = true + return view + }() + + lazy var dateLab: UILabel = { + let label = UILabel() + label.text = " " + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = UIColor(hexStr: "#0F2846") + return label + }() + + lazy var detailView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.cornerRadius = 10 + + view.addSubview(iconView) + iconView.layoutChain + .left(15) + .centerY() + .width(50) + .heightToWidth(1) + + view.addSubview(nameLab) + nameLab.layoutChain + .leftToRightOfView(iconView, offset: 14) + .right(15, relation: .greaterThanOrEqual) + .centerY(iconView) + + return view + }() + + lazy var iconView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFill + return view + }() + + lazy var nameLab: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = UIColor(hexStr: "#0F2846") + return label + }() + + override init(style: CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + setupSubviews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/QuickLocation/Section/Home/HomeView.swift b/QuickLocation/Section/Home/HomeView.swift index 5d6598d..da51d82 100644 --- a/QuickLocation/Section/Home/HomeView.swift +++ b/QuickLocation/Section/Home/HomeView.swift @@ -182,26 +182,9 @@ class HomeView: UIView { toolsView.layoutChain .left(15) - .bottom(kScreenHeight / 2 - 58) .width(40) + .centerY(self, offset: -50) - bubbleView.layoutChain.top(15).height(58) - - signInView.layoutChain.height(58) - - sosView.layoutChain - .height(56) - - sosIcon.layoutChain - .top() - .centerX() - .width(28) - .height(28) - - sosLab.layoutChain - .topToBottomOfView(sosIcon, offset: 4) - .edgesHorzontal() - searchLottieView.layoutChain .centerY(toolsView) .right() @@ -209,7 +192,6 @@ class HomeView: UIView { .height(100) locationView.layoutChain - .topToBottomOfView(searchLottieView, offset: 8) .right(15) .bottomToView(toolsView) .width(40) @@ -647,10 +629,10 @@ class HomeView: UIView { // MARK: - 侧边工具栏 lazy var toolsView: UIStackView = { - let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView]) + let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView, scheduleView]) view.axis = .vertical - view.distribution = .equalSpacing - view.alignment = .center + view.distribution = .fill + view.alignment = .fill view.backgroundColor = .black.withAlphaComponent(0.5) view.cornerRadius = 20 return view @@ -663,7 +645,7 @@ class HomeView: UIView { view.addSubview(bubbleIcon) bubbleIcon.layoutChain - .top() + .top(10) .centerX() .width(28) .height(28) @@ -681,6 +663,7 @@ class HomeView: UIView { .height(2) .centerX() .bottom(7) + lineView.layoutChain.topToBottomOfView(bubbleLab, offset: 4) return view }() @@ -727,6 +710,7 @@ class HomeView: UIView { .height(2) .centerX() .bottom(7) + lineView.layoutChain.topToBottomOfView(signInLab, offset: 4) return view }() @@ -753,6 +737,27 @@ class HomeView: UIView { view.backgroundColor = .clear view.addSubview(sosIcon) view.addSubview(sosLab) + + sosIcon.layoutChain + .top() + .centerX() + .width(28) + .height(28) + + sosLab.layoutChain + .topToBottomOfView(sosIcon, offset: 4) + .edgesHorzontal() + + let lineView = UIView() + lineView.backgroundColor = .white + view.addSubview(lineView) + lineView.layoutChain + .width(12) + .height(2) + .centerX() + .bottom(7) + lineView.layoutChain.topToBottomOfView(sosLab, offset: 4) + return view }() @@ -772,6 +777,34 @@ class HomeView: UIView { label.textAlignment = .center return label }() + + // 行程 + lazy var scheduleView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + let icon = UIImageView(image: UIImage(named: "Home/schedule")) + icon.contentMode = .scaleAspectFill + view.addSubview(icon) + icon.layoutChain + .top() + .centerX() + .width(28) + .height(28) + + let label = UILabel() + label.text = "行程" + label.font = .systemFont(ofSize: 10, weight: .medium) + label.textColor = .white + label.textAlignment = .center + view.addSubview(label) + label.layoutChain + .topToBottomOfView(icon, offset: 4) + .edgesHorzontal() + label.layoutChain.bottom(10) + + return view + }() // MARK: - 查位置 lazy var searchLottieView: LottieAnimationView = { diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index 2e8837e..c48a15c 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -87,7 +87,7 @@ class HomeViewController: BaseViewController { private func startLocationTimer() { locationTimer?.invalidate() - locationTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in + locationTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] _ in guard let self = self, let loc = self.lastLocation else { return } let coord = loc.coordinate @@ -146,6 +146,12 @@ class HomeViewController: BaseViewController { AppRouter.push(vc) }.disposed(by: disposeBag) + // 行程 + rootView.scheduleView.rx.tapGesture.subscribe { _ in + guard let model = self.viewModel.groupModel else { return } + AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key]) + }.disposed(by: disposeBag) + // 顶部圈子 rootView.groupView.rx.tapGesture.subscribe { _ in guard let groupModel = self.viewModel.groupModel else { return } diff --git a/QuickLocation/Section/Home/HomeViewModel.swift b/QuickLocation/Section/Home/HomeViewModel.swift index c5dd1c0..49fb0f3 100644 --- a/QuickLocation/Section/Home/HomeViewModel.swift +++ b/QuickLocation/Section/Home/HomeViewModel.swift @@ -88,9 +88,11 @@ class HomeViewModel { var memberList: [GroupMemberModel] = [] { didSet { + // 排列顺序 圈主 -> 自己 -> 在线 var tempList = memberList - tempList.moveToFirst { $0.user_id == AppContextManager.shared.userId } - tempList.moveToFirst { isGroupOwn(id: $0.user_id) } + tempList.moveToFirst { $0.is_online == true } // 在线 + tempList.moveToFirst { $0.user_id == AppContextManager.shared.userId } // 自己 + tempList.moveToFirst { isGroupOwn(id: $0.user_id) } // 圈主 memberList = tempList sectionedItems.onNext(memberList.mapSection()) } diff --git a/QuickLocation/Section/Home/SearchLocation/GroupChooseView.swift b/QuickLocation/Section/Home/SearchLocation/GroupChooseView.swift new file mode 100644 index 0000000..d4fe1af --- /dev/null +++ b/QuickLocation/Section/Home/SearchLocation/GroupChooseView.swift @@ -0,0 +1,240 @@ +// +// GroupChooseView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/27. +// + +import UIKit +import ObjectMapper + +class GroupChooseView: UIView { + + private static let shared = GroupChooseView(frame: CGRect(origin: .zero, size: kScreenSize)) + + private var groupModel: GroupModel? { + didSet { + guard let model = groupModel else { return } + groupList = model.groups + let count = min(model.groups.count, 5) + tableView.isScrollEnabled = model.groups.count > 5 + tableView.layoutChain.height(CGFloat(count * 68), relation: .greaterThanOrEqual) + } + } + + private var groupInfo: [String: Any] { + guard let model = groupModel, + let groupInfoModel = model.groups.first(where: { $0.group_key == defaultGroupKey }) else { return [:] } + return groupInfoModel.toJSON() + } + + private var defaultGroupKey: String = "" { + didSet { tableView.reloadData() } + } + + private var groupList: [GroupInfoModel] = [] { + didSet { tableView.reloadData() } + } + + private var completion: ((String?) -> Void)? + + @objc func tap() { completion?(nil) } + + @objc func cancelAction(button: UIButton) { completion?(nil) } + + @objc func inviteAction(button: UIButton) { + completion?(nil) + AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.groupInfo]) + } + + private lazy var bgView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.5) + view.clipsToBounds = true + return view + }() + + lazy var infoView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 10 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + return view + }() + + lazy var titleLab: UILabel = { + let label = UILabel() + label.text = "选择圈子" + label.font = .systemFont(ofSize: 16, weight: .bold) + label.textColor = ThemeManager.shared.color.titleAuxColor + return label + }() + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .grouped) + tableView.backgroundColor = .white + tableView.separatorStyle = .none + tableView.estimatedRowHeight = 68 + tableView.showsVerticalScrollIndicator = false + tableView.bounces = false + tableView.isScrollEnabled = false + tableView.register(GroupListCell.self) + tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10)) + tableView.dataSource = self + tableView.delegate = self + return tableView + }() + + lazy var cancelBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setTitle("取消", for: .normal) + btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium) + btn.backgroundColor = .white + btn.borderWidth = 1 + btn.borderColor = UIColor(hexStr: "#16B3FF") + btn.cornerRadius = 20 + btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside) + return btn + }() + + lazy var inviteBtn: 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 = 20 + btn.addTarget(self, action: #selector(inviteAction), for: .touchUpInside) + return btn + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + addSubview(bgView) + bgView.addSubview(infoView) + infoView.addSubview(titleLab) + infoView.addSubview(tableView) + infoView.addSubview(cancelBtn) + infoView.addSubview(inviteBtn) + + // bgView 全屏 + bgView.layoutChain.edges() + + // infoView 横向撑满,底部对齐,高度由内容决定 + infoView.layoutChain + .edgesHorzontal() + .bottom() + + titleLab.layoutChain + .top(16) + .left(12) + + cancelBtn.layoutChain + .left(15) + .bottom(20 + kSafeBottomMargin) + .widthToView(inviteBtn) + .height(44) + + inviteBtn.layoutChain + .leftToRightOfView(cancelBtn, offset: 7) + .right(15) + .bottomToView(cancelBtn) + .height(44) + .widthToView(inviteBtn) + + tableView.layoutChain + .topToBottomOfView(titleLab, offset: 10) + .edgesHorzontal() + .bottomToTopOfView(cancelBtn, offset: -17) + .height(78, relation: .greaterThanOrEqual) + + let tap = UITapGestureRecognizer(target: self, action: #selector(tap)) + tap.delegate = self + addGestureRecognizer(tap) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + } +} + +// MARK: - Public +extension GroupChooseView { + + /// 显示选择弹窗(底部弹出) + static func show(groupModel: GroupModel, + completion: @escaping ((String?) -> Void)) { + guard let superView = kKeyWindow else { return } + + let shared = GroupChooseView.shared + if shared.superview != nil { + shared.removeFromSuperview() + } + + shared.groupModel = groupModel + shared.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight) + superView.addSubview(shared) + superView.bringSubviewToFront(shared) + + shared.bgView.alpha = 0 + shared.infoView.transform = CGAffineTransform(translationX: 0, y: shared.infoView.frame.maxY + 100) + + shared.completion = { text in + completion(text) + GroupChooseView.dismiss() + } + + shared.layoutIfNeeded() + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) { + shared.bgView.alpha = 1 + shared.infoView.transform = .identity + } + } + + /// 关闭(底部滑出) + static func dismiss() { + guard GroupChooseView.shared.superview != nil else { return } + let shared = GroupChooseView.shared + UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn) { + shared.bgView.alpha = 0 + shared.infoView.transform = CGAffineTransform(translationX: 0, y: shared.infoView.frame.maxY + 100) + } completion: { _ in + shared.removeFromSuperview() + shared.infoView.transform = .identity + } + } +} + +// MARK: - UIGestureRecognizerDelegate +extension GroupChooseView: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if let view = touch.view, !(view == self || view == bgView) { + return false + } + return true + } +} + +// MARK: - UITableViewDataSource & UITableViewDelegate +extension GroupChooseView: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + groupList.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath) + cell.configure(model: groupList[indexPath.row], + isSelected: defaultGroupKey == groupList[indexPath.row].group_key) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + defaultGroupKey = groupList[indexPath.row].group_key + } +} diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift index 6bded03..50a983d 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift @@ -22,17 +22,18 @@ class SearchLocationResultVC: BaseViewController { private let phone: String private let code: Int private let memberData: [String : Any] - + private var groupModel: GroupModel? + override func viewDidLoad() { super.viewDidLoad() - + rootView.unlockVipView.isHidden = AppContextManager.shared.vip > 1 AppContextManager.shared.vip > 1 ? nil : rootView.unlockVipLottieView.play() - + reactiveAction() requestPhoneArea() } - + private func reactiveAction() { rootView.successBtn.rx.tap.subscribe(onNext: { _ in if self.code == 0 { // 去查看 @@ -41,8 +42,12 @@ class SearchLocationResultVC: BaseViewController { userInfo: self.memberData) AppRouter.shared.popToRoot() } - else { // 邀请Ta - + else { // 邀请Ta → 弹出圈子选择 + guard let model = self.groupModel else { + self.requestGroupInfo(showPicker: true) + return + } + GroupChooseView.show(groupModel: model) { _ in } } }).disposed(by: disposeBag) } @@ -103,10 +108,13 @@ class SearchLocationResultVC: BaseViewController { } // MARK: - 圈子列表 - private func requestGroupInfo() { - GroupService.groupInfo().subscribe { response in - guard let model = response.model else { return } - + private func requestGroupInfo(showPicker: Bool = false) { + GroupService.groupInfo().subscribe { [weak self] response in + guard let self = self, let model = response.model else { return } + self.groupModel = model + if showPicker { + GroupChooseView.show(groupModel: model) { _ in } + } }.disposed(by: disposeBag) } diff --git a/QuickLocation/Section/Login/LoginViewModel.swift b/QuickLocation/Section/Login/LoginViewModel.swift index 6418bd0..e23fe43 100644 --- a/QuickLocation/Section/Login/LoginViewModel.swift +++ b/QuickLocation/Section/Login/LoginViewModel.swift @@ -61,8 +61,4 @@ final class LoginViewModel: BaseViewModel { func performAppleLogin() { // TODO: Integrate Sign in with Apple } - - func performQQLogin() { - // TODO: Integrate QQ SDK - } } diff --git a/QuickLocation/Section/Schedule/ScheduleModel.swift b/QuickLocation/Section/Schedule/ScheduleModel.swift index e6edfad..2563fe0 100644 --- a/QuickLocation/Section/Schedule/ScheduleModel.swift +++ b/QuickLocation/Section/Schedule/ScheduleModel.swift @@ -43,6 +43,24 @@ struct ScheduleListResponse: BaseModelProtocol, ListModelType { } } +/// 圈子行程列表 +struct groupScheduleListResponse: BaseModelProtocol { + // 状态码 + var code: String? + // 消息 + var message: String? + // + var list: [ScheduleModel] = [] + + init?(map: Map) {} + + mutating func mapping(map: Map) { + code <- map["code"] + message <- map["msg"] + list <- map["data"] + } +} + struct ScheduleModel: Mappable, Equatable { var uuid: String = UUID().uuidString /// 行程id diff --git a/QuickLocation/Service/ItineraryService.swift b/QuickLocation/Service/ItineraryService.swift index 7fb96ab..c7c08b9 100644 --- a/QuickLocation/Service/ItineraryService.swift +++ b/QuickLocation/Service/ItineraryService.swift @@ -30,6 +30,18 @@ struct ItineraryService { } } + /// 查询圈子行程列表 + static func groupScheduleList(groupKey: String) -> Observable { + let api = ItineraryAPI.query(follow: false, + own: false, + history: false, + group_key: groupKey, + page: -1).multiTarget + return APIProvider.request(token: api) + .map(groupScheduleListResponse.self) + .asObservable() + } + /// 查询行程关注人 /// - Parameters: /// - id: 行程ID