对齐 Android 提现进度与时间线映射,优化钱包与积分兑换页刷新逻辑,并补充相关单元测试与真机测试说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
1668 lines
57 KiB
Swift
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
|
|
}
|
|
}
|