feat: update app workflows and permissions

This commit is contained in:
2026-07-09 17:34:00 +08:00
parent 43e6133c21
commit 8e356973bd
44 changed files with 2944 additions and 307 deletions

View File

@ -125,7 +125,7 @@ final class WildPhotographerReportListViewController: BaseViewController {
reports.forEach { record in
let card = WildReportListCardView(record: record)
card.onOpen = { [weak self] in self?.openDetail(record) }
card.onShare = { [weak self] in self?.viewModel.shareReport(record) }
card.onShare = { [weak self] in self?.shareReport(record) }
stack.addArrangedSubview(card)
}
}
@ -148,6 +148,21 @@ final class WildPhotographerReportListViewController: BaseViewController {
)
}
private func shareReport(_ record: WildReportRecord) {
showLoading()
Task { [weak self] in
guard let self else { return }
let payload = await self.viewModel.shareReport(record, api: self.api)
await MainActor.run {
self.hideLoading()
guard let payload else { return }
WeChatShareService.shareMiniProgram(payload) { [weak self] result in
self?.showWildReportShareResult(result)
}
}
}
}
private var currentScenicId: Int? {
let scenicId = AppStore.shared.currentScenicId
return scenicId > 0 ? scenicId : nil
@ -736,7 +751,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "举报说明", value: viewModel.descriptionText, multiline: true))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "微信号/手机号", value: viewModel.contactText))
if !viewModel.handleRemarkText.isEmpty {
if !viewModel.handleRemarkText.isEmpty, !viewModel.showHandlerInfo {
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理备注", value: viewModel.handleRemarkText, multiline: true))
}
@ -768,13 +783,16 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 18, left: 18, bottom: 10, right: 18))
}
card.stack.addArrangedSubview(titleWrap)
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理人", value: info.handlerName))
card.stack.addArrangedSubview(WildReportHandlerInfoRowView(title: "处理人", value: info.handlerName))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理人电话", value: info.maskedHandlerPhone, trailingIconName: "phone.fill"))
let phoneRow = WildReportHandlerInfoRowView(title: "处理人电话", value: info.maskedHandlerPhone, trailingIconName: "phone.fill")
phoneRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handlerPhoneTapped)))
phoneRow.accessibilityTraits.insert(.button)
card.stack.addArrangedSubview(phoneRow)
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理时间", value: info.processingAt))
card.stack.addArrangedSubview(WildReportHandlerInfoRowView(title: "处理时间", value: info.processingAt))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理完成时间", value: info.processedAt))
card.stack.addArrangedSubview(WildReportHandlerInfoRowView(title: "处理完成时间", value: info.processedAt))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理意见", value: info.processOpinion, multiline: true))
@ -800,7 +818,11 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
header.alignment = .center
header.addArrangedSubview(WildReportUI.label(item.submitTime, font: .app(.captionMedium), color: AppColor.textSecondary))
header.addArrangedSubview(UIView())
header.addArrangedSubview(WildReportUI.label("\(items.count - index)次补充", font: .app(.captionMedium), color: AppColor.primary))
header.addArrangedSubview(WildReportUI.label(
WildReportSupplementDisplayFormatter.sequenceTitle(forDisplayIndex: index),
font: .app(.captionMedium),
color: AppColor.primary
))
itemStack.addArrangedSubview(header)
itemStack.addArrangedSubview(WildReportUI.label(item.summaryText, font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0))
card.stack.addArrangedSubview(itemStack)
@ -948,12 +970,11 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
section.spacing = 12
section.layoutMargins = UIEdgeInsets(top: 14, left: 18, bottom: 16, right: 18)
section.isLayoutMarginsRelativeArrangement = true
section.addArrangedSubview(makeSectionHeader(
section.addArrangedSubview(makeCompactMediaSectionHeader(
iconName: "photo.on.rectangle.angled",
title: "处理凭证",
subtitle: info.completionMediaSummary,
chevron: false,
action: nil
summary: info.completionMediaSummary,
action: #selector(completionPreviewTapped)
))
let mediaScroll = UIScrollView()
@ -1199,10 +1220,33 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
return wrapper
}
@objc private func shareTapped() { viewModel.shareReport() }
@objc private func shareTapped() {
showLoading()
Task { [weak self] in
guard let self else { return }
let payload = await self.viewModel.shareReport(api: self.api)
await MainActor.run {
self.hideLoading()
guard let payload else { return }
WeChatShareService.shareMiniProgram(payload) { [weak self] result in
self?.showWildReportShareResult(result)
}
}
}
}
@objc private func previewTapped() { presentReporterMediaPreview(selectedKey: nil) }
@objc private func paymentPreviewTapped() { presentPaymentMediaPreview(selectedKey: nil) }
@objc private func completionPreviewTapped() { presentCompletionMediaPreview(selectedKey: nil) }
@objc private func locationTapped() { viewModel.showLocationPreview() }
@objc private func handlerPhoneTapped() {
guard let phone = viewModel.handlerInfo?.handlerPhone.trimmingCharacters(in: .whitespacesAndNewlines),
!phone.isEmpty,
let url = URL(string: "tel://\(phone)") else {
showToast("暂无处理人电话")
return
}
UIApplication.shared.open(url)
}
@objc private func reporterMediaTapped(_ recognizer: UITapGestureRecognizer) {
guard let tile = recognizer.view as? WildReportDetailMediaThumbnailView else { return }
@ -1367,6 +1411,60 @@ private final class WildReportDetailInfoRowView: UIView {
}
}
///
private final class WildReportHandlerInfoRowView: UIView {
init(title: String, value: String, trailingIconName: String? = nil) {
super.init(frame: .zero)
let titleLabel = WildReportUI.label(title, font: .app(.captionMedium), color: AppColor.textSecondary)
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
let valueLabel = WildReportUI.label(value, font: .app(.subtitle), color: AppColor.textPrimary, lines: 1)
valueLabel.textAlignment = .right
valueLabel.lineBreakMode = .byTruncatingTail
valueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
addSubview(titleLabel)
addSubview(valueLabel)
if let trailingIconName {
let icon = UIImageView(image: UIImage(systemName: trailingIconName))
icon.tintColor = AppColor.primary
icon.contentMode = .scaleAspectFit
addSubview(icon)
icon.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(18)
make.centerY.equalToSuperview()
make.size.equalTo(16)
}
valueLabel.snp.makeConstraints { make in
make.trailing.equalTo(icon.snp.leading).offset(-6)
}
}
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(18)
make.centerY.equalToSuperview()
}
valueLabel.snp.makeConstraints { make in
make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(14)
make.centerY.equalToSuperview()
if trailingIconName == nil {
make.trailing.equalToSuperview().inset(18)
}
}
snp.makeConstraints { make in
make.height.equalTo(48)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension MediaPreviewItem {
init?(wildReportReporterItem item: WildReportReporterMediaItem) {
guard let urlText = item.previewKey else { return nil }
@ -1393,6 +1491,28 @@ private extension MediaPreviewItem {
}
}
private extension BaseViewController {
@MainActor
func showWildReportShareResult(_ result: WeChatShareResult) {
switch result {
case .success:
showToast("分享成功")
case .cancelled:
showToast("已取消分享")
case .notInstalled:
showToast("请先安装微信")
case .unsupported:
showToast("当前微信版本不支持分享")
case .invalidPayload(let message):
showToast(message)
case .sendFailed:
showToast("微信分享发送失败")
case .unknown:
showToast("微信分享失败")
}
}
}
private extension WildReportReporterMediaItem {
var previewKey: String? {
guard let text = fileURL?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return nil }

View File

@ -20,6 +20,7 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
private let submitButton = UIButton(type: .system)
private var pickerTarget: WildReportPickerTarget = .image
private weak var activeInputView: UIView?
private var isShowingSubmitLoading = false
init(
homeViewModel: WildPhotographerReportHomeViewModel,
@ -109,7 +110,11 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.rebuildContent() }
Task { @MainActor in
guard let self else { return }
self.updateSubmitLoading()
self.rebuildContent()
}
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
@ -152,7 +157,7 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
contentStack.addArrangedSubview(paymentCard)
contentStack.setCustomSpacing(16, after: paymentCard)
contentStack.addArrangedSubview(submitButton)
submitButton.setTitle(viewModel.isSubmitting ? viewModel.submitProgressText : "提交举报", for: .normal)
submitButton.setTitle("提交举报", for: .normal)
submitButton.isEnabled = !viewModel.isSubmitting
submitButton.alpha = viewModel.isSubmitting ? 0.78 : 1
submitButton.snp.remakeConstraints { make in
@ -561,6 +566,18 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
@objc private func contactChanged() { viewModel.updateContact(contactField.text ?? "") }
@objc private func dismissKeyboard() { view.endEditing(true) }
@MainActor
private func updateSubmitLoading() {
if viewModel.isSubmitting {
guard !isShowingSubmitLoading else { return }
isShowingSubmitLoading = true
showLoading()
} else if isShowingSubmitLoading {
isShowingSubmitLoading = false
hideLoading()
}
}
private func registerKeyboardNotifications() {
NotificationCenter.default.addObserver(
self,

View File

@ -23,6 +23,7 @@ final class WildReportSupplementEvidenceViewController: BaseViewController {
private var pickerTarget: WildReportPickerTarget = .image
private weak var activeInputView: UIView?
private var isShowingSubmitLoading = false
init(
record: WildReportRecord,
@ -111,6 +112,7 @@ final class WildReportSupplementEvidenceViewController: BaseViewController {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in
guard let self else { return }
self.updateSubmitLoading()
if self.textView.isFirstResponder {
self.updateTextInputState()
self.updateSubmitButton()
@ -271,14 +273,23 @@ final class WildReportSupplementEvidenceViewController: BaseViewController {
}
private func updateSubmitButton() {
let title = viewModel.isSubmitting && !viewModel.submitProgressText.isEmpty
? viewModel.submitProgressText
: "提交补充证据"
submitButton.setTitle(title, for: .normal)
submitButton.setTitle("提交补充证据", for: .normal)
submitButton.isEnabled = !viewModel.isSubmitting
submitButton.alpha = viewModel.isSubmitting ? 0.78 : 1
}
@MainActor
private func updateSubmitLoading() {
if viewModel.isSubmitting {
guard !isShowingSubmitLoading else { return }
isShowingSubmitLoading = true
showLoading()
} else if isShowingSubmitLoading {
isShowingSubmitLoading = false
hideLoading()
}
}
private func canAddAttachment(kind: WildReportAttachmentKind, currentCount: Int, maxCount: Int) -> Bool {
let ownRemaining = max(0, maxCount - currentCount)
let evidenceRemaining = max(0, 9 - viewModel.images.count - viewModel.videos.count)