- 签到页面

- 支付宝sdk接入
- MQTT 显示收到的 快捷消息
This commit is contained in:
linshujie 2026-06-17 18:37:30 +08:00
parent 6479aef2a6
commit 937c1fe4fe
66 changed files with 1321 additions and 193 deletions

View File

@ -188,11 +188,17 @@
30C4C01D2FDBF557009215C1 /* RemoveMemberViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01C2FDBF557009215C1 /* RemoveMemberViewModel.swift */; };
30C4C0202FDC0EC5009215C1 /* GroupInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01F2FDC0EC5009215C1 /* GroupInfoView.swift */; };
30C4C0222FDC0ED3009215C1 /* GroupInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C0212FDC0ED3009215C1 /* GroupInfoVC.swift */; };
30CCDE4E2FE26CEA00F5214A /* PayResultPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCDE4D2FE26CEA00F5214A /* PayResultPopVC.swift */; };
30CCDE512FE2785D00F5214A /* SignInVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCDE502FE2785D00F5214A /* SignInVC.swift */; };
30CCDE532FE2786600F5214A /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCDE522FE2786600F5214A /* SignInView.swift */; };
30CCDE552FE2903100F5214A /* SignInModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCDE542FE2903100F5214A /* SignInModel.swift */; };
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; };
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; };
30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */; };
30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D012FE1336300E958FD /* NavigationVC.swift */; };
30D87D052FE1336300E958FD /* NavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D022FE1336300E958FD /* NavigationView.swift */; };
30D891F52FE22E0600E958FD /* OrderAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D891F42FE22E0600E958FD /* OrderAPI.swift */; };
30D891F72FE22E6E00E958FD /* OrderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D891F62FE22E6E00E958FD /* OrderService.swift */; };
30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */; };
30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */; };
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18562FD11E7A0041DCD1 /* WebOperations.swift */; };
@ -425,11 +431,17 @@
30C4C01C2FDBF557009215C1 /* RemoveMemberViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMemberViewModel.swift; sourceTree = "<group>"; };
30C4C01F2FDC0EC5009215C1 /* GroupInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoView.swift; sourceTree = "<group>"; };
30C4C0212FDC0ED3009215C1 /* GroupInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoVC.swift; sourceTree = "<group>"; };
30CCDE4D2FE26CEA00F5214A /* PayResultPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayResultPopVC.swift; sourceTree = "<group>"; };
30CCDE502FE2785D00F5214A /* SignInVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInVC.swift; sourceTree = "<group>"; };
30CCDE522FE2786600F5214A /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
30CCDE542FE2903100F5214A /* SignInModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInModel.swift; sourceTree = "<group>"; };
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; };
30D87CDC2FDFF07500E958FD /* InteractionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionView.swift; sourceTree = "<group>"; };
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickMessageView.swift; sourceTree = "<group>"; };
30D87D012FE1336300E958FD /* NavigationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationVC.swift; sourceTree = "<group>"; };
30D87D022FE1336300E958FD /* NavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationView.swift; sourceTree = "<group>"; };
30D891F42FE22E0600E958FD /* OrderAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderAPI.swift; sourceTree = "<group>"; };
30D891F62FE22E6E00E958FD /* OrderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderService.swift; sourceTree = "<group>"; };
30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipExpenseModel.swift; sourceTree = "<group>"; };
30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRechargeVM.swift; sourceTree = "<group>"; };
30DC18552FD11E7A0041DCD1 /* NavigationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTitleView.swift; sourceTree = "<group>"; };
@ -479,15 +491,11 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
3070777D2FD2A214004C37CC /* lotties */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = lotties;
sourceTree = "<group>";
};
30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = TTGTagCollectionView;
sourceTree = "<group>";
};
@ -526,6 +534,7 @@
305A74CE2FCA8C7000227D26 /* SystemAPI.swift */,
305A74CF2FCA8C7000227D26 /* UserAPI.swift */,
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */,
30D891F42FE22E0600E958FD /* OrderAPI.swift */,
);
path = API;
sourceTree = "<group>";
@ -866,6 +875,7 @@
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */,
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */,
30D87CDC2FDFF07500E958FD /* InteractionView.swift */,
30CCDE4F2FE2782700F5214A /* SignIn */,
);
path = Home;
sourceTree = "<group>";
@ -892,10 +902,10 @@
305A76352FCA8C7000227D26 /* Map */ = {
isa = PBXGroup;
children = (
30D87D032FE1336300E958FD /* Navigation */,
305A76322FCA8C7000227D26 /* CircleMember.swift */,
305A76332FCA8C7000227D26 /* MemberAnnotation.swift */,
305A76342FCA8C7000227D26 /* MemberAnnotationView.swift */,
30D87D032FE1336300E958FD /* Navigation */,
);
path = Map;
sourceTree = "<group>";
@ -939,6 +949,7 @@
305A763B2FCA8C7000227D26 /* SystemService.swift */,
305A763C2FCA8C7000227D26 /* UserService.swift */,
30BAB8522FCD337C00C33B5C /* GroupService.swift */,
30D891F62FE22E6E00E958FD /* OrderService.swift */,
);
path = Service;
sourceTree = "<group>";
@ -1034,6 +1045,7 @@
305A76692FCA8C7000227D26 /* Pop */ = {
isa = PBXGroup;
children = (
30CCDE4D2FE26CEA00F5214A /* PayResultPopVC.swift */,
30EFF3BA2FD90D7600EB35D4 /* ConfirmPopVC.swift */,
305A76642FCA8C7000227D26 /* DLAlertPopVC.swift */,
305A76652FCA8C7000227D26 /* DLCustomPopVC.swift */,
@ -1190,6 +1202,16 @@
path = GroupInfo;
sourceTree = "<group>";
};
30CCDE4F2FE2782700F5214A /* SignIn */ = {
isa = PBXGroup;
children = (
30CCDE502FE2785D00F5214A /* SignInVC.swift */,
30CCDE522FE2786600F5214A /* SignInView.swift */,
30CCDE542FE2903100F5214A /* SignInModel.swift */,
);
path = SignIn;
sourceTree = "<group>";
};
30D87CD52FDF9F1900E958FD /* MQTT */ = {
isa = PBXGroup;
children = (
@ -1445,10 +1467,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks.sh\"\n";
@ -1462,10 +1488,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources.sh\"\n";
@ -1579,6 +1609,7 @@
305A76C92FCA8C7000227D26 /* ApiManager.swift in Sources */,
305A76CA2FCA8C7000227D26 /* AppSettings.swift in Sources */,
305A76CB2FCA8C7000227D26 /* Authorize.swift in Sources */,
30CCDE552FE2903100F5214A /* SignInModel.swift in Sources */,
305A76CC2FCA8C7000227D26 /* FileTools.swift in Sources */,
305A76CD2FCA8C7000227D26 /* Permission.swift in Sources */,
305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */,
@ -1614,6 +1645,7 @@
30EFF3CF2FDA669800EB35D4 /* MyProfileVC.swift in Sources */,
30EFF3E52FDAA93400EB35D4 /* PrivacyPolicyVC.swift in Sources */,
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */,
30CCDE4E2FE26CEA00F5214A /* PayResultPopVC.swift in Sources */,
305A76E32FCA8C7000227D26 /* GroupMemberView.swift in Sources */,
30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */,
305A76E42FCA8C7000227D26 /* HomeView.swift in Sources */,
@ -1674,20 +1706,24 @@
305A77072FCA8C7000227D26 /* Helper.swift in Sources */,
305A77082FCA8C7000227D26 /* PageCollectionViewFlowLayout.swift in Sources */,
3062E8C42FCFC90F00CEF511 /* CreateGroupVipPopView.swift in Sources */,
30CCDE512FE2785D00F5214A /* SignInVC.swift in Sources */,
305A77092FCA8C7000227D26 /* PageContentView.swift in Sources */,
305A770A2FCA8C7000227D26 /* PageStyle.swift in Sources */,
305A770B2FCA8C7000227D26 /* PageTitleView.swift in Sources */,
305A770C2FCA8C7000227D26 /* PageView.swift in Sources */,
305A770D2FCA8C7000227D26 /* PageViewManager.swift in Sources */,
30D891F52FE22E0600E958FD /* OrderAPI.swift in Sources */,
307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */,
305A770E2FCA8C7000227D26 /* DLAlertPopVC.swift in Sources */,
305A770F2FCA8C7000227D26 /* DLCustomPopVC.swift in Sources */,
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */,
30CCDE532FE2786600F5214A /* SignInView.swift in Sources */,
305A77102FCA8C7000227D26 /* DLSheetPopVC.swift in Sources */,
30EFF3D32FDA69F400EB35D4 /* AvatarIconListView.swift in Sources */,
30EFF3B72FD8F86200EB35D4 /* ReviewMemberListVM.swift in Sources */,
305A77112FCA8C7000227D26 /* DLViewTransition.m in Sources */,
3062E8BA2FCEAC6500CEF511 /* CreateGroupView.swift in Sources */,
30D891F72FE22E6E00E958FD /* OrderService.swift in Sources */,
305A77192FCA8C7000227D26 /* CollectionHFlowLayout.swift in Sources */,
305A771A2FCA8C7000227D26 /* JJPageControl.swift in Sources */,
305A771B2FCA8C7000227D26 /* ReusableView.swift in Sources */,
@ -1730,7 +1766,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7LP48T8ZJE;
DEVELOPMENT_TEAM = XSJJKSY9FB;
ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickLocation/Info.plist;
@ -1779,7 +1815,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7LP48T8ZJE;
DEVELOPMENT_TEAM = XSJJKSY9FB;
ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickLocation/Info.plist;

View File

@ -3,4 +3,22 @@
uuid = "AE547311-A826-480F-B1ED-BD6B815729FA"
type = "0"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "AA447600-D860-4876-A728-F18CC2D566FC"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "QuickLocation/AppDelegate.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "70"
endingLineNumber = "70"
landmarkName = "application(_:open:options:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -0,0 +1,65 @@
//
// OrderAPI.swift
// QuickLocation
//
// Created by on 2026/6/17.
//
import Moya
import SwiftyUserDefaults
internal import Alamofire
/// API
enum OrderAPI {
///
/// - Parameters:
/// - type: member
case rechargeInfo(type: String)
///
/// - Parameters:
/// - goodsId: ID
/// - payType: alipayweixin
/// - source: center
/// - extra:
case orderPayParams(goodsId: String, payType: String, source: String, extra: [String:Any]=[:])
}
extension OrderAPI: MultiTargetProtocol {
var path: String {
switch self {
case .rechargeInfo:
return "api/order/goods"
case .orderPayParams:
return "api/order"
}
}
var method: Moya.Method {
switch self {
case .rechargeInfo:
return .get
default:
return .post
}
}
var task: Moya.Task {
switch self {
case let .rechargeInfo(type):
var params = Parameters()
params["type"] = type
return .requestParameters(parameters: params, encoding: URLEncoding())
case let .orderPayParams(goodsId, payType, source, extra):
var params = Parameters()
params["goods_id"] = goodsId
params["pay_type"] = payType
params["source"] = source
params["pay_source"] = "app"
params["extra"] = extra
return .requestParameters(parameters: params, encoding: JSONEncoding())
}
}
}

View File

@ -18,11 +18,6 @@ enum SystemAPI {
/// - phone:
case sendCode(phone: String)
///
/// - Parameters:
/// - type: member
case rechargeInfo(type: String)
///
case wechatService
}
@ -35,8 +30,6 @@ extension SystemAPI: MultiTargetProtocol {
return "api/user/config"
case .sendCode:
return "api/user/sms/code"
case .rechargeInfo:
return "api/order/goods"
case .wechatService:
return "api/weixin/service"
}
@ -44,7 +37,7 @@ extension SystemAPI: MultiTargetProtocol {
var method: Moya.Method {
switch self {
case .userConfig, .rechargeInfo, .wechatService:
case .userConfig, .wechatService:
return .get
case .sendCode:
return .post
@ -61,11 +54,6 @@ extension SystemAPI: MultiTargetProtocol {
params["phone"] = phone
return .requestParameters(parameters: params, encoding: JSONEncoding())
case let .rechargeInfo(type):
var params = Parameters()
params["type"] = type
return .requestParameters(parameters: params, encoding: URLEncoding())
case .wechatService:
return .requestParameters(parameters: parameters, encoding: URLEncoding())
}

View File

@ -25,6 +25,9 @@ enum UserAPI {
/// IM Token
case imToken
///
case signInInfo
///
case changePhone(timestamp: String, phone: String, code: String)
@ -62,6 +65,8 @@ extension UserAPI: MultiTargetProtocol {
return "api/user"
case .imToken:
return "mapi/openim/user/token/get"
case .signInInfo:
return "mapi/user/signin"
case .changePhone:
return "api/user"
case .setHeadPic:
@ -81,7 +86,7 @@ extension UserAPI: MultiTargetProtocol {
var method: Moya.Method {
switch self {
case .userInfo:
case .userInfo, .signInInfo:
return .get
case .changePhone, .setGender:
return .put
@ -110,6 +115,9 @@ extension UserAPI: MultiTargetProtocol {
params["force"] = true
return .requestParameters(parameters: params, encoding: JSONEncoding())
case .signInInfo:
return .requestParameters(parameters: Parameters(), encoding: URLEncoding())
case let .changePhone(timestamp, phone, code):
var params = Parameters()
params["phone_timestamp"] = timestamp

View File

@ -67,7 +67,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return WXApi.handleOpen(url, delegate: self)
print("📱 application open url: \(url.absoluteString) host: \(url.host ?? "")")
// host SDK
if url.scheme?.hasPrefix("Alipay") == true || url.host == "safepay" {
AlipaySDK.defaultService().processOrder(withPaymentResult: url) { resultDic in
print("📱 Alipay callback: \(resultDic ?? [:])")
guard let result = resultDic,
let resultStatus = result["resultStatus"] as? String,
resultStatus != "6001" else { return }
}
return true
}
else if url.host == "pay" {
return WXApi.handleOpen(url, delegate: self)
}
return true
}
func setupLocation() {

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group 1545@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group 1545@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_2075@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_2075@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "failure@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "failure@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "success@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "success@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group 2404@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group 2404@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_2400@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_2400@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "today_signin@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "today_signin@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,22 +0,0 @@
{
"images": [
{
"idiom": "universal",
"scale": "1x"
},
{
"filename": "map_avatar_1@2x.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "map_avatar_1@3x.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,22 +0,0 @@
{
"images": [
{
"idiom": "universal",
"scale": "1x"
},
{
"filename": "map_avatar_2@2x.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "map_avatar_2@3x.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,22 +0,0 @@
{
"images": [
{
"idiom": "universal",
"scale": "1x"
},
{
"filename": "map_avatar_3@2x.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "map_avatar_3@3x.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -1,22 +0,0 @@
{
"images": [
{
"idiom": "universal",
"scale": "1x"
},
{
"filename": "map_avatar_4@2x.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "map_avatar_4@3x.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,22 +0,0 @@
{
"images": [
{
"idiom": "universal",
"scale": "1x"
},
{
"filename": "map_avatar_5@2x.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "map_avatar_5@3x.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -28,6 +28,8 @@ extension Notification.Name {
static let RefreshUserConfigNotification = Notification.Name("RefreshUserConfigNotification")
///
static let RefreshGroupInfoNotification = Notification.Name("RefreshGroupInfoNotification")
/// /
static let RequestOrderPayStatusNotification = Notification.Name("RequestOrderPayStatusNotification")
}

View File

@ -4,6 +4,16 @@
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>alipay</string>
<key>CFBundleURLSchemes</key>
<array>
<string>Alipay2021005185689681</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
@ -17,13 +27,13 @@
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>alipays</string>
<string>baidumap</string>
<string>qqmap</string>
<string>iosamap</string>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
<string>alipays</string>
<string>baidumap</string>
<string>qqmap</string>
<string>iosamap</string>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>

View File

@ -45,6 +45,14 @@ struct MqttLocation: Codable {
let points: [Points]
}
///
struct MqttSignIn: Codable {
let topic: String?
let latitude: Double
let longitude: Double
let address: String
}
/// MQTT
struct MqttIncomingMessage: Decodable {
let type: String?
@ -55,6 +63,8 @@ struct MqttIncomingMessage: Decodable {
struct MqttIncomingData: Decodable {
let points: [Points]?
let battery: String?
let index: Int?
let group_key: String?
}
// MARK: - MQTTService
@ -98,11 +108,24 @@ final class MQTTService: NSObject {
let mqtt = CocoaMQTT5(clientID: clientID, host: host, port: port)
mqtt.username = userName
mqtt.password = password
mqtt.keepAlive = 60
mqtt.cleanSession = true
mqtt.keepAlive = 30
mqtt.autoReconnect = true
mqtt.autoReconnectTimeInterval = 5
mqtt.delegate = self
mqtt.logLevel = .warning
// MQTT 5 Android
let connProps = MqttConnectProperties()
connProps.sessionExpiryInterval = 30
connProps.receiveMaximum = 10
connProps.maximumPacketSize = 10240
connProps.topicAliasMaximum = 0
mqtt.connectProperties = connProps
// Will
let will = CocoaMQTT5Message(topic: "willtopic", payload: [])
will.qos = .qos1
will.retained = false
mqtt.willMessage = will
self.mqtt = mqtt
_ = mqtt.connect()
@ -129,6 +152,9 @@ final class MQTTService: NSObject {
/// - callback: topic
func subscribe(topic: String, qos: CocoaMQTTQoS = .qos1, callback: ((CocoaMQTT5Message) -> Void)? = nil) {
let subscription = MqttSubscription(topic: topic, qos: qos)
subscription.noLocal = true
subscription.retainAsPublished = true
subscription.retainHandling = .none
mqtt?.subscribe([subscription])
if let cb = callback {
topicCallbacks[topic] = cb
@ -199,6 +225,22 @@ final class MQTTService: NSObject {
publish(topic: "\(topic)\(AppContextManager.shared.userId)", message: payload.toJsonString())
}
// MARK: -
func reportSignIn(lat: Double, lon: Double, addr: String) {
let signIn = MqttSignIn(topic: nil, latitude: lat, longitude: lon, address: addr)
guard let jsonData = try? JSONEncoder().encode(signIn),
let dataDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
else { return }
let payload: [String: Any] = [
"type": MqttType.signIn.rawValue,
"data": dataDict,
"extra": "",
"sort": ""
]
publish(topic: "\(topic)\(AppContextManager.shared.userId)", message: payload.toJsonString())
}
}
// MARK: - CocoaMQTT5Delegate
@ -207,6 +249,8 @@ extension MQTTService: CocoaMQTT5Delegate {
func mqtt5(_ mqtt5: CocoaMQTT5, didConnectAck ack: CocoaMQTTCONNACKReasonCode, connAckData: MqttDecodeConnAck?) {
isConnected = true
print("MQTT5 connected: \(ack)")
// topic signIn/join/leave
subscribe(topic: topic)
onConnected?()
}

View File

@ -25,4 +25,7 @@
// 微信
#import <WechatOpenSDK/WXApi.h>
// 支付宝
#import <AlipaySDK/AlipaySDK.h>
#endif /* QuickLocation_Bridging_Header_h */

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:smartdrive.zuom8.cn</string>
</array>
</dict>
</plist>

View File

@ -258,6 +258,7 @@ class GroupMemberCell: UITableViewCell {
.centerY(locationLab)
.width(10)
.height(10)
.right(80, relation: .greaterThanOrEqual)
tagView.layoutChain
.leftToRightOfView(nameLab, offset: 4)

View File

@ -112,6 +112,7 @@ class HomeView: UIView {
groupView.addSubview(groupNameLab)
groupView.addSubview(groupArrowIconView)
addSubview(messageView)
addSubview(messageStackView)
addSubview(toolsView)
addSubview(searchLottieView)
addSubview(locationView)
@ -167,6 +168,10 @@ class HomeView: UIView {
.width(36)
.height(36)
messageStackView.layoutChain
.topToBottomOfView(messageView, offset: 23)
.edgesHorzontal(15)
toolsView.layoutChain
.left(23)
.centerY()
@ -440,6 +445,50 @@ class HomeView: UIView {
view.cornerRadius = 5
return view
}()
lazy var messageStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [noticeView, messageBubbleView])
view.axis = .vertical
view.alignment = .leading
view.spacing = 15
view.backgroundColor = .clear
return view
}()
// MARK: -
lazy var noticeView: UIView = {
let view = UIView()
view.isHidden = true
view.layoutChain
.height(22)
return view
}()
// MARK: -
lazy var messageBubbleView: UIView = {
let view = UIView()
view.backgroundColor = .white.withAlphaComponent(0.8)
view.cornerRadius = 10
view.isHidden = true
view.addSubview(messageLab)
messageLab.layoutChain
.edges(all: 12)
return view
}()
lazy var messageLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textColor = .clear
return label
}()
// MARK: -
// MARK: -
lazy var toolsView: UIStackView = {

View File

@ -140,6 +140,14 @@ class HomeViewController: BaseViewController {
// MARK: - Actions
private func reactiveAction() {
//
rootView.signInView.rx.tapGesture.subscribe { _ in
let vc = SignInVC(currentUserCoord: self.lastLocation?.coordinate)
vc.isNeedLogin = true
AppRouter.push(vc)
}.disposed(by: disposeBag)
//
rootView.groupView.rx.tapGesture.subscribe { _ in
guard let groupModel = self.viewModel.groupModel else { return }
let groupViewFrame = self.view.convert(self.rootView.groupView.frame, from: self.rootView)
@ -151,14 +159,17 @@ class HomeViewController: BaseViewController {
}
}.disposed(by: disposeBag)
//
rootView.groupMemberView.refreshBtn.rx.tap.subscribe(onNext: { _ in
self.requestGroupInfo()
}).disposed(by: disposeBag)
//
rootView.groupMemberView.inviteJoinBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.viewModel.groupInfo])
}).disposed(by: disposeBag)
//
rootView.locationView.rx.tapGesture.subscribe { _ in
if let ann = self.currentUserAnnotation {
self.rootView.mapView.setCenter(ann.coordinate, animated: true)
@ -193,15 +204,6 @@ class HomeViewController: BaseViewController {
let vc = NavigationVC(member: member, currentUserCoord: userCoord, groupName: groupName, groupIcon: "\(iconIndex)")
self.navigationController?.pushViewController(vc, animated: true)
}
// MQTT
MQTTService.shared.subscribe(topic: "smartdrive/") { message in
guard let msgStr = message.string,
let data = msgStr.data(using: .utf8),
let msg = try? JSONDecoder().decode(MqttIncomingMessage.self, from: data)
else { return }
print("📩 收到消息 -> 主题:\(message.topic),内容:\(msg)")
}
}
private func bindViewModel() {
@ -438,26 +440,30 @@ extension HomeViewController {
subscribedMemberIds = newIds
}
/// topic userId
/// MQTT type
private func handleMemberLocation(topic: String, payload: String?) {
print("📩 收到消息 -> 主题:\(topic),内容:\(payload)")
guard let payload = payload,
let data = payload.data(using: .utf8),
let msg = try? JSONDecoder().decode(MqttIncomingMessage.self, from: data),
let firstPoint = msg.data?.points?.first
let msg = try? JSONDecoder().decode(MqttIncomingMessage.self, from: data)
else { return }
print("📩 收到消息 -> 主题:\(topic),内容:\(msg)")
let userId = topic.replacingOccurrences(of: "smartdrive/", with: "")
let coord = CLLocationCoordinate2D(latitude: firstPoint.lat, longitude: firstPoint.lon)
guard CLLocationCoordinate2DIsValid(coord) else { return }
switch msg.type {
case "track":
guard let firstPoint = msg.data?.points?.first else { return }
let coord = CLLocationCoordinate2D(latitude: firstPoint.lat, longitude: firstPoint.lon)
guard CLLocationCoordinate2DIsValid(coord) else { return }
// time 线
let nowMs = Date().timeIntervalSince1970 * 1000
let msgTimeMs = Double(firstPoint.time)
let diffSec = (nowMs - msgTimeMs) / 1000
let isOnline = diffSec < 60
// time 线
let nowMs = Date().timeIntervalSince1970 * 1000
let msgTimeMs = Double(firstPoint.time)
let diffSec = (nowMs - msgTimeMs) / 1000
let isOnline = diffSec < 60
let battery = msg.data?.battery ?? ""
DispatchQueue.main.async { [weak self] in
let battery = msg.data?.battery ?? ""
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
//
self.lastUpdateTimes[userId] = Date()
@ -475,6 +481,40 @@ extension HomeViewController {
self.removeAnnotation(userId: userId)
}
}
case "disconnect":
self.removeAnnotation(userId: userId)
viewModel.setMemberOffline(userId: userId)
updateOnlineCount()
case "emote":
guard let index = msg.data?.index else { break }
if index >= 30 { //
guard let gk = msg.data?.group_key, gk.components(separatedBy: "/").count >= 2 else { break }
let parts = gk.components(separatedBy: "/")
let emoteUserId = parts[1]
let textIdx = index % 10
let texts = QuickMessageView.messageList
guard textIdx < texts.count else { break }
let nickName = self.viewModel.getUserNickName(id: emoteUserId)
let tip = nickName
let fullText = tip + "对你说:" + texts[textIdx]
let attr = NSMutableAttributedString(string: fullText)
attr.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .medium), range: NSRange(location: 0, length: fullText.count))
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: NSRange(location: 0, length: tip.count))
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#333333"), range: NSRange(location: tip.count, length: texts[textIdx].count))
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.rootView.messageLab.attributedText = attr
self.rootView.messageBubbleView.isHidden = false
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.rootView.messageBubbleView.isHidden = true
}
}
}
default:
print("📩 未处理 type=\(msg.type ?? "")")
}
}
/// 线
@ -520,7 +560,6 @@ extension HomeViewController {
}
let newAnn = MemberAnnotation(member: oldMember)
mapView.addAnnotation(newAnn)
print("📌 updateAnnotation userId=\(userId) coord=\(coordinate.latitude),\(coordinate.longitude)")
}
#endif
}

View File

@ -80,6 +80,10 @@ class HomeViewModel {
return model.default_group_key.contains(id)
}
func getUserNickName(id: String) -> String {
memberList.first { $0.user_id == id }?.nick_name ?? ""
}
var memberList: [GroupMemberModel] = [] {
didSet {
var tempList = memberList

View File

@ -10,7 +10,7 @@ import TagListView
class QuickMessageView: UIView {
private let list: [String] = [
static let messageList: [String] = [
"爱你哦!", "预计到达时间?", "发生什么情况?", "注意安全", "在路上", "给我打电话",
]
@ -86,7 +86,7 @@ class QuickMessageView: UIView {
backgroundColor = .clear
setupUI()
setupRx()
setupTagData(list: list)
setupTagData(list: QuickMessageView.messageList)
}
required init?(coder aDecoder: NSCoder) {

View File

@ -0,0 +1,52 @@
//
// SignInModel.swift
// QuickLocation
//
// Created by on 2026/6/17.
//
import ObjectMapper
import RxDataSources
struct SignInInfoResponse: BaseModelProtocol {
//
var code: String?
//
var message: String?
//
var model: SignInModel?
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- map["code"]
message <- map["msg"]
model <- map["data"]
}
}
struct SignInModel: Mappable, Equatable {
var uuid: String = UUID().uuidString
///
var missCount: Int = 0
var alertCount: Int = 0
var lastTime: Int = 0
/// 0 1 22
var signInStatus: Int = 0
var signCount: Int = 0
var email: String = ""
init?(map: Map) {
}
mutating func mapping(map: Map) {
missCount <- map["missCount"]
alertCount <- map["alertCount"]
lastTime <- map["lastTime"]
signInStatus <- map["signInStatus"]
signCount <- map["signCount"]
email <- map["email"]
}
}

View File

@ -0,0 +1,55 @@
//
// SignInVC.swift
// QuickLocation
//
// Created by on 2026/6/17.
//
import UIKit
import RxSwift
import RxCocoa
import CoreLocation
class SignInVC: BaseViewController {
fileprivate var rootView: SignInView!
///
var currentCoordinate: CLLocationCoordinate2D?
override func loadView() {
rootView = SignInView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
requestSignInInfo()
rootView.signInLottieView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
guard let c = self?.currentCoordinate else { return }
MQTTService.shared.reportSignIn(lat: c.latitude, lon: c.longitude, addr: "")
DLToast.show(text: "签到成功") {
self?.requestSignInInfo()
}
}).disposed(by: disposeBag)
}
// MARK: - API
private func requestSignInInfo() {
DLToast.showLoading()
UserService.signInInfo().subscribe(onNext: { response in
guard let model = response.model else { return }
self.rootView.setupData(model)
}, onError: { _ in }).disposed(by: disposeBag)
}
init(currentUserCoord: CLLocationCoordinate2D?) {
self.currentCoordinate = currentUserCoord
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,279 @@
//
// SignInView.swift
// QuickLocation
//
// Created by on 2026/6/17.
//
import UIKit
import RxSwift
import RxCocoa
import Lottie
import CoreLocation
class SignInView: UIView {
var disposeBag = DisposeBag()
func setupData(_ model: SignInModel) {
signInTextImg.image = UIImage(named: model.signInStatus == 1 ? "SignIn/today_text" : "SignIn/signIn_text")
let text = "已连续签到\(model.signCount)"
let attr = NSMutableAttributedString(string: text)
let range = (text as NSString).range(of: model.signCount.string)
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#FF8B39"), range: range)
signInDaysLab.attributedText = attr
}
///
var onSignInTap: ((CLLocationCoordinate2D?) -> Void)?
/// VC
var currentCoordinate: CLLocationCoordinate2D?
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
addSubview(emailView)
addSubview(signInLottieView)
addSubview(signInTextImg)
addSubview(signInDaysLab)
addSubview(tipsView)
addSubview(agreementTV)
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)
emailView.layoutChain
.topToBottomOfView(navBarView, offset: 20)
.centerX()
signInLottieView.layoutChain
.topToBottomOfView(emailView, offset: 30)
.edgesHorzontal(16)
.heightToWidth(1.0)
signInTextImg.layoutChain
.centerX(signInLottieView)
.centerY(signInLottieView)
signInDaysLab.layoutChain
.topToBottomOfView(signInLottieView, offset: 16)
.centerX()
agreementTV.layoutChain
.centerX()
.bottom(kSafeBottomMargin + 30)
tipsView.layoutChain
.bottomToTopOfView(agreementTV, offset: -20)
.edgesHorzontal(40)
.centerX()
}
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 emailView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let title = UILabel()
title.text = "紧急人邮箱:"
title.textColor = UIColor(hexStr: "#999999")
title.font = .systemFont(ofSize: 14, weight: .regular)
view.addSubview(title)
title.layoutChain
.left()
.centerY()
.width(84)
view.addSubview(emailLab)
emailLab.layoutChain
.leftToRightOfView(title)
.edgesVertical(2)
let icon = UIImageView()
icon.image = UIImage(named: "Group/edit")
view.addSubview(icon)
icon.layoutChain
.leftToRightOfView(emailLab, offset: 10)
.right(15)
.width(20)
.height(20)
.centerY()
return view
}()
lazy var emailLab: UILabel = {
let label = UILabel()
label.text = "暂未添加"
label.textColor = UIColor(hexStr: "#333333")
label.font = .systemFont(ofSize: 14, weight: .medium)
return label
}()
lazy var signInLottieView: LottieAnimationView = {
let view = LottieAnimationView(name: "sign_in_continuous_data")
view.loopMode = .loop
return view
}()
lazy var signInTextImg: UIImageView = {
let view = UIImageView(image: UIImage(named: "SignIn/signIn_text"))
return view
}()
lazy var signInDaysLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#333333")
label.font = .systemFont(ofSize: 16, weight: .medium)
return label
}()
lazy var tipsView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
view.cornerRadius = 5
let icon = UIImageView(image: UIImage(named: "SignIn/tips"))
view.addSubview(icon)
icon.layoutChain
.top(14)
.left(12)
.width(14)
.height(14)
view.addSubview(tipsLab)
tipsLab.layoutChain
.leftToRightOfView(icon, offset: 3)
.edgesVertical(13)
.right(14)
return view
}()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.text = "提示2日未签到系统将以您的名义 在次日邮件通知您的紧急联系人。"
label.numberOfLines = 0
label.textColor = UIColor(hexStr: "#666666")
label.font = .systemFont(ofSize: 14, weight: .regular)
return label
}()
lazy var agreementTV: UITextView = {
let textView = UITextView()
textView.font = .systemFont(ofSize: 12, weight: .regular)
textView.backgroundColor = .clear
textView.isEditable = false
textView.isScrollEnabled = false
textView.isSelectable = false
textView.linkTextAttributes = [:]
textView.delegate = self
let text = "签到即同意 用户协议 和 隐私政策"
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttributes([.foregroundColor: UIColor(hexStr: "#666666")],
range: NSRange(location: 0, length: text.length))
attributedString.addAttributes([.foregroundColor: UIColor(hexStr: "#16B3FF"),
.link: "UserAgreement"],
range: (text as NSString).range(of: "用户协议"))
attributedString.addAttributes([.foregroundColor: UIColor(hexStr: "#16B3FF"),
.link: "PrivacyPolicy"],
range: (text as NSString).range(of: "隐私政策"))
textView.attributedText = attributedString
return textView
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
signInLottieView.play()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SignInView: UITextViewDelegate {
func textView(_ textView: UITextView,
shouldInteractWith URL: URL,
in characterRange: NSRange,
interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == "UserAgreement" {
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.userAgreementUrl])
}
if URL.absoluteString == "PrivacyPolicy" {
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.privacyPolicyUrl])
}
return true
}
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
false
}
private func openURL(_ url: String) {}
}

View File

@ -41,6 +41,9 @@ final class LoginViewModel: BaseViewModel {
guard let model = response.model else { return }
Defaults[\.loginToken] = model.token
DLToast.showSuccess(text: "登录成功") {
if let userId = model.uid {
MQTTService.shared.updateClientID("smartdrive_\(userId)")
}
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
AppRouter.shared.popOrDismiss()
}

View File

@ -64,7 +64,13 @@ class NavigationVC: BaseViewController {
private func setupMap() {
#if !targetEnvironment(simulator)
rootView.mapView.delegate = self
rootView.mapView.showsUserLocation = true
//
if let from = currentUserCoord, CLLocationCoordinate2DIsValid(from) {
let startAnn = MAPointAnnotation()
startAnn.coordinate = from
startAnn.title = "起点"
rootView.mapView.addAnnotation(startAnn)
}
#endif
}
@ -153,7 +159,7 @@ extension NavigationVC: AMapNaviDriveManagerDelegate {
if let polyline = MAPolyline(coordinates: &mutableCoords, count: UInt(coords.count)) {
rootView.mapView.add(polyline)
routeOverlay = polyline
rootView.mapView.setVisibleMapRect(polyline.boundingMapRect, animated: true)
rootView.mapView.setVisibleMapRect(polyline.boundingMapRect, edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: 260, right: 50), animated: true)
}
let distM = Int(route.routeLength)
@ -178,15 +184,16 @@ extension NavigationVC: AMapNaviDriveManagerDelegate {
extension NavigationVC: MAMapViewDelegate {
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
if annotation is MAUserLocation { return nil }
let identifier = "NavPin"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MAPinAnnotationView
let identifier = "NavEnd"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if view == nil {
view = MAPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view = MAAnnotationView(annotation: annotation, reuseIdentifier: identifier)
} else {
view?.annotation = annotation
}
// view?.pinColor = .red
// view?.canShowCallout = true
let isEnd = annotation.title == member.name
view?.image = UIImage(named: isEnd ? "Map/end" : "Map/current")
view?.centerOffset = CGPoint(x: 0, y: -22)
return view
}

View File

@ -52,6 +52,10 @@ class VipRechargeVC: BaseViewController {
}
private func reactiveAction() {
rootView.payBtnView.rx.tapGesture.subscribe(onNext: { _ in
self.requestOrderPayParams()
}).disposed(by: disposeBag)
Observable.zip(
rootView.expenseCollectionView.rx.itemSelected,
rootView.expenseCollectionView.rx.modelSelected(VipExpenseModel.self)
@ -84,7 +88,7 @@ class VipRechargeVC: BaseViewController {
// MARK: - API
private func requestRechargeInfo() {
DLToast.showLoading()
SystemService.rechargeInfo(type: "member").subscribe(onNext: { [weak self] response in
OrderService.rechargeInfo(type: "member").subscribe(onNext: { [weak self] response in
guard let self = self else { return }
self.viewModel.loadData(list: response.list)
self.rootView.setupPayTypes(self.viewModel.payType)
@ -92,4 +96,64 @@ class VipRechargeVC: BaseViewController {
self.rootView.discountLab.text = self.viewModel.discountPriceString
}).disposed(by: disposeBag)
}
// MARK: - API
private func requestOrderPayParams() {
DLToast.showLoading()
let types = viewModel.payType.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }
OrderService.orderPayParams(goodsId: viewModel.goodsId,
payType: types[rootView.selectedPayTypeTag]).subscribe(onNext: { response in
guard let data = response.data else { return }
let payType = types[self.rootView.selectedPayTypeTag]
if payType == "weixin" { //
// WXApi.registerApp(model.appId, universalLink: AppSettings.kAppsUniversalLink)
//
// let request: PayReq = PayReq()
// request.partnerId = model.partnerId
// request.prepayId = model.prepayId
// request.package = model.packageValue
// request.nonceStr = model.nonceStr
// request.timeStamp = UInt32(model.timeStamp) ?? 0
// request.sign = model.sign
// WXApi.send(request)
}
else if payType == "alipay" { //
if let payParam = data["payParam"] as? String, let appId = data["payParam"] as? String {
AlipaySDK.defaultService().payOrder(payParam, fromScheme: "Alipay\(appId)") { resultDic in
print("支付宝callback -> \(resultDic)")
guard let result = resultDic,
let resultStatus = result["resultStatus"] as? String else { return }
/**
9000
8000
4000
5000
6001
6002
6004
*/
if resultStatus == "9000" {
self.showPayResultPop(showCloseBtn: false,
status: true,
title: "支付成功",
message: "恭喜您!成功开通\(self.viewModel.goodsName)",
confirmText: "确定", confirmBlock: {
AppRouter.shared.popOrDismiss()
}, cancelBlock: { })
}
else {
self.showPayResultPop(status: false,
title: "支付失败",
message: "很抱歉,请您重新支付!",
confirmText: "重新支付", confirmBlock: { }, cancelText: "取消", cancelBlock: {
AppRouter.shared.popOrDismiss()
})
}
}
}
}
}, onError: { _ in }).disposed(by: disposeBag)
}
}

View File

@ -51,6 +51,17 @@ class VipRechargeVM {
}
}
var list: [VipExpenseModel] = []
var goodsId: String {
guard list.count > 0 else { return "" }
return list[selectedIndex].goods_id
}
var goodsName: String {
guard list.count > 0 else { return "" }
return list[selectedIndex].goods_name
}
var payType: String {
guard list.count > 0 else { return "" }
return list[selectedIndex].pay_type

View File

@ -70,17 +70,17 @@ class VipRechargeView: UIView {
.edgesHorzontal()
.heightToWidth(144/375)
.bottom()
payTypeStackView.layoutChain
.top(25)
.edgesHorzontal(16)
.centerX()
payBtnView.layoutChain
.edgesHorzontal(16)
.heightToWidth(50/343)
.centerY()
payTypeStackView.layoutChain
.top(13)
.edgesHorzontal(16)
.bottomToTopOfView(payBtnView)
payPriceView.layoutChain
.left(32)
.centerY(payBtnView, offset: -7)
@ -549,8 +549,8 @@ class VipRechargeView: UIView {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 30
// stack.distribution = .fillEqually
stack.alignment = .center
// stack.distribution = .fill
stack.alignment = .leading
return stack
}()
@ -571,6 +571,7 @@ class VipRechargeView: UIView {
for (idx, (icon, name)) in payTypeMap.enumerated() {
let isSelected = idx == 0
let view = makePayTypeView(tag: idx, icon: icon, name: name, isSelected: isSelected)
view.layoutChain.width(110).height(22)
payTypeStackView.addArrangedSubview(view)
}
}

View File

@ -0,0 +1,36 @@
//
// OrderService.swift
// QuickLocation
//
// Created by on 2026/6/17.
//
import RxSwift
import Moya
struct OrderService {
static let disposeBag = DisposeBag()
///
/// - Parameters:
/// - type: member
static func rechargeInfo(type: String) -> Observable<VipExpenseResponse> {
let api = OrderAPI.rechargeInfo(type: type).multiTarget
return APIProvider.request(token: api)
.map(VipExpenseResponse.self)
.asObservable()
}
///
/// - Parameters:
/// - goodsId: ID
/// - payType: alipayweixin
/// - source: center
/// - extra:
static func orderPayParams(goodsId: String, payType: String, source: String="center", extra: [String:Any]=[:]) -> Observable<ResponseModel> {
let api = OrderAPI.orderPayParams(goodsId: goodsId, payType: payType, source: source, extra: extra).multiTarget
return APIProvider.request(token: api)
.map(ResponseModel.self)
.asObservable()
}
}

View File

@ -26,16 +26,6 @@ struct SystemService {
.asObservable()
}
///
/// - Parameters:
/// - type: member
static func rechargeInfo(type: String) -> Observable<VipExpenseResponse> {
let api = SystemAPI.rechargeInfo(type: type).multiTarget
return APIProvider.request(token: api)
.map(VipExpenseResponse.self)
.asObservable()
}
///
static func wechatService() -> Observable<ResponseModel> {
let api = SystemAPI.wechatService.multiTarget

View File

@ -41,6 +41,14 @@ struct UserService {
.asObservable()
}
///
static func signInInfo() -> Observable<SignInInfoResponse> {
let api = UserAPI.signInInfo.multiTarget
return APIProvider.request(token: api)
.map(SignInInfoResponse.self)
.asObservable()
}
///
static func changePhone(timestamp: String, phone: String, code: String) -> Observable<ResponseModel> {
let api = UserAPI.changePhone(timestamp: timestamp, phone: phone, code: code).multiTarget

View File

@ -0,0 +1,281 @@
//
// PayResultPopVC.swift
// QuickLocation
//
// Created by on 2026/6/17.
//
import UIKit
import RxSwift
import RxCocoa
import URLNavigator
class PayResultPopVC: DLCustomPopVC {
var disposeBag = DisposeBag()
// MARK: - Accessor
public var confirmBlock: (() -> Void)?
public var cancelBlock: (() -> Void)?
public var status: Bool? {
didSet {
guard let status = status else { return }
logoImgView.image = UIImage(named: status ? "Popup/success" : "Popup/failure")
}
}
public var titleText: String? {
didSet {
titleLab.text = titleText
}
}
public var contentText: String? {
didSet {
contentLab.text = contentText
}
}
public var cancelText: String? {
didSet {
closeBtn.setTitle(cancelText, for: .normal)
}
}
public var confirmText: String? {
didSet {
confirmBtn.setTitle(confirmText, for: .normal)
}
}
private func setupSubviews() {
contentBgView.addSubview(logoImgView)
contentView.addSubview(headerBgImg)
if titleText != nil,
titleText?.isEmpty == false {
contentView.addSubview(titleLab)
}
if contentText != nil,
contentText?.isEmpty == false {
contentView.addSubview(contentLab)
}
if cancelText?.isEmpty == false || confirmText?.isEmpty == false {
contentView.addSubview(stackView)
}
closeBtn.isHidden = !(cancelText?.isEmpty == false)
confirmBtn.isHidden = !(confirmText?.isEmpty == false)
}
private func setupLayout() {
logoImgView.layoutChain
.centerX()
.top(-44)
.width(88)
.height(88)
headerBgImg.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(100/315)
var tempView: UIView?
if titleText != nil,
titleText?.isEmpty == false {
titleLab.layoutChain
.top(69)
.edgesHorzontal(20)
tempView = titleLab
}
if contentText != nil,
contentText?.isEmpty == false {
if let tempView = tempView {
contentLab.layoutChain
.edgesHorzontal(20)
.topToBottomOfView(tempView, offset: 20)
} else {
contentLab.layoutChain
.top(53)
.edgesHorzontal(20)
}
tempView = contentLab
}
guard let tempView = tempView else { return }
if cancelText?.isEmpty == false || confirmText?.isEmpty == false {
stackView.layoutChain
.topToBottomOfView(tempView, offset: 30)
.left(20)
.right(20)
.bottom(15)
confirmBtn.layoutChain
.height(50)
closeBtn.layoutChain
.height(50)
}
}
// MARK: - UI
private lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.5)
return view
}()
lazy var popView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var popBgView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 30
return view
}()
lazy var logoImgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
return view
}()
lazy var headerBgImg: UIImageView = {
let view = UIImageView(image: UIImage(named: "Popup/header_bg"))
view.contentMode = .scaleAspectFill
return view
}()
lazy var titleLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 20, weight: .bold)
label.textColor = UIColor(hexStr: "#1A1A1A")
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var contentLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#767676")
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var stackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [confirmBtn, closeBtn])
view.spacing = 10
view.axis = .vertical
view.alignment = .fill
view.distribution = .fill
return view
}()
lazy var confirmBtn: 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
btn.addTouchBlock { [weak self] _ in
self?.dismiss(animated: true, completion: {
self?.confirmBlock?()
})
}
return btn
}()
lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.backgroundColor = .clear
btn.addTouchBlock { [weak self] _ in
self?.dismiss(animated: true, completion: {
self?.cancelBlock?()
})
}
return btn
}()
// MARK: - Lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
popStyle = .center
centerCornerRadius = 30
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
setupSubviews()
setupLayout()
}
}
public extension UIViewController {
///
/// - Parameters:
/// - status: true false
/// - title:
/// - message:
/// - confirmText:
/// - confirmBlock:
/// - cancelText:
/// - cancelBlock:
func showPayResultPop(showCloseBtn: Bool = true,
status: Bool,
title: String?=nil,
message: String?=nil,
confirmText: String?=nil,
confirmBlock: (() -> Void)?=nil,
cancelText: String?=nil,
cancelBlock: (() -> Void)?=nil) {
if title == nil && message == nil { return }
let vc = PayResultPopVC()
vc.titleText = title
vc.status = status
vc.contentText = message
vc.confirmText = confirmText
vc.confirmBlock = confirmBlock
vc.cancelText = cancelText
vc.cancelBlock = cancelBlock
vc.closeBtn.isHidden = !showCloseBtn
vc.dimmingClick = showCloseBtn
present(vc, animated: true)
}
static func showPayResultPop(showCloseBtn: Bool = true,
status: Bool,
title: String?=nil,
message: String?=nil,
confirmText: String?=nil,
confirmBlock: (() -> Void)?=nil,
cancelText: String?=nil,
cancelBlock: (() -> Void)?=nil) {
if title == nil && message == nil { return }
let vc = PayResultPopVC()
vc.titleText = title
vc.status = status
vc.contentText = message
vc.confirmText = confirmText
vc.confirmBlock = confirmBlock
vc.cancelText = cancelText
vc.cancelBlock = cancelBlock
vc.closeBtn.isHidden = !showCloseBtn
vc.dimmingClick = showCloseBtn
UIViewController.topMost?.present(vc, animated: true)
}
}