diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 395d8c6..87df0f4 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -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 = ""; }; 30B74B422FF2438800F6744D /* GroupMemberListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListView.swift; sourceTree = ""; }; 30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVM.swift; sourceTree = ""; }; + 30B74B462FF3608600F6744D /* ScheduleRecordModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleRecordModel.swift; sourceTree = ""; }; + 30B74B482FF3680200F6744D /* InputEmailPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEmailPopupView.swift; sourceTree = ""; }; + 30B74B4A2FF390EF00F6744D /* DrivingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrivingAPI.swift; sourceTree = ""; }; + 30B74B4C2FF391FF00F6744D /* DrivingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrivingService.swift; sourceTree = ""; }; + 30B74B4E2FF392D800F6744D /* DrivingStatsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrivingStatsModel.swift; sourceTree = ""; }; + 30B819F12FF3CE3C00FAB693 /* ItineraryTraceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryTraceView.swift; sourceTree = ""; }; + 30B819F32FF3CE4900FAB693 /* ItineraryTraceVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryTraceVC.swift; sourceTree = ""; }; 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = ""; }; 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = ""; }; 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = ""; }; @@ -636,6 +650,7 @@ 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */, 30D891F42FE22E0600E958FD /* OrderAPI.swift */, 30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */, + 30B74B4A2FF390EF00F6744D /* DrivingAPI.swift */, ); path = API; sourceTree = ""; @@ -1060,6 +1075,7 @@ 30BAB8522FCD337C00C33B5C /* GroupService.swift */, 30D891F62FE22E6E00E958FD /* OrderService.swift */, 30D74AB72FEA36A50050EB2C /* ItineraryService.swift */, + 30B74B4C2FF391FF00F6744D /* DrivingService.swift */, ); path = Service; sourceTree = ""; @@ -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 = ""; @@ -1402,6 +1422,7 @@ 30CCDE502FE2785D00F5214A /* SignInVC.swift */, 30CCDE522FE2786600F5214A /* SignInView.swift */, 30CCDE542FE2903100F5214A /* SignInModel.swift */, + 30B74B482FF3680200F6744D /* InputEmailPopupView.swift */, ); path = SignIn; sourceTree = ""; @@ -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 */, diff --git a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate index db5533c..ca1061a 100644 Binary files a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate and b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/QuickLocation/API/API.swift b/QuickLocation/API/API.swift index bbc7c4a..1d73cea 100644 --- a/QuickLocation/API/API.swift +++ b/QuickLocation/API/API.swift @@ -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?() diff --git a/QuickLocation/API/APIProvider.swift b/QuickLocation/API/APIProvider.swift index 09c10e5..0a8af44 100644 --- a/QuickLocation/API/APIProvider.swift +++ b/QuickLocation/API/APIProvider.swift @@ -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, 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(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 } diff --git a/QuickLocation/API/DrivingAPI.swift b/QuickLocation/API/DrivingAPI.swift new file mode 100644 index 0000000..e46c4c9 --- /dev/null +++ b/QuickLocation/API/DrivingAPI.swift @@ -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()) + } + } +} diff --git a/QuickLocation/API/UserAPI.swift b/QuickLocation/API/UserAPI.swift index 57ab726..7d12407 100644 --- a/QuickLocation/API/UserAPI.swift +++ b/QuickLocation/API/UserAPI.swift @@ -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()) } } } diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Contents.json new file mode 100644 index 0000000..ae2f067 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@2x.png new file mode 100644 index 0000000..9504b5c Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@3x.png new file mode 100644 index 0000000..9da84bb Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/mask_bg.imageset/Group_2170@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Contents.json new file mode 100644 index 0000000..4e95c88 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@2x.png new file mode 100644 index 0000000..f78834c Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@3x.png new file mode 100644 index 0000000..50e12e2 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/mask_tips.imageset/Group_2205@3x.png differ diff --git a/QuickLocation/Core/Extension/String+Extension.swift b/QuickLocation/Core/Extension/String+Extension.swift index 283808a..6d1cec6 100644 --- a/QuickLocation/Core/Extension/String+Extension.swift +++ b/QuickLocation/Core/Extension/String+Extension.swift @@ -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) + } +} diff --git a/QuickLocation/Manager/App/ApiManager.swift b/QuickLocation/Manager/App/ApiManager.swift index d3a021b..6bd0096 100644 --- a/QuickLocation/Manager/App/ApiManager.swift +++ b/QuickLocation/Manager/App/ApiManager.swift @@ -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 { diff --git a/QuickLocation/Section/Group/GroupMemberList/DrivingStatsModel.swift b/QuickLocation/Section/Group/GroupMemberList/DrivingStatsModel.swift new file mode 100644 index 0000000..9a92759 --- /dev/null +++ b/QuickLocation/Section/Group/GroupMemberList/DrivingStatsModel.swift @@ -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"] + } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift index c021c17..429c4d5 100644 --- a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift @@ -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 = { + RxTableViewSectionedReloadDataSource { _, tableView, indexPath, model in + let cell: ScheduleRecordCell = tableView.dequeueReusableCell(for: indexPath) + cell.configure(model) + return cell + } + }() private lazy var memberDataSource: RxCollectionViewSectionedReloadDataSource = { RxCollectionViewSectionedReloadDataSource { [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) diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift index 861baa3..45676e6 100644 --- a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift @@ -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 +typealias ScheduleRecordSection = SectionModel class GroupMemberListVM { @@ -51,16 +36,19 @@ 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] = [] - + private static let defaultDrivingEvents: [DrivingEventItem] = [ DrivingEventItem(title: "急加速", iconName: "GroupMemberList/1"), DrivingEventItem(title: "急转向", iconName: "GroupMemberList/3"), @@ -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() ) } } diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift index 6e5c72a..768113f 100644 --- a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift @@ -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 + } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceVC.swift b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceVC.swift new file mode 100644 index 0000000..4d6787a --- /dev/null +++ b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceVC.swift @@ -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. + } + */ + +} diff --git a/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceView.swift b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceView.swift new file mode 100644 index 0000000..07a9746 --- /dev/null +++ b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceView.swift @@ -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") + } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/ScheduleRecordModel.swift b/QuickLocation/Section/Group/GroupMemberList/ScheduleRecordModel.swift new file mode 100644 index 0000000..887b5d9 --- /dev/null +++ b/QuickLocation/Section/Group/GroupMemberList/ScheduleRecordModel.swift @@ -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 + } +} diff --git a/QuickLocation/Section/Home/GroupSchedule/GroupScheduleView.swift b/QuickLocation/Section/Home/GroupSchedule/GroupScheduleView.swift index 5653315..97addb1 100644 --- a/QuickLocation/Section/Home/GroupSchedule/GroupScheduleView.swift +++ b/QuickLocation/Section/Home/GroupSchedule/GroupScheduleView.swift @@ -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 }() diff --git a/QuickLocation/Section/Home/HomeView.swift b/QuickLocation/Section/Home/HomeView.swift index da51d82..36a46ad 100644 --- a/QuickLocation/Section/Home/HomeView.swift +++ b/QuickLocation/Section/Home/HomeView.swift @@ -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 diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index c48a15c..5670f23 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -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 diff --git a/QuickLocation/Section/Home/SOS/SOSView.swift b/QuickLocation/Section/Home/SOS/SOSView.swift index 7c42735..c655326 100644 --- a/QuickLocation/Section/Home/SOS/SOSView.swift +++ b/QuickLocation/Section/Home/SOS/SOSView.swift @@ -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() { diff --git a/QuickLocation/Section/Home/SignIn/InputEmailPopupView.swift b/QuickLocation/Section/Home/SignIn/InputEmailPopupView.swift new file mode 100644 index 0000000..bf7fb1f --- /dev/null +++ b/QuickLocation/Section/Home/SignIn/InputEmailPopupView.swift @@ -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") + } + +} diff --git a/QuickLocation/Section/Home/SignIn/SignInVC.swift b/QuickLocation/Section/Home/SignIn/SignInVC.swift index 78ffc4e..5868e46 100644 --- a/QuickLocation/Section/Home/SignIn/SignInVC.swift +++ b/QuickLocation/Section/Home/SignIn/SignInVC.swift @@ -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) diff --git a/QuickLocation/Section/Home/SignIn/SignInView.swift b/QuickLocation/Section/Home/SignIn/SignInView.swift index 748f8a4..72a2f6e 100644 --- a/QuickLocation/Section/Home/SignIn/SignInView.swift +++ b/QuickLocation/Section/Home/SignIn/SignInView.swift @@ -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) { diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift index e84e875..dfcc561 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift @@ -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 diff --git a/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailView.swift b/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailView.swift index 2fc750b..6c4b6cd 100644 --- a/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailView.swift +++ b/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailView.swift @@ -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 }() diff --git a/QuickLocation/Section/Schedule/LocationPicker/LocationPickerView.swift b/QuickLocation/Section/Schedule/LocationPicker/LocationPickerView.swift index 7dfc72d..4b6be7f 100644 --- a/QuickLocation/Section/Schedule/LocationPicker/LocationPickerView.swift +++ b/QuickLocation/Section/Schedule/LocationPicker/LocationPickerView.swift @@ -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 diff --git a/QuickLocation/Service/DrivingService.swift b/QuickLocation/Service/DrivingService.swift new file mode 100644 index 0000000..732efce --- /dev/null +++ b/QuickLocation/Service/DrivingService.swift @@ -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 { + 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 { + let api = DrivingAPI.playback(user_id: user_id, date: date).multiTarget + return APIProvider.request(token: api) + .map(ScheduleRecordListResponse.self) + .asObservable() + } +} diff --git a/QuickLocation/Service/UserService.swift b/QuickLocation/Service/UserService.swift index 8c8e2ca..cbf535d 100644 --- a/QuickLocation/Service/UserService.swift +++ b/QuickLocation/Service/UserService.swift @@ -151,4 +151,12 @@ struct UserService { .map(ResponseModel.self) .asObservable() } + + /// 紧急人邮箱 + static func setEmail(email: String) -> Observable { + let api = UserAPI.setEmail(email: email).multiTarget + return APIProvider.request(token: api) + .map(ResponseModel.self) + .asObservable() + } }