|
|
import SwiftUI
|
|
|
import AVFoundation
|
|
|
|
|
|
// MARK: - 条码位置标记覆盖层
|
|
|
struct CodePositionOverlay: View {
|
|
|
let detectedCodes: [DetectedCode]
|
|
|
let previewLayer: AVCaptureVideoPreviewLayer?
|
|
|
let onCodeSelected: (DetectedCode) -> Void
|
|
|
let onRescan: () -> Void
|
|
|
|
|
|
var body: some View {
|
|
|
GeometryReader { geometry in
|
|
|
ZStack {
|
|
|
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
|
|
|
|
|
|
// 重新扫描按钮 - 放在右上角
|
|
|
VStack {
|
|
|
HStack {
|
|
|
Spacer()
|
|
|
Button(action: {
|
|
|
logInfo("🔄 用户点击重新扫描按钮", className: "CodePositionOverlay")
|
|
|
|
|
|
// 添加触觉反馈
|
|
|
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
|
|
|
impactFeedback.impactOccurred()
|
|
|
|
|
|
onRescan()
|
|
|
}) {
|
|
|
HStack(spacing: 8) {
|
|
|
Image(systemName: "arrow.clockwise")
|
|
|
.font(.system(size: 18, weight: .semibold))
|
|
|
.rotationEffect(.degrees(0))
|
|
|
.animation(.easeInOut(duration: 0.3), value: true)
|
|
|
|
|
|
Text("rescan_button".localized)
|
|
|
.font(.system(size: 15, weight: .semibold))
|
|
|
}
|
|
|
.foregroundColor(.white)
|
|
|
.padding(.horizontal, 20)
|
|
|
.padding(.vertical, 10)
|
|
|
.background(
|
|
|
RoundedRectangle(cornerRadius: 25)
|
|
|
.fill(Color.blue.opacity(0.9))
|
|
|
.shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
|
|
|
)
|
|
|
}
|
|
|
.buttonStyle(RescanButtonStyle())
|
|
|
.padding(.trailing, 25)
|
|
|
.padding(.top, 25)
|
|
|
|
|
|
// 调试按钮:检查会话状态
|
|
|
#if DEBUG
|
|
|
Button(action: {
|
|
|
logInfo("🔍 调试:检查扫描会话状态", className: "CodePositionOverlay")
|
|
|
// 这里可以添加会话状态检查逻辑
|
|
|
}) {
|
|
|
Image(systemName: "info.circle")
|
|
|
.font(.system(size: 16, weight: .medium))
|
|
|
.foregroundColor(.yellow)
|
|
|
.padding(8)
|
|
|
.background(Color.black.opacity(0.6))
|
|
|
.clipShape(Circle())
|
|
|
}
|
|
|
.padding(.trailing, 10)
|
|
|
.padding(.top, 25)
|
|
|
#endif
|
|
|
}
|
|
|
Spacer()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.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)
|
|
|
}
|
|
|
|
|
|
guard previewLayer.session?.isRunning == true else {
|
|
|
logWarning("Preview layer session not running, using screen center", className: "CodePositionMarker")
|
|
|
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
|
|
|
))
|
|
|
|
|
|
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)
|
|
|
}
|
|
|
} |