|
|
@ -321,6 +321,9 @@
|
||||||
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; };
|
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; };
|
||||||
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; };
|
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; };
|
||||||
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; };
|
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; };
|
||||||
|
55B220013031000100784801 /* FeedbackVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220003031000100784800 /* FeedbackVC.swift */; };
|
||||||
|
55B220033031000100784803 /* FeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220023031000100784802 /* FeedbackView.swift */; };
|
||||||
|
55B220053031000100784805 /* FeedbackSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220043031000100784804 /* FeedbackSuccessView.swift */; };
|
||||||
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
|
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
|
||||||
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
|
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
|
||||||
55B219A2302A000100784751 /* UnlockRequestPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219A1302A000100784750 /* UnlockRequestPopView.swift */; };
|
55B219A2302A000100784751 /* UnlockRequestPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219A1302A000100784750 /* UnlockRequestPopView.swift */; };
|
||||||
|
|
@ -731,6 +734,9 @@
|
||||||
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
|
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
|
||||||
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
|
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
|
||||||
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
|
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
|
||||||
|
55B220003031000100784800 /* FeedbackVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackVC.swift; sourceTree = "<group>"; };
|
||||||
|
55B220023031000100784802 /* FeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackView.swift; sourceTree = "<group>"; };
|
||||||
|
55B220043031000100784804 /* FeedbackSuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackSuccessView.swift; sourceTree = "<group>"; };
|
||||||
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
|
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
|
||||||
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
|
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
|
||||||
55B219A1302A000100784750 /* UnlockRequestPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockRequestPopView.swift; sourceTree = "<group>"; };
|
55B219A1302A000100784750 /* UnlockRequestPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockRequestPopView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -1285,6 +1291,7 @@
|
||||||
30EFF3CB2FDA667A00EB35D4 /* MyProfile */,
|
30EFF3CB2FDA667A00EB35D4 /* MyProfile */,
|
||||||
30EFF3BC2FD9585200EB35D4 /* Account */,
|
30EFF3BC2FD9585200EB35D4 /* Account */,
|
||||||
55B218A13024A00100784710 /* About */,
|
55B218A13024A00100784710 /* About */,
|
||||||
|
55B220063031000100784806 /* Feedback */,
|
||||||
55B218A13024A00100784711 /* CancelAccount */,
|
55B218A13024A00100784711 /* CancelAccount */,
|
||||||
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
|
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
|
||||||
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
|
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
|
||||||
|
|
@ -1973,6 +1980,16 @@
|
||||||
path = MemberInfo;
|
path = MemberInfo;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
55B220063031000100784806 /* Feedback */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
55B220003031000100784800 /* FeedbackVC.swift */,
|
||||||
|
55B220023031000100784802 /* FeedbackView.swift */,
|
||||||
|
55B220043031000100784804 /* FeedbackSuccessView.swift */,
|
||||||
|
);
|
||||||
|
path = Feedback;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
55B218B13024B00100784720 /* Explore */ = {
|
55B218B13024B00100784720 /* Explore */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -2390,6 +2407,9 @@
|
||||||
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */,
|
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */,
|
||||||
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */,
|
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */,
|
||||||
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */,
|
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */,
|
||||||
|
55B220013031000100784801 /* FeedbackVC.swift in Sources */,
|
||||||
|
55B220033031000100784803 /* FeedbackView.swift in Sources */,
|
||||||
|
55B220053031000100784805 /* FeedbackSuccessView.swift in Sources */,
|
||||||
30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */,
|
30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */,
|
||||||
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */,
|
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */,
|
||||||
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */,
|
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */,
|
||||||
|
|
|
||||||
22
QuickLocation/Assets.xcassets/Group/remove_member_search.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "remove_member_search@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "remove_member_search@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Group/remove_member_search.imageset/remove_member_search@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 673 B |
BIN
QuickLocation/Assets.xcassets/Group/remove_member_search.imageset/remove_member_search@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
22
QuickLocation/Assets.xcassets/Group/remove_member_selected.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "remove_member_selected@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "remove_member_selected@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Group/remove_member_selected.imageset/remove_member_selected@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
QuickLocation/Assets.xcassets/Group/remove_member_selected.imageset/remove_member_selected@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
22
QuickLocation/Assets.xcassets/Group/remove_member_unselected.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "remove_member_unselected@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "remove_member_unselected@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 651 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "at@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "at@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 753 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "camera@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "camera@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 751 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "emoji@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "emoji@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "gallery@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "gallery@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 663 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -5,12 +5,12 @@
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icons_Photo@2x.png",
|
"filename" : "keyboard@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icons_Photo@3x.png",
|
"filename" : "keyboard@3x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "location@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "location@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 737 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "location_pin@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "location_pin@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1010 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "location_recenter@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "location_recenter@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/IM/location_recenter.imageset/location_recenter@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 886 B |
BIN
QuickLocation/Assets.xcassets/IM/location_recenter.imageset/location_recenter@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 963 B After Width: | Height: | Size: 591 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "reply@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "reply@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "send-2@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "send-2@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 498 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 581 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "voice@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "voice@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "add@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "add@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 212 B |
|
After Width: | Height: | Size: 351 B |
8
QuickLocation/Assets.xcassets/Mine/feedback_success_header.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "header@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "header@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Mine/feedback_success_header.imageset/header@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
QuickLocation/Assets.xcassets/Mine/feedback_success_header.imageset/header@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 106 KiB |
8
QuickLocation/Assets.xcassets/Mine/feedback_success_icon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "icon@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "icon@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Mine/feedback_success_icon.imageset/icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
QuickLocation/Assets.xcassets/Mine/feedback_success_icon.imageset/icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -81,6 +81,8 @@ enum Route: String {
|
||||||
case phoneReportDetail = "phoneReportDetail"
|
case phoneReportDetail = "phoneReportDetail"
|
||||||
/// 关于我们
|
/// 关于我们
|
||||||
case about = "about"
|
case about = "about"
|
||||||
|
/// 意见反馈
|
||||||
|
case feedback = "feedback"
|
||||||
/// 成员信息
|
/// 成员信息
|
||||||
case memberInfo = "memberInfo"
|
case memberInfo = "memberInfo"
|
||||||
/// 注销账号
|
/// 注销账号
|
||||||
|
|
@ -384,6 +386,11 @@ extension AppRouter: AppRouterProtocol {
|
||||||
AboutVC()
|
AboutVC()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 意见反馈
|
||||||
|
AppRouter.register(Route.feedback) { _, _ in
|
||||||
|
FeedbackVC()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 成员信息
|
// MARK: - 成员信息
|
||||||
AppRouter.register(Route.memberInfo) { _, parameters in
|
AppRouter.register(Route.memberInfo) { _, parameters in
|
||||||
MemberInfoVC(userInfo: parameters)
|
MemberInfoVC(userInfo: parameters)
|
||||||
|
|
@ -430,4 +437,3 @@ extension AppRouter: AppRouterProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CoreLocation
|
||||||
import RxSwift
|
import RxSwift
|
||||||
import RxCocoa
|
import RxCocoa
|
||||||
import OpenIMSDK
|
import OpenIMSDK
|
||||||
|
|
@ -22,6 +23,40 @@ struct QuotePreview {
|
||||||
let quotedClientMsgID: String?
|
let quotedClientMsgID: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ChatLocationPayload: Codable, Equatable {
|
||||||
|
let name: String
|
||||||
|
let address: String
|
||||||
|
let latitude: Double
|
||||||
|
let longitude: Double
|
||||||
|
|
||||||
|
var coordinate: CLLocationCoordinate2D {
|
||||||
|
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
var encodedDescription: String {
|
||||||
|
guard let data = try? JSONEncoder().encode(self),
|
||||||
|
let value = String(data: data, encoding: .utf8) else {
|
||||||
|
return [name, address].filter { !$0.isEmpty }.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(description: String?, latitude: Double, longitude: Double) -> ChatLocationPayload {
|
||||||
|
if let description,
|
||||||
|
let data = description.data(using: .utf8),
|
||||||
|
let payload = try? JSONDecoder().decode(ChatLocationPayload.self, from: data) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
let parts = (description ?? "").components(separatedBy: "\n")
|
||||||
|
return ChatLocationPayload(
|
||||||
|
name: parts.first ?? "",
|
||||||
|
address: parts.dropFirst().joined(separator: "\n"),
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ChatSectionItem {
|
enum ChatSectionItem {
|
||||||
case send(ChatMessage)
|
case send(ChatMessage)
|
||||||
case received(ChatMessage)
|
case received(ChatMessage)
|
||||||
|
|
@ -31,6 +66,8 @@ enum ChatSectionItem {
|
||||||
case voiceReceived(ChatMessage)
|
case voiceReceived(ChatMessage)
|
||||||
case imageSend(ChatMessage)
|
case imageSend(ChatMessage)
|
||||||
case imageReceived(ChatMessage)
|
case imageReceived(ChatMessage)
|
||||||
|
case locationSend(ChatMessage)
|
||||||
|
case locationReceived(ChatMessage)
|
||||||
case notification(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
case notification(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
||||||
case revoked(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
case revoked(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +94,9 @@ final class GroupChatViewModel {
|
||||||
private var lastTimeGap: TimeInterval = 0
|
private var lastTimeGap: TimeInterval = 0
|
||||||
private let timeGapThreshold: TimeInterval = 300
|
private let timeGapThreshold: TimeInterval = 300
|
||||||
|
|
||||||
var groupModel: GroupInfoModel?
|
var groupModel: GroupInfoModel? {
|
||||||
|
didSet { refreshMessageMetadata() }
|
||||||
|
}
|
||||||
var memberList: [GroupMemberModel] = [] {
|
var memberList: [GroupMemberModel] = [] {
|
||||||
didSet { buildAvatarCache() }
|
didSet { buildAvatarCache() }
|
||||||
}
|
}
|
||||||
|
|
@ -110,16 +149,23 @@ final class GroupChatViewModel {
|
||||||
cache[member.user_id] = member.userIcon
|
cache[member.user_id] = member.userIcon
|
||||||
}
|
}
|
||||||
avatarCache = cache
|
avatarCache = cache
|
||||||
refreshMessageAvatars()
|
refreshMessageMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshMessageAvatars() {
|
private func refreshMessageMetadata() {
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
var didChange = false
|
var didChange = false
|
||||||
items = items.map { item in
|
items = items.map { item in
|
||||||
guard var m = item.chatMessage, updateAvatar(&m) else { return item }
|
guard var message = item.chatMessage else { return item }
|
||||||
|
let avatarUpdated = updateAvatar(&message)
|
||||||
|
let isOwner = isCircleOwner(id: message.senderId)
|
||||||
|
let ownerUpdated = message.isCircleOwner != isOwner
|
||||||
|
if ownerUpdated {
|
||||||
|
message = message.with(isCircleOwner: isOwner)
|
||||||
|
}
|
||||||
|
guard avatarUpdated || ownerUpdated else { return item }
|
||||||
didChange = true
|
didChange = true
|
||||||
return ChatSectionItem.with(m)
|
return ChatSectionItem.with(message)
|
||||||
}
|
}
|
||||||
if didChange { messagesSubject.accept(items) }
|
if didChange { messagesSubject.accept(items) }
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +190,11 @@ final class GroupChatViewModel {
|
||||||
memberList.first { id == $0.user_id }?.nick_name ?? ""
|
memberList.first { id == $0.user_id }?.nick_name ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCircleOwner(id: String) -> Bool {
|
||||||
|
guard !id.isEmpty, let groupModel else { return false }
|
||||||
|
return groupModel.group_key.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Quote / Mention state
|
// MARK: - Quote / Mention state
|
||||||
func setPendingQuote(_ msg: OIMMessageInfo?) {
|
func setPendingQuote(_ msg: OIMMessageInfo?) {
|
||||||
pendingQuote = msg
|
pendingQuote = msg
|
||||||
|
|
@ -239,6 +290,26 @@ final class GroupChatViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendLocation(_ location: ChatLocationPayload) {
|
||||||
|
let msg = OIMMessageInfo.createLocationMessage(location.encodedDescription,
|
||||||
|
latitude: location.latitude,
|
||||||
|
longitude: location.longitude)
|
||||||
|
OIMManager.manager.sendMessage(msg,
|
||||||
|
recvID: "",
|
||||||
|
groupID: groupId,
|
||||||
|
offlinePushInfo: nil,
|
||||||
|
onSuccess: { [weak self] _ in
|
||||||
|
self?.cacheMessage(msg)
|
||||||
|
self?.appendMessage(msg)
|
||||||
|
},
|
||||||
|
onProgress: nil as OIMNumberCallback?,
|
||||||
|
onFailure: { code, errMsg in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
DLToast.showError(text: errMsg ?? "位置发送失败(\(code))")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Revoke
|
// MARK: - Revoke
|
||||||
func revokeMessage(clientMsgID: String) {
|
func revokeMessage(clientMsgID: String) {
|
||||||
OIMManager.manager.revokeMessage(conversationID,
|
OIMManager.manager.revokeMessage(conversationID,
|
||||||
|
|
@ -276,9 +347,10 @@ final class GroupChatViewModel {
|
||||||
}
|
}
|
||||||
let ts = timestampFrom(item: items[idx])
|
let ts = timestampFrom(item: items[idx])
|
||||||
let showTime: Bool = {
|
let showTime: Bool = {
|
||||||
switch items[idx] {
|
switch items[idx] {
|
||||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
||||||
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m),
|
||||||
|
let .locationSend(m), let .locationReceived(m):
|
||||||
return m.showTime
|
return m.showTime
|
||||||
case let .notification(_, s, _), let .revoked(_, s, _):
|
case let .notification(_, s, _), let .revoked(_, s, _):
|
||||||
return s
|
return s
|
||||||
|
|
@ -602,7 +674,8 @@ final class GroupChatViewModel {
|
||||||
private func timestampFrom(item: ChatSectionItem) -> TimeInterval {
|
private func timestampFrom(item: ChatSectionItem) -> TimeInterval {
|
||||||
switch item {
|
switch item {
|
||||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
||||||
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m),
|
||||||
|
let .locationSend(m), let .locationReceived(m):
|
||||||
return m.timestamp
|
return m.timestamp
|
||||||
case let .notification(_, _, ts), let .revoked(_, _, ts):
|
case let .notification(_, _, ts), let .revoked(_, _, ts):
|
||||||
return ts
|
return ts
|
||||||
|
|
@ -619,6 +692,8 @@ final class GroupChatViewModel {
|
||||||
case var .voiceReceived(m): m.showTime = show; return .voiceReceived(m)
|
case var .voiceReceived(m): m.showTime = show; return .voiceReceived(m)
|
||||||
case var .imageSend(m): m.showTime = show; return .imageSend(m)
|
case var .imageSend(m): m.showTime = show; return .imageSend(m)
|
||||||
case var .imageReceived(m): m.showTime = show; return .imageReceived(m)
|
case var .imageReceived(m): m.showTime = show; return .imageReceived(m)
|
||||||
|
case var .locationSend(m): m.showTime = show; return .locationSend(m)
|
||||||
|
case var .locationReceived(m): m.showTime = show; return .locationReceived(m)
|
||||||
case let .notification(text, _, ts): return .notification(text, showTime: show, timestamp: ts)
|
case let .notification(text, _, ts): return .notification(text, showTime: show, timestamp: ts)
|
||||||
case let .revoked(text, _, ts): return .revoked(text, showTime: show, timestamp: ts)
|
case let .revoked(text, _, ts): return .revoked(text, showTime: show, timestamp: ts)
|
||||||
}
|
}
|
||||||
|
|
@ -636,6 +711,7 @@ final class GroupChatViewModel {
|
||||||
var atUserIDs: [String] = []
|
var atUserIDs: [String] = []
|
||||||
var atNicknames: [String] = []
|
var atNicknames: [String] = []
|
||||||
var isAtAll = false
|
var isAtAll = false
|
||||||
|
var location: ChatLocationPayload?
|
||||||
|
|
||||||
let maxWH: CGFloat = 200
|
let maxWH: CGFloat = 200
|
||||||
var msgImageW: CGFloat = 0
|
var msgImageW: CGFloat = 0
|
||||||
|
|
@ -659,6 +735,11 @@ final class GroupChatViewModel {
|
||||||
imageUrl = pic.sourcePicture?.url ?? pic.bigPicture?.url ?? pic.sourcePath ?? ""
|
imageUrl = pic.sourcePicture?.url ?? pic.bigPicture?.url ?? pic.sourcePath ?? ""
|
||||||
imageW = msgImageW
|
imageW = msgImageW
|
||||||
imageH = msgImageH
|
imageH = msgImageH
|
||||||
|
} else if type == 109, let elem = msg.locationElem {
|
||||||
|
location = ChatLocationPayload.decode(description: elem.desc,
|
||||||
|
latitude: elem.latitude,
|
||||||
|
longitude: elem.longitude)
|
||||||
|
content = location?.name ?? ""
|
||||||
} else if type == 106, let at = msg.atTextElem {
|
} else if type == 106, let at = msg.atTextElem {
|
||||||
content = at.text ?? ""
|
content = at.text ?? ""
|
||||||
atUserIDs = at.atUserList ?? []
|
atUserIDs = at.atUserList ?? []
|
||||||
|
|
@ -684,12 +765,15 @@ final class GroupChatViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendID = msg.sendID ?? ""
|
let sendID = msg.sendID ?? ""
|
||||||
|
let senderName = msg.senderNickname?.isEmpty == false
|
||||||
|
? msg.senderNickname!
|
||||||
|
: getUserNickName(id: sendID)
|
||||||
return ChatMessage(
|
return ChatMessage(
|
||||||
id: msg.clientMsgID ?? UUID().uuidString,
|
id: msg.clientMsgID ?? UUID().uuidString,
|
||||||
isSelf: isSelf,
|
isSelf: isSelf,
|
||||||
senderId: sendID,
|
senderId: sendID,
|
||||||
avatar: getUserAvatar(id: sendID),
|
avatar: getUserAvatar(id: sendID),
|
||||||
senderName: msg.senderNickname ?? "",
|
senderName: senderName,
|
||||||
content: content,
|
content: content,
|
||||||
voiceUrl: voiceUrl,
|
voiceUrl: voiceUrl,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
|
|
@ -700,7 +784,9 @@ final class GroupChatViewModel {
|
||||||
quotePreview: quotePreview,
|
quotePreview: quotePreview,
|
||||||
atUserIDs: atUserIDs,
|
atUserIDs: atUserIDs,
|
||||||
atNicknames: atNicknames,
|
atNicknames: atNicknames,
|
||||||
isAtAll: isAtAll
|
isAtAll: isAtAll,
|
||||||
|
isCircleOwner: isCircleOwner(id: sendID),
|
||||||
|
location: location
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -719,6 +805,9 @@ final class GroupChatViewModel {
|
||||||
let summary = dur > 0 ? "[语音] \(dur)''" : "[语音]"
|
let summary = dur > 0 ? "[语音] \(dur)''" : "[语音]"
|
||||||
return QuotePreview(senderName: name, kind: .voice, summary: summary, quotedClientMsgID: id)
|
return QuotePreview(senderName: name, kind: .voice, summary: summary, quotedClientMsgID: id)
|
||||||
}
|
}
|
||||||
|
if type == 109 {
|
||||||
|
return QuotePreview(senderName: name, kind: .unknown, summary: "[位置]", quotedClientMsgID: id)
|
||||||
|
}
|
||||||
var text = ""
|
var text = ""
|
||||||
if type == 106 {
|
if type == 106 {
|
||||||
text = msg.atTextElem?.text ?? ""
|
text = msg.atTextElem?.text ?? ""
|
||||||
|
|
@ -764,6 +853,11 @@ final class GroupChatViewModel {
|
||||||
return chatMsg.isSelf ? .imageSend(chatMsg) : .imageReceived(chatMsg)
|
return chatMsg.isSelf ? .imageSend(chatMsg) : .imageReceived(chatMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.contentType.rawValue == 109 {
|
||||||
|
let chatMsg = convert(msg)
|
||||||
|
return chatMsg.isSelf ? .locationSend(chatMsg) : .locationReceived(chatMsg)
|
||||||
|
}
|
||||||
|
|
||||||
// 文本 / @ / 引用
|
// 文本 / @ / 引用
|
||||||
let chatMsg = convert(msg)
|
let chatMsg = convert(msg)
|
||||||
if chatMsg.content.isEmpty && chatMsg.quotePreview == nil { return nil }
|
if chatMsg.content.isEmpty && chatMsg.quotePreview == nil { return nil }
|
||||||
|
|
@ -815,7 +909,8 @@ extension ChatSectionItem {
|
||||||
var chatMessage: ChatMessage? {
|
var chatMessage: ChatMessage? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
||||||
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m),
|
||||||
|
let .locationSend(m), let .locationReceived(m):
|
||||||
return m
|
return m
|
||||||
case .notification, .revoked:
|
case .notification, .revoked:
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -826,6 +921,9 @@ extension ChatSectionItem {
|
||||||
var revokedClientMsgID: String? { nil }
|
var revokedClientMsgID: String? { nil }
|
||||||
|
|
||||||
static func with(_ msg: ChatMessage) -> ChatSectionItem {
|
static func with(_ msg: ChatMessage) -> ChatSectionItem {
|
||||||
|
if msg.location != nil {
|
||||||
|
return msg.isSelf ? .locationSend(msg) : .locationReceived(msg)
|
||||||
|
}
|
||||||
if !msg.imageUrl.isEmpty {
|
if !msg.imageUrl.isEmpty {
|
||||||
return msg.isSelf ? .imageSend(msg) : .imageReceived(msg)
|
return msg.isSelf ? .imageSend(msg) : .imageReceived(msg)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,25 @@ class RemoveMemberVC: BaseViewController {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.searchField.rx.text.orEmpty
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.subscribe(onNext: { [weak self] keyword in
|
||||||
|
self?.rootView.updateSearchPlaceholder(isHidden: !keyword.isEmpty)
|
||||||
|
self?.viewModel.updateSearchKeyword(keyword)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.searchField.rx.controlEvent(.editingDidEndOnExit)
|
||||||
|
.subscribe(onNext: { [weak self] in
|
||||||
|
self?.rootView.searchField.resignFirstResponder()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.tableView.rx.modelSelected(GroupMemberModel.self)
|
rootView.tableView.rx.modelSelected(GroupMemberModel.self)
|
||||||
.subscribe(viewModel.cellAction.inputs)
|
.subscribe(viewModel.cellAction.inputs)
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
// selectedMembers 驱动删除按钮状态和文字
|
// selectedMembers 驱动移除按钮状态
|
||||||
let selectedCount = viewModel.selectedMembers
|
let selectedCount = viewModel.selectedMembers
|
||||||
.map { $0.count }
|
.map { $0.count }
|
||||||
.share(replay: 1)
|
.share(replay: 1)
|
||||||
|
|
@ -56,20 +70,18 @@ class RemoveMemberVC: BaseViewController {
|
||||||
.bind(to: rootView.deleteBtn.rx.isEnabled)
|
.bind(to: rootView.deleteBtn.rx.isEnabled)
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
selectedCount
|
rootView.deleteBtn.rx.tap
|
||||||
.map { "删除(\($0))" }
|
.subscribe(onNext: { [weak self] in
|
||||||
.bind(to: rootView.deleteBtn.rx.title())
|
guard let self = self else { return }
|
||||||
|
self.showConfirmPop(showCloseBtn: true,
|
||||||
|
title: "确定要移除成员吗?",
|
||||||
|
message: "成员将从当前圈子中移除",
|
||||||
|
confirmText: "否",
|
||||||
|
cancelText: "是") {
|
||||||
|
self.requestRemoveMember()
|
||||||
|
}
|
||||||
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.deleteBtn.rx.tap.subscribe(onNext: { _ in
|
|
||||||
self.showConfirmPop(showCloseBtn: true,
|
|
||||||
title: "确定要移除成员吗?",
|
|
||||||
message: "成员将从当前圈子中移除",
|
|
||||||
confirmText: "否",
|
|
||||||
cancelText: "是") {
|
|
||||||
self.requestRemoveMember()
|
|
||||||
}
|
|
||||||
}).disposed(by: disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
// MARK: - UITableViewDataSource
|
||||||
|
|
@ -79,7 +91,8 @@ class RemoveMemberVC: BaseViewController {
|
||||||
let cell: RemoveMemberCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: RemoveMemberCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.configure(model,
|
cell.configure(model,
|
||||||
isOwn: self.viewModel.groupId.contains(model.user_id),
|
isOwn: self.viewModel.groupId.contains(model.user_id),
|
||||||
isSelected: self.viewModel.isSelected(id: model.user_id))
|
isSelected: self.viewModel.isSelected(id: model.user_id),
|
||||||
|
showsSeparator: indexPath.row < tableView.numberOfRows(inSection: indexPath.section) - 1)
|
||||||
return cell
|
return cell
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
@ -91,6 +104,7 @@ class RemoveMemberVC: BaseViewController {
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
guard let model = response.model else { return }
|
guard let model = response.model else { return }
|
||||||
self.viewModel.groupModel = model
|
self.viewModel.groupModel = model
|
||||||
|
self.rootView.updateMemberCount(response.list.count)
|
||||||
self.viewModel.loadData(response.list)
|
self.viewModel.loadData(response.list)
|
||||||
}.disposed(by: disposeBag)
|
}.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,94 +6,158 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
|
||||||
import RxCocoa
|
|
||||||
|
|
||||||
class RemoveMemberView: UIView {
|
final class RemoveMemberView: UIView {
|
||||||
|
|
||||||
var disposeBag = DisposeBag()
|
static let rowHeight: CGFloat = 52
|
||||||
/// 单行高度
|
|
||||||
static let rowHeight: CGFloat = 85
|
|
||||||
|
|
||||||
private func setupRx() {
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupUI() {
|
|
||||||
addSubview(navBgView)
|
|
||||||
addSubview(navView)
|
|
||||||
|
|
||||||
addSubview(titleLab)
|
|
||||||
addSubview(cornerBgView)
|
|
||||||
addSubview(tableView)
|
|
||||||
|
|
||||||
navBgView.layoutChain
|
|
||||||
.edges(excludingEdge: .bottom)
|
|
||||||
.height(kNaviHeight)
|
|
||||||
|
|
||||||
navView.layoutChain
|
|
||||||
.edges(excludingEdge: .bottom)
|
|
||||||
.height(kNaviHeight)
|
|
||||||
|
|
||||||
titleLab.layoutChain
|
|
||||||
.topToBottomOfView(navView, offset: 8)
|
|
||||||
.left(15)
|
|
||||||
|
|
||||||
cornerBgView.layoutChain
|
|
||||||
.topToBottomOfView(titleLab, offset: 16)
|
|
||||||
.edgesHorzontal(15)
|
|
||||||
|
|
||||||
tableView.layoutChain
|
|
||||||
.topToBottomOfView(titleLab, offset: 16)
|
|
||||||
.edgesHorzontal(15)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 根据数据源数量更新高度,上限到 bottom = kSafeBottomMargin + 10
|
|
||||||
func updateHeight(itemCount: Int) {
|
|
||||||
let contentH = CGFloat(itemCount) * Self.rowHeight
|
|
||||||
let maxH = dl.height - kNaviHeight - kSafeBottomMargin - 10 - 16
|
|
||||||
let targetH = min(contentH, maxH)
|
|
||||||
|
|
||||||
tableView.layoutChain.height(targetH)
|
|
||||||
cornerBgView.layoutChain.height(targetH)
|
|
||||||
tableView.isScrollEnabled = targetH > maxH
|
|
||||||
}
|
|
||||||
|
|
||||||
private var listHeightConstraint: NSLayoutConstraint?
|
private var listHeightConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
lazy var navBgView: UIImageView = {
|
override init(frame: CGRect) {
|
||||||
let iv = UIImageView()
|
super.init(frame: frame)
|
||||||
iv.image = UIImage(named: "Common/navBar_bg_2")
|
backgroundColor = UIColor(hexStr: "#F7F7F7")
|
||||||
iv.contentMode = .scaleAspectFill
|
setupUI()
|
||||||
return iv
|
}
|
||||||
}()
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
addSubview(navView)
|
||||||
|
addSubview(searchField)
|
||||||
|
addSubview(titleLab)
|
||||||
|
addSubview(listContainerView)
|
||||||
|
listContainerView.addSubview(tableView)
|
||||||
|
|
||||||
|
navView.layoutChain
|
||||||
|
.top()
|
||||||
|
.edgesHorzontal()
|
||||||
|
.height(kNaviHeight)
|
||||||
|
|
||||||
|
searchField.layoutChain
|
||||||
|
.topToBottomOfView(navView, offset: 20)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
.height(38)
|
||||||
|
|
||||||
|
searchField.addSubview(searchPlaceholderView)
|
||||||
|
searchPlaceholderView.layoutChain
|
||||||
|
.center()
|
||||||
|
.height(20)
|
||||||
|
searchPlaceholderView.layoutChain.width(52)
|
||||||
|
|
||||||
|
titleLab.layoutChain
|
||||||
|
.topToBottomOfView(searchField, offset: 19)
|
||||||
|
.left(16)
|
||||||
|
.height(19)
|
||||||
|
|
||||||
|
listContainerView.layoutChain
|
||||||
|
.topToBottomOfView(titleLab, offset: 6)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
tableView.layoutChain.edges()
|
||||||
|
listHeightConstraint = listContainerView.jh_setDimension(.height, toSize: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMemberCount(_ count: Int) {
|
||||||
|
let prefix = "圈成员"
|
||||||
|
let countText = "(\(count)人)"
|
||||||
|
let text = prefix + countText
|
||||||
|
let attributedText = NSMutableAttributedString(string: text)
|
||||||
|
attributedText.addAttributes([
|
||||||
|
.font: UIFont.systemFont(ofSize: 14, weight: .medium),
|
||||||
|
.foregroundColor: UIColor(hexStr: "#6E6E73")
|
||||||
|
], range: NSRange(location: 0, length: text.count))
|
||||||
|
attributedText.addAttributes([
|
||||||
|
.font: UIFont.systemFont(ofSize: 13, weight: .regular),
|
||||||
|
.foregroundColor: UIColor(hexStr: "#8E8E93")
|
||||||
|
], range: NSRange(location: prefix.count, length: countText.count))
|
||||||
|
titleLab.attributedText = attributedText
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateHeight(itemCount: Int) {
|
||||||
|
let contentHeight = CGFloat(itemCount) * Self.rowHeight
|
||||||
|
layoutIfNeeded()
|
||||||
|
|
||||||
|
let bottomInset = max(kSafeBottomMargin, safeAreaInsets.bottom)
|
||||||
|
let maxHeight = max(0, bounds.height - listContainerView.frame.minY - bottomInset)
|
||||||
|
listHeightConstraint?.constant = min(contentHeight, maxHeight)
|
||||||
|
tableView.isScrollEnabled = contentHeight >= maxHeight && itemCount > 0
|
||||||
|
listContainerView.isHidden = itemCount == 0
|
||||||
|
layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSearchPlaceholder(isHidden: Bool) {
|
||||||
|
searchPlaceholderView.isHidden = isHidden
|
||||||
|
}
|
||||||
|
|
||||||
lazy var navView: BaseNavigationView = {
|
lazy var navView: BaseNavigationView = {
|
||||||
let nav = BaseNavigationView(title: "移除圈子成员")
|
let nav = BaseNavigationView(title: "圈子成员")
|
||||||
nav.addRightButton(deleteBtn)
|
nav.addRightButton(deleteBtn)
|
||||||
return nav
|
return nav
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var deleteBtn: UIButton = {
|
lazy var deleteBtn: UIButton = {
|
||||||
let btn = UIButton(type: .custom)
|
let button = UIButton(type: .custom)
|
||||||
btn.setTitle("删除(0)", for: .normal)
|
button.setTitle("移除", for: .normal)
|
||||||
btn.setTitleColor(UIColor(hexStr: "#FF383C"), for: .normal)
|
button.setTitleColor(UIColor(hexStr: "#FF4D53"), for: .normal)
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
button.setTitleColor(UIColor(hexStr: "#FF4D53", alpha: 0.35), for: .disabled)
|
||||||
btn.contentHorizontalAlignment = .right
|
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
return btn
|
button.contentHorizontalAlignment = .right
|
||||||
|
button.isEnabled = false
|
||||||
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var titleLab: UILabel = {
|
lazy var searchField: UISearchTextField = {
|
||||||
let label = UILabel()
|
let textField = UISearchTextField()
|
||||||
label.text = "圈子成员"
|
textField.backgroundColor = .white
|
||||||
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
textField.borderStyle = .none
|
||||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
textField.cornerRadius = 11
|
||||||
return label
|
textField.clipsToBounds = true
|
||||||
|
textField.font = .systemFont(ofSize: 14)
|
||||||
|
textField.textColor = UIColor(hexStr: "#293445")
|
||||||
|
textField.tintColor = UIColor(hexStr: "#293445")
|
||||||
|
textField.returnKeyType = .search
|
||||||
|
textField.clearButtonMode = .whileEditing
|
||||||
|
textField.textAlignment = .left
|
||||||
|
textField.leftView = UIView(frame: .zero)
|
||||||
|
textField.leftViewMode = .never
|
||||||
|
return textField
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var cornerBgView: UIView = {
|
private lazy var searchPlaceholderView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
|
view.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
let imageView = UIImageView(image: UIImage(named: "Group/remove_member_search"))
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
view.addSubview(imageView)
|
||||||
|
imageView.layoutChain
|
||||||
|
.left()
|
||||||
|
.centerY()
|
||||||
|
.width(16)
|
||||||
|
.height(16)
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "搜索"
|
||||||
|
label.textColor = UIColor(hexStr: "#B0B0B5")
|
||||||
|
label.font = .systemFont(ofSize: 14)
|
||||||
|
view.addSubview(label)
|
||||||
|
label.layoutChain
|
||||||
|
.leftToRightOfView(imageView, offset: 6)
|
||||||
|
.right()
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleLab = UILabel()
|
||||||
|
|
||||||
|
private lazy var listContainerView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .white
|
||||||
view.cornerRadius = 10
|
view.cornerRadius = 10
|
||||||
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -101,166 +165,102 @@ class RemoveMemberView: UIView {
|
||||||
let tableView = UITableView(frame: .zero, style: .plain)
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
tableView.backgroundColor = .clear
|
tableView.backgroundColor = .clear
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
tableView.estimatedRowHeight = 85
|
tableView.rowHeight = Self.rowHeight
|
||||||
|
tableView.estimatedRowHeight = Self.rowHeight
|
||||||
tableView.isScrollEnabled = false
|
tableView.isScrollEnabled = false
|
||||||
|
tableView.showsVerticalScrollIndicator = false
|
||||||
|
tableView.keyboardDismissMode = .onDrag
|
||||||
tableView.register(RemoveMemberCell.self)
|
tableView.register(RemoveMemberCell.self)
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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: - RemoveMemberCell
|
// MARK: - RemoveMemberCell
|
||||||
class RemoveMemberCell: UITableViewCell {
|
final class RemoveMemberCell: UITableViewCell {
|
||||||
|
|
||||||
var disposeBag = DisposeBag()
|
func configure(
|
||||||
|
_ model: GroupMemberModel,
|
||||||
func configure(_ model: GroupMemberModel, isOwn: Bool, isSelected: Bool) {
|
isOwn: Bool,
|
||||||
avaterImgView.image = model.userIcon
|
isSelected: Bool,
|
||||||
|
showsSeparator: Bool
|
||||||
|
) {
|
||||||
|
avatarImageView.image = model.userIcon
|
||||||
nameLab.text = model.nick_name
|
nameLab.text = model.nick_name
|
||||||
ownView.isHidden = !isOwn
|
selectedBtn.isHidden = false
|
||||||
selectedBtn.isHidden = isOwn
|
selectedBtn.isSelected = isOwn ? false : isSelected
|
||||||
selectedBtn.isSelected = isSelected
|
selectedBtn.alpha = isOwn ? 0.45 : 1
|
||||||
|
lineView.isHidden = !showsSeparator
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
contentView.backgroundColor = .clear
|
||||||
setupSubviews()
|
setupSubviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupSubviews() {
|
|
||||||
contentView.addSubview(bgView)
|
|
||||||
bgView.addSubview(avaterImgView)
|
|
||||||
bgView.addSubview(nameLab)
|
|
||||||
bgView.addSubview(stackView)
|
|
||||||
bgView.addSubview(lineView)
|
|
||||||
|
|
||||||
bgView.layoutChain
|
|
||||||
.edgesHorzontal()
|
|
||||||
.edgesVertical()
|
|
||||||
|
|
||||||
avaterImgView.layoutChain
|
|
||||||
.edgesVertical(20)
|
|
||||||
.left(15)
|
|
||||||
.height(45)
|
|
||||||
.widthToHeight(1)
|
|
||||||
|
|
||||||
nameLab.layoutChain
|
|
||||||
.leftToRightOfView(avaterImgView, offset: 10)
|
|
||||||
.centerY(avaterImgView)
|
|
||||||
|
|
||||||
stackView.layoutChain
|
|
||||||
.right(15)
|
|
||||||
.centerY()
|
|
||||||
|
|
||||||
ownView.layoutChain.width(25).height(35)
|
|
||||||
selectedBtn.layoutChain.width(20).height(20)
|
|
||||||
|
|
||||||
lineView.layoutChain
|
|
||||||
.height(0.5)
|
|
||||||
.edgesHorzontal(15)
|
|
||||||
.bottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy var bgView: UIView = {
|
|
||||||
let view = UIView()
|
|
||||||
view.backgroundColor = .clear//UIColor(hexStr: "#F5FBFF")
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var avaterImgView: UIImageView = {
|
|
||||||
let view = UIImageView()
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
view.cornerRadius = 22.5
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var nameLab: UILabel = {
|
|
||||||
let label = UILabel()
|
|
||||||
label.textColor = UIColor(hexStr: "#0F2846")
|
|
||||||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var stackView: UIStackView = {
|
|
||||||
let view = UIStackView(arrangedSubviews: [ownView, selectedBtn])
|
|
||||||
view.axis = .horizontal
|
|
||||||
view.alignment = .trailing
|
|
||||||
view.spacing = 0
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var ownView: UIView = {
|
|
||||||
let view = UIView()
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
view.isHidden = true
|
|
||||||
|
|
||||||
let icon = UIImageView(image: UIImage(named: "Common/owner"))
|
|
||||||
view.addSubview(icon)
|
|
||||||
icon.layoutChain
|
|
||||||
.top()
|
|
||||||
.centerX()
|
|
||||||
.width(18)
|
|
||||||
.height(18)
|
|
||||||
|
|
||||||
let label = UILabel()
|
|
||||||
label.text = "圈主"
|
|
||||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
|
||||||
label.font = .systemFont(ofSize: 10, weight: .medium)
|
|
||||||
view.addSubview(label)
|
|
||||||
label.layoutChain
|
|
||||||
.topToBottomOfView(icon)
|
|
||||||
.centerX()
|
|
||||||
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var selectedBtn: UIButton = {
|
|
||||||
let btn = UIButton(type: .custom)
|
|
||||||
btn.setImage(UIImage(named: "Common/checkbox_20x20"), for: .normal)
|
|
||||||
btn.setImage(UIImage(named: "Common/delete_check"), for: .selected)
|
|
||||||
btn.isUserInteractionEnabled = false
|
|
||||||
btn.isHidden = true
|
|
||||||
return btn
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var lineView: UIView = {
|
|
||||||
let view = UIView()
|
|
||||||
view.backgroundColor = ThemeManager.shared.color.lineColor
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
private func setupSubviews() {
|
||||||
super.awakeFromNib()
|
contentView.addSubview(avatarImageView)
|
||||||
// Initialization code
|
contentView.addSubview(nameLab)
|
||||||
|
contentView.addSubview(selectedBtn)
|
||||||
|
contentView.addSubview(lineView)
|
||||||
|
|
||||||
|
avatarImageView.layoutChain
|
||||||
|
.left(16)
|
||||||
|
.centerY()
|
||||||
|
.width(38)
|
||||||
|
.height(38)
|
||||||
|
|
||||||
|
nameLab.layoutChain
|
||||||
|
.leftToRightOfView(avatarImageView, offset: 12)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
selectedBtn.layoutChain
|
||||||
|
.right(14)
|
||||||
|
.centerY()
|
||||||
|
.width(24)
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
lineView.layoutChain
|
||||||
|
.left(20)
|
||||||
|
.right(13)
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
private lazy var avatarImageView: UIImageView = {
|
||||||
super.setSelected(selected, animated: animated)
|
let imageView = UIImageView()
|
||||||
|
imageView.backgroundColor = UIColor(hexStr: "#F2F2F2")
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.cornerRadius = 9
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
// Configure the view for the selected state
|
private lazy var nameLab: UILabel = {
|
||||||
}
|
let label = UILabel()
|
||||||
|
label.textColor = UIColor(hexStr: "#111111")
|
||||||
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
override func prepareForReuse() {
|
private lazy var selectedBtn: UIButton = {
|
||||||
super.prepareForReuse()
|
let button = UIButton(type: .custom)
|
||||||
disposeBag = DisposeBag()
|
button.setImage(UIImage(named: "Group/remove_member_unselected"), for: .normal)
|
||||||
}
|
button.setImage(UIImage(named: "Group/remove_member_selected"), for: .selected)
|
||||||
|
button.isUserInteractionEnabled = false
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var lineView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#EEEEF0")
|
||||||
|
return view
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class RemoveMemberViewModel {
|
||||||
private var disposeBag = DisposeBag()
|
private var disposeBag = DisposeBag()
|
||||||
private let sectionedItems = PublishSubject<[ReviewMemberListSectionModel]>()
|
private let sectionedItems = PublishSubject<[ReviewMemberListSectionModel]>()
|
||||||
private var list: [GroupMemberModel] = []
|
private var list: [GroupMemberModel] = []
|
||||||
|
private var searchKeyword = ""
|
||||||
|
|
||||||
var selectedMembers = BehaviorRelay<[String]>(value: [])
|
var selectedMembers = BehaviorRelay<[String]>(value: [])
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ class RemoveMemberViewModel {
|
||||||
current.append(memberId)
|
current.append(memberId)
|
||||||
}
|
}
|
||||||
this.selectedMembers.accept(current)
|
this.selectedMembers.accept(current)
|
||||||
self.sectionedItems.onNext(self.list.mapSection())
|
self.emitItems()
|
||||||
return .empty()
|
return .empty()
|
||||||
}
|
}
|
||||||
}(self)
|
}(self)
|
||||||
|
|
@ -53,7 +54,27 @@ class RemoveMemberViewModel {
|
||||||
|
|
||||||
func loadData(_ list: [GroupMemberModel]) {
|
func loadData(_ list: [GroupMemberModel]) {
|
||||||
self.list = list
|
self.list = list
|
||||||
sectionedItems.onNext(list.mapSection())
|
selectedMembers.accept(selectedMembers.value.filter { selectedId in
|
||||||
|
list.contains(where: { $0.user_id == selectedId })
|
||||||
|
})
|
||||||
|
emitItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSearchKeyword(_ keyword: String) {
|
||||||
|
searchKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
emitItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func emitItems() {
|
||||||
|
let items: [GroupMemberModel]
|
||||||
|
if searchKeyword.isEmpty {
|
||||||
|
items = list
|
||||||
|
} else {
|
||||||
|
items = list.filter {
|
||||||
|
$0.nick_name.localizedCaseInsensitiveContains(searchKeyword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sectionedItems.onNext(items.mapSection())
|
||||||
}
|
}
|
||||||
|
|
||||||
init(groupId: String) {
|
init(groupId: String) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ final class AboutVC: BaseViewController {
|
||||||
|
|
||||||
rootView.feedbackBtn.rx.controlEvent(.touchUpInside)
|
rootView.feedbackBtn.rx.controlEvent(.touchUpInside)
|
||||||
.subscribe(onNext: { _ in
|
.subscribe(onNext: { _ in
|
||||||
DLToast.show(text: "敬请期待")
|
AppRouter.push(Route.feedback)
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
//
|
||||||
|
// FeedbackSuccessView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class FeedbackSuccessView: UIView {
|
||||||
|
|
||||||
|
private let cardView = UIView()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
backgroundColor = UIColor.black.withAlphaComponent(0.66)
|
||||||
|
|
||||||
|
let pointerView = UIView()
|
||||||
|
pointerView.backgroundColor = .white
|
||||||
|
pointerView.transform = CGAffineTransform(rotationAngle: .pi / 4)
|
||||||
|
addSubview(pointerView)
|
||||||
|
pointerView.layoutChain.centerX().centerY(self, offset: 94).width(28).height(28)
|
||||||
|
|
||||||
|
cardView.backgroundColor = .white
|
||||||
|
cardView.layer.cornerRadius = 36
|
||||||
|
addSubview(cardView)
|
||||||
|
cardView.layoutChain.centerX().centerY(self, offset: -8).width(280).height(208)
|
||||||
|
|
||||||
|
let headerImageView = UIImageView(image: UIImage(named: "Mine/feedback_success_header"))
|
||||||
|
headerImageView.contentMode = .scaleToFill
|
||||||
|
headerImageView.layer.cornerRadius = 36
|
||||||
|
headerImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
headerImageView.clipsToBounds = true
|
||||||
|
cardView.addSubview(headerImageView)
|
||||||
|
headerImageView.layoutChain.top().left().right().height(100)
|
||||||
|
|
||||||
|
let iconView = UIImageView(image: UIImage(named: "Mine/feedback_success_icon"))
|
||||||
|
iconView.contentMode = .scaleAspectFit
|
||||||
|
cardView.addSubview(iconView)
|
||||||
|
iconView.layoutChain.top(-60).centerX().width(120).height(120)
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = "反馈提交成功"
|
||||||
|
titleLabel.font = .systemFont(ofSize: 24, weight: .semibold)
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#1D1D1D")
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
cardView.addSubview(titleLabel)
|
||||||
|
titleLabel.layoutChain.top(78).left(16).right(16)
|
||||||
|
|
||||||
|
let detailLabel = UILabel()
|
||||||
|
detailLabel.text = "感谢您的反馈,我们会及时查看并妥\n善处理相关问题"
|
||||||
|
detailLabel.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
detailLabel.textColor = UIColor(hexStr: "#767676")
|
||||||
|
detailLabel.textAlignment = .center
|
||||||
|
detailLabel.numberOfLines = 2
|
||||||
|
detailLabel.lineBreakMode = .byWordWrapping
|
||||||
|
cardView.addSubview(detailLabel)
|
||||||
|
detailLabel.layoutChain.topToBottomOfView(titleLabel, offset: 22).left(24).right(24)
|
||||||
|
}
|
||||||
|
|
||||||
|
func show(in superview: UIView) {
|
||||||
|
frame = superview.bounds
|
||||||
|
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
alpha = 0
|
||||||
|
cardView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||||
|
superview.addSubview(self)
|
||||||
|
UIView.animate(withDuration: 0.24, delay: 0, options: .curveEaseOut) {
|
||||||
|
self.alpha = 1
|
||||||
|
self.cardView.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismiss(completion: @escaping () -> Void) {
|
||||||
|
UIView.animate(withDuration: 0.2, animations: {
|
||||||
|
self.alpha = 0
|
||||||
|
self.cardView.transform = CGAffineTransform(scaleX: 0.96, y: 0.96)
|
||||||
|
}, completion: { _ in
|
||||||
|
self.removeFromSuperview()
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
//
|
||||||
|
// FeedbackVC.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
import HXPHPicker
|
||||||
|
|
||||||
|
final class FeedbackVC: BaseViewController {
|
||||||
|
|
||||||
|
private enum FeedbackCategory: String, CaseIterable {
|
||||||
|
case feature = "功能问题"
|
||||||
|
case suggestion = "优化建议"
|
||||||
|
case other = "其他"
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FeedbackDraft {
|
||||||
|
let category: FeedbackCategory
|
||||||
|
let detail: String
|
||||||
|
let images: [UIImage]
|
||||||
|
let contact: String
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rootView: FeedbackView!
|
||||||
|
private var selectedCategory: FeedbackCategory = .feature
|
||||||
|
private var selectedImages: [UIImage] = []
|
||||||
|
private var pendingDraft: FeedbackDraft?
|
||||||
|
private var isSubmitting = false
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
rootView = FeedbackView(frame: UIScreen.main.bounds)
|
||||||
|
view = rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bind() {
|
||||||
|
rootView.navView.onBackTapped = { [weak self] in
|
||||||
|
guard self?.isSubmitting == false else { return }
|
||||||
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView.categoryButtons.forEach { button in
|
||||||
|
button.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView.detailTextView.rx.text.orEmpty
|
||||||
|
.subscribe(onNext: { [weak self] text in
|
||||||
|
self?.handleDetailChange(text)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.contactTextField.rx.controlEvent(.editingDidEndOnExit)
|
||||||
|
.subscribe(onNext: { [weak self] in
|
||||||
|
self?.rootView.contactTextField.resignFirstResponder()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.onAddImage = { [weak self] in
|
||||||
|
self?.showAlbum()
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView.onDeleteImage = { [weak self] index in
|
||||||
|
guard let self, self.selectedImages.indices.contains(index) else { return }
|
||||||
|
self.selectedImages.remove(at: index)
|
||||||
|
self.rootView.updateImages(self.selectedImages)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView.saveBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] in
|
||||||
|
self?.submit()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func categoryTapped(_ sender: UIButton) {
|
||||||
|
let categories = FeedbackCategory.allCases
|
||||||
|
guard categories.indices.contains(sender.tag) else { return }
|
||||||
|
let category = categories[sender.tag]
|
||||||
|
selectedCategory = category
|
||||||
|
rootView.updateCategory(selectedIndex: sender.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleDetailChange(_ text: String) {
|
||||||
|
let limitedText = text.count > 200 ? String(text.prefix(200)) : text
|
||||||
|
if limitedText != text {
|
||||||
|
rootView.detailTextView.text = limitedText
|
||||||
|
rootView.detailTextView.selectedRange = NSRange(location: limitedText.count, length: 0)
|
||||||
|
}
|
||||||
|
rootView.updateDetail(text: limitedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAlbum() {
|
||||||
|
let remainingCount = 3 - selectedImages.count
|
||||||
|
guard remainingCount > 0, !isSubmitting else { return }
|
||||||
|
|
||||||
|
let config = PhotoTools.getWXPickerConfig()
|
||||||
|
config.selectOptions = [.photo]
|
||||||
|
config.selectMode = .multiple
|
||||||
|
config.maximumSelectedCount = remainingCount
|
||||||
|
config.allowSyncICloudWhenSelectPhoto = false
|
||||||
|
config.previewView.bottomView.editButtonHidden = true
|
||||||
|
config.photoList.allowAddCamera = false
|
||||||
|
|
||||||
|
let pickerController = PhotoPickerController(picker: config)
|
||||||
|
pickerController.pickerDelegate = self
|
||||||
|
pickerController.modalPresentationStyle = .fullScreen
|
||||||
|
present(pickerController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func submit() {
|
||||||
|
guard !isSubmitting else { return }
|
||||||
|
view.endEditing(true)
|
||||||
|
|
||||||
|
let detail = (rootView.detailTextView.text ?? "")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !detail.isEmpty else {
|
||||||
|
DLToast.show(text: "请补充详细问题或意见")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDraft = FeedbackDraft(
|
||||||
|
category: selectedCategory,
|
||||||
|
detail: detail,
|
||||||
|
images: selectedImages,
|
||||||
|
contact: rootView.contactTextField.text ?? ""
|
||||||
|
)
|
||||||
|
isSubmitting = true
|
||||||
|
rootView.setSubmitting(true)
|
||||||
|
|
||||||
|
let successView = FeedbackSuccessView()
|
||||||
|
successView.show(in: view)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self, weak successView] in
|
||||||
|
guard let self, let successView else { return }
|
||||||
|
successView.dismiss {
|
||||||
|
AppRouter.shared.popOrDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FeedbackVC: PhotoPickerControllerDelegate {
|
||||||
|
|
||||||
|
func pickerController(
|
||||||
|
_ pickerController: PhotoPickerController,
|
||||||
|
didFinishSelection result: PickerResult
|
||||||
|
) {
|
||||||
|
let remainingCount = max(0, 3 - selectedImages.count)
|
||||||
|
guard remainingCount > 0 else { return }
|
||||||
|
|
||||||
|
result.getImage { _, _, _ in
|
||||||
|
} completionHandler: { [weak self] images in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let self else { return }
|
||||||
|
self.selectedImages.append(contentsOf: images.prefix(remainingCount))
|
||||||
|
self.selectedImages = Array(self.selectedImages.prefix(3))
|
||||||
|
self.rootView.updateImages(self.selectedImages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerController(didCancel pickerController: PhotoPickerController) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,398 @@
|
||||||
|
//
|
||||||
|
// FeedbackView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class FeedbackView: UIView {
|
||||||
|
|
||||||
|
let navView = BaseNavigationView(title: "问题反馈")
|
||||||
|
let detailTextView = UITextView()
|
||||||
|
let contactTextField = UITextField()
|
||||||
|
let saveBtn = UIButton(type: .custom)
|
||||||
|
private(set) var categoryButtons: [UIButton] = []
|
||||||
|
|
||||||
|
var onAddImage: (() -> Void)?
|
||||||
|
var onDeleteImage: ((Int) -> Void)?
|
||||||
|
|
||||||
|
private let scrollView = UIScrollView()
|
||||||
|
private let contentView = UIView()
|
||||||
|
private let detailPlaceholderLabel = UILabel()
|
||||||
|
private let countLabel = UILabel()
|
||||||
|
private let imagesCollectionView: UICollectionView
|
||||||
|
private var images: [UIImage] = []
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
layout.minimumLineSpacing = 8
|
||||||
|
layout.minimumInteritemSpacing = 8
|
||||||
|
layout.itemSize = CGSize(width: 100, height: 100)
|
||||||
|
imagesCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = UIColor(hexStr: "#F8F8F8")
|
||||||
|
setupUI()
|
||||||
|
updateCategory(selectedIndex: 0)
|
||||||
|
updateDetail(text: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
addSubview(navView)
|
||||||
|
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
|
||||||
|
|
||||||
|
saveBtn.setTitle("保存", for: .normal)
|
||||||
|
saveBtn.setTitleColor(.white, for: .normal)
|
||||||
|
saveBtn.titleLabel?.font = FontManager.boboBold(18)
|
||||||
|
saveBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||||
|
saveBtn.layer.cornerRadius = 24
|
||||||
|
saveBtn.clipsToBounds = true
|
||||||
|
addSubview(saveBtn)
|
||||||
|
saveBtn.layoutChain
|
||||||
|
.left(28)
|
||||||
|
.right(28)
|
||||||
|
.bottom(kSafeBottomMargin + 22)
|
||||||
|
.height(56)
|
||||||
|
|
||||||
|
scrollView.alwaysBounceVertical = false
|
||||||
|
scrollView.keyboardDismissMode = .onDrag
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
addSubview(scrollView)
|
||||||
|
scrollView.layoutChain
|
||||||
|
.topToBottomOfView(navView)
|
||||||
|
.edgesHorzontal()
|
||||||
|
.bottomToTopOfView(saveBtn, offset: -12)
|
||||||
|
|
||||||
|
scrollView.addSubview(contentView)
|
||||||
|
contentView.layoutChain.edges().widthToView(scrollView)
|
||||||
|
|
||||||
|
let typeTitle = makeSectionTitle(required: true, title: "您想反馈的功能类型")
|
||||||
|
contentView.addSubview(typeTitle)
|
||||||
|
typeTitle.layoutChain.top(14).left(16).right(16)
|
||||||
|
|
||||||
|
let titles = ["功能问题", "优化建议", "其他"]
|
||||||
|
categoryButtons = titles.enumerated().map { index, title in
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.tag = index
|
||||||
|
button.setTitle(title, for: .normal)
|
||||||
|
button.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
button.layer.cornerRadius = 12
|
||||||
|
button.layer.borderWidth = 1
|
||||||
|
contentView.addSubview(button)
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstButton = categoryButtons[0]
|
||||||
|
let secondButton = categoryButtons[1]
|
||||||
|
let thirdButton = categoryButtons[2]
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
firstButton.topAnchor.constraint(equalTo: typeTitle.bottomAnchor, constant: 12),
|
||||||
|
firstButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||||
|
firstButton.heightAnchor.constraint(equalToConstant: 34),
|
||||||
|
secondButton.topAnchor.constraint(equalTo: firstButton.topAnchor),
|
||||||
|
secondButton.leadingAnchor.constraint(equalTo: firstButton.trailingAnchor, constant: 16),
|
||||||
|
secondButton.widthAnchor.constraint(equalTo: firstButton.widthAnchor),
|
||||||
|
secondButton.heightAnchor.constraint(equalTo: firstButton.heightAnchor),
|
||||||
|
thirdButton.topAnchor.constraint(equalTo: firstButton.topAnchor),
|
||||||
|
thirdButton.leadingAnchor.constraint(equalTo: secondButton.trailingAnchor, constant: 16),
|
||||||
|
thirdButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
thirdButton.widthAnchor.constraint(equalTo: firstButton.widthAnchor),
|
||||||
|
thirdButton.heightAnchor.constraint(equalTo: firstButton.heightAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
let detailTitle = makeSectionTitle(required: true, title: "请补充详细问题或意见")
|
||||||
|
contentView.addSubview(detailTitle)
|
||||||
|
detailTitle.layoutChain
|
||||||
|
.topToBottomOfView(firstButton, offset: 20)
|
||||||
|
.left(16)
|
||||||
|
.right(16)
|
||||||
|
|
||||||
|
let detailContainer = UIView()
|
||||||
|
detailContainer.backgroundColor = .white
|
||||||
|
detailContainer.layer.cornerRadius = 16
|
||||||
|
contentView.addSubview(detailContainer)
|
||||||
|
detailContainer.layoutChain
|
||||||
|
.topToBottomOfView(detailTitle, offset: 12)
|
||||||
|
.left(16)
|
||||||
|
.right(16)
|
||||||
|
.height(150)
|
||||||
|
|
||||||
|
detailTextView.backgroundColor = .clear
|
||||||
|
detailTextView.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
detailTextView.textColor = UIColor(hexStr: "#293445")
|
||||||
|
detailTextView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 28, right: 10)
|
||||||
|
detailTextView.textContainer.lineFragmentPadding = 0
|
||||||
|
detailTextView.isScrollEnabled = true
|
||||||
|
detailTextView.returnKeyType = .default
|
||||||
|
detailContainer.addSubview(detailTextView)
|
||||||
|
detailTextView.layoutChain.edges()
|
||||||
|
|
||||||
|
detailPlaceholderLabel.text = "请在这里输入内容"
|
||||||
|
detailPlaceholderLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
detailPlaceholderLabel.textColor = UIColor(hexStr: "#B5B5B5")
|
||||||
|
detailPlaceholderLabel.isUserInteractionEnabled = false
|
||||||
|
detailContainer.addSubview(detailPlaceholderLabel)
|
||||||
|
detailPlaceholderLabel.layoutChain.top(12).left(14).right(14)
|
||||||
|
|
||||||
|
countLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
||||||
|
countLabel.textColor = UIColor(hexStr: "#767676")
|
||||||
|
countLabel.textAlignment = .right
|
||||||
|
detailContainer.addSubview(countLabel)
|
||||||
|
countLabel.layoutChain.right(14).bottom(10)
|
||||||
|
|
||||||
|
let imageTitle = makeSectionTitle(
|
||||||
|
required: false,
|
||||||
|
title: "请提供相关问题的图片",
|
||||||
|
detail: "(选填,最多可上传三张)"
|
||||||
|
)
|
||||||
|
contentView.addSubview(imageTitle)
|
||||||
|
imageTitle.layoutChain
|
||||||
|
.topToBottomOfView(detailContainer, offset: 18)
|
||||||
|
.left(16)
|
||||||
|
.right(16)
|
||||||
|
|
||||||
|
imagesCollectionView.backgroundColor = .clear
|
||||||
|
imagesCollectionView.isScrollEnabled = false
|
||||||
|
imagesCollectionView.showsHorizontalScrollIndicator = false
|
||||||
|
imagesCollectionView.dataSource = self
|
||||||
|
imagesCollectionView.delegate = self
|
||||||
|
imagesCollectionView.register(
|
||||||
|
FeedbackImageCell.self,
|
||||||
|
forCellWithReuseIdentifier: FeedbackImageCell.reuseIdentifier
|
||||||
|
)
|
||||||
|
imagesCollectionView.register(
|
||||||
|
FeedbackAddImageCell.self,
|
||||||
|
forCellWithReuseIdentifier: FeedbackAddImageCell.reuseIdentifier
|
||||||
|
)
|
||||||
|
contentView.addSubview(imagesCollectionView)
|
||||||
|
imagesCollectionView.layoutChain
|
||||||
|
.topToBottomOfView(imageTitle, offset: 12)
|
||||||
|
.left(16)
|
||||||
|
.right(16)
|
||||||
|
.height(100)
|
||||||
|
|
||||||
|
let contactTitle = makeSectionTitle(
|
||||||
|
required: false,
|
||||||
|
title: "联系方式",
|
||||||
|
detail: "(选填,可以留下您的手机号、微信、邮箱)"
|
||||||
|
)
|
||||||
|
contentView.addSubview(contactTitle)
|
||||||
|
contactTitle.layoutChain
|
||||||
|
.topToBottomOfView(imagesCollectionView, offset: 18)
|
||||||
|
.left(16)
|
||||||
|
.right(16)
|
||||||
|
|
||||||
|
contactTextField.backgroundColor = .white
|
||||||
|
contactTextField.layer.cornerRadius = 20
|
||||||
|
contactTextField.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
contactTextField.textColor = UIColor(hexStr: "#293445")
|
||||||
|
contactTextField.returnKeyType = .done
|
||||||
|
contactTextField.clearButtonMode = .whileEditing
|
||||||
|
contactTextField.attributedPlaceholder = NSAttributedString(
|
||||||
|
string: "请输入您的联系方式",
|
||||||
|
attributes: [
|
||||||
|
.foregroundColor: UIColor(hexStr: "#B5B5B5"),
|
||||||
|
.font: UIFont.systemFont(ofSize: 14, weight: .medium)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
contactTextField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 1))
|
||||||
|
contactTextField.leftViewMode = .always
|
||||||
|
contactTextField.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 1))
|
||||||
|
contactTextField.rightViewMode = .unlessEditing
|
||||||
|
contentView.addSubview(contactTextField)
|
||||||
|
contactTextField.layoutChain
|
||||||
|
.topToBottomOfView(contactTitle, offset: 12)
|
||||||
|
.left(16)
|
||||||
|
.right(16)
|
||||||
|
.height(40)
|
||||||
|
.bottom(24)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeSectionTitle(required: Bool, title: String, detail: String? = nil) -> UILabel {
|
||||||
|
let label = UILabel()
|
||||||
|
label.numberOfLines = 0
|
||||||
|
|
||||||
|
let text = NSMutableAttributedString()
|
||||||
|
if required {
|
||||||
|
text.append(NSAttributedString(
|
||||||
|
string: "*",
|
||||||
|
attributes: [
|
||||||
|
.font: UIFont.systemFont(ofSize: 17, weight: .medium),
|
||||||
|
.foregroundColor: UIColor(hexStr: "#F64545")
|
||||||
|
]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
text.append(NSAttributedString(
|
||||||
|
string: title,
|
||||||
|
attributes: [
|
||||||
|
.font: UIFont.systemFont(ofSize: 16, weight: .medium),
|
||||||
|
.foregroundColor: UIColor(hexStr: "#293445")
|
||||||
|
]
|
||||||
|
))
|
||||||
|
if let detail {
|
||||||
|
text.append(NSAttributedString(
|
||||||
|
string: detail,
|
||||||
|
attributes: [
|
||||||
|
.font: UIFont.systemFont(ofSize: 12, weight: .medium),
|
||||||
|
.foregroundColor: UIColor(hexStr: "#B5B5B5")
|
||||||
|
]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
label.attributedText = text
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCategory(selectedIndex: Int) {
|
||||||
|
for (index, button) in categoryButtons.enumerated() {
|
||||||
|
let isSelected = index == selectedIndex
|
||||||
|
button.backgroundColor = isSelected ? UIColor(hexStr: "#08B2F5") : .clear
|
||||||
|
button.layer.borderColor = UIColor(
|
||||||
|
hexStr: isSelected ? "#08B2F5" : "#D6D6D6"
|
||||||
|
).cgColor
|
||||||
|
button.setTitleColor(
|
||||||
|
isSelected ? .white : UIColor(hexStr: "#293445"),
|
||||||
|
for: .normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDetail(text: String) {
|
||||||
|
detailPlaceholderLabel.isHidden = !text.isEmpty
|
||||||
|
countLabel.text = "\(text.count)/200"
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateImages(_ images: [UIImage]) {
|
||||||
|
self.images = Array(images.prefix(3))
|
||||||
|
imagesCollectionView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubmitting(_ submitting: Bool) {
|
||||||
|
saveBtn.isEnabled = !submitting
|
||||||
|
saveBtn.alpha = submitting ? 0.65 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FeedbackView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||||
|
|
||||||
|
func collectionView(
|
||||||
|
_ collectionView: UICollectionView,
|
||||||
|
layout collectionViewLayout: UICollectionViewLayout,
|
||||||
|
sizeForItemAt indexPath: IndexPath
|
||||||
|
) -> CGSize {
|
||||||
|
let width = min(100, floor((collectionView.bounds.width - 16) / 3))
|
||||||
|
return CGSize(width: width, height: 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
images.count + (images.count < 3 ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(
|
||||||
|
_ collectionView: UICollectionView,
|
||||||
|
cellForItemAt indexPath: IndexPath
|
||||||
|
) -> UICollectionViewCell {
|
||||||
|
if indexPath.item < images.count {
|
||||||
|
guard let cell = collectionView.dequeueReusableCell(
|
||||||
|
withReuseIdentifier: FeedbackImageCell.reuseIdentifier,
|
||||||
|
for: indexPath
|
||||||
|
) as? FeedbackImageCell else {
|
||||||
|
return UICollectionViewCell()
|
||||||
|
}
|
||||||
|
cell.configure(image: images[indexPath.item])
|
||||||
|
cell.onDelete = { [weak self, weak cell] in
|
||||||
|
guard let self, let cell,
|
||||||
|
let currentIndexPath = collectionView.indexPath(for: cell) else { return }
|
||||||
|
self.onDeleteImage?(currentIndexPath.item)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let cell = collectionView.dequeueReusableCell(
|
||||||
|
withReuseIdentifier: FeedbackAddImageCell.reuseIdentifier,
|
||||||
|
for: indexPath
|
||||||
|
) as? FeedbackAddImageCell else {
|
||||||
|
return UICollectionViewCell()
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
guard indexPath.item == images.count, images.count < 3 else { return }
|
||||||
|
onAddImage?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FeedbackImageCell: UICollectionViewCell {
|
||||||
|
static let reuseIdentifier = "FeedbackImageCell"
|
||||||
|
|
||||||
|
var onDelete: (() -> Void)?
|
||||||
|
|
||||||
|
private let imageView = UIImageView()
|
||||||
|
private let deleteButton = UIButton(type: .custom)
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.layer.cornerRadius = 16
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
contentView.addSubview(imageView)
|
||||||
|
imageView.layoutChain.edges()
|
||||||
|
|
||||||
|
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 10, weight: .bold)
|
||||||
|
deleteButton.setImage(UIImage(systemName: "xmark", withConfiguration: symbolConfig), for: .normal)
|
||||||
|
deleteButton.tintColor = UIColor(hexStr: "#293445")
|
||||||
|
deleteButton.backgroundColor = UIColor.white.withAlphaComponent(0.92)
|
||||||
|
deleteButton.layer.cornerRadius = 11
|
||||||
|
deleteButton.layer.shadowColor = UIColor.black.withAlphaComponent(0.12).cgColor
|
||||||
|
deleteButton.layer.shadowOpacity = 1
|
||||||
|
deleteButton.layer.shadowRadius = 3
|
||||||
|
deleteButton.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||||||
|
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
|
||||||
|
contentView.addSubview(deleteButton)
|
||||||
|
deleteButton.layoutChain.top(6).right(6).width(22).height(22)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
imageView.image = nil
|
||||||
|
onDelete = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(image: UIImage) {
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func deleteTapped() {
|
||||||
|
onDelete?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FeedbackAddImageCell: UICollectionViewCell {
|
||||||
|
static let reuseIdentifier = "FeedbackAddImageCell"
|
||||||
|
|
||||||
|
private let addImageView = UIImageView(image: UIImage(named: "Mine/feedback_add"))
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
contentView.backgroundColor = .white
|
||||||
|
contentView.layer.cornerRadius = 16
|
||||||
|
addImageView.contentMode = .scaleAspectFit
|
||||||
|
contentView.addSubview(addImageView)
|
||||||
|
addImageView.layoutChain.centerX().centerY().width(48).height(48)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||