- 查找位置
|
|
@ -190,6 +190,10 @@
|
|||
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */; };
|
||||
30A87A662FEE843E0095E7C6 /* CreateBubbleDoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */; };
|
||||
30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */; };
|
||||
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */; };
|
||||
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; };
|
||||
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
|
||||
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */; };
|
||||
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */; };
|
||||
30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; };
|
||||
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */; };
|
||||
|
|
@ -241,6 +245,7 @@
|
|||
30DC185C2FD11E7A0041DCD1 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18572FD11E7A0041DCD1 /* WebViewController.swift */; };
|
||||
30DC185E2FD1211D0041DCD1 /* VipRightsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC185D2FD1211D0041DCD1 /* VipRightsVC.swift */; };
|
||||
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC185F2FD12A020041DCD1 /* VipWaivePopView.swift */; };
|
||||
30EBF7202FEFD6F2009A8A87 /* GroupChooseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EBF71F2FEFD6F2009A8A87 /* GroupChooseView.swift */; };
|
||||
30EFF2992FD65FB000EB35D4 /* VoicePlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF2982FD65FB000EB35D4 /* VoicePlayerManager.swift */; };
|
||||
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF29A2FD668C900EB35D4 /* VoiceRecordView.swift */; };
|
||||
30EFF3A42FD7C5A300EB35D4 /* GroupSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3A32FD7C5A300EB35D4 /* GroupSettingView.swift */; };
|
||||
|
|
@ -467,6 +472,10 @@
|
|||
30A87A632FEE75520095E7C6 /* CreateBubbleTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleTipsView.swift; sourceTree = "<group>"; };
|
||||
30A87A652FEE843E0095E7C6 /* CreateBubbleDoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubbleDoneView.swift; sourceTree = "<group>"; };
|
||||
30A87A672FEE86560095E7C6 /* CreateBubblePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBubblePopView.swift; sourceTree = "<group>"; };
|
||||
30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationView.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>"; };
|
||||
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultView.swift; sourceTree = "<group>"; };
|
||||
30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = "<group>"; };
|
||||
30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = "<group>"; };
|
||||
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -519,6 +528,7 @@
|
|||
30DC18572FD11E7A0041DCD1 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||
30DC185D2FD1211D0041DCD1 /* VipRightsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRightsVC.swift; sourceTree = "<group>"; };
|
||||
30DC185F2FD12A020041DCD1 /* VipWaivePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipWaivePopView.swift; sourceTree = "<group>"; };
|
||||
30EBF71F2FEFD6F2009A8A87 /* GroupChooseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChooseView.swift; sourceTree = "<group>"; };
|
||||
30EFF2982FD65FB000EB35D4 /* VoicePlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoicePlayerManager.swift; sourceTree = "<group>"; };
|
||||
30EFF29A2FD668C900EB35D4 /* VoiceRecordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceRecordView.swift; sourceTree = "<group>"; };
|
||||
30EFF3A02FD7A47900EB35D4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.storyboard"; sourceTree = "<group>"; };
|
||||
|
|
@ -958,6 +968,7 @@
|
|||
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */,
|
||||
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */,
|
||||
30D87CDC2FDFF07500E958FD /* InteractionView.swift */,
|
||||
30A87A692FEF59E60095E7C6 /* SearchLocation */,
|
||||
30A87A5C2FEE711C0095E7C6 /* Bubble */,
|
||||
30CCDE4F2FE2782700F5214A /* SignIn */,
|
||||
30CCDE562FE39F6B00F5214A /* SOS */,
|
||||
|
|
@ -1287,6 +1298,18 @@
|
|||
path = Bubble;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30A87A692FEF59E60095E7C6 /* SearchLocation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */,
|
||||
30A87A6A2FEF5B950095E7C6 /* SearchLocationView.swift */,
|
||||
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */,
|
||||
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */,
|
||||
30EBF71F2FEFD6F2009A8A87 /* GroupChooseView.swift */,
|
||||
);
|
||||
path = SearchLocation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1699,6 +1722,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */,
|
||||
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */,
|
||||
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */,
|
||||
305A768A2FCA8C7000227D26 /* Single+Response.swift in Sources */,
|
||||
|
|
@ -1796,6 +1820,7 @@
|
|||
30EFF3C02FD958AE00EB35D4 /* AccountVC.swift in Sources */,
|
||||
30C4C0202FDC0EC5009215C1 /* GroupInfoView.swift in Sources */,
|
||||
305A76BF2FCA8C7000227D26 /* ListService.swift in Sources */,
|
||||
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */,
|
||||
305A76C02FCA8C7000227D26 /* BaseNavigationController.swift in Sources */,
|
||||
305A76C12FCA8C7000227D26 /* BaseViewController.swift in Sources */,
|
||||
3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */,
|
||||
|
|
@ -1839,6 +1864,7 @@
|
|||
305A76D82FCA8C7000227D26 /* Action.swift in Sources */,
|
||||
30EFF3B02FD8122E00EB35D4 /* GroupTagListView.swift in Sources */,
|
||||
305A76D92FCA8C7000227D26 /* Action+Internal.swift in Sources */,
|
||||
30EBF7202FEFD6F2009A8A87 /* GroupChooseView.swift in Sources */,
|
||||
305A76DA2FCA8C7000227D26 /* Button+Action.swift in Sources */,
|
||||
305A76DB2FCA8C7000227D26 /* Control+Action.swift in Sources */,
|
||||
305A76DC2FCA8C7000227D26 /* InputSubject.swift in Sources */,
|
||||
|
|
@ -1849,6 +1875,7 @@
|
|||
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */,
|
||||
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */,
|
||||
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */,
|
||||
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */,
|
||||
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */,
|
||||
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
|
||||
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
|
||||
|
|
@ -1880,6 +1907,7 @@
|
|||
305A76EA2FCA8C7000227D26 /* OneTapLoginView.swift in Sources */,
|
||||
305A76EB2FCA8C7000227D26 /* CircleMember.swift in Sources */,
|
||||
30CCDE5A2FE39F9D00F5214A /* SOSViewController.swift in Sources */,
|
||||
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */,
|
||||
30D74AB42FEA25B90050EB2C /* ViewedModel.swift in Sources */,
|
||||
305A76EC2FCA8C7000227D26 /* MemberAnnotation.swift in Sources */,
|
||||
305A76ED2FCA8C7000227D26 /* MemberAnnotationView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,18 @@ enum SystemAPI {
|
|||
|
||||
/// SOS
|
||||
case sos(enable: Bool)
|
||||
|
||||
/// 查找
|
||||
/// - Parameters:
|
||||
/// - op_type: plate_num 车牌
|
||||
/// - phone 手机号
|
||||
case search(op_type: String, number: String)
|
||||
|
||||
/// 手机归属地
|
||||
/// - Parameters:
|
||||
/// - phone 手机号
|
||||
case phoneArea(phone: String)
|
||||
|
||||
}
|
||||
|
||||
extension SystemAPI: MultiTargetProtocol {
|
||||
|
|
@ -37,14 +49,18 @@ extension SystemAPI: MultiTargetProtocol {
|
|||
return "api/weixin/service"
|
||||
case .sos:
|
||||
return "mapi/sos/operate"
|
||||
case .search:
|
||||
return "mapi/car/search"
|
||||
case .phoneArea:
|
||||
return "mapi/phonearea"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .userConfig, .wechatService:
|
||||
case .userConfig, .wechatService, .phoneArea:
|
||||
return .get
|
||||
case .sendCode, .sos:
|
||||
default:
|
||||
return .post
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +83,16 @@ extension SystemAPI: MultiTargetProtocol {
|
|||
params["enable"] = enable
|
||||
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||
|
||||
case let .search(op_type, number):
|
||||
var params = Parameters()
|
||||
params["op_type"] = op_type
|
||||
params["number"] = number
|
||||
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||
|
||||
case let .phoneArea(phone):
|
||||
var params = Parameters()
|
||||
params["phone"] = phone
|
||||
return .requestParameters(parameters: params, encoding: URLEncoding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,9 @@ extension UserAPI: MultiTargetProtocol {
|
|||
case let .bubble(enable, keep_time):
|
||||
var params = Parameters()
|
||||
params["enable"] = enable
|
||||
params["keep_time"] = keep_time
|
||||
if keep_time != -1 {
|
||||
params["keep_time"] = keep_time
|
||||
}
|
||||
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "text@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "text@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "vip_pop@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "vip_pop@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "schedule@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "schedule@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1554@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1554@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/SearchLocation/bg_1.imageset/Group_1554@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
QuickLocation/Assets.xcassets/SearchLocation/bg_1.imageset/Group_1554@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 154 KiB |
22
QuickLocation/Assets.xcassets/SearchLocation/contacts.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1677@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1677@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/SearchLocation/contacts.imageset/Group_1677@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
QuickLocation/Assets.xcassets/SearchLocation/contacts.imageset/Group_1677@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_686@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_686@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/SearchLocation/done.imageset/Group_686@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
QuickLocation/Assets.xcassets/SearchLocation/done.imageset/Group_686@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
22
QuickLocation/Assets.xcassets/SearchLocation/done_off.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_687@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_687@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/SearchLocation/done_off.imageset/Group_687@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
QuickLocation/Assets.xcassets/SearchLocation/done_off.imageset/Group_687@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "mask@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "mask@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 60 KiB |
22
QuickLocation/Assets.xcassets/SearchLocation/result_bg.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "result_bg@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "result_bg@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/SearchLocation/result_bg.imageset/result_bg@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
QuickLocation/Assets.xcassets/SearchLocation/result_bg.imageset/result_bg@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
|
|
@ -31,6 +31,8 @@ extension Notification.Name {
|
|||
static let RefreshGroupInfoNotification = Notification.Name("RefreshGroupInfoNotification")
|
||||
/// 刷新IM圈子列表数据
|
||||
static let RefreshIMGroupListNotification = Notification.Name("RefreshIMGroupListNotification")
|
||||
/// 查看成员位置
|
||||
static let ShowMemberLocationNotification = Notification.Name("ShowMemberLocationNotification")
|
||||
/// 支付宝/微信支付结果回调
|
||||
static let RequestOrderPayStatusNotification = Notification.Name("RequestOrderPayStatusNotification")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -512,3 +512,10 @@ extension String {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var isPhoneNumber: Bool {
|
||||
let reg = "^1[3-9]\\d{9}$"
|
||||
return NSPredicate(format: "SELF MATCHES %@", reg).evaluate(with: self)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ class BaseViewController: UIViewController {
|
|||
(tabBarController as? MainTabBarController)?.updateTabBarVisibility()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// popToRoot 动画完成后 navigation stack 才减到 1,此时再恢复 tabBar
|
||||
(tabBarController as? MainTabBarController)?.updateTabBarVisibility()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ enum Route: String {
|
|||
case scheduleViewed = "scheduleViewed"
|
||||
/// 创建气泡
|
||||
case createBubble = "createBubble"
|
||||
/// 查找位置
|
||||
case searchLocation = "searchLocation"
|
||||
/// 查找位置结果
|
||||
case searchLocationResult = "searchLocationResult"
|
||||
}
|
||||
|
||||
extension Route: RouterTarget {
|
||||
|
|
@ -305,6 +309,20 @@ extension AppRouter: AppRouterProtocol {
|
|||
vc.isNeedLogin = true
|
||||
return vc
|
||||
}
|
||||
|
||||
// MARK: - 查找位置
|
||||
AppRouter.register(Route.searchLocation) { url, parameters in
|
||||
let vc = SearchLocationVC()
|
||||
// vc.isNeedLogin = true
|
||||
return vc
|
||||
}
|
||||
|
||||
// MARK: - 查找位置结果
|
||||
AppRouter.register(Route.searchLocationResult) { url, parameters in
|
||||
SearchLocationResultVC(phone: parameters["phone"].safeString,
|
||||
code: parameters["code"].safeInt,
|
||||
memberData: parameters["memberData"].safeDictionary as! [String : Any])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,32 @@ class CreateBubbleDoneView: UIView {
|
|||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
private var countdownTimer: Timer?
|
||||
private var endDate: Date = Date()
|
||||
|
||||
func startCountdown(endDate: Date) {
|
||||
self.endDate = endDate
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
self?.updateTime()
|
||||
}
|
||||
updateTime()
|
||||
}
|
||||
|
||||
private func updateTime() {
|
||||
let remaining = endDate.timeIntervalSince(Date())
|
||||
if remaining <= 0 {
|
||||
timeLab.text = "00:00:00"
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
return
|
||||
}
|
||||
let hours = Int(remaining) / 3600
|
||||
let minutes = (Int(remaining) % 3600) / 60
|
||||
let seconds = Int(remaining) % 60
|
||||
timeLab.text = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
private func setupRx() {
|
||||
|
||||
}
|
||||
|
|
@ -23,6 +49,7 @@ class CreateBubbleDoneView: UIView {
|
|||
addSubview(timeLab)
|
||||
addSubview(messageView)
|
||||
addSubview(iconView)
|
||||
addSubview(scrollView)
|
||||
addSubview(popupBtn)
|
||||
|
||||
titleLab.layoutChain
|
||||
|
|
@ -38,7 +65,7 @@ class CreateBubbleDoneView: UIView {
|
|||
.centerY(timeLab)
|
||||
|
||||
messageView.layoutChain
|
||||
.topToBottomOfView(timeLab, offset: 41)
|
||||
.topToBottomOfView(timeLab, offset: 25)
|
||||
.left(79)
|
||||
.right(30)
|
||||
|
||||
|
|
@ -53,6 +80,11 @@ class CreateBubbleDoneView: UIView {
|
|||
.height(50)
|
||||
.centerX()
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
|
||||
scrollView.layoutChain
|
||||
.topToBottomOfView(messageView, offset: 20)
|
||||
.edgesHorzontal()
|
||||
.bottomToTopOfView(popupBtn, offset: -20)
|
||||
}
|
||||
|
||||
lazy var titleLab: UILabel = {
|
||||
|
|
@ -114,35 +146,27 @@ class CreateBubbleDoneView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
private var countdownTimer: Timer?
|
||||
private var endDate: Date = Date()
|
||||
lazy var scrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
view.backgroundColor = .clear
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.bounces = false
|
||||
|
||||
func startCountdown(endDate: Date) {
|
||||
self.endDate = endDate
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
self?.updateTime()
|
||||
}
|
||||
updateTime()
|
||||
}
|
||||
let contentView = UIView()
|
||||
contentView.backgroundColor = .clear
|
||||
view.addSubview(contentView)
|
||||
contentView.layoutChain.edges().widthToView(view)
|
||||
|
||||
private func updateTime() {
|
||||
let remaining = endDate.timeIntervalSince(Date())
|
||||
if remaining <= 0 {
|
||||
timeLab.text = "00:00:00"
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
return
|
||||
}
|
||||
let hours = Int(remaining) / 3600
|
||||
let minutes = (Int(remaining) % 3600) / 60
|
||||
let seconds = Int(remaining) % 60
|
||||
timeLab.text = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
let textImgView = UIImageView()
|
||||
textImgView.image = UIImage(named: "Bubble/text")
|
||||
contentView.addSubview(textImgView)
|
||||
textImgView.layoutChain
|
||||
.edgesVertical()
|
||||
.edgesHorzontal(30)
|
||||
.heightToWidth(1450/630)
|
||||
|
||||
deinit {
|
||||
countdownTimer?.invalidate()
|
||||
}
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
|
|
@ -156,4 +180,8 @@ class CreateBubbleDoneView: UIView {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
countdownTimer?.invalidate()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,18 @@ class CreateBubbleVC: BaseViewController {
|
|||
view = rootView
|
||||
}
|
||||
|
||||
private var doneBtn: UIButton {
|
||||
rootView.createBubbleTipsView.doneBtn
|
||||
}
|
||||
|
||||
private var popupBtn: UIButton {
|
||||
rootView.createBubbleDoneView.popupBtn
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
rootView.createBubbleTipsView.doneBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
doneBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if AppContextManager.shared.vip > 1 {
|
||||
let hours = self.rootView.createBubbleTiemView.selectedHour.value
|
||||
|
|
@ -31,12 +39,28 @@ class CreateBubbleVC: BaseViewController {
|
|||
CreateBubblePopView.show()
|
||||
}
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
popupBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.showConfirmPop(title: "温馨提醒",
|
||||
message: "您确定要弹出这个气泡吗?",
|
||||
confirmText: "爆裂",
|
||||
confirmBlock: {
|
||||
self.requestSetBubble(enable: false, keep_time: -1)
|
||||
}, cancelText: "取消")
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func requestSetBubble(enable: Bool, keep_time: Int) {
|
||||
DLToast.showLoading()
|
||||
UserService.setBubble(enable: enable, keep_time: keep_time).subscribe(onNext: { response in
|
||||
DLToast.dismiss()
|
||||
guard enable else {
|
||||
DLToast.show(text: "气泡已弹出") {
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}
|
||||
return
|
||||
}
|
||||
self.rootView.navTitleLabel.text = "活动气泡"
|
||||
self.rootView.createBubbleDoneView.messageLab.text = self.rootView.createBubbleTipsView.messageText
|
||||
let endDate = Calendar.current.date(byAdding: .hour, value: keep_time, to: Date()) ?? Date()
|
||||
|
|
|
|||
|
|
@ -133,10 +133,9 @@ class GroupMemberView: UIView {
|
|||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.estimatedRowHeight = 76
|
||||
tableView.bounces = false
|
||||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.isScrollEnabled = false
|
||||
tableView.register(GroupMemberCell.self)
|
||||
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -88,21 +88,20 @@ class HomeView: UIView {
|
|||
.disposed(by: disposeBag)
|
||||
|
||||
|
||||
// tableView 到达顶部继续下拉时,改由 GroupMemberView 的 pan 手势接管
|
||||
// 嵌套滑动协调:contentOffset 只复位位置,scroll 状态由 pan handler + panVelocity 管理
|
||||
groupMemberView.tableView.rx.contentOffset
|
||||
.subscribe(onNext: { [weak self] offset in
|
||||
guard let self = self else { return }
|
||||
if self.isSubCanScroll {
|
||||
if offset.y <= 0 {
|
||||
self.isSubCanScroll = false
|
||||
self.groupMemberView.tableView.setContentOffset(.zero, animated: false)
|
||||
}
|
||||
} else if offset.y != 0 {
|
||||
if !self.isSubCanScroll && offset.y != 0 {
|
||||
self.groupMemberView.tableView.setContentOffset(.zero, animated: false)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// 查找位置
|
||||
searchLottieView.rx.tapGesture.subscribe { _ in
|
||||
AppRouter.push(Route.searchLocation)
|
||||
}.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
|
|
@ -315,13 +314,23 @@ class HomeView: UIView {
|
|||
// 只有 tableView 到达顶部继续下拉时,才切换为 view 拖拽
|
||||
if isSubCanScroll {
|
||||
let tableViewOffset = self.groupMemberView.tableView.contentOffset.y
|
||||
if tableViewOffset > 0, newTop >= groupMemberUpLimit {
|
||||
// 还未滑到顶部,让 tableView 处理
|
||||
if tableViewOffset > 0 {
|
||||
// 内容正在滑动,不移动 GroupMemberView
|
||||
return
|
||||
}
|
||||
// 内容滑到顶部 → 切回 Pan 拖拽(用户下拉时 velocity > 0)
|
||||
if pan.velocity(in: self).y > 0 || groupMemberView.frame.minY > groupMemberUpLimit + 1 {
|
||||
isSubCanScroll = false
|
||||
panStartTop = groupMemberView.frame.minY
|
||||
}
|
||||
} else {
|
||||
// GroupMemberView 在顶部且继续上滑 → 激活内容滑动
|
||||
if groupMemberView.frame.minY <= groupMemberUpLimit && newTop <= groupMemberUpLimit {
|
||||
isSubCanScroll = true
|
||||
panStartTop = groupMemberView.frame.minY
|
||||
topConstraint.constant = groupMemberUpLimit - groupMemberDownLimit + 10
|
||||
return
|
||||
}
|
||||
// 到达顶部继续下拉 → 关闭子滚动,交由 view 的 pan 处理
|
||||
isSubCanScroll = false
|
||||
panStartTop = groupMemberView.frame.minY
|
||||
}
|
||||
|
||||
let clamped = max(groupMemberUpLimit, min(groupMemberDownLimit, newTop))
|
||||
|
|
|
|||
|
|
@ -187,6 +187,31 @@ class HomeViewController: BaseViewController {
|
|||
self?.requestGroupInfo()
|
||||
}.disposed(by: disposeBag)
|
||||
|
||||
// 查找位置(查看成员位置)
|
||||
NotificationCenter.default.rx.notification(.ShowMemberLocationNotification, object: nil)
|
||||
.subscribe { [weak self] notification in
|
||||
guard let self = self,
|
||||
let userInfo = notification.userInfo,
|
||||
let last_position = userInfo["last_position"] as? String,
|
||||
let group_key = userInfo["group_key"] as? String else { return }
|
||||
// 解析坐标 "lat:lng:address"
|
||||
let parts = last_position.components(separatedBy: ":")
|
||||
let coord: CLLocationCoordinate2D?
|
||||
if parts.count >= 2, let lat = Double(parts[0]), let lng = Double(parts[1]) {
|
||||
coord = CLLocationCoordinate2D(latitude: lat, longitude: lng)
|
||||
} else {
|
||||
coord = nil
|
||||
}
|
||||
// 切换到成员所在圈子,完成后定位
|
||||
GroupService.operate(opType: "setdefault", requestData: ["group_key": group_key]).subscribe { _ in
|
||||
self.requestGroupInfo()
|
||||
if let c = coord, CLLocationCoordinate2DIsValid(c) {
|
||||
self.rootView.mapView.setCenter(c, animated: true)
|
||||
self.rootView.mapView.setZoomLevel(16, animated: true)
|
||||
}
|
||||
}.disposed(by: self.disposeBag)
|
||||
}.disposed(by: disposeBag)
|
||||
|
||||
// 面板关闭回调
|
||||
rootView.onDismissPanel = { [weak self] in
|
||||
self?.isMemberPanelShown = false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
//
|
||||
// SearchLocationResultVC.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import Lottie
|
||||
|
||||
class SearchLocationResultVC: BaseViewController {
|
||||
|
||||
fileprivate var rootView: SearchLocationResultView!
|
||||
|
||||
override func loadView() {
|
||||
rootView = SearchLocationResultView(frame: UIScreen.main.bounds)
|
||||
view = rootView
|
||||
}
|
||||
|
||||
private let phone: String
|
||||
private let code: Int
|
||||
private let memberData: [String : Any]
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
rootView.unlockVipView.isHidden = AppContextManager.shared.vip > 1
|
||||
AppContextManager.shared.vip > 1 ? nil : rootView.unlockVipLottieView.play()
|
||||
|
||||
reactiveAction()
|
||||
requestPhoneArea()
|
||||
}
|
||||
|
||||
private func reactiveAction() {
|
||||
rootView.successBtn.rx.tap.subscribe(onNext: { _ in
|
||||
if self.code == 0 { // 去查看
|
||||
NotificationCenter.default.post(name: .ShowMemberLocationNotification,
|
||||
object: nil,
|
||||
userInfo: self.memberData)
|
||||
AppRouter.shared.popToRoot()
|
||||
}
|
||||
else { // 邀请Ta
|
||||
|
||||
}
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func handleCode() {
|
||||
guard AppContextManager.shared.vip > 1 else { return }
|
||||
|
||||
var fileName = ""
|
||||
var message = ""
|
||||
var tips = ""
|
||||
switch code {
|
||||
case 0: // 圈子成员
|
||||
fileName = "phone_search_success"
|
||||
message = "定位成功"
|
||||
tips = "点击去查看可获得具体位置"
|
||||
rootView.inviteView.isHidden = false
|
||||
rootView.successTipsLab.text = "已获得具体位置"
|
||||
rootView.successBtn.setTitle("去查看", for: .normal)
|
||||
rootView.phoneLab.text = phone
|
||||
rootView.successLottieView.play()
|
||||
case 20004: // 未注册
|
||||
fileName = "phone_search_fail"
|
||||
message = "此用户未注册"
|
||||
tips = "根据最新的相关法规,需用户双方均安装该APP才可实现定位,未安装APP的用户将不再提供具体定位功能。"
|
||||
rootView.inviteBtn.isHidden = false
|
||||
case 20005: // 不在一个圈子里
|
||||
fileName = "phone_search_success"
|
||||
message = "定位成功"
|
||||
tips = "根据最新的相关法规,您查询的用户与您不在同一个圈子时, 不再提供具体的位置定位,邀请ta加入你的圈子后可随时查看位置。"
|
||||
rootView.inviteView.isHidden = false
|
||||
rootView.successTipsLab.text = "邀请加入圈子后分享位置"
|
||||
rootView.successBtn.setTitle("邀请他", for: .normal)
|
||||
rootView.phoneLab.text = phone
|
||||
rootView.successLottieView.play()
|
||||
requestGroupInfo()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
|
||||
self.rootView.normalLottieView.animation = LottieAnimation.filepath(path)
|
||||
self.rootView.normalLottieView.play()
|
||||
}
|
||||
self.rootView.messageLab.text = message
|
||||
self.rootView.tipsLab.text = tips
|
||||
}
|
||||
|
||||
// MARK: - 手机号归属地
|
||||
private func requestPhoneArea() {
|
||||
SystemService.phoneArea(phone: phone).subscribe(onNext: { response in
|
||||
guard let data = response.data else { return }
|
||||
self.handleCode()
|
||||
let province = data["Province"].safeString
|
||||
let city = data["City"].safeString
|
||||
self.rootView.phoneAreaLab.text = "\(province)·\(city)"
|
||||
self.rootView.unlockVipPhoneAreaLab.text = "\(province)·\(city)"
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - 圈子列表
|
||||
private func requestGroupInfo() {
|
||||
GroupService.groupInfo().subscribe { response in
|
||||
guard let model = response.model else { return }
|
||||
|
||||
}.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
init(phone: String, code: Int, memberData: [String : Any] = [:]) {
|
||||
self.phone = phone
|
||||
self.code = code
|
||||
self.memberData = memberData
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
//
|
||||
// SearchLocationResultView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import Lottie
|
||||
|
||||
class SearchLocationResultView: UIView {
|
||||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
private func setupRx() {
|
||||
backBtn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(unlockVipView)
|
||||
addSubview(navBgView)
|
||||
addSubview(navBarView)
|
||||
navBarView.addSubview(navTitleLabel)
|
||||
navBarView.addSubview(backBtn)
|
||||
|
||||
addSubview(normalView)
|
||||
addSubview(successLottieView)
|
||||
|
||||
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)
|
||||
|
||||
unlockVipView.layoutChain.edges()
|
||||
|
||||
successLottieView.layoutChain.edges()
|
||||
|
||||
normalView.layoutChain
|
||||
.topToBottomOfView(navBarView)
|
||||
.edges(excludingEdge: .top)
|
||||
}
|
||||
|
||||
// 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
|
||||
}()
|
||||
|
||||
// MARK: - 成功烟花Lottie
|
||||
lazy var successLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView(name: "phone_search_fireworks")
|
||||
view.loopMode = .loop
|
||||
view.isUserInteractionEnabled = false
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - 已解锁vip
|
||||
lazy var normalView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
|
||||
view.addSubview(normalLottieView)
|
||||
normalLottieView.layoutChain
|
||||
.top(20)
|
||||
.width(270)
|
||||
.heightToWidth(1)
|
||||
.centerX()
|
||||
|
||||
view.addSubview(phoneAreaLab)
|
||||
phoneAreaLab.layoutChain
|
||||
.topToBottomOfView(normalLottieView)
|
||||
.centerX()
|
||||
|
||||
view.addSubview(messageLab)
|
||||
messageLab.layoutChain
|
||||
.topToBottomOfView(phoneAreaLab, offset: 10)
|
||||
.centerX()
|
||||
|
||||
view.addSubview(tipsLab)
|
||||
tipsLab.layoutChain
|
||||
.topToBottomOfView(messageLab, offset: 15)
|
||||
.edgesHorzontal(31)
|
||||
|
||||
view.addSubview(inviteBtn)
|
||||
inviteBtn.layoutChain
|
||||
.topToBottomOfView(tipsLab, offset: 30)
|
||||
.edgesHorzontal(68)
|
||||
.height(50)
|
||||
|
||||
view.addSubview(inviteView)
|
||||
inviteView.layoutChain
|
||||
.topToBottomOfView(tipsLab, offset: 25)
|
||||
.edgesHorzontal(15)
|
||||
.height(80)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var phoneAreaLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
label.textColor = UIColor(hexStr: "#16B3FF")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var messageLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
label.textColor = UIColor(hexStr: "#333333")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var tipsLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var normalLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView()
|
||||
view.loopMode = .loop
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var inviteBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("邀请Ta加入", for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
btn.cornerRadius = 25
|
||||
btn.isHidden = true
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var inviteView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
|
||||
view.cornerRadius = 16
|
||||
view.isHidden = true
|
||||
|
||||
view.addSubview(successBtn)
|
||||
successBtn.layoutChain
|
||||
.right(15)
|
||||
.centerY()
|
||||
.width(90)
|
||||
.height(36)
|
||||
|
||||
view.addSubview(successTipsLab)
|
||||
successTipsLab.layoutChain
|
||||
.topToCenterYOfView(view)
|
||||
.left(15)
|
||||
|
||||
view.addSubview(phoneLab)
|
||||
phoneLab.layoutChain
|
||||
.bottomToCenterYOfView(view)
|
||||
.left(15)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var phoneLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
label.textColor = UIColor(hexStr: "#333333")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var successTipsLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "邀请加入圈子后分享位置"
|
||||
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var successBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("邀请Ta", for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
btn.cornerRadius = 18
|
||||
return btn
|
||||
}()
|
||||
|
||||
// MARK: - 未解锁vip
|
||||
lazy var unlockVipView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.clipsToBounds = true
|
||||
view.isHidden = true
|
||||
|
||||
let bgImgView = UIImageView(image: UIImage(named: "SearchLocation/result_bg"))
|
||||
bgImgView.contentMode = .scaleAspectFill
|
||||
view.addSubview(bgImgView)
|
||||
bgImgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.heightToWidth(457/357)
|
||||
|
||||
view.addSubview(unlockVipLottieView)
|
||||
unlockVipLottieView.layoutChain
|
||||
.edgesHorzontal(60)
|
||||
.heightToWidth(1)
|
||||
.top(150)
|
||||
// .bottomToView(bgImgView, offset: -15)
|
||||
|
||||
view.addSubview(unlockVipPhoneAreaLab)
|
||||
unlockVipPhoneAreaLab.layoutChain
|
||||
.topToBottomOfView(unlockVipLottieView)
|
||||
.centerX()
|
||||
|
||||
let locationView = UIView()
|
||||
locationView.backgroundColor = .clear
|
||||
view.addSubview(locationView)
|
||||
locationView.layoutChain
|
||||
.topToBottomOfView(unlockVipPhoneAreaLab, offset: 10)
|
||||
.centerX()
|
||||
|
||||
let locationTitleLab = UILabel()
|
||||
locationTitleLab.text = "位置:"
|
||||
locationTitleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
locationTitleLab.textColor = UIColor(hexStr: "#16B3FF")
|
||||
locationView.addSubview(locationTitleLab)
|
||||
locationTitleLab.layoutChain
|
||||
.left()
|
||||
.centerY()
|
||||
|
||||
let maskView = UIImageView(image: UIImage(named: "SearchLocation/mask"))
|
||||
locationView.addSubview(maskView)
|
||||
maskView.layoutChain
|
||||
.leftToRightOfView(locationTitleLab)
|
||||
.right()
|
||||
.edgesVertical()
|
||||
.width(144)
|
||||
.height(30)
|
||||
|
||||
let unlockVipBtn = UIButton()
|
||||
unlockVipBtn.setTitle("开通会员", for: .normal)
|
||||
unlockVipBtn.setTitleColor(.white, for: .normal)
|
||||
unlockVipBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
unlockVipBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
unlockVipBtn.cornerRadius = 25
|
||||
unlockVipBtn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.push(Route.vipRecharge)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
view.addSubview(unlockVipBtn)
|
||||
unlockVipBtn.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
.height(50)
|
||||
|
||||
// 最后出现地点
|
||||
let tipsDot3 = UIView()
|
||||
tipsDot3.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
tipsDot3.cornerRadius = 2.5
|
||||
let tipsLab3 = UILabel()
|
||||
tipsLab3.text = "最后出现地点"
|
||||
tipsLab3.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
tipsLab3.textColor = UIColor(hexStr: "#333333")
|
||||
view.addSubview(tipsLab3)
|
||||
view.addSubview(tipsDot3)
|
||||
tipsLab3.layoutChain
|
||||
.bottomToTopOfView(unlockVipBtn, offset: -20)
|
||||
.rightToCenterXOfView(view, offset: 30)
|
||||
|
||||
tipsDot3.layoutChain
|
||||
.rightToLeftOfView(tipsLab3, offset: -8)
|
||||
.width(5)
|
||||
.heightToWidth(1)
|
||||
.centerY(tipsLab3)
|
||||
|
||||
// "近24小时轨迹"
|
||||
let tipsDot4 = UIView()
|
||||
tipsDot4.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
tipsDot4.cornerRadius = 2.5
|
||||
let tipsLab4 = UILabel()
|
||||
tipsLab4.text = "近24小时轨迹"
|
||||
tipsLab4.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
tipsLab4.textColor = UIColor(hexStr: "#333333")
|
||||
view.addSubview(tipsLab4)
|
||||
view.addSubview(tipsDot4)
|
||||
tipsDot4.layoutChain
|
||||
.leftToCenterXOfView(view, offset: 30)
|
||||
.width(5)
|
||||
.heightToWidth(1)
|
||||
.centerY(tipsLab4)
|
||||
tipsLab4.layoutChain
|
||||
.bottomToTopOfView(unlockVipBtn, offset: -20)
|
||||
.leftToRightOfView(tipsDot4, offset: 8)
|
||||
|
||||
// 是否在线
|
||||
let tipsDot1 = UIView()
|
||||
tipsDot1.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
tipsDot1.cornerRadius = 2.5
|
||||
let tipsLab1 = UILabel()
|
||||
tipsLab1.text = "是否在线"
|
||||
tipsLab1.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
tipsLab1.textColor = UIColor(hexStr: "#333333")
|
||||
view.addSubview(tipsLab1)
|
||||
view.addSubview(tipsDot1)
|
||||
tipsDot1.layoutChain
|
||||
.leftToView(tipsDot3)
|
||||
.width(5)
|
||||
.heightToWidth(1)
|
||||
.centerY(tipsLab1)
|
||||
tipsLab1.layoutChain
|
||||
.bottomToTopOfView(tipsLab3, offset: -10)
|
||||
.leftToRightOfView(tipsDot1, offset: 8)
|
||||
|
||||
// 实时位置变化
|
||||
let tipsDot2 = UIView()
|
||||
tipsDot2.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
tipsDot2.cornerRadius = 2.5
|
||||
let tipsLab2 = UILabel()
|
||||
tipsLab2.text = "实时位置变化"
|
||||
tipsLab2.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
tipsLab2.textColor = UIColor(hexStr: "#333333")
|
||||
view.addSubview(tipsLab2)
|
||||
view.addSubview(tipsDot2)
|
||||
tipsDot2.layoutChain
|
||||
.leftToView(tipsDot4)
|
||||
.width(5)
|
||||
.heightToWidth(1)
|
||||
.centerY(tipsLab2)
|
||||
tipsLab2.layoutChain
|
||||
.bottomToTopOfView(tipsLab4, offset: -10)
|
||||
.leftToRightOfView(tipsDot2, offset: 8)
|
||||
|
||||
// 再迈一步,你就能看到:
|
||||
let unlockVipTitleLab = UILabel()
|
||||
unlockVipTitleLab.text = "再迈一步,你就能看到:"
|
||||
unlockVipTitleLab.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
unlockVipTitleLab.textColor = UIColor(hexStr: "#16B3FF")
|
||||
view.addSubview(unlockVipTitleLab)
|
||||
unlockVipTitleLab.layoutChain
|
||||
.bottomToTopOfView(tipsLab1, offset: -8)
|
||||
.centerX()
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var unlockVipLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView(name: "phone_search_no_vip")
|
||||
view.loopMode = .loop
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var unlockVipPhoneAreaLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
label.textColor = UIColor(hexStr: "#16B3FF")
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .white
|
||||
setupUI()
|
||||
setupRx()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// SearchLocationVC.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import Lottie
|
||||
|
||||
class SearchLocationVC: BaseViewController {
|
||||
|
||||
fileprivate var rootView: SearchLocationView!
|
||||
|
||||
override func loadView() {
|
||||
rootView = SearchLocationView(frame: UIScreen.main.bounds)
|
||||
view = rootView
|
||||
}
|
||||
|
||||
private var searchBtn: UIButton {
|
||||
rootView.searchBtn
|
||||
}
|
||||
|
||||
private var code: Int = -999
|
||||
private var memberData: [String : Any] = [:]
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
searchBtn.rx.tap.subscribe(onNext: { _ in
|
||||
guard let phone = self.rootView.phoneInputTF.text, phone.isPhoneNumber else {
|
||||
DLToast.show(text: "请输入正确的手机号码")
|
||||
return
|
||||
}
|
||||
self.requestSearchPhone(phone: phone)
|
||||
self.rootView.searchProgressView.isHidden = false
|
||||
self.rootView.searchPhoneLottieView.play()
|
||||
self.rootView.playVideo {
|
||||
self.rootView.videoView.isHidden = true
|
||||
self.rootView.searchProgressView.isHidden = true
|
||||
self.rootView.searchPhoneLottieView.stop()
|
||||
|
||||
guard self.code != -999 else {
|
||||
DLToast.show(text: "发生未知错误,请重试")
|
||||
return
|
||||
}
|
||||
|
||||
if self.code == 0 {
|
||||
AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone,
|
||||
"code": self.code,
|
||||
"memberData": self.memberData])
|
||||
}
|
||||
else {
|
||||
AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, "code": self.code])
|
||||
}
|
||||
}
|
||||
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
rootView.startMarqueeAnimation()
|
||||
}
|
||||
|
||||
// MARK: - 查找号码
|
||||
private func requestSearchPhone(phone: String) {
|
||||
SystemService.search(op_type: "phone",
|
||||
number: phone).subscribe(onNext: { response in
|
||||
self.code = 0
|
||||
/**
|
||||
"group_name" : "",
|
||||
"user_id" : "X16097989",
|
||||
|
||||
"is_online" : true,
|
||||
"group_key" : "smartdrive\/X2804080\/1002",
|
||||
"last_position" : "29.613138:106.509789:星光一路"
|
||||
*/
|
||||
if let data = response.data {
|
||||
self.memberData = data
|
||||
}
|
||||
}, onError: { (error) in
|
||||
guard let code = error.underlyingError?.code else { return }
|
||||
self.code = code
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,543 @@
|
|||
//
|
||||
// SearchLocationView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import AVFoundation
|
||||
import Lottie
|
||||
|
||||
class SearchLocationView: UIView {
|
||||
|
||||
var disposeBag = DisposeBag()
|
||||
let onVideoComplete = PublishSubject<Void>()
|
||||
private var player: AVPlayer?
|
||||
private var timeObserver: Any?
|
||||
|
||||
private func setupRx() {
|
||||
backBtn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
phoneInputTF.rx.text
|
||||
.map { text -> String? in
|
||||
guard let text = text else { return nil }
|
||||
return String(text.prefix(11))
|
||||
}.bind(to: phoneInputTF.rx.text)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
phoneInputTF.rx.text.orEmpty.map { phone -> Bool in
|
||||
if phone.count == 11 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
.bind(to: searchBtn.rx.isEnabled)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
phoneInputTF.rx.controlEvent(.editingDidEndOnExit)
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.phoneInputTF.resignFirstResponder()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(navBgView)
|
||||
addSubview(navBarView)
|
||||
navBarView.addSubview(navTitleLabel)
|
||||
navBarView.addSubview(backBtn)
|
||||
addSubview(scrollView)
|
||||
addSubview(videoView)
|
||||
addSubview(searchProgressView)
|
||||
|
||||
videoView.layoutChain.edges()
|
||||
|
||||
searchProgressView.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
.height(221)
|
||||
|
||||
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)
|
||||
|
||||
scrollView.layoutChain
|
||||
.topToBottomOfView(navBarView)
|
||||
.edges(excludingEdge: .top)
|
||||
}
|
||||
|
||||
// MARK: - Marquee
|
||||
private func randomPhoneNumber() -> String {
|
||||
// 所有合法前3位号段
|
||||
let prefixes = [
|
||||
"134","135","136","137","138","139","147","150","151","152","157","158","159","178","182","183","184","187","188","198",
|
||||
"130","131","132","145","155","156","166","175","176","185","186","196",
|
||||
"133","149","153","173","177","180","181","189","191","199"
|
||||
]
|
||||
// 随机选一个号段
|
||||
let prefix = prefixes.randomElement()!
|
||||
|
||||
// 随机生成后面8位数字
|
||||
var suffix = ""
|
||||
for _ in 0..<8 {
|
||||
suffix.append("\(Int.random(in: 0...9))")
|
||||
}
|
||||
|
||||
return prefix + suffix
|
||||
}
|
||||
|
||||
private func setupMarquee() {
|
||||
let surnames = ["张", "李", "王", "陈", "刘", "杨", "赵", "黄", "周", "吴",
|
||||
"林", "何", "马", "胡", "郑", "梁", "谢", "宋", "唐", "韩"]
|
||||
|
||||
let container = UIView()
|
||||
container.backgroundColor = .clear
|
||||
marqueeScrollView.addSubview(container)
|
||||
|
||||
var prevView: UIView?
|
||||
for _ in 0..<10 {
|
||||
let iconIndex = Int.random(in: 1...15)
|
||||
let surname = surnames.randomElement() ?? "张"
|
||||
let maskedName = "\(surname)**"
|
||||
let phone = randomPhoneNumber()
|
||||
let maskedPhone = phone.prefix(3) + "******" + phone.suffix(2)
|
||||
|
||||
let itemView = UIView()
|
||||
itemView.backgroundColor = UIColor(hexStr: "#EFF9FF")
|
||||
itemView.cornerRadius = 6
|
||||
container.addSubview(itemView)
|
||||
|
||||
let avatar = UIImageView(image: UIImage(named: "UserIcon/\(iconIndex)"))
|
||||
avatar.contentMode = .scaleAspectFill
|
||||
avatar.cornerRadius = 12
|
||||
avatar.clipsToBounds = true
|
||||
itemView.addSubview(avatar)
|
||||
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
let fullText = "\(maskedName) 定位到了 \(maskedPhone)"
|
||||
let attr = NSMutableAttributedString(string: fullText)
|
||||
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
|
||||
range: NSRange(fullText.range(of: maskedName)!, in: fullText))
|
||||
if let phoneRange = fullText.range(of: maskedPhone) {
|
||||
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
|
||||
range: NSRange(phoneRange, in: fullText))
|
||||
}
|
||||
label.attributedText = attr
|
||||
itemView.addSubview(label)
|
||||
|
||||
avatar.layoutChain
|
||||
.left(10).centerY()
|
||||
.width(24).height(24)
|
||||
|
||||
label.layoutChain
|
||||
.leftToRightOfView(avatar, offset: 6)
|
||||
.centerY().right(10)
|
||||
|
||||
itemView.layoutChain
|
||||
.centerY()
|
||||
.height(30)
|
||||
|
||||
if let prev = prevView {
|
||||
itemView.layoutChain.leftToRightOfView(prev, offset: 20)
|
||||
} else {
|
||||
itemView.layoutChain.left()
|
||||
}
|
||||
prevView = itemView
|
||||
}
|
||||
|
||||
if let last = prevView {
|
||||
container.layoutChain
|
||||
.edges().heightToView(marqueeScrollView)
|
||||
.rightToView(last)
|
||||
}
|
||||
}
|
||||
|
||||
func startMarqueeAnimation() {
|
||||
marqueeScrollView.layer.removeAllAnimations()
|
||||
marqueeScrollView.contentOffset.x = 0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let contentW = self.marqueeScrollView.contentSize.width
|
||||
guard contentW > self.marqueeScrollView.bounds.width else { return }
|
||||
let duration = contentW / 100
|
||||
|
||||
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear, .repeat]) {
|
||||
self.marqueeScrollView.contentOffset.x = contentW - self.marqueeScrollView.bounds.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func willMove(toWindow newWindow: UIWindow?) {
|
||||
super.willMove(toWindow: newWindow)
|
||||
if newWindow != nil {
|
||||
startMarqueeAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 scrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
view.backgroundColor = .clear
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.bounces = false
|
||||
|
||||
let contentView = UIView()
|
||||
contentView.backgroundColor = .clear
|
||||
view.addSubview(contentView)
|
||||
contentView.layoutChain.edges().widthToView(view)
|
||||
|
||||
let bgImgView = UIImageView()
|
||||
bgImgView.image = UIImage(named: "SearchLocation/bg_1")
|
||||
contentView.addSubview(bgImgView)
|
||||
bgImgView.layoutChain
|
||||
.top(15)
|
||||
.edgesHorzontal(56.5)
|
||||
.heightToWidth(626/524)
|
||||
|
||||
contentView.addSubview(titleLab)
|
||||
titleLab.layoutChain
|
||||
.topToBottomOfView(bgImgView, offset: 0)
|
||||
.centerX()
|
||||
|
||||
contentView.addSubview(marqueeScrollView)
|
||||
marqueeScrollView.layoutChain
|
||||
.topToBottomOfView(titleLab, offset: 10)
|
||||
.edgesHorzontal()
|
||||
.height(30)
|
||||
|
||||
contentView.addSubview(searchInputView)
|
||||
searchInputView.layoutChain
|
||||
.topToBottomOfView(marqueeScrollView, offset: 20)
|
||||
.edgesHorzontal(15)
|
||||
.bottom(kSafeBottomMargin + 30)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var videoView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .black
|
||||
v.isHidden = true
|
||||
|
||||
return v
|
||||
}()
|
||||
|
||||
func playVideo(completion: (() -> Void)? = nil) {
|
||||
guard let path = Bundle.main.path(forResource: "search_interlude", ofType: "mp4") ?? Bundle.main.path(forResource: "search_interlude", ofType: "mp4", inDirectory: "video") else {
|
||||
print("[video] file not found in bundle: \(Bundle.main.resourcePath ?? "")")
|
||||
return
|
||||
}
|
||||
videoView.isHidden = false
|
||||
videoView.layoutIfNeeded()
|
||||
|
||||
let player = AVPlayer(url: URL(fileURLWithPath: path))
|
||||
self.player = player
|
||||
let playerLayer = AVPlayerLayer(player: player)
|
||||
playerLayer.frame = videoView.bounds.isEmpty ? UIScreen.main.bounds : videoView.bounds
|
||||
playerLayer.videoGravity = .resizeAspectFill
|
||||
videoView.layer.addSublayer(playerLayer)
|
||||
|
||||
// 进度跟踪
|
||||
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: 600), queue: .main) { [weak self] time in
|
||||
guard let self = self, let duration = self.player?.currentItem?.duration.seconds, duration > 0 else { return }
|
||||
self.updateVideoProgress(Float(time.seconds / duration))
|
||||
}
|
||||
|
||||
NotificationCenter.default.rx.notification(.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
|
||||
.take(1)
|
||||
.subscribe(onNext: { _ in
|
||||
self.updateVideoProgress(1)
|
||||
completion?()
|
||||
self.onVideoComplete.onNext(())
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
player.play()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
playerLayer.frame = self.videoView.bounds
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if let playerLayer = videoView.layer.sublayers?.compactMap({ $0 as? AVPlayerLayer }).first {
|
||||
playerLayer.frame = videoView.bounds
|
||||
}
|
||||
}
|
||||
|
||||
lazy var titleLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "有 \(Int.random(in: 1000...10000)) 人正在使用此功能"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var marqueeScrollView: UIScrollView = {
|
||||
let sv = UIScrollView()
|
||||
sv.backgroundColor = .clear
|
||||
sv.showsHorizontalScrollIndicator = false
|
||||
sv.isScrollEnabled = false
|
||||
return sv
|
||||
}()
|
||||
|
||||
lazy var searchInputView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
|
||||
view.cornerRadius = 10
|
||||
|
||||
let titleLab = UILabel()
|
||||
titleLab.text = "TA在哪?输入号码就知道"
|
||||
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
view.addSubview(titleLab)
|
||||
titleLab.layoutChain
|
||||
.top(18)
|
||||
.left(15)
|
||||
|
||||
let inputView = UIView()
|
||||
inputView.backgroundColor = .white
|
||||
inputView.cornerRadius = 4
|
||||
view.addSubview(inputView)
|
||||
inputView.layoutChain
|
||||
.topToBottomOfView(titleLab, offset: 20)
|
||||
.edgesHorzontal(15)
|
||||
.height(40)
|
||||
|
||||
let contactsBtn = UIButton()
|
||||
contactsBtn.setTitle(" 通讯录导入", for: .normal)
|
||||
contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
|
||||
contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
contactsBtn.setImage(UIImage(named: "SearchLocation/contacts"), for: .normal)
|
||||
contactsBtn.extendEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15)
|
||||
contactsBtn.rx.tap.subscribe(onNext: { _ in
|
||||
//TODO: - 通讯录
|
||||
}).disposed(by: disposeBag)
|
||||
inputView.addSubview(contactsBtn)
|
||||
contactsBtn.layoutChain
|
||||
.right(15)
|
||||
.width(90)
|
||||
.edgesVertical()
|
||||
contactsBtn.sizeToFit()
|
||||
|
||||
inputView.addSubview(phoneInputTF)
|
||||
phoneInputTF.layoutChain
|
||||
.edgesVertical(10)
|
||||
.left(15)
|
||||
.rightToLeftOfView(contactsBtn, offset: -8)
|
||||
|
||||
let tipsLab = UILabel()
|
||||
tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私"
|
||||
tipsLab.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
tipsLab.textColor = ThemeManager.shared.color.contentColor
|
||||
view.addSubview(tipsLab)
|
||||
tipsLab.layoutChain
|
||||
.topToBottomOfView(inputView, offset: 6)
|
||||
.right(15)
|
||||
|
||||
view.addSubview(searchBtn)
|
||||
searchBtn.layoutChain
|
||||
.topToBottomOfView(tipsLab, offset: 35)
|
||||
.edgesHorzontal(52)
|
||||
.height(50)
|
||||
.bottom(30)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var phoneInputTF: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
textField.placeholder = "请输入要查找的号码"
|
||||
textField.keyboardType = .numberPad
|
||||
textField.returnKeyType = .done
|
||||
return textField
|
||||
}()
|
||||
|
||||
lazy var searchBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("开始查找", for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
btn.cornerRadius = 25
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var searchProgressView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .black.withAlphaComponent(0.8)
|
||||
view.cornerRadius = 16
|
||||
view.borderWidth = 0.5
|
||||
view.borderColor = .white
|
||||
view.isHidden = true
|
||||
|
||||
view.addSubview(searchPhoneLottieView)
|
||||
searchPhoneLottieView.layoutChain
|
||||
.top(30)
|
||||
.left(15)
|
||||
.width(120)
|
||||
.heightToWidth(1)
|
||||
|
||||
let steps = ["分析用户号码", "正在找 CGI 蜂窝参数", "SS7 信息交流", "号码已获授权", "处理完成"]
|
||||
var prevStepView: UIView?
|
||||
for (i, text) in steps.enumerated() {
|
||||
let stepView = UIView()
|
||||
view.addSubview(stepView)
|
||||
|
||||
let icon = UIImageView(image: UIImage(named: "SearchLocation/done_off"))
|
||||
icon.contentMode = .scaleAspectFit
|
||||
stepView.addSubview(icon)
|
||||
progressIcons.append(icon)
|
||||
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = .white
|
||||
stepView.addSubview(label)
|
||||
|
||||
icon.layoutChain
|
||||
.left().centerY()
|
||||
.width(18).height(18)
|
||||
|
||||
label.layoutChain
|
||||
.leftToRightOfView(icon, offset: 8)
|
||||
.centerY().right()
|
||||
|
||||
stepView.layoutChain
|
||||
.leftToRightOfView(searchPhoneLottieView, offset: 10)
|
||||
.right(10)
|
||||
.height(26)
|
||||
|
||||
if let prev = prevStepView {
|
||||
stepView.layoutChain.topToBottomOfView(prev, offset: 4)
|
||||
} else {
|
||||
stepView.layoutChain.top(20)
|
||||
}
|
||||
prevStepView = stepView
|
||||
}
|
||||
|
||||
// 进度条 + 百分比
|
||||
progressBar.layer.cornerRadius = 4
|
||||
progressBar.clipsToBounds = true
|
||||
progressBar.trackTintColor = .white.withAlphaComponent(0.2)
|
||||
progressBar.progressTintColor = UIColor(hexStr: "#16B3FF")
|
||||
view.addSubview(progressBar)
|
||||
|
||||
progressLab.textColor = .white
|
||||
progressLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
progressLab.textAlignment = .right
|
||||
view.addSubview(progressLab)
|
||||
|
||||
progressBar.layoutChain
|
||||
.topToBottomOfView(prevStepView!, offset: 15)
|
||||
.left(20)
|
||||
.height(8)
|
||||
|
||||
progressLab.layoutChain
|
||||
.leftToRightOfView(progressBar, offset: 8)
|
||||
.centerY(progressBar)
|
||||
.right(20)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
private var progressIcons: [UIImageView] = []
|
||||
|
||||
private lazy var progressBar: UIProgressView = {
|
||||
let p = UIProgressView()
|
||||
return p
|
||||
}()
|
||||
|
||||
private lazy var progressLab: UILabel = {
|
||||
let l = UILabel()
|
||||
return l
|
||||
}()
|
||||
|
||||
/// 根据视频进度更新步骤图标和进度条 (0~1)
|
||||
func updateVideoProgress(_ progress: Float) {
|
||||
let clamped = max(0, min(1, progress))
|
||||
progressBar.progress = clamped
|
||||
progressLab.text = "\(Int(clamped * 100))%"
|
||||
|
||||
let stepCount = progressIcons.count
|
||||
let stepProgress = 1.0 / Float(stepCount)
|
||||
for (i, icon) in progressIcons.enumerated() {
|
||||
let stepStart = stepProgress * Float(i)
|
||||
icon.image = UIImage(named: clamped >= stepStart + stepProgress * 0.5 ? "SearchLocation/done" : "SearchLocation/done_off")
|
||||
}
|
||||
}
|
||||
|
||||
lazy var searchPhoneLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView(name: "phone_search_interlude")
|
||||
view.loopMode = .loop
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .white
|
||||
setupUI()
|
||||
setupRx()
|
||||
setupMarquee()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -322,7 +322,7 @@ class LoginView: UIView {
|
|||
textField.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
textField.placeholderColor(placeholder: "请输入验证码",
|
||||
color: .white)
|
||||
textField.keyboardType = .numbersAndPunctuation
|
||||
textField.keyboardType = .numberPad
|
||||
return textField
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -41,4 +41,25 @@ struct SystemService {
|
|||
.map(ResponseModel.self)
|
||||
.asObservable()
|
||||
}
|
||||
|
||||
/// 查找
|
||||
/// - Parameters:
|
||||
/// - op_type: plate_num 车牌 phone 手机号
|
||||
/// - number: 对应号码
|
||||
static func search(op_type: String, number: String) -> Observable<ResponseModel> {
|
||||
let api = SystemAPI.search(op_type: op_type, number: number).multiTarget
|
||||
return APIProvider.request(token: api, handle: false)
|
||||
.map(ResponseModel.self)
|
||||
.asObservable()
|
||||
}
|
||||
|
||||
/// 手机归属地
|
||||
/// - Parameters:
|
||||
/// - phone 手机号
|
||||
static func phoneArea(phone: String) -> Observable<ResponseModel> {
|
||||
let api = SystemAPI.phoneArea(phone: phone).multiTarget
|
||||
return APIProvider.request(token: api)
|
||||
.map(ResponseModel.self)
|
||||
.asObservable()
|
||||
}
|
||||
}
|
||||
|
|
|
|||