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