// // 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 = "已处理" var displayColor: UIColor { switch self { case .pending: return AppColor.warning case .processing: return AppColor.primary case .processed: return AppColor.success } } var backgroundColor: UIColor { switch self { case .pending: return AppColor.warningBackground case .processing: return AppColor.infoBackground case .processed: return AppColor.successBackground } } } extension WildReportStatus { /// 后端举报处理状态码:0 待处理、1 处理中、2 已处理。 init(handleStatus: Int, fallbackText: String = "") { switch handleStatus { case 0: self = .pending case 1: self = .processing case 2: self = .processed default: switch fallbackText { case WildReportStatus.processing.rawValue: self = .processing case WildReportStatus.processed.rawValue: self = .processed 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( 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 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 { 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) case video(index: Int) var id: String { switch self { case .image(let index): return "reporter-image-\(index)" case .video(let index): return "reporter-video-\(index)" } } } /// 举报提交结果,用于成功页展示。 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 } }