// // WildPhotographerReportModels.swift // suixinkan // import CoreLocation import Foundation import MapKit import UIKit /// 举报摄影师页面状态,用于演示加载、空态和异常态。 enum WildReportPageState: String, CaseIterable, Hashable { case normal case loading case empty case error var title: String { switch self { case .normal: return "正常" case .loading: return "加载" case .empty: return "空状态" case .error: return "异常" } } } /// 举报附件类型,区分现场图片、视频和支付截图。 enum WildReportAttachmentKind: Hashable { case image case video case payment } /// 举报类型接口返回的单项数据,由后端动态配置。 struct WildReportType: Decodable, Hashable { let value: Int let label: String static let defaultSelected = WildReportType(value: 2, label: "疑似打野") static let fallbackOptions = [ WildReportType(value: 1, label: "私下收款"), WildReportType(value: 2, label: "疑似打野"), WildReportType(value: 3, label: "未戴工牌"), WildReportType(value: 4, label: "其他") ] } /// 举报类型接口返回的列表包裹。 struct WildReportTypeListResponse: Decodable, Hashable { let list: [WildReportType] init(list: [WildReportType]) { self.list = list } } /// 举报附件实体,表示一条本地待上传或演示用的上传项。 struct WildReportAttachment: Hashable { let id: UUID let kind: WildReportAttachmentKind let title: String let localFileURL: URL? let fileName: String let fileType: Int let uploadedURL: String? init( id: UUID = UUID(), kind: WildReportAttachmentKind, title: String, localFileURL: URL? = nil, fileName: String? = nil, uploadedURL: String? = nil ) { self.id = id self.kind = kind self.title = title self.localFileURL = localFileURL self.fileName = fileName ?? "\(title).\(kind.defaultFileExtension)" self.fileType = kind.reportFileType self.uploadedURL = uploadedURL } } extension WildReportAttachment { /// 读取待上传附件的本地文件数据。 func uploadData() throws -> Data { guard let localFileURL else { throw OSSUploadError.emptyFile } return try Data(contentsOf: localFileURL) } } extension WildReportAttachmentKind { /// 提交举报接口的文件类型:1 图片,2 视频。 var reportFileType: Int { switch self { case .image, .payment: return 1 case .video: return 2 } } /// 本地附件默认扩展名。 var defaultFileExtension: String { switch self { case .image, .payment: return "jpg" case .video: return "mp4" } } } /// 举报处理状态。 enum WildReportStatus: String, Hashable, CaseIterable { case pending = "待处理" case processing = "处理中" case processed = "已处理" case rejected = "驳回" var displayColor: UIColor { switch self { case .pending: return AppColor.warning case .processing: return AppColor.primary case .processed: return AppColor.success case .rejected: return AppColor.danger } } var backgroundColor: UIColor { switch self { case .pending: return AppColor.warningBackground case .processing: return AppColor.infoBackground case .processed: return AppColor.successBackground case .rejected: return AppColor.dangerBackground } } } extension WildReportStatus { /// 后端举报处理状态码:0 待处理、1 处理中、2 已处理、3 驳回。 init(handleStatus: Int, fallbackText: String = "") { switch handleStatus { case 0: self = .pending case 1: self = .processing case 2: self = .processed case 3: self = .rejected default: switch fallbackText { case WildReportStatus.processing.rawValue: self = .processing case WildReportStatus.processed.rawValue: self = .processed case WildReportStatus.rejected.rawValue: self = .rejected default: self = .pending } } } } /// 我的举报列表筛选条件。 enum WildReportListFilter: String, CaseIterable, Hashable { case all = "全部" case pending = "待处理" case processing = "处理中" case processed = "已处理" } extension WildReportListFilter { /// 后端列表筛选状态码:全部不传,0 待处理、1 处理中、2 已处理。 var handleStatusValue: Int? { switch self { case .all: return nil case .pending: return 0 case .processing: return 1 case .processed: return 2 } } } /// 我的举报列表接口请求参数。 struct WildReportListRequest: Equatable { let scenicId: Int? let handleStatus: Int? let page: Int let pageSize: Int init( scenicId: Int?, handleStatus: Int?, page: Int = 1, pageSize: Int = 10 ) { self.scenicId = scenicId self.handleStatus = handleStatus self.page = page self.pageSize = pageSize } var queryItems: [URLQueryItem] { var items: [URLQueryItem] = [] if let scenicId, scenicId > 0 { items.append(URLQueryItem(name: "scenic_id", value: String(scenicId))) } if let handleStatus { items.append(URLQueryItem(name: "handle_status", value: String(handleStatus))) } items.append(URLQueryItem(name: "page", value: String(max(1, page)))) items.append(URLQueryItem(name: "page_size", value: String(min(max(1, pageSize), 50)))) return items } } /// 我的举报列表接口响应体。 struct WildReportListResponse: Decodable, Equatable { let list: [WildReportListItem] let total: Int let page: Int let pageSize: Int enum CodingKeys: String, CodingKey { case list case total case page case pageSize = "page_size" } init(list: [WildReportListItem], total: Int, page: Int, pageSize: Int) { self.list = list self.total = total self.page = page self.pageSize = pageSize } } /// 我的举报列表接口单条举报数据。 struct WildReportListItem: Decodable, Equatable, Hashable { let id: Int let complaintNo: String let title: String let reportType: Int let reportTypeText: String let desc: String let scenicId: Int let scenicName: String let locationName: String let latitude: String let longitude: String let handleStatus: Int let handleStatusText: String let createdAt: String enum CodingKeys: String, CodingKey { case id case complaintNo = "complaint_no" case title case reportType = "report_type" case reportTypeText = "report_type_text" case desc case scenicId = "scenic_id" case scenicName = "scenic_name" case locationName = "location_name" case latitude case longitude case handleStatus = "handle_status" case handleStatusText = "handle_status_text" case createdAt = "created_at" } init( id: Int, complaintNo: String, title: String, reportType: Int, reportTypeText: String, desc: String, scenicId: Int, scenicName: String, locationName: String, latitude: String, longitude: String, handleStatus: Int, handleStatusText: String, createdAt: String ) { self.id = id self.complaintNo = complaintNo self.title = title self.reportType = reportType self.reportTypeText = reportTypeText self.desc = desc self.scenicId = scenicId self.scenicName = scenicName self.locationName = locationName self.latitude = latitude self.longitude = longitude self.handleStatus = handleStatus self.handleStatusText = handleStatusText self.createdAt = createdAt } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0 complaintNo = try container.decodeIfPresent(String.self, forKey: .complaintNo) ?? "" title = try container.decodeIfPresent(String.self, forKey: .title) ?? "" reportType = try container.decodeIfPresent(Int.self, forKey: .reportType) ?? 0 reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? "" desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? "" scenicId = try container.decodeIfPresent(Int.self, forKey: .scenicId) ?? 0 scenicName = try container.decodeIfPresent(String.self, forKey: .scenicName) ?? "" locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? "" latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? "" longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? "" handleStatus = try container.decodeIfPresent(Int.self, forKey: .handleStatus) ?? 0 handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText) ?? "" createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? "" } /// 转换为当前 UIKit 列表和详情页复用的举报记录实体。 func toRecord() -> WildReportRecord { let scenic = scenicName.trimmingCharacters(in: .whitespacesAndNewlines) let location = locationName.trimmingCharacters(in: .whitespacesAndNewlines) let displayLocation: String if !scenic.isEmpty, !location.isEmpty, scenic != location { displayLocation = "\(scenic) · \(location)" } else if !location.isEmpty { displayLocation = location } else { displayLocation = scenic } return WildReportRecord( serverID: id > 0 ? id : nil, id: complaintNo, title: title.isEmpty ? "\(reportTypeText)举报" : title, reportType: WildReportType(value: reportType, label: reportTypeText.isEmpty ? "其他" : reportTypeText), status: WildReportStatus(handleStatus: handleStatus, fallbackText: handleStatusText), location: displayLocation, submitTime: createdAt, description: desc, wechatID: "", phoneNumber: "", imageCount: 0, videoCount: 0, handlerInfo: nil ) } } /// 举报详情接口返回的证据统计。 struct WildReportEvidenceSummary: Decodable, Equatable, Hashable { let imageCount: Int let videoCount: Int enum CodingKeys: String, CodingKey { case imageCount = "image_count" case videoCount = "video_count" } init(imageCount: Int = 0, videoCount: Int = 0) { self.imageCount = imageCount self.videoCount = videoCount } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) imageCount = try container.decodeIfPresent(Int.self, forKey: .imageCount) ?? 0 videoCount = try container.decodeIfPresent(Int.self, forKey: .videoCount) ?? 0 } } /// 举报详情接口返回的单个附件。 struct WildReportDetailEvidence: Decodable, Equatable, Hashable { let fileType: Int let fileTypeText: String let fileURL: String let fileName: String enum CodingKeys: String, CodingKey { case fileType = "file_type" case fileTypeText = "file_type_text" case fileURL = "file_url" case fileName = "file_name" } init(fileType: Int = 0, fileTypeText: String = "", fileURL: String = "", fileName: String = "") { self.fileType = fileType self.fileTypeText = fileTypeText self.fileURL = fileURL self.fileName = fileName } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) fileType = try container.decodeIfPresent(Int.self, forKey: .fileType) ?? 0 fileTypeText = try container.decodeIfPresent(String.self, forKey: .fileTypeText) ?? "" fileURL = try container.decodeIfPresent(String.self, forKey: .fileURL) ?? "" fileName = try container.decodeIfPresent(String.self, forKey: .fileName) ?? "" } var isVideo: Bool { fileType == 2 } var isImage: Bool { fileType == 1 || fileType == 5 } } /// 举报详情接口返回的补充内容或举报内容时间轴条目。 struct WildReportDetailContent: Decodable, Equatable, Hashable { let type: String let id: Int let desc: String let time: String let timeLabel: String let evidences: [WildReportDetailEvidence] enum CodingKeys: String, CodingKey { case type case id case desc case time case timeLabel = "time_label" case evidences } init( type: String = "", id: Int = 0, desc: String = "", time: String = "", timeLabel: String = "", evidences: [WildReportDetailEvidence] = [] ) { self.type = type self.id = id self.desc = desc self.time = time self.timeLabel = timeLabel self.evidences = evidences } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) type = try container.decodeIfPresent(String.self, forKey: .type) ?? "" id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0 desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? "" time = try container.decodeIfPresent(String.self, forKey: .time) ?? "" timeLabel = try container.decodeIfPresent(String.self, forKey: .timeLabel) ?? "" evidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .evidences) ?? [] } var imageCount: Int { evidences.filter(\.isImage).count } var videoCount: Int { evidences.filter(\.isVideo).count } } /// 举报详情接口返回的处理日志时间轴。 struct WildReportDetailTimelineItem: Decodable, Equatable, Hashable { let time: String let content: String enum CodingKeys: String, CodingKey { case time case content } init(time: String = "", content: String = "") { self.time = time self.content = content } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) time = try container.decodeIfPresent(String.self, forKey: .time) ?? "" content = try container.decodeIfPresent(String.self, forKey: .content) ?? "" } } /// 举报详情接口业务数据。 struct WildReportDetailData: Decodable, Equatable, Hashable { let id: Int let complaintNo: String let title: String let reportType: Int let reportTypeText: String let desc: String let supplements: [WildReportDetailContent] let reportContents: [WildReportDetailContent] let scenicId: Int let scenicName: String let storeUserId: Int let contactPhone: String let locationName: String let locationAddress: String let latitude: String let longitude: String let handleStatus: Int let handleStatusText: String let handleRemark: String let evidenceSummary: WildReportEvidenceSummary? let evidences: [WildReportDetailEvidence] let paymentScreenshots: [WildReportDetailEvidence] let timeline: [WildReportDetailTimelineItem] let createdAt: String enum CodingKeys: String, CodingKey { case id case complaintNo = "complaint_no" case title case reportType = "report_type" case reportTypeText = "report_type_text" case desc case supplements case reportContents = "report_contents" case scenicId = "scenic_id" case scenicName = "scenic_name" case storeUserId = "store_user_id" case contactPhone = "contact_phone" case locationName = "location_name" case locationAddress = "location_address" case latitude case longitude case handleStatus = "handle_status" case handleStatusText = "handle_status_text" case handleRemark = "handle_remark" case evidenceSummary = "evidence_summary" case evidences case paymentScreenshots = "payment_screenshots" case timeline case createdAt = "created_at" } init( id: Int = 0, complaintNo: String = "", title: String = "", reportType: Int = 0, reportTypeText: String = "", desc: String = "", supplements: [WildReportDetailContent] = [], reportContents: [WildReportDetailContent] = [], scenicId: Int = 0, scenicName: String = "", storeUserId: Int = 0, contactPhone: String = "", locationName: String = "", locationAddress: String = "", latitude: String = "", longitude: String = "", handleStatus: Int = 0, handleStatusText: String = "", handleRemark: String = "", evidenceSummary: WildReportEvidenceSummary? = nil, evidences: [WildReportDetailEvidence] = [], paymentScreenshots: [WildReportDetailEvidence] = [], timeline: [WildReportDetailTimelineItem] = [], createdAt: String = "" ) { self.id = id self.complaintNo = complaintNo self.title = title self.reportType = reportType self.reportTypeText = reportTypeText self.desc = desc self.supplements = supplements self.reportContents = reportContents self.scenicId = scenicId self.scenicName = scenicName self.storeUserId = storeUserId self.contactPhone = contactPhone self.locationName = locationName self.locationAddress = locationAddress self.latitude = latitude self.longitude = longitude self.handleStatus = handleStatus self.handleStatusText = handleStatusText self.handleRemark = handleRemark self.evidenceSummary = evidenceSummary self.evidences = evidences self.paymentScreenshots = paymentScreenshots self.timeline = timeline self.createdAt = createdAt } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0 complaintNo = try container.decodeIfPresent(String.self, forKey: .complaintNo) ?? "" title = try container.decodeIfPresent(String.self, forKey: .title) ?? "" reportType = try container.decodeIfPresent(Int.self, forKey: .reportType) ?? 0 reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? "" desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? "" supplements = try container.decodeIfPresent([WildReportDetailContent].self, forKey: .supplements) ?? [] reportContents = try container.decodeIfPresent([WildReportDetailContent].self, forKey: .reportContents) ?? [] scenicId = try container.decodeIfPresent(Int.self, forKey: .scenicId) ?? 0 scenicName = try container.decodeIfPresent(String.self, forKey: .scenicName) ?? "" storeUserId = try container.decodeIfPresent(Int.self, forKey: .storeUserId) ?? 0 contactPhone = try container.decodeIfPresent(String.self, forKey: .contactPhone) ?? "" locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? "" locationAddress = try container.decodeIfPresent(String.self, forKey: .locationAddress) ?? "" latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? "" longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? "" handleStatus = try container.decodeIfPresent(Int.self, forKey: .handleStatus) ?? 0 handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText) ?? "" handleRemark = try container.decodeIfPresent(String.self, forKey: .handleRemark) ?? "" evidenceSummary = try container.decodeIfPresent(WildReportEvidenceSummary.self, forKey: .evidenceSummary) evidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .evidences) ?? [] paymentScreenshots = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .paymentScreenshots) ?? [] timeline = try container.decodeIfPresent([WildReportDetailTimelineItem].self, forKey: .timeline) ?? [] createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? "" } var displayLocation: String { let scenic = scenicName.trimmingCharacters(in: .whitespacesAndNewlines) let detail = (locationAddress.nonEmpty ?? locationName).trimmingCharacters(in: .whitespacesAndNewlines) if !scenic.isEmpty, !detail.isEmpty, scenic != detail { return "\(scenic) · \(detail)" } return detail.nonEmpty ?? scenic } var displayStatusText: String { handleStatusText.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? WildReportStatus(handleStatus: handleStatus).rawValue } var imageCount: Int { evidenceSummary?.imageCount ?? evidences.filter(\.isImage).count } var videoCount: Int { evidenceSummary?.videoCount ?? evidences.filter(\.isVideo).count } var supplementRecords: [WildReportSupplementaryEvidence] { let contents = supplements.isEmpty ? reportContents.filter { $0.type == "supplement" } : supplements return contents.map { content in WildReportSupplementaryEvidence( id: content.id > 0 ? String(content.id) : content.time, submitTime: content.time, text: content.desc, imageCount: content.imageCount, videoCount: content.videoCount ) } } /// 转换为当前 UIKit 列表和详情页复用的举报记录实体。 func toRecord(fallback: WildReportRecord? = nil) -> WildReportRecord { WildReportRecord( serverID: id > 0 ? id : fallback?.serverID, id: complaintNo.nonEmpty ?? fallback?.id ?? "", title: title.nonEmpty ?? fallback?.title ?? "摄影师举报", reportType: WildReportType( value: reportType > 0 ? reportType : fallback?.reportType.value ?? 4, label: reportTypeText.nonEmpty ?? fallback?.reportType.label ?? "其他" ), status: WildReportStatus(handleStatus: handleStatus, fallbackText: handleStatusText), location: displayLocation.nonEmpty ?? fallback?.location ?? "", submitTime: createdAt.nonEmpty ?? fallback?.submitTime ?? "", description: desc.nonEmpty ?? fallback?.description ?? "", wechatID: fallback?.wechatID ?? "", phoneNumber: contactPhone.nonEmpty ?? fallback?.phoneNumber ?? "", imageCount: imageCount, videoCount: videoCount, handlerInfo: fallback?.handlerInfo ) } } /// 补充证据记录实体。 struct WildReportSupplementaryEvidence: Hashable { let id: String let submitTime: String let text: String let imageCount: Int let videoCount: Int var summaryText: String { var parts: [String] = [] if !text.isEmpty { parts.append(text) } if imageCount > 0 { parts.append("\(imageCount)张图片") } if videoCount > 0 { parts.append("\(videoCount)段视频") } return parts.isEmpty ? "补充证据" : parts.joined(separator: " · ") } } /// 举报位置解析工具。 enum WildReportLocationParser { /// 将「景区 · 详细地址」拆成两段展示文案。 static func split(_ location: String) -> (scenic: String, detail: String) { let parts = location .components(separatedBy: " · ") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } if parts.count >= 2 { return (parts[0], parts[1]) } return (location, location) } } /// 附近风险点实体。 struct WildReportRiskPoint: Hashable { let id: Int let title: String let location: String let distance: String let riskLevel: String let evidenceCount: Int let latestTime: String let description: String } /// 举报记录实体。 struct WildReportRecord: Hashable { var serverID: Int? = nil let id: String let title: String let reportType: WildReportType let status: WildReportStatus let location: String let submitTime: String let description: String let wechatID: String let phoneNumber: String let imageCount: Int let videoCount: Int let handlerInfo: WildReportHandlerInfo? var contactDisplayText: String { switch (wechatID.isEmpty, phoneNumber.isEmpty) { case (true, true): return "未填写" case (false, false): return "\(wechatID) / \(phoneNumber)" case (false, true): return wechatID case (true, false): return phoneNumber } } } /// 处理完成后的媒体项。 struct WildReportCompletionMediaItem: Hashable { enum Kind: Hashable { case image case video } let id: String let kind: Kind let coverURL: String let duration: String? } /// 举报处理人信息实体。 struct WildReportHandlerInfo: Hashable { let handlerName: String let handlerPhone: String let processingAt: String let processedAt: String let processOpinion: String let completionMedia: [WildReportCompletionMediaItem] var maskedHandlerPhone: String { guard handlerPhone.count == 11, handlerPhone.allSatisfy(\.isNumber) else { return handlerPhone } return "\(handlerPhone.prefix(3))****\(handlerPhone.suffix(4))" } var completionMediaSummary: String { let imageCount = completionMedia.filter { $0.kind == .image }.count let videoCount = completionMedia.filter { $0.kind == .video }.count switch (imageCount, videoCount) { case (0, 0): return "0项" case (_, 0): return "\(imageCount)张" case (0, _): return "\(videoCount)段" default: return "\(imageCount)张 \(videoCount)段" } } } /// 举报进度节点状态。 enum WildReportProgressStepState: Hashable { case completed case current case pending } /// 举报进度时间轴节点。 struct WildReportProgressStep: Hashable { let id: String let title: String let timeText: String let state: WildReportProgressStepState } /// 举报人材料预览项。 enum WildReportReporterMediaItem: Hashable { case image(index: Int, url: String?, fileName: String?, fileTypeText: String?) case video(index: Int, url: String?, fileName: String?, fileTypeText: String?) var id: String { switch self { case .image(let index, let url, _, _): return "reporter-image-\(index)-\(url ?? "")" case .video(let index, let url, _, _): return "reporter-video-\(index)-\(url ?? "")" } } var fileURL: String? { switch self { case .image(_, let url, _, _), .video(_, let url, _, _): return url } } var fileName: String? { switch self { case .image(_, _, let fileName, _), .video(_, _, let fileName, _): return fileName } } var fileTypeText: String? { switch self { case .image(_, _, _, let fileTypeText), .video(_, _, _, let fileTypeText): return fileTypeText } } } /// 举报提交结果,用于成功页展示。 struct WildReportSubmitResult: Hashable { let record: WildReportRecord let imageCount: Int let videoCount: Int } /// 风险地图标记类型。 enum WildReportMapMarkerKind: Hashable { case selfLocation case wildPhotographer case processingClue case peerPhotographer var color: UIColor { switch self { case .selfLocation: return AppColor.primary case .wildPhotographer: return AppColor.danger case .processingClue: return AppColor.warning case .peerPhotographer: return AppColor.success } } var title: String { switch self { case .selfLocation: return "我的位置" case .wildPhotographer: return "疑似打野" case .processingClue: return "处理中线索" case .peerPhotographer: return "内部摄影师" } } } /// 雷达地图标记颜色类型。 enum WildReportRadarMarkerType: String, Hashable { case blue case green case red case yellow } /// 风险地图接口标记类型,兼容后端新增枚举。 enum WildReportRiskMarkerType: Hashable { case blue case green case red case yellow case unknown(String) init(rawValue: String) { switch rawValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { case "blue", "self", "self_location": self = .blue case "green": self = .green case "red": self = .red case "yellow": self = .yellow default: self = .unknown(rawValue) } } var rawValue: String { switch self { case .blue: return "blue" case .green: return "green" case .red: return "red" case .yellow: return "yellow" case .unknown(let value): return value } } var markerKind: WildReportMapMarkerKind { switch self { case .blue: return .selfLocation case .green: return .peerPhotographer case .yellow: return .processingClue case .red, .unknown: return .wildPhotographer } } var radarType: WildReportRadarMarkerType { switch self { case .blue: return .blue case .green: return .green case .yellow: return .yellow case .red, .unknown: return .red } } } /// 风险地图后端状态字段,兼容数字和字符串两种形态。 struct WildReportDecodedHandleStatus: Decodable, Equatable, Hashable { let intValue: Int? let textValue: String? init(intValue: Int? = nil, textValue: String? = nil) { self.intValue = intValue self.textValue = textValue } init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let value = try? container.decode(Int.self) { intValue = value textValue = nil } else if let value = try? container.decode(String.self) { intValue = Int(value) textValue = value } else { intValue = nil textValue = nil } } var displayText: String { if let text = textValue?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { switch text { case "0": return WildReportStatus.pending.rawValue case "1": return WildReportStatus.processing.rawValue case "2": return WildReportStatus.processed.rawValue case "3": return WildReportStatus.rejected.rawValue default: return text } } return WildReportStatus(handleStatus: intValue ?? 0).rawValue } } /// 附近风险地图接口返回体。 struct WildReportRiskMapResponse: Decodable, Equatable { let scenicId: Int let scenicName: String let legend: [WildReportRiskLegendItem] let greenMarkerTip: String let markers: [WildReportRiskMapMarkerDTO] enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case scenicName = "scenic_name" case legend case greenMarkerTip = "green_marker_tip" case markers } init( scenicId: Int, scenicName: String, legend: [WildReportRiskLegendItem] = [], greenMarkerTip: String = "", markers: [WildReportRiskMapMarkerDTO] = [] ) { self.scenicId = scenicId self.scenicName = scenicName self.legend = legend self.greenMarkerTip = greenMarkerTip self.markers = markers } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) scenicId = try container.decodeIfPresent(Int.self, forKey: .scenicId) ?? 0 scenicName = try container.decodeIfPresent(String.self, forKey: .scenicName) ?? "" legend = try container.decodeIfPresent([WildReportRiskLegendItem].self, forKey: .legend) ?? [] greenMarkerTip = try container.decodeIfPresent(String.self, forKey: .greenMarkerTip) ?? "" markers = try container.decodeIfPresent([WildReportRiskMapMarkerDTO].self, forKey: .markers) ?? [] } } /// 风险地图图例项,兼容后端不同字段命名。 struct WildReportRiskLegendItem: Decodable, Equatable, Hashable { let markerType: String? let title: String? let color: String? enum CodingKeys: String, CodingKey { case markerType = "marker_type" case type case title case text case name case color } init(markerType: String? = nil, title: String? = nil, color: String? = nil) { self.markerType = markerType self.title = title self.color = color } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) markerType = try container.decodeIfPresent(String.self, forKey: .markerType) ?? (try container.decodeIfPresent(String.self, forKey: .type)) title = try container.decodeIfPresent(String.self, forKey: .title) ?? (try container.decodeIfPresent(String.self, forKey: .text)) ?? (try container.decodeIfPresent(String.self, forKey: .name)) color = try container.decodeIfPresent(String.self, forKey: .color) } } /// 风险地图列表接口的标记摘要。 struct WildReportRiskMapMarkerDTO: Decodable, Equatable, Hashable { let markerType: String let id: Int let latitude: String let longitude: String let locationName: String let title: String let reportTypeText: String let handleStatus: WildReportDecodedHandleStatus? let handleStatusText: String? let distanceM: Double let createdAt: String let tag: String? let name: String? let avatar: String? let storeName: String? let online: Bool? let greenMarkerTip: String? let activityText: String? enum CodingKeys: String, CodingKey { case markerType = "marker_type" case id case latitude case longitude case locationName = "location_name" case title case reportTypeText = "report_type_text" case handleStatus = "handle_status" case handleStatusText = "handle_status_text" case distanceM = "distance_m" case createdAt = "created_at" case tag case name case avatar case storeName = "store_name" case online case greenMarkerTip = "green_marker_tip" case activityText = "activity_text" } init( markerType: String, id: Int, latitude: String, longitude: String, locationName: String, title: String, reportTypeText: String = "", handleStatus: WildReportDecodedHandleStatus? = nil, handleStatusText: String? = nil, distanceM: Double = 0, createdAt: String = "", tag: String? = nil, name: String? = nil, avatar: String? = nil, storeName: String? = nil, online: Bool? = nil, greenMarkerTip: String? = nil, activityText: String? = nil ) { self.markerType = markerType self.id = id self.latitude = latitude self.longitude = longitude self.locationName = locationName self.title = title self.reportTypeText = reportTypeText self.handleStatus = handleStatus self.handleStatusText = handleStatusText self.distanceM = distanceM self.createdAt = createdAt self.tag = tag self.name = name self.avatar = avatar self.storeName = storeName self.online = online self.greenMarkerTip = greenMarkerTip self.activityText = activityText } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) markerType = try container.decodeIfPresent(String.self, forKey: .markerType) ?? "" id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0 latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? "" longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? "" locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? "" title = try container.decodeIfPresent(String.self, forKey: .title) ?? "" reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? "" handleStatus = try container.decodeIfPresent(WildReportDecodedHandleStatus.self, forKey: .handleStatus) handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText) distanceM = try container.decodeIfPresent(Double.self, forKey: .distanceM) ?? 0 createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? "" tag = try container.decodeIfPresent(String.self, forKey: .tag) name = try container.decodeIfPresent(String.self, forKey: .name) avatar = try container.decodeIfPresent(String.self, forKey: .avatar) storeName = try container.decodeIfPresent(String.self, forKey: .storeName) online = try container.decodeIfPresent(Bool.self, forKey: .online) greenMarkerTip = try container.decodeIfPresent(String.self, forKey: .greenMarkerTip) activityText = try container.decodeIfPresent(String.self, forKey: .activityText) } } /// 风险地图标记详情接口返回体。 struct WildReportMarkerDetailResponse: Decodable, Equatable, Hashable { let markerType: String let id: Int let title: String let tag: String let name: String let avatar: String let storeName: String let online: Bool let locationName: String let latitude: String let longitude: String let distanceM: Double let greenMarkerTip: String let activityText: String let reportTypeText: String let handleStatus: WildReportDecodedHandleStatus? let handleStatusText: String let desc: String let reporterName: String let reporterPhone: String let createdAt: String let reportContents: [WildReportDetailContent] let evidences: [WildReportDetailEvidence] enum CodingKeys: String, CodingKey { case markerType = "marker_type" case id case title case tag case name case avatar case storeName = "store_name" case online case locationName = "location_name" case latitude case longitude case distanceM = "distance_m" case greenMarkerTip = "green_marker_tip" case activityText = "activity_text" case reportTypeText = "report_type_text" case handleStatus = "handle_status" case handleStatusText = "handle_status_text" case desc case reporterName = "reporter_name" case reporterPhone = "reporter_phone" case contactPhone = "contact_phone" case createdAt = "created_at" case reportContents = "report_contents" case evidences } init( markerType: String, id: Int, title: String = "", tag: String = "", name: String = "", avatar: String = "", storeName: String = "", online: Bool = false, locationName: String = "", latitude: String = "", longitude: String = "", distanceM: Double = 0, greenMarkerTip: String = "", activityText: String = "", reportTypeText: String = "", handleStatus: WildReportDecodedHandleStatus? = nil, handleStatusText: String = "", desc: String = "", reporterName: String = "", reporterPhone: String = "", createdAt: String = "", reportContents: [WildReportDetailContent] = [], evidences: [WildReportDetailEvidence] = [] ) { self.markerType = markerType self.id = id self.title = title self.tag = tag self.name = name self.avatar = avatar self.storeName = storeName self.online = online self.locationName = locationName self.latitude = latitude self.longitude = longitude self.distanceM = distanceM self.greenMarkerTip = greenMarkerTip self.activityText = activityText self.reportTypeText = reportTypeText self.handleStatus = handleStatus self.handleStatusText = handleStatusText self.desc = desc self.reporterName = reporterName self.reporterPhone = reporterPhone self.createdAt = createdAt self.reportContents = reportContents self.evidences = evidences } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) markerType = try container.decodeIfPresent(String.self, forKey: .markerType) ?? "" id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0 title = try container.decodeIfPresent(String.self, forKey: .title) ?? "" tag = try container.decodeIfPresent(String.self, forKey: .tag) ?? "" name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" avatar = try container.decodeIfPresent(String.self, forKey: .avatar) ?? "" storeName = try container.decodeIfPresent(String.self, forKey: .storeName) ?? "" online = try container.decodeIfPresent(Bool.self, forKey: .online) ?? false locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? "" latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? "" longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? "" distanceM = try container.decodeIfPresent(Double.self, forKey: .distanceM) ?? 0 greenMarkerTip = try container.decodeIfPresent(String.self, forKey: .greenMarkerTip) ?? "" activityText = try container.decodeIfPresent(String.self, forKey: .activityText) ?? "" reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? "" handleStatus = try container.decodeIfPresent(WildReportDecodedHandleStatus.self, forKey: .handleStatus) handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText) ?? "" desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? "" reporterName = try container.decodeIfPresent(String.self, forKey: .reporterName) ?? "" reporterPhone = try container.decodeIfPresent(String.self, forKey: .reporterPhone) ?? (try container.decodeIfPresent(String.self, forKey: .contactPhone)) ?? "" createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? "" reportContents = try container.decodeIfPresent([WildReportDetailContent].self, forKey: .reportContents) ?? [] evidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .evidences) ?? [] } } /// 雷达线索举报内容条目。 struct WildReportRadarReportContent: Hashable { let time: String let content: String } /// 雷达线索处理记录。 struct WildReportRadarProcessRecord: Hashable { let handlerName: String let phone: String let maskedPhone: String let statusText: String let processResult: String let processedAt: String? let processingAt: String? } /// 雷达线索处理时间轴节点。 struct WildReportRadarProcessingTimelineItem: Hashable { let title: String let time: String let status: String } /// 雷达地图活动线索详情。 struct WildReportRadarActiveClue: Hashable { let id: String let type: WildReportRadarMarkerType let displayTitle: String let statusText: String let actionText: String let nearestAbnormalText: String? let distance: String? let direction: String? let updatedAt: String? let name: String? let storeName: String? let avatar: String? let summary: String? let detail: String? let distanceText: String? let reporterName: String? let reporterPhone: String? let maskedReporterPhone: String? let reportContents: [WildReportRadarReportContent] let images: [String] let processingStarted: Bool let completed: Bool let record: WildReportRadarProcessRecord? let processingTimeline: [WildReportRadarProcessingTimelineItem] let completionPhoto: String? } /// 风险地图标记详情。 enum WildReportMapMarkerDetail: Hashable { case activeClue(WildReportRadarActiveClue) } /// 风险地图标记实体。 struct WildReportMapMarker: Hashable { let id: String let title: String let coordinate: CLLocationCoordinate2D let kind: WildReportMapMarkerKind let detail: WildReportMapMarkerDetail static func == (lhs: WildReportMapMarker, rhs: WildReportMapMarker) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } } extension CLLocationCoordinate2D: @retroactive Equatable { public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude } } private extension String { var nonEmpty: String? { let text = trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text } }