452 lines
17 KiB
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)
|
|
)
|
|
}
|
|
}
|
|
|