Implement report supplement evidence flow
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user