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 }