Files
suixinkan_uikit/suixinkan/Features/WildPhotographerReport/Models/WildPhotographerReportModels.swift

960 lines
31 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
}
/// 线
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
}
}