You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

273 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import SwiftUI
import AVFoundation
import AudioToolbox
import Combine
import CoreData
// 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?
@State private var navigateToDetail = false
@State private var selectedHistoryItem: HistoryItem?
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
)
//
if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty {
CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes,
previewLayer: previewLayer,
onCodeSelected: handleCodeSelection
)
}
//
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()
}
)
}
}
.navigationTitle("扫描器")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(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))
}
.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")
// HistoryItem Core Data
let historyItem = createHistoryItem(from: selectedCode)
//
selectedHistoryItem = historyItem
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
}
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)
}
}
}
#if DEBUG
struct ScannerView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ScannerView()
}
}
}
#endif