|
|
@ -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 = "<group>"; };
|
||||
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
|
||||
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultView.swift; sourceTree = "<group>"; };
|
||||
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = "<group>"; };
|
||||
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = "<group>"; };
|
||||
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
|
||||
30B74B422FF2438800F6744D /* GroupMemberListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListView.swift; sourceTree = "<group>"; };
|
||||
30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVM.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>"; };
|
||||
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -1310,6 +1322,25 @@
|
|||
path = SearchLocation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30B74B382FF2105C00F6744D /* GroupSchedule */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */,
|
||||
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */,
|
||||
);
|
||||
path = GroupSchedule;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30B74B3F2FF2435200F6744D /* GroupMemberList */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */,
|
||||
30B74B422FF2438800F6744D /* GroupMemberListView.swift */,
|
||||
30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */,
|
||||
);
|
||||
path = GroupMemberList;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@2x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/4.imageset/Group_1914@3x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@2x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/6.imageset/Group_408@3x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
22
QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/date_left.imageset/Group_1900@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
22
QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@2x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/date_right.imageset/Group_1900@3x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
22
QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 438 B |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/member_left.imageset/Vector@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 532 B |
22
QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@2x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 439 B |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/member_right.imageset/Vector@3x(1).png
vendored
Normal file
|
After Width: | Height: | Size: 493 B |
22
QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
22
QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/title_bg.imageset/Group_1989@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -58,7 +58,8 @@ class AppContextManager: NSObject {
|
|||
var sex: Int {
|
||||
account?.sex ?? -1
|
||||
}
|
||||
/// VIP
|
||||
|
||||
/// 会员 1:非会员 2:普通会员 3:终身会员
|
||||
var vip: Int {
|
||||
account?.vip ?? 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<GroupMemberListSectionModel> = {
|
||||
RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> { [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<DrivingEventSection> = {
|
||||
RxCollectionViewSectionedReloadDataSource<DrivingEventSection> { _, 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, DrivingEventItem>
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Date>(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..<totalDays).map { i in
|
||||
let offset = i - pastDays
|
||||
let date = calendar.date(byAdding: .day, value: offset, to: today) ?? today
|
||||
let day = calendar.component(.day, from: date)
|
||||
let isFuture = date > 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<CGPoint>) {
|
||||
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") }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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..<validPoints.count - 1 {
|
||||
if let wp = AMapGeoPoint.location(withLatitude: CGFloat(validPoints[i].latitude ?? 0),
|
||||
longitude: CGFloat(validPoints[i].longitude ?? 0)) {
|
||||
waypoints.append(wp)
|
||||
}
|
||||
}
|
||||
request.waypoints = waypoints
|
||||
}
|
||||
request.strategy = 0
|
||||
routeSearch?.aMapDrivingRouteSearch(request)
|
||||
}
|
||||
|
||||
private static func numberImage(_ num: Int) -> 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)")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ScheduleModel>()
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,4 @@ final class LoginViewModel: BaseViewModel {
|
|||
func performAppleLogin() {
|
||||
// TODO: Integrate Sign in with Apple
|
||||
}
|
||||
|
||||
func performQQLogin() {
|
||||
// TODO: Integrate QQ SDK
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -30,6 +30,18 @@ struct ItineraryService {
|
|||
}
|
||||
}
|
||||
|
||||
/// 查询圈子行程列表
|
||||
static func groupScheduleList(groupKey: String) -> Observable<groupScheduleListResponse> {
|
||||
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
|
||||
|
|
|
|||