|
|
import SwiftUI
|
|
|
import AVFoundation
|
|
|
import AudioToolbox
|
|
|
import Combine
|
|
|
import CoreData
|
|
|
import QRCode
|
|
|
import Vision
|
|
|
|
|
|
// MARK: - 主扫描视图
|
|
|
struct ScannerView: View {
|
|
|
@StateObject private var scannerViewModel = ScannerViewModel()
|
|
|
@State private var showPreviewPause = false
|
|
|
@State private var screenOrientation = UIDevice.current.orientation
|
|
|
@State private var previewLayer: AVCaptureVideoPreviewLayer?
|
|
|
@State private var navigateToDetail = false
|
|
|
@State private var selectedHistoryItem: HistoryItem?
|
|
|
|
|
|
// 图片解码相关状态
|
|
|
@State private var showImagePicker = false
|
|
|
@State private var isDecodingImage = false
|
|
|
@State private var decodedImageCodes: [DetectedCode] = []
|
|
|
@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.count > 1) || (decodedImageCodes.count > 1)),
|
|
|
detectedCodesCount: scannerViewModel.detectedCodes.count + decodedImageCodes.count,
|
|
|
onImageDecode: { showImagePicker = true }
|
|
|
)
|
|
|
|
|
|
// 条码位置标记覆盖层 - 只在需要用户选择时显示
|
|
|
if showPreviewPause &&
|
|
|
((!scannerViewModel.detectedCodes.isEmpty && scannerViewModel.detectedCodes.count > 1) ||
|
|
|
(!decodedImageCodes.isEmpty && decodedImageCodes.count > 1)) {
|
|
|
CodePositionOverlay(
|
|
|
detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes,
|
|
|
previewLayer: previewLayer,
|
|
|
onCodeSelected: handleCodeSelection
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// 测试按钮(调试用)- 只在需要用户选择时显示
|
|
|
if showPreviewPause &&
|
|
|
((scannerViewModel.detectedCodes.count > 1) || (decodedImageCodes.count > 1)) {
|
|
|
let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first
|
|
|
if let code = singleCode {
|
|
|
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("扫描器")
|
|
|
.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()
|
|
|
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 ?? "未知内容",
|
|
|
bounds: qrCode.bounds
|
|
|
)
|
|
|
}
|
|
|
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 = "图片中未检测到二维码或条形码"
|
|
|
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 ?? "未知内容"
|
|
|
|
|
|
logInfo("检测到条形码 #\(index + 1): 类型=\(barcodeType), 内容=\(content)", className: "ScannerView")
|
|
|
|
|
|
return DetectedCode(
|
|
|
type: barcodeType,
|
|
|
content: content,
|
|
|
bounds: observation.boundingBox
|
|
|
)
|
|
|
}
|
|
|
} 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<String> = []
|
|
|
|
|
|
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: - 图片选择器(兼容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)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 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("解码失败")
|
|
|
.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("重新选择图片")
|
|
|
.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()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
#endif |