import SwiftUI import AVFoundation import AudioToolbox import Combine // MARK: - 主扫描视图 struct ScannerView: View { @StateObject private var scannerViewModel = ScannerViewModel() @State private var showPreviewPause = false @State private var selectedScanningStyle: ScanningLineStyle = .modern @State private var screenOrientation = UIDevice.current.orientation @State private var previewLayer: AVCaptureVideoPreviewLayer? @Environment(\.dismiss) private var dismiss var body: some View { ZStack { // 相机权限检查 if scannerViewModel.cameraAuthorizationStatus == .authorized { // 相机预览层 CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) .ignoresSafeArea() // 扫描界面覆盖层 ScanningOverlayView( showPreviewPause: showPreviewPause, selectedStyle: $selectedScanningStyle, detectedCodesCount: scannerViewModel.detectedCodes.count, onClose: { dismiss() } ) // 条码位置标记覆盖层 if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { CodePositionOverlay( detectedCodes: scannerViewModel.detectedCodes, previewLayer: previewLayer, onCodeSelected: handleCodeSelection, onRescan: resetToScanning ) } // 测试按钮(调试用) if showPreviewPause && scannerViewModel.detectedCodes.count == 1 { TestAutoSelectButton( detectedCode: scannerViewModel.detectedCodes[0], onSelect: handleCodeSelection ) } } else { // 权限未授权时的UI CameraPermissionView( authorizationStatus: scannerViewModel.cameraAuthorizationStatus, onRequestPermission: { scannerViewModel.refreshCameraPermission() }, onOpenSettings: { scannerViewModel.openSettings() } ) } } .onAppear { // 只有在相机权限已授权时才启动扫描 if scannerViewModel.cameraAuthorizationStatus == .authorized { scannerViewModel.startScanning() } } .onDisappear { scannerViewModel.stopScanning() } .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() } } // 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") let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)" logInfo(" 格式化结果: \(formattedResult)", className: "ScannerView") NotificationCenter.default.post(name: .scannerDidScanCode, object: formattedResult) dismiss() } private func pauseForPreview() { showPreviewPause = true } private func resetToScanning() { logInfo("🔄 ScannerView 开始重置到扫描状态", className: "ScannerView") // 重置UI状态 showPreviewPause = false // 重置扫描状态并重新开始 scannerViewModel.resetDetection() scannerViewModel.restartScanning() // 延迟检查会话状态 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { logInfo("🔍 检查扫描会话状态", className: "ScannerView") self.scannerViewModel.checkSessionStatus() } logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView") } private func autoSelectSingleCode(code: DetectedCode) { logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView") DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { guard self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 else { logInfo("条件不满足,取消自动选择", className: "ScannerView") return } logInfo("条件满足,执行自动选择", className: "ScannerView") self.handleCodeSelection(code) } } } // MARK: - 扫描界面覆盖层 struct ScanningOverlayView: View { let showPreviewPause: Bool @Binding var selectedStyle: ScanningLineStyle let detectedCodesCount: Int let onClose: () -> Void var body: some View { VStack { Spacer() // 扫描线组件 if !showPreviewPause { ScanningLineView(style: selectedStyle) } // 提示文本 ScanningInstructionView( showPreviewPause: showPreviewPause, detectedCodesCount: detectedCodesCount ) Spacer() // 底部按钮区域 ScanningBottomButtonsView( showPreviewPause: showPreviewPause, selectedStyle: $selectedStyle, onClose: onClose ) } } } // MARK: - 扫描指令视图 struct ScanningInstructionView: View { let showPreviewPause: Bool let detectedCodesCount: Int var body: some View { if showPreviewPause { VStack(spacing: 8) { Text("detected_codes".localized) .foregroundColor(.white) .font(.headline) if detectedCodesCount == 1 { Text("auto_result_1s".localized) .foregroundColor(.green) .font(.subheadline) } else { Text("select_code_instruction".localized) .foregroundColor(.white.opacity(0.8)) .font(.subheadline) } } .padding(.top, 20) } else { Text("scan_instruction".localized) .foregroundColor(.white) .font(.headline) .padding(.top, 20) } } } // MARK: - 扫描底部按钮视图 struct ScanningBottomButtonsView: View { let showPreviewPause: Bool @Binding var selectedStyle: ScanningLineStyle let onClose: () -> Void var body: some View { VStack(spacing: 15) { // 扫描线样式选择器 if !showPreviewPause { ScanningStyleSelectorView(selectedStyle: $selectedStyle) } // 关闭按钮 - 只在非预览选择状态时显示 if !showPreviewPause { Button("close_button".localized) { onClose() } .foregroundColor(.white) .padding() .background(Color.black.opacity(0.6)) .cornerRadius(10) } } .padding(.bottom, 50) } } // MARK: - 扫描线样式选择器 struct ScanningStyleSelectorView: View { @Binding var selectedStyle: ScanningLineStyle var body: some View { HStack(spacing: 10) { ForEach(ScanningLineStyle.allCases, id: \.self) { style in Button(style.localizedName) { selectedStyle = style } .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 4) .background(selectedStyle == style ? Color.green : Color.gray.opacity(0.6)) .cornerRadius(8) .font(.caption) } } .padding(.bottom, 10) } } // MARK: - 测试自动选择按钮 struct TestAutoSelectButton: View { let detectedCode: DetectedCode let onSelect: (DetectedCode) -> Void var body: some View { VStack { HStack { Spacer() Button("test_auto_select".localized) { onSelect(detectedCode) } .foregroundColor(.white) .padding(8) .background(Color.red) .cornerRadius(8) .padding(.trailing, 20) } Spacer() } } } // MARK: - 相机预览视图 struct CameraPreviewView: UIViewRepresentable { let session: AVCaptureSession @Binding var previewLayer: AVCaptureVideoPreviewLayer? func makeUIView(context: Context) -> UIView { let view = UIView() let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) DispatchQueue.main.async { self.previewLayer = previewLayer } return view } func updateUIView(_ uiView: UIView, context: Context) { if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer { previewLayer.frame = uiView.bounds DispatchQueue.main.async { self.previewLayer = previewLayer } } } } // MARK: - 扫描器视图模型 class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate { @Published var detectedCodes: [DetectedCode] = [] @Published var showAlert = false @Published var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined @Published var showPermissionAlert = false var captureSession: AVCaptureSession! private var metadataOutput: AVCaptureMetadataOutput? override init() { super.init() checkCameraPermission() } // MARK: - 相机权限管理 private func checkCameraPermission() { logInfo("🔍 检查相机权限状态", className: "ScannerViewModel") switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: logInfo("✅ 相机权限已授权", className: "ScannerViewModel") cameraAuthorizationStatus = .authorized setupCaptureSession() case .notDetermined: logInfo("❓ 相机权限未确定,请求权限", className: "ScannerViewModel") requestCameraPermission() case .denied, .restricted: logWarning("❌ 相机权限被拒绝或受限", className: "ScannerViewModel") cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) showPermissionAlert = true @unknown default: logWarning("❓ 未知的相机权限状态", className: "ScannerViewModel") cameraAuthorizationStatus = .notDetermined } } private func requestCameraPermission() { logInfo("🔐 请求相机权限", className: "ScannerViewModel") AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in DispatchQueue.main.async { if granted { logInfo("✅ 相机权限请求成功", className: "ScannerViewModel") self?.cameraAuthorizationStatus = .authorized self?.setupCaptureSession() } else { logWarning("❌ 相机权限请求被拒绝", className: "ScannerViewModel") self?.cameraAuthorizationStatus = .denied self?.showPermissionAlert = true } } } } func openSettings() { logInfo("⚙️ 打开系统设置", className: "ScannerViewModel") if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(settingsUrl) { success in if success { logInfo("✅ 成功打开系统设置", className: "ScannerViewModel") } else { logWarning("⚠️ 打开系统设置失败", className: "ScannerViewModel") } } } } func refreshCameraPermission() { logInfo("🔍 重新检查相机权限状态", className: "ScannerViewModel") checkCameraPermission() } // MARK: - 相机设置 private func setupCaptureSession() { captureSession = AVCaptureSession() guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { showAlert = true return } let videoInput: AVCaptureDeviceInput do { videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) } catch { showAlert = true return } if captureSession.canAddInput(videoInput) { captureSession.addInput(videoInput) } else { showAlert = true return } metadataOutput = AVCaptureMetadataOutput() if let metadataOutput = metadataOutput, captureSession.canAddOutput(metadataOutput) { captureSession.addOutput(metadataOutput) metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) metadataOutput.metadataObjectTypes = [.qr, .ean8, .ean13, .code128, .code39, .upce, .pdf417, .aztec] } else { showAlert = true return } } // MARK: - 扫描控制 func startScanning() { logInfo("🔄 开始扫描", className: "ScannerViewModel") // 检查会话是否已经在运行 if captureSession?.isRunning == true { logInfo("ℹ️ 扫描会话已经在运行", className: "ScannerViewModel") return } DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } self.captureSession?.startRunning() // 检查启动状态 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { if self.captureSession?.isRunning == true { logInfo("✅ 扫描会话启动成功", className: "ScannerViewModel") } else { logWarning("⚠️ 扫描会话启动失败", className: "ScannerViewModel") } } } } func stopScanning() { logInfo("🔄 停止扫描", className: "ScannerViewModel") // 检查会话是否在运行 if captureSession?.isRunning == true { DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.captureSession?.stopRunning() DispatchQueue.main.async { logInfo("✅ 扫描会话已停止", className: "ScannerViewModel") } } } else { logInfo("ℹ️ 扫描会话已经停止", className: "ScannerViewModel") } } func resetDetection() { DispatchQueue.main.async { logInfo("🔄 重置检测状态,清空 detectedCodes", className: "ScannerViewModel") self.detectedCodes = [] } } func isSessionRunning() -> Bool { return captureSession?.isRunning == true } func checkSessionStatus() { let isRunning = captureSession?.isRunning == true logInfo("📊 扫描会话状态检查: \(isRunning ? "运行中" : "已停止")", className: "ScannerViewModel") if !isRunning { logWarning("⚠️ 扫描会话未运行,尝试重新启动", className: "ScannerViewModel") startScanning() } } func restartScanning() { logInfo("🔄 重新开始扫描", className: "ScannerViewModel") // 确保在主线程执行UI相关操作 DispatchQueue.main.async { [weak self] in guard let self = self else { return } // 先停止当前会话 if self.captureSession?.isRunning == true { logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel") self.captureSession?.stopRunning() } // 重置检测状态 self.detectedCodes = [] // 延迟后重新启动会话 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel") // 在后台线程启动会话 DispatchQueue.global(qos: .userInitiated).async { self.captureSession?.startRunning() // 检查会话状态 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { if self.captureSession?.isRunning == true { logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel") } else { logWarning("⚠️ 扫描会话启动失败,尝试重新启动", className: "ScannerViewModel") // 如果启动失败,再次尝试 DispatchQueue.global(qos: .userInitiated).async { self.captureSession?.startRunning() DispatchQueue.main.async { if self.captureSession?.isRunning == true { logInfo("✅ 扫描会话第二次尝试启动成功", className: "ScannerViewModel") } else { logError("❌ 扫描会话启动失败", className: "ScannerViewModel") } } } } } } } } } // MARK: - AVCaptureMetadataOutputObjectsDelegate func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel") // 震动反馈 AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) // 停止扫描 stopScanning() // 处理所有检测到的条码 var codes: [DetectedCode] = [] for metadataObject in metadataObjects { if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, let stringValue = readableObject.stringValue { let codeType = readableObject.type.rawValue let bounds = readableObject.bounds let detectedCode = DetectedCode( type: codeType, content: stringValue, bounds: bounds ) codes.append(detectedCode) logInfo("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)", className: "ScannerViewModel") } } logInfo("准备更新 detectedCodes,数量: \(codes.count)", className: "ScannerViewModel") // 更新检测到的条码列表 DispatchQueue.main.async { logInfo("在主线程更新 detectedCodes", className: "ScannerViewModel") self.detectedCodes = codes } } } // MARK: - 条码位置标记覆盖层 struct CodePositionOverlay: View { let detectedCodes: [DetectedCode] let previewLayer: AVCaptureVideoPreviewLayer? let onCodeSelected: (DetectedCode) -> Void let onRescan: () -> Void var body: some View { GeometryReader { geometry in ZStack { ForEach(detectedCodes) { code in CodePositionMarker( code: code, screenSize: geometry.size, previewLayer: previewLayer, onCodeSelected: onCodeSelected ) } // 调试信息:显示触摸区域边界 #if DEBUG ForEach(detectedCodes) { code in let position = calculateDebugPosition(code: code, screenSize: geometry.size) Rectangle() .stroke(Color.red, lineWidth: 1) .frame(width: 80, height: 80) .position(x: position.x, y: position.y) .opacity(0.3) } #endif // 重新扫描按钮 - 放在右上角 VStack { HStack { Spacer() Button(action: { logInfo("🔄 用户点击重新扫描按钮", className: "CodePositionOverlay") // 添加触觉反馈 let impactFeedback = UIImpactFeedbackGenerator(style: .medium) impactFeedback.impactOccurred() onRescan() }) { HStack(spacing: 8) { Image(systemName: "arrow.clockwise") .font(.system(size: 18, weight: .semibold)) .rotationEffect(.degrees(0)) .animation(.easeInOut(duration: 0.3), value: true) Text("rescan_button".localized) .font(.system(size: 15, weight: .semibold)) } .foregroundColor(.white) .padding(.horizontal, 20) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 25) .fill(Color.blue.opacity(0.9)) .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2) ) } .buttonStyle(RescanButtonStyle()) .padding(.trailing, 25) .padding(.top, 25) // 调试按钮:检查会话状态 #if DEBUG Button(action: { logInfo("🔍 调试:检查扫描会话状态", className: "CodePositionOverlay") // 这里可以添加会话状态检查逻辑 }) { Image(systemName: "info.circle") .font(.system(size: 16, weight: .medium)) .foregroundColor(.yellow) .padding(8) .background(Color.black.opacity(0.6)) .clipShape(Circle()) } .padding(.trailing, 10) .padding(.top, 25) #endif } Spacer() } } } .allowsHitTesting(true) .contentShape(Rectangle()) // 确保整个区域都可以接收触摸事件 .zIndex(1000) // 确保在最上层,不被其他视图遮挡 } // 调试用的位置计算方法 private func calculateDebugPosition(code: DetectedCode, screenSize: CGSize) -> CGPoint { guard let previewLayer = previewLayer else { return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } let metadataObject = code.bounds let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint( x: metadataObject.midX, y: metadataObject.midY )) let clampedX = max(40, min(screenSize.width - 40, convertedPoint.x)) let clampedY = max(40, min(screenSize.height - 40, convertedPoint.y)) return CGPoint(x: clampedX, y: clampedY) } } // MARK: - 单个条码位置标记 struct CodePositionMarker: View { let code: DetectedCode let screenSize: CGSize let previewLayer: AVCaptureVideoPreviewLayer? let onCodeSelected: (DetectedCode) -> Void var body: some View { GeometryReader { geometry in let position = calculatePosition(screenSize: geometry.size) ZStack { // 触摸区域背景(透明,但可以接收触摸事件) Circle() .fill(Color.clear) .frame(width: 80, height: 80) .contentShape(Circle()) .onTapGesture { logDebug("🎯 CodePositionMarker 被点击!", className: "CodePositionMarker") logDebug(" 条码ID: \(code.id)", className: "CodePositionMarker") logDebug(" 条码类型: \(code.type)", className: "CodePositionMarker") logDebug(" 条码内容: \(code.content)", className: "CodePositionMarker") logDebug(" 点击位置: x=\(position.x), y=\(position.y)", className: "CodePositionMarker") // 添加触觉反馈 let impactFeedback = UIImpactFeedbackGenerator(style: .medium) impactFeedback.impactOccurred() onCodeSelected(code) } // 外圈 Circle() .stroke(Color.green, lineWidth: 3) .frame(width: 40, height: 40) // 内圈 Circle() .fill(Color.green.opacity(0.3)) .frame(width: 20, height: 20) // 中心点 Circle() .fill(Color.green) .frame(width: 6, height: 6) } .position(x: position.x, y: position.y) .zIndex(1001) // 确保触摸区域在最上层 .onAppear { logDebug("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)", className: "CodePositionMarker") logDebug("Screen size: \(geometry.size)", className: "CodePositionMarker") logDebug("Code bounds: \(code.bounds)", className: "CodePositionMarker") } } } private func calculatePosition(screenSize: CGSize) -> CGPoint { guard let previewLayer = previewLayer else { logWarning("No preview layer available, using screen center", className: "CodePositionMarker") return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } guard previewLayer.session?.isRunning == true else { logWarning("Preview layer session not running, using screen center", className: "CodePositionMarker") return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } let metadataObject = code.bounds let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint( x: metadataObject.midX, y: metadataObject.midY )) guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else { logWarning("Invalid converted point: \(convertedPoint), using screen center", className: "CodePositionMarker") return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x)) let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y)) logDebug("AVFoundation bounds: \(code.bounds)", className: "CodePositionMarker") logDebug("Converted point: \(convertedPoint)", className: "CodePositionMarker") logDebug("Screen size: \(screenSize)", className: "CodePositionMarker") logDebug("Clamped: x=\(clampedX), y=\(clampedY)", className: "CodePositionMarker") return CGPoint(x: clampedX, y: clampedY) } } // MARK: - 重新扫描按钮样式 struct RescanButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.95 : 1.0) .opacity(configuration.isPressed ? 0.8 : 1.0) .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) } } // MARK: - 扫描线动画修饰符 struct ScanningLineModifier: ViewModifier { @State private var isAnimating = false func body(content: Content) -> some View { content .offset(y: isAnimating ? 150 : -150) .onAppear { withAnimation( Animation.linear(duration: 2) .repeatForever(autoreverses: false) ) { isAnimating = true } } } } // MARK: - 脉冲动画修饰符 struct PulseAnimationModifier: ViewModifier { @State private var isPulsing = false func body(content: Content) -> some View { content .scaleEffect(isPulsing ? 1.5 : 1.0) .opacity(isPulsing ? 0.0 : 0.8) .onAppear { withAnimation( Animation.easeInOut(duration: 1.5) .repeatForever(autoreverses: false) ) { isPulsing = true } } } } // MARK: - 扫描线样式枚举 enum ScanningLineStyle: String, CaseIterable { case modern = "style_modern" case classic = "style_classic" case neon = "style_neon" case minimal = "style_minimal" case retro = "style_retro" var localizedName: String { switch self { case .modern: return "style_modern".localized case .classic: return "style_classic".localized case .neon: return "style_neon".localized case .minimal: return "style_minimal".localized case .retro: return "style_retro".localized } } } // MARK: - 扫描线视图 struct ScanningLineView: View { let style: ScanningLineStyle var body: some View { Group { switch style { case .modern: ModernScanningLine() case .classic: ClassicScanningLine() case .neon: NeonScanningLine() case .minimal: MinimalScanningLine() case .retro: RetroScanningLine() } } } } // MARK: - 现代扫描线 struct ModernScanningLine: View { var body: some View { Rectangle() .fill( LinearGradient( colors: [.blue, .cyan, .blue], startPoint: .leading, endPoint: .trailing ) ) .frame(width: 200, height: 3) .shadow(color: .blue, radius: 5, x: 0, y: 0) .modifier(ScanningLineModifier()) } } // MARK: - 经典扫描线 struct ClassicScanningLine: View { var body: some View { Rectangle() .fill(Color.green) .frame(width: 150, height: 2) .modifier(ScanningLineModifier()) } } // MARK: - 霓虹扫描线 struct NeonScanningLine: View { var body: some View { Rectangle() .fill(Color.purple) .frame(width: 180, height: 4) .shadow(color: .purple, radius: 8, x: 0, y: 0) .modifier(ScanningLineModifier()) } } // MARK: - 极简扫描线 struct MinimalScanningLine: View { var body: some View { Rectangle() .fill(Color.white) .frame(width: 100, height: 1) .modifier(ScanningLineModifier()) } } // MARK: - 复古扫描线 struct RetroScanningLine: View { var body: some View { HStack(spacing: 2) { ForEach(0..<5, id: \.self) { _ in Rectangle() .fill(Color.orange) .frame(width: 2, height: 20) } } .modifier(ScanningLineModifier()) } } // MARK: - 检测到的条码数据结构 struct DetectedCode: Identifiable { let id = UUID() let type: String let content: String let bounds: CGRect } // MARK: - 通知名称扩展 extension Notification.Name { static let scannerDidScanCode = Notification.Name("scannerDidScanCode") } // MARK: - 相机权限视图 struct CameraPermissionView: View { let authorizationStatus: AVAuthorizationStatus let onRequestPermission: () -> Void let onOpenSettings: () -> Void var body: some View { VStack(spacing: 30) { Spacer() // 相机图标 Image(systemName: "camera.fill") .font(.system(size: 80)) .foregroundColor(.gray) // 标题 Text("camera_permission_title".localized) .font(.largeTitle) .fontWeight(.bold) .multilineTextAlignment(.center) // 描述文本 Text(getDescriptionText()) .font(.body) .multilineTextAlignment(.center) .foregroundColor(.secondary) .padding(.horizontal, 40) // 操作按钮 VStack(spacing: 15) { if authorizationStatus == .notDetermined { Button(action: onRequestPermission) { HStack { Image(systemName: "camera.badge.ellipsis") Text("request_camera_permission".localized) } .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background(Color.blue) .cornerRadius(12) } } else if authorizationStatus == .denied || authorizationStatus == .restricted { Button(action: onOpenSettings) { HStack { Image(systemName: "gear") Text("open_settings".localized) } .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background(Color.orange) .cornerRadius(12) } } } .padding(.horizontal, 40) Spacer() } .background(Color(.systemBackground)) } private func getDescriptionText() -> String { switch authorizationStatus { case .notDetermined: return "camera_permission_description".localized case .denied: return "camera_permission_denied".localized case .restricted: return "camera_permission_restricted".localized default: return "camera_permission_unknown".localized } } } #if DEBUG struct ScannerView_Previews: PreviewProvider { static var previews: some View { ScannerView() } } #endif