Remove obsolete files related to localization and QR code functionality, including various views and utility classes. This cleanup enhances project maintainability by eliminating unused code and streamlining the overall structure.

main
v504 1 year ago
parent 51d83b4f96
commit f3344d6253

@ -171,7 +171,7 @@ class LanguageManager: ObservableObject {
} else {
// 首次启动,默认使用系统语言
currentLanguage = .system
let systemLanguage = detectSystemLanguage()
_ = detectSystemLanguage()
// 保存系统语言设置
UserDefaults.standard.set("system", forKey: languageKey)

@ -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
}
}
}

@ -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
}
}
}

@ -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)
}
}

@ -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")
}

@ -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<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: - 解码失败提示覆盖层
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

@ -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
}
}
}

@ -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())
}
}

@ -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)
)
}
}
}

@ -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()
}
}
}

@ -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应用获得了:
- **更清晰的项目结构**: 按功能分组,便于理解和维护
- **更好的代码组织**: 相关文件集中,减少查找时间
- **更高的可扩展性**: 新功能可以很容易地集成到现有结构中
- **更好的团队协作**: 开发者可以快速定位相关代码
这种结构化的组织方式为项目的长期维护和扩展奠定了良好的基础。
Loading…
Cancel
Save