Files
suixinkan_uikit/suixinkan/Features/WildPhotographerReport/Models/WildPhotographerReportModels.swift
T
lujiuyinandCursor 05804ba7d6 完善钱包提现状态展示与举报摄影师列表交互。
对齐 Android 提现进度与时间线映射,优化钱包与积分兑换页刷新逻辑,并补充相关单元测试与真机测试说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 16:51:32 +08:00

1668 lines
57 KiB
Swift

//
// 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
}
/// 举报类型接口返回的列表包裹。
struct WildReportTypeListResponse: Decodable, Hashable {
let list: [WildReportType]
init(list: [WildReportType]) {
self.list = list
}
}
/// 举报首页须知与规则接口响应体。
struct WildReportCopyResponse: Decodable, Equatable, Hashable {
let notice: [String]
let ruleGroups: [WildReportRuleGroup]
enum CodingKeys: String, CodingKey {
case notice
case ruleGroups = "rule_groups"
}
init(notice: [String] = [], ruleGroups: [WildReportRuleGroup] = []) {
self.notice = notice
self.ruleGroups = ruleGroups
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
notice = try container.decodeIfPresent([String].self, forKey: .notice) ?? []
ruleGroups = try container.decodeIfPresent([WildReportRuleGroup].self, forKey: .ruleGroups) ?? []
}
}
/// 举报规则分组,用于首页规则入口与举报规则页展示。
struct WildReportRuleGroup: Decodable, Equatable, Hashable {
let title: String
let items: [String]
enum CodingKeys: String, CodingKey {
case title
case items
}
init(title: String = "", items: [String] = []) {
self.title = title
self.items = items
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
items = try container.decodeIfPresent([String].self, forKey: .items) ?? []
}
}
/// 举报附件实体,表示一条本地待上传的附件。
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 WildReportMiniProgramShareData: Decodable, Equatable, Hashable {
let userName: String
let path: String
let title: String
let description: String
let imagePath: String
let webpageURL: String
let withShareTicket: Bool
let miniProgramTypeValue: Int
let sceneValue: Int
let disableForward: Bool
enum CodingKeys: String, CodingKey {
case userName
case userNameSnake = "user_name"
case path
case title
case description
case imagePath
case imagePathSnake = "image_path"
case webpageURL = "webpageUrl"
case webpageURLSnake = "webpage_url"
case withShareTicket
case withShareTicketSnake = "with_share_ticket"
case miniProgramTypeValue = "miniprogramType"
case miniProgramTypeValueSnake = "mini_program_type"
case sceneValue = "scene"
case disableForward
case disableForwardSnake = "disable_forward"
}
init(
userName: String = "",
path: String = "",
title: String = "",
description: String = "",
imagePath: String = "",
webpageURL: String = "",
withShareTicket: Bool = false,
miniProgramTypeValue: Int = WeChatMiniProgramType.release.rawValue,
sceneValue: Int = WeChatShareScene.session.rawValue,
disableForward: Bool = false
) {
self.userName = userName
self.path = path
self.title = title
self.description = description
self.imagePath = imagePath
self.webpageURL = webpageURL
self.withShareTicket = withShareTicket
self.miniProgramTypeValue = miniProgramTypeValue
self.sceneValue = sceneValue
self.disableForward = disableForward
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
userName = try container.decodeString(primary: .userName, fallback: .userNameSnake)
path = try container.decodeIfPresent(String.self, forKey: .path) ?? ""
title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
description = try container.decodeIfPresent(String.self, forKey: .description) ?? ""
imagePath = try container.decodeString(primary: .imagePath, fallback: .imagePathSnake)
webpageURL = try container.decodeString(primary: .webpageURL, fallback: .webpageURLSnake)
withShareTicket = try container.decodeBool(primary: .withShareTicket, fallback: .withShareTicketSnake)
miniProgramTypeValue = try container.decodeInt(
primary: .miniProgramTypeValue,
fallback: .miniProgramTypeValueSnake,
defaultValue: WeChatMiniProgramType.release.rawValue
)
sceneValue = try container.decodeInt(
primary: .sceneValue,
fallback: nil,
defaultValue: WeChatShareScene.session.rawValue
)
disableForward = try container.decodeBool(primary: .disableForward, fallback: .disableForwardSnake)
}
var miniProgramType: WeChatMiniProgramType {
WeChatMiniProgramType(rawValue: miniProgramTypeValue) ?? .release
}
var scene: WeChatShareScene {
WeChatShareScene(rawValue: sceneValue) ?? .session
}
var payload: WeChatMiniProgramSharePayload {
WeChatMiniProgramSharePayload(
title: title,
description: description,
webpageURL: webpageURL,
userName: userName,
path: path.nonEmpty,
miniProgramType: miniProgramType,
withShareTicket: withShareTicket,
disableForward: disableForward
)
}
}
private extension KeyedDecodingContainer where Key == WildReportMiniProgramShareData.CodingKeys {
func decodeString(primary: Key, fallback: Key?) throws -> String {
if let value = try decodeIfPresent(String.self, forKey: primary) {
return value
}
if let fallback, let value = try decodeIfPresent(String.self, forKey: fallback) {
return value
}
return ""
}
func decodeBool(primary: Key, fallback: Key?) throws -> Bool {
if let value = try decodeIfPresent(Bool.self, forKey: primary) {
return value
}
if let fallback, let value = try decodeIfPresent(Bool.self, forKey: fallback) {
return value
}
return false
}
func decodeInt(primary: Key, fallback: Key?, defaultValue: Int) throws -> Int {
if let value = try decodeIfPresent(Int.self, forKey: primary) {
return value
}
if let fallback, let value = try decodeIfPresent(Int.self, forKey: fallback) {
return value
}
return defaultValue
}
}
/// 我的举报列表接口单条举报数据。
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
let evidenceKind: Int
let evidenceKindText: String
enum CodingKeys: String, CodingKey {
case fileType = "file_type"
case fileTypeText = "file_type_text"
case fileURL = "file_url"
case fileName = "file_name"
case evidenceKind = "evidence_kind"
case evidenceKindText = "evidence_kind_text"
}
init(
fileType: Int = 0,
fileTypeText: String = "",
fileURL: String = "",
fileName: String = "",
evidenceKind: Int = 0,
evidenceKindText: String = ""
) {
self.fileType = fileType
self.fileTypeText = fileTypeText
self.fileURL = fileURL
self.fileName = fileName
self.evidenceKind = evidenceKind
self.evidenceKindText = evidenceKindText
}
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) ?? ""
evidenceKind = try container.decodeIfPresent(Int.self, forKey: .evidenceKind) ?? 0
evidenceKindText = try container.decodeIfPresent(String.self, forKey: .evidenceKindText) ?? ""
}
var isVideo: Bool { fileType == 2 }
var isImage: Bool { fileType == 1 || fileType == 5 }
}
/// 举报详情接口返回的处理人信息。
struct WildReportDetailHandler: Decodable, Equatable, Hashable {
let id: Int
let name: String
let phone: String
let startedAt: String
let finishedAt: String
let remark: String
enum CodingKeys: String, CodingKey {
case id
case name
case phone
case startedAt = "started_at"
case finishedAt = "finished_at"
case remark
}
init(id: Int = 0, name: String = "", phone: String = "", startedAt: String = "", finishedAt: String = "", remark: String = "") {
self.id = id
self.name = name
self.phone = phone
self.startedAt = startedAt
self.finishedAt = finishedAt
self.remark = remark
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? ""
startedAt = try container.decodeIfPresent(String.self, forKey: .startedAt) ?? ""
finishedAt = try container.decodeIfPresent(String.self, forKey: .finishedAt) ?? ""
remark = try container.decodeIfPresent(String.self, forKey: .remark) ?? ""
}
}
/// 举报详情接口返回的补充内容或举报内容时间轴条目。
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 handledAt: String
let handler: WildReportDetailHandler?
let handlerName: String
let handlerPhone: String
let evidenceSummary: WildReportEvidenceSummary?
let evidences: [WildReportDetailEvidence]
let handleEvidences: [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 handledAt = "handled_at"
case handler
case handlerName = "handler_name"
case handlerPhone = "handler_phone"
case evidenceSummary = "evidence_summary"
case evidences
case handleEvidences = "handle_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 = "",
handledAt: String = "",
handler: WildReportDetailHandler? = nil,
handlerName: String = "",
handlerPhone: String = "",
evidenceSummary: WildReportEvidenceSummary? = nil,
evidences: [WildReportDetailEvidence] = [],
handleEvidences: [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.handledAt = handledAt
self.handler = handler
self.handlerName = handlerName
self.handlerPhone = handlerPhone
self.evidenceSummary = evidenceSummary
self.evidences = evidences
self.handleEvidences = handleEvidences
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) ?? ""
handledAt = try container.decodeIfPresent(String.self, forKey: .handledAt) ?? ""
handler = try container.decodeIfPresent(WildReportDetailHandler.self, forKey: .handler)
handlerName = try container.decodeIfPresent(String.self, forKey: .handlerName) ?? ""
handlerPhone = try container.decodeIfPresent(String.self, forKey: .handlerPhone) ?? ""
evidenceSummary = try container.decodeIfPresent(WildReportEvidenceSummary.self, forKey: .evidenceSummary)
evidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .evidences) ?? []
handleEvidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .handleEvidences) ?? []
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,
evidences: content.evidences
)
}
}
var detailHandlerInfo: WildReportHandlerInfo? {
let resolvedName = handler?.name.nonEmpty ?? handlerName.nonEmpty ?? ""
let resolvedPhone = handler?.phone.nonEmpty ?? handlerPhone.nonEmpty ?? ""
let processingAt = handler?.startedAt.nonEmpty ?? ""
let processedAt = handler?.finishedAt.nonEmpty ?? handledAt.nonEmpty ?? ""
let processOpinion = handler?.remark.nonEmpty ?? handleRemark.nonEmpty ?? ""
let media = handleEvidences.enumerated().compactMap { index, evidence -> WildReportCompletionMediaItem? in
guard let url = evidence.fileURL.nonEmpty else { return nil }
return WildReportCompletionMediaItem(
id: "\(index)-\(url)",
kind: evidence.isVideo ? .video : .image,
coverURL: url,
duration: evidence.isVideo ? evidence.fileTypeText.nonEmpty : nil
)
}
guard !resolvedName.isEmpty
|| !resolvedPhone.isEmpty
|| !processingAt.isEmpty
|| !processedAt.isEmpty
|| !processOpinion.isEmpty
|| !media.isEmpty
else {
return nil
}
return WildReportHandlerInfo(
handlerName: resolvedName,
handlerPhone: resolvedPhone,
processingAt: processingAt,
processedAt: processedAt,
processOpinion: processOpinion,
completionMedia: media
)
}
/// 转换为当前 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: detailHandlerInfo ?? fallback?.handlerInfo
)
}
}
/// 补充证据记录实体。
struct WildReportSupplementaryEvidence: Hashable {
let id: String
let submitTime: String
let text: String
let evidences: [WildReportDetailEvidence]
var imageCount: Int { evidences.filter(\.isImage).count }
var videoCount: Int { evidences.filter(\.isVideo).count }
var hasMedia: Bool { imageCount > 0 || videoCount > 0 }
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 WildReportSupplementDisplayFormatter {
/// 返回按当前列表展示顺序从 1 起算的补充次数文案。
static func sequenceTitle(forDisplayIndex index: Int) -> String {
"第\(max(0, index) + 1)次补充"
}
}
/// 举报位置解析工具。
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 clueId: String
let title: String
let statusTag: 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 clueId = "clue_id"
case title
case statusTag = "status_tag"
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,
clueId: String = "",
title: String = "",
statusTag: 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.clueId = clueId
self.title = title
self.statusTag = statusTag
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
clueId = try container.decodeIfPresent(String.self, forKey: .clueId) ?? ""
title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
statusTag = try container.decodeIfPresent(String.self, forKey: .statusTag) ?? ""
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
}
}