Files
suixinkan_ios_new/suixinkan/Features/WildPhotographerReport/Views/WildPhotographerReportHomeViews.swift

452 lines
17 KiB
Swift

//
// WildPhotographerReportHomeViews
// suixinkan
//
// Created by Codex on 2026/7/1.
//
import MapKit
import SwiftUI
import UIKit
struct WildPhotographerReportHomeView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@StateObject private var viewModel = WildPhotographerReportHomeViewModel()
var body: some View {
Group {
switch viewModel.pageState {
case .normal:
normalContent
case .loading:
loadingContent
case .empty:
stateContent(
title: viewModel.emptyTitle,
systemImage: viewModel.emptyImage,
message: viewModel.emptyMessage,
actionTitle: "恢复演示数据"
) {
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.restoreDemoData()
}
}
case .error:
stateContent(
title: "加载失败",
systemImage: "wifi.exclamationmark",
message: "举报服务暂时不可用,请稍后重试。",
actionTitle: "重新加载"
) {
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.retryLoadAfterError()
}
}
}
}
.animation(.easeInOut(duration: 0.22), value: viewModel.pageState)
.navigationTitle("举报摄影师")
.navigationBarTitleDisplayMode(.inline)
#if DEBUG
.toolbar {
WildReportStateToolbar(selection: $viewModel.pageState)
}
#endif
.background(Color(hex: 0xF8FAFF).ignoresSafeArea())
}
@ViewBuilder
private var normalContent: some View {
ScrollView {
VStack(spacing: 12) {
reportTab
}
.frame(maxWidth: contentMaxWidth)
.padding(.horizontal, 18)
.padding(.top, 14)
.padding(.bottom, 28)
}
}
private var reportTab: some View {
VStack(spacing: 12) {
heroCard
riskMapEntry
noticeCard
trustBadges
}
}
private var riskMapEntry: some View {
NavigationLink {
WildReportRiskMapView(
record: viewModel.reports.first ?? WildPhotographerReportMockStore.seedReports[0],
riskPoints: viewModel.riskPoints
)
} label: {
HStack(spacing: 16) {
ZStack {
RoundedRectangle(cornerRadius: 14)
.fill(AppDesign.primarySoft)
Image(systemName: "map.fill")
.font(.system(size: 26, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
.frame(width: 54, height: 54)
VStack(alignment: .leading, spacing: 5) {
Text("附近风险地图")
.font(.system(size: 17, weight: .bold))
.foregroundStyle(AppDesign.textPrimary)
Text("查看附近摄影师风险点位")
.font(.system(size: 13))
.foregroundStyle(AppDesign.textSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Color(hex: 0xB6BECA))
}
.padding(16)
.background(.white, in: RoundedRectangle(cornerRadius: 16))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(AppDesign.border, lineWidth: 1)
)
}
.buttonStyle(.plain)
}
private var heroCard: some View {
VStack(alignment: .leading, spacing: 18) {
HStack(alignment: .top, spacing: 18) {
VStack(alignment: .leading, spacing: 10) {
Text("实名举报")
.font(.system(size: 26, weight: .heavy))
.foregroundStyle(AppDesign.textPrimary)
Text("摄影师")
.foregroundStyle(AppDesign.primary)
.font(.system(size: 26, weight: .heavy))
.lineLimit(1)
.minimumScaleFactor(0.8)
}
Spacer(minLength: 10)
WildReportHeroIllustration()
.frame(width: 112, height: 112)
.padding(.top, -2)
}
Text("可上传图片、视频、文字和位置,景区公安将接收并现场处理。")
.font(.system(size: 15, weight: .regular))
.foregroundStyle(AppDesign.textSecondary)
.lineSpacing(4)
.fixedSize(horizontal: false, vertical: true)
HStack(spacing: 12) {
NavigationLink {
WildPhotographerReportSubmitView(homeViewModel: viewModel)
} label: {
HStack(spacing: 8) {
Image(systemName: "checkmark.shield.fill")
.font(.system(size: 18, weight: .semibold))
Text("立即举报")
.font(.system(size: 17, weight: .bold))
}
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: 12))
}
.buttonStyle(.plain)
NavigationLink {
WildPhotographerReportListView(homeViewModel: viewModel)
} label: {
HStack(spacing: 8) {
Image(systemName: "doc.text.fill")
.font(.system(size: 17, weight: .semibold))
Text("我的举报")
.font(.system(size: 17, weight: .bold))
}
.foregroundStyle(AppDesign.primary)
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(.white, in: RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color(hex: 0xBFDAFF), lineWidth: 1)
)
}
.buttonStyle(.plain)
}
}
.padding(20)
.background(
LinearGradient(
colors: [.white, Color(hex: 0xEEF6FF)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
in: RoundedRectangle(cornerRadius: 20)
)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color(hex: 0xBFD7FF), lineWidth: 1)
)
.shadow(color: Color(hex: 0x0F172A).opacity(0.05), radius: 14, y: 8)
}
private var noticeCard: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 10) {
HStack(spacing: 8) {
Image(systemName: "list.clipboard.fill")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(AppDesign.primary)
Text("举报须知")
.font(.system(size: 17, weight: .bold))
.foregroundStyle(AppDesign.textPrimary)
}
Spacer()
NavigationLink {
WildReportRuleView()
} label: {
HStack(spacing: 4) {
Image(systemName: "shield.lefthalf.filled")
.font(.system(size: 12, weight: .semibold))
Text("举报规则")
.font(.system(size: 13, weight: .bold))
Image(systemName: "chevron.right")
.font(.system(size: 10, weight: .bold))
}
.foregroundStyle(AppDesign.primary)
.padding(.horizontal, 10)
.frame(height: 30)
.background(AppDesign.primarySoft, in: Capsule())
}
.buttonStyle(.plain)
}
VStack(alignment: .leading, spacing: 10) {
bullet("需实名登录")
bullet("请尽量上传清晰证据")
bullet("恶意举报将被追责")
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(.white, in: RoundedRectangle(cornerRadius: 16))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(AppDesign.border, lineWidth: 1)
)
}
private var trustBadges: some View {
HStack(spacing: 10) {
trustBadge(icon: "person.crop.circle.badge.checkmark", title: "实名", subtitle: "身份核验 真实可靠", tint: AppDesign.primary)
trustBadge(icon: "checkmark.shield.fill", title: "安全", subtitle: "数据加密 隐私保护", tint: AppDesign.success)
trustBadge(icon: "person.text.rectangle.fill", title: "景区公安处理", subtitle: "专业受理 依法处置", tint: AppDesign.primary)
}
}
private var loadingContent: some View {
ScrollView {
VStack(spacing: 14) {
RoundedRectangle(cornerRadius: 20)
.fill(Color(hex: 0xE8EEF7))
.frame(height: 250)
ForEach(0..<3, id: \.self) { _ in
RoundedRectangle(cornerRadius: 16)
.fill(Color(hex: 0xE8EEF7))
.frame(height: 86)
}
}
.frame(maxWidth: contentMaxWidth)
.padding(.horizontal, 18)
.padding(.top, 14)
.padding(.bottom, 28)
.redacted(reason: .placeholder)
}
}
private func stateContent(
title: String,
systemImage: String,
message: String,
actionTitle: String,
action: @escaping () -> Void
) -> some View {
VStack(spacing: 16) {
AppContentUnavailableView(title, systemImage: systemImage, description: Text(message))
Button(actionTitle, action: action)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 132, height: 42)
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: 10))
}
.frame(maxWidth: contentMaxWidth)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal, 20)
.padding(.bottom, 28)
.background(Color(hex: 0xF8FAFF))
}
private func bullet(_ text: String) -> some View {
HStack(spacing: 10) {
Circle()
.fill(AppDesign.primary)
.frame(width: 6, height: 6)
Text(text)
.font(.system(size: 14))
.foregroundStyle(Color(hex: 0x4B5563))
}
}
private func trustBadge(icon: String, title: String, subtitle: String, tint: Color) -> some View {
VStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 20, weight: .semibold))
.foregroundStyle(tint)
.frame(width: 34, height: 34)
.background(tint.opacity(0.10), in: Circle())
Text(title)
.font(.system(size: 14, weight: .bold))
.foregroundStyle(AppDesign.textPrimary)
.lineLimit(1)
Text(subtitle)
.font(.system(size: 10))
.foregroundStyle(AppDesign.textSecondary)
.lineLimit(1)
.minimumScaleFactor(0.7)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.frame(minHeight: 86)
.background(.white, in: RoundedRectangle(cornerRadius: 14))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(AppDesign.border, lineWidth: 1)
)
}
private var contentMaxWidth: CGFloat {
horizontalSizeClass == .regular ? 760 : .infinity
}
}
private struct WildReportRuleView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 14) {
ruleHero
ruleSection(
icon: "person.badge.shield.checkmark.fill",
title: "实名提交",
items: [
"举报人需完成实名登录后提交。",
"平台仅向景区公安处理人员展示必要核验信息。",
"举报记录将用于后续处理进度追踪。"
]
)
ruleSection(
icon: "photo.on.rectangle.angled",
title: "证据要求",
items: [
"请尽量上传清晰图片、视频、位置和文字说明。",
"证据应与疑似违规揽客、线下收费、未备案拍摄服务相关。",
"支付截图可作为辅助证据,敏感信息可适当遮挡。"
]
)
ruleSection(
icon: "exclamationmark.triangle.fill",
title: "禁止恶意举报",
items: [
"禁止捏造事实、冒用他人信息或重复恶意举报。",
"恶意举报将被记录并可能承担相应责任。",
"如信息有误,可在举报详情中补充说明。"
]
)
}
.padding(.horizontal, 18)
.padding(.top, 14)
.padding(.bottom, 28)
}
.navigationTitle("举报规则")
.navigationBarTitleDisplayMode(.inline)
.background(Color(hex: 0xF8FAFF).ignoresSafeArea())
}
private var ruleHero: some View {
VStack(alignment: .leading, spacing: 8) {
Label("规则说明", systemImage: "shield.checkered")
.font(.system(size: 19, weight: .bold))
.foregroundStyle(AppDesign.textPrimary)
Text("请根据现场真实情况提交举报,景区公安将结合证据和位置进行核查处理。")
.font(.system(size: 14))
.foregroundStyle(AppDesign.textSecondary)
.lineSpacing(4)
}
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
LinearGradient(
colors: [.white, Color(hex: 0xEEF6FF)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
in: RoundedRectangle(cornerRadius: 18)
)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color(hex: 0xCFE1FF), lineWidth: 1)
)
}
private func ruleSection(icon: String, title: String, items: [String]) -> some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(width: 34, height: 34)
.background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 10))
Text(title)
.font(.system(size: 17, weight: .bold))
.foregroundStyle(AppDesign.textPrimary)
}
VStack(alignment: .leading, spacing: 8) {
ForEach(items, id: \.self) { item in
HStack(alignment: .top, spacing: 8) {
Circle()
.fill(AppDesign.primary)
.frame(width: 5, height: 5)
.padding(.top, 7)
Text(item)
.font(.system(size: 14))
.foregroundStyle(AppDesign.textSecondary)
.lineSpacing(3)
}
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.white, in: RoundedRectangle(cornerRadius: 16))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(AppDesign.border, lineWidth: 1)
)
}
}