// // 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) ) } }