|
|
@ -194,6 +194,11 @@
|
||||||
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; };
|
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; };
|
||||||
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
|
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
|
||||||
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.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 */; };
|
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */; };
|
||||||
30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; };
|
30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; };
|
||||||
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */; };
|
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */; };
|
||||||
|
|
@ -476,6 +481,11 @@
|
||||||
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; };
|
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; };
|
||||||
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = "<group>"; };
|
||||||
30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = "<group>"; };
|
30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = "<group>"; };
|
||||||
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = "<group>"; };
|
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -948,6 +958,7 @@
|
||||||
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
|
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
|
||||||
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
|
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
|
||||||
307073E42FD18A20004C37CC /* GroupChat */,
|
307073E42FD18A20004C37CC /* GroupChat */,
|
||||||
|
30B74B3F2FF2435200F6744D /* GroupMemberList */,
|
||||||
30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
|
30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
|
||||||
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */,
|
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */,
|
||||||
30C4C0172FDBF066009215C1 /* RemoveMember */,
|
30C4C0172FDBF066009215C1 /* RemoveMember */,
|
||||||
|
|
@ -972,6 +983,7 @@
|
||||||
30A87A5C2FEE711C0095E7C6 /* Bubble */,
|
30A87A5C2FEE711C0095E7C6 /* Bubble */,
|
||||||
30CCDE4F2FE2782700F5214A /* SignIn */,
|
30CCDE4F2FE2782700F5214A /* SignIn */,
|
||||||
30CCDE562FE39F6B00F5214A /* SOS */,
|
30CCDE562FE39F6B00F5214A /* SOS */,
|
||||||
|
30B74B382FF2105C00F6744D /* GroupSchedule */,
|
||||||
);
|
);
|
||||||
path = Home;
|
path = Home;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -1310,6 +1322,25 @@
|
||||||
path = SearchLocation;
|
path = SearchLocation;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -1722,6 +1753,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */,
|
||||||
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */,
|
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */,
|
||||||
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */,
|
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */,
|
||||||
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */,
|
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */,
|
||||||
|
|
@ -1786,6 +1818,7 @@
|
||||||
305A76A72FCA8C7000227D26 /* NSAttributedString+Extension.swift in Sources */,
|
305A76A72FCA8C7000227D26 /* NSAttributedString+Extension.swift in Sources */,
|
||||||
30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */,
|
30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */,
|
||||||
305A76A82FCA8C7000227D26 /* ObjectMapper+Extension.swift in Sources */,
|
305A76A82FCA8C7000227D26 /* ObjectMapper+Extension.swift in Sources */,
|
||||||
|
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */,
|
||||||
305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */,
|
305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */,
|
||||||
305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */,
|
305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */,
|
||||||
305A76AB2FCA8C7000227D26 /* ScaleType.swift in Sources */,
|
305A76AB2FCA8C7000227D26 /* ScaleType.swift in Sources */,
|
||||||
|
|
@ -1938,6 +1971,7 @@
|
||||||
305A76FB2FCA8C7000227D26 /* EmptyDataSet.swift in Sources */,
|
305A76FB2FCA8C7000227D26 /* EmptyDataSet.swift in Sources */,
|
||||||
30BF300E2FED09CC00D9CB52 /* ScheduleDetailVC.swift in Sources */,
|
30BF300E2FED09CC00D9CB52 /* ScheduleDetailVC.swift in Sources */,
|
||||||
305A76FC2FCA8C7000227D26 /* EmptyDataSetDelegate.swift in Sources */,
|
305A76FC2FCA8C7000227D26 /* EmptyDataSetDelegate.swift in Sources */,
|
||||||
|
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */,
|
||||||
305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */,
|
305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */,
|
||||||
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */,
|
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */,
|
||||||
30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */,
|
30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */,
|
||||||
|
|
@ -1953,6 +1987,7 @@
|
||||||
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */,
|
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */,
|
||||||
30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */,
|
30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */,
|
||||||
305A77062FCA8C7000227D26 /* MXScrollViewController.m in Sources */,
|
305A77062FCA8C7000227D26 /* MXScrollViewController.m in Sources */,
|
||||||
|
30B74B432FF2438800F6744D /* GroupMemberListView.swift in Sources */,
|
||||||
305A77072FCA8C7000227D26 /* Helper.swift in Sources */,
|
305A77072FCA8C7000227D26 /* Helper.swift in Sources */,
|
||||||
30BF30102FED0C8E00D9CB52 /* ScheduleDetailVM.swift in Sources */,
|
30BF30102FED0C8E00D9CB52 /* ScheduleDetailVM.swift in Sources */,
|
||||||
30DA36BD2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift in Sources */,
|
30DA36BD2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift in Sources */,
|
||||||
|
|
@ -1971,6 +2006,7 @@
|
||||||
305A770F2FCA8C7000227D26 /* DLCustomPopVC.swift in Sources */,
|
305A770F2FCA8C7000227D26 /* DLCustomPopVC.swift in Sources */,
|
||||||
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */,
|
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */,
|
||||||
30CCDE532FE2786600F5214A /* SignInView.swift in Sources */,
|
30CCDE532FE2786600F5214A /* SignInView.swift in Sources */,
|
||||||
|
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */,
|
||||||
305A77102FCA8C7000227D26 /* DLSheetPopVC.swift in Sources */,
|
305A77102FCA8C7000227D26 /* DLSheetPopVC.swift in Sources */,
|
||||||
30EFF3D32FDA69F400EB35D4 /* AvatarIconListView.swift in Sources */,
|
30EFF3D32FDA69F400EB35D4 /* AvatarIconListView.swift in Sources */,
|
||||||
30EFF3B72FD8F86200EB35D4 /* ReviewMemberListVM.swift in Sources */,
|
30EFF3B72FD8F86200EB35D4 /* ReviewMemberListVM.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,10 @@ extension ItineraryAPI: MultiTargetProtocol {
|
||||||
if !group_key.isEmpty {
|
if !group_key.isEmpty {
|
||||||
params["group_key"] = group_key
|
params["group_key"] = group_key
|
||||||
}
|
}
|
||||||
params["page"] = page
|
if page != -1 {
|
||||||
params["limit"] = 20
|
params["page"] = page
|
||||||
|
params["limit"] = 20
|
||||||
|
}
|
||||||
return .requestParameters(parameters: params, encoding: URLEncoding())
|
return .requestParameters(parameters: params, encoding: URLEncoding())
|
||||||
|
|
||||||
case let .queryFollowList(id):
|
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 {
|
var sex: Int {
|
||||||
account?.sex ?? -1
|
account?.sex ?? -1
|
||||||
}
|
}
|
||||||
/// VIP
|
|
||||||
|
/// 会员 1:非会员 2:普通会员 3:终身会员
|
||||||
var vip: Int {
|
var vip: Int {
|
||||||
account?.vip ?? 1
|
account?.vip ?? 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ enum Route: String {
|
||||||
case searchLocation = "searchLocation"
|
case searchLocation = "searchLocation"
|
||||||
/// 查找位置结果
|
/// 查找位置结果
|
||||||
case searchLocationResult = "searchLocationResult"
|
case searchLocationResult = "searchLocationResult"
|
||||||
|
/// 圈子行程列表
|
||||||
|
case groupSchedule = "groupSchedule"
|
||||||
|
/// 圈子成员列表
|
||||||
|
case groupMemberList = "groupMemberList"
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Route: RouterTarget {
|
extension Route: RouterTarget {
|
||||||
|
|
@ -323,6 +327,17 @@ extension AppRouter: AppRouterProtocol {
|
||||||
code: parameters["code"].safeInt,
|
code: parameters["code"].safeInt,
|
||||||
memberData: parameters["memberData"].safeDictionary as! [String : Any])
|
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)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 审核
|
||||||
rootView.reviewBtn.rx.tap.subscribe(onNext: { _ in
|
rootView.reviewBtn.rx.tap.subscribe(onNext: { _ in
|
||||||
AppRouter.push(Route.reviewMemberList, userInfo: ["groupId": self.viewModel.groupId])
|
AppRouter.push(Route.reviewMemberList, userInfo: ["groupId": self.viewModel.groupId])
|
||||||
}).disposed(by: disposeBag)
|
}).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
|
rootView.voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
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.showsVerticalScrollIndicator = false
|
||||||
tableView.bounces = false
|
tableView.bounces = false
|
||||||
tableView.isScrollEnabled = false
|
tableView.isScrollEnabled = false
|
||||||
tableView.register(GroupListPopCell.self)
|
tableView.register(GroupListCell.self)
|
||||||
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
|
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
|
||||||
tableView.dataSource = self
|
tableView.dataSource = self
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
|
|
@ -273,7 +273,7 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
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],
|
cell.configure(model: groupList[indexPath.row],
|
||||||
isSelected: defaultGroupKey == groupList[indexPath.row].group_key)
|
isSelected: defaultGroupKey == groupList[indexPath.row].group_key)
|
||||||
return cell
|
return cell
|
||||||
|
|
@ -286,7 +286,7 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
|
||||||
|
|
||||||
// MARK: - GroupListPopCell
|
// MARK: - GroupListPopCell
|
||||||
class GroupListPopCell: UITableViewCell {
|
class GroupListCell: UITableViewCell {
|
||||||
|
|
||||||
func configure(model: GroupInfoModel, isSelected: Bool) {
|
func configure(model: GroupInfoModel, isSelected: Bool) {
|
||||||
avaterImgView.image = model.groupIcon
|
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
|
toolsView.layoutChain
|
||||||
.left(15)
|
.left(15)
|
||||||
.bottom(kScreenHeight / 2 - 58)
|
|
||||||
.width(40)
|
.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
|
searchLottieView.layoutChain
|
||||||
.centerY(toolsView)
|
.centerY(toolsView)
|
||||||
.right()
|
.right()
|
||||||
|
|
@ -209,7 +192,6 @@ class HomeView: UIView {
|
||||||
.height(100)
|
.height(100)
|
||||||
|
|
||||||
locationView.layoutChain
|
locationView.layoutChain
|
||||||
.topToBottomOfView(searchLottieView, offset: 8)
|
|
||||||
.right(15)
|
.right(15)
|
||||||
.bottomToView(toolsView)
|
.bottomToView(toolsView)
|
||||||
.width(40)
|
.width(40)
|
||||||
|
|
@ -647,10 +629,10 @@ class HomeView: UIView {
|
||||||
|
|
||||||
// MARK: - 侧边工具栏
|
// MARK: - 侧边工具栏
|
||||||
lazy var toolsView: UIStackView = {
|
lazy var toolsView: UIStackView = {
|
||||||
let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView])
|
let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView, scheduleView])
|
||||||
view.axis = .vertical
|
view.axis = .vertical
|
||||||
view.distribution = .equalSpacing
|
view.distribution = .fill
|
||||||
view.alignment = .center
|
view.alignment = .fill
|
||||||
view.backgroundColor = .black.withAlphaComponent(0.5)
|
view.backgroundColor = .black.withAlphaComponent(0.5)
|
||||||
view.cornerRadius = 20
|
view.cornerRadius = 20
|
||||||
return view
|
return view
|
||||||
|
|
@ -663,7 +645,7 @@ class HomeView: UIView {
|
||||||
|
|
||||||
view.addSubview(bubbleIcon)
|
view.addSubview(bubbleIcon)
|
||||||
bubbleIcon.layoutChain
|
bubbleIcon.layoutChain
|
||||||
.top()
|
.top(10)
|
||||||
.centerX()
|
.centerX()
|
||||||
.width(28)
|
.width(28)
|
||||||
.height(28)
|
.height(28)
|
||||||
|
|
@ -681,6 +663,7 @@ class HomeView: UIView {
|
||||||
.height(2)
|
.height(2)
|
||||||
.centerX()
|
.centerX()
|
||||||
.bottom(7)
|
.bottom(7)
|
||||||
|
lineView.layoutChain.topToBottomOfView(bubbleLab, offset: 4)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
@ -727,6 +710,7 @@ class HomeView: UIView {
|
||||||
.height(2)
|
.height(2)
|
||||||
.centerX()
|
.centerX()
|
||||||
.bottom(7)
|
.bottom(7)
|
||||||
|
lineView.layoutChain.topToBottomOfView(signInLab, offset: 4)
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -753,6 +737,27 @@ class HomeView: UIView {
|
||||||
view.backgroundColor = .clear
|
view.backgroundColor = .clear
|
||||||
view.addSubview(sosIcon)
|
view.addSubview(sosIcon)
|
||||||
view.addSubview(sosLab)
|
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
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -772,6 +777,34 @@ class HomeView: UIView {
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
return label
|
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: - 查位置
|
// MARK: - 查位置
|
||||||
lazy var searchLottieView: LottieAnimationView = {
|
lazy var searchLottieView: LottieAnimationView = {
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class HomeViewController: BaseViewController {
|
||||||
|
|
||||||
private func startLocationTimer() {
|
private func startLocationTimer() {
|
||||||
locationTimer?.invalidate()
|
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 }
|
guard let self = self, let loc = self.lastLocation else { return }
|
||||||
let coord = loc.coordinate
|
let coord = loc.coordinate
|
||||||
|
|
||||||
|
|
@ -146,6 +146,12 @@ class HomeViewController: BaseViewController {
|
||||||
AppRouter.push(vc)
|
AppRouter.push(vc)
|
||||||
}.disposed(by: disposeBag)
|
}.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
|
rootView.groupView.rx.tapGesture.subscribe { _ in
|
||||||
guard let groupModel = self.viewModel.groupModel else { return }
|
guard let groupModel = self.viewModel.groupModel else { return }
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,11 @@ class HomeViewModel {
|
||||||
|
|
||||||
var memberList: [GroupMemberModel] = [] {
|
var memberList: [GroupMemberModel] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
|
// 排列顺序 圈主 -> 自己 -> 在线
|
||||||
var tempList = memberList
|
var tempList = memberList
|
||||||
tempList.moveToFirst { $0.user_id == AppContextManager.shared.userId }
|
tempList.moveToFirst { $0.is_online == true } // 在线
|
||||||
tempList.moveToFirst { isGroupOwn(id: $0.user_id) }
|
tempList.moveToFirst { $0.user_id == AppContextManager.shared.userId } // 自己
|
||||||
|
tempList.moveToFirst { isGroupOwn(id: $0.user_id) } // 圈主
|
||||||
memberList = tempList
|
memberList = tempList
|
||||||
sectionedItems.onNext(memberList.mapSection())
|
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 phone: String
|
||||||
private let code: Int
|
private let code: Int
|
||||||
private let memberData: [String : Any]
|
private let memberData: [String : Any]
|
||||||
|
private var groupModel: GroupModel?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
rootView.unlockVipView.isHidden = AppContextManager.shared.vip > 1
|
rootView.unlockVipView.isHidden = AppContextManager.shared.vip > 1
|
||||||
AppContextManager.shared.vip > 1 ? nil : rootView.unlockVipLottieView.play()
|
AppContextManager.shared.vip > 1 ? nil : rootView.unlockVipLottieView.play()
|
||||||
|
|
||||||
reactiveAction()
|
reactiveAction()
|
||||||
requestPhoneArea()
|
requestPhoneArea()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reactiveAction() {
|
private func reactiveAction() {
|
||||||
rootView.successBtn.rx.tap.subscribe(onNext: { _ in
|
rootView.successBtn.rx.tap.subscribe(onNext: { _ in
|
||||||
if self.code == 0 { // 去查看
|
if self.code == 0 { // 去查看
|
||||||
|
|
@ -41,8 +42,12 @@ class SearchLocationResultVC: BaseViewController {
|
||||||
userInfo: self.memberData)
|
userInfo: self.memberData)
|
||||||
AppRouter.shared.popToRoot()
|
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)
|
}).disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
@ -103,10 +108,13 @@ class SearchLocationResultVC: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 圈子列表
|
// MARK: - 圈子列表
|
||||||
private func requestGroupInfo() {
|
private func requestGroupInfo(showPicker: Bool = false) {
|
||||||
GroupService.groupInfo().subscribe { response in
|
GroupService.groupInfo().subscribe { [weak self] response in
|
||||||
guard let model = response.model else { return }
|
guard let self = self, let model = response.model else { return }
|
||||||
|
self.groupModel = model
|
||||||
|
if showPicker {
|
||||||
|
GroupChooseView.show(groupModel: model) { _ in }
|
||||||
|
}
|
||||||
}.disposed(by: disposeBag)
|
}.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,4 @@ final class LoginViewModel: BaseViewModel {
|
||||||
func performAppleLogin() {
|
func performAppleLogin() {
|
||||||
// TODO: Integrate Sign in with Apple
|
// 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 {
|
struct ScheduleModel: Mappable, Equatable {
|
||||||
var uuid: String = UUID().uuidString
|
var uuid: String = UUID().uuidString
|
||||||
/// 行程id
|
/// 行程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:
|
/// - Parameters:
|
||||||
/// - id: 行程ID
|
/// - id: 行程ID
|
||||||
|
|
|
||||||