import SwiftUI import AVFoundation import AudioToolbox import Combine import CoreData import QRCode // 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] = [] var body: some View { ZStack { // 相机权限检查 if scannerViewModel.cameraAuthorizationStatus == .authorized { // 相机预览层 CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) .ignoresSafeArea() // 扫描界面覆盖层 ScanningOverlayView( showPreviewPause: showPreviewPause, detectedCodesCount: scannerViewModel.detectedCodes.count, onImageDecode: { showImagePicker = true } ) // 条码位置标记覆盖层 if showPreviewPause && (!scannerViewModel.detectedCodes.isEmpty || !decodedImageCodes.isEmpty) { CodePositionOverlay( detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes, previewLayer: previewLayer, onCodeSelected: handleCodeSelection ) } // 测试按钮(调试用) if showPreviewPause && (scannerViewModel.detectedCodes.count + decodedImageCodes.count) == 1 { let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first if let code = singleCode { TestAutoSelectButton( detectedCode: code, onSelect: handleCodeSelection ) } } } 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 { QRCodeDetailView(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.dataType = DataType.qrcode.rawValue historyItem.dataSource = DataSource.scanned.rawValue historyItem.createdAt = Date() historyItem.isFavorite = false // 根据条码类型设置相应的类型字段 if detectedCode.type.lowercased().contains("qr") || detectedCode.type.lowercased().contains("二维码") { // 尝试解析二维码类型 let parsedData = QRCodeParser.parseQRCode(detectedCode.content) historyItem.qrCodeType = parsedData.type.rawValue } else { // 条形码类型 historyItem.barcodeType = detectedCode.type } // 保存到 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() logInfo("🔍 开始解码图片", className: "ScannerView") // 在后台线程进行解码 DispatchQueue.global(qos: .userInitiated).async { [self] in // 使用QRCode库检测二维码 let detected = QRCode.DetectQRCodes(in: image) if detected.count > 0 { logInfo("✅ 检测到 \(detected.count) 个二维码", className: "ScannerView") let results = detected.enumerated().map { index, qrCode in DetectedCode( type: "QR Code", content: qrCode.messageString ?? "未知内容", bounds: qrCode.bounds ) } DispatchQueue.main.async { self.decodedImageCodes = results self.isDecodingImage = false self.pauseForPreview() logInfo("✅ 图片解码完成,共 \(results.count) 个结果", className: "ScannerView") // 如果只有一个二维码,自动选择并跳转 if results.count == 1 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.handleCodeSelection(results[0]) } } } } else { DispatchQueue.main.async { self.isDecodingImage = false logWarning("❌ 图片中未检测到二维码", className: "ScannerView") } } } } /// 重置图片解码状态 private func resetImageDecodeState() { decodedImageCodes.removeAll() } } // 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) } } } #if DEBUG struct ScannerView_Previews: PreviewProvider { static var previews: some View { NavigationView { ScannerView() } } } #endif