Implement report supplement evidence flow

This commit is contained in:
2026-07-08 17:23:28 +08:00
parent 290a01e699
commit fd5dc2bbe6
9 changed files with 1906 additions and 421 deletions

View File

@ -115,12 +115,14 @@ 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
}
}
@ -129,12 +131,13 @@ enum WildReportStatus: String, Hashable, CaseIterable {
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
/// 0 1 2 3
init(handleStatus: Int, fallbackText: String = "") {
switch handleStatus {
case 0:
@ -143,12 +146,16 @@ extension WildReportStatus {
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
}
@ -329,6 +336,7 @@ struct WildReportListItem: Decodable, Equatable, Hashable {
}
return WildReportRecord(
serverID: id > 0 ? id : nil,
id: complaintNo,
title: title.isEmpty ? "\(reportTypeText)举报" : title,
reportType: WildReportType(value: reportType, label: reportTypeText.isEmpty ? "其他" : reportTypeText),
@ -345,6 +353,325 @@ struct WildReportListItem: Decodable, Equatable, Hashable {
}
}
///
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
@ -391,6 +718,7 @@ struct WildReportRiskPoint: Hashable {
///
struct WildReportRecord: Hashable {
var serverID: Int? = nil
let id: String
let title: String
let reportType: WildReportType
@ -470,13 +798,34 @@ struct WildReportProgressStep: Hashable {
///
enum WildReportReporterMediaItem: Hashable {
case image(index: Int)
case video(index: Int)
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): return "reporter-image-\(index)"
case .video(let index): return "reporter-video-\(index)"
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
}
}
}
@ -601,3 +950,10 @@ extension CLLocationCoordinate2D: @retroactive Equatable {
lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
}
private extension String {
var nonEmpty: String? {
let text = trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
}