parent
7cddc4499a
commit
6de0cfd68a
|
|
@ -199,6 +199,13 @@
|
|||
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; };
|
||||
30B74B432FF2438800F6744D /* GroupMemberListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B422FF2438800F6744D /* GroupMemberListView.swift */; };
|
||||
30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */; };
|
||||
30B74B472FF3608600F6744D /* ScheduleRecordModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B462FF3608600F6744D /* ScheduleRecordModel.swift */; };
|
||||
30B74B492FF3680200F6744D /* InputEmailPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B482FF3680200F6744D /* InputEmailPopupView.swift */; };
|
||||
30B74B4B2FF390EF00F6744D /* DrivingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B4A2FF390EF00F6744D /* DrivingAPI.swift */; };
|
||||
30B74B4D2FF391FF00F6744D /* DrivingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B4C2FF391FF00F6744D /* DrivingService.swift */; };
|
||||
30B74B4F2FF392D800F6744D /* DrivingStatsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B4E2FF392D800F6744D /* DrivingStatsModel.swift */; };
|
||||
30B819F22FF3CE3C00FAB693 /* ItineraryTraceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B819F12FF3CE3C00FAB693 /* ItineraryTraceView.swift */; };
|
||||
30B819F42FF3CE4900FAB693 /* ItineraryTraceVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B819F32FF3CE4900FAB693 /* ItineraryTraceVC.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 */; };
|
||||
|
|
@ -486,6 +493,13 @@
|
|||
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
|
||||
30B74B422FF2438800F6744D /* GroupMemberListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListView.swift; sourceTree = "<group>"; };
|
||||
30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVM.swift; sourceTree = "<group>"; };
|
||||
30B74B462FF3608600F6744D /* ScheduleRecordModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleRecordModel.swift; sourceTree = "<group>"; };
|
||||
30B74B482FF3680200F6744D /* InputEmailPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEmailPopupView.swift; sourceTree = "<group>"; };
|
||||
30B74B4A2FF390EF00F6744D /* DrivingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrivingAPI.swift; sourceTree = "<group>"; };
|
||||
30B74B4C2FF391FF00F6744D /* DrivingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrivingService.swift; sourceTree = "<group>"; };
|
||||
30B74B4E2FF392D800F6744D /* DrivingStatsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrivingStatsModel.swift; sourceTree = "<group>"; };
|
||||
30B819F12FF3CE3C00FAB693 /* ItineraryTraceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryTraceView.swift; sourceTree = "<group>"; };
|
||||
30B819F32FF3CE4900FAB693 /* ItineraryTraceVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryTraceVC.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>"; };
|
||||
|
|
@ -636,6 +650,7 @@
|
|||
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */,
|
||||
30D891F42FE22E0600E958FD /* OrderAPI.swift */,
|
||||
30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */,
|
||||
30B74B4A2FF390EF00F6744D /* DrivingAPI.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1060,6 +1075,7 @@
|
|||
30BAB8522FCD337C00C33B5C /* GroupService.swift */,
|
||||
30D891F62FE22E6E00E958FD /* OrderService.swift */,
|
||||
30D74AB72FEA36A50050EB2C /* ItineraryService.swift */,
|
||||
30B74B4C2FF391FF00F6744D /* DrivingService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1337,6 +1353,10 @@
|
|||
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */,
|
||||
30B74B422FF2438800F6744D /* GroupMemberListView.swift */,
|
||||
30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */,
|
||||
30B74B4E2FF392D800F6744D /* DrivingStatsModel.swift */,
|
||||
30B74B462FF3608600F6744D /* ScheduleRecordModel.swift */,
|
||||
30B819F32FF3CE4900FAB693 /* ItineraryTraceVC.swift */,
|
||||
30B819F12FF3CE3C00FAB693 /* ItineraryTraceView.swift */,
|
||||
);
|
||||
path = GroupMemberList;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1402,6 +1422,7 @@
|
|||
30CCDE502FE2785D00F5214A /* SignInVC.swift */,
|
||||
30CCDE522FE2786600F5214A /* SignInView.swift */,
|
||||
30CCDE542FE2903100F5214A /* SignInModel.swift */,
|
||||
30B74B482FF3680200F6744D /* InputEmailPopupView.swift */,
|
||||
);
|
||||
path = SignIn;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1802,6 +1823,7 @@
|
|||
305A769F2FCA8C7000227D26 /* TextContentArrowCell.swift in Sources */,
|
||||
30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */,
|
||||
30D87D052FE1336300E958FD /* NavigationView.swift in Sources */,
|
||||
30B819F42FF3CE4900FAB693 /* ItineraryTraceVC.swift in Sources */,
|
||||
305A76A02FCA8C7000227D26 /* TextTableViewCell.swift in Sources */,
|
||||
305A76A12FCA8C7000227D26 /* UIButton+RTL.m in Sources */,
|
||||
30EFF3A62FD7C5AF00EB35D4 /* GroupSettingVC.swift in Sources */,
|
||||
|
|
@ -1827,6 +1849,7 @@
|
|||
305A76AC2FCA8C7000227D26 /* String+Extension.swift in Sources */,
|
||||
30D74AB82FEA36A50050EB2C /* ItineraryService.swift in Sources */,
|
||||
30EFF3C62FDA433E00EB35D4 /* ChangePhoneView.swift in Sources */,
|
||||
30B74B492FF3680200F6744D /* InputEmailPopupView.swift in Sources */,
|
||||
305A76AD2FCA8C7000227D26 /* UIApplicationExtension.swift in Sources */,
|
||||
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */,
|
||||
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */,
|
||||
|
|
@ -1841,6 +1864,7 @@
|
|||
30EFF3E02FDA9CE300EB35D4 /* EmergencyContactModel.swift in Sources */,
|
||||
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
|
||||
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
|
||||
30B819F22FF3CE3C00FAB693 /* ItineraryTraceView.swift in Sources */,
|
||||
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
|
||||
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
|
||||
305A76B92FCA8C7000227D26 /* URL+Extension.swift in Sources */,
|
||||
|
|
@ -1872,6 +1896,7 @@
|
|||
305A76C62FCA8C7000227D26 /* AppContextManager.swift in Sources */,
|
||||
30D74ABD2FEA67EA0050EB2C /* CreateScheduleVC.swift in Sources */,
|
||||
305A76C72FCA8C7000227D26 /* UserConfigModel.swift in Sources */,
|
||||
30B74B472FF3608600F6744D /* ScheduleRecordModel.swift in Sources */,
|
||||
305A76C82FCA8C7000227D26 /* UserConfigResponse.swift in Sources */,
|
||||
305A76C92FCA8C7000227D26 /* ApiManager.swift in Sources */,
|
||||
305A76CA2FCA8C7000227D26 /* AppSettings.swift in Sources */,
|
||||
|
|
@ -1891,6 +1916,7 @@
|
|||
305A76D42FCA8C7000227D26 /* GroupModel.swift in Sources */,
|
||||
305A798C2FCAB99300227D26 /* HomeViewModel.swift in Sources */,
|
||||
30EFF3D12FDA69EC00EB35D4 /* AvatarIconListVC.swift in Sources */,
|
||||
30B74B4B2FF390EF00F6744D /* DrivingAPI.swift in Sources */,
|
||||
305A76D52FCA8C7000227D26 /* SystemResponse.swift in Sources */,
|
||||
305A76D62FCA8C7000227D26 /* ImagePlugin.swift in Sources */,
|
||||
305A76D72FCA8C7000227D26 /* NotEmpty.swift in Sources */,
|
||||
|
|
@ -1901,6 +1927,7 @@
|
|||
305A76DA2FCA8C7000227D26 /* Button+Action.swift in Sources */,
|
||||
305A76DB2FCA8C7000227D26 /* Control+Action.swift in Sources */,
|
||||
305A76DC2FCA8C7000227D26 /* InputSubject.swift in Sources */,
|
||||
30B74B4D2FF391FF00F6744D /* DrivingService.swift in Sources */,
|
||||
305A76DD2FCA8C7000227D26 /* NSObject+Rx.swift in Sources */,
|
||||
305A79902FCAC61A00227D26 /* InviteMemberVC.swift in Sources */,
|
||||
30A87A542FEE50B10095E7C6 /* ScheduleHistoryVM.swift in Sources */,
|
||||
|
|
@ -1967,6 +1994,7 @@
|
|||
30EFF3B32FD8F1C200EB35D4 /* ReviewMemberListView.swift in Sources */,
|
||||
305A76F82FCA8C7000227D26 /* DLAlert.swift in Sources */,
|
||||
305A76F92FCA8C7000227D26 /* DLToast.swift in Sources */,
|
||||
30B74B4F2FF392D800F6744D /* DrivingStatsModel.swift in Sources */,
|
||||
305A76FA2FCA8C7000227D26 /* DLEmptyDataSet.swift in Sources */,
|
||||
305A76FB2FCA8C7000227D26 /* EmptyDataSet.swift in Sources */,
|
||||
30BF300E2FED09CC00D9CB52 /* ScheduleDetailVC.swift in Sources */,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -11,11 +11,14 @@ import SwiftyUserDefaults
|
|||
|
||||
public protocol MultiTargetProtocol: TargetType {
|
||||
var multiTarget: MultiTarget { get }
|
||||
/// 为 true 时跳过 Response Body 打印
|
||||
var suppressResponseLog: Bool { get }
|
||||
}
|
||||
|
||||
public extension MultiTargetProtocol {
|
||||
|
||||
var multiTarget: MultiTarget { MultiTarget(self) }
|
||||
var suppressResponseLog: Bool { false }
|
||||
|
||||
var headers: [String : String]? {
|
||||
AppNetworkConfig.shared.httpHeader?()
|
||||
|
|
|
|||
|
|
@ -38,32 +38,53 @@ let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClos
|
|||
do {
|
||||
var request = try endpoint.urlRequest()
|
||||
// Modify the request however you like.
|
||||
request.timeoutInterval = 15 //设置请求超时时间
|
||||
request.timeoutInterval = 60 //设置请求超时时间
|
||||
done(.success(request))
|
||||
} catch {
|
||||
done(.failure(MoyaError.underlying(error, nil)))
|
||||
}
|
||||
}
|
||||
|
||||
let plugins: [PluginType] = [
|
||||
SignPlugin(),
|
||||
NetworkLoggerPlugin(configuration: .init(formatter: .init(responseData: JSONResponseDataFormatter), logOptions: .verbose)),
|
||||
NetworkActivityPlugin(networkActivityClosure: { change, _ in
|
||||
#if !SHARE_EXTENSION
|
||||
DispatchQueue.main.async {
|
||||
switch change {
|
||||
case .began:
|
||||
UIApplication.shared.isNetworkActivityIndicatorVisible = true
|
||||
case .ended:
|
||||
UIApplication.shared.isNetworkActivityIndicatorVisible = false
|
||||
}
|
||||
/// 通用 Logger,仅 playback 跳过 Response Body
|
||||
private final class FilteredLogger: PluginType {
|
||||
private let logger = NetworkLoggerPlugin(configuration: .init(
|
||||
formatter: .init(responseData: JSONResponseDataFormatter),
|
||||
logOptions: .verbose
|
||||
))
|
||||
func willSend(_ request: RequestType, target: TargetType) {
|
||||
logger.willSend(request, target: target)
|
||||
}
|
||||
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
|
||||
if let mt = target as? MultiTarget,
|
||||
case let .target(underlying) = mt,
|
||||
let api = underlying as? MultiTargetProtocol,
|
||||
api.suppressResponseLog,
|
||||
case .success(let response) = result {
|
||||
print("[suppress] \(api.path) status: \(response.statusCode)")
|
||||
return
|
||||
}
|
||||
#endif
|
||||
})
|
||||
]
|
||||
logger.didReceive(result, target: target)
|
||||
}
|
||||
}
|
||||
|
||||
public let APIProvider = MoyaProvider<MultiTarget>(requestClosure: requestClosure,
|
||||
plugins: plugins).rx
|
||||
plugins: [
|
||||
SignPlugin(),
|
||||
FilteredLogger(),
|
||||
NetworkActivityPlugin(networkActivityClosure: { change, _ in
|
||||
#if !SHARE_EXTENSION
|
||||
DispatchQueue.main.async {
|
||||
switch change {
|
||||
case .began:
|
||||
UIApplication.shared.isNetworkActivityIndicatorVisible = true
|
||||
case .ended:
|
||||
UIApplication.shared.isNetworkActivityIndicatorVisible = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
})
|
||||
]).rx
|
||||
|
||||
public extension Error {
|
||||
var underlyingError: NSError? {
|
||||
guard let moyaError = self as? MoyaError else { return nil }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// DrivingAPI.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/30.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import SwiftyUserDefaults
|
||||
internal import Alamofire
|
||||
|
||||
/// 驾驶事件API
|
||||
enum DrivingAPI {
|
||||
/// 驾驶事件
|
||||
case drivingEvents(user_id: String, start_time: String, end_time: String)
|
||||
/// 轨迹回放
|
||||
case playback(user_id: String, date: String)
|
||||
}
|
||||
|
||||
extension DrivingAPI: MultiTargetProtocol {
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .drivingEvents:
|
||||
return "mapi/driving-events/stats"
|
||||
case .playback:
|
||||
return "mapi/trips/playback"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .drivingEvents, .playback:
|
||||
return .get
|
||||
default:
|
||||
return .post
|
||||
}
|
||||
}
|
||||
|
||||
var suppressResponseLog: Bool {
|
||||
switch self {
|
||||
case .playback: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case let .drivingEvents(user_id, start_time, end_time):
|
||||
var params = Parameters()
|
||||
params["user_id"] = user_id
|
||||
params["start_time"] = start_time
|
||||
params["end_time"] = end_time
|
||||
return .requestParameters(parameters: params, encoding: URLEncoding())
|
||||
|
||||
case let .playback(user_id, date):
|
||||
var params = Parameters()
|
||||
params["user_id"] = user_id
|
||||
params["date"] = date
|
||||
params["simplify"] = true
|
||||
return .requestParameters(parameters: params, encoding: URLEncoding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,9 @@ enum UserAPI {
|
|||
/// - enable:
|
||||
/// - keep_time:
|
||||
case bubble(enable: Bool, keep_time: Int)
|
||||
|
||||
/// 紧急人邮箱
|
||||
case setEmail(email: String)
|
||||
}
|
||||
|
||||
extension UserAPI: MultiTargetProtocol {
|
||||
|
|
@ -108,6 +111,8 @@ extension UserAPI: MultiTargetProtocol {
|
|||
return "mapi/user/followed"
|
||||
case .bubble:
|
||||
return "mapi/bubble/operate"
|
||||
case .setEmail:
|
||||
return "mapi/user/signin/setemail"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +204,11 @@ extension UserAPI: MultiTargetProtocol {
|
|||
params["keep_time"] = keep_time
|
||||
}
|
||||
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||
|
||||
case let .setEmail(email):
|
||||
var params = Parameters()
|
||||
params["email"] = email
|
||||
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Contents.json
vendored
Normal file
22
QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2170@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2170@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@2x.png
vendored
Normal file
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 657 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@3x.png
vendored
Normal file
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
22
QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Contents.json
vendored
Normal file
22
QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2205@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2205@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@2x.png
vendored
Normal file
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@3x.png
vendored
Normal file
BIN
QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
|
|
@ -464,7 +464,7 @@ public extension String {
|
|||
// MARK: - 正则
|
||||
public extension String {
|
||||
/// 检测Email 邮箱格式是否有效
|
||||
static func checkEmailAddressIsValid(address: String?) -> Bool {
|
||||
static func checkEmailAddressIsValid(_ address: String?) -> Bool {
|
||||
guard address != nil else {
|
||||
return false
|
||||
}
|
||||
|
|
@ -519,3 +519,32 @@ extension String {
|
|||
return NSPredicate(format: "SELF MATCHES %@", reg).evaluate(with: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
extension String {
|
||||
// MARK: - 解析 ISO8601 字符串 2026-06-26T14:18:52+08:00 -> Date
|
||||
private func parseISO8601(_ isoStr: String) -> Date? {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withTimeZone]
|
||||
return formatter.date(from: isoStr)
|
||||
}
|
||||
|
||||
// MARK: - Date 转自定义格式字符串
|
||||
/// - Parameters:
|
||||
/// - date: 目标日期
|
||||
/// - format: 格式模板 如 yyyy-MM-dd HH:mm:ss
|
||||
/// - Returns: 格式化后的日期字符串
|
||||
func formatDate(_ date: Date, format: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "zh_CN")
|
||||
formatter.timeZone = TimeZone(identifier: "Asia/Shanghai")
|
||||
formatter.dateFormat = format
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
// MARK: - ISO字符串 直接转自定义格式
|
||||
func isoStringToCustom(_ isoStr: String, format: String) -> String? {
|
||||
guard let date = parseISO8601(isoStr) else { return nil }
|
||||
return formatDate(date, format: format)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,13 +72,23 @@ extension ApiManager {
|
|||
DLToast.dismiss()
|
||||
switch result {
|
||||
case let .success(response):
|
||||
let isPlayback = response.request?.url?.absoluteString.contains("mapi/trips/playback") ?? false
|
||||
// 处理加密响应
|
||||
let t0 = CFAbsoluteTimeGetCurrent()
|
||||
let decryptedData = ApiManager.decryptIfNeeded(response.data)
|
||||
if isPlayback {
|
||||
let rawSize = response.data.count
|
||||
let decryptTime = CFAbsoluteTimeGetCurrent() - t0
|
||||
print("[playback] raw: \(rawSize)B, decrypt: \(String(format: "%.2f", decryptTime))s")
|
||||
}
|
||||
// response转json
|
||||
let json = JSON(decryptedData ?? response.data)
|
||||
print("============== decrypt ====================")
|
||||
print(json)
|
||||
if !isPlayback {
|
||||
print("============== decrypt ====================")
|
||||
print(json)
|
||||
}
|
||||
let code = json["code"].int
|
||||
let success: Bool? = json["success"].bool
|
||||
let message = json["message"].string
|
||||
let combineMessage = message ?? "请检查网络连接"
|
||||
|
||||
|
|
@ -100,6 +110,16 @@ extension ApiManager {
|
|||
case GatewayStatusCode.failure.rawValue, GatewayStatusCode.noAuthority.rawValue: // 退出当前界面
|
||||
handlePopView(message)
|
||||
default:
|
||||
// 接口没返回code的时候取success字段
|
||||
if success != nil, success == true {
|
||||
if let decrypted = decryptedData {
|
||||
let decryptedResponse = Response(statusCode: response.statusCode,
|
||||
data: decrypted,
|
||||
request: response.request, response: response.response)
|
||||
return .success(decryptedResponse)
|
||||
}
|
||||
return result
|
||||
}
|
||||
/// 统一提示错误
|
||||
let code = GatewayStatusCode(rawValue: code ?? -9999) ?? .unknownError
|
||||
if code == .unknownError, handle {
|
||||
|
|
@ -167,15 +187,15 @@ extension ApiManager {
|
|||
let encrypt = json["encrypt"] as? Int,
|
||||
encrypt == 1,
|
||||
let encryptedString = json["data"] as? String,
|
||||
let decryptedString = aes256CBCDecrypt(encryptedString, key: jieMiKey, iv: jieMiIV),
|
||||
let decryptedData = decryptedString.data(using: .utf8),
|
||||
let decryptedJSON = try? JSONSerialization.jsonObject(with: decryptedData) as? [String: Any]
|
||||
let decryptedData = aes256CBCDecrypt(encryptedString, key: jieMiKey, iv: jieMiIV)
|
||||
else { return nil }
|
||||
|
||||
return try? JSONSerialization.data(withJSONObject: decryptedJSON)
|
||||
// 验证解密结果是否为合法 JSON
|
||||
guard (try? JSONSerialization.jsonObject(with: decryptedData)) != nil else { return nil }
|
||||
return decryptedData
|
||||
}
|
||||
|
||||
private class func aes256CBCDecrypt(_ base64String: String, key: String, iv: String) -> String? {
|
||||
private class func aes256CBCDecrypt(_ base64String: String, key: String, iv: String) -> Data? {
|
||||
guard let data = Data(base64Encoded: base64String),
|
||||
let keyData = key.data(using: .utf8),
|
||||
let ivData = iv.data(using: .utf8) else { return nil }
|
||||
|
|
@ -202,7 +222,7 @@ extension ApiManager {
|
|||
}
|
||||
|
||||
guard status == kCCSuccess else { return nil }
|
||||
return String(data: Data(bytes: buffer, count: numBytesDecrypted), encoding: .utf8)
|
||||
return Data(bytes: buffer, count: numBytesDecrypted)
|
||||
}
|
||||
|
||||
class func getMD5ID() -> String {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// DrivingStatsModel.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/30.
|
||||
//
|
||||
|
||||
import ObjectMapper
|
||||
import RxDataSources
|
||||
|
||||
/// 驾驶事件
|
||||
struct DrivingStatsResponse: BaseModelProtocol {
|
||||
// 状态码
|
||||
var code: String?
|
||||
// 消息
|
||||
var message: String?
|
||||
//
|
||||
var model: DrivingStatsModel?
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
code <- map["code"]
|
||||
message <- map["msg"]
|
||||
model <- map["data"]
|
||||
}
|
||||
}
|
||||
|
||||
struct DrivingStatsModel: Mappable {
|
||||
var uuid: String = UUID().uuidString
|
||||
/// 行驶总距离
|
||||
var distance_km: Double = 0
|
||||
/// 频繁变道次数
|
||||
var frequent_lane_change: Int = 0
|
||||
/// 急加速
|
||||
var hard_acceleration: Int = 0
|
||||
/// 急刹
|
||||
var hard_braking: Int = 0
|
||||
/// 长时间驾驶次数
|
||||
var long_driving: Int = 0
|
||||
/// 超低速
|
||||
var low_speeding: Int = 0
|
||||
/// 最大速度
|
||||
var max_speed: Double = 0
|
||||
/// 急转弯
|
||||
var sharp_turn: Int = 0
|
||||
/// 手机信号丢失
|
||||
var signal_loss: Int = 0
|
||||
/// 超速
|
||||
var speeding: Int = 0
|
||||
/// 总违规次数
|
||||
var total: Int = 0
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
distance_km <- map["distance_km"]
|
||||
frequent_lane_change <- map["frequent_lane_change"]
|
||||
hard_acceleration <- map["hard_acceleration"]
|
||||
hard_braking <- map["hard_braking"]
|
||||
long_driving <- map["long_driving"]
|
||||
low_speeding <- map["low_speeding"]
|
||||
max_speed <- map["max_speed"]
|
||||
sharp_turn <- map["sharp_turn"]
|
||||
signal_loss <- map["signal_loss"]
|
||||
speeding <- map["speeding"]
|
||||
total <- map["total"]
|
||||
}
|
||||
}
|
||||
|
|
@ -31,12 +31,15 @@ class GroupMemberListVC: BaseViewController {
|
|||
requestGroupInfo()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
rootView.unlockVipMaskView.isHidden = AppContextManager.shared.vip > 1
|
||||
}
|
||||
|
||||
private func reactiveAction() {
|
||||
|
||||
}
|
||||
|
||||
private var selectedRow = 0
|
||||
|
||||
private func bindViewModel() {
|
||||
viewModel.output.sectionedItems
|
||||
.bind(to: rootView.collectionView.rx.items(dataSource: memberDataSource))
|
||||
|
|
@ -46,24 +49,68 @@ class GroupMemberListVC: BaseViewController {
|
|||
.bind(to: rootView.drivingEventCV.rx.items(dataSource: drivingDataSource))
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// 成员点击
|
||||
viewModel.output.scheduleSectionedItems
|
||||
.bind(to: rootView.tableView.rx.items(dataSource: scheduleDataSource))
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
viewModel.output.scheduleSectionedItems
|
||||
.subscribe(onNext: { [weak self] items in
|
||||
self?.rootView.updateTableViewHeight(count: items.first?.items.count ?? 0)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
// 日期选择 → 滚动报告到顶部 + 请求驾驶数据
|
||||
rootView.selectedDate
|
||||
.skip(1)
|
||||
.subscribe(onNext: { [weak self] date in
|
||||
self?.rootView.scrollReportToTop()
|
||||
self?.requestDrivingEvents(for: date)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
// 成员点击 → 重置日期到当天(selectedDate 发射 → 自动触发 API 请求)
|
||||
rootView.collectionView.rx.modelSelected(GroupMemberModel.self)
|
||||
.subscribe(onNext: { [weak self] model in
|
||||
guard let self = self, let row = self.viewModel.rowOf(userId: model.user_id) else { return }
|
||||
self.selectedRow = row
|
||||
guard let self = self else { return }
|
||||
self.viewModel.memberId = model.user_id
|
||||
self.rootView.selectedMemberIsSelf = self.viewModel.isCurrentUser(id: model.user_id)
|
||||
self.rootView.selectedDate.accept(Date())
|
||||
self.rootView.scrollToToday()
|
||||
self.rootView.scrollReportToTop()
|
||||
self.rootView.collectionView.reloadData()
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
/// 根据日期生成起止时间戳并请求驾驶数据
|
||||
private func requestDrivingEvents(for date: Date) {
|
||||
guard !viewModel.memberId.isEmpty else { return }
|
||||
let calendar = Calendar.current
|
||||
let start = calendar.startOfDay(for: date)
|
||||
let end: Date
|
||||
if calendar.isDateInToday(date) {
|
||||
end = Date() // 当天以当前时间为结束
|
||||
} else {
|
||||
end = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: date) ?? date
|
||||
}
|
||||
requestDrivingEvents(start_time: "\(Int64(start.timeIntervalSince1970 * 1000))",
|
||||
end_time: "\(Int64(end.timeIntervalSince1970 * 1000))")
|
||||
|
||||
requestPlayback(date: "\(Int64(start.timeIntervalSince1970 * 1000))")
|
||||
}
|
||||
|
||||
// MARK: - dataSource
|
||||
private lazy var scheduleDataSource: RxTableViewSectionedReloadDataSource<ScheduleRecordSection> = {
|
||||
RxTableViewSectionedReloadDataSource<ScheduleRecordSection> { _, tableView, indexPath, model in
|
||||
let cell: ScheduleRecordCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(model)
|
||||
return cell
|
||||
}
|
||||
}()
|
||||
|
||||
private lazy var memberDataSource: RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> = {
|
||||
RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> { [weak self] datasource, collectionView, indexPath, model in
|
||||
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(model: model,
|
||||
isCurrentUser: self?.viewModel.isCurrentUser(id: model.user_id) ?? false,
|
||||
isSelected: indexPath.row == (self?.selectedRow ?? 0))
|
||||
isSelected: model.user_id == self?.viewModel.memberId)
|
||||
return cell
|
||||
}
|
||||
}()
|
||||
|
|
@ -84,13 +131,38 @@ class GroupMemberListVC: BaseViewController {
|
|||
guard let model = response.model else { return }
|
||||
self.viewModel.groupModel = model
|
||||
self.viewModel.loadData(response.list)
|
||||
// 默认选中第一个成员,更新日期可点范围
|
||||
if let first = self.viewModel.firstMemberId {
|
||||
self.rootView.selectedMemberIsSelf = self.viewModel.isCurrentUser(id: first)
|
||||
self.rootView.selectedMemberIsSelf = self.viewModel.isCurrentUser(id: self.viewModel.memberId)
|
||||
// 首次加载后请求今日驾驶数据
|
||||
if !self.viewModel.memberId.isEmpty {
|
||||
self.requestDrivingEvents(for: Date())
|
||||
}
|
||||
}.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// 驾驶事件
|
||||
private func requestDrivingEvents(start_time: String, end_time: String) {
|
||||
DrivingService.drivingEvents(user_id: viewModel.memberId, start_time: start_time, end_time: end_time).subscribe { response in
|
||||
guard let model = response.model else { return }
|
||||
self.rootView.mileageLab.text = String(format: "%.1fkm", model.distance_km)
|
||||
self.rootView.speedLab.text = String(format: "%.0fkm/h", model.max_speed)
|
||||
self.viewModel.updateDrivingStats(model)
|
||||
}.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// 行程记录
|
||||
private func requestPlayback(date: String) {
|
||||
let start = CFAbsoluteTimeGetCurrent()
|
||||
dl.showLoading(text: "行程记录获取中...")
|
||||
DrivingService.playback(user_id: viewModel.memberId, date: date).subscribe(onNext: { [weak self] response in
|
||||
let elapsed = CFAbsoluteTimeGetCurrent() - start
|
||||
print("[playback] total: \(String(format: "%.1f", elapsed))s, items: \(response.list.count)")
|
||||
self?.dl.dismiss()
|
||||
self?.viewModel.loadScheduleRecords(response.list)
|
||||
}, onError: { _ in
|
||||
self.dl.dismiss()
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
init(groupKey: String) {
|
||||
viewModel = GroupMemberListVM(groupKey: groupKey)
|
||||
|
|
|
|||
|
|
@ -10,22 +10,6 @@ import RxRelay
|
|||
import RxDataSources
|
||||
import ObjectMapper
|
||||
|
||||
// MARK: - DrivingStats 模型
|
||||
struct DrivingStatsData {
|
||||
let distance_km: Double?
|
||||
let frequent_lane_change: Int?
|
||||
let hard_acceleration: Int?
|
||||
let hard_braking: Int?
|
||||
let long_driving: Int?
|
||||
let low_speeding: Int?
|
||||
let max_speed: Double?
|
||||
let period: String?
|
||||
let sharp_turn: Int?
|
||||
let signal_loss: Int?
|
||||
let speeding: Int?
|
||||
let total: Int?
|
||||
}
|
||||
|
||||
struct DrivingEventItem: IdentifiableType, Equatable {
|
||||
typealias Identity = String
|
||||
let identity: String
|
||||
|
|
@ -42,6 +26,7 @@ struct DrivingEventItem: IdentifiableType, Equatable {
|
|||
}
|
||||
|
||||
typealias DrivingEventSection = SectionModel<String, DrivingEventItem>
|
||||
typealias ScheduleRecordSection = SectionModel<String, ScheduleRecordModel>
|
||||
|
||||
class GroupMemberListVM {
|
||||
|
||||
|
|
@ -51,13 +36,16 @@ class GroupMemberListVM {
|
|||
struct Output {
|
||||
var sectionedItems: Observable<[GroupMemberListSectionModel]>
|
||||
var drivingSectionedItems: Observable<[DrivingEventSection]>
|
||||
var scheduleSectionedItems: Observable<[ScheduleRecordSection]>
|
||||
}
|
||||
|
||||
let output: Output
|
||||
|
||||
private var disposeBag = DisposeBag()
|
||||
var memberId: String = ""
|
||||
|
||||
private let sectionedItems = PublishSubject<[GroupMemberListSectionModel]>()
|
||||
private let drivingItemsRelay = BehaviorRelay<[DrivingEventItem]>(value: GroupMemberListVM.defaultDrivingEvents)
|
||||
private let scheduleItemsRelay = BehaviorRelay<[ScheduleRecordModel]>(value: [])
|
||||
|
||||
private var memberList: [GroupMemberModel] = []
|
||||
|
||||
|
|
@ -89,30 +77,33 @@ class GroupMemberListVM {
|
|||
tempmemberList.moveToFirst { $0.user_id == AppContextManager.shared.userId }
|
||||
tempmemberList.moveToFirst { isGroupOwn(id: $0.user_id) }
|
||||
memberList = tempmemberList
|
||||
memberId = memberList.first?.user_id ?? ""
|
||||
sectionedItems.onNext(memberList.mapSection())
|
||||
}
|
||||
|
||||
/// 第一个成员的 userId
|
||||
var firstMemberId: String? { memberList.first?.user_id }
|
||||
|
||||
/// 获取成员在列表中的行号
|
||||
func rowOf(userId: String) -> Int? {
|
||||
memberList.firstIndex(where: { $0.user_id == userId })
|
||||
}
|
||||
|
||||
/// 加载行程记录
|
||||
func loadScheduleRecords(_ list: [ScheduleRecordModel]) {
|
||||
scheduleItemsRelay.accept(list)
|
||||
}
|
||||
|
||||
/// 更新驾驶事件统计
|
||||
func updateDrivingStats(_ stats: DrivingStatsData) {
|
||||
func updateDrivingStats(_ stats: DrivingStatsModel) {
|
||||
let items = Self.defaultDrivingEvents.enumerated().map { i, item in
|
||||
let count: Int
|
||||
switch i {
|
||||
case 0: count = stats.hard_acceleration ?? 0
|
||||
case 1: count = stats.speeding ?? 0
|
||||
case 2: count = stats.sharp_turn ?? 0
|
||||
case 3: count = stats.low_speeding ?? 0
|
||||
case 4: count = stats.hard_braking ?? 0
|
||||
case 5: count = stats.frequent_lane_change ?? 0
|
||||
case 6: count = stats.signal_loss ?? 0
|
||||
case 7: count = stats.long_driving ?? 0
|
||||
case 0: count = stats.hard_acceleration
|
||||
case 1: count = stats.sharp_turn
|
||||
case 2: count = stats.hard_braking
|
||||
case 3: count = stats.signal_loss
|
||||
case 4: count = stats.speeding
|
||||
case 5: count = stats.low_speeding
|
||||
case 6: count = stats.frequent_lane_change
|
||||
case 7: count = stats.long_driving
|
||||
default: count = 0
|
||||
}
|
||||
return DrivingEventItem(title: item.title, iconName: item.iconName, count: count)
|
||||
|
|
@ -124,7 +115,8 @@ class GroupMemberListVM {
|
|||
self.groupKey = groupKey
|
||||
output = Output(
|
||||
sectionedItems: sectionedItems.asObservable(),
|
||||
drivingSectionedItems: drivingItemsRelay.map { $0.mapSection() }.asObservable()
|
||||
drivingSectionedItems: drivingItemsRelay.map { $0.mapSection() }.asObservable(),
|
||||
scheduleSectionedItems: scheduleItemsRelay.map { $0.mapSection() }.asObservable()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,43 @@ class GroupMemberListView: UIView {
|
|||
return cv
|
||||
}()
|
||||
|
||||
// MARK: - 未解锁会员遮罩
|
||||
lazy var unlockVipMaskView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.isHidden = true
|
||||
|
||||
let maskBg = UIImageView(image: UIImage(named: "GroupMemberList/mask_bg"))
|
||||
view.addSubview(maskBg)
|
||||
maskBg.layoutChain.edges()
|
||||
|
||||
let tipsImg = UIImageView(image: UIImage(named: "GroupMemberList/mask_tips"))
|
||||
view.addSubview(tipsImg)
|
||||
tipsImg.layoutChain
|
||||
.top(83)
|
||||
.centerX()
|
||||
.width(283)
|
||||
.height(194)
|
||||
|
||||
let btn = UIButton()
|
||||
btn.setTitle("立即开通", for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setBackgroundColor(UIColor(hexStr: "#FF7B24"), for: .normal)
|
||||
btn.cornerRadius = 25
|
||||
btn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.push(Route.vipRecharge)
|
||||
}).disposed(by: disposeBag)
|
||||
view.addSubview(btn)
|
||||
btn.layoutChain
|
||||
.topToBottomOfView(tipsImg, offset: 23)
|
||||
.centerX()
|
||||
.width(224)
|
||||
.height(50)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - 报告
|
||||
lazy var reportView: UIView = {
|
||||
let view = UIView()
|
||||
|
|
@ -221,6 +258,11 @@ class GroupMemberListView: UIView {
|
|||
.topToBottomOfView(dateView)
|
||||
.edges(excludingEdge: .top)
|
||||
|
||||
view.addSubview(unlockVipMaskView)
|
||||
unlockVipMaskView.layoutChain
|
||||
.topToBottomOfView(dateView)
|
||||
.edges(excludingEdge: .top)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
|
|
@ -342,12 +384,7 @@ class GroupMemberListView: UIView {
|
|||
.rightToLeftOfView(dateNextBtn, offset: -5)
|
||||
.edgesVertical()
|
||||
|
||||
// 直接翻到最后一页(第 4 页,index 28 开始),今天在第 3 位
|
||||
DispatchQueue.main.async {
|
||||
let page: CGFloat = 4 // 第 5 页,items 28-34
|
||||
let offset = page * CGFloat(self.daysPerPage) * self.dateItemWidth
|
||||
self.dateCollectionView.contentOffset.x = offset
|
||||
}
|
||||
DispatchQueue.main.async { self.scrollToToday() }
|
||||
|
||||
return view
|
||||
}()
|
||||
|
|
@ -406,6 +443,11 @@ class GroupMemberListView: UIView {
|
|||
.top(5)
|
||||
.edgesHorzontal(15)
|
||||
.height(249)
|
||||
|
||||
contentView.addSubview(recordView)
|
||||
recordView.layoutChain
|
||||
.topToBottomOfView(drivingAnalysisView, offset: 15)
|
||||
.edgesHorzontal(15)
|
||||
.bottom(20)
|
||||
|
||||
return view
|
||||
|
|
@ -455,6 +497,76 @@ class GroupMemberListView: UIView {
|
|||
return cv
|
||||
}()
|
||||
|
||||
// MARK: - 行程记录
|
||||
lazy var recordView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.cornerRadius = 10
|
||||
|
||||
let titleBg = UIImageView(image: UIImage(named: "GroupMemberList/title_bg"))
|
||||
view.addSubview(titleBg)
|
||||
|
||||
let titleLab = UILabel()
|
||||
titleLab.text = "行程记录"
|
||||
titleLab.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
view.addSubview(titleLab)
|
||||
titleLab.layoutChain
|
||||
.top(15)
|
||||
.centerX()
|
||||
|
||||
titleBg.layoutChain
|
||||
.centerX()
|
||||
.bottomToView(titleLab, offset: 5)
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.layoutChain
|
||||
.topToBottomOfView(titleBg, offset: 10)
|
||||
.edgesHorzontal()
|
||||
.bottom()
|
||||
|
||||
view.addSubview(nodataLab)
|
||||
nodataLab.layoutChain.centerX().centerY()
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.estimatedRowHeight = 177
|
||||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.isScrollEnabled = false
|
||||
tableView.register(ScheduleRecordCell.self)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
lazy var nodataLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.text = "暂无行程记录"
|
||||
l.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
l.textColor = UIColor(hexStr: "#999999")
|
||||
return l
|
||||
}()
|
||||
|
||||
/// 滚动报告区域到顶部
|
||||
func scrollReportToTop() {
|
||||
scrollView.setContentOffset(.zero, animated: false)
|
||||
}
|
||||
|
||||
/// 滚到今天所在页(第 5 页,items 28-34,今天在第 3 位)
|
||||
func scrollToToday() {
|
||||
let page: CGFloat = 4
|
||||
let offset = page * CGFloat(daysPerPage) * dateItemWidth
|
||||
dateCollectionView.contentOffset.x = offset
|
||||
}
|
||||
|
||||
func updateTableViewHeight(count: Int) {
|
||||
let h = CGFloat(count) * 179
|
||||
tableView.layoutChain.height(h > 0 ? h : 179)
|
||||
nodataLab.isHidden = h > 0
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateArrowVisibility()
|
||||
|
|
@ -547,7 +659,7 @@ class DateCell: UICollectionViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - GroupMemberListCell
|
||||
// MARK: - GroupMemberListCell 成员
|
||||
class GroupMemberListCell: UICollectionViewCell {
|
||||
|
||||
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) {
|
||||
|
|
@ -706,7 +818,7 @@ class GroupMemberListCell: UICollectionViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - DrivingEventCell
|
||||
// MARK: - DrivingEventCell 驾驶事件
|
||||
class DrivingEventCell: UICollectionViewCell {
|
||||
|
||||
func configure(_ item: DrivingEventItem) {
|
||||
|
|
@ -758,3 +870,254 @@ class DrivingEventCell: UICollectionViewCell {
|
|||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
}
|
||||
|
||||
// MARK: - ScheduleRecordCells
|
||||
class ScheduleRecordCell: UITableViewCell {
|
||||
|
||||
func configure(_ model: ScheduleRecordModel) {
|
||||
distanceLab.text = String(format: "%.1fkm", model.distance_km)
|
||||
speedLab.text = String(format: "最高时速 %.0fkm/h", model.max_speed)
|
||||
scoreLab.text = model.score.string
|
||||
|
||||
dayLab.text = model.start_time.isoStringToCustom(model.start_time, format: "dd") ?? ""
|
||||
yearMonthLab.text = model.start_time.isoStringToCustom(model.start_time, format: "yyyy.MM") ?? ""
|
||||
|
||||
let startTime = model.start_time.isoStringToCustom(model.start_time, format: "HH:mm") ?? ""
|
||||
let endTime = model.end_time.isoStringToCustom(model.end_time, format: "HH:mm") ?? ""
|
||||
durationLab.text = "\(startTime)-\(endTime)(\(model.duration_minutes)分钟)"
|
||||
|
||||
guard let start_address = model.start_address,
|
||||
let end_address = model.end_address else { return }
|
||||
|
||||
startLab.text = start_address.street
|
||||
endLab.text = end_address.street
|
||||
}
|
||||
|
||||
private func setupViews() {
|
||||
contentView.addSubview(dotView)
|
||||
contentView.addSubview(dayLab)
|
||||
contentView.addSubview(yearMonthLab)
|
||||
contentView.addSubview(distanceLab)
|
||||
contentView.addSubview(durationLab)
|
||||
contentView.addSubview(detailView)
|
||||
detailView.addSubview(headerBgView)
|
||||
detailView.addSubview(speedLab)
|
||||
detailView.addSubview(scroreTitleLab)
|
||||
detailView.addSubview(scoreLab)
|
||||
detailView.addSubview(unitLab)
|
||||
detailView.addSubview(startTitleLab)
|
||||
detailView.addSubview(startLab)
|
||||
detailView.addSubview(endTitleLab)
|
||||
detailView.addSubview(endLab)
|
||||
contentView.addSubview(lineView)
|
||||
|
||||
dotView.layoutChain
|
||||
.left(15)
|
||||
.width(8)
|
||||
.heightToWidth(1)
|
||||
|
||||
dayLab.layoutChain
|
||||
.top(15)
|
||||
.leftToRightOfView(dotView, offset: 8)
|
||||
|
||||
dotView.layoutChain.centerY(dayLab)
|
||||
|
||||
yearMonthLab.layoutChain
|
||||
.leftToRightOfView(dayLab, offset: 4)
|
||||
.bottomToView(dayLab, offset: -2)
|
||||
|
||||
distanceLab.layoutChain
|
||||
.leftToRightOfView(yearMonthLab, offset: 8)
|
||||
.bottomToView(yearMonthLab)
|
||||
|
||||
durationLab.layoutChain
|
||||
.centerY(distanceLab)
|
||||
.leftToRightOfView(distanceLab, offset: 6)
|
||||
|
||||
detailView.layoutChain
|
||||
.topToBottomOfView(dayLab, offset: 15)
|
||||
.left(28)
|
||||
.right(15)
|
||||
.height(110)
|
||||
.bottom(15)
|
||||
|
||||
headerBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(38)
|
||||
|
||||
speedLab.layoutChain
|
||||
.top(11)
|
||||
.left(15)
|
||||
|
||||
unitLab.layoutChain
|
||||
.right(10)
|
||||
.centerY(speedLab)
|
||||
|
||||
scoreLab.layoutChain
|
||||
.rightToLeftOfView(unitLab)
|
||||
.centerY(speedLab)
|
||||
|
||||
scroreTitleLab.layoutChain
|
||||
.rightToLeftOfView(scoreLab, offset: -3)
|
||||
.centerY(speedLab)
|
||||
|
||||
startTitleLab.layoutChain
|
||||
.topToBottomOfView(headerBgView, offset: 12)
|
||||
.left(15)
|
||||
|
||||
startLab.layoutChain
|
||||
.leftToRightOfView(startTitleLab, offset: 2)
|
||||
.centerY(startTitleLab)
|
||||
|
||||
endTitleLab.layoutChain
|
||||
.topToBottomOfView(startTitleLab, offset: 11)
|
||||
.leftToView(startTitleLab)
|
||||
|
||||
endLab.layoutChain
|
||||
.leftToRightOfView(endTitleLab, offset: 2)
|
||||
.centerY(endTitleLab)
|
||||
|
||||
lineView.layoutChain
|
||||
.height(0.5)
|
||||
.edgesHorzontal(15)
|
||||
.bottom()
|
||||
}
|
||||
|
||||
lazy var dotView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
view.cornerRadius = 4
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var dayLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 20, weight: .bold)
|
||||
l.textColor = UIColor(hexStr: "#16B3FF")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var yearMonthLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 12, weight: .bold)
|
||||
l.textColor = UIColor(hexStr: "#66CDFF")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var distanceLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#333333")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var durationLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
l.textColor = UIColor(hexStr: "#333333")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var detailView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
|
||||
view.cornerRadius = 10
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var headerBgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#C4E8FF")
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var speedLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#333333")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var scroreTitleLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.text = "安全评分"
|
||||
l.font = .systemFont(ofSize: 10, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#333333")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var scoreLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.text = "0"
|
||||
l.font = .systemFont(ofSize: 24, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#16B3FF")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var unitLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.text = "分"
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#16B3FF")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var startTitleLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.text = "起点:"
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#666666")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var startLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#333333")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var endTitleLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.text = "终点:"
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#666666")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var endLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#333333")
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var lineView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = ThemeManager.shared.color.lineColor
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
setupViews()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// ItineraryTraceVC.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ItineraryTraceVC: BaseViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
//
|
||||
// ItineraryTraceView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import AMapNaviKit
|
||||
|
||||
class ItineraryTraceView: UIView {
|
||||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
private func setupRx() {
|
||||
backBtn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(mapView)
|
||||
addSubview(navBgView)
|
||||
addSubview(navBarView)
|
||||
navBarView.addSubview(backBtn)
|
||||
navBarView.addSubview(navTitleLabel)
|
||||
|
||||
mapView.layoutChain
|
||||
.edges()
|
||||
|
||||
navBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.heightToWidth(160/375)
|
||||
|
||||
navBarView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(kNaviHeight)
|
||||
|
||||
backBtn.layoutChain
|
||||
.centerY(navTitleLabel)
|
||||
.left(15)
|
||||
.width(24).height(24)
|
||||
|
||||
navTitleLabel.layoutChain
|
||||
.top(kStatusBarHeight + 12)
|
||||
.centerX()
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 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 v = UIView()
|
||||
v.backgroundColor = .clear
|
||||
return v
|
||||
}()
|
||||
|
||||
lazy var backBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setImage(UIImage(named: "Common/back"), for: .normal)
|
||||
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 30)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var navTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 18, weight: .medium)
|
||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
label.text = "行程轨迹"
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var deleteBtn: UIButton = {
|
||||
let btn = UIButton()
|
||||
btn.setImage(UIImage(named: "Common/delete"), for: .normal)
|
||||
btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 20, bottom: 15, right: 15)
|
||||
btn.isHidden = true
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var mapView: MAMapView! = {
|
||||
let mv = MAMapView()
|
||||
mv.zoomLevel = 14
|
||||
mv.showsUserLocation = false
|
||||
mv.showsCompass = false
|
||||
mv.userTrackingMode = .none
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: kNaviHeight) }
|
||||
return mv
|
||||
}()
|
||||
|
||||
lazy var bottomView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.layer.cornerRadius = 20
|
||||
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
|
||||
view.addSubview(titleLab)
|
||||
titleLab.layoutChain
|
||||
.top(20)
|
||||
.left(20)
|
||||
|
||||
view.addSubview(startTimeLab)
|
||||
startTimeLab.layoutChain
|
||||
.topToBottomOfView(titleLab, offset: 8)
|
||||
.leftToView(titleLab)
|
||||
|
||||
let dotView = UIView()
|
||||
dotView.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
dotView.cornerRadius = 3
|
||||
view.addSubview(dotView)
|
||||
dotView.layoutChain
|
||||
.leftToRightOfView(startTimeLab, offset: 10)
|
||||
.centerY(startTimeLab)
|
||||
.width(6)
|
||||
.heightToWidth(1)
|
||||
|
||||
view.addSubview(startAddressLab)
|
||||
startAddressLab.layoutChain
|
||||
.centerY(startTimeLab)
|
||||
.leftToRightOfView(dotView, offset: 12)
|
||||
|
||||
view.addSubview(endTimeLab)
|
||||
endTimeLab.layoutChain
|
||||
.topToBottomOfView(startTimeLab, offset: 24)
|
||||
.leftToView(startTimeLab)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
/// xxx 的行程详细信息
|
||||
lazy var titleLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#0F2846")
|
||||
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var startTimeLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#666666")
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var startAddressLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var endTimeLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#666666")
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var endAddressLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
setupUI()
|
||||
setupRx()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
//
|
||||
// ScheduleRecordModel.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/30.
|
||||
//
|
||||
|
||||
import ObjectMapper
|
||||
import RxDataSources
|
||||
|
||||
/// 驾驶记录
|
||||
struct ScheduleRecordListResponse: BaseModelProtocol {
|
||||
// 状态码
|
||||
var code: String?
|
||||
// 消息
|
||||
var message: String?
|
||||
// 日期
|
||||
var date = ""
|
||||
// 用户ID
|
||||
var user_id = ""
|
||||
//
|
||||
var list: [ScheduleRecordModel] = []
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
code <- map["code"]
|
||||
message <- map["msg"]
|
||||
date <- map["date"]
|
||||
user_id <- map["user_id"]
|
||||
list <- map["data.trips"]
|
||||
}
|
||||
}
|
||||
|
||||
struct ScheduleRecordModel: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var id: String = ""
|
||||
/// 开始时间
|
||||
var start_time: String = ""
|
||||
/// 结束时间
|
||||
var end_time: String = ""
|
||||
/// 公里数
|
||||
var distance_km: String = ""
|
||||
/// 驾驶时间
|
||||
var duration_minutes: Int = 0
|
||||
/// 平均时速
|
||||
var avg_speed: String = ""
|
||||
/// 最高时速
|
||||
var max_speed: Double = 0
|
||||
/// 评分
|
||||
var score: Int = 0
|
||||
|
||||
var start_location: TripLocation?
|
||||
var end_location: TripLocation?
|
||||
var start_address: TripAddress?
|
||||
var end_address: TripAddress?
|
||||
var trajectory_path: [TrackPoint] = []
|
||||
var driving_events: [TrackEvent] = []
|
||||
var stay_points: [StayPoint] = []
|
||||
var statistics: TripStatistics?
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
id <- map["id"]
|
||||
start_time <- map["start_time"]
|
||||
end_time <- map["end_time"]
|
||||
distance_km <- map["distance_km"]
|
||||
duration_minutes <- map["duration_minutes"]
|
||||
avg_speed <- map["avg_speed"]
|
||||
max_speed <- map["max_speed"]
|
||||
score <- map["score"]
|
||||
start_location <- map["start_location"]
|
||||
end_location <- map["end_location"]
|
||||
start_address <- map["start_address"]
|
||||
end_address <- map["end_address"]
|
||||
trajectory_path <- map["trajectory_path"]
|
||||
driving_events <- map["driving_events"]
|
||||
stay_points <- map["stay_points"]
|
||||
statistics <- map["statistics"]
|
||||
}
|
||||
}
|
||||
|
||||
extension ScheduleRecordModel: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 对应的子模型
|
||||
struct TripLocation: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var lat: Double = 0
|
||||
var lng: Double = 0
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
lat <- map["lat"]
|
||||
lng <- map["lng"]
|
||||
}
|
||||
}
|
||||
|
||||
extension TripLocation: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
||||
struct TripAddress: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var formatted_address: String = ""
|
||||
var country: String = ""
|
||||
var province: String = ""
|
||||
var city: String = ""
|
||||
var district: String = ""
|
||||
var street: String = ""
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
formatted_address <- map["formatted_address"]
|
||||
country <- map["country"]
|
||||
province <- map["province"]
|
||||
city <- map["city"]
|
||||
district <- map["district"]
|
||||
street <- map["street"]
|
||||
}
|
||||
}
|
||||
|
||||
extension TripAddress: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
||||
struct TrackPoint: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var lat: Double = 0
|
||||
var lng: Double = 0
|
||||
var timestamp: Int64 = 0
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
lat <- map["lat"]
|
||||
lng <- map["lng"]
|
||||
timestamp <- map["timestamp"]
|
||||
}
|
||||
}
|
||||
|
||||
extension TrackPoint: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
||||
struct TrackEvent: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var type: String = ""
|
||||
var timestamp: Int64 = 0
|
||||
var value: Int = 0
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
type <- map["type"]
|
||||
timestamp <- map["timestamp"]
|
||||
value <- map["value"]
|
||||
}
|
||||
}
|
||||
|
||||
extension TrackEvent: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
||||
struct StayPoint: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var lat: Double = 0
|
||||
var lng: Double = 0
|
||||
var start_time: Int64 = 0
|
||||
var end_time: Int64 = 0
|
||||
var address: String = ""
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
lat <- map["lat"]
|
||||
lng <- map["lng"]
|
||||
start_time <- map["start_time"]
|
||||
end_time <- map["end_time"]
|
||||
address <- map["address"]
|
||||
}
|
||||
}
|
||||
|
||||
extension StayPoint: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
||||
struct TripStatistics: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var hard_acceleration: Int = 0
|
||||
var hard_braking: Int = 0
|
||||
var sharp_turn: Int = 0
|
||||
var speeding: Int = 0
|
||||
var low_speeding: Int = 0
|
||||
var frequent_lane_change: Int = 0
|
||||
var signal_loss: Int = 0
|
||||
var long_driving: Int = 0
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
hard_acceleration <- map["hard_acceleration"]
|
||||
hard_braking <- map["hard_braking"]
|
||||
sharp_turn <- map["sharp_turn"]
|
||||
speeding <- map["speeding"]
|
||||
low_speeding <- map["low_speeding"]
|
||||
frequent_lane_change <- map["frequent_lane_change"]
|
||||
signal_loss <- map["signal_loss"]
|
||||
long_driving <- map["long_driving"]
|
||||
}
|
||||
}
|
||||
|
||||
extension TripStatistics: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +192,7 @@ class GroupScheduleView: UIView {
|
|||
mv.showsUserLocation = false
|
||||
mv.showsCompass = false
|
||||
mv.userTrackingMode = .none
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: kNaviHeight) }
|
||||
return mv
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ class HomeView: UIView {
|
|||
mv.showsScale = false
|
||||
mv.isRotateEnabled = false
|
||||
mv.isRotateCameraEnabled = false
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: kStatusBarHeight + 30) }
|
||||
return mv
|
||||
}()
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class HomeViewController: BaseViewController {
|
|||
|
||||
private func startLocationTimer() {
|
||||
locationTimer?.invalidate()
|
||||
locationTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] _ in
|
||||
locationTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
|
||||
guard let self = self, let loc = self.lastLocation else { return }
|
||||
let coord = loc.coordinate
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ class SOSView: UIView {
|
|||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// 添加紧急联系人
|
||||
addContactView.rx.tapGesture.subscribe(onNext: { _ in
|
||||
AppRouter.push(Route.emergencyContact)
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
//
|
||||
// InputEmailPopupView.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
class InputEmailPopupView: UIView {
|
||||
|
||||
private static let shared = InputEmailPopupView(frame: CGRect(origin: .zero, size: kScreenSize))
|
||||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
private var completion: ((String) -> Void)?
|
||||
|
||||
static func show(completion: @escaping ((String) -> Void)) {
|
||||
guard let superView = kKeyWindow else {
|
||||
return
|
||||
}
|
||||
|
||||
if InputEmailPopupView.shared.superview != nil {
|
||||
InputEmailPopupView.shared.removeFromSuperview()
|
||||
InputEmailPopupView.shared.bgView.frame = .zero
|
||||
}
|
||||
InputEmailPopupView.shared.bgView.alpha = 1
|
||||
InputEmailPopupView.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
||||
superView.addSubview(InputEmailPopupView.shared)
|
||||
superView.bringSubviewToFront(InputEmailPopupView.shared)
|
||||
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
InputEmailPopupView.shared.bgView.alpha = 1
|
||||
}
|
||||
|
||||
InputEmailPopupView.shared.completion = { text in
|
||||
completion(text)
|
||||
InputEmailPopupView.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭
|
||||
static func dismiss() {
|
||||
guard InputEmailPopupView.shared.superview != nil else { return }
|
||||
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
|
||||
InputEmailPopupView.shared.bgView.alpha = 0
|
||||
} completion: { _ in
|
||||
InputEmailPopupView.shared.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupRx() {
|
||||
emailTF.rx.controlEvent(.editingDidEndOnExit)
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.emailTF.resignFirstResponder()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
confirmBtn.rx.tap.subscribe(onNext: { _ in
|
||||
guard let text = self.emailTF.text, String.checkEmailAddressIsValid(text) else {
|
||||
DLToast.show(text: "请输入正确的邮箱")
|
||||
return
|
||||
}
|
||||
self.completion?(text)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
cancelBtn.rx.tap.subscribe(onNext: { _ in
|
||||
InputEmailPopupView.dismiss()
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private lazy var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .black.withAlphaComponent(0.5)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var infoView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.cornerRadius = 15
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "添加邮箱"
|
||||
label.textColor = UIColor(hexStr: "#333333")
|
||||
label.font = .systemFont(ofSize: 20, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var textFieldView: UIView = {
|
||||
let view = UIView()
|
||||
view.cornerRadius = 10
|
||||
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var emailTF: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
textField.placeholder = "请添加邮箱地址"
|
||||
textField.returnKeyType = .done
|
||||
return textField
|
||||
}()
|
||||
|
||||
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
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var cancelBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("取消", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.backgroundColor = .clear
|
||||
return btn
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
addSubview(bgView)
|
||||
|
||||
bgView.addSubview(infoView)
|
||||
infoView.addSubview(titleLab)
|
||||
infoView.addSubview(textFieldView)
|
||||
textFieldView.addSubview(emailTF)
|
||||
infoView.addSubview(confirmBtn)
|
||||
infoView.addSubview(cancelBtn)
|
||||
|
||||
infoView.layoutChain
|
||||
.edgesHorzontal(30)
|
||||
.heightToWidth(296/315)
|
||||
.centerY()
|
||||
|
||||
titleLab.layoutChain
|
||||
.top(24)
|
||||
.centerX()
|
||||
|
||||
textFieldView.layoutChain
|
||||
.topToBottomOfView(titleLab, offset: 28)
|
||||
.edgesHorzontal(20)
|
||||
.height(62)
|
||||
|
||||
emailTF.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.edgesVertical()
|
||||
|
||||
confirmBtn.layoutChain
|
||||
.topToBottomOfView(textFieldView, offset: 46)
|
||||
.edgesHorzontal(20)
|
||||
.height(50)
|
||||
|
||||
cancelBtn.layoutChain
|
||||
.topToBottomOfView(confirmBtn)
|
||||
.edgesHorzontal(20)
|
||||
.height(50)
|
||||
|
||||
setupRx()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -46,6 +46,12 @@ class SignInVC: BaseViewController {
|
|||
}
|
||||
}
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
rootView.emailView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
|
||||
InputEmailPopupView.show { email in
|
||||
self?.requestSetEmail(email: email)
|
||||
}
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
|
|
@ -58,6 +64,13 @@ class SignInVC: BaseViewController {
|
|||
}, onError: { _ in }).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func requestSetEmail(email: String) {
|
||||
DLToast.showLoading()
|
||||
UserService.setEmail(email: email).subscribe(onNext: { response in
|
||||
self.rootView.emailLab.text = email
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
init(lastLocation: CLLocation?) {
|
||||
self.lastLocation = lastLocation
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
|
|
|||
|
|
@ -16,12 +16,19 @@ class SignInView: UIView {
|
|||
var disposeBag = DisposeBag()
|
||||
|
||||
func setupData(_ model: SignInModel) {
|
||||
|
||||
emailLab.text = model.email
|
||||
signInTextImg.image = UIImage(named: model.signInStatus == 1 ? "SignIn/today_text" : "SignIn/signIn_text")
|
||||
|
||||
let text = "已连续签到\(model.signCount)天"
|
||||
let fileName = model.signInStatus == 2 ? "sign_in_un_continuous_data" : "sign_in_continuous_data"
|
||||
if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
|
||||
self.signInLottieView.animation = LottieAnimation.filepath(path)
|
||||
self.signInLottieView.play()
|
||||
}
|
||||
|
||||
let dayText = model.signInStatus == 2 ? model.missCount.string : model.signCount.string
|
||||
let text = model.signInStatus == 2 ? "已有\(model.missCount)天未签到" : "已连续签到\(model.signCount)天"
|
||||
let attr = NSMutableAttributedString(string: text)
|
||||
let range = (text as NSString).range(of: model.signCount.string)
|
||||
let range = (text as NSString).range(of: dayText)
|
||||
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#FF8B39"), range: range)
|
||||
signInDaysLab.attributedText = attr
|
||||
}
|
||||
|
|
@ -168,7 +175,7 @@ class SignInView: UIView {
|
|||
}()
|
||||
|
||||
lazy var signInLottieView: LottieAnimationView = {
|
||||
let view = LottieAnimationView(name: "sign_in_continuous_data")
|
||||
let view = LottieAnimationView()
|
||||
view.loopMode = .loop
|
||||
return view
|
||||
}()
|
||||
|
|
@ -223,8 +230,8 @@ class SignInView: UIView {
|
|||
textView.backgroundColor = .clear
|
||||
textView.isEditable = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.isSelectable = false
|
||||
textView.linkTextAttributes = [:]
|
||||
textView.isSelectable = true
|
||||
textView.linkTextAttributes = [.foregroundColor: UIColor(hexStr: "#16B3FF")]
|
||||
textView.delegate = self
|
||||
|
||||
let text = "签到即同意 用户协议 和 隐私政策"
|
||||
|
|
@ -247,8 +254,6 @@ class SignInView: UIView {
|
|||
backgroundColor = .white
|
||||
setupUI()
|
||||
setupRx()
|
||||
|
||||
signInLottieView.play()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ class CreateScheduleView: UIView {
|
|||
mv.showsUserLocation = false
|
||||
mv.showsCompass = false
|
||||
mv.userTrackingMode = .none
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: kNaviHeight) }
|
||||
return mv
|
||||
}()
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ class ItineraryDetailView: UIView {
|
|||
mv.showsUserLocation = false
|
||||
mv.showsCompass = false
|
||||
mv.userTrackingMode = .none
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: kNaviHeight) }
|
||||
return mv
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class LocationPickerView: UIView {
|
|||
mv.zoomLevel = 18
|
||||
mv.showsUserLocation = false
|
||||
mv.showsCompass = false
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: 25) }
|
||||
return mv
|
||||
}()
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// DrivingService.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/6/30.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import Moya
|
||||
|
||||
struct DrivingService {
|
||||
static let disposeBag = DisposeBag()
|
||||
|
||||
/// 驾驶事件
|
||||
/// - Parameters:
|
||||
/// - user_id: 用户id
|
||||
/// - start_time: 开始时间戳
|
||||
/// - end_time: 结束时间戳
|
||||
static func drivingEvents(user_id: String, start_time: String, end_time: String) -> Observable<DrivingStatsResponse> {
|
||||
let api = DrivingAPI.drivingEvents(user_id: user_id, start_time: start_time, end_time: end_time).multiTarget
|
||||
return APIProvider.request(token: api)
|
||||
.map(DrivingStatsResponse.self)
|
||||
.asObservable()
|
||||
}
|
||||
|
||||
/// 轨迹回放
|
||||
/// - Parameters:
|
||||
/// - user_id: 用户id
|
||||
/// - date: 开始时间戳
|
||||
static func playback(user_id: String, date: String) -> Observable<ScheduleRecordListResponse> {
|
||||
let api = DrivingAPI.playback(user_id: user_id, date: date).multiTarget
|
||||
return APIProvider.request(token: api)
|
||||
.map(ScheduleRecordListResponse.self)
|
||||
.asObservable()
|
||||
}
|
||||
}
|
||||
|
|
@ -151,4 +151,12 @@ struct UserService {
|
|||
.map(ResponseModel.self)
|
||||
.asObservable()
|
||||
}
|
||||
|
||||
/// 紧急人邮箱
|
||||
static func setEmail(email: String) -> Observable<ResponseModel> {
|
||||
let api = UserAPI.setEmail(email: email).multiTarget
|
||||
return APIProvider.request(token: api)
|
||||
.map(ResponseModel.self)
|
||||
.asObservable()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue