import AVFoundation import SwiftUI import VisionKit // 扫码登录他端(#8):本机已登录,用相机扫另一台设备显示的登录二维码(/link?r=&c=),解析校验后把 // requestId / code 交引擎走批准流程。用 VisionKit 的 DataScannerViewController(系统级扫码 UI,免手搓 // AVFoundation 会话)。模拟器无相机 → isSupported 为假,回落「相机不可用」提示。 struct ScanSheet: View { // 扫到合法登录二维码时回调 (requestId, code),随后自动关闭扫码页。 let onScanned: (String, String) -> Void @Environment(\.dismiss) private var dismiss // nil=权限询问中;true/false=已授权 / 被拒。 @State private var authorized: Bool? @State private var invalid = false var body: some View { NavigationStack { content .navigationTitle(t("ios.scan.title")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button(t("common.cancel")) { dismiss() } } } } .task { await requestCamera() } } @ViewBuilder private var content: some View { if authorized == nil { ProgressView() } else if authorized == true, DataScannerViewController.isSupported { scanner } else { ContentUnavailableView(t("ios.scan.cameraDenied"), systemImage: "camera.fill") } } private var scanner: some View { ZStack(alignment: .bottom) { LoginScannerView(onScan: handleScan) VStack(spacing: 8) { if invalid { Text(t("ios.scan.invalid")) .font(.callout) .padding(.vertical, 8) .padding(.horizontal, 14) .background(.red.opacity(0.85), in: Capsule()) .foregroundStyle(.white) } Text(t("ios.scan.hint")) .font(.footnote) .multilineTextAlignment(.center) .padding(.vertical, 8) .padding(.horizontal, 14) .background(.black.opacity(0.5), in: Capsule()) .foregroundStyle(.white) } .padding(.bottom, 40) } .ignoresSafeArea(edges: .bottom) } private func requestCamera() async { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: authorized = true case .notDetermined: authorized = await AVCaptureDevice.requestAccess(for: .video) default: authorized = false } } // 扫到一个码:解析校验;合法即回调并关闭,非法则提示「无法识别」并继续扫描。 private func handleScan(_ raw: String) { guard let parsed = parseLinkApprovalUrl(raw) else { invalid = true return } onScanned(parsed.requestId, parsed.code) dismiss() } } // 解析扫到的登录二维码(/link?r=&c=):仅校验路径为 /link 且 r/c 非空。origin 不强校验——r/c 一律 // 发往本端自己的后端,外站码只会 404(无开放重定向风险),对齐 web 桌面侧的放宽逻辑(见 net/qr.ts)。 private func parseLinkApprovalUrl(_ raw: String) -> (requestId: String, code: String)? { guard let comp = URLComponents(string: raw), comp.path == "/link" else { return nil } let items = comp.queryItems ?? [] guard let r = items.first(where: { $0.name == "r" })?.value, !r.isEmpty, let c = items.first(where: { $0.name == "c" })?.value, !c.isEmpty else { return nil } return (r, c) } // DataScannerViewController 的 SwiftUI 封装:识别 QR 码,首个命中即回调(done 闸门避免重复触发)。 private struct LoginScannerView: UIViewControllerRepresentable { let onScan: (String) -> Void func makeUIViewController(context: Context) -> DataScannerViewController { let scanner = DataScannerViewController( recognizedDataTypes: [ .barcode(symbologies: [ .qr ]) ], qualityLevel: .balanced, isHighFrameRateTrackingEnabled: false, isHighlightingEnabled: true) scanner.delegate = context.coordinator return scanner } func updateUIViewController(_ vc: DataScannerViewController, context: Context) { try? vc.startScanning() } func makeCoordinator() -> Coordinator { Coordinator(onScan: onScan) } final class Coordinator: NSObject, DataScannerViewControllerDelegate { private let onScan: (String) -> Void private var done = false init(onScan: @escaping (String) -> Void) { self.onScan = onScan } func dataScanner(_ scanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) { guard !done else { return } for item in addedItems { if case let .barcode(barcode) = item, let str = barcode.payloadStringValue { done = true onScan(str) return } } } } }