diff --git a/MyQrCode/ContentView.swift b/MyQrCode/Core/ContentView.swift similarity index 100% rename from MyQrCode/ContentView.swift rename to MyQrCode/Core/ContentView.swift diff --git a/MyQrCode/MyQrCodeApp.swift b/MyQrCode/Core/MyQrCodeApp.swift similarity index 100% rename from MyQrCode/MyQrCodeApp.swift rename to MyQrCode/Core/MyQrCodeApp.swift diff --git a/MyQrCode/CODE_OPTIMIZATION_SUMMARY.md b/MyQrCode/Docs/CODE_OPTIMIZATION_SUMMARY.md similarity index 100% rename from MyQrCode/CODE_OPTIMIZATION_SUMMARY.md rename to MyQrCode/Docs/CODE_OPTIMIZATION_SUMMARY.md diff --git a/MyQrCode/MULTILINGUAL_FIX_SUMMARY.md b/MyQrCode/Docs/MULTILINGUAL_FIX_SUMMARY.md similarity index 100% rename from MyQrCode/MULTILINGUAL_FIX_SUMMARY.md rename to MyQrCode/Docs/MULTILINGUAL_FIX_SUMMARY.md diff --git a/MyQrCode/LanguageManager.swift b/MyQrCode/Managers/LanguageManager.swift similarity index 99% rename from MyQrCode/LanguageManager.swift rename to MyQrCode/Managers/LanguageManager.swift index 8bbfb67..90a8db4 100644 --- a/MyQrCode/LanguageManager.swift +++ b/MyQrCode/Managers/LanguageManager.swift @@ -171,7 +171,7 @@ class LanguageManager: ObservableObject { } else { // 首次启动,默认使用系统语言 currentLanguage = .system - let systemLanguage = detectSystemLanguage() + _ = detectSystemLanguage() // 保存系统语言设置 UserDefaults.standard.set("system", forKey: languageKey) diff --git a/MyQrCode/Logger.swift b/MyQrCode/Managers/Logger.swift similarity index 100% rename from MyQrCode/Logger.swift rename to MyQrCode/Managers/Logger.swift diff --git a/MyQrCode/Views/CodeContentInputView.swift b/MyQrCode/Views/Generator/CodeContentInputView.swift similarity index 100% rename from MyQrCode/Views/CodeContentInputView.swift rename to MyQrCode/Views/Generator/CodeContentInputView.swift diff --git a/MyQrCode/Views/CodeTypeSelectionView.swift b/MyQrCode/Views/Generator/CodeTypeSelectionView.swift similarity index 100% rename from MyQrCode/Views/CodeTypeSelectionView.swift rename to MyQrCode/Views/Generator/CodeTypeSelectionView.swift diff --git a/MyQrCode/Views/CreateCodeView.swift b/MyQrCode/Views/Generator/CreateCodeView.swift similarity index 100% rename from MyQrCode/Views/CreateCodeView.swift rename to MyQrCode/Views/Generator/CreateCodeView.swift diff --git a/MyQrCode/Views/CreateQRCodeView.swift b/MyQrCode/Views/Generator/CreateQRCodeView.swift similarity index 100% rename from MyQrCode/Views/CreateQRCodeView.swift rename to MyQrCode/Views/Generator/CreateQRCodeView.swift diff --git a/MyQrCode/Views/ImageComposerView.swift b/MyQrCode/Views/Generator/ImageComposerView.swift similarity index 100% rename from MyQrCode/Views/ImageComposerView.swift rename to MyQrCode/Views/Generator/ImageComposerView.swift diff --git a/MyQrCode/Views/QRCodeStyleView.swift b/MyQrCode/Views/Generator/QRCodeStyleView.swift similarity index 100% rename from MyQrCode/Views/QRCodeStyleView.swift rename to MyQrCode/Views/Generator/QRCodeStyleView.swift diff --git a/MyQrCode/Views/BarcodeCharacterHintView.swift b/MyQrCode/Views/History/BarcodeCharacterHintView.swift similarity index 100% rename from MyQrCode/Views/BarcodeCharacterHintView.swift rename to MyQrCode/Views/History/BarcodeCharacterHintView.swift diff --git a/MyQrCode/Views/BarcodeDetailView.swift b/MyQrCode/Views/History/BarcodeDetailView.swift similarity index 100% rename from MyQrCode/Views/BarcodeDetailView.swift rename to MyQrCode/Views/History/BarcodeDetailView.swift diff --git a/MyQrCode/Views/BarcodePreviewView.swift b/MyQrCode/Views/History/BarcodePreviewView.swift similarity index 100% rename from MyQrCode/Views/BarcodePreviewView.swift rename to MyQrCode/Views/History/BarcodePreviewView.swift diff --git a/MyQrCode/Views/BarcodeValidationInfoView.swift b/MyQrCode/Views/History/BarcodeValidationInfoView.swift similarity index 100% rename from MyQrCode/Views/BarcodeValidationInfoView.swift rename to MyQrCode/Views/History/BarcodeValidationInfoView.swift diff --git a/MyQrCode/Views/HistoryView.swift b/MyQrCode/Views/History/HistoryView.swift similarity index 100% rename from MyQrCode/Views/HistoryView.swift rename to MyQrCode/Views/History/HistoryView.swift diff --git a/MyQrCode/Views/QRCodeDetailView.swift b/MyQrCode/Views/History/QRCodeDetailView.swift similarity index 100% rename from MyQrCode/Views/QRCodeDetailView.swift rename to MyQrCode/Views/History/QRCodeDetailView.swift diff --git a/MyQrCode/Views/QRCodeSavedView.swift b/MyQrCode/Views/History/QRCodeSavedView.swift similarity index 100% rename from MyQrCode/Views/QRCodeSavedView.swift rename to MyQrCode/Views/History/QRCodeSavedView.swift diff --git a/MyQrCode/ScannerView/CameraPermissionView.swift b/MyQrCode/Views/Scanner/CameraPermissionView.swift similarity index 100% rename from MyQrCode/ScannerView/CameraPermissionView.swift rename to MyQrCode/Views/Scanner/CameraPermissionView.swift diff --git a/MyQrCode/ScannerView/CameraPreviewView.swift b/MyQrCode/Views/Scanner/CameraPreviewView.swift similarity index 100% rename from MyQrCode/ScannerView/CameraPreviewView.swift rename to MyQrCode/Views/Scanner/CameraPreviewView.swift diff --git a/MyQrCode/ScannerView/CodePositionOverlay.swift b/MyQrCode/Views/Scanner/CodePositionOverlay.swift similarity index 100% rename from MyQrCode/ScannerView/CodePositionOverlay.swift rename to MyQrCode/Views/Scanner/CodePositionOverlay.swift diff --git a/MyQrCode/ScannerView/Models.swift b/MyQrCode/Views/Scanner/Models.swift similarity index 100% rename from MyQrCode/ScannerView/Models.swift rename to MyQrCode/Views/Scanner/Models.swift diff --git a/MyQrCode/ScannerView/ScannerView.swift b/MyQrCode/Views/Scanner/ScannerView.swift similarity index 100% rename from MyQrCode/ScannerView/ScannerView.swift rename to MyQrCode/Views/Scanner/ScannerView.swift diff --git a/MyQrCode/ScannerView/ScannerViewModel.swift b/MyQrCode/Views/Scanner/ScannerViewModel.swift similarity index 100% rename from MyQrCode/ScannerView/ScannerViewModel.swift rename to MyQrCode/Views/Scanner/ScannerViewModel.swift diff --git a/MyQrCode/ScannerView/ScanningLineView.swift b/MyQrCode/Views/Scanner/ScanningLineView.swift similarity index 100% rename from MyQrCode/ScannerView/ScanningLineView.swift rename to MyQrCode/Views/Scanner/ScanningLineView.swift diff --git a/MyQrCode/ScannerView/ScanningOverlayView.swift b/MyQrCode/Views/Scanner/ScanningOverlayView.swift similarity index 100% rename from MyQrCode/ScannerView/ScanningOverlayView.swift rename to MyQrCode/Views/Scanner/ScanningOverlayView.swift diff --git a/MyQrCode/ScannerView/TestAutoSelectButton.swift b/MyQrCode/Views/Scanner/TestAutoSelectButton.swift similarity index 100% rename from MyQrCode/ScannerView/TestAutoSelectButton.swift rename to MyQrCode/Views/Scanner/TestAutoSelectButton.swift diff --git a/MyQrCode/Views/ScannerView/CameraPermissionView.swift b/MyQrCode/Views/ScannerView/CameraPermissionView.swift deleted file mode 100644 index e351914..0000000 --- a/MyQrCode/Views/ScannerView/CameraPermissionView.swift +++ /dev/null @@ -1,86 +0,0 @@ -import SwiftUI -import AVFoundation - -// MARK: - 相机权限视图 -struct CameraPermissionView: View { - @EnvironmentObject var languageManager: LanguageManager - 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) - .id(languageManager.refreshTrigger) - - // 描述文本 - Text(getDescriptionText()) - .font(.body) - .multilineTextAlignment(.center) - .foregroundColor(.secondary) - .padding(.horizontal, 40) - .id(languageManager.refreshTrigger) - - // 操作按钮 - VStack(spacing: 15) { - if authorizationStatus == .notDetermined { - Button(action: onRequestPermission) { - HStack { - Image(systemName: "camera.badge.ellipsis") - Text("request_camera_permission".localized) - .id(languageManager.refreshTrigger) - } - .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) - .id(languageManager.refreshTrigger) - } - .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 - } - } -} \ No newline at end of file diff --git a/MyQrCode/Views/ScannerView/CameraPreviewView.swift b/MyQrCode/Views/ScannerView/CameraPreviewView.swift deleted file mode 100644 index 855ba5f..0000000 --- a/MyQrCode/Views/ScannerView/CameraPreviewView.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI -import AVFoundation - -// MARK: - 相机预览视图 -struct CameraPreviewView: UIViewRepresentable { - let session: AVCaptureSession - @Binding var previewLayer: AVCaptureVideoPreviewLayer? - - func makeUIView(context: Context) -> UIView { - let view = UIView() - view.backgroundColor = .black - - let previewLayer = AVCaptureVideoPreviewLayer(session: session) - previewLayer.videoGravity = .resizeAspectFill - view.layer.addSublayer(previewLayer) - - // 设置预览层为绑定变量 - DispatchQueue.main.async { - self.previewLayer = previewLayer - } - - return view - } - - func updateUIView(_ uiView: UIView, context: Context) { - guard let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer else { return } - - // 确保预览层尺寸正确 - DispatchQueue.main.async { - previewLayer.frame = uiView.bounds - self.previewLayer = previewLayer - } - } -} \ No newline at end of file diff --git a/MyQrCode/Views/ScannerView/CodePositionOverlay.swift b/MyQrCode/Views/ScannerView/CodePositionOverlay.swift deleted file mode 100644 index 460f10c..0000000 --- a/MyQrCode/Views/ScannerView/CodePositionOverlay.swift +++ /dev/null @@ -1,163 +0,0 @@ -import SwiftUI -import AVFoundation - -// MARK: - 条码位置标记覆盖层 -struct CodePositionOverlay: View { - let detectedCodes: [DetectedCode] - let previewLayer: AVCaptureVideoPreviewLayer? - let onCodeSelected: (DetectedCode) -> Void - - var body: some View { - ZStack { - GeometryReader { geometry in - 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 - } - .ignoresSafeArea() - } - .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) - } - - // 即使会话停止,我们仍然可以使用预览层进行坐标转换 - // 因为预览层的配置仍然有效 - let metadataObject = code.bounds - - // 检查边界值是否有效 - guard metadataObject.width > 0 && metadataObject.height > 0 else { - logWarning("Invalid metadata bounds: \(metadataObject), using screen center", className: "CodePositionMarker") - return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) - } - - 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) - } -} diff --git a/MyQrCode/Views/ScannerView/Models.swift b/MyQrCode/Views/ScannerView/Models.swift deleted file mode 100644 index 172dd95..0000000 --- a/MyQrCode/Views/ScannerView/Models.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import CoreGraphics - -// MARK: - 检测到的条码数据结构 -struct DetectedCode: Identifiable { - let id = UUID() - let type: String - let content: String - let bounds: CGRect - let source: CodeSource // 添加来源字段 -} - -// MARK: - 条码来源枚举 -enum CodeSource { - case camera // 相机扫描 - case image // 图片解码 -} - -// MARK: - 通知名称扩展 -extension Notification.Name { - static let scannerDidScanCode = Notification.Name("scannerDidScanCode") -} \ No newline at end of file diff --git a/MyQrCode/Views/ScannerView/ScannerView.swift b/MyQrCode/Views/ScannerView/ScannerView.swift deleted file mode 100644 index e31e24a..0000000 --- a/MyQrCode/Views/ScannerView/ScannerView.swift +++ /dev/null @@ -1,611 +0,0 @@ -import SwiftUI -import AVFoundation -import Combine -import CoreData -import QRCode -import Vision - -// MARK: - 主扫描视图 -struct ScannerView: View { - @StateObject private var scannerViewModel = ScannerViewModel() - @EnvironmentObject var languageManager: LanguageManager - @State private var showPreviewPause = false - @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.isEmpty, - detectedCodesCount: scannerViewModel.detectedCodes.count, - onImageDecode: { showImagePicker = true } - ) - - // 条码位置标记覆盖层 - 显示所有相机扫描的条码选择点 - if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { - CodePositionOverlay( - detectedCodes: scannerViewModel.detectedCodes, - previewLayer: previewLayer, - onCodeSelected: handleCodeSelection - ) - } - - // 测试按钮(调试用)- 在相机扫描条码时显示 - if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { - if let code = scannerViewModel.detectedCodes.first { - 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("scanner_title".localized) - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(false) - .sheet(isPresented: $showImagePicker) { - ImagePicker( - onImageSelected: handleImageDecodeResult, - shouldProcessImage: false - ) - } - .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)) - .id(languageManager.refreshTrigger) - } - .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) - .id(languageManager.refreshTrigger) - } - .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") - - // 调试信息 - print("🔍 handleDetectedCodes 被调用:") - print(" 条码数量: \(codes.count)") - print(" 条码内容: \(codes.map { "\($0.type): \($0.content)" })") - print(" 条码来源: \(codes.map { $0.source })") - - if codes.count == 1 { - logInfo("单个条码,显示选择点并0.5秒后自动跳转", className: "ScannerView") - pauseForPreview() - // 0.5秒后自动选择单个条码 - autoSelectSingleCode(code: codes[0], delay: 0.5) - } else { - logInfo("多个条码,显示选择点等待用户选择", className: "ScannerView") - pauseForPreview() - } - } - - private func handleOrientationChange() { - logInfo("Screen orientation changed", 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() - - // 调试信息 - print("⏸️ pauseForPreview 被调用:") - print(" showPreviewPause: \(showPreviewPause)") - print(" detectedCodes.count: \(scannerViewModel.detectedCodes.count)") - } - - private func resetToScanning() { - logInfo("🔄 ScannerView 开始重置到扫描状态", className: "ScannerView") - - // 重置UI状态 - showPreviewPause = false - - // 重置扫描状态 - scannerViewModel.resetDetection() - - // 重置图片解码状态 - resetImageDecodeState() - - // 恢复相机功能并重新开始扫描 - scannerViewModel.resumeCamera() - - logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView") - } - - private func autoSelectSingleCode(code: DetectedCode, delay: TimeInterval = 1.0) { - logInfo("开始自动选择定时器,条码类型: \(code.type),延迟: \(delay)秒", className: "ScannerView") - - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - 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 ?? "unknown_content".localized, - bounds: qrCode.bounds, - source: .image - ) - } - 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 = "no_codes_detected_in_image".localized - 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 ?? "unknown_content".localized - - logInfo("检测到条形码 #\(index + 1): 类型=\(barcodeType), 内容=\(content)", className: "ScannerView") - - return DetectedCode( - type: barcodeType, - content: content, - bounds: observation.boundingBox, - source: .image - ) - } - } 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: - 解码失败提示覆盖层 -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("decode_failed".localized) - .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("reselect_image".localized) - .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() - .environmentObject(LanguageManager.shared) - } - } -} -#endif \ No newline at end of file diff --git a/MyQrCode/Views/ScannerView/ScannerViewModel.swift b/MyQrCode/Views/ScannerView/ScannerViewModel.swift deleted file mode 100644 index afa7ddc..0000000 --- a/MyQrCode/Views/ScannerView/ScannerViewModel.swift +++ /dev/null @@ -1,430 +0,0 @@ -import SwiftUI -import AVFoundation -import AudioToolbox -import Combine - -// MARK: - 扫描器视图模型 -class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate { - @Published var detectedCodes: [DetectedCode] = [] - @Published var showAlert = false - @Published var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined - @Published var isTorchOn = false - - var captureSession: AVCaptureSession! - private var metadataOutput: AVCaptureMetadataOutput? - private var videoDevice: AVCaptureDevice? - private var isProcessingDetection = false // 添加处理状态标记 - - 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) - - @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 - } - } - } - } - - 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 - } - - // 保存视频设备引用,用于手电筒控制 - videoDevice = videoCaptureDevice - - 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") - - // 立即设置处理标记,防止新的检测被处理 - isProcessingDetection = true - - // 检查会话是否在运行 - if captureSession?.isRunning == true { - // 立即停止扫描会话 - captureSession?.stopRunning() - logInfo("✅ 扫描会话已停止", className: "ScannerViewModel") - } else { - logInfo("ℹ️ 扫描会话已经停止", className: "ScannerViewModel") - } - } - - /// 暂停相机功能(用于预览暂停状态) - func pauseCamera() { - logInfo("⏸️ 暂停相机功能", className: "ScannerViewModel") - - // 设置处理标记 - isProcessingDetection = true - - // 停止会话(但保留配置) - if captureSession?.isRunning == true { - captureSession?.stopRunning() - logInfo("✅ 相机会话已暂停", className: "ScannerViewModel") - } else { - logInfo("ℹ️ 相机会话已经停止", className: "ScannerViewModel") - } - } - - /// 恢复相机功能(用于恢复扫描) - func resumeCamera() { - logInfo("▶️ 恢复相机功能", className: "ScannerViewModel") - - // 检查相机权限 - guard cameraAuthorizationStatus == .authorized else { - logWarning("❌ 相机权限未授权,无法恢复相机", className: "ScannerViewModel") - return - } - - // 检查会话配置 - if captureSession == nil || captureSession.inputs.isEmpty || captureSession.outputs.isEmpty { - logInfo("🔄 重新设置相机会话", className: "ScannerViewModel") - setupCaptureSession() - } - - // 重置处理标记 - isProcessingDetection = false - - // 启动相机会话 - if captureSession?.isRunning != true { - logInfo("🚀 启动相机会话", className: "ScannerViewModel") - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - self?.captureSession?.startRunning() - - DispatchQueue.main.async { - if self?.captureSession?.isRunning == true { - logInfo("✅ 相机会话启动成功", className: "ScannerViewModel") - } else { - logWarning("⚠️ 相机会话启动失败", className: "ScannerViewModel") - } - } - } - } - - logInfo("✅ 相机功能已恢复", className: "ScannerViewModel") - } - - - - func resetDetection() { - DispatchQueue.main.async { - logInfo("🔄 重置检测状态,清空 detectedCodes", className: "ScannerViewModel") - self.detectedCodes = [] - self.isProcessingDetection = false // 重置处理标记 - } - } - - 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") - - // 先停止当前会话 - if captureSession?.isRunning == true { - logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel") - captureSession?.stopRunning() - } - - // 重置检测状态 - resetDetection() - - // 延迟后重新启动会话 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel") - - // 在后台线程启动会话 - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - self?.captureSession?.startRunning() - - // 检查会话状态 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - if self?.captureSession?.isRunning == true { - logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel") - } else { - logWarning("⚠️ 扫描会话启动失败", className: "ScannerViewModel") - } - } - } - } - } - - // MARK: - AVCaptureMetadataOutputObjectsDelegate - - func metadataOutput(_ output: AVCaptureMetadataOutput, - didOutput metadataObjects: [AVMetadataObject], - from connection: AVCaptureConnection) { - - // 防止重复处理检测结果 - guard !isProcessingDetection else { - logInfo("⚠️ 正在处理检测结果,忽略新的检测", className: "ScannerViewModel") - return - } - - logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel") - - // 设置处理标记 - isProcessingDetection = true - - // 震动反馈 - AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - - // 停止扫描 - stopScanning() - - // 处理所有检测到的条码 - var codes: [DetectedCode] = [] - - for metadataObject in metadataObjects { - if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, - let stringValue = readableObject.stringValue { - - let codeType = getBarcodeTypeString(from: readableObject.type) - let bounds = readableObject.bounds - - let detectedCode = DetectedCode( - type: codeType, - content: stringValue, - bounds: bounds, - source: .camera - ) - - 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: - 手电筒控制 - - /// 检查设备是否支持手电筒 - var isTorchAvailable: Bool { - guard let device = videoDevice else { return false } - return device.hasTorch && device.isTorchAvailable - } - - /// 切换手电筒状态 - func toggleTorch() { - guard let device = videoDevice else { - logWarning("❌ 没有可用的视频设备", className: "ScannerViewModel") - return - } - - guard device.hasTorch && device.isTorchAvailable else { - logWarning("❌ 设备不支持手电筒", className: "ScannerViewModel") - return - } - - do { - try device.lockForConfiguration() - - if isTorchOn { - // 关闭手电筒 - device.torchMode = .off - isTorchOn = false - logInfo("🔦 手电筒已关闭", className: "ScannerViewModel") - } else { - // 打开手电筒 - try device.setTorchModeOn(level: 1.0) - isTorchOn = true - logInfo("🔦 手电筒已打开", className: "ScannerViewModel") - } - - device.unlockForConfiguration() - - } catch { - logError("❌ 手电筒控制失败: \(error.localizedDescription)", className: "ScannerViewModel") - device.unlockForConfiguration() - } - } - - /// 关闭手电筒 - func turnOffTorch() { - guard let device = videoDevice else { return } - - do { - try device.lockForConfiguration() - device.torchMode = .off - isTorchOn = false - device.unlockForConfiguration() - logInfo("🔦 手电筒已关闭", className: "ScannerViewModel") - } catch { - logError("❌ 关闭手电筒失败: \(error.localizedDescription)", className: "ScannerViewModel") - device.unlockForConfiguration() - } - } - - // MARK: - 条形码类型转换 - - /// 获取条形码类型的可读字符串 - private func getBarcodeTypeString(from metadataType: AVMetadataObject.ObjectType) -> String { - switch metadataType { - case .ean8: - return "EAN-8" - case .ean13: - return "EAN-13" - case .upce: - return "UPC-E" - case .code39: - return "Code 39" - case .code93: - return "Code 93" - 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: - // 处理可能包含前缀的类型字符串 - let typeString = metadataType.rawValue - if typeString.contains("org.gs1.") { - // 移除 org.gs1. 前缀 - let cleanType = typeString.replacingOccurrences(of: "org.gs1.", with: "") - return cleanType.uppercased() - } - return typeString - } - } -} \ No newline at end of file diff --git a/MyQrCode/Views/ScannerView/ScanningLineView.swift b/MyQrCode/Views/ScannerView/ScanningLineView.swift deleted file mode 100644 index f973192..0000000 --- a/MyQrCode/Views/ScannerView/ScanningLineView.swift +++ /dev/null @@ -1,152 +0,0 @@ -import SwiftUI - -// MARK: - 扫描线视图 -struct ScanningLineView: View { - @EnvironmentObject var languageManager: LanguageManager - 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: - 扫描线样式枚举 -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 - } - } - - func getLocalizedName(languageManager: LanguageManager) -> String { - switch self { - case .modern: return languageManager.localizedString(for: "style_modern") - case .classic: return languageManager.localizedString(for: "style_classic") - case .neon: return languageManager.localizedString(for: "style_neon") - case .minimal: return languageManager.localizedString(for: "style_minimal") - case .retro: return languageManager.localizedString(for: "style_retro") - } - } -} - -// 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: - 现代扫描线 -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()) - } -} \ No newline at end of file diff --git a/MyQrCode/Views/ScannerView/ScanningOverlayView.swift b/MyQrCode/Views/ScannerView/ScanningOverlayView.swift deleted file mode 100644 index 5710fd2..0000000 --- a/MyQrCode/Views/ScannerView/ScanningOverlayView.swift +++ /dev/null @@ -1,220 +0,0 @@ -import SwiftUI - -// MARK: - 扫描界面覆盖层 -struct ScanningOverlayView: View { - let showPreviewPause: Bool - let detectedCodesCount: Int - let onImageDecode: () -> Void - - var body: some View { - VStack { - Spacer() - - // 扫描线组件 - if !showPreviewPause { - ScanningLineView(style: .modern) - } - - // 提示文本 - ScanningInstructionView( - showPreviewPause: showPreviewPause, - detectedCodesCount: detectedCodesCount - ) - - Spacer() - - // 底部按钮区域 - ScanningBottomButtonsView( - showPreviewPause: showPreviewPause, - onImageDecode: onImageDecode - ) - } - } -} - -// MARK: - 扫描指令视图 -struct ScanningInstructionView: View { - @EnvironmentObject var languageManager: LanguageManager - let showPreviewPause: Bool - let detectedCodesCount: Int - - var body: some View { - if showPreviewPause { - VStack(spacing: 8) { - Text("detected_codes".localized) - .foregroundColor(.white) - .font(.headline) - .id(languageManager.refreshTrigger) - - if detectedCodesCount == 1 { - Text("auto_result_1s".localized) - .foregroundColor(.green) - .font(.subheadline) - .id(languageManager.refreshTrigger) - } else { - Text("select_code_instruction".localized) - .foregroundColor(.white.opacity(0.8)) - .font(.subheadline) - .id(languageManager.refreshTrigger) - } - } - .padding(.top, 20) - } else { - Text("scan_instruction".localized) - .foregroundColor(.white) - .font(.headline) - .padding(.top, 20) - .id(languageManager.refreshTrigger) - } - } -} - -// MARK: - 扫描底部按钮视图 -struct ScanningBottomButtonsView: View { - let showPreviewPause: Bool - let onImageDecode: () -> Void - - var body: some View { - VStack(spacing: 15) { - // 图片解码按钮 - if !showPreviewPause { - Button(action: { - onImageDecode() - }) { - HStack(spacing: 8) { - Image(systemName: "photo.on.rectangle.angled") - .font(.system(size: 16, weight: .semibold)) - - Text("image_decode".localized) - .font(.subheadline) - .fontWeight(.medium) - } - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(Color.blue.opacity(0.3)) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color.blue.opacity(0.6), lineWidth: 1) - ) - ) - } - .buttonStyle(PlainButtonStyle()) - } - - // 移除关闭按钮,因为现在使用导航返回 - } - .padding(.bottom, 50) - } -} - -// MARK: - 扫描线样式选择器 -struct ScanningStyleSelectorView: View { - @EnvironmentObject var languageManager: LanguageManager - @Binding var selectedStyle: ScanningLineStyle - - var body: some View { - VStack(spacing: 12) { - // 标题 - Text("scanning_line_style".localized) - .font(.caption) - .foregroundColor(.white.opacity(0.8)) - .padding(.bottom, 4) - - // 样式选择器 - HStack(spacing: 8) { - ForEach(ScanningLineStyle.allCases, id: \.self) { style in - Button(action: { - withAnimation(.easeInOut(duration: 0.2)) { - selectedStyle = style - } - - // 添加触觉反馈 - let impactFeedback = UIImpactFeedbackGenerator(style: .light) - impactFeedback.impactOccurred() - }) { - VStack(spacing: 4) { - // 样式预览 - stylePreview(style) - .frame(width: 24, height: 24) - - // 样式名称 - Text(style.getLocalizedName(languageManager: languageManager)) - .font(.caption2) - .foregroundColor(.white) - .id(languageManager.refreshTrigger) - } - .frame(width: 60, height: 50) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(selectedStyle == style ? - Color.green.opacity(0.8) : - Color.black.opacity(0.6)) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(selectedStyle == style ? - Color.green : - Color.white.opacity(0.3), - lineWidth: selectedStyle == style ? 2 : 1) - ) - ) - } - .buttonStyle(PlainButtonStyle()) - } - } - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background( - RoundedRectangle(cornerRadius: 16) - .fill(Color.black.opacity(0.7)) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(Color.white.opacity(0.2), lineWidth: 1) - ) - ) - .padding(.bottom, 10) - } - - // 样式预览 - @ViewBuilder - private func stylePreview(_ style: ScanningLineStyle) -> some View { - switch style { - case .modern: - Rectangle() - .fill( - LinearGradient( - colors: [.blue, .cyan, .blue], - startPoint: .leading, - endPoint: .trailing - ) - ) - .frame(width: 20, height: 2) - .shadow(color: .blue, radius: 2, x: 0, y: 0) - case .classic: - Rectangle() - .fill(Color.green) - .frame(width: 16, height: 2) - case .neon: - Rectangle() - .fill(Color.purple) - .frame(width: 18, height: 3) - .shadow(color: .purple, radius: 3, x: 0, y: 0) - case .minimal: - Rectangle() - .fill(Color.white) - .frame(width: 14, height: 1) - case .retro: - Rectangle() - .fill(Color.orange) - .frame(width: 20, height: 2) - .overlay( - Rectangle() - .stroke(Color.yellow, lineWidth: 0.5) - .frame(width: 18, height: 1.5) - ) - } - } -} \ No newline at end of file diff --git a/MyQrCode/Views/ScannerView/TestAutoSelectButton.swift b/MyQrCode/Views/ScannerView/TestAutoSelectButton.swift deleted file mode 100644 index 6520296..0000000 --- a/MyQrCode/Views/ScannerView/TestAutoSelectButton.swift +++ /dev/null @@ -1,26 +0,0 @@ -import SwiftUI - -// MARK: - 测试自动选择按钮 -struct TestAutoSelectButton: View { - @EnvironmentObject var languageManager: LanguageManager - let detectedCode: DetectedCode - let onSelect: (DetectedCode) -> Void - - var body: some View { - VStack { - HStack { - Spacer() - Button("test_auto_select".localized) { - onSelect(detectedCode) - } - .id(languageManager.refreshTrigger) - .foregroundColor(.white) - .padding(8) - .background(Color.red) - .cornerRadius(8) - .padding(.trailing, 20) - } - Spacer() - } - } -} \ No newline at end of file diff --git a/MyQrCode/Views/AppPermissionsView.swift b/MyQrCode/Views/Settings/AppPermissionsView.swift similarity index 100% rename from MyQrCode/Views/AppPermissionsView.swift rename to MyQrCode/Views/Settings/AppPermissionsView.swift diff --git a/MyQrCode/LanguageSettingsView.swift b/MyQrCode/Views/Settings/LanguageSettingsView.swift similarity index 100% rename from MyQrCode/LanguageSettingsView.swift rename to MyQrCode/Views/Settings/LanguageSettingsView.swift diff --git a/MyQrCode/Views/PrivacyPolicyView.swift b/MyQrCode/Views/Settings/PrivacyPolicyView.swift similarity index 100% rename from MyQrCode/Views/PrivacyPolicyView.swift rename to MyQrCode/Views/Settings/PrivacyPolicyView.swift diff --git a/MyQrCode/Views/SettingsView.swift b/MyQrCode/Views/Settings/SettingsView.swift similarity index 100% rename from MyQrCode/Views/SettingsView.swift rename to MyQrCode/Views/Settings/SettingsView.swift diff --git a/MyQrCode/Views/LaunchScreenView.swift b/MyQrCode/Views/Utils/LaunchScreenView.swift similarity index 100% rename from MyQrCode/Views/LaunchScreenView.swift rename to MyQrCode/Views/Utils/LaunchScreenView.swift diff --git a/docs/PROJECT_STRUCTURE_REFACTOR_README.md b/docs/PROJECT_STRUCTURE_REFACTOR_README.md new file mode 100644 index 0000000..05525f2 --- /dev/null +++ b/docs/PROJECT_STRUCTURE_REFACTOR_README.md @@ -0,0 +1,285 @@ +# 项目结构重构文档 + +## 概述 + +本文档记录了MyQrCode应用的项目结构重构过程,将相关功能的view按类型分组到同一个组中,提高代码的可维护性和可读性。 + +## 重构前的结构 + +``` +MyQrCode/Views/ +├── Components/ # 通用组件 +├── ScannerView/ # 扫描相关(旧结构) +├── CreateCodeView.swift +├── CreateQRCodeView.swift +├── QRCodeStyleView.swift +├── CodeContentInputView.swift +├── CodeTypeSelectionView.swift +├── ImageComposerView.swift +├── HistoryView.swift +├── QRCodeDetailView.swift +├── QRCodeSavedView.swift +├── BarcodeDetailView.swift +├── BarcodePreviewView.swift +├── BarcodeCharacterHintView.swift +├── BarcodeValidationInfoView.swift +├── SettingsView.swift +├── AppPermissionsView.swift +├── PrivacyPolicyView.swift +├── LaunchScreenView.swift +└── 其他文件... +``` + +## 重构后的结构 + +``` +MyQrCode/ +├── Core/ # 核心文件 +│ ├── MyQrCodeApp.swift +│ └── ContentView.swift +├── Managers/ # 管理器文件 +│ ├── LanguageManager.swift +│ └── Logger.swift +├── Views/ # 视图文件 +│ ├── Scanner/ # 扫描相关功能 +│ │ ├── ScannerView.swift +│ │ ├── ScannerViewModel.swift +│ │ ├── CameraPermissionView.swift +│ │ ├── CameraPreviewView.swift +│ │ ├── ScanningOverlayView.swift +│ │ ├── CodePositionOverlay.swift +│ │ ├── ScanningLineView.swift +│ │ ├── Models.swift +│ │ └── TestAutoSelectButton.swift +│ ├── Generator/ # 生成相关功能 +│ │ ├── CreateCodeView.swift +│ │ ├── CreateQRCodeView.swift +│ │ ├── QRCodeStyleView.swift +│ │ ├── CodeContentInputView.swift +│ │ ├── CodeTypeSelectionView.swift +│ │ └── ImageComposerView.swift +│ ├── History/ # 历史记录相关功能 +│ │ ├── HistoryView.swift +│ │ ├── QRCodeDetailView.swift +│ │ ├── QRCodeSavedView.swift +│ │ ├── BarcodeDetailView.swift +│ │ ├── BarcodePreviewView.swift +│ │ ├── BarcodeCharacterHintView.swift +│ │ └── BarcodeValidationInfoView.swift +│ ├── Settings/ # 设置相关功能 +│ │ ├── SettingsView.swift +│ │ ├── AppPermissionsView.swift +│ │ ├── PrivacyPolicyView.swift +│ │ └── LanguageSettingsView.swift +│ ├── Components/ # 通用组件 +│ │ ├── CardView.swift +│ │ ├── FormView.swift +│ │ ├── InputFieldView.swift +│ │ ├── ListView.swift +│ │ ├── PickerView.swift +│ │ ├── QRCodePreviewView.swift +│ │ ├── ValidationView.swift +│ │ ├── UtilityFunctions.swift +│ │ ├── InputComponentFactory.swift +│ │ ├── InputTitleView.swift +│ │ ├── InputHintView.swift +│ │ ├── KeyboardToolbarView.swift +│ │ ├── TextInputView.swift +│ │ ├── TextEditorView.swift +│ │ ├── URLInputView.swift +│ │ ├── EmailInputView.swift +│ │ ├── PhoneInputView.swift +│ │ ├── ContactInputView.swift +│ │ ├── LocationInputView.swift +│ │ ├── WiFiInputView.swift +│ │ ├── SocialInputView.swift +│ │ ├── CalendarInputView.swift +│ │ └── DatePickerView.swift +│ └── Utils/ # 工具类 +│ └── LaunchScreenView.swift +├── Models/ # 数据模型 +├── Utils/ # 工具类 +├── Resources/ # 资源文件 +├── Assets.xcassets/ # 资源文件 +├── Docs/ # 文档文件 +│ ├── MULTILINGUAL_FIX_SUMMARY.md +│ └── CODE_OPTIMIZATION_SUMMARY.md +├── Info.plist # 应用配置 +└── *.lproj/ # 本地化文件 +``` + +## 分组原则 + +### 1. Core组 +**功能**: 应用核心文件 +**包含文件**: +- `MyQrCodeApp.swift` - 应用入口点 +- `ContentView.swift` - 主内容视图 + +### 2. Managers组 +**功能**: 应用管理器 +**包含文件**: +- `LanguageManager.swift` - 语言管理器 +- `Logger.swift` - 日志管理器 + +### 3. Views组 +**功能**: 所有视图文件,按功能进一步分组 + +#### 3.1 Scanner组 +**功能**: 扫描二维码和条形码 +**包含文件**: +- `ScannerView.swift` - 主扫描界面 +- `ScannerViewModel.swift` - 扫描逻辑管理 +- `CameraPermissionView.swift` - 相机权限界面 +- `CameraPreviewView.swift` - 相机预览界面 +- `ScanningOverlayView.swift` - 扫描覆盖层 +- `CodePositionOverlay.swift` - 代码位置覆盖层 +- `ScanningLineView.swift` - 扫描线动画 +- `Models.swift` - 扫描相关数据模型 +- `TestAutoSelectButton.swift` - 测试自动选择按钮 + +#### 3.2 Generator组 +**功能**: 生成二维码和条形码 +**包含文件**: +- `CreateCodeView.swift` - 创建代码主界面 +- `CreateQRCodeView.swift` - 创建二维码界面 +- `QRCodeStyleView.swift` - 二维码样式选择界面 +- `CodeContentInputView.swift` - 代码内容输入界面 +- `CodeTypeSelectionView.swift` - 代码类型选择界面 +- `ImageComposerView.swift` - 图片合成界面 + +#### 3.3 History组 +**功能**: 历史记录管理 +**包含文件**: +- `HistoryView.swift` - 历史记录主界面 +- `QRCodeDetailView.swift` - 二维码详情界面 +- `QRCodeSavedView.swift` - 二维码保存界面 +- `BarcodeDetailView.swift` - 条形码详情界面 +- `BarcodePreviewView.swift` - 条形码预览界面 +- `BarcodeCharacterHintView.swift` - 条形码字符提示界面 +- `BarcodeValidationInfoView.swift` - 条形码验证信息界面 + +#### 3.4 Settings组 +**功能**: 应用设置 +**包含文件**: +- `SettingsView.swift` - 设置主界面 +- `AppPermissionsView.swift` - 应用权限界面 +- `PrivacyPolicyView.swift` - 隐私政策界面 +- `LanguageSettingsView.swift` - 语言设置界面 + +#### 3.5 Components组 +**功能**: 通用UI组件 +**包含文件**: 所有可复用的UI组件 + +#### 3.6 Utils组 +**功能**: 视图工具类 +**包含文件**: +- `LaunchScreenView.swift` - 启动页面 + +### 4. 其他组 +- **Models**: 数据模型文件 +- **Utils**: 工具类文件 +- **Resources**: 资源文件 +- **Docs**: 文档文件 +- **Assets.xcassets**: 资源文件 +- **Info.plist**: 应用配置 +- ***.lproj**: 本地化文件 + +## 重构优势 + +### 1. 提高可维护性 +- **功能分组**: 相关功能文件集中在一起,便于查找和维护 +- **职责清晰**: 每个组都有明确的职责范围 +- **减少耦合**: 不同功能组之间的依赖关系更清晰 + +### 2. 提高可读性 +- **结构清晰**: 新开发者可以快速理解项目结构 +- **导航便利**: 在IDE中可以更容易地导航到相关文件 +- **文档化**: 通过文件夹结构自动形成文档 + +### 3. 便于扩展 +- **模块化**: 每个功能组可以独立开发和测试 +- **可复用**: 通用组件可以在不同功能组中复用 +- **可扩展**: 新增功能时可以很容易地确定放置位置 + +## 技术实现 + +### 文件移动 +使用命令行工具进行文件移动: +```bash +# 创建新的目录结构 +mkdir -p MyQrCode/Core +mkdir -p MyQrCode/Managers +mkdir -p MyQrCode/Views/{Scanner,Generator,History,Settings,Utils} +mkdir -p MyQrCode/Docs + +# 移动核心文件 +mv MyQrCode/MyQrCodeApp.swift MyQrCode/Core/ +mv MyQrCode/ContentView.swift MyQrCode/Core/ + +# 移动管理器文件 +mv MyQrCode/LanguageManager.swift MyQrCode/Managers/ +mv MyQrCode/Logger.swift MyQrCode/Managers/ + +# 移动视图文件 +mv MyQrCode/Views/ScannerView/* MyQrCode/Views/Scanner/ +mv MyQrCode/Views/CreateCodeView.swift MyQrCode/Views/Generator/ +mv MyQrCode/Views/CreateQRCodeView.swift MyQrCode/Views/Generator/ +mv MyQrCode/Views/QRCodeStyleView.swift MyQrCode/Views/Generator/ +mv MyQrCode/Views/CodeContentInputView.swift MyQrCode/Views/Generator/ +mv MyQrCode/Views/CodeTypeSelectionView.swift MyQrCode/Views/Generator/ +mv MyQrCode/Views/ImageComposerView.swift MyQrCode/Views/Generator/ +mv MyQrCode/Views/HistoryView.swift MyQrCode/Views/History/ +mv MyQrCode/Views/QRCodeDetailView.swift MyQrCode/Views/History/ +mv MyQrCode/Views/QRCodeSavedView.swift MyQrCode/Views/History/ +mv MyQrCode/Views/BarcodeDetailView.swift MyQrCode/Views/History/ +mv MyQrCode/Views/BarcodePreviewView.swift MyQrCode/Views/History/ +mv MyQrCode/Views/BarcodeCharacterHintView.swift MyQrCode/Views/History/ +mv MyQrCode/Views/BarcodeValidationInfoView.swift MyQrCode/Views/History/ +mv MyQrCode/Views/SettingsView.swift MyQrCode/Views/Settings/ +mv MyQrCode/Views/AppPermissionsView.swift MyQrCode/Views/Settings/ +mv MyQrCode/Views/PrivacyPolicyView.swift MyQrCode/Views/Settings/ +mv MyQrCode/LanguageSettingsView.swift MyQrCode/Views/Settings/ +mv MyQrCode/Views/LaunchScreenView.swift MyQrCode/Views/Utils/ + +# 移动文档文件 +mv MyQrCode/MULTILINGUAL_FIX_SUMMARY.md MyQrCode/Docs/ +mv MyQrCode/CODE_OPTIMIZATION_SUMMARY.md MyQrCode/Docs/ +``` + +### 引用更新 +由于Swift的模块系统会自动解析文件路径,大部分引用不需要手动更新。但为了确保项目结构清晰,建议: + +1. **保持import语句简洁**: 不需要显式import每个子目录 +2. **使用相对路径**: Swift会自动处理文件引用 +3. **更新文档**: 确保文档反映新的项目结构 + +## 验证步骤 + +### 1. 编译验证 +```bash +xcodebuild -project MyQrCode.xcodeproj -scheme MyQrCode -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' build +``` + +### 2. 功能测试 +- 扫描功能正常工作 +- 生成功能正常工作 +- 历史记录功能正常工作 +- 设置功能正常工作 +- 启动页面正常显示 + +### 3. 导航测试 +- 所有NavigationLink正常工作 +- 页面跳转无错误 +- 返回功能正常 + +## 总结 + +通过这次项目结构重构,MyQrCode应用获得了: + +- **更清晰的项目结构**: 按功能分组,便于理解和维护 +- **更好的代码组织**: 相关文件集中,减少查找时间 +- **更高的可扩展性**: 新功能可以很容易地集成到现有结构中 +- **更好的团队协作**: 开发者可以快速定位相关代码 + +这种结构化的组织方式为项目的长期维护和扩展奠定了良好的基础。