feat: update app workflows and permissions
This commit is contained in:
@ -24,6 +24,8 @@ protocol WildPhotographerReportServing {
|
||||
func reportList(_ request: WildReportListRequest) async throws -> WildReportListResponse
|
||||
/// 拉取单条举报详情。
|
||||
func reportDetail(id: Int) async throws -> WildReportDetailData
|
||||
/// 拉取举报微信小程序分享配置。
|
||||
func reportShare(id: String) async throws -> WildReportMiniProgramShareData
|
||||
}
|
||||
|
||||
/// 举报摄影师 API,负责和 `photog/report` 相关接口通讯。
|
||||
@ -115,6 +117,17 @@ final class WildPhotographerReportAPI: WildPhotographerReportServing {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取举报微信小程序分享配置。
|
||||
func reportShare(id: String) async throws -> WildReportMiniProgramShareData {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/report/share",
|
||||
queryItems: [URLQueryItem(name: "id", value: id)]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 补充证据接口请求体。
|
||||
|
||||
@ -273,6 +273,138 @@ struct WildReportListResponse: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 举报微信小程序分享配置,由后端按具体举报记录生成。
|
||||
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
|
||||
@ -416,19 +548,32 @@ struct WildReportDetailEvidence: Decodable, Equatable, Hashable {
|
||||
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 = "") {
|
||||
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 {
|
||||
@ -437,12 +582,52 @@ struct WildReportDetailEvidence: Decodable, Equatable, Hashable {
|
||||
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
|
||||
@ -534,8 +719,13 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
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
|
||||
@ -560,8 +750,13 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
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"
|
||||
@ -587,8 +782,13 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
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 = ""
|
||||
@ -612,8 +812,13 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
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
|
||||
@ -640,8 +845,13 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
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) ?? ""
|
||||
@ -684,6 +894,40 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
@ -702,7 +946,7 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
phoneNumber: contactPhone.nonEmpty ?? fallback?.phoneNumber ?? "",
|
||||
imageCount: imageCount,
|
||||
videoCount: videoCount,
|
||||
handlerInfo: fallback?.handlerInfo
|
||||
handlerInfo: detailHandlerInfo ?? fallback?.handlerInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -724,6 +968,14 @@ struct WildReportSupplementaryEvidence: Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 举报补充证据展示文案格式化工具。
|
||||
enum WildReportSupplementDisplayFormatter {
|
||||
/// 返回按当前列表展示顺序从 1 起算的补充次数文案。
|
||||
static func sequenceTitle(forDisplayIndex index: Int) -> String {
|
||||
"第\(max(0, index) + 1)次补充"
|
||||
}
|
||||
}
|
||||
|
||||
/// 举报位置解析工具。
|
||||
enum WildReportLocationParser {
|
||||
/// 将「景区 · 详细地址」拆成两段展示文案。
|
||||
|
||||
@ -291,7 +291,6 @@ final class WildPhotographerReportSubmitViewModel {
|
||||
guard !isSubmitting else { return }
|
||||
|
||||
isSubmitting = true
|
||||
submitProgressText = "正在上传证据"
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isSubmitting = false
|
||||
@ -303,19 +302,13 @@ final class WildPhotographerReportSubmitViewModel {
|
||||
let evidenceRequests = try await uploadAttachments(
|
||||
images + videos,
|
||||
uploader: uploader,
|
||||
scenicId: scenicId,
|
||||
progressPrefix: "正在上传现场证据"
|
||||
scenicId: scenicId
|
||||
)
|
||||
let paymentRequests = try await uploadAttachments(
|
||||
paymentImages,
|
||||
uploader: uploader,
|
||||
scenicId: scenicId,
|
||||
progressPrefix: "正在上传支付截图",
|
||||
completedOffset: evidenceRequests.count,
|
||||
totalOverride: evidenceRequests.count + paymentImages.count
|
||||
scenicId: scenicId
|
||||
)
|
||||
submitProgressText = "正在提交举报"
|
||||
notifyStateChange()
|
||||
|
||||
let submitResponse = try await api.submitReport(buildSubmitRequest(
|
||||
scenicId: scenicId,
|
||||
@ -447,27 +440,17 @@ final class WildPhotographerReportSubmitViewModel {
|
||||
private func uploadAttachments(
|
||||
_ attachments: [WildReportAttachment],
|
||||
uploader: any WildReportAttachmentUploading,
|
||||
scenicId: Int,
|
||||
progressPrefix: String,
|
||||
completedOffset: Int = 0,
|
||||
totalOverride: Int? = nil
|
||||
scenicId: Int
|
||||
) async throws -> [WildReportEvidenceRequest] {
|
||||
var requests: [WildReportEvidenceRequest] = []
|
||||
let total = totalOverride ?? attachments.count
|
||||
for (index, attachment) in attachments.enumerated() {
|
||||
let current = completedOffset + index + 1
|
||||
submitProgressText = "\(progressPrefix) \(current)/\(max(total, current))"
|
||||
notifyStateChange()
|
||||
for attachment in attachments {
|
||||
let data = try attachment.uploadData()
|
||||
let url = try await uploader.uploadWildReportAttachment(
|
||||
data: data,
|
||||
fileName: attachment.fileName,
|
||||
fileType: attachment.fileType,
|
||||
scenicId: scenicId
|
||||
) { [weak self] progress in
|
||||
self?.submitProgressText = "\(progressPrefix) \(current)/\(max(total, current)) · \(progress)%"
|
||||
self?.notifyStateChange()
|
||||
}
|
||||
) { _ in }
|
||||
requests.append(WildReportEvidenceRequest(
|
||||
fileURL: url,
|
||||
fileType: attachment.fileType,
|
||||
@ -666,9 +649,21 @@ final class WildPhotographerReportDetailViewModel {
|
||||
showActionMessage("举报位置:\(record.location)")
|
||||
}
|
||||
|
||||
/// 生成分享提示文案。
|
||||
func shareReport() {
|
||||
showActionMessage("已生成举报 \(record.id) 的分享信息,可发送给景区处理人员查看进度。")
|
||||
/// 拉取当前举报的小程序分享载荷。
|
||||
func shareReport(api: any WildPhotographerReportServing) async -> WeChatMiniProgramSharePayload? {
|
||||
guard let serverID = record.serverID, serverID > 0 else {
|
||||
showActionMessage("当前举报暂未同步到服务器,无法分享")
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let shareData = try await api.reportShare(id: String(serverID))
|
||||
return shareData.payload
|
||||
} catch is CancellationError {
|
||||
return nil
|
||||
} catch {
|
||||
showActionMessage(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func showActionMessage(_ message: String) {
|
||||
@ -782,11 +777,27 @@ final class WildPhotographerReportListViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成分享提示文案。
|
||||
func shareReport(_ record: WildReportRecord) {
|
||||
shareMessage = "已生成举报 \(record.id) 的分享信息,可发送给景区处理人员查看进度。"
|
||||
onShowMessage?(shareMessage ?? "")
|
||||
notifyStateChange()
|
||||
/// 拉取指定举报的小程序分享载荷。
|
||||
func shareReport(_ record: WildReportRecord, api: any WildPhotographerReportServing) async -> WeChatMiniProgramSharePayload? {
|
||||
guard let serverID = record.serverID, serverID > 0 else {
|
||||
shareMessage = "当前举报暂未同步到服务器,无法分享"
|
||||
onShowMessage?(shareMessage ?? "")
|
||||
notifyStateChange()
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let shareData = try await api.reportShare(id: String(serverID))
|
||||
shareMessage = nil
|
||||
notifyStateChange()
|
||||
return shareData.payload
|
||||
} catch is CancellationError {
|
||||
return nil
|
||||
} catch {
|
||||
shareMessage = error.localizedDescription
|
||||
onShowMessage?(shareMessage ?? "")
|
||||
notifyStateChange()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func reload(api: any WildPhotographerReportServing, scenicId: Int?) async {
|
||||
@ -907,7 +918,6 @@ final class WildReportSupplementEvidenceViewModel {
|
||||
guard !isSubmitting else { return }
|
||||
|
||||
isSubmitting = true
|
||||
submitProgressText = evidenceCount > 0 ? "正在上传补充证据" : "正在提交补充证据"
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isSubmitting = false
|
||||
@ -921,8 +931,6 @@ final class WildReportSupplementEvidenceViewModel {
|
||||
uploader: uploader,
|
||||
scenicId: scenicId
|
||||
)
|
||||
submitProgressText = "正在提交补充证据"
|
||||
notifyStateChange()
|
||||
|
||||
try await api.supplementReport(WildReportSupplementRequest(
|
||||
id: serverID ?? 0,
|
||||
@ -980,20 +988,14 @@ final class WildReportSupplementEvidenceViewModel {
|
||||
scenicId: Int
|
||||
) async throws -> [WildReportEvidenceRequest] {
|
||||
var requests: [WildReportEvidenceRequest] = []
|
||||
for (index, attachment) in attachments.enumerated() {
|
||||
let current = index + 1
|
||||
submitProgressText = "正在上传补充证据 \(current)/\(attachments.count)"
|
||||
notifyStateChange()
|
||||
for attachment in attachments {
|
||||
let data = try attachment.uploadData()
|
||||
let url = try await uploader.uploadWildReportAttachment(
|
||||
data: data,
|
||||
fileName: attachment.fileName,
|
||||
fileType: attachment.fileType,
|
||||
scenicId: scenicId
|
||||
) { [weak self] progress in
|
||||
self?.submitProgressText = "正在上传补充证据 \(current)/\(attachments.count) · \(progress)%"
|
||||
self?.notifyStateChange()
|
||||
}
|
||||
) { _ in }
|
||||
requests.append(WildReportEvidenceRequest(
|
||||
fileURL: url,
|
||||
fileType: attachment.fileType,
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
- 网络入口:`NetworkServices.shared.wildPhotographerReportAPI`
|
||||
- 后端成功码:`100000`
|
||||
- 响应包裹:工程统一 `APIEnvelope<T>`
|
||||
- 当前状态:“首页文案”、“举报类型”、“提交举报”、“我的举报列表”、“举报详情”、“补充证据提交”和“附近风险地图”已接接口;模块内生产 Mock 数据已移除。
|
||||
- 当前状态:“首页文案”、“举报类型”、“提交举报”、“我的举报列表”、“举报详情”、“微信小程序分享”、“补充证据提交”和“附近风险地图”已接接口;模块内生产 Mock 数据已移除。
|
||||
|
||||
## 首页文案
|
||||
|
||||
@ -334,7 +334,7 @@ struct WildReportListResponse: Decodable, Equatable {
|
||||
|
||||
### 用途
|
||||
|
||||
“举报详情”页面进入时调用。列表页传入 `WildReportRecord.serverID`,详情接口返回完整举报信息、现场证据、支付截图、补充内容和处理时间线。
|
||||
“举报详情”页面进入时调用。列表页传入 `WildReportRecord.serverID`,详情接口返回完整举报信息、现场证据、支付截图、补充内容、处理信息和处理时间线。
|
||||
|
||||
### 请求
|
||||
|
||||
@ -358,7 +358,10 @@ GET /api/yf-handset-app/photog/report/detail?id=32
|
||||
- `supplements` / `report_contents`:补充内容记录,优先使用 `supplements`,为空时从 `report_contents` 过滤 `type=supplement`。
|
||||
- `location_name` / `location_address`:详情页位置展示。
|
||||
- `handle_status` / `handle_status_text`:处理状态;iOS 支持 0 待处理、1 处理中、2 已处理、3 驳回。
|
||||
- `handle_remark`:处理备注,有值时在详情信息卡展示。
|
||||
- `handle_remark`:处理备注;`handler.remark` 为空时作为“处理信息”卡的处理意见兜底。
|
||||
- `handler`:处理人结构,包含 `name`、`phone`、`started_at`、`finished_at`、`remark`,详情页“处理信息”卡优先使用。
|
||||
- `handler_name` / `handler_phone` / `handled_at`:处理信息顶层兼容字段;`handler` 缺字段时作为兜底。
|
||||
- `handle_evidences`:处理凭证附件数组,字段同证据附件,并额外支持 `evidence_kind` / `evidence_kind_text`。
|
||||
- `timeline`:处理进度时间线;为空时只展示列表记录中的当前处理状态,不生成本地模拟流程。
|
||||
|
||||
### iOS 模型
|
||||
@ -366,6 +369,7 @@ GET /api/yf-handset-app/photog/report/detail?id=32
|
||||
```swift
|
||||
struct WildReportDetailData: Decodable, Equatable, Hashable
|
||||
struct WildReportDetailEvidence: Decodable, Equatable, Hashable
|
||||
struct WildReportDetailHandler: Decodable, Equatable, Hashable
|
||||
struct WildReportDetailContent: Decodable, Equatable, Hashable
|
||||
struct WildReportDetailTimelineItem: Decodable, Equatable, Hashable
|
||||
```
|
||||
@ -376,6 +380,9 @@ struct WildReportDetailTimelineItem: Decodable, Equatable, Hashable
|
||||
- 如果传入记录没有 `serverID`,说明来自本地提交成功页等临时记录,不请求详情接口。
|
||||
- 详情接口失败时保留现有基础信息,并 toast 错误。
|
||||
- 图片证据使用 Kingfisher 加载网络图;视频证据展示视频样式缩略块和文件信息。
|
||||
- 状态为已处理或驳回且存在处理信息时展示“处理信息”卡:处理人、处理人电话、处理时间、处理完成时间、处理意见。
|
||||
- “处理人电话”展示脱敏文案,点击使用接口原始 `handler.phone` 或 `handler_phone` 发起 `tel://`。
|
||||
- `handle_evidences` 非空时在“处理信息”卡内展示“处理凭证”横向缩略图,`file_type=2` 按视频处理,其余按图片处理。
|
||||
|
||||
### 当前调用位置
|
||||
|
||||
@ -383,7 +390,62 @@ struct WildReportDetailTimelineItem: Decodable, Equatable, Hashable
|
||||
- `WildPhotographerReportDetailViewModel.loadDetailIfNeeded(api:)`
|
||||
- `WildPhotographerReportAPI.reportDetail(id:)`
|
||||
|
||||
## 5. 补充证据提交
|
||||
## 5. 举报微信小程序分享
|
||||
|
||||
### 用途
|
||||
|
||||
“我的举报”列表卡片和“举报详情”头部点击分享时调用,后端按举报 ID 返回微信小程序分享配置,iOS 映射为 `WeChatMiniProgramSharePayload` 后调用 `WeChatShareService.shareMiniProgram(...)`。
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
GET /api/yf-handset-app/photog/report/share?id=32
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `id` | string | 必填,举报记录服务端 ID;iOS 使用 `WildReportRecord.serverID` |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"userName": "wx8c85189bf3bdda29",
|
||||
"path": "pages/scenic/radar/report-detail/index?id=32",
|
||||
"title": "私下收款举报",
|
||||
"imagePath": "/assets/share/report-share-cover.png",
|
||||
"webpageUrl": "https://www.youfuntour.com",
|
||||
"withShareTicket": true,
|
||||
"miniprogramType": 0,
|
||||
"scene": 0
|
||||
},
|
||||
"time": "2026-07-09 15:42:19"
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
- `title`:微信分享卡片标题,不能为空。
|
||||
- `webpageUrl`:小程序打不开时的兼容网页地址,微信要求不超过 1024 字节。
|
||||
- `userName`:微信 SDK `WXMiniProgramObject.userName` 字段;请后端确认返回值是否应为小程序原始 ID `gh_...`。
|
||||
- `path`:小程序页面路径,可携带举报 ID 或分享 token。
|
||||
- `miniprogramType`:0 正式版、1 开发版、2 体验版;由后端按环境明确返回。
|
||||
- `withShareTicket`:是否携带群分享票据。
|
||||
- `scene`:当前仅支持 0 微信好友会话;非 0 会被通用微信分享模块拒绝。
|
||||
- `imagePath`:当前返回的是小程序内资源路径,iOS 不下载;微信小程序预览图暂使用 App 图标兜底。若后续需要定制卡片图,请返回可下载的 `https` 图片地址。
|
||||
|
||||
### 当前调用位置
|
||||
|
||||
- `WildPhotographerReportListViewController` 列表卡片分享按钮
|
||||
- `WildPhotographerReportDetailViewController` 详情头部分享按钮
|
||||
- `WildPhotographerReportAPI.reportShare(id:)`
|
||||
|
||||
## 6. 补充证据提交
|
||||
|
||||
### 请求
|
||||
|
||||
|
||||
Reference in New Issue
Block a user