import SwiftUI import AVFoundation import AudioToolbox import Combine import CoreData import QRCode import Vision // MARK: - 主扫描视图 struct ScannerView: View { @StateObject private var scannerViewModel = ScannerViewModel() @State private var showPreviewPause = false @State private var screenOrientation = UIDevice.current.orientation @State private var previewLayer: AVCaptureVideoPreviewLayer? @State private var navigateToDetail = false @State private var selectedHistoryItem: HistoryItem? // 图片解码相关状态 @State private var showImagePicker = false @State private var isDecodingImage = false @State private var decodedImageCodes: [DetectedCode] = [] @State private var showDecodeFailure = false @State private var decodeFailureMessage = "" var body: some View { ZStack { // 相机权限检查 if scannerViewModel.cameraAuthorizationStatus == .authorized { // 相机预览层 CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) .ignoresSafeArea() // 扫描界面覆盖层 ScanningOverlayView( showPreviewPause: showPreviewPause && ((scannerViewModel.detectedCodes.count > 1) || (decodedImageCodes.count > 1)), detectedCodesCount: scannerViewModel.detectedCodes.count + decodedImageCodes.count, onImageDecode: { showImagePicker = true } ) // 条码位置标记覆盖层 - 只在需要用户选择时显示 if showPreviewPause && ((!scannerViewModel.detectedCodes.isEmpty && scannerViewModel.detectedCodes.count > 1) || (!decodedImageCodes.isEmpty && decodedImageCodes.count > 1)) { CodePositionOverlay( detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes, previewLayer: previewLayer, onCodeSelected: handleCodeSelection ) } // 测试按钮(调试用)- 只在需要用户选择时显示 if showPreviewPause && ((scannerViewModel.detectedCodes.count > 1) || (decodedImageCodes.count > 1)) { let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first if let code = singleCode { TestAutoSelectButton( detectedCode: code, onSelect: handleCodeSelection ) } } // 解码失败提示 if showDecodeFailure { DecodeFailureOverlay( message: decodeFailureMessage, onDismiss: { showDecodeFailure = false } ) } } else { // 权限未授权时的UI CameraPermissionView( authorizationStatus: scannerViewModel.cameraAuthorizationStatus, onRequestPermission: { scannerViewModel.refreshCameraPermission() }, onOpenSettings: { scannerViewModel.openSettings() } ) } } .navigationTitle("扫描器") .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(false) .sheet(isPresented: $showImagePicker) { ImagePicker(onImageSelected: handleImageDecodeResult) } .toolbar { ToolbarItem(placement: .navigationBarLeading) { // 手电筒按钮 - 只在相机权限已授权时显示 if scannerViewModel.cameraAuthorizationStatus == .authorized && scannerViewModel.isTorchAvailable { Button(action: { logInfo("🔦 用户点击手电筒按钮", className: "ScannerView") // 添加触觉反馈 let impactFeedback = UIImpactFeedbackGenerator(style: .medium) impactFeedback.impactOccurred() scannerViewModel.toggleTorch() }) { Image(systemName: scannerViewModel.isTorchOn ? "bolt.fill" : "bolt") .font(.system(size: 18, weight: .semibold)) .foregroundColor(scannerViewModel.isTorchOn ? .yellow : .blue) } } } ToolbarItem(placement: .navigationBarTrailing) { // 重新扫描按钮 - 只在预览暂停状态时显示 if showPreviewPause { Button(action: { logInfo("🔄 用户点击工具栏重新扫描按钮", className: "ScannerView") // 添加触觉反馈 let impactFeedback = UIImpactFeedbackGenerator(style: .medium) impactFeedback.impactOccurred() resetToScanning() }) { HStack(spacing: 6) { Image(systemName: "arrow.clockwise") .font(.system(size: 16, weight: .semibold)) Text("rescan_button".localized) .font(.system(size: 14, weight: .medium)) } .foregroundColor(.blue) } } } } .onAppear { // 只有在相机权限已授权时才启动扫描 if scannerViewModel.cameraAuthorizationStatus == .authorized { scannerViewModel.startScanning() } } .onDisappear { scannerViewModel.stopScanning() // 确保退出时关闭手电筒 if scannerViewModel.isTorchOn { scannerViewModel.turnOffTorch() } } .alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) { Button("OK") { } } message: { Text("scan_error_message".localized) } .onReceive(scannerViewModel.$detectedCodes) { codes in handleDetectedCodes(codes) } .onReceive(scannerViewModel.$cameraAuthorizationStatus) { status in if status == .authorized { logInfo("🎯 相机权限已授权,启动扫描", className: "ScannerView") scannerViewModel.startScanning() } } .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in handleOrientationChange() } .background( NavigationLink( destination: Group { if let historyItem = selectedHistoryItem { // 根据数据类型跳转到相应的详情页 if historyItem.dataType == DataType.qrcode.rawValue { QRCodeDetailView(historyItem: historyItem) .onDisappear { // 从详情页返回时,重新开始扫描 logInfo("🔄 从二维码详情页返回,重新开始扫描", className: "ScannerView") resetToScanning() } } else { BarcodeDetailView(historyItem: historyItem) .onDisappear { // 从详情页返回时,重新开始扫描 logInfo("🔄 从条形码详情页返回,重新开始扫描", className: "ScannerView") resetToScanning() } } } }, isActive: $navigateToDetail ) { EmptyView() } ) } // MARK: - 私有方法 private func handleDetectedCodes(_ codes: [DetectedCode]) { guard !codes.isEmpty else { return } logInfo("检测到条码数量: \(codes.count)", className: "ScannerView") if codes.count == 1 { logInfo("单个条码,准备自动选择", className: "ScannerView") pauseForPreview() autoSelectSingleCode(code: codes[0]) } else { logInfo("多个条码,等待用户选择", className: "ScannerView") pauseForPreview() } } private func handleOrientationChange() { screenOrientation = UIDevice.current.orientation logInfo("Screen orientation changed to: \(screenOrientation.rawValue)", className: "ScannerView") } private func handleCodeSelection(_ selectedCode: DetectedCode) { logInfo("🎯 ScannerView 收到条码选择回调", className: "ScannerView") logInfo(" 选择的条码ID: \(selectedCode.id)", className: "ScannerView") logInfo(" 选择的条码类型: \(selectedCode.type)", className: "ScannerView") logInfo(" 选择的条码内容: \(selectedCode.content)", className: "ScannerView") logInfo(" 选择的条码位置: \(selectedCode.bounds)", className: "ScannerView") // 停止扫描功能,避免在详情页面继续扫描 scannerViewModel.stopScanning() logInfo("🛑 已停止扫描功能", className: "ScannerView") // 创建 HistoryItem 并保存到 Core Data let historyItem = createHistoryItem(from: selectedCode) // 延迟一小段时间确保扫描完全停止后再跳转 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // 设置选中的历史记录项并导航到详情页 self.selectedHistoryItem = historyItem self.navigateToDetail = true } // 发送通知(保持向后兼容) let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)" logInfo(" 格式化结果: \(formattedResult)", className: "ScannerView") NotificationCenter.default.post(name: .scannerDidScanCode, object: formattedResult) } private func createHistoryItem(from detectedCode: DetectedCode) -> HistoryItem { let context = CoreDataManager.shared.container.viewContext let historyItem = HistoryItem(context: context) historyItem.id = UUID() historyItem.content = detectedCode.content historyItem.dataSource = DataSource.scanned.rawValue historyItem.createdAt = Date() historyItem.isFavorite = false // 根据条码类型设置相应的类型字段 let isQRCode = detectedCode.type.lowercased().contains("qr") || detectedCode.type.lowercased().contains("二维码") || detectedCode.type.lowercased().contains("data matrix") || detectedCode.type.lowercased().contains("aztec") if isQRCode { // 二维码类型 historyItem.dataType = DataType.qrcode.rawValue // 尝试解析二维码类型 let parsedData = QRCodeParser.parseQRCode(detectedCode.content) historyItem.qrCodeType = parsedData.type.rawValue historyItem.barcodeType = nil // 清空条形码类型 logInfo("📱 创建二维码历史记录,类型: \(detectedCode.type)", className: "ScannerView") } else { // 条形码类型 historyItem.dataType = DataType.barcode.rawValue historyItem.barcodeType = detectedCode.type historyItem.qrCodeType = nil // 清空二维码类型 logInfo("📊 创建条形码历史记录,类型: \(detectedCode.type)", className: "ScannerView") } // 保存到 Core Data CoreDataManager.shared.addHistoryItem(historyItem) logInfo("✅ 已创建并保存历史记录项", className: "ScannerView") return historyItem } private func pauseForPreview() { showPreviewPause = true // 暂停相机功能,防止在预览暂停时继续拍照 scannerViewModel.pauseCamera() } private func resetToScanning() { logInfo("🔄 ScannerView 开始重置到扫描状态", className: "ScannerView") // 重置UI状态 showPreviewPause = false // 重置扫描状态 scannerViewModel.resetDetection() // 重置图片解码状态 resetImageDecodeState() // 恢复相机功能并重新开始扫描 scannerViewModel.resumeCamera() logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView") } private func autoSelectSingleCode(code: DetectedCode) { logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView") DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { let totalCodes = self.scannerViewModel.detectedCodes.count + self.decodedImageCodes.count guard self.showPreviewPause && totalCodes == 1 else { logInfo("条件不满足,取消自动选择", className: "ScannerView") return } logInfo("条件满足,执行自动选择", className: "ScannerView") self.handleCodeSelection(code) } } // MARK: - 图片解码相关方法 /// 处理图片解码结果 private func handleImageDecodeResult(_ image: UIImage) { isDecodingImage = true decodedImageCodes.removeAll() showDecodeFailure = false decodeFailureMessage = "" logInfo("🔍 开始解码图片", className: "ScannerView") // 在后台线程进行解码 DispatchQueue.global(qos: .userInitiated).async { [self] in var allResults: [DetectedCode] = [] // 使用Vision框架检测所有条码(包括二维码和条形码) if let cgImage = image.cgImage { let barcodeResults = detectBarcodes(in: cgImage) if !barcodeResults.isEmpty { logInfo("✅ 检测到 \(barcodeResults.count) 个条码", className: "ScannerView") allResults.append(contentsOf: barcodeResults) } } // 如果没有检测到条码,尝试使用QRCode库作为备用方案 if allResults.isEmpty { let detectedQR = QRCode.DetectQRCodes(in: image) if detectedQR.count > 0 { logInfo("✅ 使用QRCode库检测到 \(detectedQR.count) 个二维码", className: "ScannerView") let qrResults = detectedQR.enumerated().map { index, qrCode in DetectedCode( type: "QR Code", content: qrCode.messageString ?? "未知内容", bounds: qrCode.bounds ) } allResults.append(contentsOf: qrResults) } } DispatchQueue.main.async { if !allResults.isEmpty { // 去重:移除相同内容的重复条码 let uniqueResults = self.removeDuplicateCodes(allResults) self.decodedImageCodes = uniqueResults self.isDecodingImage = false logInfo("✅ 图片解码完成,去重后共 \(uniqueResults.count) 个结果", className: "ScannerView") // 图片解码结果直接跳转,不显示选择点 if uniqueResults.count == 1 { // 直接处理单个条码,跳转到结果页 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.handleCodeSelection(uniqueResults[0]) } } else if uniqueResults.count > 1 { // 多个条码时,显示选择点让用户选择 self.pauseForPreview() logInfo("📱 检测到多个条码,显示选择点", className: "ScannerView") } } else { self.isDecodingImage = false self.decodeFailureMessage = "图片中未检测到二维码或条形码" self.showDecodeFailure = true logWarning("❌ 图片中未检测到二维码或条形码", className: "ScannerView") } } } } /// 重置图片解码状态 private func resetImageDecodeState() { decodedImageCodes.removeAll() showDecodeFailure = false decodeFailureMessage = "" } /// 使用Vision框架检测条形码 private func detectBarcodes(in cgImage: CGImage) -> [DetectedCode] { let request = VNDetectBarcodesRequest { request, error in if let error = error { logWarning("条形码检测错误: \(error.localizedDescription)", className: "ScannerView") return } } // 设置条形码类型 request.symbologies = [ .ean8, .ean13, .upce, .code39, .code39Checksum, .code39FullASCII, .code39FullASCIIChecksum, .code93, .code93i, .code128, .itf14, .pdf417, .qr, .dataMatrix, .aztec ] let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) do { try handler.perform([request]) let results = request.results ?? [] guard !results.isEmpty else { return [] } return results.enumerated().map { index, observation in let barcodeType = getBarcodeTypeString(from: observation.symbology) let content = observation.payloadStringValue ?? "未知内容" logInfo("检测到条形码 #\(index + 1): 类型=\(barcodeType), 内容=\(content)", className: "ScannerView") return DetectedCode( type: barcodeType, content: content, bounds: observation.boundingBox ) } } catch { logWarning("条形码检测请求失败: \(error.localizedDescription)", className: "ScannerView") return [] } } /// 获取条形码类型的可读字符串 private func getBarcodeTypeString(from symbology: VNBarcodeSymbology) -> String { switch symbology { case .ean8: return "EAN-8" case .ean13: return "EAN-13" case .upce: return "UPC-E" case .code39: return "Code 39" case .code39Checksum: return "Code 39 (Checksum)" case .code39FullASCII: return "Code 39 (Full ASCII)" case .code39FullASCIIChecksum: return "Code 39 (Full ASCII + Checksum)" case .code93: return "Code 93" case .code93i: return "Code 93i" case .code128: return "Code 128" case .itf14: return "ITF-14" case .pdf417: return "PDF417" case .qr: return "QR Code" case .dataMatrix: return "Data Matrix" case .aztec: return "Aztec" default: return "Unknown Barcode" } } /// 移除重复的条码(基于内容去重) private func removeDuplicateCodes(_ codes: [DetectedCode]) -> [DetectedCode] { var uniqueCodes: [DetectedCode] = [] var seenContents: Set = [] for code in codes { if !seenContents.contains(code.content) { seenContents.insert(code.content) uniqueCodes.append(code) } else { logInfo("🔄 发现重复条码,内容: \(code.content),已跳过", className: "ScannerView") } } return uniqueCodes } } // MARK: - 图片选择器(兼容iOS 15) struct ImagePicker: UIViewControllerRepresentable { let onImageSelected: (UIImage) -> Void func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.delegate = context.coordinator picker.sourceType = .photoLibrary picker.allowsEditing = false return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { let parent: ImagePicker init(_ parent: ImagePicker) { self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let image = info[.originalImage] as? UIImage { parent.onImageSelected(image) } picker.dismiss(animated: true) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } } } // MARK: - 解码失败提示覆盖层 struct DecodeFailureOverlay: View { let message: String let onDismiss: () -> Void var body: some View { ZStack { // 半透明背景 Color.black.opacity(0.7) .ignoresSafeArea() .onTapGesture { onDismiss() } // 失败提示卡片 VStack(spacing: 20) { // 失败图标 Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 50)) .foregroundColor(.orange) // 失败标题 Text("解码失败") .font(.title2) .fontWeight(.bold) .foregroundColor(.white) // 失败消息 Text(message) .font(.body) .foregroundColor(.white.opacity(0.8)) .multilineTextAlignment(.center) .padding(.horizontal, 20) // 重试按钮 Button(action: { onDismiss() }) { HStack(spacing: 8) { Image(systemName: "arrow.clockwise") .font(.system(size: 16, weight: .semibold)) Text("重新选择图片") .font(.headline) .fontWeight(.medium) } .foregroundColor(.white) .padding(.horizontal, 20) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 12) .fill(Color.blue.opacity(0.8)) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.blue, lineWidth: 1) ) ) } .buttonStyle(PlainButtonStyle()) } .padding(30) .background( RoundedRectangle(cornerRadius: 20) .fill(Color(.systemGray6).opacity(0.9)) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(Color.white.opacity(0.2), lineWidth: 1) ) ) .shadow(color: .black.opacity(0.3), radius: 20, x: 0, y: 10) } .zIndex(2000) // 确保在最上层 .transition(.opacity.combined(with: .scale)) } } #if DEBUG struct ScannerView_Previews: PreviewProvider { static var previews: some View { NavigationView { ScannerView() } } } #endif