// // XuXuWebView.swift // XuXuCheck // // Created by henseycai on 2025/6/25. // import Foundation import SwiftUI struct XuXuWebView: View { // @Bindable var webViewModel = WebViewModel() var currentUrl: String @Environment(\.dismiss) private var dissmiss @Environment(LoginStatus.self) var loginStatus @Environment(WebViewModel.self) var webViewModel @Environment(Status.self) var status @State private var showingNaiveSafariViewFullScreenCover = false @State var imageViewModel = ImagePickerViewModel() @State var iScanQrPresented = false @State private var scannedCode: String? /// 顶部留白高度(pt)。 /// 网页的头部是按"浏览器里已经让开状态栏"来设计的,把 webview 直接铺到物理顶端(y=0)后, /// 它的标题会顶进灵动岛,左上角的返回键也会落进系统手势区里导致点不到。 /// 这里稍微下移一点点:标题和返回键都能落在灵动岛下方,又不像原来那样露出一大条蓝色空白。 /// 调整手感只需要改这一个数值。 private let topSafeInset: CGFloat = 20 let actions = ["changeText('hello from Swift')", "goBack()"] static var date: String { let formatter = DateFormatter() formatter.dateStyle = .medium return formatter.string(from: Date()) } var body: some View { ZStack{ // 网页的底色层:必须铺满四周(含底部)。 // 它就是 webview 后面那层背景,底部漏掉的话,webview 停在安全区上时 // 最下面 34pt 露出的会是 XuXuWebView 外面父视图的白底,在网页浅色背景下面很显眼。 // 键盘那一路单独忽略,是为了键盘弹出时这层底色不被顶起来。 Color(hex: 0x395cff).ignoresSafeArea(.keyboard,edges: .bottom) .ignoresSafeArea(.container,edges: [.top,.horizontal,.bottom]) VStack { WebViewRepresentable(currentUrl: currentUrl) .cornerRadius(1) .onChange(of: webViewModel.hasNewCommand) { oldState, newState in print(webViewModel.hasNewCommand) if oldState == false && newState == true { //https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-some-code-when-state-changes-using-onchange //https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:) switch webViewModel.jsCommand?.lowercased() { case "print": break case "tel": break case "clearcache": break case "setdefaultmember": break case "changeself": break case "goback": // print(" go back") status.showNavDetail = false // status.showNavDetail.toggle() dissmiss() break case "scancode": iScanQrPresented.toggle() break case "selectphoto": print("i will select photo") imageViewModel.choosePhoto() break case "selectcamera": print("i will take photo") imageViewModel.takePhoto() break case "scanpaper": showingNaiveSafariViewFullScreenCover.toggle() break case "logout": print("logout") // 网页里退出登录后要退出本页回到 Mine,和 goback 一样属"正常离开", // 所以这里立刻把导航条恢复;onDisappear 那层是兜底, // 万一将来又加一条不以 goback/logout 结束的退出路径也不会漏。 status.showNavDetail = false dissmiss() break case "login": break default: break } webViewModel.hasNewCommand = false } } .onChange(of: imageViewModel.isPresentingImagePicker){ oldState, newState in if oldState == true && newState == false { if let image = imageViewModel.selectedImage { // webViewModel.callJsImage(imageString: image.jpegBase64) if let base64 = image.getJpegBase64(){ webViewModel.callJsImage(imageString: base64) } //webViewModel.callJsImage(imageString: image.getJpegBase64()) // webViewModel.callJsImage(imageString: convertImageToBase64(image: image)) } } } .fullScreenCover(isPresented: $imageViewModel.isPresentingImagePicker, content: { ImagePicker(sourceType: imageViewModel.sourceType, completionHandler: imageViewModel.didSelectImage) }) .fullScreenCover( isPresented: $showingNaiveSafariViewFullScreenCover ) { ScanView() } .sheet(isPresented: $iScanQrPresented) { CodeScannerView(codeTypes: [.qr]) { response in if case let .success(result) = response { scannedCode = result.string if let code = scannedCode { webViewModel.callJsBarcode(named: code) } iScanQrPresented = false } } } } // 顶部(半通屏):先把内容下移 topSafeInset,再让容器铺到物理顶端。 // 顺序不能反 —— 先忽略安全区再加 .padding,等于给一个已经满屏的视图加外衬距,底部会被挤出容器。 // 这样网页从灵动岛下方开始渲染,上面露出的那一小条就是本页的蓝色底。 // 底部(全通屏):必须一直铺到物理底边。原先这里只忽略 .top,webview 停在底部安全区上, // 最下面 34pt 没有任何一层去画,露出的是父视图的白底。 // 顶部和底部的现象都是"留白",但成因相反:顶部是让位让多了(要收),底部是压根没铺到(要铺满)。 // 判断依据是这块区域显示的是谁的颜色,详见 docs/webview-safe-area.md .padding(.top, topSafeInset) .ignoresSafeArea(.container, edges: [.top, .bottom]) // 底部导航的藏/显示必须成对绑在本视图的生命周期上。 // 注意 showNavDetail 名字是反的:true = 正在看详情页 = 把 TabBar 推出屏幕 // (ContentView 里 `TabBar().offset(y: showNavDetail ? 200 : 0)`)。 // 原先只有 onAppear 这一半,"恢复"是由网页上报 goback 命令顺带做的 —— // 于是任何不以 goback 结束的退出路径都会把导航条留在屏幕外 // (Mine → 设置 → 退出登录 就是:那个分支只 dismiss,没有恢复)。 .onAppear { //status.showNavDetail.toggle() status.showNavDetail = true } .onDisappear { status.showNavDetail = false } } // .frame(maxWidth: .infinity,maxHeight: .infinity) // .edgesIgnoringSafeArea(.all) } } #Preview { XuXuWebView(currentUrl: API.webrecordUrl + GetGeneralRequestParams()) }