新增举报摄影师模块并接入首页路由,桌面应用名改为随心瞰商家版。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,451 @@
|
||||
//
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user