|
|
import SwiftUI
|
|
|
import AVFoundation
|
|
|
import AudioToolbox
|
|
|
import Combine
|
|
|
import CoreData
|
|
|
import QRCode
|
|
|
|
|
|
// MARK: - 主扫描视图
|
|
|
struct ScannerView: View {
|
|
|
@StateObject private var scannerViewModel = ScannerViewModel()
|
|
|
@State private var showPreviewPause = false
|
|
|
@State private var 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?
|
|
|
|
|
|
// 图片解码相关状态
|
|
|
@State private var showImagePicker = false
|
|
|
@State private var isDecodingImage = false
|
|
|
@State private var decodedImageCodes: [DetectedCode] = []
|
|
|
|
|
|
var body: some View {
|
|
|
ZStack {
|
|
|
// 相机权限检查
|
|
|
if scannerViewModel.cameraAuthorizationStatus == .authorized {
|
|
|
// 相机预览层
|
|
|
CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer)
|
|
|
.ignoresSafeArea()
|
|
|
|
|
|
// 扫描界面覆盖层
|
|
|
ScanningOverlayView(
|
|
|
showPreviewPause: showPreviewPause,
|
|
|
selectedStyle: $selectedScanningStyle,
|
|
|
detectedCodesCount: scannerViewModel.detectedCodes.count,
|
|
|
onImageDecode: { showImagePicker = true }
|
|
|
)
|
|
|
|
|
|
// 条码位置标记覆盖层
|
|
|
if showPreviewPause && (!scannerViewModel.detectedCodes.isEmpty || !decodedImageCodes.isEmpty) {
|
|
|
CodePositionOverlay(
|
|
|
detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes,
|
|
|
previewLayer: previewLayer,
|
|
|
onCodeSelected: handleCodeSelection
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// 测试按钮(调试用)
|
|
|
if showPreviewPause && (scannerViewModel.detectedCodes.count + decodedImageCodes.count) == 1 {
|
|
|
let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first
|
|
|
if let code = singleCode {
|
|
|
TestAutoSelectButton(
|
|
|
detectedCode: code,
|
|
|
onSelect: handleCodeSelection
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
// 权限未授权时的UI
|
|
|
CameraPermissionView(
|
|
|
authorizationStatus: scannerViewModel.cameraAuthorizationStatus,
|
|
|
onRequestPermission: {
|
|
|
scannerViewModel.refreshCameraPermission()
|
|
|
},
|
|
|
onOpenSettings: {
|
|
|
scannerViewModel.openSettings()
|
|
|
}
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
.navigationTitle("扫描器")
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
.navigationBarBackButtonHidden(false)
|
|
|
.sheet(isPresented: $showImagePicker) {
|
|
|
ImagePicker(onImageSelected: handleImageDecodeResult)
|
|
|
}
|
|
|
.toolbar {
|
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
|
// 手电筒按钮 - 只在相机权限已授权时显示
|
|
|
if scannerViewModel.cameraAuthorizationStatus == .authorized && scannerViewModel.isTorchAvailable {
|
|
|
Button(action: {
|
|
|
logInfo("🔦 用户点击手电筒按钮", className: "ScannerView")
|
|
|
|
|
|
// 添加触觉反馈
|
|
|
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
|
|
|
impactFeedback.impactOccurred()
|
|
|
|
|
|
scannerViewModel.toggleTorch()
|
|
|
}) {
|
|
|
Image(systemName: scannerViewModel.isTorchOn ? "bolt.fill" : "bolt")
|
|
|
.font(.system(size: 18, weight: .semibold))
|
|
|
.foregroundColor(scannerViewModel.isTorchOn ? .yellow : .blue)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
// 重新扫描按钮 - 只在预览暂停状态时显示
|
|
|
if showPreviewPause {
|
|
|
Button(action: {
|
|
|
logInfo("🔄 用户点击工具栏重新扫描按钮", className: "ScannerView")
|
|
|
|
|
|
// 添加触觉反馈
|
|
|
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
|
|
|
impactFeedback.impactOccurred()
|
|
|
|
|
|
resetToScanning()
|
|
|
}) {
|
|
|
HStack(spacing: 6) {
|
|
|
Image(systemName: "arrow.clockwise")
|
|
|
.font(.system(size: 16, weight: .semibold))
|
|
|
|
|
|
Text("rescan_button".localized)
|
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
}
|
|
|
.foregroundColor(.blue)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.onAppear {
|
|
|
// 只有在相机权限已授权时才启动扫描
|
|
|
if scannerViewModel.cameraAuthorizationStatus == .authorized {
|
|
|
scannerViewModel.startScanning()
|
|
|
}
|
|
|
}
|
|
|
.onDisappear {
|
|
|
scannerViewModel.stopScanning()
|
|
|
// 确保退出时关闭手电筒
|
|
|
if scannerViewModel.isTorchOn {
|
|
|
scannerViewModel.turnOffTorch()
|
|
|
}
|
|
|
}
|
|
|
.alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) {
|
|
|
Button("OK") { }
|
|
|
} message: {
|
|
|
Text("scan_error_message".localized)
|
|
|
}
|
|
|
.onReceive(scannerViewModel.$detectedCodes) { codes in
|
|
|
handleDetectedCodes(codes)
|
|
|
}
|
|
|
.onReceive(scannerViewModel.$cameraAuthorizationStatus) { status in
|
|
|
if status == .authorized {
|
|
|
logInfo("🎯 相机权限已授权,启动扫描", className: "ScannerView")
|
|
|
scannerViewModel.startScanning()
|
|
|
}
|
|
|
}
|
|
|
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
|
|
handleOrientationChange()
|
|
|
}
|
|
|
.background(
|
|
|
NavigationLink(
|
|
|
destination: Group {
|
|
|
if let historyItem = selectedHistoryItem {
|
|
|
QRCodeDetailView(historyItem: historyItem)
|
|
|
.onDisappear {
|
|
|
// 从详情页返回时,重新开始扫描
|
|
|
logInfo("🔄 从详情页返回,重新开始扫描", className: "ScannerView")
|
|
|
resetToScanning()
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
isActive: $navigateToDetail
|
|
|
) {
|
|
|
EmptyView()
|
|
|
}
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// MARK: - 私有方法
|
|
|
|
|
|
private func handleDetectedCodes(_ codes: [DetectedCode]) {
|
|
|
guard !codes.isEmpty else { return }
|
|
|
|
|
|
logInfo("检测到条码数量: \(codes.count)", className: "ScannerView")
|
|
|
|
|
|
if codes.count == 1 {
|
|
|
logInfo("单个条码,准备自动选择", className: "ScannerView")
|
|
|
pauseForPreview()
|
|
|
autoSelectSingleCode(code: codes[0])
|
|
|
} else {
|
|
|
logInfo("多个条码,等待用户选择", className: "ScannerView")
|
|
|
pauseForPreview()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private func handleOrientationChange() {
|
|
|
screenOrientation = UIDevice.current.orientation
|
|
|
logInfo("Screen orientation changed to: \(screenOrientation.rawValue)", className: "ScannerView")
|
|
|
}
|
|
|
|
|
|
private func handleCodeSelection(_ selectedCode: DetectedCode) {
|
|
|
logInfo("🎯 ScannerView 收到条码选择回调", className: "ScannerView")
|
|
|
logInfo(" 选择的条码ID: \(selectedCode.id)", className: "ScannerView")
|
|
|
logInfo(" 选择的条码类型: \(selectedCode.type)", className: "ScannerView")
|
|
|
logInfo(" 选择的条码内容: \(selectedCode.content)", className: "ScannerView")
|
|
|
logInfo(" 选择的条码位置: \(selectedCode.bounds)", className: "ScannerView")
|
|
|
|
|
|
// 停止扫描功能,避免在详情页面继续扫描
|
|
|
scannerViewModel.stopScanning()
|
|
|
logInfo("🛑 已停止扫描功能", className: "ScannerView")
|
|
|
|
|
|
// 创建 HistoryItem 并保存到 Core Data
|
|
|
let historyItem = createHistoryItem(from: selectedCode)
|
|
|
|
|
|
// 延迟一小段时间确保扫描完全停止后再跳转
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
|
// 设置选中的历史记录项并导航到详情页
|
|
|
self.selectedHistoryItem = historyItem
|
|
|
self.navigateToDetail = true
|
|
|
}
|
|
|
|
|
|
// 发送通知(保持向后兼容)
|
|
|
let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)"
|
|
|
logInfo(" 格式化结果: \(formattedResult)", className: "ScannerView")
|
|
|
NotificationCenter.default.post(name: .scannerDidScanCode, object: formattedResult)
|
|
|
}
|
|
|
|
|
|
private func createHistoryItem(from detectedCode: DetectedCode) -> HistoryItem {
|
|
|
let context = CoreDataManager.shared.container.viewContext
|
|
|
let historyItem = HistoryItem(context: context)
|
|
|
|
|
|
historyItem.id = UUID()
|
|
|
historyItem.content = detectedCode.content
|
|
|
historyItem.dataType = DataType.qrcode.rawValue
|
|
|
historyItem.dataSource = DataSource.scanned.rawValue
|
|
|
historyItem.createdAt = Date()
|
|
|
historyItem.isFavorite = false
|
|
|
|
|
|
// 根据条码类型设置相应的类型字段
|
|
|
if detectedCode.type.lowercased().contains("qr") || detectedCode.type.lowercased().contains("二维码") {
|
|
|
// 尝试解析二维码类型
|
|
|
let parsedData = QRCodeParser.parseQRCode(detectedCode.content)
|
|
|
historyItem.qrCodeType = parsedData.type.rawValue
|
|
|
} else {
|
|
|
// 条形码类型
|
|
|
historyItem.barcodeType = detectedCode.type
|
|
|
}
|
|
|
|
|
|
// 保存到 Core Data
|
|
|
CoreDataManager.shared.addHistoryItem(historyItem)
|
|
|
|
|
|
logInfo("✅ 已创建并保存历史记录项", className: "ScannerView")
|
|
|
return historyItem
|
|
|
}
|
|
|
|
|
|
private func pauseForPreview() {
|
|
|
showPreviewPause = true
|
|
|
// 暂停相机功能,防止在预览暂停时继续拍照
|
|
|
scannerViewModel.pauseCamera()
|
|
|
}
|
|
|
|
|
|
private func resetToScanning() {
|
|
|
logInfo("🔄 ScannerView 开始重置到扫描状态", className: "ScannerView")
|
|
|
|
|
|
// 重置UI状态
|
|
|
showPreviewPause = false
|
|
|
|
|
|
// 重置扫描状态
|
|
|
scannerViewModel.resetDetection()
|
|
|
|
|
|
// 重置图片解码状态
|
|
|
resetImageDecodeState()
|
|
|
|
|
|
// 恢复相机功能并重新开始扫描
|
|
|
scannerViewModel.resumeCamera()
|
|
|
|
|
|
logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView")
|
|
|
}
|
|
|
|
|
|
private func autoSelectSingleCode(code: DetectedCode) {
|
|
|
logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView")
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
|
let totalCodes = self.scannerViewModel.detectedCodes.count + self.decodedImageCodes.count
|
|
|
guard self.showPreviewPause && totalCodes == 1 else {
|
|
|
logInfo("条件不满足,取消自动选择", className: "ScannerView")
|
|
|
return
|
|
|
}
|
|
|
|
|
|
logInfo("条件满足,执行自动选择", className: "ScannerView")
|
|
|
self.handleCodeSelection(code)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 图片解码相关方法
|
|
|
|
|
|
/// 处理图片解码结果
|
|
|
private func handleImageDecodeResult(_ image: UIImage) {
|
|
|
isDecodingImage = true
|
|
|
decodedImageCodes.removeAll()
|
|
|
|
|
|
logInfo("🔍 开始解码图片", className: "ScannerView")
|
|
|
|
|
|
// 在后台线程进行解码
|
|
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
|
|
|
|
// 使用QRCode库检测二维码
|
|
|
let detected = QRCode.DetectQRCodes(in: image)
|
|
|
if detected.count > 0 {
|
|
|
logInfo("✅ 检测到 \(detected.count) 个二维码", className: "ScannerView")
|
|
|
|
|
|
let results = detected.enumerated().map { index, qrCode in
|
|
|
DetectedCode(
|
|
|
type: "QR Code",
|
|
|
content: qrCode.messageString ?? "未知内容",
|
|
|
bounds: qrCode.bounds
|
|
|
)
|
|
|
}
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
self.decodedImageCodes = results
|
|
|
self.isDecodingImage = false
|
|
|
self.pauseForPreview()
|
|
|
logInfo("✅ 图片解码完成,共 \(results.count) 个结果", className: "ScannerView")
|
|
|
|
|
|
// 如果只有一个二维码,自动选择并跳转
|
|
|
if results.count == 1 {
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
|
self.handleCodeSelection(results[0])
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
DispatchQueue.main.async {
|
|
|
self.isDecodingImage = false
|
|
|
logWarning("❌ 图片中未检测到二维码", className: "ScannerView")
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// 重置图片解码状态
|
|
|
private func resetImageDecodeState() {
|
|
|
decodedImageCodes.removeAll()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 图片选择器(兼容iOS 15)
|
|
|
struct ImagePicker: UIViewControllerRepresentable {
|
|
|
let onImageSelected: (UIImage) -> Void
|
|
|
|
|
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
|
|
let picker = UIImagePickerController()
|
|
|
picker.delegate = context.coordinator
|
|
|
picker.sourceType = .photoLibrary
|
|
|
picker.allowsEditing = false
|
|
|
return picker
|
|
|
}
|
|
|
|
|
|
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
|
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
|
Coordinator(self)
|
|
|
}
|
|
|
|
|
|
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
|
|
let parent: ImagePicker
|
|
|
|
|
|
init(_ parent: ImagePicker) {
|
|
|
self.parent = parent
|
|
|
}
|
|
|
|
|
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
|
|
if let image = info[.originalImage] as? UIImage {
|
|
|
parent.onImageSelected(image)
|
|
|
}
|
|
|
picker.dismiss(animated: true)
|
|
|
}
|
|
|
|
|
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
|
|
picker.dismiss(animated: true)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#if DEBUG
|
|
|
struct ScannerView_Previews: PreviewProvider {
|
|
|
static var previews: some View {
|
|
|
NavigationView {
|
|
|
ScannerView()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
#endif |