充电管理
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
56046A0096E81BD9D3480337 /* ScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98591BF58FB3CB969F8A277 /* ScanViewController.swift */; };
|
||||||
5C5E148BC19DCC3A1F56E894 /* libPods-OrderSchedulingNotificationService.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F4D22CCB18DAC27B34DA947D /* libPods-OrderSchedulingNotificationService.a */; };
|
5C5E148BC19DCC3A1F56E894 /* libPods-OrderSchedulingNotificationService.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F4D22CCB18DAC27B34DA947D /* libPods-OrderSchedulingNotificationService.a */; };
|
||||||
791887422A7CCCCD007EA0C1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 791887402A7CCCCD007EA0C1 /* AppDelegate.swift */; };
|
791887422A7CCCCD007EA0C1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 791887402A7CCCCD007EA0C1 /* AppDelegate.swift */; };
|
||||||
791887432A7CCCCD007EA0C1 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 791887412A7CCCCD007EA0C1 /* SceneDelegate.swift */; };
|
791887432A7CCCCD007EA0C1 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 791887412A7CCCCD007EA0C1 /* SceneDelegate.swift */; };
|
||||||
@@ -267,6 +268,7 @@
|
|||||||
BD034FA1DD7C17CCBA6B0E5D /* Pods-OrderScheduling.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OrderScheduling.release.xcconfig"; path = "Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling.release.xcconfig"; sourceTree = "<group>"; };
|
BD034FA1DD7C17CCBA6B0E5D /* Pods-OrderScheduling.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OrderScheduling.release.xcconfig"; path = "Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
BEFB1D0033356A04B31FA263 /* Pods-OrderScheduling.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OrderScheduling.prerelease.xcconfig"; path = "Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling.prerelease.xcconfig"; sourceTree = "<group>"; };
|
BEFB1D0033356A04B31FA263 /* Pods-OrderScheduling.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OrderScheduling.prerelease.xcconfig"; path = "Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling.prerelease.xcconfig"; sourceTree = "<group>"; };
|
||||||
DF9CE87B72CAB01C1BFFDCF9 /* Pods-OrderSchedulingNotificationService.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OrderSchedulingNotificationService.prerelease.xcconfig"; path = "Target Support Files/Pods-OrderSchedulingNotificationService/Pods-OrderSchedulingNotificationService.prerelease.xcconfig"; sourceTree = "<group>"; };
|
DF9CE87B72CAB01C1BFFDCF9 /* Pods-OrderSchedulingNotificationService.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OrderSchedulingNotificationService.prerelease.xcconfig"; path = "Target Support Files/Pods-OrderSchedulingNotificationService/Pods-OrderSchedulingNotificationService.prerelease.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
E98591BF58FB3CB969F8A277 /* ScanViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScanViewController.swift; sourceTree = "<group>"; };
|
||||||
F4D22CCB18DAC27B34DA947D /* libPods-OrderSchedulingNotificationService.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OrderSchedulingNotificationService.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
F4D22CCB18DAC27B34DA947D /* libPods-OrderSchedulingNotificationService.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OrderSchedulingNotificationService.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -426,6 +428,7 @@
|
|||||||
children = (
|
children = (
|
||||||
791887942A80C361007EA0C1 /* WebViewController.swift */,
|
791887942A80C361007EA0C1 /* WebViewController.swift */,
|
||||||
794FBB1E2A92F7C300D57BB8 /* WebViewTool.swift */,
|
794FBB1E2A92F7C300D57BB8 /* WebViewTool.swift */,
|
||||||
|
E98591BF58FB3CB969F8A277 /* ScanViewController.swift */,
|
||||||
);
|
);
|
||||||
path = WebView;
|
path = WebView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1086,14 +1089,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OrderScheduling/Pods-OrderScheduling-resources.sh\"\n";
|
||||||
@@ -1212,6 +1211,7 @@
|
|||||||
79CE24AA2EF52EAF007FCF90 /* IdentityAlertView.swift in Sources */,
|
79CE24AA2EF52EAF007FCF90 /* IdentityAlertView.swift in Sources */,
|
||||||
79D964E02E77FE2100309946 /* AppealInputViewController.swift in Sources */,
|
79D964E02E77FE2100309946 /* AppealInputViewController.swift in Sources */,
|
||||||
791887A62A80D50C007EA0C1 /* ParametersList.swift in Sources */,
|
791887A62A80D50C007EA0C1 /* ParametersList.swift in Sources */,
|
||||||
|
56046A0096E81BD9D3480337 /* ScanViewController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
//
|
||||||
|
// ScanViewController.swift
|
||||||
|
// OrderScheduling
|
||||||
|
//
|
||||||
|
// Created by 中道 on 2026/7/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
import SnapKit
|
||||||
|
import DDAutoUIKit_Private
|
||||||
|
import DDControlsKit_Private
|
||||||
|
import DDToastKit_Private
|
||||||
|
|
||||||
|
class ScanViewController: ZDViewController {
|
||||||
|
|
||||||
|
private lazy var session = AVCaptureSession()
|
||||||
|
private var previewLayer: AVCaptureVideoPreviewLayer?
|
||||||
|
private var scanCompletion: ((String?) -> Void)?
|
||||||
|
|
||||||
|
private let scanRectView = UIView()
|
||||||
|
private let tipLabel = UILabel()
|
||||||
|
private let backButton = UIButton(type: .custom)
|
||||||
|
private let torchButton = UIButton(type: .custom)
|
||||||
|
|
||||||
|
init(completion: @escaping (String?) -> Void) {
|
||||||
|
self.scanCompletion = completion
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
setupUI()
|
||||||
|
checkPermissionAndSetupCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
navigationController?.navigationBar.isHidden = true
|
||||||
|
if session.isRunning == false {
|
||||||
|
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||||
|
self?.session.startRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
navigationController?.navigationBar.isHidden = false
|
||||||
|
if session.isRunning == true {
|
||||||
|
session.stopRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
view.backgroundColor = .black
|
||||||
|
|
||||||
|
scanRectView.layer.borderColor = UIColor.hex("00FF00").cgColor
|
||||||
|
scanRectView.layer.borderWidth = 1.0
|
||||||
|
view.addSubview(scanRectView)
|
||||||
|
|
||||||
|
tipLabel.text = "将二维码/条码放入框内,即可自动扫描"
|
||||||
|
tipLabel.textColor = .white
|
||||||
|
tipLabel.font = UIFont.systemFont(ofSize: 14)
|
||||||
|
tipLabel.textAlignment = .center
|
||||||
|
view.addSubview(tipLabel)
|
||||||
|
|
||||||
|
backButton.setImage(UIImage(named: "nav_back_white") ?? UIImage(systemName: "chevron.left"), for: .normal)
|
||||||
|
backButton.tintColor = .white
|
||||||
|
backButton.addTarget(self, action: #selector(backAction), for: .touchUpInside)
|
||||||
|
view.addSubview(backButton)
|
||||||
|
|
||||||
|
torchButton.setTitle("手电筒", for: .normal)
|
||||||
|
torchButton.setTitleColor(.white, for: .normal)
|
||||||
|
torchButton.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
||||||
|
torchButton.addTarget(self, action: #selector(torchAction), for: .touchUpInside)
|
||||||
|
view.addSubview(torchButton)
|
||||||
|
|
||||||
|
scanRectView.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.width.height.equalTo(auto(260))
|
||||||
|
}
|
||||||
|
|
||||||
|
tipLabel.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.top.equalTo(scanRectView.snp.bottom).offset(auto(20))
|
||||||
|
make.left.right.equalToSuperview().inset(auto(20))
|
||||||
|
}
|
||||||
|
|
||||||
|
backButton.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(auto(10))
|
||||||
|
make.left.equalTo(auto(15))
|
||||||
|
make.width.height.equalTo(auto(44))
|
||||||
|
}
|
||||||
|
|
||||||
|
torchButton.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.top.equalTo(tipLabel.snp.bottom).offset(auto(30))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkPermissionAndSetupCamera() {
|
||||||
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
|
switch status {
|
||||||
|
case .authorized:
|
||||||
|
setupCamera()
|
||||||
|
case .notDetermined:
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if granted {
|
||||||
|
self?.setupCamera()
|
||||||
|
} else {
|
||||||
|
self?.showPermissionAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
showPermissionAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupCamera() {
|
||||||
|
guard let device = AVCaptureDevice.default(for: .video) else {
|
||||||
|
showErrorAndBack("无法获取相机设备")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let input = try AVCaptureDeviceInput(device: device)
|
||||||
|
let output = AVCaptureMetadataOutput()
|
||||||
|
output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
||||||
|
|
||||||
|
session.beginConfiguration()
|
||||||
|
if session.canAddInput(input) {
|
||||||
|
session.addInput(input)
|
||||||
|
}
|
||||||
|
if session.canAddOutput(output) {
|
||||||
|
session.addOutput(output)
|
||||||
|
}
|
||||||
|
output.metadataObjectTypes = [.qr, .ean13, .ean8, .code128, .code39, .code93, .pdf417]
|
||||||
|
session.commitConfiguration()
|
||||||
|
|
||||||
|
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||||
|
previewLayer.videoGravity = .resizeAspectFill
|
||||||
|
previewLayer.frame = view.bounds
|
||||||
|
view.layer.insertSublayer(previewLayer, at: 0)
|
||||||
|
self.previewLayer = previewLayer
|
||||||
|
|
||||||
|
updateScanRect()
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||||
|
self?.session.startRunning()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showErrorAndBack("相机初始化失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
previewLayer?.frame = view.bounds
|
||||||
|
updateScanRect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScanRect() {
|
||||||
|
guard let previewLayer = previewLayer else { return }
|
||||||
|
let rect = scanRectView.frame
|
||||||
|
let scanRect = previewLayer.metadataOutputRectConverted(fromLayerRect: rect)
|
||||||
|
if let output = session.outputs.first as? AVCaptureMetadataOutput {
|
||||||
|
output.rectOfInterest = scanRect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func backAction() {
|
||||||
|
scanCompletion?(nil)
|
||||||
|
navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func torchAction() {
|
||||||
|
guard let device = AVCaptureDevice.default(for: .video), device.hasTorch else { return }
|
||||||
|
do {
|
||||||
|
try device.lockForConfiguration()
|
||||||
|
device.torchMode = device.torchMode == .on ? .off : .on
|
||||||
|
device.unlockForConfiguration()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showPermissionAlert() {
|
||||||
|
let alert = UIAlertController(title: "相机权限未开启", message: "请在设置中开启相机权限以使用扫码功能", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
|
||||||
|
self?.scanCompletion?(nil)
|
||||||
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
})
|
||||||
|
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
|
||||||
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showErrorAndBack(_ message: String) {
|
||||||
|
view.dd_makeToast(message)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||||||
|
self?.scanCompletion?(nil)
|
||||||
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ScanViewController: AVCaptureMetadataOutputObjectsDelegate {
|
||||||
|
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
||||||
|
guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
||||||
|
let code = object.stringValue else { return }
|
||||||
|
session.stopRunning()
|
||||||
|
scanCompletion?(code)
|
||||||
|
navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,25 @@ import DDLogKit_Private
|
|||||||
import DDToastKit_Private
|
import DDToastKit_Private
|
||||||
import DDControlsKit_Private
|
import DDControlsKit_Private
|
||||||
import DDUIKit
|
import DDUIKit
|
||||||
|
import SafariServices
|
||||||
|
import CoreLocation
|
||||||
|
import DDMAMapKit_Private
|
||||||
|
|
||||||
|
let WebViewPayCallbackNotification = NSNotification.Name(rawValue: "WebViewPayCallbackNotification")
|
||||||
|
|
||||||
class WebViewController : ZDViewController {
|
class WebViewController : ZDViewController {
|
||||||
lazy var webView = WKWebView.init()
|
lazy var webView: WKWebView = {
|
||||||
|
let config = WKWebViewConfiguration()
|
||||||
|
config.userContentController = WKUserContentController()
|
||||||
|
|
||||||
|
// 注入环境标识
|
||||||
|
let script = WKUserScript(source: "window.__IN_APP_WEBVIEW__ = true;", injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||||
|
config.userContentController.addUserScript(script)
|
||||||
|
|
||||||
|
let webView = WKWebView(frame: .zero, configuration: config)
|
||||||
|
return webView
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var statusBarBackgroundView = DDView.init()
|
lazy var statusBarBackgroundView = DDView.init()
|
||||||
var showNavBar : Bool
|
var showNavBar : Bool
|
||||||
public var vcTitle : String?
|
public var vcTitle : String?
|
||||||
@@ -23,6 +39,9 @@ class WebViewController : ZDViewController {
|
|||||||
var screenEdgePanGestureRecognizerEnable : Bool
|
var screenEdgePanGestureRecognizerEnable : Bool
|
||||||
public var disappearHandler: (() -> Void)?
|
public var disappearHandler: (() -> Void)?
|
||||||
|
|
||||||
|
private let locationManager = CLLocationManager()
|
||||||
|
private var locationCallback: String?
|
||||||
|
|
||||||
public init(showNavBar:Bool = true,title:String?,url:String,screenEdgePanGestureRecognizerEnable:Bool = true) {
|
public init(showNavBar:Bool = true,title:String?,url:String,screenEdgePanGestureRecognizerEnable:Bool = true) {
|
||||||
self.showNavBar = showNavBar
|
self.showNavBar = showNavBar
|
||||||
self.vcTitle = title
|
self.vcTitle = title
|
||||||
@@ -67,6 +86,11 @@ class WebViewController : ZDViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.dd_showHUD()
|
view.dd_showHUD()
|
||||||
|
|
||||||
|
locationManager.delegate = self
|
||||||
|
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(handlePayCallback(_:)), name: WebViewPayCallbackNotification, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewSafeAreaInsetsDidChange() {
|
override func viewSafeAreaInsetsDidChange() {
|
||||||
@@ -102,7 +126,7 @@ class WebViewController : ZDViewController {
|
|||||||
let nav = navigationController as? DDNavigationController
|
let nav = navigationController as? DDNavigationController
|
||||||
nav?.screenEdgePanGestureRecognizerEnable(false)
|
nav?.screenEdgePanGestureRecognizerEnable(false)
|
||||||
}
|
}
|
||||||
webView.configuration.userContentController.add(self, name: "nativeObject")
|
addScriptMessageHandlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
@@ -114,7 +138,7 @@ class WebViewController : ZDViewController {
|
|||||||
let nav = navigationController as? DDNavigationController
|
let nav = navigationController as? DDNavigationController
|
||||||
nav?.screenEdgePanGestureRecognizerEnable(true)
|
nav?.screenEdgePanGestureRecognizerEnable(true)
|
||||||
}
|
}
|
||||||
webView.configuration.userContentController.removeScriptMessageHandler(forName: "nativeObject")
|
removeScriptMessageHandlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func dd_backActionPop(_ isAnimated: Bool) {
|
override func dd_backActionPop(_ isAnimated: Bool) {
|
||||||
@@ -124,12 +148,80 @@ class WebViewController : ZDViewController {
|
|||||||
super.dd_backActionPop(isAnimated)
|
super.dd_backActionPop(isAnimated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addScriptMessageHandlers() {
|
||||||
|
let handlers = ["nativeObject", "scan", "getLocation", "openMapNavigation", "callPhone", "openDocument"]
|
||||||
|
handlers.forEach { name in
|
||||||
|
webView.configuration.userContentController.add(self, name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeScriptMessageHandlers() {
|
||||||
|
let handlers = ["nativeObject", "scan", "getLocation", "openMapNavigation", "callPhone", "openDocument"]
|
||||||
|
handlers.forEach { name in
|
||||||
|
webView.configuration.userContentController.removeScriptMessageHandler(forName: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handlePayCallback(_ notification: Notification) {
|
||||||
|
guard let orderNo = notification.userInfo?["orderNo"] as? String else { return }
|
||||||
|
let js = """
|
||||||
|
(function() {
|
||||||
|
if (window.onPayCallback) {
|
||||||
|
window.onPayCallback({ orderNo: '\(orderNo)' });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})()
|
||||||
|
"""
|
||||||
|
webView.evaluateJavaScript(js) { [weak self] result, _ in
|
||||||
|
if let handled = result as? Bool, handled == true {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 兜底:刷新当前页面
|
||||||
|
self?.webView.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func evaluateCallback(_ callback: String?, with result: [String: Any]) {
|
||||||
|
guard let callback = callback, callback.isEmpty == false else { return }
|
||||||
|
guard let data = try? JSONSerialization.data(withJSONObject: result, options: []),
|
||||||
|
let jsonString = String(data: data, encoding: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let js = "window['\(callback)'](\(jsonString));"
|
||||||
|
webView.evaluateJavaScript(js, completionHandler: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WebViewController : WKScriptMessageHandler {
|
extension WebViewController : WKScriptMessageHandler {
|
||||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
let dict = message.body as? [String:Any]
|
let dict = message.body as? [String:Any]
|
||||||
let action = dict?["action"] as? String
|
let name = message.name
|
||||||
|
|
||||||
|
if name == "nativeObject" {
|
||||||
|
handleNativeObject(dict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "scan":
|
||||||
|
handleScan(dict)
|
||||||
|
case "getLocation":
|
||||||
|
handleGetLocation(dict)
|
||||||
|
case "openMapNavigation":
|
||||||
|
handleOpenMapNavigation(dict)
|
||||||
|
case "callPhone":
|
||||||
|
handleCallPhone(dict)
|
||||||
|
case "openDocument":
|
||||||
|
handleOpenDocument(dict)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleNativeObject(_ dict: [String: Any]?) {
|
||||||
|
let action = dict?["action"] as? String
|
||||||
if action == "goBack" {
|
if action == "goBack" {
|
||||||
navigationController?.popViewController(animated: true)
|
navigationController?.popViewController(animated: true)
|
||||||
}else if action == "goTraining" {
|
}else if action == "goTraining" {
|
||||||
@@ -189,6 +281,124 @@ extension WebViewController : WKScriptMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - scan
|
||||||
|
private func handleScan(_ dict: [String: Any]?) {
|
||||||
|
let callback = dict?["callback"] as? String
|
||||||
|
let vc = ScanViewController { [weak self] code in
|
||||||
|
if let code = code {
|
||||||
|
self?.evaluateCallback(callback, with: ["success": true, "code": code])
|
||||||
|
} else {
|
||||||
|
self?.evaluateCallback(callback, with: ["success": false])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - getLocation
|
||||||
|
private func handleGetLocation(_ dict: [String: Any]?) {
|
||||||
|
locationCallback = dict?["callback"] as? String
|
||||||
|
let status = CLLocationManager.authorizationStatus()
|
||||||
|
switch status {
|
||||||
|
case .authorizedWhenInUse, .authorizedAlways:
|
||||||
|
locationManager.requestLocation()
|
||||||
|
case .notDetermined:
|
||||||
|
locationManager.requestWhenInUseAuthorization()
|
||||||
|
default:
|
||||||
|
evaluateCallback(locationCallback, with: ["success": false, "error": "location_denied"])
|
||||||
|
locationCallback = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - openMapNavigation
|
||||||
|
private func handleOpenMapNavigation(_ dict: [String: Any]?) {
|
||||||
|
let name = dict?["name"] as? String ?? ""
|
||||||
|
let address = dict?["address"] as? String ?? ""
|
||||||
|
let latitude = dict?["latitude"] as? Double ?? dict?["lat"] as? Double
|
||||||
|
let longitude = dict?["longitude"] as? Double ?? dict?["lng"] as? Double
|
||||||
|
|
||||||
|
guard let latitude = latitude, let longitude = longitude else {
|
||||||
|
view.dd_makeToast("缺少导航坐标")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先高德地图
|
||||||
|
let amapUrlString = "iosamap://route?dlat=\(latitude)&dlon=\(longitude)&dname=\(name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")&dev=0&t=0"
|
||||||
|
if let amapUrl = URL(string: amapUrlString), UIApplication.shared.canOpenURL(amapUrl) {
|
||||||
|
UIApplication.shared.open(amapUrl, options: [:], completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退 Apple Maps
|
||||||
|
let appleUrlString = "http://maps.apple.com/?daddr=\(latitude),\(longitude)&q=\(name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")"
|
||||||
|
if let appleUrl = URL(string: appleUrlString) {
|
||||||
|
UIApplication.shared.open(appleUrl, options: [:], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - callPhone
|
||||||
|
private func handleCallPhone(_ dict: [String: Any]?) {
|
||||||
|
let callback = dict?["callback"] as? String
|
||||||
|
guard let phone = dict?["phone"] as? String, phone.isEmpty == false else {
|
||||||
|
evaluateCallback(callback, with: ["success": false, "error": "phone_empty"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let tel = phone.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "-", with: "")
|
||||||
|
guard let url = URL(string: "tel://\(tel)") else {
|
||||||
|
evaluateCallback(callback, with: ["success": false, "error": "phone_invalid"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
UIApplication.shared.open(url, options: [:]) { [weak self] success in
|
||||||
|
self?.evaluateCallback(callback, with: ["success": success])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - openDocument
|
||||||
|
private func handleOpenDocument(_ dict: [String: Any]?) {
|
||||||
|
guard let urlString = dict?["url"] as? String, let url = URL(string: urlString) else {
|
||||||
|
view.dd_makeToast("文档地址无效")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let vc = SFSafariViewController(url: url)
|
||||||
|
vc.modalPresentationStyle = .fullScreen
|
||||||
|
present(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WebViewController : CLLocationManagerDelegate {
|
||||||
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
|
guard let location = locations.last else { return }
|
||||||
|
let result: [String: Any] = [
|
||||||
|
"success": true,
|
||||||
|
"latitude": location.coordinate.latitude,
|
||||||
|
"lat": location.coordinate.latitude,
|
||||||
|
"y": location.coordinate.latitude,
|
||||||
|
"longitude": location.coordinate.longitude,
|
||||||
|
"lng": location.coordinate.longitude,
|
||||||
|
"x": location.coordinate.longitude,
|
||||||
|
"lon": location.coordinate.longitude,
|
||||||
|
"coordType": "wgs84",
|
||||||
|
"accuracy": location.horizontalAccuracy
|
||||||
|
]
|
||||||
|
evaluateCallback(locationCallback, with: result)
|
||||||
|
locationCallback = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
evaluateCallback(locationCallback, with: ["success": false, "error": error.localizedDescription])
|
||||||
|
locationCallback = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
||||||
|
if status == .authorizedWhenInUse || status == .authorizedAlways {
|
||||||
|
if locationCallback != nil {
|
||||||
|
locationManager.requestLocation()
|
||||||
|
}
|
||||||
|
} else if status != .notDetermined, locationCallback != nil {
|
||||||
|
evaluateCallback(locationCallback, with: ["success": false, "error": "location_denied"])
|
||||||
|
locationCallback = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func unwrapEscapedJSONString(_ raw: String) -> String {
|
private func unwrapEscapedJSONString(_ raw: String) -> String {
|
||||||
@@ -245,11 +455,37 @@ extension WebViewController : WKNavigationDelegate,WKUIDelegate {
|
|||||||
DDLog(message: "didFailProvisionalNavigation--------------------------------\(String(describing: webView.url?.absoluteString))")
|
DDLog(message: "didFailProvisionalNavigation--------------------------------\(String(describing: webView.url?.absoluteString))")
|
||||||
}
|
}
|
||||||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
|
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
|
||||||
let urlString = navigationAction.request.url?.absoluteString
|
guard let url = navigationAction.request.url else {
|
||||||
let prefix = "tel:"
|
decisionHandler(.allow)
|
||||||
if let urlString, urlString.hasPrefix(prefix) == true, let tel = urlString.components(separatedBy: prefix).last {
|
return
|
||||||
UIApplication.shared.dd_openUrl(type: .phone, appending: tel)
|
|
||||||
}
|
}
|
||||||
|
let urlString = url.absoluteString
|
||||||
|
let scheme = url.scheme?.lowercased() ?? ""
|
||||||
|
|
||||||
|
// tel: 拨号
|
||||||
|
if scheme == "tel", let tel = urlString.components(separatedBy: "tel:").last {
|
||||||
|
UIApplication.shared.dd_openUrl(type: .phone, appending: tel)
|
||||||
|
decisionHandler(.cancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信 / 支付宝 / 高德等外部 App Scheme,交给系统打开
|
||||||
|
let externalSchemes = ["weixin", "wechat", "alipay", "alipays", "iosamap", "baidumap", "qqmap"]
|
||||||
|
if externalSchemes.contains(scheme) {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
decisionHandler(.cancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许微信支付 H5 页面直接跳转
|
||||||
|
if scheme == "https", let host = url.host {
|
||||||
|
let wxHosts = ["wx.tenpay.com", "payapp.wechatpay.com", "mclient.alipay.com", "mobile.alipay.com"]
|
||||||
|
if wxHosts.contains(host) {
|
||||||
|
decisionHandler(.allow)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
decisionHandler(.allow)
|
decisionHandler(.allow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>扫码充电需要访问相机</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>获取附近充电站需要您的位置信息</string>
|
||||||
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
|
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>FORCE_FULL_ACCURACY</key>
|
<key>FORCE_FULL_ACCURACY</key>
|
||||||
@@ -39,6 +43,17 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.zhongdao.orderscheduling.paycallback</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>myappscheme</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
return .allButUpsideDown
|
return .allButUpsideDown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||||
|
handlePayCallbackURL(url)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
JPUSHService.resetBadge()
|
JPUSHService.resetBadge()
|
||||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||||
@@ -166,3 +171,15 @@ extension AppDelegate : JPUSHRegisterDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlePayCallbackURL(_ url: URL) {
|
||||||
|
guard url.scheme?.lowercased() == "myappscheme" else { return }
|
||||||
|
guard url.host?.lowercased() == "paycallback" else { return }
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||||
|
let orderNo = components?.queryItems?.first(where: { $0.name == "orderNo" })?.value
|
||||||
|
var userInfo: [String: Any] = [:]
|
||||||
|
if let orderNo = orderNo {
|
||||||
|
userInfo["orderNo"] = orderNo
|
||||||
|
}
|
||||||
|
NotificationCenter.default.post(name: WebViewPayCallbackNotification, object: nil, userInfo: userInfo)
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
||||||
|
guard let url = URLContexts.first?.url else { return }
|
||||||
|
handlePayCallbackURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||||
// Called as the scene transitions from the foreground to the background.
|
// Called as the scene transitions from the foreground to the background.
|
||||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
|
|||||||
Reference in New Issue
Block a user