// // WildPhotographerReportDetailViews // suixinkan // // Created by Codex on 2026/7/1. // import MapKit import SwiftUI import UIKit struct WildPhotographerReportDetailView: View { @StateObject private var viewModel: WildPhotographerReportDetailViewModel @ObservedObject var homeViewModel: WildPhotographerReportHomeViewModel @Environment(\.dismiss) private var dismiss @Environment(\.horizontalSizeClass) private var horizontalSizeClass @EnvironmentObject private var toastCenter: ToastCenter @State private var previewImageURL: String? init(record: WildReportRecord, homeViewModel: WildPhotographerReportHomeViewModel) { self.homeViewModel = homeViewModel _viewModel = StateObject( wrappedValue: WildPhotographerReportDetailViewModel(record: record, homeViewModel: homeViewModel) ) } private var supplementaryItems: [WildReportSupplementaryEvidence] { homeViewModel.supplementaryEvidences(for: viewModel.record.id) } var body: some View { ScrollView { VStack(spacing: 12) { summaryCard reportInfoCard if viewModel.showHandlerInfo, let handlerInfo = viewModel.handlerInfo { handlerInfoCard(handlerInfo) } evidenceCard if !supplementaryItems.isEmpty { supplementaryEvidenceCard } timelineCard } .frame(maxWidth: contentMaxWidth) .padding(.horizontal, 18) .padding(.top, 14) .padding(.bottom, 120) } .navigationTitle("举报详情") .navigationBarTitleDisplayMode(.inline) .background(Color(hex: 0xF8FAFF).ignoresSafeArea()) .safeAreaInset(edge: .bottom) { bottomActionBar } .onChange(of: viewModel.actionMessage) { message in guard let message else { return } toastCenter.show(message) viewModel.actionMessage = nil } .fullScreenCover(item: Binding( get: { previewImageURL.map { WildReportPreviewImageURL(url: $0) } }, set: { previewImageURL = $0?.url } )) { item in WildReportMapURLImagePreviewView(imageURL: item.url) { previewImageURL = nil } } } private var summaryCard: some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .top, spacing: 14) { Image(systemName: "exclamationmark.shield.fill") .font(.system(size: 30, weight: .semibold)) .foregroundStyle(.white) .frame(width: 62, height: 62) .background(AppDesign.primary, in: RoundedRectangle(cornerRadius: 16)) VStack(alignment: .leading, spacing: 8) { Text(viewModel.heroTitle) .font(.system(size: 23, weight: .heavy)) .foregroundStyle(AppDesign.textPrimary) .lineLimit(2) HStack(spacing: 6) { Image(systemName: "mappin.circle.fill") .font(.system(size: 12, weight: .semibold)) Text(viewModel.scenicAreaName) .font(.system(size: 14, weight: .bold)) } .foregroundStyle(AppDesign.primary) .padding(.horizontal, 10) .frame(height: 27) .background(AppDesign.primarySoft, in: Capsule()) } Spacer(minLength: 0) Button { viewModel.shareReport() } label: { VStack(spacing: 4) { Image(systemName: "square.and.arrow.up") .font(.system(size: 17, weight: .semibold)) Text("分享") .font(.system(size: 11, weight: .bold)) } .foregroundStyle(AppDesign.primary) .frame(width: 48, height: 48) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 14)) } .buttonStyle(.plain) .accessibilityLabel("分享举报详情") } Divider() HStack(spacing: 8) { VStack(alignment: .leading, spacing: 4) { Text("举报编号") .font(.system(size: 14)) .foregroundStyle(AppDesign.textSecondary) Text(viewModel.record.id) .font(.system(size: 19, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) } Spacer() Text(viewModel.displayStatusText) .font(.system(size: 16, weight: .bold)) .foregroundStyle(viewModel.displayStatusColor) .padding(.horizontal, 14) .frame(height: 38) .background(viewModel.displayStatusColor.opacity(0.12), in: Capsule()) } } .padding(18) .background(.white, in: RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(AppDesign.border, lineWidth: 1) ) } private var reportInfoCard: some View { VStack(spacing: 0) { reportInfoRow(title: "举报类型", value: viewModel.record.reportType.rawValue) Divider().padding(.leading, 18) reportInfoRow(title: "举报说明", value: viewModel.descriptionText) Divider().padding(.leading, 18) reportInfoRow(title: "微信号/手机号", value: viewModel.contactText) } .background(.white, in: RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(AppDesign.border, lineWidth: 1) ) } private func reportInfoRow(title: String, value: String) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.system(size: 13)) .foregroundStyle(AppDesign.textSecondary) Text(value) .font(.system(size: 15, weight: .medium)) .foregroundStyle(AppDesign.textPrimary) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 18) .padding(.vertical, 14) } private func handlerInfoCard(_ info: WildReportHandlerInfo) -> some View { VStack(alignment: .leading, spacing: 0) { Text("处理信息") .font(.system(size: 18, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) .padding(.horizontal, 18) .padding(.top, 16) .padding(.bottom, 12) handlerInfoRow(label: "处理人", value: info.handlerName) Divider().padding(.leading, 18) handlerInfoPhoneRow(info: info) Divider().padding(.leading, 18) handlerInfoRow(label: "处理时间", value: info.processingAt) Divider().padding(.leading, 18) handlerInfoRow(label: "处理完成时间", value: info.processedAt) Divider().padding(.leading, 18) handlerInfoMultilineRow(label: "处理意见", value: info.processOpinion) if !info.completionMedia.isEmpty { Divider().padding(.leading, 18) handlerCompletionMediaSection(info: info) } } .background(.white, in: RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(AppDesign.border, lineWidth: 1) ) } private func handlerInfoRow(label: String, value: String) -> some View { HStack { Text(label) .font(.system(size: 13)) .foregroundStyle(AppDesign.textSecondary) Spacer() Text(value) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) } .padding(.horizontal, 18) .padding(.vertical, 14) } private func handlerInfoMultilineRow(label: String, value: String) -> some View { VStack(alignment: .leading, spacing: 6) { Text(label) .font(.system(size: 13)) .foregroundStyle(AppDesign.textSecondary) Text(value) .font(.system(size: 14, weight: .medium)) .foregroundStyle(AppDesign.textPrimary) .lineSpacing(4) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 18) .padding(.vertical, 14) } private func handlerInfoPhoneRow(info: WildReportHandlerInfo) -> some View { HStack { Text("处理人电话") .font(.system(size: 13)) .foregroundStyle(AppDesign.textSecondary) Spacer() Button { guard let url = URL(string: "tel://\(info.handlerPhone)") else { return } UIApplication.shared.open(url) } label: { HStack(spacing: 6) { Text(info.maskedHandlerPhone) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(AppDesign.primary) Image(systemName: "phone.fill") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(AppDesign.primary) } } .buttonStyle(.plain) } .padding(.horizontal, 18) .padding(.vertical, 14) } private func handlerCompletionMediaSection(info: WildReportHandlerInfo) -> some View { VStack(alignment: .leading, spacing: 12) { HStack { HStack(spacing: 8) { Image(systemName: "photo.on.rectangle.angled") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(AppDesign.primary) Text("处理凭证") .font(.system(size: 15, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) } Spacer() Text(info.completionMediaSummary) .font(.system(size: 13, weight: .medium)) .foregroundStyle(AppDesign.textSecondary) } .padding(.horizontal, 18) .padding(.top, 14) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(info.completionMedia) { item in Button { if item.kind == .image { previewImageURL = item.coverURL } else { viewModel.showMediaPreview(kind: "处理凭证视频") } } label: { handlerCompletionMediaThumbnail(item) } .buttonStyle(.plain) } } .padding(.horizontal, 18) } .padding(.bottom, 16) } } private func handlerCompletionMediaThumbnail(_ item: WildReportCompletionMediaItem) -> some View { ZStack { AsyncImage(url: URL(string: item.coverURL)) { phase in switch phase { case .success(let image): image .resizable() .scaledToFill() default: RoundedRectangle(cornerRadius: 10) .fill(Color(hex: 0xE5E7EB)) .overlay { ProgressView() } } } .frame(width: 108, height: 108) .clipShape(RoundedRectangle(cornerRadius: 10)) if item.kind == .video { Color.black.opacity(0.28) .clipShape(RoundedRectangle(cornerRadius: 10)) Image(systemName: "play.circle.fill") .font(.system(size: 28, weight: .semibold)) .foregroundStyle(.white) if let duration = item.duration, !duration.isEmpty { VStack { Spacer() HStack { Spacer() Text(duration) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(.white) .padding(.horizontal, 6) .padding(.vertical, 3) .background(.black.opacity(0.45), in: Capsule()) .padding(6) } } } } } .frame(width: 108, height: 108) } private var supplementaryEvidenceCard: some View { VStack(alignment: .leading, spacing: 12) { Text("补充证据记录") .font(.system(size: 18, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) .padding(.horizontal, 18) .padding(.top, 16) ForEach(Array(supplementaryItems.enumerated()), id: \.element.id) { index, item in VStack(alignment: .leading, spacing: 8) { HStack { Text(item.submitTime) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(AppDesign.primary) Spacer() Text("第\(supplementaryItems.count - index)次补充") .font(.system(size: 12)) .foregroundStyle(AppDesign.textSecondary) } Text(item.summaryText) .font(.system(size: 14)) .foregroundStyle(AppDesign.textPrimary) .lineSpacing(4) } .padding(.horizontal, 18) .padding(.vertical, 12) if index < supplementaryItems.count - 1 { Divider().padding(.leading, 18) } } .padding(.bottom, 8) } .background(.white, in: RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(AppDesign.border, lineWidth: 1) ) } private var evidenceCard: some View { VStack(spacing: 0) { if !viewModel.reporterMediaItems.isEmpty { reporterMaterialsSection Divider().padding(.horizontal, 18) } evidenceSectionHeader(icon: "mappin.circle.fill", title: "位置", value: viewModel.detailAddress) { viewModel.showLocationPreview() } locationPreview } .padding(.vertical, 2) .background(.white, in: RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(AppDesign.border, lineWidth: 1) ) } private var reporterMaterialsSection: some View { VStack(alignment: .leading, spacing: 12) { Button { viewModel.showMediaPreview(kind: "举报人材料") } label: { HStack { HStack(spacing: 8) { Image(systemName: "photo.on.rectangle.angled") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(AppDesign.primary) Text("举报人材料") .font(.system(size: 15, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) } Spacer() Text(viewModel.reporterMediaSummary) .font(.system(size: 13, weight: .medium)) .foregroundStyle(AppDesign.textSecondary) Image(systemName: "chevron.right") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(Color(hex: 0xB6BECA)) } } .buttonStyle(.plain) .padding(.horizontal, 18) .padding(.top, 14) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(viewModel.reporterMediaItems) { item in Button { viewModel.showReporterMediaPreview(item) } label: { reporterMediaThumbnail(item) } .buttonStyle(.plain) } } .padding(.horizontal, 18) } .padding(.bottom, 16) } } private func reporterMediaThumbnail(_ item: WildReportReporterMediaItem) -> some View { Group { switch item { case .image(let index): WildReportEvidenceImagePreview(index: index) case .video: WildReportEvidenceVideoPreview() } } .frame(width: 108, height: 108) .clipShape(RoundedRectangle(cornerRadius: 10)) } private func evidenceSectionHeader(icon: String, title: String, value: String, action: @escaping () -> Void) -> some View { Button(action: action) { HStack(spacing: 14) { Image(systemName: icon) .font(.system(size: 22, weight: .semibold)) .foregroundStyle(AppDesign.primary) .frame(width: 50, height: 50) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 12)) Text(title) .font(.system(size: 20, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) Spacer() Text(value) .font(.system(size: 18, weight: .medium)) .foregroundStyle(AppDesign.textSecondary) .lineLimit(1) Image(systemName: "chevron.right") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(Color(hex: 0xB6BECA)) } .padding(.horizontal, 18) .padding(.top, 14) .padding(.bottom, 10) } .buttonStyle(.plain) } private var locationPreview: some View { WildReportEvidenceMapPreview( scenicName: viewModel.scenicAreaName, detailAddress: viewModel.detailAddress ) .frame(height: 98) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal, 18) .padding(.bottom, 16) } private var timelineCard: some View { VStack(alignment: .leading, spacing: 0) { ForEach(Array(viewModel.progressSteps.enumerated()), id: \.element.id) { index, step in timelineRow(step, isLast: index == viewModel.progressSteps.count - 1) } } .padding(.horizontal, 18) .padding(.vertical, 18) .background(.white, in: RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(AppDesign.border, lineWidth: 1) ) } private func timelineRow(_ step: WildReportProgressStep, isLast: Bool) -> some View { HStack(alignment: .top, spacing: 12) { VStack(spacing: 0) { timelineNode(for: step.state) if !isLast { Rectangle() .fill(step.state == .pending ? Color(hex: 0xD1D5DB).opacity(0.8) : AppDesign.primary) .frame(width: step.state == .pending ? 1 : 2) .frame(maxHeight: .infinity) .padding(.vertical, 4) } } .frame(width: 24) VStack(alignment: .leading, spacing: 4) { Text(step.title) .font(.system(size: 16, weight: step.state == .current ? .bold : .semibold)) .foregroundStyle(step.state == .pending ? Color(hex: 0x9CA3AF) : AppDesign.textPrimary) if step.state != .pending && !step.timeText.isEmpty { Text(step.timeText) .font(.system(size: 13)) .foregroundStyle(step.state == .current ? AppDesign.primary : AppDesign.textSecondary) } } .padding(.bottom, isLast ? 0 : 18) Spacer(minLength: 0) } } private func timelineNode(for state: WildReportProgressStepState) -> some View { ZStack { switch state { case .completed: Circle() .fill(AppDesign.primary) .frame(width: 24, height: 24) Image(systemName: "checkmark") .font(.system(size: 11, weight: .bold)) .foregroundStyle(.white) case .current: Circle() .stroke(AppDesign.primary, lineWidth: 2) .frame(width: 24, height: 24) Circle() .fill(AppDesign.primary) .frame(width: 10, height: 10) case .pending: Circle() .stroke(Color(hex: 0xD1D5DB), lineWidth: 2) .frame(width: 24, height: 24) } } } private var bottomActionBar: some View { HStack(spacing: 12) { NavigationLink { WildReportSupplementEvidenceView( reportID: viewModel.record.id, homeViewModel: homeViewModel ) } label: { HStack(spacing: 8) { Image(systemName: "doc.badge.plus") .font(.system(size: 18, weight: .semibold)) Text("补充证据") .font(.system(size: 17, weight: .bold)) } .foregroundStyle(AppDesign.primary) .frame(maxWidth: .infinity) .frame(height: 52) .background(.white, in: RoundedRectangle(cornerRadius: 14)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(AppDesign.primary, lineWidth: 1.2) ) } .buttonStyle(.plain) NavigationLink { WildReportRiskMapView( record: viewModel.record, riskPoints: homeViewModel.riskPoints ) } label: { HStack(spacing: 8) { Image(systemName: "map.fill") .font(.system(size: 18, weight: .semibold)) Text("查看附近风险地图") .font(.system(size: 17, weight: .bold)) .lineLimit(1) .minimumScaleFactor(0.8) } .foregroundStyle(.white) .frame(maxWidth: .infinity) .frame(height: 52) .background(AppDesign.primary, in: RoundedRectangle(cornerRadius: 14)) } .buttonStyle(.plain) } .padding(.horizontal, 16) .padding(.top, 10) .padding(.bottom, 12) .background(.white.opacity(0.96)) .overlay(alignment: .top) { Rectangle() .fill(Color.black.opacity(0.06)) .frame(height: 0.5) } } private var contentMaxWidth: CGFloat { horizontalSizeClass == .regular ? 760 : .infinity } } private struct WildReportEvidenceImagePreview: View { let index: Int private var gradientColors: [Color] { switch index { case 1: return [Color(hex: 0xD7E8F8), Color(hex: 0x7E9CB4)] case 2: return [Color(hex: 0xBFD9EE), Color(hex: 0x435B70)] default: return [Color(hex: 0xC7DFEC), Color(hex: 0x2D3D4B)] } } var body: some View { ZStack(alignment: .bottomTrailing) { LinearGradient( colors: gradientColors, startPoint: .topLeading, endPoint: .bottomTrailing ) Image(systemName: "mountain.2.fill") .font(.system(size: 52, weight: .regular)) .foregroundStyle(.white.opacity(0.32)) .offset(x: -16, y: -8) Image(systemName: "camera.fill") .font(.system(size: 22, weight: .semibold)) .foregroundStyle(.white.opacity(0.9)) .offset(x: -10, y: -18) Image(systemName: "person.fill") .font(.system(size: 44, weight: .semibold)) .foregroundStyle(Color(hex: 0x101827).opacity(0.78)) .offset(x: -24 + CGFloat(index * 12), y: 10) } .clipped() } } private struct WildReportEvidenceVideoPreview: View { var body: some View { ZStack { WildReportEvidenceImagePreview(index: 2) Circle() .stroke(.white, lineWidth: 2.5) .frame(width: 42, height: 42) .background(.black.opacity(0.08), in: Circle()) Image(systemName: "play.fill") .font(.system(size: 18, weight: .bold)) .foregroundStyle(.white) .offset(x: 2) Text("00:32") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.white) .shadow(color: .black.opacity(0.25), radius: 4, y: 2) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) .padding(10) } .clipped() } } private struct WildReportEvidenceMapPreview: View { let scenicName: String let detailAddress: String var body: some View { GeometryReader { proxy in ZStack { LinearGradient( colors: [Color(hex: 0xE1F4D8), Color(hex: 0xBDE6C7)], startPoint: .topLeading, endPoint: .bottomTrailing ) mapPath(in: proxy.size) .stroke( Color(hex: 0x65A6D7).opacity(0.75), style: StrokeStyle(lineWidth: 1.4, dash: [5, 5]) ) mapPathSecondary(in: proxy.size) .stroke( Color.white.opacity(0.88), style: StrokeStyle(lineWidth: 5, lineCap: .round) ) mapPathSecondary(in: proxy.size) .stroke(Color(hex: 0x99C59C), lineWidth: 1) Text(detailAddress.replacingOccurrences(of: "附近", with: "")) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(Color(hex: 0x64748B)) .position(x: proxy.size.width * 0.47, y: proxy.size.height * 0.32) Text(scenicName) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(Color(hex: 0x418C48)) .position(x: proxy.size.width * 0.78, y: proxy.size.height * 0.58) ZStack { Circle() .fill(Color(hex: 0xEF4444).opacity(0.18)) .frame(width: 64, height: 64) Circle() .fill(Color(hex: 0xEF4444).opacity(0.22)) .frame(width: 44, height: 44) Image(systemName: "mappin.circle.fill") .font(.system(size: 36, weight: .semibold)) .foregroundStyle(Color(hex: 0xEF4444), .white) } .position(x: proxy.size.width * 0.53, y: proxy.size.height * 0.54) } } } private func mapPath(in size: CGSize) -> Path { var path = Path() path.move(to: CGPoint(x: size.width * 0.05, y: size.height * 0.32)) path.addCurve( to: CGPoint(x: size.width * 0.95, y: size.height * 0.22), control1: CGPoint(x: size.width * 0.28, y: size.height * 0.62), control2: CGPoint(x: size.width * 0.56, y: size.height * 0.02) ) return path } private func mapPathSecondary(in size: CGSize) -> Path { var path = Path() path.move(to: CGPoint(x: size.width * 0.08, y: size.height * 0.72)) path.addCurve( to: CGPoint(x: size.width * 0.88, y: size.height * 0.78), control1: CGPoint(x: size.width * 0.30, y: size.height * 0.42), control2: CGPoint(x: size.width * 0.56, y: size.height * 0.92) ) return path } } struct WildReportSupplementEvidenceView: View { let reportID: String @ObservedObject var homeViewModel: WildPhotographerReportHomeViewModel @StateObject private var viewModel: WildReportSupplementEvidenceViewModel @Environment(\.dismiss) private var dismiss @Environment(\.horizontalSizeClass) private var horizontalSizeClass @EnvironmentObject private var toastCenter: ToastCenter init(reportID: String, homeViewModel: WildPhotographerReportHomeViewModel) { self.reportID = reportID self.homeViewModel = homeViewModel _viewModel = StateObject( wrappedValue: WildReportSupplementEvidenceViewModel( reportID: reportID, homeViewModel: homeViewModel ) ) } var body: some View { ScrollView { VStack(spacing: 14) { supplementTextCard supplementUploadCard( icon: "photo.fill", title: "上传图片", subtitle: "支持 JPG、PNG 格式,最多 9 张", attachments: $viewModel.images, maxCount: 9, onAdd: viewModel.addImage ) supplementUploadCard( icon: "video.fill", title: "上传视频", subtitle: "支持 MP4 格式,最多 3 段", attachments: $viewModel.videos, maxCount: 3, onAdd: viewModel.addVideo ) submitButton } .frame(maxWidth: contentMaxWidth) .padding(.horizontal, 18) .padding(.top, 16) .padding(.bottom, 28) } .navigationTitle("补充证据") .navigationBarTitleDisplayMode(.inline) .background(Color(hex: 0xF8FAFF).ignoresSafeArea()) .onChange(of: viewModel.validationMessage) { message in guard let message else { return } toastCenter.show(message) viewModel.validationMessage = nil } .onChange(of: viewModel.didSubmit) { didSubmit in guard didSubmit else { return } dismiss() } .onChange(of: viewModel.text) { newValue in viewModel.updateText(newValue) } } private var supplementTextCard: some View { VStack(alignment: .leading, spacing: 10) { Text("补充说明") .font(.system(size: 17, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) ZStack(alignment: .topLeading) { TextEditor(text: $viewModel.text) .font(.system(size: 15)) .foregroundStyle(AppDesign.textPrimary) .padding(.horizontal, 8) .padding(.vertical, 10) .frame(minHeight: 112) .scrollContentBackground(.hidden) if viewModel.text.isEmpty { Text("请补充新的线索说明...") .font(.system(size: 15)) .foregroundStyle(Color(hex: 0x9CA3AF)) .padding(.horizontal, 14) .padding(.vertical, 18) .allowsHitTesting(false) } Text("\(viewModel.text.count)/500") .font(.system(size: 14)) .foregroundStyle(Color(hex: 0x6B7280)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) .padding(.trailing, 12) .padding(.bottom, 12) .allowsHitTesting(false) } .background(Color(hex: 0xFBFCFE), in: RoundedRectangle(cornerRadius: 12)) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(AppDesign.inputBorder, lineWidth: 1) ) } .padding(16) .background(.white, in: RoundedRectangle(cornerRadius: 16)) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(AppDesign.border, lineWidth: 1) ) } private func supplementUploadCard( icon: String, title: String, subtitle: String, attachments: Binding<[WildReportAttachment]>, maxCount: Int, onAdd: @escaping () -> Void ) -> some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 10) { Image(systemName: icon) .font(.system(size: 22, weight: .semibold)) .foregroundStyle(AppDesign.primary) VStack(alignment: .leading, spacing: 4) { Text(title) .font(.system(size: 16, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) Text(subtitle) .font(.system(size: 12)) .foregroundStyle(AppDesign.textSecondary) } } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(attachments.wrappedValue) { attachment in supplementAttachmentTile(attachment.title) { attachments.wrappedValue.removeAll { $0.id == attachment.id } } } if attachments.wrappedValue.count < maxCount { supplementAddTile(action: onAdd) } } } } .padding(16) .background(.white, in: RoundedRectangle(cornerRadius: 16)) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(AppDesign.border, lineWidth: 1) ) } private func supplementAttachmentTile(_ title: String, onRemove: @escaping () -> Void) -> some View { ZStack(alignment: .topTrailing) { RoundedRectangle(cornerRadius: 10) .fill(AppDesign.primarySoft) .frame(width: 72, height: 72) .overlay { Text(title) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(AppDesign.primary) .multilineTextAlignment(.center) .padding(6) } Button(action: onRemove) { Image(systemName: "xmark.circle.fill") .font(.system(size: 18)) .foregroundStyle(Color(hex: 0x6B7280)) .background(Circle().fill(.white)) } .offset(x: 6, y: -6) } } private func supplementAddTile(action: @escaping () -> Void) -> some View { Button(action: action) { RoundedRectangle(cornerRadius: 10) .stroke(AppDesign.primary, style: StrokeStyle(lineWidth: 1.2, dash: [4, 4])) .frame(width: 72, height: 72) .overlay { Image(systemName: "plus") .font(.system(size: 22, weight: .semibold)) .foregroundStyle(AppDesign.primary) } } .buttonStyle(.plain) } private var submitButton: some View { Button { viewModel.submit() } label: { Text("提交补充证据") .font(.system(size: 18, weight: .bold)) .foregroundStyle(.white) .frame(maxWidth: .infinity) .frame(height: 52) .background(AppDesign.primary, in: RoundedRectangle(cornerRadius: 14)) } .buttonStyle(.plain) } private var contentMaxWidth: CGFloat { horizontalSizeClass == .regular ? 760 : .infinity } }